diff --git a/web/apps/accounts/src/services/passkey.ts b/web/apps/accounts/src/services/passkey.ts index 2dfecaa3b8..f470b1d1cc 100644 --- a/web/apps/accounts/src/services/passkey.ts +++ b/web/apps/accounts/src/services/passkey.ts @@ -169,7 +169,7 @@ const beginPasskeyRegistration = async (token: string) => { // binary data. // // Binary data in the returned `PublicKeyCredentialCreationOptions` are - // serialized as a "URLEncodedBase64", which is a URL-encoded Base64 string + // serialized as a "URLEncodedBase64", which is a URL-encoded base64 string // without any padding. The library is following the WebAuthn recommendation // when it does this: // diff --git a/web/apps/auth/src/services/remote.ts b/web/apps/auth/src/services/remote.ts index f9061110d9..758bd31b66 100644 --- a/web/apps/auth/src/services/remote.ts +++ b/web/apps/auth/src/services/remote.ts @@ -1,5 +1,6 @@ import log from "@/base/log"; import { apiURL } from "@/base/origins"; +import { ensureString } from "@/utils/ensure"; import ComlinkCryptoWorker from "@ente/shared/crypto"; import { ApiError, CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; @@ -34,7 +35,10 @@ export const getAuthCodes = async (): Promise => { entity.header, authenticatorKey, ); - return codeFromURIString(entity.id, decryptedCode); + return codeFromURIString( + entity.id, + ensureString(decryptedCode), + ); } catch (e) { log.error(`Failed to parse codeID ${entity.id}`, e); return undefined; diff --git a/web/apps/cast/src/services/render.ts b/web/apps/cast/src/services/render.ts index 8f832b320d..d512b039ef 100644 --- a/web/apps/cast/src/services/render.ts +++ b/web/apps/cast/src/services/render.ts @@ -212,6 +212,7 @@ const decryptEnteFile = async ( if (magicMetadata?.data) { fileMagicMetadata = { ...encryptedFile.magicMetadata, + // @ts-expect-error TODO: Need to use zod here. data: await worker.decryptMetadata( magicMetadata.data, magicMetadata.header, @@ -222,6 +223,7 @@ const decryptEnteFile = async ( if (pubMagicMetadata?.data) { filePubMagicMetadata = { ...pubMagicMetadata, + // @ts-expect-error TODO: Need to use zod here. data: await worker.decryptMetadata( pubMagicMetadata.data, pubMagicMetadata.header, @@ -237,9 +239,11 @@ const decryptEnteFile = async ( pubMagicMetadata: filePubMagicMetadata, }; if (file.pubMagicMetadata?.data.editedTime) { + // @ts-expect-error TODO: Need to use zod here. file.metadata.creationTime = file.pubMagicMetadata.data.editedTime; } if (file.pubMagicMetadata?.data.editedName) { + // @ts-expect-error TODO: Need to use zod here. file.metadata.title = file.pubMagicMetadata.data.editedName; } // @ts-expect-error TODO: The core types need to be updated to allow the diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index dae58a96ae..3575cb9654 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -426,7 +426,7 @@ const createCollection = async ( let encryptedMagicMetadata: EncryptedMagicMetadata; if (magicMetadataProps) { const magicMetadata = await updateMagicMetadata(magicMetadataProps); - const { file: encryptedMagicMetadataProps } = + const encryptedMagicMetadataProps = await cryptoWorker.encryptMetadata( magicMetadataProps, collectionKey, @@ -434,8 +434,8 @@ const createCollection = async ( encryptedMagicMetadata = { ...magicMetadata, - data: encryptedMagicMetadataProps.encryptedData, - header: encryptedMagicMetadataProps.decryptionHeader, + data: encryptedMagicMetadataProps.encryptedDataB64, + header: encryptedMagicMetadataProps.decryptionHeaderB64, }; } const newCollection: EncryptedCollection = { @@ -799,18 +799,19 @@ export const updateCollectionMagicMetadata = async ( const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - const { file: encryptedMagicMetadata } = await cryptoWorker.encryptMetadata( - updatedMagicMetadata.data, - collection.key, - ); + const { encryptedDataB64, decryptionHeaderB64 } = + await cryptoWorker.encryptMetadata( + updatedMagicMetadata.data, + collection.key, + ); const reqBody: UpdateMagicMetadataRequest = { id: collection.id, magicMetadata: { version: updatedMagicMetadata.version, count: updatedMagicMetadata.count, - data: encryptedMagicMetadata.encryptedData, - header: encryptedMagicMetadata.decryptionHeader, + data: encryptedDataB64, + header: decryptionHeaderB64, }, }; @@ -843,18 +844,19 @@ export const updateSharedCollectionMagicMetadata = async ( const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - const { file: encryptedMagicMetadata } = await cryptoWorker.encryptMetadata( - updatedMagicMetadata.data, - collection.key, - ); + const { encryptedDataB64, decryptionHeaderB64 } = + await cryptoWorker.encryptMetadata( + updatedMagicMetadata.data, + collection.key, + ); const reqBody: UpdateMagicMetadataRequest = { id: collection.id, magicMetadata: { version: updatedMagicMetadata.version, count: updatedMagicMetadata.count, - data: encryptedMagicMetadata.encryptedData, - header: encryptedMagicMetadata.decryptionHeader, + data: encryptedDataB64, + header: decryptionHeaderB64, }, }; @@ -887,18 +889,19 @@ export const updatePublicCollectionMagicMetadata = async ( const cryptoWorker = await ComlinkCryptoWorker.getInstance(); - const { file: encryptedMagicMetadata } = await cryptoWorker.encryptMetadata( - updatedPublicMagicMetadata.data, - collection.key, - ); + const { encryptedDataB64, decryptionHeaderB64 } = + await cryptoWorker.encryptMetadata( + updatedPublicMagicMetadata.data, + collection.key, + ); const reqBody: UpdateMagicMetadataRequest = { id: collection.id, magicMetadata: { version: updatedPublicMagicMetadata.version, count: updatedPublicMagicMetadata.count, - data: encryptedMagicMetadata.encryptedData, - header: encryptedMagicMetadata.decryptionHeader, + data: encryptedDataB64, + header: decryptionHeaderB64, }, }; diff --git a/web/apps/photos/src/services/entityService.ts b/web/apps/photos/src/services/entityService.ts index 1e418aa2c6..401819b110 100644 --- a/web/apps/photos/src/services/entityService.ts +++ b/web/apps/photos/src/services/entityService.ts @@ -143,6 +143,7 @@ const syncEntity = async (type: EntityType): Promise> => { } const entityKey = await getEntityKey(type); + // @ts-expect-error TODO: Need to use zod here. const newDecryptedEntities: Array> = await Promise.all( response.diff.map(async (entity: EncryptedEntity) => { if (entity.isDeleted) { diff --git a/web/apps/photos/src/services/fileService.ts b/web/apps/photos/src/services/fileService.ts index 37cbb42798..e7df1a6d99 100644 --- a/web/apps/photos/src/services/fileService.ts +++ b/web/apps/photos/src/services/fileService.ts @@ -191,7 +191,7 @@ export const updateFileMagicMetadata = async ( file, updatedMagicMetadata, } of fileWithUpdatedMagicMetadataList) { - const { file: encryptedMagicMetadata } = + const { encryptedDataB64, decryptionHeaderB64 } = await cryptoWorker.encryptMetadata( updatedMagicMetadata.data, file.key, @@ -201,8 +201,8 @@ export const updateFileMagicMetadata = async ( magicMetadata: { version: updatedMagicMetadata.version, count: updatedMagicMetadata.count, - data: encryptedMagicMetadata.encryptedData, - header: encryptedMagicMetadata.decryptionHeader, + data: encryptedDataB64, + header: decryptionHeaderB64, }, }); } @@ -238,7 +238,7 @@ export const updateFilePublicMagicMetadata = async ( file, updatedPublicMagicMetadata: updatePublicMagicMetadata, } of fileWithUpdatedPublicMagicMetadataList) { - const { file: encryptedPubMagicMetadata } = + const { encryptedDataB64, decryptionHeaderB64 } = await cryptoWorker.encryptMetadata( updatePublicMagicMetadata.data, file.key, @@ -248,8 +248,8 @@ export const updateFilePublicMagicMetadata = async ( magicMetadata: { version: updatePublicMagicMetadata.version, count: updatePublicMagicMetadata.count, - data: encryptedPubMagicMetadata.encryptedData, - header: encryptedPubMagicMetadata.decryptionHeader, + data: encryptedDataB64, + header: decryptionHeaderB64, }, }); } diff --git a/web/apps/photos/src/services/upload/uploadService.ts b/web/apps/photos/src/services/upload/uploadService.ts index d28d211cd4..8511136352 100644 --- a/web/apps/photos/src/services/upload/uploadService.ts +++ b/web/apps/photos/src/services/upload/uploadService.ts @@ -245,6 +245,11 @@ interface LocalFileAttributes< decryptionHeader: string; } +interface EncryptedMetadata { + encryptedDataB64: string; + decryptionHeaderB64: string; +} + interface EncryptionResult< T extends string | Uint8Array | EncryptedFileStream, > { @@ -255,7 +260,7 @@ interface EncryptionResult< interface ProcessedFile { file: LocalFileAttributes; thumbnail: LocalFileAttributes; - metadata: LocalFileAttributes; + metadata: EncryptedMetadata; pubMagicMetadata: EncryptedMagicMetadata; localID: number; } @@ -1119,25 +1124,31 @@ const encryptFile = async ( worker, ); - const { file: encryptedThumbnail } = await worker.encryptThumbnail( - file.thumbnail, - fileKey, - ); + const { + encryptedData: thumbEncryptedData, + decryptionHeaderB64: thumbDecryptionHeader, + } = await worker.encryptThumbnail(file.thumbnail, fileKey); + const encryptedThumbnail = { + encryptedData: thumbEncryptedData, + decryptionHeader: thumbDecryptionHeader, + }; - const { file: encryptedMetadata } = await worker.encryptMetadata( + const encryptedMetadata = await worker.encryptMetadata( file.metadata, fileKey, ); let encryptedPubMagicMetadata: EncryptedMagicMetadata; if (file.pubMagicMetadata) { - const { file: encryptedPubMagicMetadataData } = - await worker.encryptMetadata(file.pubMagicMetadata.data, fileKey); + const encryptedPubMagicMetadataData = await worker.encryptMetadata( + file.pubMagicMetadata.data, + fileKey, + ); encryptedPubMagicMetadata = { version: file.pubMagicMetadata.version, count: file.pubMagicMetadata.count, - data: encryptedPubMagicMetadataData.encryptedData, - header: encryptedPubMagicMetadataData.decryptionHeader, + data: encryptedPubMagicMetadataData.encryptedDataB64, + header: encryptedPubMagicMetadataData.decryptionHeaderB64, }; } @@ -1267,7 +1278,10 @@ const uploadToBucket = async ( decryptionHeader: file.thumbnail.decryptionHeader, objectKey: thumbnailObjectKey, }, - metadata: file.metadata, + metadata: { + encryptedData: file.metadata.encryptedDataB64, + decryptionHeader: file.metadata.decryptionHeaderB64, + }, pubMagicMetadata: file.pubMagicMetadata, }; return backupedFile; diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index d94e3d5eff..5855a91898 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -177,6 +177,7 @@ export async function decryptFile( return { ...restFileProps, key: fileKey, + // @ts-expect-error TODO: Need to use zod here. metadata: fileMetadata, magicMetadata: fileMagicMetadata, pubMagicMetadata: filePubMagicMetadata, diff --git a/web/apps/photos/src/utils/magicMetadata/index.ts b/web/apps/photos/src/utils/magicMetadata/index.ts index 8d94a574f9..aac24f9245 100644 --- a/web/apps/photos/src/utils/magicMetadata/index.ts +++ b/web/apps/photos/src/utils/magicMetadata/index.ts @@ -56,6 +56,7 @@ export async function updateMagicMetadata( } if (typeof originalMagicMetadata?.data === "string") { + // @ts-expect-error TODO: Need to use zod here. originalMagicMetadata.data = await cryptoWorker.decryptMetadata( originalMagicMetadata.data, originalMagicMetadata.header, diff --git a/web/packages/base/crypto/ente.ts b/web/packages/base/crypto/ente.ts index dd230d6a15..017c38aa2c 100644 --- a/web/packages/base/crypto/ente.ts +++ b/web/packages/base/crypto/ente.ts @@ -1,56 +1,197 @@ /** * @file Higher level functions that use the ontology of Ente's types * - * These are thin wrappers over the (thin-) wrappers in internal/libsodium.ts. - * The main difference is that these functions don't talk in terms of the crypto - * algorithms, but rather in terms the higher-level Ente specific goal we are - * trying to accomplish. + * [Note: Crypto code hierarchy] + * + * The functions in this file (base/crypto/ente.ts) are are thin wrappers over + * the (thin-) wrappers in internal/libsodium.ts. The main difference is that + * these functions don't talk in terms of the crypto algorithms, but rather in + * terms the higher-level Ente specific goal we are trying to accomplish. + * + * Some of these are also exposed via the web worker in + * internal/crypto.worker.ts. The web worker variants should be used when we + * need to perform these operations from the main thread, so that the UI remains + * responsive while the potentially CPU-intensive encryption etc happens. + * + * 1. ente.ts or crypto.worker.ts (high level, Ente specific). + * 2. internal/libsodium.ts (wrappers over libsodium) + * 3. libsodium (JS bindings). */ import * as libsodium from "@ente/shared/crypto/internal/libsodium"; /** - * Encrypt arbitrary metadata associated with a file using the file's key. + * Encrypt arbitrary data associated with an Ente object (file, collection, + * entity) using the object's key. * - * @param metadata The metadata (bytes) to encrypt. + * Use {@link decryptAssociatedData} to decrypt the result. * - * @param keyB64 Base64 encoded string containing the encryption key (this'll - * generally be the file's key). + * See {@link encryptChaChaOneShot} for the implementation details. * - * @returns Base64 encoded strings containing the encrypted data and the - * decryption header. + * @param data A {@link Uint8Array} containing the bytes to encrypt. + * + * @param keyB64 base64 string containing the encryption key. This is expected + * to the key of the object with which {@link data} is associated. For example, + * if this is data associated with a file, then this will be the file's key. + * + * @returns The encrypted data and the (base64 encoded) decryption header. */ -export const encryptFileMetadata = async ( - metadata: Uint8Array, +export const encryptAssociatedData = libsodium.encryptChaChaOneShot; + +/** + * Encrypt the thumbnail for a file. + * + * This is just an alias for {@link encryptAssociatedData}. + * + * @param data The thumbnail's data. + * + * @param keyB64 The key associated with the file whose thumbnail this is. + * + * @returns The encrypted thumbnail, and the associated decryption header + * (base64 encoded). + */ +export const encryptThumbnail = encryptAssociatedData; + +/** + * Encrypted the embedding associated with a file using the file's key. + * + * This as a variant of {@link encryptAssociatedData} tailored for + * encrypting the embeddings (a.k.a. derived data) associated with a file. In + * particular, it returns the encrypted data in the result as a base64 string + * instead of its bytes. + * + * Use {@link decryptFileEmbedding} to decrypt the result. + */ +export const encryptFileEmbedding = async ( + data: Uint8Array, keyB64: string, ) => { - const { file } = await libsodium.encryptChaChaOneShot(metadata, keyB64); + const { encryptedData, decryptionHeaderB64 } = await encryptAssociatedData( + data, + keyB64, + ); return { - encryptedMetadataB64: await libsodium.toB64(file.encryptedData), - decryptionHeaderB64: file.decryptionHeader, + encryptedDataB64: await libsodium.toB64(encryptedData), + decryptionHeaderB64, }; }; /** - * Decrypt arbitrary metadata associated with a file using the file's key. + * Encrypt the metadata associated with an Ente object (file, collection or + * entity) using the object's key. * - * @param encryptedMetadataB64 Base64 encoded string containing the encrypted - * data. + * This is a variant of {@link encryptAssociatedData} tailored for encrypting + * any arbitrary metadata associated with an Ente object. For example, it is + * used for encrypting the various metadata fields (See: [Note: Metadatum]) + * associated with a file, using that file's key. * - * @param headerB64 Base64 encoded string containing the decryption header + * Instead of raw bytes, it takes as input an arbitrary JSON object which it + * encodes into a string, and encrypts that. And instead of returning the raw + * encrypted bytes, it returns their base64 string representation. + * + * Use {@link decryptMetadata} to decrypt the result. + * + * @param metadata The JSON value to encrypt. It can be an arbitrary JSON value, + * but since TypeScript currently doesn't have a native JSON type, it is typed + * as an unknown. + * + * @returns The encrypted data and decryption header, both as base64 strings. + */ +export const encryptMetadata = async (metadata: unknown, keyB64: string) => { + const encodedMetadata = new TextEncoder().encode(JSON.stringify(metadata)); + + const { encryptedData, decryptionHeaderB64 } = await encryptAssociatedData( + encodedMetadata, + keyB64, + ); + return { + encryptedDataB64: await libsodium.toB64(encryptedData), + decryptionHeaderB64, + }; +}; + +/** + * Decrypt arbitrary data associated with an Ente object (file, collection or + * entity) using the object's key. + * + * This is the sibling of {@link encryptAssociatedData}. + * + * See {@link decryptChaChaOneShot2} for the implementation details. + * + * @param encryptedData A {@link Uint8Array} containing the bytes to decrypt. + * + * @param headerB64 A base64 string containing the decryption header that was * produced during encryption. * - * @param keyB64 Base64 encoded string containing the encryption key (this'll - * generally be the file's key). + * @param keyB64 A base64 string containing the encryption key. This is expected + * to be the key of the object to which {@link encryptedDataB64} is associated. * - * @returns The decrypted metadata bytes. + * @returns The decrypted bytes. */ -export const decryptFileMetadata = async ( - encryptedMetadataB64: string, +export const decryptAssociatedData = libsodium.decryptChaChaOneShot; + +/** + * Decrypt the thumbnail for a file. + * + * This is just an alias for {@link decryptAssociatedData}. + */ +export const decryptThumbnail = decryptAssociatedData; + +/** + * Decrypt the embedding associated with a file using the file's key. + * + * This is the sibling of {@link encryptFileEmbedding}. + * + * @param encryptedDataB64 A base64 string containing the encrypted embedding. + * + * @param headerB64 A base64 string containing the decryption header produced + * during encryption. + * + * @param keyB64 A base64 string containing the encryption key. This is expected + * to be the key of the file with which {@link encryptedDataB64} is associated. + * + * @returns The decrypted metadata JSON object. + */ +export const decryptFileEmbedding = async ( + encryptedDataB64: string, decryptionHeaderB64: string, keyB64: string, ) => - libsodium.decryptChaChaOneShot( - await libsodium.fromB64(encryptedMetadataB64), - await libsodium.fromB64(decryptionHeaderB64), + decryptAssociatedData( + await libsodium.fromB64(encryptedDataB64), + decryptionHeaderB64, keyB64, ); + +/** + * Decrypt the metadata associated with an Ente object (file, collection or + * entity) using the object's key. + * + * This is the sibling of {@link decryptMetadata}. + * + * @param encryptedDataB64 base64 encoded string containing the encrypted data. + * + * @param headerB64 base64 encoded string containing the decryption header + * produced during encryption. + * + * @param keyB64 base64 encoded string containing the encryption key. This is + * expected to be the key of the object with which {@link encryptedDataB64} is + * associated. + * + * @returns The decrypted JSON value. Since TypeScript does not have a native + * JSON type, we need to return it as an `unknown`. + */ + +export const decryptMetadata = async ( + encryptedDataB64: string, + decryptionHeaderB64: string, + keyB64: string, +) => + JSON.parse( + new TextDecoder().decode( + await decryptAssociatedData( + await libsodium.fromB64(encryptedDataB64), + decryptionHeaderB64, + keyB64, + ), + ), + ) as unknown; diff --git a/web/packages/new/photos/services/download.ts b/web/packages/new/photos/services/download.ts index 694fc7cda7..10b7f200bc 100644 --- a/web/packages/new/photos/services/download.ts +++ b/web/packages/new/photos/services/download.ts @@ -127,7 +127,7 @@ class DownloadManagerImpl { const encrypted = await downloadClient.downloadThumbnail(file); const decrypted = await cryptoWorker.decryptThumbnail( encrypted, - await cryptoWorker.fromB64(file.thumbnail.decryptionHeader), + file.thumbnail.decryptionHeader, file.key, ); return decrypted; diff --git a/web/packages/new/photos/services/ml/embedding.ts b/web/packages/new/photos/services/ml/embedding.ts index 32395476be..2c96f43152 100644 --- a/web/packages/new/photos/services/ml/embedding.ts +++ b/web/packages/new/photos/services/ml/embedding.ts @@ -1,4 +1,4 @@ -import { decryptFileMetadata, encryptFileMetadata } from "@/base/crypto/ente"; +import { decryptFileEmbedding, encryptFileEmbedding } from "@/base/crypto/ente"; import { authenticatedRequestHeaders, ensureOk } from "@/base/http"; import log from "@/base/log"; import { apiURL } from "@/base/origins"; @@ -195,7 +195,7 @@ export const fetchDerivedData = async ( } try { - const decryptedBytes = await decryptFileMetadata( + const decryptedBytes = await decryptFileEmbedding( remoteEmbedding.encryptedEmbedding, remoteEmbedding.decryptionHeader, file.key, @@ -293,15 +293,15 @@ const putEmbedding = async ( model: EmbeddingModel, embedding: Uint8Array, ) => { - const { encryptedMetadataB64, decryptionHeaderB64 } = - await encryptFileMetadata(embedding, enteFile.key); + const { encryptedDataB64, decryptionHeaderB64 } = + await encryptFileEmbedding(embedding, enteFile.key); const res = await fetch(await apiURL("/embeddings"), { method: "PUT", headers: await authenticatedRequestHeaders(), body: JSON.stringify({ fileID: enteFile.id, - encryptedEmbedding: encryptedMetadataB64, + encryptedEmbedding: encryptedDataB64, decryptionHeader: decryptionHeaderB64, model, }), diff --git a/web/packages/shared/crypto/internal/crypto.worker.ts b/web/packages/shared/crypto/internal/crypto.worker.ts index e709aa74d6..d825ba5a57 100644 --- a/web/packages/shared/crypto/internal/crypto.worker.ts +++ b/web/packages/shared/crypto/internal/crypto.worker.ts @@ -1,44 +1,38 @@ +import * as ente from "@/base/crypto/ente"; import * as libsodium from "@ente/shared/crypto/internal/libsodium"; import * as Comlink from "comlink"; import type { StateAddress } from "libsodium-wrappers"; -const textDecoder = new TextDecoder(); -const textEncoder = new TextEncoder(); - +/** + * A web worker that exposes some of the functions defined in either the Ente + * specific layer (base/crypto/ente.ts) or the internal libsodium layer + * (internal/libsodium.ts). + * + * Running these in a web worker allows us to use potentially CPU-intensive + * crypto operations from the main thread without stalling the UI. + * + * See: [Note: Crypto code hierarchy]. + * + * Note: Keep these methods logic free. They should just act as trivial proxies. + */ export class DedicatedCryptoWorker { - async decryptMetadata( - encryptedMetadata: string, - header: string, - key: string, - ) { - const encodedMetadata = await libsodium.decryptChaChaOneShot( - await libsodium.fromB64(encryptedMetadata), - await libsodium.fromB64(header), - key, - ); - return JSON.parse(textDecoder.decode(encodedMetadata)); - } - async decryptThumbnail( - fileData: Uint8Array, - header: Uint8Array, - key: string, + encryptedData: Uint8Array, + headerB64: string, + keyB64: string, ) { - return libsodium.decryptChaChaOneShot(fileData, header, key); + return ente.decryptThumbnail(encryptedData, headerB64, keyB64); } - async decryptEmbedding( - encryptedEmbedding: string, - header: string, - key: string, + async decryptMetadata( + encryptedDataB64: string, + decryptionHeaderB64: string, + keyB64: string, ) { - const encodedEmbedding = await libsodium.decryptChaChaOneShot( - await libsodium.fromB64(encryptedEmbedding), - await libsodium.fromB64(header), - key, - ); - return Float32Array.from( - JSON.parse(textDecoder.decode(encodedEmbedding)), + return ente.decryptMetadata( + encryptedDataB64, + decryptionHeaderB64, + keyB64, ); } @@ -46,41 +40,12 @@ export class DedicatedCryptoWorker { return libsodium.decryptChaCha(fileData, header, key); } - async encryptMetadata(metadata: Object, key: string) { - const encodedMetadata = textEncoder.encode(JSON.stringify(metadata)); - - const { file: encryptedMetadata } = - await libsodium.encryptChaChaOneShot(encodedMetadata, key); - const { encryptedData, ...other } = encryptedMetadata; - return { - file: { - encryptedData: await libsodium.toB64(encryptedData), - ...other, - }, - key, - }; + async encryptThumbnail(data: Uint8Array, keyB64: string) { + return ente.encryptThumbnail(data, keyB64); } - async encryptThumbnail(fileData: Uint8Array, key: string) { - return libsodium.encryptChaChaOneShot(fileData, key); - } - - async encryptEmbedding(embedding: Float32Array, key: string) { - const encodedEmbedding = textEncoder.encode( - JSON.stringify(Array.from(embedding)), - ); - const { file: encryptEmbedding } = await libsodium.encryptChaChaOneShot( - encodedEmbedding, - key, - ); - const { encryptedData, ...other } = encryptEmbedding; - return { - file: { - encryptedData: await libsodium.toB64(encryptedData), - ...other, - }, - key, - }; + async encryptMetadata(metadata: unknown, keyB64: string) { + return ente.encryptMetadata(metadata, keyB64); } async encryptFile(fileData: Uint8Array) { diff --git a/web/packages/shared/crypto/internal/libsodium.ts b/web/packages/shared/crypto/internal/libsodium.ts index 857c826d5a..9e2df77dfe 100644 --- a/web/packages/shared/crypto/internal/libsodium.ts +++ b/web/packages/shared/crypto/internal/libsodium.ts @@ -11,7 +11,7 @@ import { CustomError } from "@ente/shared/error"; import sodium, { type StateAddress } from "libsodium-wrappers"; /** - * Convert a {@link Uint8Array} to a Base64 encoded string. + * Convert bytes ({@link Uint8Array}) to a base64 string. * * See also {@link toB64URLSafe} and {@link toB64URLSafeNoPadding}. */ @@ -21,7 +21,7 @@ export const toB64 = async (input: Uint8Array) => { }; /** - * Convert a Base64 encoded string to a {@link Uint8Array}. + * Convert a base64 string to bytes ({@link Uint8Array}). * * This is the converse of {@link toBase64}. */ @@ -31,7 +31,7 @@ export const fromB64 = async (input: string) => { }; /** - * Convert a {@link Uint8Array} to a URL-safe Base64 encoded string. + * Convert bytes ({@link Uint8Array}) to a URL-safe base64 string. * * See also {@link toB64URLSafe} and {@link toB64URLSafeNoPadding}. */ @@ -41,7 +41,7 @@ export const toB64URLSafe = async (input: Uint8Array) => { }; /** - * Convert a {@link Uint8Array} to a unpadded URL-safe Base64 encoded string. + * Convert bytes ({@link Uint8Array}) to a unpadded URL-safe base64 string. * * This differs from {@link toB64URLSafe} in that it does not append any * trailing padding character(s) "=" to make the resultant string's length be an @@ -62,7 +62,7 @@ export const toB64URLSafeNoPadding = async (input: Uint8Array) => { }; /** - * Convert a unpadded URL-safe Base64 encoded string to a {@link Uint8Array}. + * Convert a unpadded URL-safe base64 string to bytes ({@link Uint8Array}). * * This is the converse of {@link toB64URLSafeNoPadding}, and does not expect * its input string's length to be a an integer multiple of 4. @@ -110,10 +110,56 @@ export async function fromHex(input: string) { return await toB64(sodium.from_hex(input)); } -export async function encryptChaChaOneShot(data: Uint8Array, key: string) { +/** + * Encrypt the given {@link data} using the given (base64 encoded) key. + * + * Use {@link decryptChaChaOneShot} to decrypt the result. + * + * [Note: Salsa and ChaCha] + * + * This uses the same stream encryption algorithm (XChaCha20 stream cipher with + * Poly1305 MAC authentication) that we use for encrypting other streams, in + * particular the actual file's contents. + * + * The difference here is that this function does a one shot instead of a + * streaming encryption. This is only meant to be used for relatively small + * amounts of data (few MBs). + * + * See: https://doc.libsodium.org/secret-key_cryptography/secretstream + * + * Libsodium also provides the `crypto_secretbox_easy` APIs for one shot + * encryption, which we do use in other places where we need to one shot + * encryption of independent bits of data. + * + * These secretbox APIs use XSalsa20 with Poly1305. XSalsa20 is a minor variant + * (predecessor in fact) of XChaCha20. + * + * See: https://doc.libsodium.org/secret-key_cryptography/secretbox + * + * The difference here is that this function is meant to used for data + * associated with a file (or some other Ente object, like a collection or an + * entity). There is no technical reason to do it that way, just this way all + * data associated with a file, including its actual contents, use the same + * underlying (streaming) libsodium APIs. In other cases, where we have free + * standing independent data, we continue using the secretbox APIs for one shot + * encryption and decryption. + * + * @param data A {@link Uint8Array} containing the bytes that we want to + * encrypt. + * + * @param keyB64 A base64 string containing the encryption key. + * + * @returns The encrypted data (bytes) and decryption header pair (base64 + * encoded string). Both these values are needed to decrypt the data. The header + * does not need to be secret. + */ +export const encryptChaChaOneShot = async ( + data: Uint8Array, + keyB64: string, +) => { await sodium.ready; - const uintkey: Uint8Array = await fromB64(key); + const uintkey: Uint8Array = await fromB64(keyB64); const initPushResult = sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey); const [pushState, header] = [initPushResult.state, initPushResult.header]; @@ -125,13 +171,10 @@ export async function encryptChaChaOneShot(data: Uint8Array, key: string) { sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL, ); return { - key: await toB64(uintkey), - file: { - encryptedData: pushResult, - decryptionHeader: await toB64(header), - }, + encryptedData: pushResult, + decryptionHeaderB64: await toB64(header), }; -} +}; export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024; @@ -207,23 +250,38 @@ export async function encryptFileChunk( return pushResult; } -export async function decryptChaChaOneShot( - data: Uint8Array, - header: Uint8Array, - key: string, -) { +/** + * Decrypt the result of {@link encryptChaChaOneShot}. + * + * @param encryptedData A {@link Uint8Array} containing the bytes to decrypt. + * + * @param header A base64 string containing the bytes of the decryption header + * that was produced during encryption. + * + * @param keyB64 The base64 string containing the key that was used to encrypt + * the data. + * + * @returns The decrypted bytes. + * + * @returns The decrypted metadata bytes. + */ +export const decryptChaChaOneShot = async ( + encryptedData: Uint8Array, + headerB64: string, + keyB64: string, +) => { await sodium.ready; const pullState = sodium.crypto_secretstream_xchacha20poly1305_init_pull( - header, - await fromB64(key), + await fromB64(headerB64), + await fromB64(keyB64), ); const pullResult = sodium.crypto_secretstream_xchacha20poly1305_pull( pullState, - data, + encryptedData, null, ); return pullResult.message; -} +}; export const decryptChaCha = async ( data: Uint8Array, @@ -453,7 +511,7 @@ export async function generateSaltToDeriveKey() { } /** - * Generate a new public/private keypair, and return their Base64 + * Generate a new public/private keypair, and return their base64 * representations. */ export const generateKeyPair = async () => {