release(v4.5.0): browser-side encrypted storage + multi-factor unlock
Adds the foundations Prism's web client (and any future browser-based Shade app) needs: at-rest-encrypted IndexedDB storage that mirrors the SQLite backend byte-for-byte at the AAD/nonce level, browser-safe subpath imports so Vite/webpack/esbuild stop hitting bun:sqlite, and KeyManager support for argon2id and N-factor composite unlock. @shade/storage-encrypted - EncryptedIndexedDBStorage (subpath: /idb) — full StorageProvider using one object store per _enc table; reuses aeadSeal/aeadOpen + row-codec sealers so a row sealed under the SQLite or Postgres backend decrypts under IDB given the same KeyManager. bumpPeerIdentityVersion is atomic under one IDB transaction. - KeyManager argon2id source — memory-hard KDF for low-entropy secrets (PINs). Backed by @noble/hashes/argon2 (already a transitive dep). DEFAULT_ARGON2ID exported (m=64 MiB, t=3, p=1). - KeyManager composite source — HKDF-combine N sub-sources into one master. Every source mandatory; order significant by design; composite-of-composite rejected; optional info string for app-level domain separation. - Subpath exports (/crypto, /sqlite, /postgres, /idb) plus a `browser` condition on the default import that resolves to a barrel excluding the Bun- and Postgres-specific entries. Browser bundles no longer pull bun:sqlite transitively. Tests - 73 tests in shade-storage-encrypted (was 31). New coverage: argon2id determinism + reject paths, composite same-factors → same master, wrong-PIN/passphrase/order-swap → different master, info domain separation, all 28 StorageProvider methods on EncryptedIndexedDBStorage, fingerprint-mismatch rejection, and cross-impl roundtrip with EncryptedSQLiteStorage proving the AAD/ nonce derivation is implementation-agnostic. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import { scryptAsync } from '@noble/hashes/scrypt.js';
|
||||
import { argon2idAsync } from '@noble/hashes/argon2.js';
|
||||
import { hkdf } from '@noble/hashes/hkdf.js';
|
||||
import { sha256 } from '@noble/hashes/sha2.js';
|
||||
|
||||
@@ -45,6 +46,50 @@ export async function deriveMasterKey(
|
||||
return scryptAsync(TEXT.encode(passphrase.normalize('NFKC')), salt, params);
|
||||
}
|
||||
|
||||
/** Argon2id parameters — memory-hard KDF preferred for low-entropy secrets (PINs). */
|
||||
export interface Argon2idParams {
|
||||
/** Memory cost in KiB. */
|
||||
m: number;
|
||||
/** Time cost (iterations). */
|
||||
t: number;
|
||||
/** Parallelism. */
|
||||
p: number;
|
||||
/** Output length in bytes. */
|
||||
dkLen: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default: m=64 MiB, t=3, p=1, 32-byte output. Tuned for ~250–400 ms on a
|
||||
* modern Chromium / Firefox / Safari laptop. RFC 9106 "second recommended"
|
||||
* profile shrunk to a browser-friendly memory footprint — strong enough for
|
||||
* 4–6 digit PINs as a defense-in-depth factor on top of a passphrase.
|
||||
*/
|
||||
export const DEFAULT_ARGON2ID: Argon2idParams = { m: 64 * 1024, t: 3, p: 1, dkLen: 32 };
|
||||
|
||||
/**
|
||||
* Derive a 32-byte master key from a low-entropy secret + salt using
|
||||
* argon2id. Salt MUST be persisted alongside the DB (16-byte random).
|
||||
*/
|
||||
export async function deriveMasterKeyArgon2id(
|
||||
secret: string | Uint8Array,
|
||||
salt: Uint8Array,
|
||||
params: Argon2idParams = DEFAULT_ARGON2ID,
|
||||
): Promise<Uint8Array> {
|
||||
if (typeof secret === 'string' ? secret.length === 0 : secret.length === 0) {
|
||||
throw new Error('argon2id secret must be non-empty');
|
||||
}
|
||||
if (salt.length < 16) {
|
||||
throw new Error('salt must be at least 16 bytes');
|
||||
}
|
||||
const password = typeof secret === 'string' ? TEXT.encode(secret.normalize('NFKC')) : secret;
|
||||
return argon2idAsync(password, salt, {
|
||||
m: params.m,
|
||||
t: params.t,
|
||||
p: params.p,
|
||||
dkLen: params.dkLen,
|
||||
});
|
||||
}
|
||||
|
||||
/** HKDF-SHA-256 with explicit info string. */
|
||||
export function hkdfDerive(ikm: Uint8Array, info: string, length = 32, salt?: Uint8Array): Uint8Array {
|
||||
return hkdf(sha256, ikm, salt, TEXT.encode(info), length);
|
||||
|
||||
Reference in New Issue
Block a user