diff --git a/src/crypto/box.ts b/src/crypto/box.ts index 484152b..d7c57a5 100644 --- a/src/crypto/box.ts +++ b/src/crypto/box.ts @@ -1,17 +1,19 @@ -// Stub: see the README "Development workflow" section for TDD policy. +import sodium from "libsodium-wrappers-sumo"; +// XSalsa20-Poly1305 secretbox decryption. Throws on authentication failure +// (tampered ciphertext, wrong key, wrong nonce). Used pervasively by Ente +// for sealed key material and metadata. export const decryptBox = ( - _ciphertext: Uint8Array, - _nonce: Uint8Array, - _key: Uint8Array, -): Uint8Array => { - throw new Error("crypto.decryptBox not implemented"); -}; + ciphertext: Uint8Array, + nonce: Uint8Array, + key: Uint8Array, +): Uint8Array => sodium.crypto_secretbox_open_easy(ciphertext, nonce, key); +// Anonymous sealed-box (X25519 + XSalsa20-Poly1305) decryption. Used to +// recover the auth token returned by login. The recipient (us) needs both +// halves of the keypair; sealed-box is anonymous on the sender side. export const decryptSealed = ( - _ciphertext: Uint8Array, - _publicKey: Uint8Array, - _secretKey: Uint8Array, -): Uint8Array => { - throw new Error("crypto.decryptSealed not implemented"); -}; + ciphertext: Uint8Array, + publicKey: Uint8Array, + secretKey: Uint8Array, +): Uint8Array => sodium.crypto_box_seal_open(ciphertext, publicKey, secretKey); diff --git a/src/crypto/encoding.ts b/src/crypto/encoding.ts index 85cc6ef..6fa886c 100644 --- a/src/crypto/encoding.ts +++ b/src/crypto/encoding.ts @@ -1,13 +1,31 @@ -// Stub: see the README "Development workflow" section for TDD policy. +import sodium from "libsodium-wrappers-sumo"; -export const fromBase64 = (_s: string): Uint8Array => { - throw new Error("crypto.fromBase64 not implemented"); -}; +export const toBase64 = (b: Uint8Array): string => + sodium.to_base64(b, sodium.base64_variants.ORIGINAL); -export const toBase64 = (_b: Uint8Array): string => { - throw new Error("crypto.toBase64 not implemented"); -}; +export const toBase64URL = (b: Uint8Array): string => + sodium.to_base64(b, sodium.base64_variants.URLSAFE_NO_PADDING); -export const toBase64URL = (_b: Uint8Array): string => { - throw new Error("crypto.toBase64URL not implemented"); +// Ente uses standard base64 for most fields, URL-safe (with padding stripped) +// for the auth token. Rather than make callers specify, fromBase64 accepts +// any of the four variants libsodium understands and returns the bytes. +const VARIANTS = [ + sodium.base64_variants.ORIGINAL, + sodium.base64_variants.ORIGINAL_NO_PADDING, + sodium.base64_variants.URLSAFE, + sodium.base64_variants.URLSAFE_NO_PADDING, +] as const; + +export const fromBase64 = (s: string): Uint8Array => { + let lastError: unknown; + for (const variant of VARIANTS) { + try { + return sodium.from_base64(s, variant); + } catch (err) { + lastError = err; + } + } + throw lastError instanceof Error + ? lastError + : new Error("invalid base64 input"); }; diff --git a/src/crypto/kdf.ts b/src/crypto/kdf.ts index 3c39487..7ac385f 100644 --- a/src/crypto/kdf.ts +++ b/src/crypto/kdf.ts @@ -1,14 +1,25 @@ -// Stub: see the README "Development workflow" section for TDD policy. +import sodium from "libsodium-wrappers-sumo"; +// Argon2id over (password, salt, opsLimit, memLimit) producing a 32-byte +// Key Encryption Key. Parameters come from the server; quack passes them +// straight through. memLimit is in bytes, opsLimit is the iteration count. export const deriveKEK = async ( - _password: string, - _salt: Uint8Array, - _opsLimit: number, - _memLimit: number, -): Promise => { - throw new Error("crypto.deriveKEK not implemented"); -}; + password: string, + salt: Uint8Array, + opsLimit: number, + memLimit: number, +): Promise => + sodium.crypto_pwhash( + 32, + password, + salt, + opsLimit, + memLimit, + sodium.crypto_pwhash_ALG_ARGON2ID13, + ); -export const deriveLoginSubkey = (_kek: Uint8Array): Uint8Array => { - throw new Error("crypto.deriveLoginSubkey not implemented"); -}; +// BLAKE2b-based KDF from the KEK. Subkey id 1 and context "loginctx" are +// fixed by the upstream Ente clients; the first 16 bytes of the 32-byte +// output are used as the SRP password. Any deviation breaks login. +export const deriveLoginSubkey = (kek: Uint8Array): Uint8Array => + sodium.crypto_kdf_derive_from_key(32, 1, "loginctx", kek).slice(0, 16); diff --git a/src/crypto/sodium.ts b/src/crypto/sodium.ts index f0c1c6b..1990ace 100644 --- a/src/crypto/sodium.ts +++ b/src/crypto/sodium.ts @@ -1,6 +1,12 @@ -// Stub: see the README "Development workflow" section for TDD policy. -// This module's real implementation lands in a follow-up commit. +import sodium from "libsodium-wrappers-sumo"; +// libsodium ships as WebAssembly and loads asynchronously. The `sodium.ready` +// promise resolves once the WASM module is instantiated and the bindings are +// safe to call. After it resolves, all subsequent calls are synchronous. +// +// `init` is the single front door for that. Code that needs crypto awaits +// init() once before doing anything. Repeat calls await the same promise, +// so they are effectively free after the first. export const init = async (): Promise => { - throw new Error("crypto.init not implemented"); + await sodium.ready; }; diff --git a/src/crypto/stream.ts b/src/crypto/stream.ts index 477e8af..1197f9c 100644 --- a/src/crypto/stream.ts +++ b/src/crypto/stream.ts @@ -1,22 +1,38 @@ -// Stub: see the README "Development workflow" section for TDD policy. +import sodium from "libsodium-wrappers-sumo"; +// Plaintext chunk size used by Ente for file content streams. Hard-coded by +// the server; clients must match. export const STREAM_CHUNK_SIZE = 4 * 1024 * 1024; + +// Per-chunk overhead added by libsodium's secretstream construction: +// 16 bytes of Poly1305 tag plus 1 byte of secretstream tag. export const STREAM_CHUNK_OVERHEAD = 17; -export interface StreamPullState { - readonly _opaque: unique symbol; -} +// Opaque handle to libsodium's secretstream pull state. Threaded through +// successive pullStreamChunk calls. +export type StreamPullState = sodium.StateAddress; +// Initialise a pull stream from the per-file decryption header and the +// per-file key. export const initStreamPull = ( - _header: Uint8Array, - _key: Uint8Array, -): StreamPullState => { - throw new Error("crypto.initStreamPull not implemented"); -}; + header: Uint8Array, + key: Uint8Array, +): StreamPullState => + sodium.crypto_secretstream_xchacha20poly1305_init_pull(header, key); +// Decrypt one ciphertext chunk. Returns the plaintext and the secretstream +// tag (0=MESSAGE, 1=PUSH, 2=REKEY, 3=FINAL). The caller should verify the +// stream ended on TAG_FINAL to detect truncation. export const pullStreamChunk = ( - _state: StreamPullState, - _ciphertext: Uint8Array, + state: StreamPullState, + ciphertext: Uint8Array, ): { plaintext: Uint8Array; tag: number } => { - throw new Error("crypto.pullStreamChunk not implemented"); + const result = sodium.crypto_secretstream_xchacha20poly1305_pull( + state, + ciphertext, + ); + if (result === false) { + throw new Error("secretstream chunk authentication failed"); + } + return { plaintext: result.message, tag: result.tag }; };