Tests for the entire crypto/ public surface, written against the API
shape declared in the README. The accompanying src/crypto/ modules are
stubs that throw 'not implemented' so the test files compile and tests
fail with clear errors rather than module-not-found.
Tests cover:
* init() resolves and is idempotent
* fromBase64 / toBase64 / toBase64URL round-trips, including URL-safe
input with stripped padding (the form Ente uses for auth tokens)
* deriveKEK matches sodium.crypto_pwhash with Argon2id parameters
* deriveLoginSubkey matches sodium.crypto_kdf_derive_from_key with
subkey id 1 and ctx 'loginctx', truncated to 16 bytes
* decryptBox round-trips, rejects tampering, wrong key, wrong nonce
* decryptSealed round-trips, rejects wrong keypair and tampering
* Secretstream pull decrypts multi-chunk streams in order, exposes
per-chunk tags, rejects tampering, wrong key, and out-of-order chunks
* Constants STREAM_CHUNK_SIZE (4 MiB) and STREAM_CHUNK_OVERHEAD (17)
Tests are commented to serve as the canonical API documentation per the
README development workflow policy. Verified: 29 tests fail (red), 3
trivial constant tests pass; lint and fmt-check are green.
eslint.config.mjs is updated to honour the leading-underscore convention
for intentionally unused parameters (the stubs).
137 lines
4.9 KiB
TypeScript
137 lines
4.9 KiB
TypeScript
/**
|
|
* Tests for `crypto.deriveKEK` and `crypto.deriveLoginSubkey`.
|
|
*
|
|
* These two functions implement the password-side of Ente's authentication
|
|
* flow:
|
|
*
|
|
* 1. The user types a password.
|
|
* 2. `deriveKEK` runs Argon2id over the password using a server-issued
|
|
* 16-byte salt and server-issued mem/ops parameters. The result is a
|
|
* 32-byte Key Encryption Key (KEK).
|
|
* 3. `deriveLoginSubkey` runs libsodium's BLAKE2b-based KDF over the KEK
|
|
* with a fixed subkey id and context, takes the first 16 bytes, and
|
|
* uses that as the password input to SRP-6a. The user's password
|
|
* itself never touches the network.
|
|
*
|
|
* Both derivations must match the upstream Ente implementations bit for
|
|
* bit. If they drift, the SRP handshake fails and the user cannot log in.
|
|
* The tests below pin the exact algorithms and parameters.
|
|
*/
|
|
|
|
import sodium from "libsodium-wrappers-sumo";
|
|
import { beforeAll, describe, expect, it } from "vitest";
|
|
import { deriveKEK, deriveLoginSubkey, init } from "../../src/crypto/index.js";
|
|
|
|
describe("crypto.deriveKEK (Argon2id)", () => {
|
|
beforeAll(async () => {
|
|
await init();
|
|
await sodium.ready;
|
|
});
|
|
|
|
/**
|
|
* Cheap parameters used so the test suite stays under the 30-second
|
|
* budget. The real production parameters Ente uses are larger
|
|
* (memLimit up to 1 GiB, opsLimit 3-16). The algorithm is the same
|
|
* regardless of parameters.
|
|
*/
|
|
const TEST_OPS = 2;
|
|
const TEST_MEM = 64 * 1024 * 1024; // 64 MiB
|
|
|
|
it("matches sodium.crypto_pwhash with Argon2id, 32-byte output", async () => {
|
|
// The contract: deriveKEK is exactly
|
|
// crypto_pwhash(32, password, salt, opsLimit, memLimit, ARGON2ID13)
|
|
// No wrapper-side normalisation, no parameter mangling, nothing
|
|
// implicit. We compute the expected value with sodium directly and
|
|
// assert byte equality.
|
|
const password = "correct horse battery staple";
|
|
const salt = new Uint8Array(sodium.crypto_pwhash_SALTBYTES);
|
|
salt.fill(0x42); // any salt works, we just need it to be deterministic
|
|
|
|
const expected = sodium.crypto_pwhash(
|
|
32,
|
|
password,
|
|
salt,
|
|
TEST_OPS,
|
|
TEST_MEM,
|
|
sodium.crypto_pwhash_ALG_ARGON2ID13,
|
|
);
|
|
|
|
const got = await deriveKEK(password, salt, TEST_OPS, TEST_MEM);
|
|
expect(got).toEqual(expected);
|
|
expect(got.length).toBe(32);
|
|
});
|
|
|
|
it("produces different keys for different passwords with the same salt", async () => {
|
|
const salt = new Uint8Array(sodium.crypto_pwhash_SALTBYTES);
|
|
salt.fill(0x01);
|
|
const a = await deriveKEK("alpha", salt, TEST_OPS, TEST_MEM);
|
|
const b = await deriveKEK("beta", salt, TEST_OPS, TEST_MEM);
|
|
expect(a).not.toEqual(b);
|
|
});
|
|
|
|
it("produces different keys for the same password with different salts", async () => {
|
|
const password = "same password";
|
|
const saltA = new Uint8Array(sodium.crypto_pwhash_SALTBYTES);
|
|
saltA.fill(0x01);
|
|
const saltB = new Uint8Array(sodium.crypto_pwhash_SALTBYTES);
|
|
saltB.fill(0x02);
|
|
const a = await deriveKEK(password, saltA, TEST_OPS, TEST_MEM);
|
|
const b = await deriveKEK(password, saltB, TEST_OPS, TEST_MEM);
|
|
expect(a).not.toEqual(b);
|
|
});
|
|
});
|
|
|
|
describe("crypto.deriveLoginSubkey", () => {
|
|
beforeAll(async () => {
|
|
await init();
|
|
await sodium.ready;
|
|
});
|
|
|
|
/**
|
|
* The exact derivation Ente uses, taken from the web client and the Go
|
|
* CLI:
|
|
*
|
|
* subkey = crypto_kdf_derive_from_key(
|
|
* outputLen = 32,
|
|
* subkeyId = 1,
|
|
* context = "loginctx",
|
|
* key = kek,
|
|
* ).slice(0, 16)
|
|
*
|
|
* The 32-byte output is truncated to 16 bytes; that 16-byte value is
|
|
* the password input to SRP-6a. Any deviation in subkey id, context,
|
|
* or truncation length breaks login.
|
|
*/
|
|
it("derives 16 bytes via KDF subkey 1, ctx 'loginctx'", () => {
|
|
const kek = new Uint8Array(32);
|
|
for (let i = 0; i < 32; i++) kek[i] = i; // 0x00..0x1f
|
|
|
|
const expected32 = sodium.crypto_kdf_derive_from_key(
|
|
32,
|
|
1,
|
|
"loginctx",
|
|
kek,
|
|
);
|
|
const expected16 = expected32.slice(0, 16);
|
|
|
|
const got = deriveLoginSubkey(kek);
|
|
expect(got.length).toBe(16);
|
|
expect(got).toEqual(expected16);
|
|
});
|
|
|
|
it("returns a different subkey for a different KEK", () => {
|
|
const kekA = new Uint8Array(32).fill(0x11);
|
|
const kekB = new Uint8Array(32).fill(0x22);
|
|
const a = deriveLoginSubkey(kekA);
|
|
const b = deriveLoginSubkey(kekB);
|
|
expect(a).not.toEqual(b);
|
|
});
|
|
|
|
it("is deterministic for a given KEK", () => {
|
|
const kek = new Uint8Array(32).fill(0x37);
|
|
const a = deriveLoginSubkey(kek);
|
|
const b = deriveLoginSubkey(kek);
|
|
expect(a).toEqual(b);
|
|
});
|
|
});
|