Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
# @suaveplan/crypto-vault - Cryptographic foundation for the Vaulted password-manager ecosystem: BIP39 mnemonics, Argon2id-based HD key derivation, two-secret key derivation (2SKD), SRP-6a authentication, hierarchical key tree (AUK -> personal keyset -> vault key -> item key), and SecureBuffer with eager zeroization.
|
||||
|
||||
## Summary
|
||||
|
||||
Cryptographic foundation for the Vaulted password-manager ecosystem.
|
||||
Provides BIP39 mnemonic generation, OWASP-grade Argon2id derivation,
|
||||
two-secret key derivation (2SKD: password + Secret Key + email → AUK),
|
||||
SRP-6a authentication primitives, and a hierarchical key tree
|
||||
(AUK → personal keyset → vault key → item key) — all held in `SecureBuffer`
|
||||
instances that zeroize on lock.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [argon2id](./src/argon2id/argon2id.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [argon2id](./src/argon2id/argon2id.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [bip39](./src/bip39/bip39.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [bip39](./src/bip39/bip39.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [codes](./src/errors/codes.md) — `codes.ts` is the canonical, frozen registry of every error code emitted by `@suaveplan/crypto-vault`. The registry exi…
|
||||
- [crypto-vault-error](./src/errors/crypto-vault-error.md) — `CryptoVaultError` is the umbrella error class for `@suaveplan/crypto-vault`. Every failure mode in the package — inval…
|
||||
- [handle](./src/handle/opaque-handle.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [opaque-handle](./src/handle/opaque-handle.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [hd-key](./src/hd-key/hd-key.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [hd-key](./src/hd-key/hd-key.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [key-hierarchy](./src/key-hierarchy/key-hierarchy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [key-hierarchy](./src/key-hierarchy/key-hierarchy.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [orchestration](./src/orchestration/orchestration.md) — High-level orchestration helpers that bundle keychain generation, password-AEAD encryption, signed public-key file prod…
|
||||
- [internals](./src/orchestration/internals.md) — Internal building blocks for the orchestration surface.
|
||||
- [orchestration](./src/orchestration/orchestration.md) — High-level orchestration helpers that bundle keychain generation, password-AEAD encryption, signed public-key file prod…
|
||||
- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [secure](./src/secure/secure-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [secure-buffer](./src/secure/secure-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [srp-client](./src/srp/srp-client.md) — `srp-client.ts` implements the client side of the SRP-6a zero-knowledge password-authenticated key exchange per RFC 505…
|
||||
- [srp-group](./src/srp/srp-group.md) — `srp-group.ts` exports the canonical 2048-bit MODP group from RFC 5054 Appendix A as `bigint` constants. The SRP-6a cli…
|
||||
- [twoskd](./src/twoskd/twoskd.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [twoskd](./src/twoskd/twoskd.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
<!-- end:auto-docs --> - [argon2id](./src/argon2id/argon2id.md) — <!-- begin:auto-docs --> _(no submodules)_- [bip39](./src/bip39/bip39.md) — <!-- begin:auto-docs --> _(no submodules)_- [bip39](./src/bip39/bip39.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — `codes.ts` is the canonical, frozen registry of every error code emitted by `@suaveplan/crypto-vault`. The registry exi…
|
||||
- [crypto-vault-error](./src/errors/crypto-vault-error.md) — `CryptoVaultError` is the umbrella error class for `@suaveplan/crypto-vault`. Every failure mode in the package — inval…
|
||||
- [handle](./src/handle/opaque-handle.md) — <!-- begin:auto-docs --> _(no submodules)_- [opaque-handle](./src/handle/opaque-handle.md) — <!-- begin:auto-docs --> _(no submodules)_- [hd-key](./src/hd-key/hd-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [hd-key](./src/hd-key/hd-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-hierarchy](./src/key-hierarchy/key-hierarchy.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-hierarchy](./src/key-hierarchy/key-hierarchy.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [secure](./src/secure/secure-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [secure-buffer](./src/secure/secure-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [srp-client](./src/srp/srp-client.md) — `srp-client.ts` implements the client side of the SRP-6a zero-knowledge password-authenticated key exchange per RFC 505…
|
||||
- [srp-group](./src/srp/srp-group.md) — `srp-group.ts` exports the canonical 2048-bit MODP group from RFC 5054 Appendix A as `bigint` constants. The SRP-6a cli…
|
||||
- [twoskd](./src/twoskd/twoskd.md) — <!-- begin:auto-docs --> _(no submodules)_- [twoskd](./src/twoskd/twoskd.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [argon2id](./src/argon2id/argon2id.md) — <!-- begin:auto-docs --> _(no submodules)_- [bip39](./src/bip39/bip39.md) — <!-- begin:auto-docs --> _(no submodules)_- [bip39](./src/bip39/bip39.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — `codes.ts` is the canonical, frozen registry of every error code emitted by `@suaveplan/crypto-vault`. The registry exi…
|
||||
- [crypto-vault-error](./src/errors/crypto-vault-error.md) — `CryptoVaultError` is the umbrella error class for `@suaveplan/crypto-vault`. Every failure mode in the package — inval…
|
||||
- [handle](./src/handle/opaque-handle.md) — <!-- begin:auto-docs --> _(no submodules)_- [opaque-handle](./src/handle/opaque-handle.md) — <!-- begin:auto-docs --> _(no submodules)_- [hd-key](./src/hd-key/hd-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [hd-key](./src/hd-key/hd-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-hierarchy](./src/key-hierarchy/key-hierarchy.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-hierarchy](./src/key-hierarchy/key-hierarchy.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [secure](./src/secure/secure-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [secure-buffer](./src/secure/secure-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [srp-client](./src/srp/srp-client.md) — `srp-client.ts` implements the client side of the SRP-6a zero-knowledge password-authenticated key exchange per RFC 505…
|
||||
- [srp-group](./src/srp/srp-group.md) — `srp-group.ts` exports the canonical 2048-bit MODP group from RFC 5054 Appendix A as `bigint` constants. The SRP-6a cli…
|
||||
- [twoskd](./src/twoskd/twoskd.md) — <!-- begin:auto-docs --> _(no submodules)_- [twoskd](./src/twoskd/twoskd.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [argon2id](./src/argon2id/argon2id.md) — <!-- begin:auto-docs --> _(no submodules)_- [bip39](./src/bip39/bip39.md) — <!-- begin:auto-docs --> _(no submodules)_- [bip39](./src/bip39/bip39.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — `codes.ts` is the canonical, frozen registry of every error code emitted by `@suaveplan/crypto-vault`. The registry exi…
|
||||
- [crypto-vault-error](./src/errors/crypto-vault-error.md) — `CryptoVaultError` is the umbrella error class for `@suaveplan/crypto-vault`. Every failure mode in the package — inval…
|
||||
- [handle](./src/handle/opaque-handle.md) — <!-- begin:auto-docs --> _(no submodules)_- [opaque-handle](./src/handle/opaque-handle.md) — <!-- begin:auto-docs --> _(no submodules)_- [hd-key](./src/hd-key/hd-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [hd-key](./src/hd-key/hd-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-hierarchy](./src/key-hierarchy/key-hierarchy.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-hierarchy](./src/key-hierarchy/key-hierarchy.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [secure](./src/secure/secure-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [secure-buffer](./src/secure/secure-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [srp-client](./src/srp/srp-client.md) — `srp-client.ts` implements the client side of the SRP-6a zero-knowledge password-authenticated key exchange per RFC 505…
|
||||
- [srp-group](./src/srp/srp-group.md) — `srp-group.ts` exports the canonical 2048-bit MODP group from RFC 5054 Appendix A as `bigint` constants. The SRP-6a cli…
|
||||
- [twoskd](./src/twoskd/twoskd.md) — <!-- begin:auto-docs --> _(no submodules)_- [twoskd](./src/twoskd/twoskd.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_- [argon2id](./src/argon2id/argon2id.md) — <!-- begin:auto-docs --> _(no submodules)_- [bip39](./src/bip39/bip39.md) — <!-- begin:auto-docs --> _(no submodules)_ - [bip39](./src/bip39/bip39.md) — <!-- begin:auto-docs --> _(no submodules)_- [codes](./src/errors/codes.md) — `codes.ts` is the canonical, frozen registry of every error code emitted by `@suaveplan/crypto-vault`. The registry exi…
|
||||
- [crypto-vault-error](./src/errors/crypto-vault-error.md) — `CryptoVaultError` is the umbrella error class for `@suaveplan/crypto-vault`. Every failure mode in the package — inval…
|
||||
- [handle](./src/handle/opaque-handle.md) — <!-- begin:auto-docs --> _(no submodules)_ - [opaque-handle](./src/handle/opaque-handle.md) — <!-- begin:auto-docs --> _(no submodules)_- [hd-key](./src/hd-key/hd-key.md) — <!-- begin:auto-docs --> _(no submodules)_ - [hd-key](./src/hd-key/hd-key.md) — <!-- begin:auto-docs --> _(no submodules)_- [key-hierarchy](./src/key-hierarchy/key-hierarchy.md) — <!-- begin:auto-docs --> _(no submodules)_ - [key-hierarchy](./src/key-hierarchy/key-hierarchy.md) — <!-- begin:auto-docs --> _(no submodules)_- [otel](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_ - [instruments](./src/otel/instruments.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [secure](./src/secure/secure-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_ - [secure-buffer](./src/secure/secure-buffer.md) — <!-- begin:auto-docs --> _(no submodules)_- [srp-client](./src/srp/srp-client.md) — `srp-client.ts` implements the client side of the SRP-6a zero-knowledge password-authenticated key exchange per RFC 505…
|
||||
- [srp-group](./src/srp/srp-group.md) — `srp-group.ts` exports the canonical 2048-bit MODP group from RFC 5054 Appendix A as `bigint` constants. The SRP-6a cli…
|
||||
- [twoskd](./src/twoskd/twoskd.md) — <!-- begin:auto-docs --> _(no submodules)_ - [twoskd](./src/twoskd/twoskd.md) — <!-- begin:auto-docs --> _(no submodules)_- [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_ - [types](./src/types/types.md) — <!-- begin:auto-docs --> _(no submodules)_
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/crypto-vault
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```ts
|
||||
import {
|
||||
DEFAULT_ARGON2ID_PARAMS,
|
||||
deriveAuk,
|
||||
deriveItemKey,
|
||||
derivePersonalKeyset,
|
||||
deriveVaultKey,
|
||||
generateMnemonic,
|
||||
SecureBuffer,
|
||||
SrpClient,
|
||||
zeroizeAll,
|
||||
} from "@suaveplan/crypto-vault";
|
||||
|
||||
// 1. Generate a backup phrase.
|
||||
const mnemonic = generateMnemonic(128);
|
||||
|
||||
// 2. Derive the AUK from the user's secrets.
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const auk = await deriveAuk(
|
||||
"correct horse battery staple",
|
||||
"A3-7PMW82-XJZS9-NQDLV-EYKBR",
|
||||
"alice@example.com",
|
||||
salt,
|
||||
DEFAULT_ARGON2ID_PARAMS
|
||||
);
|
||||
|
||||
// 3. Fan out into the hierarchy.
|
||||
const personal = derivePersonalKeyset(auk);
|
||||
const vaultKey = deriveVaultKey(personal, "vault-1");
|
||||
const itemKey = deriveItemKey(vaultKey, "login-bank");
|
||||
|
||||
// 4. Authenticate via SRP-6a.
|
||||
const client = new SrpClient();
|
||||
const x = client.derivePrivateKey("alice@example.com", "pw", salt);
|
||||
const v = client.deriveVerifier(x);
|
||||
|
||||
// 5. Lock — sweep every live SecureBuffer.
|
||||
const wrapped = new SecureBuffer(itemKey);
|
||||
zeroizeAll();
|
||||
console.log(wrapped.isClosed); // true
|
||||
```
|
||||
|
||||
## Modules
|
||||
|
||||
- [`bip39/`](./src/bip39/bip39.md) — BIP39 mnemonic generation, conversion,
|
||||
and validation (English word list).
|
||||
- [`argon2id/`](./src/argon2id/argon2id.md) — OWASP-safe Argon2id derivation
|
||||
wrapper around `@suaveplan/crypto-hash`.
|
||||
- [`hd-key/`](./src/hd-key/hd-key.md) — BIP39 seed and BIP32-shaped HD key
|
||||
derivation.
|
||||
- [`twoskd/`](./src/twoskd/twoskd.md) — Two-Secret Key Derivation
|
||||
(`password ⊕ secretKey ⊕ email → AUK`).
|
||||
- [`key-hierarchy/`](./src/key-hierarchy/key-hierarchy.md) — HKDF-driven
|
||||
AUK → personal keyset → vault key → item key tree.
|
||||
- [`srp/srp-client`](./src/srp/srp-client.md) — RFC 5054 SRP-6a client.
|
||||
- [`srp/srp-group`](./src/srp/srp-group.md) — RFC 5054 2048-bit MODP group
|
||||
parameters.
|
||||
- [`secure/`](./src/secure/secure-buffer.md) — `SecureBuffer` envelope with
|
||||
eager zeroization, clone isolation, and global `zeroizeAll()`.
|
||||
- [`handle/`](./src/handle/opaque-handle.md) — `OpaqueKeyHandle` for the
|
||||
vault layer to hold key references without seeing raw bytes.
|
||||
- [`errors/codes`](./src/errors/codes.md) — frozen error-code registry.
|
||||
- [`errors/crypto-vault-error`](./src/errors/crypto-vault-error.md) —
|
||||
`CryptoVaultError` umbrella class.
|
||||
- [`schemas/`](./src/schemas/schemas.md) — Zod schemas for every public
|
||||
shape (`Argon2idParams`, `PersonalKeyset`, etc.).
|
||||
- [`types/`](./src/types/types.md) — shared types (`Argon2idParams`,
|
||||
`Auk`, `PersonalKeyset`, …).
|
||||
- [`otel/`](./src/otel/instruments.md) — lazy OpenTelemetry instrument set.
|
||||
|
||||
## Security model
|
||||
|
||||
- Plaintext key material lives only inside `SecureBuffer` instances; the
|
||||
buffers' `toString` and `toJSON` mask the bytes.
|
||||
- `zeroizeAll()` walks every reachable `SecureBuffer` via a `WeakRef`
|
||||
registry and overwrites its bytes with zeros.
|
||||
- Argon2id parameters below the OWASP minimums (memoryKB ≥ 64 MiB,
|
||||
iterations ≥ 3, parallelism ≥ 1) are rejected at runtime.
|
||||
- All MAC / proof comparisons are constant-time
|
||||
(`@suaveplan/crypto-utils`'s `constantTimeEqual`).
|
||||
- SRP-6a uses SHA-256 over the RFC 5054 2048-bit safe-prime group with
|
||||
full server-proof verification.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# Argon2id wrapper
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`argon2id.ts` is the OWASP-grade Argon2id derivation entry point for
|
||||
`@suaveplan/crypto-vault`. It wraps `@suaveplan/crypto-hash`'s primitive
|
||||
`argon2id` with two enterprise concerns the primitive itself does not
|
||||
enforce: an OWASP-safe parameter floor (memoryKB ≥ 64 MiB, iterations ≥ 3,
|
||||
parallelism ≥ 1) and a hard ceiling (memoryKB ≤ 1 GiB, outputLength ≤ 4 KiB)
|
||||
that prevents accidental denial-of-service from misconfigured callers.
|
||||
|
||||
## Features
|
||||
|
||||
- `assertSafeArgon2idParams(params)` — pure validator usable in tests, in
|
||||
config validation, and as a precondition for any Argon2id call.
|
||||
- `deriveKeyWithArgon2id(password, salt, params?, options?)` — full
|
||||
derivation pipeline with telemetry recording and structured errors.
|
||||
- `DEFAULT_ARGON2ID_PARAMS` — frozen `{ memoryKB: 65536, iterations: 3,
|
||||
parallelism: 4, outputLength: 32 }` constant.
|
||||
- `OWASP_MIN_*` and `ARGON2ID_MAX_*` exported constants for callers that
|
||||
build their own parameter UI.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DEFAULT_ARGON2ID_PARAMS,
|
||||
deriveKeyWithArgon2id,
|
||||
} from "@suaveplan/crypto-vault/argon2id/argon2id";
|
||||
|
||||
const password = new TextEncoder().encode("correct horse battery staple");
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = await deriveKeyWithArgon2id(password, salt, DEFAULT_ARGON2ID_PARAMS);
|
||||
console.log(key.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
assertSafeArgon2idParams,
|
||||
deriveKeyWithArgon2id,
|
||||
} from "@suaveplan/crypto-vault/argon2id/argon2id";
|
||||
import type { Argon2idParams } from "@suaveplan/crypto-vault/types";
|
||||
|
||||
// Stronger profile for highly sensitive accounts.
|
||||
const strict: Argon2idParams = {
|
||||
memoryKB: 131_072, // 128 MiB
|
||||
iterations: 4,
|
||||
parallelism: 4,
|
||||
outputLength: 64,
|
||||
};
|
||||
assertSafeArgon2idParams(strict); // throws if any cell breaks the bands
|
||||
|
||||
const password = new TextEncoder().encode("super-strong-passphrase");
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const key = await deriveKeyWithArgon2id(password, salt, strict, {
|
||||
clock: () => 0, // deterministic telemetry timestamps for tests
|
||||
});
|
||||
console.log(key.length); // 64
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `deriveKeyWithArgon2id(password, salt, params?, options?)`
|
||||
|
||||
- `password: Uint8Array` — must be non-empty.
|
||||
- `salt: Uint8Array` — must be non-empty (16+ bytes recommended).
|
||||
- `params: Argon2idParams = DEFAULT_ARGON2ID_PARAMS` — validated through
|
||||
`assertSafeArgon2idParams`.
|
||||
- `options.clock?: () => number` — wall-clock source for the duration
|
||||
histogram.
|
||||
|
||||
Returns `Promise<Uint8Array>` of length `params.outputLength`.
|
||||
|
||||
Throws `CryptoVaultError`:
|
||||
|
||||
- `ARGON2ID.INVALID_INPUT` for empty `password` or `salt`.
|
||||
- `ARGON2ID.UNSAFE_PARAMETERS` for parameters outside the OWASP / hard-cap
|
||||
bands.
|
||||
|
||||
### `assertSafeArgon2idParams(params)`
|
||||
|
||||
Throws `CryptoVaultError(ARGON2ID.UNSAFE_PARAMETERS)` when any of these
|
||||
conditions are violated:
|
||||
|
||||
- `memoryKB` integer in `[OWASP_MIN_MEMORY_KB, ARGON2ID_MAX_MEMORY_KB]`.
|
||||
- `iterations` integer ≥ `OWASP_MIN_ITERATIONS`.
|
||||
- `parallelism` integer ≥ `OWASP_MIN_PARALLELISM`.
|
||||
- `outputLength` integer in `(0, ARGON2ID_MAX_OUTPUT_LENGTH]`.
|
||||
|
||||
### Constants
|
||||
|
||||
- `OWASP_MIN_MEMORY_KB = 65_536`
|
||||
- `OWASP_MIN_ITERATIONS = 3`
|
||||
- `OWASP_MIN_PARALLELISM = 1`
|
||||
- `ARGON2ID_MAX_MEMORY_KB = 1_048_576`
|
||||
- `ARGON2ID_MAX_OUTPUT_LENGTH = 4_096`
|
||||
- `DEFAULT_ARGON2ID_PARAMS` — frozen object.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The underlying primitive is sync (noble's `@noble/hashes/argon2`); the
|
||||
wrapper exposes `Promise<Uint8Array>` to keep the public API stable
|
||||
against future native or worker-backed implementations.
|
||||
- The OWASP minimums encode the November 2024 password storage cheat sheet
|
||||
guidance for Argon2id (m=64MiB, t=3, p=1+).
|
||||
- The hard cap on memory cost (1 GiB) and output length (4 KiB) defends
|
||||
against caller bugs and denial-of-service-by-misconfiguration. A caller
|
||||
that legitimately needs more should open a follow-up change rather than
|
||||
bypass the guard.
|
||||
- Telemetry timing comes from the injected (or default) clock, not
|
||||
`performance.now()`; this keeps the histogram reproducible under tests.
|
||||
@@ -0,0 +1,109 @@
|
||||
# BIP39 mnemonics
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`bip39.ts` wraps `@scure/bip39` (the audited BIP39 reference implementation)
|
||||
with the Suaveplan ergonomics expected by the rest of `crypto-vault`: a
|
||||
literal-typed `strength` argument, RNG injection, structured
|
||||
`CryptoVaultError` failures, and a default-enforced English word list. The
|
||||
goal is to provide everything a vault layer needs for backup phrase
|
||||
generation, restore, and validation, while keeping the cryptographic core
|
||||
delegated to a maintained upstream package.
|
||||
|
||||
## Features
|
||||
|
||||
- `generateMnemonic(strength?, options?)` — 128-bit (12-word) or 256-bit
|
||||
(24-word) phrases, with optional `Rng` injection for deterministic tests.
|
||||
- `entropyToMnemonic(entropy)` and `mnemonicToEntropy(mnemonic)` — strict
|
||||
round-trip; rejects malformed inputs with a typed error.
|
||||
- `validateMnemonic(mnemonic)` — non-throwing boolean form for UIs.
|
||||
- `BIP39_ENGLISH_WORDLIST` — frozen view of the official English list for
|
||||
consumers that need autocomplete data.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
generateMnemonic,
|
||||
mnemonicToEntropy,
|
||||
} from "@suaveplan/crypto-vault/bip39/bip39";
|
||||
|
||||
const phrase = generateMnemonic(128);
|
||||
const entropy = mnemonicToEntropy(phrase);
|
||||
console.log(phrase.split(" ").length); // 12
|
||||
console.log(entropy.length); // 16
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
entropyToMnemonic,
|
||||
generateMnemonic,
|
||||
validateMnemonic,
|
||||
} from "@suaveplan/crypto-vault/bip39/bip39";
|
||||
|
||||
// Deterministic test fixtures — supply a seeded RNG.
|
||||
const fixedBytes = new Uint8Array(16).fill(0x42);
|
||||
const phrase = generateMnemonic(128, {
|
||||
rng: {
|
||||
nextUint32: () => 0,
|
||||
nextFloat: () => 0,
|
||||
nextBytes: (n) => fixedBytes.slice(0, n),
|
||||
},
|
||||
});
|
||||
|
||||
if (!validateMnemonic(phrase)) {
|
||||
throw new Error("generation produced an invalid phrase");
|
||||
}
|
||||
console.log(phrase === entropyToMnemonic(fixedBytes)); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `generateMnemonic(strength?, options?)`
|
||||
|
||||
- `strength: 128 | 256` (default 128).
|
||||
- `options.rng?: Rng` — uses `nextBytes(strength / 8)` when provided.
|
||||
|
||||
Returns a space-separated phrase using the English word list.
|
||||
|
||||
Throws `CryptoVaultError(CRYPTOVAULT.BIP39.INVALID_ENTROPY_LENGTH)` when
|
||||
`strength` is not 128 or 256, or when the injected RNG returns a buffer of
|
||||
the wrong length.
|
||||
|
||||
### `entropyToMnemonic(entropy: Uint8Array)`
|
||||
|
||||
Converts 16-byte or 32-byte entropy into a BIP39 phrase. Throws
|
||||
`CRYPTOVAULT.BIP39.INVALID_ENTROPY_LENGTH` for any other length.
|
||||
|
||||
### `mnemonicToEntropy(mnemonic: string)`
|
||||
|
||||
Validates the phrase against the English word list and the BIP39 checksum,
|
||||
then returns the underlying entropy. Throws
|
||||
`CRYPTOVAULT.BIP39.INVALID_MNEMONIC` on any failure (empty input, unknown
|
||||
word, bad checksum, or wrong word count). The original error is attached
|
||||
as `cause`.
|
||||
|
||||
### `validateMnemonic(mnemonic: string)`
|
||||
|
||||
Non-throwing boolean wrapper around `mnemonicToEntropy`.
|
||||
|
||||
### `BIP39_ENGLISH_WORDLIST`
|
||||
|
||||
A `readonly string[]` view of the 2048-word English list.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The package only ships the English word list. Other languages are out of
|
||||
scope per the proposal; consumers that need them should depend on
|
||||
`@scure/bip39` directly.
|
||||
- The wrapper translates `@scure/bip39`'s thrown errors into
|
||||
`CryptoVaultError` so callers can rely on the package's stable code
|
||||
registry without learning upstream error shapes.
|
||||
- `generateMnemonic` reads entropy _before_ delegating to `entropyToMnemonic`
|
||||
inside `@scure/bip39`. This is what allows `options.rng.nextBytes` to
|
||||
drive the result deterministically.
|
||||
@@ -0,0 +1,78 @@
|
||||
# crypto-vault — Error code registry
|
||||
|
||||
## Purpose
|
||||
|
||||
`codes.ts` is the canonical, frozen registry of every error code emitted by
|
||||
`@suaveplan/crypto-vault`. The registry exists so that consumers — vault
|
||||
orchestrators, sync layers, telemetry pipelines, audit log post-processors —
|
||||
can assert against a stable set of strings rather than parsing free-form
|
||||
messages. The registry is built with `defineCodes` from `@suaveplan/error`,
|
||||
which freezes the resulting record at module load and infers each code's
|
||||
literal type so `instanceof CryptoVaultError` can narrow `code` to the union.
|
||||
|
||||
## Features
|
||||
|
||||
- Frozen `CRYPTO_VAULT_CODES` record with `CRYPTOVAULT.<MODULE>.<REASON>`
|
||||
namespacing.
|
||||
- `CryptoVaultCode` type union for exhaustive `switch` discrimination.
|
||||
- One canonical home for every failure mode the package can produce. Adding a
|
||||
new code requires touching this file plus the production path that throws.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
CRYPTO_VAULT_CODES,
|
||||
CryptoVaultError,
|
||||
} from "@suaveplan/crypto-vault/errors";
|
||||
|
||||
throw new CryptoVaultError(
|
||||
CRYPTO_VAULT_CODES["BIP39.INVALID_ENTROPY_LENGTH"],
|
||||
"error.crypto-vault.bip39.invalid-entropy-length",
|
||||
{ context: { length: 7 } }
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { CryptoVaultError } from "@suaveplan/crypto-vault/errors";
|
||||
import type { CryptoVaultCode } from "@suaveplan/crypto-vault/errors";
|
||||
|
||||
function describe(err: unknown): string {
|
||||
if (!(err instanceof CryptoVaultError)) return "unknown error";
|
||||
const code: CryptoVaultCode = err.code;
|
||||
switch (code) {
|
||||
case "CRYPTOVAULT.BIP39.INVALID_MNEMONIC":
|
||||
return "Invalid recovery phrase";
|
||||
case "CRYPTOVAULT.SRP.AUTHENTICATION_FAILED":
|
||||
return "Wrong password — try again";
|
||||
default:
|
||||
return `Unhandled crypto-vault code: ${code}`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CRYPTO_VAULT_CODES`
|
||||
|
||||
A frozen `Record<string, string>` mapping each reason key (e.g.
|
||||
`BIP39.INVALID_ENTROPY_LENGTH`) to its fully-qualified code (e.g.
|
||||
`CRYPTOVAULT.BIP39.INVALID_ENTROPY_LENGTH`).
|
||||
|
||||
### `CryptoVaultCode`
|
||||
|
||||
Union of every string literal value held by `CRYPTO_VAULT_CODES`. Use as the
|
||||
generic parameter of `CryptoVaultError`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The registry is intentionally append-only. Removing a code is a breaking
|
||||
change for any consumer that asserts on it.
|
||||
- Codes are checked at runtime via the `defineCodes` factory — any duplicate
|
||||
reason in the array would surface as the same key collapsing in the output
|
||||
record.
|
||||
- Every code listed here must be thrown by at least one production path. The
|
||||
spec-conformance test enumerates the registry and verifies the production
|
||||
surface throws each code at least once.
|
||||
@@ -0,0 +1,93 @@
|
||||
# CryptoVaultError
|
||||
|
||||
## Purpose
|
||||
|
||||
`CryptoVaultError` is the umbrella error class for `@suaveplan/crypto-vault`.
|
||||
Every failure mode in the package — invalid BIP39 entropy, unsafe Argon2id
|
||||
parameters, SRP authentication failures, zeroized SecureBuffer access — is
|
||||
surfaced as a `CryptoVaultError` carrying a code from
|
||||
`CRYPTO_VAULT_CODES`. Extending `SuaveplanError` means the error participates
|
||||
in the project's wire format, OTel span events, redaction, and i18n
|
||||
infrastructure for free.
|
||||
|
||||
## Features
|
||||
|
||||
- Inherits structured `code`, `context`, `timestamp`, and `traceId` from
|
||||
`SuaveplanError`.
|
||||
- Code field is narrowed to the `CryptoVaultCode` union so `switch (err.code)`
|
||||
is exhaustive.
|
||||
- Tagged with a unique brand symbol so cross-realm `instanceof` checks (web
|
||||
workers, iframes) can be defended against via `CryptoVaultError.is(value)`.
|
||||
- Public constructor — internal modules instantiate freely; consumers may
|
||||
also instantiate when re-throwing across boundaries.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
CRYPTO_VAULT_CODES,
|
||||
CryptoVaultError,
|
||||
} from "@suaveplan/crypto-vault/errors";
|
||||
|
||||
throw new CryptoVaultError(
|
||||
CRYPTO_VAULT_CODES["BIP39.INVALID_MNEMONIC"],
|
||||
"error.crypto-vault.bip39.invalid-mnemonic"
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
CRYPTO_VAULT_CODES,
|
||||
CryptoVaultError,
|
||||
} from "@suaveplan/crypto-vault/errors";
|
||||
|
||||
try {
|
||||
// ... call into crypto-vault ...
|
||||
} catch (raw) {
|
||||
if (CryptoVaultError.is(raw)) {
|
||||
switch (raw.code) {
|
||||
case CRYPTO_VAULT_CODES["SRP.AUTHENTICATION_FAILED"]:
|
||||
return { kind: "auth-failed" } as const;
|
||||
case CRYPTO_VAULT_CODES["SECURE.ZEROIZED"]:
|
||||
return { kind: "vault-locked" } as const;
|
||||
default:
|
||||
throw raw;
|
||||
}
|
||||
}
|
||||
throw raw;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class CryptoVaultError extends SuaveplanError<CryptoVaultCode>`
|
||||
|
||||
#### Constructor
|
||||
|
||||
`new CryptoVaultError(code, messageKey, options?)`
|
||||
|
||||
- `code: CryptoVaultCode` — fully-qualified registry code.
|
||||
- `messageKey: string` — i18n key resolved by `@suaveplan/error`'s
|
||||
`MessageResolver`.
|
||||
- `options?: SuaveplanErrorOptions` — forwarded to `SuaveplanError`.
|
||||
|
||||
#### `static is(value: unknown): value is CryptoVaultError`
|
||||
|
||||
Returns `true` when `value` was constructed by this package. Combines
|
||||
`instanceof` with a brand-symbol identity check, defending against cross-realm
|
||||
prototype divergence.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The brand symbol is a `unique symbol` keyed off
|
||||
`Symbol("@suaveplan/crypto-vault.error")`; the symbol identity is not
|
||||
stable across realms by design — `CryptoVaultError.is` uses it as a
|
||||
same-realm assertion only. Cross-realm instances must be re-thrown by
|
||||
their owning realm before reaching this guard.
|
||||
- The constructor sets the brand field after `super(...)` so the assignment
|
||||
happens after the prototype chain is initialised.
|
||||
- The class is intentionally non-abstract. Internal callers throw it
|
||||
directly with a `CRYPTO_VAULT_CODES.*` value rather than maintaining one
|
||||
subclass per error reason.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Opaque key handles
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`opaque-handle.ts` lets the broader `vault` layer hold a typed reference
|
||||
to a SecureBuffer without ever seeing the raw bytes. The handle is a
|
||||
frozen, brand-symbol-tagged object containing only a stable `id`, a
|
||||
`length`, and a `_zeroize` thunk. The mapping back to the SecureBuffer is
|
||||
held in a module-level `WeakMap` keyed by the handle, so a leaked handle
|
||||
on its own does not yield key material.
|
||||
|
||||
## Features
|
||||
|
||||
- `wrap(SecureBuffer): OpaqueKeyHandle` — frozen handle with a unique
|
||||
symbol id, the buffer's length, and a `_zeroize` callable.
|
||||
- `unwrap(OpaqueKeyHandle): SecureBuffer` — package-internal resolver,
|
||||
validates the brand and throws when the handle is malformed or the
|
||||
underlying buffer has been GC'd.
|
||||
- `isValidHandle(value): value is OpaqueKeyHandle` — runtime brand check
|
||||
for values arriving across an `unknown`-typed boundary.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SecureBuffer } from "@suaveplan/crypto-vault/secure/secure-buffer";
|
||||
import { wrap } from "@suaveplan/crypto-vault/handle/opaque-handle";
|
||||
|
||||
const buf = new SecureBuffer(32);
|
||||
buf.fillRandom();
|
||||
const handle = wrap(buf);
|
||||
console.log(handle.length); // 32
|
||||
handle._zeroize();
|
||||
console.log(buf.isClosed); // true
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { SecureBuffer } from "@suaveplan/crypto-vault/secure/secure-buffer";
|
||||
import {
|
||||
isValidHandle,
|
||||
wrap,
|
||||
} from "@suaveplan/crypto-vault/handle/opaque-handle";
|
||||
|
||||
function passToVaultLayer(handle: unknown) {
|
||||
if (!isValidHandle(handle)) {
|
||||
throw new Error("vault layer received a non-handle value");
|
||||
}
|
||||
// The vault layer stores the handle but cannot read the underlying bytes.
|
||||
return handle;
|
||||
}
|
||||
|
||||
const wrapped = wrap(new SecureBuffer(32));
|
||||
passToVaultLayer(wrapped);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `wrap(buffer: SecureBuffer): OpaqueKeyHandle`
|
||||
|
||||
Returns a frozen handle. The handle's `_zeroize` thunk forwards to the
|
||||
buffer's `close()` method.
|
||||
|
||||
### `unwrap(handle: OpaqueKeyHandle): SecureBuffer`
|
||||
|
||||
Resolves the handle back to the underlying SecureBuffer. Throws
|
||||
`CryptoVaultError(CRYPTOVAULT.OPAQUE.INVALID_HANDLE)` for missing brand,
|
||||
unknown handle, or GC'd buffer. **Internal — vault-side packages
|
||||
within `@suaveplan/*` may import this; consumers outside the project
|
||||
should treat handles as opaque.**
|
||||
|
||||
### `isValidHandle(value: unknown): value is OpaqueKeyHandle`
|
||||
|
||||
Runtime brand check.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The handle uses a `unique symbol` brand so the TypeScript compiler can
|
||||
refuse foreign objects masquerading as `OpaqueKeyHandle`.
|
||||
- The `WeakMap<OpaqueKeyHandle, SecureBuffer>` ensures that once the
|
||||
handle becomes unreachable, the SecureBuffer link is also collected.
|
||||
Callers that want to keep the buffer alive must keep a strong reference
|
||||
to the handle.
|
||||
- The `_zeroize` field deliberately starts with an underscore to flag
|
||||
it as an implementation detail rather than a stable public method.
|
||||
- This module is the boundary that prevents the `vault` layer from
|
||||
needing direct `Uint8Array` access to derived keys; the orchestrator
|
||||
hands handles to AEAD operations and the AEAD layer calls `unwrap`
|
||||
immediately before passing the bytes into the noble cipher.
|
||||
@@ -0,0 +1,102 @@
|
||||
# HD key derivation
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`hd-key.ts` turns a BIP39 mnemonic into deterministic key material that the
|
||||
rest of the package can fan out into a full hierarchy. The module performs
|
||||
two jobs: BIP39 seed derivation (PBKDF2-HMAC-SHA512 with 2048 rounds and the
|
||||
mnemonic + passphrase salt scheme) and indexed child-key derivation
|
||||
(HMAC-SHA512 over the parent + 32-bit big-endian index). Together they
|
||||
provide a flat, safe, BIP32-shaped HD path without requiring callers to
|
||||
maintain hardened-derivation chain codes.
|
||||
|
||||
## Features
|
||||
|
||||
- `deriveMasterKeyFromMnemonic(mnemonic, passphrase?)` — 32-byte master key
|
||||
(first half of the BIP39 seed).
|
||||
- `deriveSeedFromMnemonic(mnemonic, passphrase?)` — full 64-byte seed for
|
||||
callers that need both halves.
|
||||
- `deriveChildKey(parent, index)` — 32-byte HMAC-SHA512 child.
|
||||
- All entry points reject invalid mnemonics (word-count, unknown word,
|
||||
checksum failure) with `CryptoVaultError(CRYPTOVAULT.HD.INVALID_MNEMONIC)`.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { generateMnemonic } from "@suaveplan/crypto-vault/bip39/bip39";
|
||||
import { deriveMasterKeyFromMnemonic } from "@suaveplan/crypto-vault/hd-key/hd-key";
|
||||
|
||||
const phrase = generateMnemonic(128);
|
||||
const master = deriveMasterKeyFromMnemonic(phrase);
|
||||
console.log(master.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
deriveChildKey,
|
||||
deriveMasterKeyFromMnemonic,
|
||||
deriveSeedFromMnemonic,
|
||||
} from "@suaveplan/crypto-vault/hd-key/hd-key";
|
||||
|
||||
const phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
||||
|
||||
// Two devices, same mnemonic + passphrase => same master key (deterministic).
|
||||
const m1 = deriveMasterKeyFromMnemonic(phrase, "shared-passphrase");
|
||||
const m2 = deriveMasterKeyFromMnemonic(phrase, "shared-passphrase");
|
||||
console.log(Buffer.from(m1).equals(Buffer.from(m2))); // true
|
||||
|
||||
// Different passphrases => different masters.
|
||||
const m3 = deriveMasterKeyFromMnemonic(phrase, "different");
|
||||
console.log(Buffer.from(m1).equals(Buffer.from(m3))); // false
|
||||
|
||||
// Indexed children.
|
||||
const seed = deriveSeedFromMnemonic(phrase, "shared-passphrase");
|
||||
const c0 = deriveChildKey(seed.slice(0, 32), 0);
|
||||
const c1 = deriveChildKey(seed.slice(0, 32), 1);
|
||||
console.log(Buffer.from(c0).equals(Buffer.from(c1))); // false
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `deriveMasterKeyFromMnemonic(mnemonic, passphrase?): Uint8Array`
|
||||
|
||||
- `mnemonic: string` — must be 12 / 15 / 18 / 21 / 24 BIP39 English words
|
||||
with a valid checksum.
|
||||
- `passphrase?: string` — optional BIP39 passphrase (defaults to `""`).
|
||||
|
||||
Returns a 32-byte master key (first half of the BIP39 seed).
|
||||
|
||||
Throws `CryptoVaultError(CRYPTOVAULT.HD.INVALID_MNEMONIC)` on empty input,
|
||||
unsupported word counts, unknown words, or checksum failures.
|
||||
|
||||
### `deriveSeedFromMnemonic(mnemonic, passphrase?): Uint8Array`
|
||||
|
||||
Same validation as above; returns the full 64-byte seed.
|
||||
|
||||
### `deriveChildKey(parent, index): Uint8Array`
|
||||
|
||||
- `parent: Uint8Array` — any byte string (typically a master key or another
|
||||
derived key).
|
||||
- `index: number` — non-negative integer ≤ `0xffffffff`.
|
||||
|
||||
Returns a 32-byte child key (`HMAC-SHA512(parent, uint32-be(index))[:32]`).
|
||||
|
||||
Throws `CryptoVaultError(CRYPTOVAULT.HD.INVALID_INDEX)` for non-integer,
|
||||
negative, or > 32-bit indices.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- BIP39 seed derivation is intentionally synchronous (`mnemonicToSeedSync`)
|
||||
so it can be called from constructors and worker init. PBKDF2 with 2048
|
||||
rounds completes in ~5 ms on modern hardware.
|
||||
- The validator runs both word-list membership and the BIP39 checksum
|
||||
(`@scure/bip39.validateMnemonic`) so callers cannot bypass the checksum
|
||||
by re-importing words.
|
||||
- Child-key derivation deliberately does not implement BIP32 hardened paths
|
||||
with chain codes. The spec-driven hierarchy in `key-hierarchy/` uses HKDF
|
||||
for fan-out and only needs a single deterministic 32-byte parent.
|
||||
@@ -0,0 +1,114 @@
|
||||
# Hierarchical key derivation
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`key-hierarchy.ts` fans the AUK out into a deterministic tree of keys —
|
||||
personal keyset → vault key → item key — using HKDF-SHA256 with versioned
|
||||
info labels. The hierarchy makes targeted rotation cheap: rotating a vault
|
||||
key only invalidates that vault's items; rotating an item key only
|
||||
invalidates that item. The AUK never leaves `crypto-vault`'s memory
|
||||
boundary; only the derived material is handed downstream.
|
||||
|
||||
## Features
|
||||
|
||||
- `derivePersonalKeyset(auk)` — three pairwise-distinct 32-byte keys
|
||||
(`encryptKey`, `signKey`, `kxKey`).
|
||||
- `deriveVaultKey(personalKeyset, vaultId)` — 32-byte vault-scoped key.
|
||||
- `deriveItemKey(vaultKey, itemId)` — 32-byte item-scoped key.
|
||||
- All info labels are prefixed with `v1/` so future hierarchy revisions can
|
||||
fork without breaking existing vaults.
|
||||
- Strict input validation: empty `vaultId` / `itemId` and wrong-length AUK
|
||||
raise typed `CryptoVaultError`s.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { deriveAuk } from "@suaveplan/crypto-vault/twoskd/twoskd";
|
||||
import {
|
||||
deriveItemKey,
|
||||
derivePersonalKeyset,
|
||||
deriveVaultKey,
|
||||
} from "@suaveplan/crypto-vault/key-hierarchy/key-hierarchy";
|
||||
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const auk = await deriveAuk("pw", "sk", "alice@example.com", salt);
|
||||
const personal = derivePersonalKeyset(auk);
|
||||
const vaultKey = deriveVaultKey(personal, "personal-vault");
|
||||
const itemKey = deriveItemKey(vaultKey, "login-bank-2026");
|
||||
console.log(itemKey.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
deriveItemKey,
|
||||
derivePersonalKeyset,
|
||||
deriveVaultKey,
|
||||
} from "@suaveplan/crypto-vault/key-hierarchy/key-hierarchy";
|
||||
|
||||
// Rotate a single vault key without invalidating any item key — the new
|
||||
// vault key is independent of the old one's exact value because both are
|
||||
// HKDF-derived from the (stable) personal keyset with different info.
|
||||
function rotateVaultKey(
|
||||
personalKeyset: ReturnType<typeof derivePersonalKeyset>,
|
||||
oldVaultId: string,
|
||||
newVaultId: string
|
||||
) {
|
||||
const oldVaultKey = deriveVaultKey(personalKeyset, oldVaultId);
|
||||
const newVaultKey = deriveVaultKey(personalKeyset, newVaultId);
|
||||
// Re-encrypt only the vault header with newVaultKey.
|
||||
return { oldVaultKey, newVaultKey };
|
||||
}
|
||||
|
||||
// Item keys can rotate by including a counter in itemId.
|
||||
const personal = /* … */ derivePersonalKeyset(new Uint8Array(32));
|
||||
const vaultKey = deriveVaultKey(personal, "vault-1");
|
||||
const v1 = deriveItemKey(vaultKey, "login-bank/v1");
|
||||
const v2 = deriveItemKey(vaultKey, "login-bank/v2");
|
||||
console.log(Buffer.from(v1).equals(Buffer.from(v2))); // false
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `derivePersonalKeyset(auk: Auk): PersonalKeyset`
|
||||
|
||||
Returns `{ encryptKey, signKey, kxKey }` — three distinct 32-byte arrays
|
||||
derived via HKDF-Expand from the AUK with info labels
|
||||
`v1/personal/encrypt`, `v1/personal/sign`, `v1/personal/kx`.
|
||||
|
||||
Throws `CryptoVaultError(CRYPTOVAULT.HIERARCHY.INVALID_AUK_LENGTH)` when
|
||||
`auk.length !== 32`.
|
||||
|
||||
### `deriveVaultKey(personalKeyset, vaultId): VaultKey`
|
||||
|
||||
Derives a 32-byte vault key via HKDF-SHA256 over `personalKeyset.encryptKey`
|
||||
with info `v1/vault/<vaultId>`. Throws
|
||||
`CryptoVaultError(CRYPTOVAULT.HIERARCHY.EMPTY_VAULT_ID)` for empty input.
|
||||
|
||||
### `deriveItemKey(vaultKey, itemId): ItemKey`
|
||||
|
||||
Derives a 32-byte item key via HKDF-SHA256 over the supplied vault key with
|
||||
info `v1/item/<itemId>`. Throws
|
||||
`CryptoVaultError(CRYPTOVAULT.HIERARCHY.EMPTY_ITEM_ID)` for empty input.
|
||||
|
||||
### Constants
|
||||
|
||||
- `AUK_LENGTH = 32`, `HIERARCHY_KEY_LENGTH = 32`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- HKDF salt is intentionally empty: the AUK is already a uniformly random
|
||||
cryptographic key, so HKDF-Extract over an empty salt is no weaker than
|
||||
using a random salt and saves one allocation per derivation.
|
||||
- Info labels are version-prefixed (`v1/...`). A future hierarchy revision
|
||||
would bump the prefix and treat `v1/` material as legacy.
|
||||
- The personal keyset's `encryptKey` is used as the IKM for vault keys.
|
||||
This narrows the cross-vault blast radius if the encrypt key leaks: the
|
||||
`signKey` and `kxKey` slots are unaffected.
|
||||
- All three derivations are O(1) — no hashing of vault contents, no
|
||||
iteration. Per-item key derivation completes in ~10 µs on modern
|
||||
hardware.
|
||||
@@ -0,0 +1,88 @@
|
||||
# internals
|
||||
|
||||
Internal building blocks for the orchestration surface.
|
||||
|
||||
## Purpose
|
||||
|
||||
`internals` is a private implementation seam: it holds the lower-level helpers
|
||||
that `orchestration.ts` composes (vault sealing, vault opening, Ed25519 sign +
|
||||
append, public-key-file projection). Splitting these helpers into a sibling
|
||||
module keeps `orchestration.ts` under the file-size budget without polluting
|
||||
the public barrel — none of the symbols here are re-exported from
|
||||
`src/orchestration/index.ts` or `src/index.ts`.
|
||||
|
||||
This file is documented because every `.ts` in the package requires a sibling
|
||||
`.md` (see `CLAUDE.md` §4); the docs treat it as an implementation note rather
|
||||
than a public contract.
|
||||
|
||||
## Features
|
||||
|
||||
- `sealVault(plaintext, password, params)` — Argon2id KEK derivation followed
|
||||
by XChaCha20-Poly1305 AEAD encryption; emits the canonical wire layout
|
||||
`salt(16) || nonce(24) || ciphertext`.
|
||||
- `openVaultBytes(vaultBytes, password)` — inverse of `sealVault`. Always uses
|
||||
`DEFAULT_ARGON2ID_PARAMS`. Collapses short input, wrong password, and
|
||||
ciphertext tamper into a single `ORCHESTRATION.DECRYPT_FAILED` error.
|
||||
- `buildPublicKeyFile(keychain, signingSecretKey)` — projects the keychain to
|
||||
its public-only form, encodes it, and appends an Ed25519 signature.
|
||||
- Re-exported header byte-size constants (`SALT_BYTES`, `NONCE_BYTES`,
|
||||
`SIGNATURE_BYTES`, `VAULT_HEADER_BYTES`) for use in the public layer.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
// internals are package-private — consumers go through @suaveplan/crypto-vault
|
||||
import { sealVault } from "./internals.js";
|
||||
import { DEFAULT_ARGON2ID_PARAMS } from "../argon2id/argon2id.js";
|
||||
|
||||
const bytes = await sealVault(
|
||||
new TextEncoder().encode("payload"),
|
||||
"pw",
|
||||
DEFAULT_ARGON2ID_PARAMS
|
||||
);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
// Compose with a custom Argon2id profile for a hardened on-disk artefact.
|
||||
import { sealVault, openVaultBytes } from "./internals.js";
|
||||
|
||||
const hardened = {
|
||||
memoryKB: 256 * 1024,
|
||||
iterations: 4,
|
||||
parallelism: 4,
|
||||
outputLength: 32,
|
||||
};
|
||||
|
||||
const wire = await sealVault(payload, "pw", hardened);
|
||||
const back = await openVaultBytes(wire, "pw");
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `sealVault(plaintext: Uint8Array, password: string, params: Argon2idParams)`
|
||||
→ `Promise<Uint8Array>`. The wire bytes are exactly
|
||||
`salt(16) || nonce(24) || ciphertext`. Salt and nonce are drawn from
|
||||
`crypto.getRandomValues`. The function does not modify `plaintext`.
|
||||
- `openVaultBytes(vaultBytes: Uint8Array, password: string)`
|
||||
→ `Promise<Uint8Array>`. Throws
|
||||
`CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.DECRYPT_FAILED)` for any failure.
|
||||
- `buildPublicKeyFile(keychain, signingSecretKey)` →
|
||||
`{ publicKeyBytes, publicKeys }`. The signature is appended; the returned
|
||||
`publicKeys` is the same in-memory projection produced by `extractPublicKeys`.
|
||||
- `SALT_BYTES = 16`, `NONCE_BYTES = 24`, `SIGNATURE_BYTES = 64`,
|
||||
`VAULT_HEADER_BYTES = SALT_BYTES + NONCE_BYTES`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `openVaultBytes` always re-derives the KEK with `DEFAULT_ARGON2ID_PARAMS`
|
||||
rather than reading params from a header. This keeps the wire format flat at
|
||||
the cost of locking the on-disk profile to the package default; a future
|
||||
versioned header is the migration path.
|
||||
- Decryption failure is intentionally indistinguishable from a short-input or
|
||||
tampered ciphertext failure — the same error code surfaces in all three
|
||||
cases. Tests verify the property.
|
||||
- The signing helper takes a raw 32-byte Ed25519 secret key (a `KeySlotPrivate.secretKey`
|
||||
field) rather than the full slot. This keeps the seam narrow and makes the
|
||||
unit tests trivial to feed with a hand-rolled keypair.
|
||||
@@ -0,0 +1,146 @@
|
||||
# orchestration
|
||||
|
||||
High-level orchestration helpers that bundle keychain generation, password-AEAD
|
||||
encryption, signed public-key file production, and storage I/O behind a single
|
||||
call surface.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`@suaveplan/crypto-vault` already ships rich low-level primitives — BIP39
|
||||
mnemonics, Argon2id KDF, hierarchical key derivation, SecureBuffer, SRP — but
|
||||
no opinionated "create a root keychain plus encrypted vault plus signed
|
||||
public-key file" entrypoint. The orchestration module fills that gap.
|
||||
|
||||
It composes `@suaveplan/crypto`'s `generateKeychain` / `encodeKeychain` /
|
||||
`encodePublicKeychain` primitives with this package's
|
||||
`deriveKeyWithArgon2id` KEK derivation and the Ed25519 signing surface from
|
||||
`@suaveplan/crypto-curve`, producing the single-call surface used by
|
||||
`@suaveplan/server-keys` operator bins (`create-root-keys`, `create-server-keys`,
|
||||
`roll-server-keys`, `view-keys`, `view-vault`, `view-public-key-file`, etc.).
|
||||
|
||||
## Features
|
||||
|
||||
- One-call `createRootKeys` / `createServerKeys` / `rollServerKeys` producing
|
||||
keychain + encrypted vault + signed public-key file in one trip.
|
||||
- Deterministic server-key derivation: `(rootMaster, version)` reproduces the
|
||||
same private material across hosts so rotation never loses key continuity.
|
||||
- Versioned rotation via `rollServerKeys` with strict `version ≥ 0` validation.
|
||||
- Round-trip `openVault` / `packVault` with constant-time wrong-password
|
||||
rejection (no information leak about which of password / integrity failed).
|
||||
- `viewPublicKeys` with optional Ed25519 signature verification.
|
||||
- Idempotent storage helpers (`saveVaultToStorage`,
|
||||
`savePublicKeysToStorage`, `getDataFromStorage`) that translate "missing key"
|
||||
outcomes from any backend into a typed `CryptoVaultError`.
|
||||
- suaveplan-native wire formats — explicitly NOT byte-compatible with the
|
||||
upstream `@suaveplan/keys` BitPacker format; rotation is an offline migration.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import {
|
||||
createRootKeys,
|
||||
openVault,
|
||||
saveVaultToStorage,
|
||||
} from "@suaveplan/crypto-vault";
|
||||
import { MemoryStorage } from "@suaveplan/storage";
|
||||
|
||||
const storage = new MemoryStorage({ cleanupIntervalMs: 0 });
|
||||
|
||||
const root = await createRootKeys("correct horse battery staple");
|
||||
await saveVaultToStorage(storage, "root.vault", root.vaultBytes);
|
||||
|
||||
const recovered = await openVault(root.vaultBytes, "correct horse battery staple");
|
||||
// Eight default-purpose slots: "mast" (master), "sign", "encr", "kx ",
|
||||
// "salt", "hash", "strm", "dev " — keyed by the 4-byte purpose tag.
|
||||
console.log(recovered["mast"].publicKey.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
createRootKeys,
|
||||
createServerKeys,
|
||||
rollServerKeys,
|
||||
viewPublicKeys,
|
||||
} from "@suaveplan/crypto-vault";
|
||||
|
||||
const root = await createRootKeys("root-pw");
|
||||
|
||||
// Provision a server keychain at version 1 (default)
|
||||
const server1 = await createServerKeys(root, "server-pw");
|
||||
|
||||
// Roll the server keychain forward — deterministically derives a fresh master.
|
||||
const server2 = await rollServerKeys(root, server1.version, "server-pw");
|
||||
|
||||
// Verify the server's signed public-key file using the root sign pubkey.
|
||||
const decoded = await viewPublicKeys(
|
||||
server2.publicKeyBytes,
|
||||
root.keychain["sign"].publicKey
|
||||
);
|
||||
console.log(decoded["sign"].publicKey === server2.publicKeys["sign"].publicKey);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `createRootKeys(password, options?)` → `Promise<RootKeysResult>`. Generates a
|
||||
fresh root keychain via `generateKeychain`. `options.masterKey` lets callers
|
||||
supply a pre-existing 32-byte seed; `options.layoutFields` overrides the
|
||||
default v1 layout `{ masterVersion: 1, userId: 0, depth: 0, slotVersion: 1 }`;
|
||||
`options.argon2id` overrides KDF parameters.
|
||||
- `createServerKeys(rootKeys, serverPassword, options?)` →
|
||||
`Promise<ServerKeysResult>`. Deterministically derives a child master via
|
||||
`HMAC-SHA512(rootMaster, u32be(version))` truncated to 32 bytes. Throws
|
||||
`CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.INVALID_VERSION)` on negative or
|
||||
non-integer `version`. Public-key file is signed by the *root* sign slot.
|
||||
- `rollServerKeys(rootKeys, currentVersion, serverPassword, options?)` →
|
||||
`Promise<ServerKeysResult>`. Returns a result at `version = currentVersion + 1`.
|
||||
Throws the same `INVALID_VERSION` error on negative `currentVersion`.
|
||||
- `openVault(vaultBytes, password)` → `Promise<Keychain>`. Splits the wire
|
||||
layout (`salt(16) || nonce(24) || ciphertext`), re-derives the KEK with
|
||||
Argon2id, and decrypts. Throws
|
||||
`CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.DECRYPT_FAILED)` on wrong
|
||||
password, short input, or tampered ciphertext.
|
||||
- `packVault(keychain, password, options?)` → `Promise<Uint8Array>`. Inverse of
|
||||
`openVault`; reusable by callers that already hold a `Keychain`.
|
||||
- `viewPublicKeys(publicKeyBytes, optionalSignKey?)` → `Promise<PublicKeychain>`.
|
||||
Splits the trailing 64-byte Ed25519 signature, decodes the body, and (when
|
||||
`optionalSignKey` is provided) verifies the signature. Throws
|
||||
`CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.PUBLIC_KEY_SIGNATURE_INVALID)` on
|
||||
short input or signature mismatch.
|
||||
- `saveVaultToStorage(storage, key, vaultBytes)` /
|
||||
`savePublicKeysToStorage(storage, key, publicKeyBytes)` → `Promise<void>`.
|
||||
Thin idempotent wrappers over `AbstractStorage.set`.
|
||||
- `getDataFromStorage(storage, key)` → `Promise<Uint8Array>`. Translates a
|
||||
missing key (either `undefined` return or a thrown backend error) into
|
||||
`CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.STORAGE_KEY_NOT_FOUND)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The KEK is derived with `deriveKeyWithArgon2id` using
|
||||
`DEFAULT_ARGON2ID_PARAMS` (OWASP-compliant: 64 MiB memory, 3 iterations,
|
||||
parallelism 4) unless callers override via `options.argon2id`.
|
||||
- Vault layout: 16-byte Argon2id salt, then a 24-byte XChaCha20-Poly1305
|
||||
nonce, then ciphertext. Both header values are drawn from
|
||||
`crypto.getRandomValues` so two encryptions of the same plaintext under the
|
||||
same password share no bytes.
|
||||
- Public-key file layout: `encodePublicKeychain(extractPublicKeys(keychain))`
|
||||
followed by a 64-byte Ed25519 signature. For root keychains the keychain
|
||||
self-signs; for server keychains the root's sign slot signs the child's
|
||||
public bytes so a verifier holding only the root sign pubkey can attest the
|
||||
full chain.
|
||||
- `openVault` always uses `DEFAULT_ARGON2ID_PARAMS` to re-derive the KEK; the
|
||||
salt is embedded in the vault header but the params themselves are not — the
|
||||
trade-off keeps the format flat at the cost of locking the on-disk profile to
|
||||
the package default. Future overrides will append a versioned header.
|
||||
- `viewPublicKeys` collapses every signature failure into a single error code
|
||||
to avoid leaking which check failed first; tests rely on that property when
|
||||
asserting the conformance behaviour.
|
||||
- Storage helpers do not introspect the value — they pass `Uint8Array` straight
|
||||
through. Backends with their own size limits (e.g. `MemoryStorage`'s
|
||||
`maxValueSize`) surface as their own typed error, which is intentional: the
|
||||
vault layer should not silently mutate the backend's contract.
|
||||
@@ -0,0 +1,98 @@
|
||||
# crypto-vault — OpenTelemetry instruments
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`instruments.ts` holds the full OTel instrument set emitted by
|
||||
`@suaveplan/crypto-vault` production paths. The module exposes a lazy
|
||||
singleton that defers `metrics.getMeter(...)` until first use, so loading
|
||||
the package does nothing observable when no exporter is wired up. Every
|
||||
instrument is safe-by-construction — no key bytes, salts, plaintext, or
|
||||
user-identifiable data appears in attributes.
|
||||
|
||||
## Features
|
||||
|
||||
- Lazy `getCryptoVaultInstruments()` accessor — first call materialises the
|
||||
instrument set against the global meter; subsequent calls hit the cached
|
||||
set.
|
||||
- `setCryptoVaultMeterProvider(provider)` lets tests register a deterministic
|
||||
in-memory `MeterProvider` and then assert against captured observations.
|
||||
- `resetCryptoVaultInstruments()` clears the cache so tests can re-run with a
|
||||
fresh provider without leaking state.
|
||||
- Counters and histograms covering Argon2id derivations, HKDF derivations,
|
||||
zeroization, SecureBuffer allocations / zeroizations, and the SRP-6a
|
||||
handshake.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { getCryptoVaultInstruments } from "@suaveplan/crypto-vault/otel";
|
||||
|
||||
const i = getCryptoVaultInstruments();
|
||||
i.argon2idDurationMs.record(143.2);
|
||||
i.secureBufferAllocateTotal.add(1, { length: "32" });
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
getCryptoVaultInstruments,
|
||||
resetCryptoVaultInstruments,
|
||||
setCryptoVaultMeterProvider,
|
||||
} from "@suaveplan/crypto-vault/otel";
|
||||
|
||||
class CountingMeter {
|
||||
public readonly events: Array<[string, number]> = [];
|
||||
public createHistogram = (name: string) => ({
|
||||
record: (v: number) => this.events.push([`hist:${name}`, v]),
|
||||
});
|
||||
public createCounter = (name: string) => ({
|
||||
add: (v: number) => this.events.push([`ctr:${name}`, v]),
|
||||
});
|
||||
}
|
||||
const meter = new CountingMeter();
|
||||
setCryptoVaultMeterProvider({ getMeter: () => meter as unknown as never });
|
||||
|
||||
getCryptoVaultInstruments().secureBufferZeroizeTotal.add(1);
|
||||
console.log(meter.events); // [["ctr:crypto_vault.secure_buffer.zeroize.total", 1]]
|
||||
|
||||
setCryptoVaultMeterProvider(undefined);
|
||||
resetCryptoVaultInstruments();
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `getCryptoVaultInstruments(): CryptoVaultInstruments`
|
||||
|
||||
Returns the cached instrument set, building it on first call against the
|
||||
global OTel meter.
|
||||
|
||||
### `setCryptoVaultMeterProvider(provider?)`
|
||||
|
||||
Replace the meter provider used by subsequent
|
||||
`getCryptoVaultInstruments()` calls. Pass `undefined` to revert to the
|
||||
global default.
|
||||
|
||||
### `resetCryptoVaultInstruments(): void`
|
||||
|
||||
Clear the cache so the next `getCryptoVaultInstruments()` re-acquires the
|
||||
meter. Tests use this to defeat singleton leakage between suites.
|
||||
|
||||
### `CryptoVaultInstruments`
|
||||
|
||||
Bag of `Counter` and `Histogram` instances. Field names map to the metric
|
||||
names listed under "Features".
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The cache is intentionally module-level — production code should not need
|
||||
per-instance instrument sets. Tests that want isolation register a
|
||||
provider, run their assertions, then reset.
|
||||
- Metric names follow the OTel semantic-conventions style:
|
||||
`<package>.<feature>.<dimension>_<unit>`.
|
||||
- The package never adds key material, vault IDs, item IDs, salts, or email
|
||||
addresses to instrument attributes. The only attributes used are
|
||||
fixed-cardinality status / outcome strings.
|
||||
@@ -0,0 +1,86 @@
|
||||
# Zod schemas
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`schemas.ts` exposes Zod schemas for every public-surface shape that
|
||||
`@suaveplan/crypto-vault` accepts or returns. Schemas are the runtime
|
||||
counterpart to the TypeScript types in `types/types.ts` — consumers can
|
||||
parse caller-supplied JSON, configuration, or RPC payloads through these
|
||||
schemas to get the same safety guarantees the package's own validation
|
||||
provides.
|
||||
|
||||
## Features
|
||||
|
||||
- `Argon2idParamsSchema` — pinned to the OWASP minimums and the
|
||||
package-internal hard ceilings for `memoryKB` and `outputLength`.
|
||||
- `AukSchema` — refines `Uint8Array` to exactly 32 bytes.
|
||||
- `HierarchyKeySchema` — same shape, used for personal-keyset slots and
|
||||
vault/item keys.
|
||||
- `PersonalKeysetSchema` — strict object schema for the three-key bundle.
|
||||
- `Bip39StrengthSchema` — `128 | 256` literal union.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { Argon2idParamsSchema } from "@suaveplan/crypto-vault/schemas/schemas";
|
||||
|
||||
const params = Argon2idParamsSchema.parse({
|
||||
memoryKB: 65536,
|
||||
iterations: 3,
|
||||
parallelism: 4,
|
||||
outputLength: 32,
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
AukSchema,
|
||||
PersonalKeysetSchema,
|
||||
} from "@suaveplan/crypto-vault/schemas/schemas";
|
||||
|
||||
function importPersonalKeyset(json: unknown) {
|
||||
const ks = PersonalKeysetSchema.parse(json);
|
||||
AukSchema.parse(ks.encryptKey); // also valid as an AUK shape (32 bytes)
|
||||
return ks;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `Argon2idParamsSchema`
|
||||
|
||||
Strict object schema enforcing:
|
||||
|
||||
- `memoryKB ∈ [OWASP_MIN_MEMORY_KB, ARGON2ID_MAX_MEMORY_KB]` (integer).
|
||||
- `iterations ≥ OWASP_MIN_ITERATIONS` (integer).
|
||||
- `parallelism ≥ OWASP_MIN_PARALLELISM` (integer).
|
||||
- `outputLength ∈ (0, ARGON2ID_MAX_OUTPUT_LENGTH]` (integer).
|
||||
|
||||
### `AukSchema`, `HierarchyKeySchema`
|
||||
|
||||
Instance-of `Uint8Array` refined to exactly 32 bytes.
|
||||
|
||||
### `PersonalKeysetSchema`
|
||||
|
||||
Strict object with three `HierarchyKeySchema` fields.
|
||||
|
||||
### `Bip39StrengthSchema`
|
||||
|
||||
`z.union([z.literal(128), z.literal(256)])`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The schemas import the same constants used by the runtime validators so
|
||||
the two paths cannot drift. Adjusting an OWASP minimum updates both at
|
||||
once.
|
||||
- `z.instanceof(Uint8Array)` is used so cross-realm `Uint8Array`s (e.g.
|
||||
from a worker) parse correctly even though their prototype chain is
|
||||
different — Zod's instanceof check is structural via `instanceof`.
|
||||
- Strict mode is enabled on object schemas to reject unknown keys; this
|
||||
is intentional, as crypto-vault has zero forward-compatible padding
|
||||
fields.
|
||||
@@ -0,0 +1,128 @@
|
||||
# SecureBuffer
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`SecureBuffer` is the canonical envelope for sensitive cryptographic
|
||||
material in `@suaveplan/crypto-vault`. It owns a private `Uint8Array`,
|
||||
exposes an opt-in `bytes` accessor, and overwrites the backing storage
|
||||
with zero bytes the moment `close()` is called. A module-level registry
|
||||
of `WeakRef<SecureBuffer>` lets `zeroizeAll()` sweep every live buffer in
|
||||
one call — useful on lock, on a security event, or as a final
|
||||
process-shutdown hook. Strings and JSON serializations are masked so the
|
||||
buffer can never leak through accidental logging.
|
||||
|
||||
## Features
|
||||
|
||||
- Eager zeroization via `Uint8Array#fill(0)` on `close()`.
|
||||
- Idempotent close (`close()` after `close()` is a no-op).
|
||||
- Clone isolation — `clone()` allocates a fresh independent backing store.
|
||||
- Slice copy semantics — `slice()` returns a fresh `Uint8Array`; the caller
|
||||
cannot mutate the buffer through the result.
|
||||
- Random fill via `fillRandom(rng?)` (uses `crypto.getRandomValues` by
|
||||
default; honours an injected `Rng` with `nextBytes`).
|
||||
- `toString` and `toJSON` print only `length` and `closed` flags.
|
||||
- Global `zeroizeAll()` sweeps every reachable `SecureBuffer` and closes it.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SecureBuffer } from "@suaveplan/crypto-vault/secure/secure-buffer";
|
||||
|
||||
const buf = new SecureBuffer(32);
|
||||
buf.fillRandom();
|
||||
// ... use buf.bytes ...
|
||||
buf.close();
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
SecureBuffer,
|
||||
zeroizeAll,
|
||||
} from "@suaveplan/crypto-vault/secure/secure-buffer";
|
||||
|
||||
// Initialise from existing key material — copied, never shared.
|
||||
const seed = new Uint8Array(32).fill(7);
|
||||
const masterBuf = new SecureBuffer(seed);
|
||||
|
||||
// Clone for an isolated branch derivation.
|
||||
const branch = masterBuf.clone();
|
||||
branch.bytes[0] = 1;
|
||||
console.log(masterBuf.bytes[0]); // 7 — clone is fully isolated
|
||||
|
||||
// Lock everything in one shot.
|
||||
zeroizeAll();
|
||||
console.log(masterBuf.isClosed); // true
|
||||
console.log(branch.isClosed); // true
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class SecureBuffer`
|
||||
|
||||
#### `new SecureBuffer(lengthOrSource, options?)`
|
||||
|
||||
- `lengthOrSource: number | Uint8Array`. A `number` allocates a fresh
|
||||
zero-filled buffer of that length. A `Uint8Array` is **copied** into the
|
||||
internal store — the buffer never aliases caller storage.
|
||||
- `options?: SecureBufferOptions = { clock?, rng? }`. Reserved for future
|
||||
allocation telemetry; `fillRandom` reads `rng` directly.
|
||||
|
||||
Throws `CryptoVaultError(CRYPTOVAULT.SECURE.INVALID_LENGTH)` when the
|
||||
numeric length is negative or non-integer.
|
||||
|
||||
#### `bytes: Uint8Array`
|
||||
|
||||
Mutable view of the live bytes. Throws
|
||||
`CryptoVaultError(CRYPTOVAULT.SECURE.ZEROIZED)` once the buffer has been
|
||||
closed.
|
||||
|
||||
#### `length: number`, `isClosed: boolean`
|
||||
|
||||
Length of the backing store and the closed flag.
|
||||
|
||||
#### `close(): void`
|
||||
|
||||
Overwrite the bytes with zeros and mark closed. Idempotent.
|
||||
|
||||
#### `clone(): SecureBuffer`
|
||||
|
||||
Allocate a fresh `SecureBuffer` with a copy of the current bytes.
|
||||
|
||||
#### `slice(start?, end?): Uint8Array`
|
||||
|
||||
Copy a range out into a new `Uint8Array`. The result is not registered.
|
||||
|
||||
#### `fillRandom(rng?): void`
|
||||
|
||||
Fill the bytes with CSPRNG output. Honours `rng?.nextBytes(n)` when
|
||||
provided; otherwise falls back to `crypto.getRandomValues`.
|
||||
|
||||
#### `toString()`, `toJSON()`
|
||||
|
||||
Return `[SecureBuffer length=<n> closed=<bool>]` and a structurally
|
||||
equivalent JSON object — never the raw bytes.
|
||||
|
||||
### `zeroizeAll(): void`
|
||||
|
||||
Walk every live `WeakRef<SecureBuffer>` in the module-level registry, call
|
||||
`close()` on each non-closed instance, and prune the registry.
|
||||
|
||||
### `liveSecureBufferCount(): number`
|
||||
|
||||
Test helper — prune dead refs and return the number of live entries.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The registry stores `WeakRef`s so registered buffers can still be
|
||||
garbage-collected normally.
|
||||
- JavaScript cannot guarantee the GC has not relocated memory before
|
||||
`close()`, so eager zeroization is best-effort. Combine with aggressive
|
||||
scoping: allocate inside a function, close, return nothing sensitive.
|
||||
- Memory-locking (`mlock`) is intentionally out of scope. A future native
|
||||
binding will plug in via the `SecureBufferOptions` bag without breaking
|
||||
callers.
|
||||
@@ -0,0 +1,133 @@
|
||||
# SRP-6a client
|
||||
|
||||
## Purpose
|
||||
|
||||
`srp-client.ts` implements the client side of the SRP-6a zero-knowledge
|
||||
password-authenticated key exchange per RFC 5054, using SHA-256 as the
|
||||
one-way hash and the 2048-bit safe-prime group `N`/`g` from Appendix A.
|
||||
The class hands the caller everything needed for an interoperable
|
||||
handshake against any conformant server: salt generation, private key
|
||||
derivation `x = H(s, H(I:p))`, verifier `v = g^x mod N`, ephemeral
|
||||
keypair `(a, A = g^a mod N)`, session key derivation, and constant-time
|
||||
server-proof verification.
|
||||
|
||||
## Features
|
||||
|
||||
- Salt generation via `generateSalt(length?)` honouring an injected RNG.
|
||||
- Private key, verifier, and ephemeral keypair primitives — pure,
|
||||
deterministic given inputs (or RNG).
|
||||
- `deriveSessionKey(...)` produces `K = SHA-256(S)` and the client proof
|
||||
`M1 = H(A || B || K)` per the RFC.
|
||||
- Tamper-resistant `B` validation: `B mod N == 0` aborts with
|
||||
`SRP.INVALID_B`. The `u = H(A || B) == 0` case is excluded via the
|
||||
collision-resistance of SHA-256 and is documented in code rather than
|
||||
re-checked at runtime.
|
||||
- `verifyServerProof` is constant-time (`constantTimeEqual`) and throws on
|
||||
mismatch with `CRYPTOVAULT.SRP.AUTHENTICATION_FAILED`.
|
||||
- Reject groups with `N` shorter than 2048 bits at construction time.
|
||||
- OTel histograms for handshake duration and counters for success / failure.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SrpClient } from "@suaveplan/crypto-vault/srp/srp-client";
|
||||
|
||||
const client = new SrpClient();
|
||||
const salt = client.generateSalt();
|
||||
const privateKey = client.derivePrivateKey(
|
||||
"alice@example.com",
|
||||
"correct horse battery staple",
|
||||
salt
|
||||
);
|
||||
const verifier = client.deriveVerifier(privateKey);
|
||||
console.log(verifier.length); // 256
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import { SrpClient } from "@suaveplan/crypto-vault/srp/srp-client";
|
||||
import {
|
||||
SRP_G,
|
||||
SRP_N_2048,
|
||||
} from "@suaveplan/crypto-vault/srp/srp-group";
|
||||
import { pow } from "@noble/curves/abstract/modular";
|
||||
|
||||
// Symmetric handshake fixture: same client + a hand-rolled server share.
|
||||
const client = new SrpClient();
|
||||
const salt = client.generateSalt();
|
||||
const x = client.derivePrivateKey("alice", "pw", salt);
|
||||
const v = BigInt("0x" + Buffer.from(client.deriveVerifier(x)).toString("hex"));
|
||||
|
||||
// Server side computes B = (k*v + g^b) mod N (hand-rolled for the example).
|
||||
const k = 3n; // simplified — production: H(N||g)
|
||||
const b = 7n;
|
||||
const B = ((k * v) + pow(SRP_G, b, SRP_N_2048)) % SRP_N_2048;
|
||||
|
||||
// Client side derives session key and proof.
|
||||
const { a, A } = client.generateEphemeralKeypair();
|
||||
const result = client.deriveSessionKey(a, A, B, x);
|
||||
console.log(result.K.length); // 32
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `class SrpClient`
|
||||
|
||||
#### `new SrpClient(options?)`
|
||||
|
||||
`SrpClientOptions` accepts:
|
||||
|
||||
- `clock?: () => number` — defaults to `Date.now`.
|
||||
- `rng?: Rng` — overrides the default CSPRNG for salt and ephemeral
|
||||
generation.
|
||||
- `N?: bigint`, `g?: bigint` — group overrides for tests. Construction
|
||||
rejects any `N` shorter than 2048 bits with `SRP.WEAK_PARAMETERS`.
|
||||
|
||||
#### `generateSalt(length = 16): Uint8Array`
|
||||
|
||||
Random salt. Honours `rng?.nextBytes` if provided; otherwise uses
|
||||
`crypto.getRandomValues` via `@suaveplan/crypto-utils`.
|
||||
|
||||
#### `derivePrivateKey(identity, password, salt): Uint8Array`
|
||||
|
||||
Computes `x = H(salt, H(identity:password))`. Throws
|
||||
`SRP.INVALID_PRIVATE_KEY` for empty inputs.
|
||||
|
||||
#### `deriveVerifier(privateKey): Uint8Array`
|
||||
|
||||
Returns a 256-byte big-endian `v = g^x mod N`.
|
||||
|
||||
#### `generateEphemeralKeypair(): EphemeralKeypair`
|
||||
|
||||
`{ a: bigint; A: bigint }` — `a` is the private exponent (kept by the
|
||||
client), `A` the public form sent to the server. Reseeds when the entropy
|
||||
collapses to zero.
|
||||
|
||||
#### `deriveSessionKey(a, A, B, privateKey): SessionKeyResult`
|
||||
|
||||
Returns `{ K, S, M1 }` per the RFC. Throws `SRP.INVALID_B` when `B mod N`
|
||||
is zero.
|
||||
|
||||
#### `verifyServerProof(A, M1, K, serverProof): boolean`
|
||||
|
||||
Constant-time compare with the recomputed `M2 = H(A || M1 || K)`. Throws
|
||||
`SRP.AUTHENTICATION_FAILED` on mismatch; otherwise returns `true`.
|
||||
|
||||
#### `computeServerProof(A, M1, K): Uint8Array`
|
||||
|
||||
Helper exposed for symmetric tests and server-side adapters.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The implementation deliberately keeps every state transition pure —
|
||||
callers thread `(a, A, B, x)` through the methods rather than relying
|
||||
on instance fields.
|
||||
- All `bigint` ↔ byte conversions are big-endian and zero-padded to the
|
||||
group byte length so that hashing is deterministic regardless of the
|
||||
numeric value.
|
||||
- `k = H(N || g)` and `u = H(A || B)` use SHA-256 with both inputs padded
|
||||
to 256 bytes per RFC 5054 §2.5.3 / §2.6.
|
||||
- Constant-time comparison protects against timing-based proof guessing,
|
||||
matching the broader Suaveplan posture of using `constantTimeEqual` for
|
||||
every MAC / proof check.
|
||||
@@ -0,0 +1,71 @@
|
||||
# SRP-6a 2048-bit group
|
||||
|
||||
## Purpose
|
||||
|
||||
`srp-group.ts` exports the canonical 2048-bit MODP group from RFC 5054
|
||||
Appendix A as `bigint` constants. The SRP-6a client uses this group to
|
||||
exchange ephemeral keys, derive the shared secret, and produce mutual
|
||||
proofs. The constants are pinned because they are part of the protocol
|
||||
contract — switching groups silently would break interoperability with
|
||||
every server using the same RFC.
|
||||
|
||||
## Features
|
||||
|
||||
- `SRP_N_2048` — the 2048-bit safe prime `N` as a positive `bigint`.
|
||||
- `SRP_G` — the generator `g = 2`.
|
||||
- `SRP_N_BITLEN = 2048`, `SRP_N_BYTELEN = 256` — convenience constants for
|
||||
byte-padding and validation.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { SRP_G, SRP_N_2048 } from "@suaveplan/crypto-vault/srp/srp-group";
|
||||
console.log(SRP_N_2048.toString(2).length); // 2048
|
||||
console.log(SRP_G); // 2n
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
SRP_G,
|
||||
SRP_N_2048,
|
||||
SRP_N_BITLEN,
|
||||
SRP_N_BYTELEN,
|
||||
} from "@suaveplan/crypto-vault/srp/srp-group";
|
||||
import { pow } from "@noble/curves/abstract/modular";
|
||||
|
||||
// Compute v = g^x mod N for an arbitrary private key x.
|
||||
function makeVerifier(x: bigint): bigint {
|
||||
return pow(SRP_G, x, SRP_N_2048);
|
||||
}
|
||||
|
||||
console.log(SRP_N_BITLEN); // 2048
|
||||
console.log(SRP_N_BYTELEN); // 256
|
||||
console.log(typeof makeVerifier(123n)); // bigint
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `SRP_N_2048: bigint`
|
||||
|
||||
The exact 2048-bit safe prime defined in RFC 5054 Appendix A, parsed from
|
||||
the canonical hex constant.
|
||||
|
||||
### `SRP_G: bigint`
|
||||
|
||||
The generator: `2n`.
|
||||
|
||||
### `SRP_N_BITLEN: number`, `SRP_N_BYTELEN: number`
|
||||
|
||||
Bit length (`2048`) and byte length (`256`) of the prime.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The hex string is laid out as a TypeScript string concatenation matching
|
||||
the line breaks in the RFC for diffability against the source.
|
||||
- The constant is parsed once at module load; subsequent imports share the
|
||||
resulting `bigint`.
|
||||
- The 1024-bit and 1536-bit groups from the same RFC are intentionally
|
||||
omitted. RFC 5054 §1 states they should be deprecated for new
|
||||
deployments, and the proposal pins us to ≥ 2048 bits.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Two-Secret Key Derivation (2SKD)
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`twoskd.ts` derives the Account Unlock Key (AUK) from three independent
|
||||
factors: the user's password (something they know), the Secret Key (a
|
||||
high-entropy device-bound string — something they have), and the account
|
||||
email (binding the derivation to the account identity). 2SKD is the entry
|
||||
point of the entire crypto-vault hierarchy: every downstream key — personal
|
||||
keyset, vault key, item key — is HKDF-derived from this AUK. Compromising
|
||||
any single factor does not yield the AUK; an attacker must compromise both
|
||||
the password and the Secret Key.
|
||||
|
||||
## Features
|
||||
|
||||
- Combines `password`, `secretKey`, and `email` into a single Argon2id input
|
||||
via HMAC-SHA-256.
|
||||
- Uses the project-wide OWASP-safe Argon2id wrapper for the slow step.
|
||||
- Email is normalised (trim + lowercase) before mixing so users entering
|
||||
`Alice@Example.COM` and `alice@example.com` derive the same AUK.
|
||||
- Empty-input rejection: empty `password`, empty `secretKey`, or
|
||||
empty/whitespace `email` throws a typed `CryptoVaultError`.
|
||||
- Honours `CryptoVaultOptions.clock` and `rng` so consumers can inject
|
||||
deterministic test sources without changing the call surface.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import { deriveAuk } from "@suaveplan/crypto-vault/twoskd/twoskd";
|
||||
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
const auk = await deriveAuk(
|
||||
"correct horse battery staple",
|
||||
"A3-7PMW82-XJZS9-NQDLV-EYKBR",
|
||||
"alice@example.com",
|
||||
salt
|
||||
);
|
||||
console.log(auk.length); // 32
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import {
|
||||
DEFAULT_ARGON2ID_PARAMS,
|
||||
type Argon2idParams,
|
||||
} from "@suaveplan/crypto-vault/argon2id/argon2id";
|
||||
import { deriveAuk } from "@suaveplan/crypto-vault/twoskd/twoskd";
|
||||
|
||||
const high: Argon2idParams = {
|
||||
...DEFAULT_ARGON2ID_PARAMS,
|
||||
memoryKB: 131_072, // 128 MiB
|
||||
iterations: 4,
|
||||
};
|
||||
const salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
|
||||
// Deterministic for the same triple — useful for unlock retry without
|
||||
// re-running the user through the whole prompt sequence.
|
||||
const auk1 = await deriveAuk(
|
||||
"pw",
|
||||
"sk",
|
||||
"alice@example.com",
|
||||
salt,
|
||||
high,
|
||||
{ clock: () => 0 }
|
||||
);
|
||||
const auk2 = await deriveAuk(
|
||||
"pw",
|
||||
"sk",
|
||||
"ALICE@example.com",
|
||||
salt,
|
||||
high,
|
||||
{ clock: () => 0 }
|
||||
);
|
||||
console.log(Buffer.from(auk1).equals(Buffer.from(auk2))); // true (email normalised)
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `deriveAuk(password, secretKey, email, salt, params?, options?)`
|
||||
|
||||
- `password: string` — non-empty.
|
||||
- `secretKey: string` — non-empty (high-entropy 30+ chars recommended).
|
||||
- `email: string` — non-empty / non-whitespace; normalised internally.
|
||||
- `salt: Uint8Array` — non-empty (16+ bytes recommended).
|
||||
- `params: Argon2idParams = DEFAULT_ARGON2ID_PARAMS` — validated through
|
||||
`assertSafeArgon2idParams`.
|
||||
- `options: CryptoVaultOptions = {}` — `{ clock?, rng? }`.
|
||||
|
||||
Returns `Promise<Uint8Array>` — the 32-byte AUK by default.
|
||||
|
||||
Throws `CryptoVaultError`:
|
||||
|
||||
- `TWOSKD.MISSING_PASSWORD`, `TWOSKD.MISSING_SECRET_KEY`,
|
||||
`TWOSKD.MISSING_EMAIL` for empty inputs.
|
||||
- `ARGON2ID.UNSAFE_PARAMETERS` propagated from the Argon2id wrapper.
|
||||
- `ARGON2ID.INVALID_INPUT` for an empty `salt`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- HMAC-SHA-256 is used as the input commit step rather than concatenation
|
||||
alone, so a 32-byte fixed-length string enters Argon2id regardless of
|
||||
the password length.
|
||||
- Email normalisation matches the rules used by upstream 1Password 2SKD
|
||||
(RFC 5321 dot-atom rules are not applied — only trim + lowercase). This
|
||||
preserves portability with other 2SKD-style password managers.
|
||||
- The order of bytes inside the HMAC message matters and is fixed:
|
||||
`password || 0x00 || email`. The null byte prevents collisions where
|
||||
`("ab", "c")` and `("a", "bc")` would otherwise hash identically.
|
||||
- `options.rng` is not consumed by 2SKD because the derivation is fully
|
||||
deterministic; it is accepted only to match the broader
|
||||
`CryptoVaultOptions` shape and is silently discarded.
|
||||
@@ -0,0 +1,101 @@
|
||||
# crypto-vault — Public types
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`types.ts` collects the cross-module type surface shared across
|
||||
`@suaveplan/crypto-vault`. Every type here is consumed by ≥2 modules — for
|
||||
example `Argon2idParams` is used by both the `argon2id/argon2id` and
|
||||
`twoskd/twoskd` derivation paths, and `Auk` / `PersonalKeyset` flow from the
|
||||
2SKD layer through the key-hierarchy layer. Pulling these types into a single
|
||||
file keeps the public contract reviewable and prevents accidental shape drift
|
||||
between feature modules.
|
||||
|
||||
## Features
|
||||
|
||||
- `CryptoVaultOptions` — common `{ clock?, rng? }` bag accepted by every
|
||||
telemetry-aware entry point.
|
||||
- `Argon2idParams` — Suaveplan-flavoured Argon2id parameter shape mapped onto
|
||||
noble's underlying `(memoryCost, timeCost, parallelism, length)` at the
|
||||
call site.
|
||||
- `PersonalKeyset` — three-key bundle (`encryptKey`, `signKey`, `kxKey`)
|
||||
derived from the AUK.
|
||||
- `VaultKey`, `ItemKey`, `Auk` — typed aliases for 32-byte `Uint8Array`s
|
||||
flowing through the hierarchy.
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```ts
|
||||
import type { Argon2idParams } from "@suaveplan/crypto-vault/types";
|
||||
|
||||
const owaspMin: Argon2idParams = {
|
||||
memoryKB: 65536,
|
||||
iterations: 3,
|
||||
parallelism: 4,
|
||||
outputLength: 32,
|
||||
};
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```ts
|
||||
import type {
|
||||
Auk,
|
||||
CryptoVaultOptions,
|
||||
PersonalKeyset,
|
||||
} from "@suaveplan/crypto-vault/types";
|
||||
|
||||
function summarisePerKeyset(
|
||||
auk: Auk,
|
||||
keyset: PersonalKeyset,
|
||||
options?: CryptoVaultOptions
|
||||
): { ts: number; lengths: [number, number, number] } {
|
||||
const ts = (options?.clock ?? Date.now)();
|
||||
return {
|
||||
ts,
|
||||
lengths: [
|
||||
keyset.encryptKey.length,
|
||||
keyset.signKey.length,
|
||||
keyset.kxKey.length,
|
||||
],
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `CryptoVaultOptions`
|
||||
|
||||
- `clock?: Clock` — wall-clock time source from `@suaveplan/types`.
|
||||
- `rng?: Rng` — random source from `@suaveplan/types`.
|
||||
|
||||
### `Argon2idParams`
|
||||
|
||||
- `memoryKB: number` — memory cost in KiB.
|
||||
- `iterations: number` — Argon2id `t` (time cost).
|
||||
- `parallelism: number` — Argon2id `p` (lane count).
|
||||
- `outputLength: number` — derived key length in bytes.
|
||||
|
||||
### `PersonalKeyset`
|
||||
|
||||
- `encryptKey: Uint8Array` — 32 bytes.
|
||||
- `signKey: Uint8Array` — 32 bytes.
|
||||
- `kxKey: Uint8Array` — 32 bytes.
|
||||
|
||||
### `VaultKey`, `ItemKey`, `Auk`
|
||||
|
||||
Typed aliases for `Uint8Array`. They are nominal in name only (TypeScript
|
||||
treats them structurally) — runtime guarantees come from the modules that
|
||||
produce them (each enforces the 32-byte length).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The file lives outside any feature directory to make it cheap to import
|
||||
without dragging in module-level OTel instruments or runtime code.
|
||||
- `Argon2idParams` deliberately uses Suaveplan-flavoured names. Internally
|
||||
these map to noble's `Argon2idOptions` (`memoryCost`, `timeCost`,
|
||||
`parallelism`, `length`) at the wrapper boundary in `argon2id/argon2id.ts`.
|
||||
- `Clock` and `Rng` are imported from `@suaveplan/types` so this file does
|
||||
not re-declare cross-cutting contracts.
|
||||
Reference in New Issue
Block a user