add support for showing password protected albums

This commit is contained in:
Neeraj Gupta 2022-02-23 14:31:00 +05:30
parent 7326b3ff19
commit bb5b37997e
No known key found for this signature in database
GPG key ID: 3C5A1684DC1729E1
9 changed files with 176 additions and 15 deletions

View file

@ -194,7 +194,8 @@ export default function PreviewCard(props: IProps) {
url =
await PublicCollectionDownloadManager.getThumbnail(
file,
publicCollectionGalleryContext.token
publicCollectionGalleryContext.token,
publicCollectionGalleryContext.passwordToken
);
} else {
url = await DownloadManager.getThumbnail(file);

View file

@ -67,6 +67,7 @@ export default function Credentials() {
keyAttributes.keyDecryptionNonce,
kek
);
if (isFirstLogin()) {
await generateAndSaveIntermediateKeyAttributes(
passphrase,

View file

@ -5,8 +5,10 @@ import {
getLocalPublicCollection,
getLocalPublicFiles,
getPublicCollection,
getPublicCollectionPassword,
getPublicCollectionUID,
removePublicCollectionWithFiles,
setPublicCollectionPassword,
syncPublicFiles,
} from 'services/publicCollectionService';
import { Collection } from 'types/collection';
@ -28,6 +30,10 @@ import LoadingBar from 'react-top-loading-bar';
import CryptoWorker from 'utils/crypto';
import { PAGES } from 'constants/pages';
import { useRouter } from 'next/router';
import LogoImg from 'components/LogoImg';
import SingleInputForm from 'components/SingleInputForm';
import { Card } from 'react-bootstrap';
import { logError } from 'utils/sentry';
const Loader = () => (
<Container>
@ -39,6 +45,7 @@ const Loader = () => (
const bs58 = require('bs58');
export default function PublicCollectionGallery() {
const token = useRef<string>(null);
const passwordToken = useRef<string>(null);
const collectionKey = useRef<string>(null);
const url = useRef<string>(null);
const [publicFiles, setPublicFiles] = useState<EnteFile[]>(null);
@ -54,6 +61,8 @@ export default function PublicCollectionGallery() {
const loadingBar = useRef(null);
const isLoadingBarRunning = useRef(false);
const router = useRouter();
const [isPasswordProtected, setIsPasswordProtected] =
useState<boolean>(false);
const openMessageDialog = () => setMessageDialogView(true);
const closeMessageDialog = () => setMessageDialogView(false);
@ -115,6 +124,9 @@ export default function PublicCollectionGallery() {
mergeMetadata(localFiles)
);
setPublicFiles(localPublicFiles);
passwordToken.current = await getPublicCollectionPassword(
collectionUID
);
}
await syncWithRemote();
} finally {
@ -133,10 +145,23 @@ export default function PublicCollectionGallery() {
token.current,
collectionKey.current
);
const collectionUID = getPublicCollectionUID(token.current);
setPublicCollection(collection);
await syncPublicFiles(token.current, collection, setPublicFiles);
setErrorMessage(null);
// check if we need to prompt user for the password
if (
(collection?.publicURLs?.[0]?.passwordEnabled ?? false) &&
(await getPublicCollectionPassword(collectionUID)) === ''
) {
setIsPasswordProtected(true);
return;
} else {
await syncPublicFiles(
token.current,
collection,
setPublicFiles
);
}
} catch (e) {
const parsedError = parseSharingErrorCodes(e);
if (
@ -162,6 +187,47 @@ export default function PublicCollectionGallery() {
}
};
const verifyPassphrase = async (password, setFieldError) => {
try {
const cryptoWorker = await new CryptoWorker();
let hashedPassword: string = null;
try {
const publicUrl = publicCollection.publicURLs[0];
hashedPassword = await cryptoWorker.deriveKey(
password,
publicUrl.nonce,
publicUrl.opsLimit,
publicUrl.memLimit
);
} catch (e) {
setFieldError(
'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`
);
return;
}
const collectionUID = getPublicCollectionUID(token.current);
try {
setPublicCollectionPassword(collectionUID, hashedPassword);
await syncWithRemote();
passwordToken.current = hashedPassword;
setIsPasswordProtected(false);
finishLoadingBar();
} catch (e) {
// reset local password token
passwordToken.current = null;
setPublicCollectionPassword(collectionUID, '');
logError(e, 'user entered a wrong password for album');
setFieldError('passphrase', constants.INCORRECT_PASSPHRASE);
}
} catch (e) {
setFieldError(
'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`
);
}
};
if (!publicFiles && loading) {
return <Loader />;
}
@ -169,6 +235,30 @@ export default function PublicCollectionGallery() {
if (errorMessage && !loading) {
return <Container>{errorMessage}</Container>;
}
if (isPasswordProtected && !loading) {
return (
<Container>
<Card style={{ minWidth: '320px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px' }}>
<Card.Title style={{ marginBottom: '24px' }}>
<LogoImg src="/icon.svg" />
{constants.PASSWORD}
</Card.Title>
<Card.Subtitle style={{ marginBottom: '32px' }}>
{/* <LogoImg src="/icon.svg" /> */}
{constants.LINK_PASSWORD}
</Card.Subtitle>
<SingleInputForm
callback={verifyPassphrase}
placeholder={constants.RETURN_PASSPHRASE_HINT}
buttonText={'unlock'}
fieldType="password"
/>
</Card.Body>
</Card>
</Container>
);
}
if (!publicFiles && !loading) {
return <Container>{constants.NOT_FOUND}</Container>;
@ -179,6 +269,7 @@ export default function PublicCollectionGallery() {
value={{
...defaultPublicCollectionGalleryContext,
token: token.current,
passwordToken: passwordToken.current,
accessedThroughSharedURL: true,
setDialogMessage,
openReportForm,

View file

@ -19,7 +19,11 @@ class PublicCollectionDownloadManager {
private fileObjectURLPromise = new Map<string, Promise<string>>();
private thumbnailObjectURLPromise = new Map<number, Promise<string>>();
public async getThumbnail(file: EnteFile, token: string) {
public async getThumbnail(
file: EnteFile,
token: string,
passwordToken: string
) {
try {
if (!token) {
return null;
@ -42,7 +46,11 @@ class PublicCollectionDownloadManager {
if (cacheResp) {
return URL.createObjectURL(await cacheResp.blob());
}
const thumb = await this.downloadThumb(token, file);
const thumb = await this.downloadThumb(
token,
passwordToken,
file
);
const thumbBlob = new Blob([thumb]);
try {
await thumbnailCache?.put(
@ -65,11 +73,18 @@ class PublicCollectionDownloadManager {
}
}
private downloadThumb = async (token: string, file: EnteFile) => {
private downloadThumb = async (
token: string,
passwordToken: string,
file: EnteFile
) => {
const resp = await HTTPService.get(
getPublicCollectionThumbnailURL(file.id),
null,
{ 'X-Auth-Access-Token': token },
{
'X-Auth-Access-Token': token,
'X-Auth-Access-Token-JWT': passwordToken,
},
{ responseType: 'arraybuffer' }
);
if (typeof resp.data === 'undefined') {
@ -84,14 +99,23 @@ class PublicCollectionDownloadManager {
return decrypted;
};
getFile = async (file: EnteFile, token: string, forPreview = false) => {
getFile = async (
file: EnteFile,
token: string,
passwordToken: string,
forPreview = false
) => {
const shouldBeConverted = forPreview && needsConversionForPreview(file);
const fileKey = shouldBeConverted
? `${file.id}_converted`
: `${file.id}`;
try {
const getFilePromise = async (convert: boolean) => {
const fileStream = await this.downloadFile(token, file);
const fileStream = await this.downloadFile(
token,
passwordToken,
file
);
let fileBlob = await new Response(fileStream).blob();
if (convert) {
fileBlob = await convertForPreview(file, fileBlob);
@ -117,7 +141,7 @@ class PublicCollectionDownloadManager {
return await this.fileObjectURLPromise.get(file.id.toString());
}
async downloadFile(token: string, file: EnteFile) {
async downloadFile(token: string, passwordToken: string, file: EnteFile) {
const worker = await new CryptoWorker();
if (!token) {
return null;
@ -129,7 +153,10 @@ class PublicCollectionDownloadManager {
const resp = await HTTPService.get(
getPublicCollectionFileURL(file.id),
null,
{ 'X-Auth-Access-Token': token },
{
'X-Auth-Access-Token': token,
'X-Auth-Access-Token-JWT': passwordToken,
},
{ responseType: 'arraybuffer' }
);
if (typeof resp.data === 'undefined') {

View file

@ -12,6 +12,7 @@ import {
} from 'types/publicCollection';
import CryptoWorker from 'utils/crypto';
import { REPORT_REASON } from 'constants/publicCollection';
import { CustomError, parseSharingErrorCodes } from 'utils/error';
const ENDPOINT = getEndpoint();
const PUBLIC_COLLECTION_FILES_TABLE = 'public-collection-files';
@ -22,6 +23,9 @@ export const getPublicCollectionUID = (token: string) => `${token}`;
const getPublicCollectionSyncTimeUID = (collectionUID: string) =>
`public-${collectionUID}-time`;
const getPublicCollectionPasswordKey = (collectionUID: string) =>
`public-${collectionUID}-passkey`;
export const getLocalPublicFiles = async (collectionUID: string) => {
const localSavedPublicCollectionFiles =
(
@ -55,6 +59,26 @@ export const savePublicCollectionFiles = async (
);
};
export const getPublicCollectionPassword = async (
collectionKey: string
): Promise<string> => {
return (
(await localForage.getItem<string>(
getPublicCollectionPasswordKey(collectionKey)
)) || ''
);
};
export const setPublicCollectionPassword = async (
collectionKey: string,
passToken: string
): Promise<string> => {
return await localForage.setItem<string>(
getPublicCollectionPasswordKey(collectionKey),
passToken
);
};
export const getLocalPublicCollection = async (collectionKey: string) => {
const localCollections =
(await localForage.getItem<Collection[]>(PUBLIC_COLLECTIONS_TABLE)) ||
@ -134,11 +158,15 @@ export const syncPublicFiles = async (
const lastSyncTime = await getPublicCollectionLastSyncTime(
collectionUID
);
if (collection.updationTime === lastSyncTime) {
return files;
}
const passwordToken = await getPublicCollectionPassword(
collectionUID
);
// if (collection.updationTime === lastSyncTime) {
// return files;
// }
const fetchedFiles = await getPublicFiles(
token,
passwordToken,
collection,
lastSyncTime,
files,
@ -171,7 +199,12 @@ export const syncPublicFiles = async (
);
setPublicFiles([...sortFiles(mergeMetadata(files))]);
} catch (e) {
const parsedError = parseSharingErrorCodes(e);
logError(e, 'failed to sync shared collection files');
if (parsedError.message === CustomError.TOKEN_EXPIRED) {
console.log('invalid token or password');
throw e;
}
}
return [...sortFiles(mergeMetadata(files))];
} catch (e) {
@ -182,6 +215,7 @@ export const syncPublicFiles = async (
const getPublicFiles = async (
token: string,
passwordToken: string,
collection: Collection,
sinceTime: number,
files: EnteFile[],
@ -203,6 +237,7 @@ const getPublicFiles = async (
{
'Cache-Control': 'no-cache',
'X-Auth-Access-Token': token,
'X-Auth-Access-Token-JWT': passwordToken,
}
);
decryptedFiles.push(
@ -311,6 +346,7 @@ export const removePublicCollectionWithFiles = async (
collectionKey: string
) => {
const collectionUID = getPublicCollectionUID(token);
console.log('remove information about public collection ' + collectionUID);
const publicCollections =
(await localForage.getItem<Collection[]>(PUBLIC_COLLECTIONS_TABLE)) ||
[];
@ -320,7 +356,7 @@ export const removePublicCollectionWithFiles = async (
(collection) => collection.key !== collectionKey
)
);
await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID));
await localForage.removeItem(getPublicCollectionSyncTimeUID(collectionUID));
const publicCollectionFiles =

View file

@ -27,6 +27,8 @@ export interface PublicURL {
enableDownload: boolean;
passwordEnabled: boolean;
nonce: string;
opsLimit: number;
memLimit: number;
}
export interface CreatePublicAccessTokenRequest {

View file

@ -4,6 +4,7 @@ import { EnteFile } from 'types/file';
export interface PublicCollectionGalleryContextType {
token: string;
passwordToken: string;
accessedThroughSharedURL: boolean;
setDialogMessage: SetDialogMessage;
openReportForm: () => void;

View file

@ -4,6 +4,7 @@ import { PublicCollectionGalleryContextType } from 'types/publicCollection';
export const defaultPublicCollectionGalleryContext: PublicCollectionGalleryContextType =
{
token: null,
passwordToken: null,
accessedThroughSharedURL: false,
setDialogMessage: () => null,
openReportForm: () => null,

View file

@ -74,6 +74,7 @@ const englishConstants = {
SENDING: 'sending...',
SENT: 'sent!',
PASSWORD: 'password',
LINK_PASSWORD: 'enter password to unlock the album',
ENTER_PASSPHRASE: 'enter your password',
RETURN_PASSPHRASE_HINT: 'password',
SET_PASSPHRASE: 'set password',