From 564008693276b7eb06d419e08d5f782a728c2cd5 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 Aug 2024 09:58:10 +0530 Subject: [PATCH 1/9] Remove unnecessary conversion attempt --- web/packages/base/crypto/libsodium.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/packages/base/crypto/libsodium.ts b/web/packages/base/crypto/libsodium.ts index 31eb85141d..6145970972 100644 --- a/web/packages/base/crypto/libsodium.ts +++ b/web/packages/base/crypto/libsodium.ts @@ -231,7 +231,7 @@ export const encryptBoxB64 = async ( const nonce = sodium.randombytes_buf(sodium.crypto_secretbox_NONCEBYTES); const encryptedData = sodium.crypto_secretbox_easy( await bytes(data), - await bytes(nonce), + nonce, await bytes(key), ); return { From 62c32d02ce4fc2c3405b304c67731d77ca0d6c58 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 Aug 2024 11:38:56 +0530 Subject: [PATCH 2/9] Start treating it as a gateway --- .../photos/src/services/collectionService.ts | 3 +- web/apps/photos/src/services/entityService.ts | 3 +- web/apps/photos/src/services/fileService.ts | 2 +- web/packages/base/crypto/ente.ts | 224 ------------------ web/packages/base/crypto/index.ts | 220 +++++++++++++++++ web/packages/base/crypto/worker.ts | 3 +- web/packages/base/session-store.ts | 2 +- web/packages/media/file-metadata.ts | 2 +- web/packages/new/photos/services/file-data.ts | 2 +- .../new/photos/services/ml/ml-data.ts | 2 +- .../new/photos/services/user-entity.ts | 2 +- 11 files changed, 230 insertions(+), 235 deletions(-) delete mode 100644 web/packages/base/crypto/ente.ts diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index 0558c7473f..60d4ea35cc 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -1,5 +1,4 @@ -import { sharedCryptoWorker } from "@/base/crypto"; -import { encryptMetadataJSON } from "@/base/crypto/ente"; +import { encryptMetadataJSON, sharedCryptoWorker } from "@/base/crypto"; import log from "@/base/log"; import { apiURL } from "@/base/origins"; import { ItemVisibility } from "@/media/file-metadata"; diff --git a/web/apps/photos/src/services/entityService.ts b/web/apps/photos/src/services/entityService.ts index 2516a185db..7bd32e556a 100644 --- a/web/apps/photos/src/services/entityService.ts +++ b/web/apps/photos/src/services/entityService.ts @@ -1,5 +1,4 @@ -import { sharedCryptoWorker } from "@/base/crypto"; -import { decryptMetadataJSON } from "@/base/crypto/ente"; +import { decryptMetadataJSON, sharedCryptoWorker } from "@/base/crypto"; import log from "@/base/log"; import { apiURL } from "@/base/origins"; import HTTPService from "@ente/shared/network/HTTPService"; diff --git a/web/apps/photos/src/services/fileService.ts b/web/apps/photos/src/services/fileService.ts index 5cd16129c5..82ce81ede0 100644 --- a/web/apps/photos/src/services/fileService.ts +++ b/web/apps/photos/src/services/fileService.ts @@ -1,4 +1,4 @@ -import { encryptMetadataJSON } from "@/base/crypto/ente"; +import { encryptMetadataJSON } from "@/base/crypto"; import log from "@/base/log"; import { apiURL } from "@/base/origins"; import { getLocalFiles, setLocalFiles } from "@/new/photos/services/files"; diff --git a/web/packages/base/crypto/ente.ts b/web/packages/base/crypto/ente.ts deleted file mode 100644 index 476e589505..0000000000 --- a/web/packages/base/crypto/ente.ts +++ /dev/null @@ -1,224 +0,0 @@ -/** - * @file Higher level functions that use the ontology of Ente's types - * - * [Note: Crypto code hierarchy] - * - * 1. crypto/ente.ts (Ente specific higher level functions) - * 2. crypto/libsodium.ts (More primitive wrappers over libsodium) - * 3. libsodium-wrappers (JavaScript bindings to libsodium) - * - * Our cryptography primitives are provided by libsodium, specifically, its - * JavaScript bindings ("libsodium-wrappers"). This is the lowest layer. - * - * Direct usage of "libsodium-wrappers" is restricted to `crypto/libsodium.ts`. - * This is the next higher layer, and the first one that our code should - * directly use. Usually the functions in this file are thin wrappers over the - * raw libsodium APIs, with a bit of massaging. They also ensure that - * sodium.ready has been called before accessing libsodium's APIs, thus all the - * functions it exposes are async. - * - * The highest layer is this file, `crypto/ente.ts`. These are usually simple - * compositions of functionality exposed by `crypto/libsodium.ts`, but the - * difference is that the functions in ente.ts don't talk in terms of the crypto - * algorithms, but rather in terms the higher-level Ente specific goal we are - * trying to accomplish. - * - * There is an additional actor in the play. Cryptographic operations are CPU - * intensive and would cause the UI to stutter if used directly on the main - * thread. To keep the UI smooth, we instead want to run them in a web worker. - * However, sometimes we already _are_ running in a web worker, and delegating - * to another worker is wasteful. - * - * To handle both these scenario, each function in this file is split into the - * external API, and the underlying implementation (denoted by an "_" prefix). - * The external API functions check to see if we're already in a web worker, and - * if so directly invoke the implementation. Otherwise the call the sibling - * function in a shared "crypto" web worker (which then invokes the - * implementation, but this time in the context of a web worker). - * - * To avoid a circular dependency during webpack imports, we need to keep the - * implementation functions in a separate file (ente-impl.ts). This is a bit - * unfortunate, since it makes them harder to read and reason about (since their - * documentation and parameter names are all in ente.ts). - * - * Some older code directly calls the functions in the shared crypto.worker.ts, - * but that should be avoided since it makes the code not behave the way we want - * when we're already in a web worker. There are exceptions to this - * recommendation though (in circumstances where we create more crypto workers - * instead of using the shared one). - */ -import { sharedCryptoWorker } from "."; -import { assertionFailed } from "../assert"; -import { inWorker } from "../env"; -import * as ei from "./ente-impl"; -import type { BytesOrB64, EncryptedBlob, EncryptedBox } from "./types"; - -/** - * Some of these functions have not yet been needed on the main thread, and for - * these we don't have a corresponding sharedCryptoWorker method. - * - * This assertion will let us know when we need to implement them. This will - * gracefully degrade in production: the functionality will work, just that the - * crypto operations will happen on the main thread itself. - */ -const assertInWorker = (x: T): T => { - if (!inWorker()) assertionFailed("Currently only usable in a web worker"); - return x; -}; - -/** - * Encrypt the given data, returning a box containing the encrypted data and a - * randomly generated nonce that was used during encryption. - * - * Both the encrypted data and the nonce are returned as base64 strings. - * - * Use {@link decryptBoxB64} to decrypt the result. - * - * > The suffix "Box" comes from the fact that it uses the so called secretbox - * > APIs provided by libsodium under the hood. - * > - * > See: [Note: 3 forms of encryption (Box | Blob | Stream)] - */ -export const encryptBoxB64 = (data: BytesOrB64, key: BytesOrB64) => - inWorker() - ? ei._encryptBoxB64(data, key) - : sharedCryptoWorker().then((w) => w.encryptBoxB64(data, key)); - -/** - * Encrypt the given data, returning a blob containing the encrypted data and a - * decryption header. - * - * This function is usually used to encrypt data associated with an Ente object - * (file, collection, entity) using the object's key. - * - * Use {@link decryptBlob} to decrypt the result. - * - * > The suffix "Blob" comes from our convention of naming functions that use - * > the secretstream APIs in one-shot mode. - * > - * > See: [Note: 3 forms of encryption (Box | Blob | Stream)] - */ -export const encryptBlob = (data: BytesOrB64, key: BytesOrB64) => - assertInWorker(ei._encryptBlob(data, key)); - -/** - * A variant of {@link encryptBlob} that returns the result components as base64 - * strings. - */ -export const encryptBlobB64 = (data: BytesOrB64, key: BytesOrB64) => - assertInWorker(ei._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. - * - * This is a variant of {@link encryptBlobB64} tailored for encrypting any - * arbitrary metadata associated with an Ente object. For example, it is used - * for encrypting the various metadata fields associated with a file, using that - * file's key. - * - * Instead of raw bytes, it takes as input an arbitrary JSON object which it - * encodes into a string, and encrypts that. - * - * Use {@link decryptMetadataJSON} to decrypt the result. - * - * @param jsonValue The JSON value to encrypt. This can be an arbitrary JSON - * value, but since TypeScript currently doesn't have a native JSON type, it is - * typed as {@link unknown}. - * - * @param key The encryption key. - */ -export const encryptMetadataJSON_New = (jsonValue: unknown, key: BytesOrB64) => - inWorker() - ? ei._encryptMetadataJSON_New(jsonValue, key) - : sharedCryptoWorker().then((w) => - w.encryptMetadataJSON_New(jsonValue, key), - ); - -/** - * Deprecated, use {@link encryptMetadataJSON_New} instead. - */ -export const encryptMetadataJSON = async (r: { - jsonValue: unknown; - keyB64: string; -}) => - inWorker() - ? ei._encryptMetadataJSON(r) - : sharedCryptoWorker().then((w) => w.encryptMetadataJSON(r)); - -/** - * Decrypt a box encrypted using {@link encryptBoxB64}. - */ -export const decryptBox = (box: EncryptedBox, key: BytesOrB64) => - inWorker() - ? ei._decryptBox(box, key) - : sharedCryptoWorker().then((w) => w.decryptBox(box, key)); - -/** - * Variant of {@link decryptBox} that returns the result as a base64 string. - */ -export const decryptBoxB64 = (box: EncryptedBox, key: BytesOrB64) => - inWorker() - ? ei._decryptBoxB64(box, key) - : sharedCryptoWorker().then((w) => w.decryptBoxB64(box, key)); - -/** - * Decrypt a blob encrypted using either {@link encryptBlob} or - * {@link encryptBlobB64}. - */ -export const decryptBlob = (blob: EncryptedBlob, key: BytesOrB64) => - assertInWorker(ei._decryptBlob(blob, key)); - -/** - * A variant of {@link decryptBlob} that returns the result as a base64 string. - */ -export const decryptBlobB64 = (blob: EncryptedBlob, key: BytesOrB64) => - inWorker() - ? 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}. - * - * @returns The decrypted JSON value. Since TypeScript does not have a native - * JSON type, we need to return it as an `unknown`. - */ -export const decryptMetadataJSON_New = ( - blob: EncryptedBlob, - key: BytesOrB64, -) => - inWorker() - ? ei._decryptMetadataJSON_New(blob, key) - : sharedCryptoWorker().then((w) => - w.decryptMetadataJSON_New(blob, key), - ); - -/** - * Deprecated, retains the old API. - */ -export const decryptMetadataJSON = (r: { - encryptedDataB64: string; - decryptionHeaderB64: string; - keyB64: string; -}) => - inWorker() - ? ei._decryptMetadataJSON(r) - : sharedCryptoWorker().then((w) => w.decryptMetadataJSON(r)); diff --git a/web/packages/base/crypto/index.ts b/web/packages/base/crypto/index.ts index 736978f7ff..c1f09516eb 100644 --- a/web/packages/base/crypto/index.ts +++ b/web/packages/base/crypto/index.ts @@ -1,4 +1,53 @@ +/** + * @file Higher level functions that use the ontology of Ente's requirements. + * + * [Note: Crypto code hierarchy] + * + * 1. @/base/crypto (Crypto API for our code) + * 2. @/base/crypto/libsodium (Lower level wrappers over libsodium) + * 3. libsodium-wrappers (JavaScript bindings to libsodium) + * + * Our cryptography primitives are provided by libsodium, specifically, its + * JavaScript bindings ("libsodium-wrappers"). This is the lowest layer. + * + * Direct usage of "libsodium-wrappers" is restricted to `crypto/libsodium.ts`. + * This is the next higher layer. Usually the functions in this file are thin + * wrappers over the raw libsodium APIs, with a bit of massaging. They also + * ensure that sodium.ready has been called before accessing libsodium's APIs, + * thus all the functions it exposes are async. + * + * The highest layer is this file, `crypto/index.ts`, and the one that our own + * code should use. These are usually simple compositions of functionality + * exposed by `crypto/libsodium.ts`, the primary difference being that these + * functions try to talk in terms of higher-level Ente specific goal we are + * trying to accomplish instead of the specific underlying crypto algorithms. + * + * There is an additional actor in play. Cryptographic operations like + * encryption are CPU intensive and would cause the UI to stutter if used + * directly on the main thread. To keep the UI smooth, we instead want to run + * them in a web worker. However, sometimes we already _are_ running in a web + * worker, and delegating to another worker is wasteful. + * + * To handle both these scenario, the potentially CPU intensive functions in + * this file are split into the external API, and the underlying implementation + * (denoted by an "_" prefix). To avoid a circular dependency during webpack + * imports, we need to keep the implementation functions in a separate file + * (`ente-impl.ts`). + * + * The external API functions check to see if we're already in a web worker, and + * if so directly invoke the implementation. Otherwise the call the sibling + * function in a shared "crypto" web worker (which then invokes the + * implementation function, but this time in the context of a web worker). + * + * Also, some code (e.g. the uploader) creates it own crypto worker instances, + * and thus directly calls the functions in the web worker (it created) instead + * of going through this file. + */ import { ComlinkWorker } from "@/base/worker/comlink-worker"; +import { assertionFailed } from "../assert"; +import { inWorker } from "../env"; +import * as ei from "./ente-impl"; +import type { BytesOrB64, EncryptedBlob, EncryptedBox } from "./types"; import type { CryptoWorker } from "./worker"; /** @@ -21,3 +70,174 @@ export const createComlinkCryptoWorker = () => "crypto", new Worker(new URL("worker.ts", import.meta.url)), ); + +/** + * Some of the potentially CPU intensive functions below have not yet been + * needed on the main thread, and for these we don't have a corresponding + * sharedCryptoWorker method. + * + * This assertion will let us know when we need to implement them. This will + * gracefully degrade in production: the functionality will work, just that the + * crypto operations will happen on the main thread itself. + */ +const assertInWorker = (x: T): T => { + if (!inWorker()) assertionFailed("Currently only usable in a web worker"); + return x; +}; + +/** + * Encrypt the given data, returning a box containing the encrypted data and a + * randomly generated nonce that was used during encryption. + * + * Both the encrypted data and the nonce are returned as base64 strings. + * + * Use {@link decryptBoxB64} to decrypt the result. + * + * > The suffix "Box" comes from the fact that it uses the so called secretbox + * > APIs provided by libsodium under the hood. + * > + * > See: [Note: 3 forms of encryption (Box | Blob | Stream)] + */ +export const encryptBoxB64 = (data: BytesOrB64, key: BytesOrB64) => + inWorker() + ? ei._encryptBoxB64(data, key) + : sharedCryptoWorker().then((w) => w.encryptBoxB64(data, key)); + +/** + * Encrypt the given data, returning a blob containing the encrypted data and a + * decryption header. + * + * This function is usually used to encrypt data associated with an Ente object + * (file, collection, entity) using the object's key. + * + * Use {@link decryptBlob} to decrypt the result. + * + * > The suffix "Blob" comes from our convention of naming functions that use + * > the secretstream APIs in one-shot mode. + * > + * > See: [Note: 3 forms of encryption (Box | Blob | Stream)] + */ +export const encryptBlob = (data: BytesOrB64, key: BytesOrB64) => + assertInWorker(ei._encryptBlob(data, key)); + +/** + * A variant of {@link encryptBlob} that returns the result components as base64 + * strings. + */ +export const encryptBlobB64 = (data: BytesOrB64, key: BytesOrB64) => + assertInWorker(ei._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. + * + * This is a variant of {@link encryptBlobB64} tailored for encrypting any + * arbitrary metadata associated with an Ente object. For example, it is used + * for encrypting the various metadata fields associated with a file, using that + * file's key. + * + * Instead of raw bytes, it takes as input an arbitrary JSON object which it + * encodes into a string, and encrypts that. + * + * Use {@link decryptMetadataJSON} to decrypt the result. + * + * @param jsonValue The JSON value to encrypt. This can be an arbitrary JSON + * value, but since TypeScript currently doesn't have a native JSON type, it is + * typed as {@link unknown}. + * + * @param key The encryption key. + */ +export const encryptMetadataJSON_New = (jsonValue: unknown, key: BytesOrB64) => + inWorker() + ? ei._encryptMetadataJSON_New(jsonValue, key) + : sharedCryptoWorker().then((w) => + w.encryptMetadataJSON_New(jsonValue, key), + ); + +/** + * Deprecated, use {@link encryptMetadataJSON_New} instead. + */ +export const encryptMetadataJSON = async (r: { + jsonValue: unknown; + keyB64: string; +}) => + inWorker() + ? ei._encryptMetadataJSON(r) + : sharedCryptoWorker().then((w) => w.encryptMetadataJSON(r)); + +/** + * Decrypt a box encrypted using {@link encryptBoxB64}. + */ +export const decryptBox = (box: EncryptedBox, key: BytesOrB64) => + inWorker() + ? ei._decryptBox(box, key) + : sharedCryptoWorker().then((w) => w.decryptBox(box, key)); + +/** + * Variant of {@link decryptBox} that returns the result as a base64 string. + */ +export const decryptBoxB64 = (box: EncryptedBox, key: BytesOrB64) => + inWorker() + ? ei._decryptBoxB64(box, key) + : sharedCryptoWorker().then((w) => w.decryptBoxB64(box, key)); + +/** + * Decrypt a blob encrypted using either {@link encryptBlob} or + * {@link encryptBlobB64}. + */ +export const decryptBlob = (blob: EncryptedBlob, key: BytesOrB64) => + assertInWorker(ei._decryptBlob(blob, key)); + +/** + * A variant of {@link decryptBlob} that returns the result as a base64 string. + */ +export const decryptBlobB64 = (blob: EncryptedBlob, key: BytesOrB64) => + inWorker() + ? 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}. + * + * @returns The decrypted JSON value. Since TypeScript does not have a native + * JSON type, we need to return it as an `unknown`. + */ +export const decryptMetadataJSON_New = ( + blob: EncryptedBlob, + key: BytesOrB64, +) => + inWorker() + ? ei._decryptMetadataJSON_New(blob, key) + : sharedCryptoWorker().then((w) => + w.decryptMetadataJSON_New(blob, key), + ); + +/** + * Deprecated, retains the old API. + */ +export const decryptMetadataJSON = (r: { + encryptedDataB64: string; + decryptionHeaderB64: string; + keyB64: string; +}) => + inWorker() + ? ei._decryptMetadataJSON(r) + : sharedCryptoWorker().then((w) => w.decryptMetadataJSON(r)); diff --git a/web/packages/base/crypto/worker.ts b/web/packages/base/crypto/worker.ts index 7ce5b56775..26b3e481c6 100644 --- a/web/packages/base/crypto/worker.ts +++ b/web/packages/base/crypto/worker.ts @@ -5,7 +5,8 @@ import * as libsodium from "./libsodium"; /** * A web worker that exposes some of the functions defined in either the Ente - * specific layer (crypto/ente.ts) or the libsodium layer (crypto/libsodium.ts). + * specific layer (@/base/crypto) or the libsodium layer + * (@/base/crypto/libsodium.ts). * * See: [Note: Crypto code hierarchy]. * diff --git a/web/packages/base/session-store.ts b/web/packages/base/session-store.ts index 26782ce964..2493467348 100644 --- a/web/packages/base/session-store.ts +++ b/web/packages/base/session-store.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { decryptBox } from "./crypto/ente"; +import { decryptBox } from "./crypto"; import { toB64 } from "./crypto/libsodium"; /** diff --git a/web/packages/media/file-metadata.ts b/web/packages/media/file-metadata.ts index c4954f43aa..011200ff95 100644 --- a/web/packages/media/file-metadata.ts +++ b/web/packages/media/file-metadata.ts @@ -1,4 +1,4 @@ -import { decryptMetadataJSON, encryptMetadataJSON } from "@/base/crypto/ente"; +import { decryptMetadataJSON, encryptMetadataJSON } from "@/base/crypto"; import { authenticatedRequestHeaders, ensureOk } from "@/base/http"; import { apiURL } from "@/base/origins"; import { diff --git a/web/packages/new/photos/services/file-data.ts b/web/packages/new/photos/services/file-data.ts index df4ddbf9f4..3ea3fd999d 100644 --- a/web/packages/new/photos/services/file-data.ts +++ b/web/packages/new/photos/services/file-data.ts @@ -1,4 +1,4 @@ -import { encryptBlobB64 } from "@/base/crypto/ente"; +import { encryptBlobB64 } from "@/base/crypto"; import { authenticatedRequestHeaders, ensureOk } from "@/base/http"; import { apiURL } from "@/base/origins"; import type { EnteFile } from "@/new/photos/types/file"; diff --git a/web/packages/new/photos/services/ml/ml-data.ts b/web/packages/new/photos/services/ml/ml-data.ts index fb31bc9dcc..f0b2abfac6 100644 --- a/web/packages/new/photos/services/ml/ml-data.ts +++ b/web/packages/new/photos/services/ml/ml-data.ts @@ -1,4 +1,4 @@ -import { decryptBlob } from "@/base/crypto/ente"; +import { decryptBlob } from "@/base/crypto"; import log from "@/base/log"; import type { EnteFile } from "@/new/photos/types/file"; import { nullToUndefined } from "@/utils/transform"; diff --git a/web/packages/new/photos/services/user-entity.ts b/web/packages/new/photos/services/user-entity.ts index 7ea981df09..297429ce10 100644 --- a/web/packages/new/photos/services/user-entity.ts +++ b/web/packages/new/photos/services/user-entity.ts @@ -1,4 +1,4 @@ -import { decryptBlob, decryptBoxB64 } from "@/base/crypto/ente"; +import { decryptBlob, decryptBoxB64 } from "@/base/crypto"; import { authenticatedRequestHeaders, ensureOk, HTTPError } from "@/base/http"; import { getKV, getKVN, setKV } from "@/base/kv"; import { apiURL } from "@/base/origins"; From c0e48c7ada333a398fa139e4ec59cac7a720e9a2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 Aug 2024 11:49:57 +0530 Subject: [PATCH 3/9] keygen --- web/packages/base/crypto/index.ts | 7 +++++++ web/packages/base/crypto/libsodium.ts | 11 +++++++++++ 2 files changed, 18 insertions(+) diff --git a/web/packages/base/crypto/index.ts b/web/packages/base/crypto/index.ts index c1f09516eb..4a43b1ba15 100644 --- a/web/packages/base/crypto/index.ts +++ b/web/packages/base/crypto/index.ts @@ -47,6 +47,7 @@ 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"; @@ -85,6 +86,12 @@ const assertInWorker = (x: T): T => { return x; }; +/** + * Generate a new randomly generated 256-bit key suitable for use with the *Box + * encryption functions. + */ +export const generateBoxKey = libsodium.generateBoxKey; + /** * Encrypt the given data, returning a box containing the encrypted data and a * randomly generated nonce that was used during encryption. diff --git a/web/packages/base/crypto/libsodium.ts b/web/packages/base/crypto/libsodium.ts index 6145970972..1efcda7676 100644 --- a/web/packages/base/crypto/libsodium.ts +++ b/web/packages/base/crypto/libsodium.ts @@ -128,6 +128,17 @@ export async function fromHex(input: string) { const bytes = async (bob: BytesOrB64) => typeof bob == "string" ? fromB64(bob) : bob; +/** + * Generate a key for use with the *Box encryption functions. + * + * This returns a new randomly generated 256-bit key suitable for being used + * with libsodium's secretbox APIs. + */ +export const generateBoxKey = async () => { + await sodium.ready; + return toB64(sodium.crypto_secretbox_keygen()); +}; + /** * Encrypt the given data using libsodium's secretbox APIs, using a randomly * generated nonce. From c39fcb39682e297f3ab1f9bd86533fc68b261df3 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 Aug 2024 11:59:26 +0530 Subject: [PATCH 4/9] Use --- web/packages/new/photos/services/user-entity.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/web/packages/new/photos/services/user-entity.ts b/web/packages/new/photos/services/user-entity.ts index 297429ce10..61ece151eb 100644 --- a/web/packages/new/photos/services/user-entity.ts +++ b/web/packages/new/photos/services/user-entity.ts @@ -1,4 +1,9 @@ -import { decryptBlob, decryptBoxB64 } from "@/base/crypto"; +import { + decryptBlob, + decryptBoxB64, + encryptBoxB64, + generateBoxKey, +} from "@/base/crypto"; import { authenticatedRequestHeaders, ensureOk, HTTPError } from "@/base/http"; import { getKV, getKVN, setKV } from "@/base/kv"; import { apiURL } from "@/base/origins"; @@ -229,6 +234,13 @@ const saveRemoteUserEntityKey = ( entityKey: RemoteUserEntityKey, ) => setKV(entityKeyKey(type), JSON.stringify(entityKey)); +/** + * Generate a new entity key and return it after encrypting it using the user's + * master key. + */ +const generateEncryptedEntityKey = async () => + encryptBoxB64(await generateBoxKey(), await masterKeyFromSession()); + /** * Decrypt an encrypted entity key using the user's master key. */ From 3f9a7e08a14368ecf785e5abb65040c06db67a82 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 Aug 2024 15:36:30 +0530 Subject: [PATCH 5/9] Better state during initial load --- web/packages/new/photos/services/ml/index.ts | 4 +++- web/packages/new/photos/services/ml/worker.ts | 5 +++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts index 07b8afe5cd..9aa2c4338c 100644 --- a/web/packages/new/photos/services/ml/index.ts +++ b/web/packages/new/photos/services/ml/index.ts @@ -490,8 +490,10 @@ const getMLStatus = async (): Promise => { const state = await (await worker()).state; if (state == "indexing" || state == "fetching") { phase = state; + } else if (state == "init" || indexableCount > 0) { + phase = "scheduled"; } else { - phase = indexableCount > 0 ? "scheduled" : "done"; + phase = "done"; } return { diff --git a/web/packages/new/photos/services/ml/worker.ts b/web/packages/new/photos/services/ml/worker.ts index 0823a5806a..d6e2e17a43 100644 --- a/web/packages/new/photos/services/ml/worker.ts +++ b/web/packages/new/photos/services/ml/worker.ts @@ -43,6 +43,7 @@ import type { CLIPMatches, MLWorkerDelegate } from "./worker-types"; /** * A rough hint at what the worker is up to. * + * - "init": Worker has been created but hasn't done anything yet. * - "idle": Not doing anything * - "tick": Transitioning to a new state * - "indexing": Indexing @@ -52,7 +53,7 @@ import type { CLIPMatches, MLWorkerDelegate } from "./worker-types"; * data for more than 50% of the files that we requested from it in the last * fetch during indexing. */ -export type WorkerState = "idle" | "tick" | "indexing" | "fetching"; +export type WorkerState = "init" | "idle" | "tick" | "indexing" | "fetching"; const idleDurationStart = 5; /* 5 seconds */ const idleDurationMax = 16 * 60; /* 16 minutes */ @@ -92,7 +93,7 @@ interface IndexableItem { */ export class MLWorker { /** The last known state of the worker. */ - public state: WorkerState = "idle"; + public state: WorkerState = "init"; private electron: ElectronMLWorker | undefined; private delegate: MLWorkerDelegate | undefined; From e6f26e62ae76d91581443864f85d3a997b6ddd96 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 Aug 2024 15:40:57 +0530 Subject: [PATCH 6/9] Fix jumping --- web/packages/new/photos/services/ml/worker.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/packages/new/photos/services/ml/worker.ts b/web/packages/new/photos/services/ml/worker.ts index d6e2e17a43..bbec976a34 100644 --- a/web/packages/new/photos/services/ml/worker.ts +++ b/web/packages/new/photos/services/ml/worker.ts @@ -203,7 +203,12 @@ export class MLWorker { const liveQ = this.liveQ; this.liveQ = []; - this.state = "indexing"; + + // Retain the previous state if it was one of the indexing states. This + // prevents jumping between "fetching" and "indexing" being shown in the + // UI during the initial load. + if (this.state != "fetching" && this.state != "indexing") + this.state = "indexing"; // Use the liveQ if present, otherwise get the next batch to backfill. const items = liveQ.length ? liveQ : await this.backfillQ(); From d6151a89e8cb1a4e28989de9ca0286f2c27243e2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 Aug 2024 15:46:52 +0530 Subject: [PATCH 7/9] Fix --- web/packages/new/photos/services/ml/worker.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/packages/new/photos/services/ml/worker.ts b/web/packages/new/photos/services/ml/worker.ts index bbec976a34..e59e0f8731 100644 --- a/web/packages/new/photos/services/ml/worker.ts +++ b/web/packages/new/photos/services/ml/worker.ts @@ -139,7 +139,7 @@ export class MLWorker { /** Invoked in response to external events. */ private wakeUp() { - if (this.state == "idle") { + if (this.state == "init" || this.state == "idle") { // We are currently paused. Get back to work. if (this.idleTimeout) clearTimeout(this.idleTimeout); this.idleTimeout = undefined; From 63d65a4311b9b2fc4258cd73e63d78bd25481c3d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 Aug 2024 15:55:07 +0530 Subject: [PATCH 8/9] Expose decryptBlob to main thread --- web/packages/base/crypto/index.ts | 4 +++- web/packages/base/crypto/worker.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/web/packages/base/crypto/index.ts b/web/packages/base/crypto/index.ts index 4a43b1ba15..386d1ac7da 100644 --- a/web/packages/base/crypto/index.ts +++ b/web/packages/base/crypto/index.ts @@ -205,7 +205,9 @@ export const decryptBoxB64 = (box: EncryptedBox, key: BytesOrB64) => * {@link encryptBlobB64}. */ export const decryptBlob = (blob: EncryptedBlob, key: BytesOrB64) => - assertInWorker(ei._decryptBlob(blob, key)); + inWorker() + ? ei._decryptBlob(blob, key) + : sharedCryptoWorker().then((w) => w.decryptBlob(blob, key)); /** * A variant of {@link decryptBlob} that returns the result as a base64 string. diff --git a/web/packages/base/crypto/worker.ts b/web/packages/base/crypto/worker.ts index 26b3e481c6..82f2d47624 100644 --- a/web/packages/base/crypto/worker.ts +++ b/web/packages/base/crypto/worker.ts @@ -19,6 +19,7 @@ export class CryptoWorker { encryptMetadataJSON = ei._encryptMetadataJSON; decryptBox = ei._decryptBox; decryptBoxB64 = ei._decryptBoxB64; + decryptBlob = ei._decryptBlob; decryptBlobB64 = ei._decryptBlobB64; decryptThumbnail = ei._decryptThumbnail; decryptMetadataJSON_New = ei._decryptMetadataJSON_New; From 0a35291084ebe88a4a5f56667d56642c3bfec140 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 Aug 2024 21:16:00 +0530 Subject: [PATCH 9/9] Lint fix --- web/packages/new/photos/services/user-entity.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/packages/new/photos/services/user-entity.ts b/web/packages/new/photos/services/user-entity.ts index 61ece151eb..7e26726dd5 100644 --- a/web/packages/new/photos/services/user-entity.ts +++ b/web/packages/new/photos/services/user-entity.ts @@ -238,7 +238,8 @@ const saveRemoteUserEntityKey = ( * Generate a new entity key and return it after encrypting it using the user's * master key. */ -const generateEncryptedEntityKey = async () => +// TODO: Temporary export to silence lint +export const generateEncryptedEntityKey = async () => encryptBoxB64(await generateBoxKey(), await masterKeyFromSession()); /**