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:
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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<Uint8Array> => {
|
||||
throw new Error("crypto.deriveKEK not implemented");
|
||||
};
|
||||
password: string,
|
||||
salt: Uint8Array,
|
||||
opsLimit: number,
|
||||
memLimit: number,
|
||||
): Promise<Uint8Array> =>
|
||||
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);
|
||||
|
||||
@@ -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<void> => {
|
||||
throw new Error("crypto.init not implemented");
|
||||
await sodium.ready;
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user