Phase 2 green: implement crypto primitives

Each stub is replaced with a thin wrapper over libsodium-wrappers-sumo:

  * init() awaits sodium.ready
  * toBase64 / toBase64URL / fromBase64 use sodium's base64 variants;
    fromBase64 tries all four (standard, standard-no-pad, URL-safe,
    URL-safe-no-pad) so callers don't have to know which form Ente
    delivered
  * deriveKEK is sodium.crypto_pwhash with ALG_ARGON2ID13 and 32-byte
    output
  * deriveLoginSubkey is sodium.crypto_kdf_derive_from_key(32, 1,
    'loginctx', kek).slice(0, 16) per the upstream Ente clients
  * decryptBox is sodium.crypto_secretbox_open_easy
  * decryptSealed is sodium.crypto_box_seal_open
  * initStreamPull / pullStreamChunk wrap the secretstream pull API,
    throwing on authentication failure rather than returning false

All 32 tests pass; make check is green.
This commit is contained in:
2026-05-09 12:44:59 -07:00
parent 676d42c5eb
commit 8aecf977e9
5 changed files with 101 additions and 48 deletions

View File

@@ -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 };
};