diff --git a/web/apps/photos/src/services/upload/upload-service.ts b/web/apps/photos/src/services/upload/upload-service.ts index 7b0ae0a28e..018688d257 100644 --- a/web/apps/photos/src/services/upload/upload-service.ts +++ b/web/apps/photos/src/services/upload/upload-service.ts @@ -2,6 +2,7 @@ import { streamEncryptionChunkSize, type B64EncryptionResult, } from "@/base/crypto/libsodium"; +import type { BytesOrB64 } from "@/base/crypto/types"; import { type CryptoWorker } from "@/base/crypto/worker"; import { ensureElectron } from "@/base/electron"; import { basename, nameAndExtension } from "@/base/file"; @@ -259,28 +260,20 @@ interface EncryptedFileStream { chunkCount: number; } -interface LocalFileAttributes< - T extends string | Uint8Array | EncryptedFileStream, -> { - encryptedData: T; - decryptionHeader: string; -} - interface EncryptedMetadata { encryptedDataB64: string; decryptionHeaderB64: string; } -interface EncryptionResult< - T extends string | Uint8Array | EncryptedFileStream, -> { - file: LocalFileAttributes; - key: string; -} - interface ProcessedFile { - file: LocalFileAttributes; - thumbnail: LocalFileAttributes; + file: { + encryptedData: Uint8Array | EncryptedFileStream; + decryptionHeader: string; + }; + thumbnail: { + encryptedData: Uint8Array; + decryptionHeader: string; + }; metadata: EncryptedMetadata; pubMagicMetadata: EncryptedMagicMetadata; localID: number; @@ -1354,30 +1347,35 @@ const encryptFile = async ( encryptionKey: string, worker: CryptoWorker, ): Promise => { - const { key: fileKey, file: encryptedFiledata } = await encryptFiledata( - file.fileStreamOrData, - worker, - ); + const fileKey = await worker.generateBlobOrStreamKey(); + + const { fileStreamOrData, thumbnail, metadata, pubMagicMetadata, localID } = + file; + + const encryptedFiledata = + fileStreamOrData instanceof Uint8Array + ? await worker.encryptStreamBytes(fileStreamOrData, fileKey) + : await encryptFileStream(fileStreamOrData, fileKey, worker); const encryptedThumbnail = await worker.encryptThumbnail( - file.thumbnail, + thumbnail, fileKey, ); const encryptedMetadata = await worker.encryptMetadataJSON({ - jsonValue: file.metadata, + jsonValue: metadata, keyB64: fileKey, }); let encryptedPubMagicMetadata: EncryptedMagicMetadata; - if (file.pubMagicMetadata) { + if (pubMagicMetadata) { const encryptedPubMagicMetadataData = await worker.encryptMetadataJSON({ - jsonValue: file.pubMagicMetadata.data, + jsonValue: pubMagicMetadata.data, keyB64: fileKey, }); encryptedPubMagicMetadata = { - version: file.pubMagicMetadata.version, - count: file.pubMagicMetadata.count, + version: pubMagicMetadata.version, + count: pubMagicMetadata.count, data: encryptedPubMagicMetadataData.encryptedDataB64, header: encryptedPubMagicMetadataData.decryptionHeaderB64, }; @@ -1391,34 +1389,26 @@ const encryptFile = async ( thumbnail: encryptedThumbnail, metadata: encryptedMetadata, pubMagicMetadata: encryptedPubMagicMetadata, - localID: file.localID, + localID: localID, }, fileKey: encryptedKey, }; return result; }; -const encryptFiledata = async ( - fileStreamOrData: FileStream | Uint8Array, - worker: CryptoWorker, -): Promise> => - fileStreamOrData instanceof Uint8Array - ? await worker.encryptFile(fileStreamOrData) - : await encryptFileStream(fileStreamOrData, worker); - const encryptFileStream = async ( - fileData: FileStream, + { stream, chunkCount }: FileStream, + fileKey: BytesOrB64, worker: CryptoWorker, ) => { - const { stream, chunkCount } = fileData; const fileStreamReader = stream.getReader(); - const { key, decryptionHeader, pushState } = - await worker.initChunkEncryption(); + const { decryptionHeader, pushState } = + await worker.initChunkEncryption(fileKey); const ref = { pullCount: 1 }; const encryptedFileStream = new ReadableStream({ async pull(controller) { const { value } = await fileStreamReader.read(); - const encryptedFileChunk = await worker.encryptFileChunk( + const encryptedFileChunk = await worker.encryptStreamChunk( value, pushState, ref.pullCount === chunkCount, @@ -1431,11 +1421,8 @@ const encryptFileStream = async ( }, }); return { - key, - file: { - decryptionHeader, - encryptedData: { stream: encryptedFileStream, chunkCount }, - }, + decryptionHeader, + encryptedData: { stream: encryptedFileStream, chunkCount }, }; }; diff --git a/web/packages/base/crypto/ente-impl.ts b/web/packages/base/crypto/ente-impl.ts index f2279077a9..d5369eb07f 100644 --- a/web/packages/base/crypto/ente-impl.ts +++ b/web/packages/base/crypto/ente-impl.ts @@ -2,6 +2,10 @@ import * as libsodium from "./libsodium"; import type { BytesOrB64, EncryptedBlob, EncryptedFile } from "./types"; +export const _generateBoxKey = libsodium.generateBoxKey; + +export const _generateBlobOrStreamKey = libsodium.generateBlobOrStreamKey; + export const _encryptBoxB64 = libsodium.encryptBoxB64; export const _encryptBlob = libsodium.encryptBlob; @@ -19,6 +23,12 @@ export const _encryptThumbnail = async ( }; }; +export const _encryptStreamBytes = libsodium.encryptStreamBytes; + +export const _initChunkEncryption = libsodium.initChunkEncryption; + +export const _encryptStreamChunk = libsodium.encryptStreamChunk; + export const _encryptMetadataJSON_New = (jsonValue: unknown, key: BytesOrB64) => _encryptBlobB64(new TextEncoder().encode(JSON.stringify(jsonValue)), key); diff --git a/web/packages/base/crypto/index.ts b/web/packages/base/crypto/index.ts index 327a271662..13db907fd4 100644 --- a/web/packages/base/crypto/index.ts +++ b/web/packages/base/crypto/index.ts @@ -49,7 +49,6 @@ import { ComlinkWorker } from "@/base/worker/comlink-worker"; import { assertionFailed } from "../assert"; import { inWorker } from "../env"; import * as ei from "./ente-impl"; -import * as libsodium from "./libsodium"; import type { BytesOrB64, EncryptedBlob, EncryptedBox } from "./types"; import type { CryptoWorker } from "./worker"; @@ -89,16 +88,22 @@ const assertInWorker = (x: T): T => { }; /** - * Return a new randomly generated 256-bit key suitable for use with the *Box - * encryption functions. + * Return a new randomly generated 256-bit key (as a base64 string) suitable for + * use with the *Box encryption functions. */ -export const generateNewBoxKey = libsodium.generateNewBoxKey; +export const generateBoxKey = () => + inWorker() + ? ei._generateBoxKey() + : sharedCryptoWorker().then((w) => w.generateBoxKey()); /** - * Return a new randomly generated 256-bit key suitable for use with the *Blob - * or *Stream encryption functions. + * Return a new randomly generated 256-bit key (as a base64 string) suitable for + * use with the *Blob or *Stream encryption functions. */ -export const generateNewBlobOrStreamKey = libsodium.generateNewBlobOrStreamKey; +export const generateBlobOrStreamKey = () => + inWorker() + ? ei._generateBlobOrStreamKey() + : sharedCryptoWorker().then((w) => w.generateBlobOrStreamKey()); /** * Encrypt the given data, returning a box containing the encrypted data and a @@ -144,18 +149,6 @@ export const encryptBlobB64 = (data: BytesOrB64, key: BytesOrB64) => ? ei._encryptBlobB64(data, key) : sharedCryptoWorker().then((w) => w._encryptBlobB64(data, key)); -/** - * Encrypt the thumbnail for a file. - * - * This is midway variant of {@link encryptBlob} and {@link encryptBlobB64} that - * returns the decryption header as a base64 string, but leaves the data - * unchanged. - * - * Use {@link decryptThumbnail} to decrypt the result. - */ -export const encryptThumbnail = (data: BytesOrB64, key: BytesOrB64) => - assertInWorker(ei._encryptThumbnail(data, key)); - /** * Encrypt the JSON metadata associated with an Ente object (file, collection or * entity) using the object's key. @@ -228,12 +221,6 @@ export const decryptBlobB64 = (blob: EncryptedBlob, key: BytesOrB64) => ? ei._decryptBlobB64(blob, key) : sharedCryptoWorker().then((w) => w.decryptBlobB64(blob, key)); -/** - * Decrypt the thumbnail encrypted using {@link encryptThumbnail}. - */ -export const decryptThumbnail = (blob: EncryptedBlob, key: BytesOrB64) => - assertInWorker(ei._decryptThumbnail(blob, key)); - /** * Decrypt the metadata JSON encrypted using {@link encryptMetadataJSON}. * diff --git a/web/packages/base/crypto/libsodium.ts b/web/packages/base/crypto/libsodium.ts index 3f652311e2..40636c5b21 100644 --- a/web/packages/base/crypto/libsodium.ts +++ b/web/packages/base/crypto/libsodium.ts @@ -17,6 +17,7 @@ import type { EncryptedBlobBytes, EncryptedBox, EncryptedBoxB64, + EncryptedFile, } from "./types"; /** @@ -128,23 +129,25 @@ const bytes = async (bob: BytesOrB64) => typeof bob == "string" ? fromB64(bob) : bob; /** - * Generate a key for use with the *Box encryption functions. + * Generate a new key for use with the *Box encryption functions, and return its + * base64 string representation. * * This returns a new randomly generated 256-bit key suitable for being used * with libsodium's secretbox APIs. */ -export const generateNewBoxKey = async () => { +export const generateBoxKey = async () => { await sodium.ready; return toB64(sodium.crypto_secretbox_keygen()); }; /** - * Generate a key for use with the *Blob or *Stream encryption functions. + * Generate a new key for use with the *Blob or *Stream encryption functions, + * and return its base64 string representation. * * This returns a new randomly generated 256-bit key suitable for being used * with libsodium's secretstream APIs. */ -export const generateNewBlobOrStreamKey = async () => { +export const generateBlobOrStreamKey = async () => { await sodium.ready; return toB64(sodium.crypto_secretstream_xchacha20poly1305_keygen()); }; @@ -284,9 +287,9 @@ export const encryptBlob = async ( ): Promise => { await sodium.ready; - const uintkey = await bytes(key); + const keyBytes = await bytes(key); const initPushResult = - sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey); + sodium.crypto_secretstream_xchacha20poly1305_init_push(keyBytes); const [pushState, header] = [initPushResult.state, initPushResult.header]; const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push( @@ -340,21 +343,22 @@ export const streamEncryptionChunkSize = 4 * 1024 * 1024; * * @param data The data to encrypt. * - * @returns The encrypted data, the decryption header as {@link Uint8Array}s, - * and the newly generated key that was used for encryption. + * @returns The encrypted bytes ({@link Uint8Array}) and the decryption header + * (as a base64 string). * * - See: [Note: 3 forms of encryption (Box | Blob | Stream)]. * * - See: https://doc.libsodium.org/secret-key_cryptography/secretstream */ -export const encryptChaCha = async (data: Uint8Array) => { +export const encryptStreamBytes = async ( + data: Uint8Array, + key: BytesOrB64, +): Promise => { await sodium.ready; - const uintkey: Uint8Array = - sodium.crypto_secretstream_xchacha20poly1305_keygen(); - + const keyBytes = await bytes(key); const initPushResult = - sodium.crypto_secretstream_xchacha20poly1305_init_push(uintkey); + sodium.crypto_secretstream_xchacha20poly1305_init_push(keyBytes); const [pushState, header] = [initPushResult.state, initPushResult.header]; let bytesRead = 0; let tag = sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE; @@ -379,45 +383,74 @@ export const encryptChaCha = async (data: Uint8Array) => { encryptedChunks.push(pushResult); } return { - key: await toB64(uintkey), - file: { - encryptedData: mergeUint8Arrays(encryptedChunks), - decryptionHeader: await toB64(header), - }, + encryptedData: mergeUint8Arrays(encryptedChunks), + decryptionHeader: await toB64(header), }; }; -export async function initChunkEncryption() { +/** + * Initialize libsodium's secretstream APIs for encrypting + * {@link streamEncryptionChunkSize} chunks. Subsequently, each chunk can be + * encrypted using {@link encryptStreamChunk}. + * + * Use {@link initChunkDecryption} to initialize the decryption routine, and + * {@link decryptStreamChunk} to decrypt the individual chunks. + * + * See also: {@link encryptStreamBytes} which also does chunked encryption but + * encrypts all the chunks in a single call. + * + * @param key The key to use for encryption. + * + * @returns The decryption header (as a base64 string) which should be preserved + * and used during decryption, and an opaque "push state" that should be passed + * to subsequent calls to {@link encryptStreamChunk} along with the chunks's + * contents. + */ +export const initChunkEncryption = async (key: BytesOrB64) => { await sodium.ready; - const key = sodium.crypto_secretstream_xchacha20poly1305_keygen(); - const initPushResult = - sodium.crypto_secretstream_xchacha20poly1305_init_push(key); - const [pushState, header] = [initPushResult.state, initPushResult.header]; + const keyBytes = await bytes(key); + const { state, header } = + sodium.crypto_secretstream_xchacha20poly1305_init_push(keyBytes); return { - key: await toB64(key), decryptionHeader: await toB64(header), - pushState, + pushState: state, }; -} +}; -export async function encryptFileChunk( +/** + * Encrypt an individual chunk using libsodium's secretstream APIs. + * + * This function is not meant to be standalone, but is instead called in tandem + * with {@link initChunkEncryption} for encrypting data after breaking it into + * chunks. + * + * @param data The chunk's data as bytes ({@link Uint8Array}). + * + * @param pushState The state for this instantiation of chunked encryption. This + * should be treated as opaque libsodium state that should be passed to all + * calls to {@link encryptStreamChunk} that are paired with a particular + * {@link initChunkEncryption}. + * + * @param isFinalChunk `true` if this is the last chunk in the sequence. + * + * @returns The encrypted chunk. + */ +export const encryptStreamChunk = async ( data: Uint8Array, pushState: sodium.StateAddress, isFinalChunk: boolean, -) { +) => { await sodium.ready; const tag = isFinalChunk ? sodium.crypto_secretstream_xchacha20poly1305_TAG_FINAL : sodium.crypto_secretstream_xchacha20poly1305_TAG_MESSAGE; - const pushResult = sodium.crypto_secretstream_xchacha20poly1305_push( + return sodium.crypto_secretstream_xchacha20poly1305_push( pushState, data, null, tag, ); - - return pushResult; -} +}; /** * Decrypt the result of {@link encryptBoxB64} and return the decrypted bytes. diff --git a/web/packages/base/crypto/worker.ts b/web/packages/base/crypto/worker.ts index 36d5a51a09..ffe0503dde 100644 --- a/web/packages/base/crypto/worker.ts +++ b/web/packages/base/crypto/worker.ts @@ -13,9 +13,14 @@ import * as libsodium from "./libsodium"; * Note: Keep these methods logic free. They are meant to be trivial proxies. */ export class CryptoWorker { + generateBoxKey = ei._generateBoxKey; + generateBlobOrStreamKey = ei._generateBlobOrStreamKey; encryptBoxB64 = ei._encryptBoxB64; encryptThumbnail = ei._encryptThumbnail; _encryptBlobB64 = ei._encryptBlobB64; + encryptStreamBytes = ei._encryptStreamBytes; + initChunkEncryption = ei._initChunkEncryption; + encryptStreamChunk = ei._encryptStreamChunk; encryptMetadataJSON_New = ei._encryptMetadataJSON_New; encryptMetadataJSON = ei._encryptMetadataJSON; decryptBox = ei._decryptBox; @@ -32,22 +37,6 @@ export class CryptoWorker { return libsodium.decryptChaCha(fileData, header, key); } - async encryptFile(fileData: Uint8Array) { - return libsodium.encryptChaCha(fileData); - } - - async encryptFileChunk( - data: Uint8Array, - pushState: StateAddress, - isFinalChunk: boolean, - ) { - return libsodium.encryptFileChunk(data, pushState, isFinalChunk); - } - - async initChunkEncryption() { - return libsodium.initChunkEncryption(); - } - async initChunkDecryption(header: Uint8Array, key: Uint8Array) { return libsodium.initChunkDecryption(header, key); } diff --git a/web/packages/new/photos/services/user-entity/index.ts b/web/packages/new/photos/services/user-entity/index.ts index 5321a2c642..803a177e66 100644 --- a/web/packages/new/photos/services/user-entity/index.ts +++ b/web/packages/new/photos/services/user-entity/index.ts @@ -2,7 +2,7 @@ import { decryptBoxB64, encryptBlobB64, encryptBoxB64, - generateNewBlobOrStreamKey, + generateBlobOrStreamKey, } from "@/base/crypto"; import { nullishToEmpty, nullToUndefined } from "@/utils/transform"; import { z } from "zod"; @@ -258,16 +258,16 @@ const getOrCreateEntityKeyB64 = async ( // As a sanity check, genarate the key but immediately encrypt it as if it // were fetched from remote and then try to decrypt it before doing anything // with it. - const generated = await generateNewEncryptedEntityKey(masterKey); + const generated = await generateEncryptedEntityKey(masterKey); const result = decryptEntityKey(generated, masterKey); await postUserEntityKey(type, generated); await saveRemoteUserEntityKey(type, generated); return result; }; -const generateNewEncryptedEntityKey = async (masterKey: Uint8Array) => { +const generateEncryptedEntityKey = async (masterKey: Uint8Array) => { const { encryptedData, nonce } = await encryptBoxB64( - await generateNewBlobOrStreamKey(), + await generateBlobOrStreamKey(), masterKey, ); // Remote calls it the header, but it really is the nonce.