281 lines
21 KiB
Markdown
281 lines
21 KiB
Markdown
# Spec: @suaveplan/crypto-vault
|
|
|
|
## Capability
|
|
|
|
Cryptographic foundation for the Vaulted password-manager ecosystem. Provides BIP39 mnemonic generation, Argon2id-based HD key derivation, two-secret key derivation (2SKD: password + Secret Key → Account Unlock Key), SRP-6a authentication primitives, and a hierarchical key derivation path (AUK → personal keyset → vault key → item key). All key material is held in secure memory and zeroized on lock.
|
|
|
|
## Requirements
|
|
|
|
### Requirement: BIP39 mnemonic generation and entropy conversion
|
|
|
|
The package SHALL export `generateMnemonic(strength?: 128 | 256, options?: { rng?: Rng }): string` producing a BIP39-compliant mnemonic phrase with 12 words (128-bit) or 24 words (256-bit) of entropy, and `mnemonicToEntropy(mnemonic: string): Uint8Array` which converts a mnemonic back to its raw entropy bytes. `entropyToMnemonic(entropy: Uint8Array): string` SHALL accept only 16-byte or 32-byte inputs and produce the canonical English word list phrase. Invalid entropy lengths MUST throw `CryptoVaultError` with code `CRYPTOVAULT.BIP39.INVALID_ENTROPY_LENGTH`. Invalid mnemonics (checksum failure, unknown words) MUST throw `CryptoVaultError` with code `CRYPTOVAULT.BIP39.INVALID_MNEMONIC`.
|
|
|
|
#### Scenario: Generate a 12-word mnemonic from 128-bit entropy
|
|
|
|
- **WHEN** `generateMnemonic(128)` is called
|
|
- **THEN** the result is a string of exactly 12 space-separated words from the English BIP39 word list AND the final word satisfies the checksum
|
|
|
|
#### Scenario: Generate a 24-word mnemonic from 256-bit entropy
|
|
|
|
- **WHEN** `generateMnemonic(256)` is called
|
|
- **THEN** the result is a string of exactly 24 space-separated words from the English BIP39 word list AND the final word satisfies the checksum
|
|
|
|
#### Scenario: Entropy round-trips through mnemonic
|
|
|
|
- **WHEN** 16-byte or 32-byte entropy is passed through `entropyToMnemonic` then `mnemonicToEntropy`
|
|
- **THEN** the resulting bytes equal the input byte-for-byte
|
|
|
|
#### Scenario: Invalid entropy length is rejected
|
|
|
|
- **WHEN** `entropyToMnemonic` is called with a `Uint8Array` whose length is not 16 or 32
|
|
- **THEN** a `CryptoVaultError` with code `CRYPTOVAULT.BIP39.INVALID_ENTROPY_LENGTH` is thrown
|
|
|
|
#### Scenario: Invalid mnemonic is rejected
|
|
|
|
- **WHEN** `mnemonicToEntropy` is called with a malformed mnemonic (bad checksum, unknown word, or wrong word count)
|
|
- **THEN** a `CryptoVaultError` with code `CRYPTOVAULT.BIP39.INVALID_MNEMONIC` is thrown
|
|
|
|
#### Scenario: Injectable RNG determines entropy
|
|
|
|
- **WHEN** `generateMnemonic` is called with a test RNG in `options.rng`
|
|
- **THEN** the resulting mnemonic is deterministic for that RNG
|
|
|
|
**Verified by:** `src/bip39/bip39.test.ts::"generates a 12-word mnemonic"` AND `src/bip39/bip39.test.ts::"generates a 24-word mnemonic"` AND `src/bip39/bip39.test.ts::"round-trips 16-byte entropy"` AND `src/bip39/bip39.test.ts::"round-trips 32-byte entropy"` AND `src/bip39/bip39.test.ts::"rejects entropy length other than 16 or 32"` AND `src/bip39/bip39.test.ts::"rejects mnemonic with bad checksum"` AND `src/bip39/bip39.test.ts::"rejects mnemonic with unknown word"` AND `src/bip39/bip39.test.ts::"rejects empty mnemonic"` AND `src/bip39/bip39.test.ts::"injectable rng determines entropy"`
|
|
|
|
---
|
|
|
|
### Requirement: Argon2id parameterization and HD key derivation
|
|
|
|
The package SHALL export `deriveKeyWithArgon2id(password: Uint8Array, salt: Uint8Array, params: Argon2idParams): Promise<Uint8Array>` producing a derived key via Argon2id. `Argon2idParams` MUST expose `memoryKB`, `iterations`, `parallelism`, and `outputLength` with safe defaults (`memoryKB: 65536`, `iterations: 3`, `parallelism: 4`, `outputLength: 32`). Parameter values below OWASP minimums (`memoryKB < 65536`, `iterations < 3`) MUST throw `CryptoVaultError` with code `CRYPTOVAULT.ARGON2ID.UNSAFE_PARAMETERS`. `deriveMasterKeyFromMnemonic(mnemonic: string, passphrase?: string)` SHALL derive the BIP39 seed (mnemonic + optional passphrase → 512-bit seed via PBKDF2) then feed it into a BIP32-like HD path to produce a 256-bit master key.
|
|
|
|
#### Scenario: Argon2id derives a 32-byte key from password and salt
|
|
|
|
- **WHEN** `deriveKeyWithArgon2id` is called with a password, a unique 16-byte salt, and default parameters
|
|
- **THEN** the result is a 32-byte `Uint8Array` AND the same inputs always produce the same output
|
|
|
|
#### Scenario: Different salts produce different keys
|
|
|
|
- **WHEN** `deriveKeyWithArgon2id` is called with the same password but different salts
|
|
- **THEN** the resulting keys are different
|
|
|
|
#### Scenario: Unsafe parameters are rejected
|
|
|
|
- **WHEN** `deriveKeyWithArgon2id` is called with `memoryKB < 65536` or `iterations < 3`
|
|
- **THEN** a `CryptoVaultError` with code `CRYPTOVAULT.ARGON2ID.UNSAFE_PARAMETERS` is thrown
|
|
|
|
#### Scenario: BIP39 seed derivation produces consistent master key
|
|
|
|
- **WHEN** `deriveMasterKeyFromMnemonic` is called with a valid mnemonic and no passphrase
|
|
- **THEN** the result is a 32-byte key that is deterministic for that mnemonic
|
|
|
|
#### Scenario: Passphrase changes the derived master key
|
|
|
|
- **WHEN** `deriveMasterKeyFromMnemonic` is called with the same mnemonic but different passphrases
|
|
- **THEN** the resulting master keys are different
|
|
|
|
**Verified by:** `src/argon2id/argon2id.test.ts::"derives a 32-byte key with default params"` AND `src/argon2id/argon2id.test.ts::"deterministic for same inputs"` AND `src/argon2id/argon2id.test.ts::"different salts produce different keys"` AND `src/argon2id/argon2id.test.ts::"rejects memory below 65536 KB"` AND `src/argon2id/argon2id.test.ts::"rejects iterations below 3"` AND `src/hd-key/hd-key.test.ts::"derives deterministic master key from mnemonic"` AND `src/hd-key/hd-key.test.ts::"passphrase changes derived key"` AND `src/hd-key/hd-key.test.ts::"rejects invalid mnemonic"`
|
|
|
|
---
|
|
|
|
### Requirement: Two-Secret Key Derivation (2SKD)
|
|
|
|
The package SHALL export `deriveAuk(password: string, secretKey: string, email: string, salt: Uint8Array, params: Argon2idParams, options?: { clock?: Clock; rng?: Rng }): Promise<Uint8Array>` which implements 2SKD: the Account Unlock Key (AUK) is derived from the user's password AND their Secret Key (a high-entropy device-bound string). The derivation SHALL mix the password, the Secret Key, and the account-specific email into the Argon2id password input so that compromise of either factor alone does not reveal the AUK. The output MUST be 32 bytes. The exact combinator is: compute `combined = HMAC-SHA256(secretKey, password + "\0" + email)` then run `Argon2id(combined, salt, params)`.
|
|
|
|
#### Scenario: Same inputs produce the same AUK
|
|
|
|
- **WHEN** `deriveAuk` is called with identical password, secretKey, email, salt, and params
|
|
- **THEN** the resulting 32-byte AUK is identical across calls
|
|
|
|
#### Scenario: Different password produces different AUK
|
|
|
|
- **WHEN** `deriveAuk` is called with the same secretKey and email but a different password
|
|
- **THEN** the resulting AUK is different
|
|
|
|
#### Scenario: Different Secret Key produces different AUK
|
|
|
|
- **WHEN** `deriveAuk` is called with the same password and email but a different Secret Key
|
|
- **THEN** the resulting AUK is different
|
|
|
|
#### Scenario: Different email produces different AUK
|
|
|
|
- **WHEN** `deriveAuk` is called with the same password and secretKey but a different email
|
|
- **THEN** the resulting AUK is different
|
|
|
|
#### Scenario: Injectable clock and RNG are accepted
|
|
|
|
- **WHEN** `deriveAuk` is called with `options.clock` and `options.rng`
|
|
- **THEN** the derivation succeeds and the options are forwarded to telemetry and entropy paths respectively
|
|
|
|
**Verified by:** `src/twoskd/twoskd.test.ts::"deterministic AUK for same inputs"` AND `src/twoskd/twoskd.test.ts::"different password yields different AUK"` AND `src/twoskd/twoskd.test.ts::"different secret key yields different AUK"` AND `src/twoskd/twoskd.test.ts::"different email yields different AUK"` AND `src/twoskd/twoskd.test.ts::"rejects empty password"` AND `src/twoskd/twoskd.test.ts::"rejects empty secret key"` AND `src/twoskd/twoskd.test.ts::"accepts injectable clock and rng"`
|
|
|
|
---
|
|
|
|
### Requirement: Key hierarchy — AUK to personal keyset to vault key to item key
|
|
|
|
The package SHALL export `derivePersonalKeyset(auk: Uint8Array): PersonalKeyset` containing an encrypt key, a sign key, and a key-exchange key, all derived via HKDF-SHA256 from the AUK with distinct context labels. `deriveVaultKey(personalKeyset: PersonalKeyset, vaultId: string): VaultKey` SHALL derive a vault-specific key via HKDF using the vault UUID as salt/info. `deriveItemKey(vaultKey: VaultKey, itemId: string): ItemKey` SHALL derive a per-item key via HKDF using the item UUID. All derived keys MUST be 32-byte `Uint8Array` instances. The hierarchy ensures that vault rotation or item deletion only requires re-deriving the affected branch.
|
|
|
|
#### Scenario: Personal keyset derives three distinct keys from AUK
|
|
|
|
- **WHEN** `derivePersonalKeyset` is called with a 32-byte AUK
|
|
- **THEN** the result contains `encryptKey`, `signKey`, and `kxKey` that are pairwise different 32-byte arrays
|
|
|
|
#### Scenario: Same AUK produces the same personal keyset
|
|
|
|
- **WHEN** `derivePersonalKeyset` is called twice with the same AUK
|
|
- **THEN** both results contain identical key material
|
|
|
|
#### Scenario: Vault key is deterministic per vaultId
|
|
|
|
- **WHEN** `deriveVaultKey` is called with the same personalKeyset and vaultId
|
|
- **THEN** the resulting vault key is identical
|
|
|
|
#### Scenario: Different vaultIds produce different vault keys
|
|
|
|
- **WHEN** `deriveVaultKey` is called with the same personalKeyset but different vaultIds
|
|
- **THEN** the resulting vault keys are different
|
|
|
|
#### Scenario: Item key is deterministic per itemId
|
|
|
|
- **WHEN** `deriveItemKey` is called with the same vaultKey and itemId
|
|
- **THEN** the resulting item key is identical
|
|
|
|
#### Scenario: Different itemIds produce different item keys
|
|
|
|
- **WHEN** `deriveItemKey` is called with the same vaultKey but different itemIds
|
|
- **THEN** the resulting item keys are different
|
|
|
|
**Verified by:** `src/key-hierarchy/key-hierarchy.test.ts::"derives three distinct keys in personal keyset"` AND `src/key-hierarchy/key-hierarchy.test.ts::"deterministic personal keyset for same AUK"` AND `src/key-hierarchy/key-hierarchy.test.ts::"deterministic vault key for same vaultId"` AND `src/key-hierarchy/key-hierarchy.test.ts::"different vaultId yields different vault key"` AND `src/key-hierarchy/key-hierarchy.test.ts::"deterministic item key for same itemId"` AND `src/key-hierarchy/key-hierarchy.test.ts::"different itemId yields different item key"` AND `src/key-hierarchy/key-hierarchy.test.ts::"rejects non-32-byte AUK"` AND `src/key-hierarchy/key-hierarchy.test.ts::"rejects empty vaultId"` AND `src/key-hierarchy/key-hierarchy.test.ts::"rejects empty itemId"`
|
|
|
|
---
|
|
|
|
### Requirement: SRP-6a client integration
|
|
|
|
The package SHALL export `SrpClient` implementing the SRP-6a protocol for zero-knowledge password authentication. The constructor SHALL accept an optional options bag with `clock?: Clock` and `rng?: Rng`. `SrpClient` MUST expose `generateSalt(): Uint8Array`, `derivePrivateKey(email: string, password: string, salt: Uint8Array): Uint8Array`, `deriveVerifier(privateKey: Uint8Array): Uint8Array`, `generateEphemeralKeypair(): EphemeralKeypair`, `deriveSessionKey(clientEphemeral: Uint8Array, serverEphemeral: Uint8Array, privateKey: Uint8Array, salt: Uint8Array): SessionKeyResult`, and `verifyServerProof(clientProof: Uint8Array, serverProof: Uint8Array, sessionKey: Uint8Array): boolean`. The implementation SHALL use the group's safe prime parameters (2048-bit minimum) and SHA-256 as the one-way function per RFC 5054. Bad server proof MUST reject authentication with `CryptoVaultError` code `CRYPTOVAULT.SRP.AUTHENTICATION_FAILED`.
|
|
|
|
#### Scenario: Full SRP-6a handshake produces matching session keys
|
|
|
|
- **WHEN** client and server both run SRP-6a with the same email, password, and salt
|
|
- **THEN** both sides derive the same session key AND the client successfully verifies the server proof
|
|
|
|
#### Scenario: Wrong password yields different session key
|
|
|
|
- **WHEN** the client uses a different password than the one registered for the salt/verifier
|
|
- **THEN** the derived session key differs from the server's AND `verifyServerProof` returns false
|
|
|
|
#### Scenario: Bad server proof is rejected
|
|
|
|
- **WHEN** `verifyServerProof` is called with a tampered server proof
|
|
- **THEN** it returns false AND throws `CryptoVaultError` with code `CRYPTOVAULT.SRP.AUTHENTICATION_FAILED`
|
|
|
|
#### Scenario: Verifier is deterministic for identical inputs
|
|
|
|
- **WHEN** `deriveVerifier` is called twice with the same private key
|
|
- **THEN** both outputs are identical
|
|
|
|
#### Scenario: Injectable RNG produces deterministic salt
|
|
|
|
- **WHEN** `generateSalt` is called with a test RNG passed to the constructor
|
|
- **THEN** the salt is deterministic for that RNG
|
|
|
|
**Verified by:** `src/srp/srp-client.test.ts::"full handshake produces matching session keys"` AND `src/srp/srp-client.test.ts::"wrong password yields authentication failure"` AND `src/srp/srp-client.test.ts::"tampered server proof is rejected"` AND `src/srp/srp-client.test.ts::"verifier is deterministic"` AND `src/srp/srp-client.test.ts::"salt is unique per generation"` AND `src/srp/srp-client.test.ts::"rejects weak group parameters"` AND `src/srp/srp-client.test.ts::"injectable rng produces deterministic salt"`
|
|
|
|
---
|
|
|
|
### Requirement: Secure memory and zeroization
|
|
|
|
The package SHALL hold all sensitive key material (`Auk`, `PersonalKeyset`, `VaultKey`, `ItemKey`, `SessionKey`) in `SecureBuffer` instances backed by `Uint8Array` with eager zeroization via `fill(0)` on close; memory locking via `mlock` is deferred to a future native binding. `SecureBuffer` SHALL accept constructor options `clock?: Clock` and `rng?: Rng`. The package SHALL provide `zeroizeAll(): void` which overwrites every live buffer with zeros then releases the reference. No key material SHALL be exposed as a plain string or unprotected `Uint8Array` outside the secure envelope. After `zeroizeAll()`, any attempt to use a previously derived key MUST throw `CryptoVaultError` with code `CRYPTOVAULT.SECURE.ZEROIZED`.
|
|
|
|
#### Scenario: Zeroization overwrites all live key material
|
|
|
|
- **WHEN** `zeroizeAll` is called after deriving AUK, personal keyset, vault key, and item key
|
|
- **THEN** every underlying buffer contains only zero bytes
|
|
|
|
#### Scenario: Using a zeroized key throws
|
|
|
|
- **WHEN** a key is accessed after `zeroizeAll` has been called
|
|
- **THEN** a `CryptoVaultError` with code `CRYPTOVAULT.SECURE.ZEROIZED` is thrown
|
|
|
|
#### Scenario: SecureBuffer does not leak via toString
|
|
|
|
- **WHEN** a `SecureBuffer` containing key material is coerced to string
|
|
- **THEN** the result is `"[SecureBuffer]"` or equivalent — never the raw bytes
|
|
|
|
#### Scenario: Injectable clock and RNG are accepted
|
|
|
|
- **WHEN** a `SecureBuffer` is constructed with `clock` and `rng` options
|
|
- **THEN** construction succeeds and the options are available for allocation telemetry and secure randomness
|
|
|
|
**Verified by:** `src/secure/secure-buffer.test.ts::"zeroizeAll overwrites all live buffers"` AND `src/secure/secure-buffer.test.ts::"accessing zeroized key throws"` AND `src/secure/secure-buffer.test.ts::"toString does not leak bytes"` AND `src/secure/secure-buffer.test.ts::"JSON.stringify does not leak bytes"` AND `src/secure/secure-buffer.test.ts::"clone creates independent copy"` AND `src/secure/secure-buffer.test.ts::"slice does not share underlying buffer"` AND `src/secure/secure-buffer.test.ts::"accepts injectable clock and rng"`
|
|
|
|
## ADDED Requirements (extend-crypto-vault-orchestration — 2026-05-18)
|
|
|
|
### Requirement: createRootKeys produces a keychain + encrypted vault + signed public-key bytes
|
|
|
|
The package SHALL export `async createRootKeys(password: string, options?: CreateRootKeysOptions): Promise<RootKeysResult>` that generates a fresh root `Keychain` via `@suaveplan/crypto` `generateKeychain`, derives an Argon2id KEK from `password` (defaults from `DEFAULT_ARGON2ID_PARAMS`), AEAD-encrypts the canonical-encoded keychain bytes into vault bytes, extracts the public keychain via `extractPublicKeys`, and returns `{ keychain, vaultBytes, publicKeyBytes, publicKeys }`. On success, `openVault(vaultBytes, password)` SHALL recover an equivalent keychain.
|
|
|
|
**Verified by:** `src/orchestration/orchestration.test.ts::"createRootKeys output round-trips through openVault"`
|
|
|
|
---
|
|
|
|
### Requirement: createServerKeys derives a versioned child keychain from root master
|
|
|
|
The package SHALL export `async createServerKeys(rootKeys, serverPassword, options?): Promise<ServerKeysResult>` that derives a server `Keychain` from `rootKeys.keychain.master.private`, encrypts under `serverPassword`, and returns `{ keychain, vaultBytes, publicKeyBytes, publicKeys, version }`. Default `version` SHALL be `1`. Re-running with same `(rootKeys, password, version)` SHALL produce identical sign-slot public keys (deterministic derivation).
|
|
|
|
**Verified by:** `src/orchestration/orchestration.test.ts::"createServerKeys deterministically derives from root master"`
|
|
|
|
---
|
|
|
|
### Requirement: rollServerKeys bumps the version deterministically
|
|
|
|
The package SHALL export `async rollServerKeys(rootKeys, currentVersion, serverPassword, options?): Promise<ServerKeysResult>` returning a result at `version = currentVersion + 1`. `currentVersion < 0` SHALL raise `CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.INVALID_VERSION)`.
|
|
|
|
**Verified by:** `src/orchestration/orchestration.test.ts::"rollServerKeys increments version"` AND `src/orchestration/orchestration.test.ts::"rollServerKeys rejects negative currentVersion"`
|
|
|
|
---
|
|
|
|
### Requirement: openVault and packVault are inverses
|
|
|
|
The package SHALL export `async openVault(vaultBytes, password)` and `async packVault(keychain, password, options?)`. `openVault(packVault(k, p), p)` SHALL produce a keychain with the same canonical-encoded public bytes as `k`. Wrong password or tampered bytes SHALL raise `CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.DECRYPT_FAILED)`.
|
|
|
|
**Verified by:** `src/orchestration/orchestration.test.ts::"openVault and packVault round-trip"` AND `src/orchestration/orchestration.test.ts::"openVault rejects wrong password"` AND `src/orchestration/orchestration.test.ts::"openVault rejects tampered bytes"`
|
|
|
|
---
|
|
|
|
### Requirement: viewPublicKeys decodes a signed public-key file with optional verification
|
|
|
|
The package SHALL export `async viewPublicKeys(publicKeyBytes, optionalSignKey?): Promise<PublicKeychain>` that decodes via `decodePublicKeychain`. When `optionalSignKey` is provided, the embedded signature SHALL be verified; mismatch SHALL raise `CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.PUBLIC_KEY_SIGNATURE_INVALID)`.
|
|
|
|
**Verified by:** `src/orchestration/orchestration.test.ts::"viewPublicKeys verifies signature when sign key supplied"` AND `src/orchestration/orchestration.test.ts::"viewPublicKeys rejects wrong sign key"`
|
|
|
|
---
|
|
|
|
### Requirement: Storage helpers wrap AbstractStorage idempotently
|
|
|
|
The package SHALL export `async saveVaultToStorage(storage, key, vaultBytes)`, `async savePublicKeysToStorage(storage, key, publicKeyBytes)`, and `async getDataFromStorage(storage, key)`. `getDataFromStorage` SHALL raise `CryptoVaultError(CRYPTOVAULT.ORCHESTRATION.STORAGE_KEY_NOT_FOUND)` when the key is absent. Save helpers SHALL be idempotent.
|
|
|
|
**Verified by:** `src/orchestration/orchestration.test.ts::"saveVaultToStorage + getDataFromStorage round-trip"` AND `src/orchestration/orchestration.test.ts::"getDataFromStorage raises CRYPTO_VAULT_STORAGE_KEY_NOT_FOUND on missing key"` AND `src/orchestration/orchestration.test.ts::"saveVaultToStorage is idempotent"`
|
|
|
|
---
|
|
|
|
### Requirement: New error codes registered without breaking existing ones
|
|
|
|
The package SHALL extend `CRYPTO_VAULT_CODES` with `ORCHESTRATION.DECRYPT_FAILED`, `ORCHESTRATION.PUBLIC_KEY_SIGNATURE_INVALID`, `ORCHESTRATION.STORAGE_KEY_NOT_FOUND`, `ORCHESTRATION.INVALID_VERSION`. Every existing code remains registered. Every new code SHALL be raised by at least one production call site.
|
|
|
|
**Verified by:** `src/orchestration/orchestration.spec-conformance.test.ts::"every new orchestration code is thrown from a production path"`
|
|
|
|
---
|
|
|
|
### Requirement: Subpath export, sibling docs, no breaking change to existing surface
|
|
|
|
The package SHALL declare `./orchestration/orchestration` in `package.json` `exports`. `src/orchestration/orchestration.ts` SHALL have a sibling `.md` of ≥200 words. Existing exports SHALL be unchanged.
|
|
|
|
**Verified by:** `tests/dep-graph.test.ts::"orchestration subpath export is reachable"` AND `tests/dep-graph.test.ts::"orchestration.ts has a sibling .md"`
|
|
|
|
## Error codes
|
|
|
|
`CRYPTOVAULT.BIP39.INVALID_ENTROPY_LENGTH`, `CRYPTOVAULT.BIP39.INVALID_MNEMONIC`, `CRYPTOVAULT.ARGON2ID.UNSAFE_PARAMETERS`, `CRYPTOVAULT.TWOSKD.MISSING_PASSWORD`, `CRYPTOVAULT.TWOSKD.MISSING_SECRET_KEY`, `CRYPTOVAULT.HIERARCHY.INVALID_AUK_LENGTH`, `CRYPTOVAULT.HIERARCHY.EMPTY_VAULT_ID`, `CRYPTOVAULT.HIERARCHY.EMPTY_ITEM_ID`, `CRYPTOVAULT.SRP.AUTHENTICATION_FAILED`, `CRYPTOVAULT.SRP.WEAK_PARAMETERS`, `CRYPTOVAULT.SECURE.ZEROIZED`, `CRYPTOVAULT.ORCHESTRATION.DECRYPT_FAILED`, `CRYPTOVAULT.ORCHESTRATION.PUBLIC_KEY_SIGNATURE_INVALID`, `CRYPTOVAULT.ORCHESTRATION.STORAGE_KEY_NOT_FOUND`, `CRYPTOVAULT.ORCHESTRATION.INVALID_VERSION`
|
|
|
|
## Version
|
|
|
|
Published: `0.1.0`. Spec deltas folded: `crypto-vault`, `extend-crypto-vault-orchestration` (2026-05-18).
|