diff --git a/src/components/pages/gallery/PreviewCard.tsx b/src/components/pages/gallery/PreviewCard.tsx index 3eb8edfe3..1a4810528 100644 --- a/src/components/pages/gallery/PreviewCard.tsx +++ b/src/components/pages/gallery/PreviewCard.tsx @@ -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); diff --git a/src/pages/credentials/index.tsx b/src/pages/credentials/index.tsx index 2171b8b81..f4ff2234c 100644 --- a/src/pages/credentials/index.tsx +++ b/src/pages/credentials/index.tsx @@ -67,6 +67,7 @@ export default function Credentials() { keyAttributes.keyDecryptionNonce, kek ); + if (isFirstLogin()) { await generateAndSaveIntermediateKeyAttributes( passphrase, diff --git a/src/pages/shared-albums/index.tsx b/src/pages/shared-albums/index.tsx index c0386d633..9b23fe860 100644 --- a/src/pages/shared-albums/index.tsx +++ b/src/pages/shared-albums/index.tsx @@ -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 = () => ( @@ -39,6 +45,7 @@ const Loader = () => ( const bs58 = require('bs58'); export default function PublicCollectionGallery() { const token = useRef(null); + const passwordToken = useRef(null); const collectionKey = useRef(null); const url = useRef(null); const [publicFiles, setPublicFiles] = useState(null); @@ -54,6 +61,8 @@ export default function PublicCollectionGallery() { const loadingBar = useRef(null); const isLoadingBarRunning = useRef(false); const router = useRouter(); + const [isPasswordProtected, setIsPasswordProtected] = + useState(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 ; } @@ -169,6 +235,30 @@ export default function PublicCollectionGallery() { if (errorMessage && !loading) { return {errorMessage}; } + if (isPasswordProtected && !loading) { + return ( + + + + + + {constants.PASSWORD} + + + {/* */} + {constants.LINK_PASSWORD} + + + + + + ); + } if (!publicFiles && !loading) { return {constants.NOT_FOUND}; @@ -179,6 +269,7 @@ export default function PublicCollectionGallery() { value={{ ...defaultPublicCollectionGalleryContext, token: token.current, + passwordToken: passwordToken.current, accessedThroughSharedURL: true, setDialogMessage, openReportForm, diff --git a/src/services/publicCollectionDownloadManager.ts b/src/services/publicCollectionDownloadManager.ts index f9ab95846..0326c9e7c 100644 --- a/src/services/publicCollectionDownloadManager.ts +++ b/src/services/publicCollectionDownloadManager.ts @@ -19,7 +19,11 @@ class PublicCollectionDownloadManager { private fileObjectURLPromise = new Map>(); private thumbnailObjectURLPromise = new Map>(); - 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') { diff --git a/src/services/publicCollectionService.ts b/src/services/publicCollectionService.ts index 84c95627f..084897f8c 100644 --- a/src/services/publicCollectionService.ts +++ b/src/services/publicCollectionService.ts @@ -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 => { + return ( + (await localForage.getItem( + getPublicCollectionPasswordKey(collectionKey) + )) || '' + ); +}; + +export const setPublicCollectionPassword = async ( + collectionKey: string, + passToken: string +): Promise => { + return await localForage.setItem( + getPublicCollectionPasswordKey(collectionKey), + passToken + ); +}; + export const getLocalPublicCollection = async (collectionKey: string) => { const localCollections = (await localForage.getItem(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(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 = diff --git a/src/types/collection/index.ts b/src/types/collection/index.ts index fe0425229..e75df8dec 100644 --- a/src/types/collection/index.ts +++ b/src/types/collection/index.ts @@ -27,6 +27,8 @@ export interface PublicURL { enableDownload: boolean; passwordEnabled: boolean; nonce: string; + opsLimit: number; + memLimit: number; } export interface CreatePublicAccessTokenRequest { diff --git a/src/types/publicCollection/index.ts b/src/types/publicCollection/index.ts index 11a41ad75..072a9210a 100644 --- a/src/types/publicCollection/index.ts +++ b/src/types/publicCollection/index.ts @@ -4,6 +4,7 @@ import { EnteFile } from 'types/file'; export interface PublicCollectionGalleryContextType { token: string; + passwordToken: string; accessedThroughSharedURL: boolean; setDialogMessage: SetDialogMessage; openReportForm: () => void; diff --git a/src/utils/publicCollectionGallery/index.ts b/src/utils/publicCollectionGallery/index.ts index 16206d435..cdcb7b5fa 100644 --- a/src/utils/publicCollectionGallery/index.ts +++ b/src/utils/publicCollectionGallery/index.ts @@ -4,6 +4,7 @@ import { PublicCollectionGalleryContextType } from 'types/publicCollection'; export const defaultPublicCollectionGalleryContext: PublicCollectionGalleryContextType = { token: null, + passwordToken: null, accessedThroughSharedURL: false, setDialogMessage: () => null, openReportForm: () => null, diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index c4b7ba1e0..85bd69fc1 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -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',