/** * 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); }); });