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 = (
|
export const decryptBox = (
|
||||||
_ciphertext: Uint8Array,
|
ciphertext: Uint8Array,
|
||||||
_nonce: Uint8Array,
|
nonce: Uint8Array,
|
||||||
_key: Uint8Array,
|
key: Uint8Array,
|
||||||
): Uint8Array => {
|
): Uint8Array => sodium.crypto_secretbox_open_easy(ciphertext, nonce, key);
|
||||||
throw new Error("crypto.decryptBox not implemented");
|
|
||||||
};
|
|
||||||
|
|
||||||
|
// 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 = (
|
export const decryptSealed = (
|
||||||
_ciphertext: Uint8Array,
|
ciphertext: Uint8Array,
|
||||||
_publicKey: Uint8Array,
|
publicKey: Uint8Array,
|
||||||
_secretKey: Uint8Array,
|
secretKey: Uint8Array,
|
||||||
): Uint8Array => {
|
): Uint8Array => sodium.crypto_box_seal_open(ciphertext, publicKey, secretKey);
|
||||||
throw new Error("crypto.decryptSealed not implemented");
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -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 => {
|
export const toBase64 = (b: Uint8Array): string =>
|
||||||
throw new Error("crypto.fromBase64 not implemented");
|
sodium.to_base64(b, sodium.base64_variants.ORIGINAL);
|
||||||
};
|
|
||||||
|
|
||||||
export const toBase64 = (_b: Uint8Array): string => {
|
export const toBase64URL = (b: Uint8Array): string =>
|
||||||
throw new Error("crypto.toBase64 not implemented");
|
sodium.to_base64(b, sodium.base64_variants.URLSAFE_NO_PADDING);
|
||||||
};
|
|
||||||
|
|
||||||
export const toBase64URL = (_b: Uint8Array): string => {
|
// Ente uses standard base64 for most fields, URL-safe (with padding stripped)
|
||||||
throw new Error("crypto.toBase64URL not implemented");
|
// 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 (
|
export const deriveKEK = async (
|
||||||
_password: string,
|
password: string,
|
||||||
_salt: Uint8Array,
|
salt: Uint8Array,
|
||||||
_opsLimit: number,
|
opsLimit: number,
|
||||||
_memLimit: number,
|
memLimit: number,
|
||||||
): Promise<Uint8Array> => {
|
): Promise<Uint8Array> =>
|
||||||
throw new Error("crypto.deriveKEK not implemented");
|
sodium.crypto_pwhash(
|
||||||
};
|
32,
|
||||||
|
password,
|
||||||
|
salt,
|
||||||
|
opsLimit,
|
||||||
|
memLimit,
|
||||||
|
sodium.crypto_pwhash_ALG_ARGON2ID13,
|
||||||
|
);
|
||||||
|
|
||||||
export const deriveLoginSubkey = (_kek: Uint8Array): Uint8Array => {
|
// BLAKE2b-based KDF from the KEK. Subkey id 1 and context "loginctx" are
|
||||||
throw new Error("crypto.deriveLoginSubkey not implemented");
|
// 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.
|
import sodium from "libsodium-wrappers-sumo";
|
||||||
// This module's real implementation lands in a follow-up commit.
|
|
||||||
|
|
||||||
|
// 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> => {
|
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;
|
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 const STREAM_CHUNK_OVERHEAD = 17;
|
||||||
|
|
||||||
export interface StreamPullState {
|
// Opaque handle to libsodium's secretstream pull state. Threaded through
|
||||||
readonly _opaque: unique symbol;
|
// 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 = (
|
export const initStreamPull = (
|
||||||
_header: Uint8Array,
|
header: Uint8Array,
|
||||||
_key: Uint8Array,
|
key: Uint8Array,
|
||||||
): StreamPullState => {
|
): StreamPullState =>
|
||||||
throw new Error("crypto.initStreamPull not implemented");
|
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 = (
|
export const pullStreamChunk = (
|
||||||
_state: StreamPullState,
|
state: StreamPullState,
|
||||||
_ciphertext: Uint8Array,
|
ciphertext: Uint8Array,
|
||||||
): { plaintext: Uint8Array; tag: number } => {
|
): { 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