From 6e9414bc81802d76f02edd2329c4eb5d603b1223 Mon Sep 17 00:00:00 2001 From: "glm-5.2" Date: Fri, 19 Jun 2026 09:49:06 +0000 Subject: [PATCH] =?UTF-8?q?docs(architecture):=20add=20ADR-020,=20resolve?= =?UTF-8?q?=20OQ-20=20=E2=80=94=20HD=20derivation=20for=20encryption=20key?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The vault uses SLIP-0010 HD derivation from the BIP39 seed for the AES-256-GCM encryption key, not PBKDF2. This replaces the TypeScript predecessor's (@alkdev/storage/src/graphs/crypto.ts) PBKDF2-based approach. Key decisions: - HD derivation at m/74'/2'/0'/0' produces the encryption key - PBKDF2 is not implemented in the vault; no password-based derivation - salt field is unused in v2 (wire-format compat only) - key_version=1 reserved for TS PBKDF2 data; key_version=2 for vault HD - TS-encrypted data requires one-time migration to v2 - CURRENT_KEY_VERSION changes from 1 to 2 (source drift flagged) OQ-20 resolved: the encryption key derivation method is locked. OQ-22 (key rotation workflow) remains open but does not block implementation. --- docs/architecture/README.md | 7 +- docs/architecture/crates/vault/README.md | 5 +- docs/architecture/crates/vault/encryption.md | 89 ++++--- .../crates/vault/mnemonic-derivation.md | 5 +- docs/architecture/crates/vault/service.md | 3 +- .../020-hd-derivation-for-encryption-keys.md | 217 ++++++++++++++++++ docs/architecture/open-questions.md | 12 +- docs/architecture/overview.md | 3 +- 8 files changed, 296 insertions(+), 45 deletions(-) create mode 100644 docs/architecture/decisions/020-hd-derivation-for-encryption-keys.md diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 34d7a37..178458c 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -7,9 +7,9 @@ last_updated: 2026-06-19 ## Current State -**Pre-implementation.** The project has completed a pivot from a three-layer model to an ALPN-as-service model. The greenfield workspace contains only `alknet-vault` (stable — implementation exists) and research/reference material. Foundational ADRs (001–019) are in place, including the BiStream type definition (ADR-007), vault integration (ADR-008), ALPN router/endpoint (ADR-010), AuthContext structure (ADR-011), call protocol stream model (ADR-012), Rust as canonical implementation language (ADR-013), secret material flow with capability injection (ADR-014), privilege model with authority context (ADR-015), abort cascade for nested calls (ADR-016), call protocol client and adapter contract (ADR-017), vault standalone crate (ADR-018), and vault assembly-layer-only access (ADR-019). The alknet-core, alknet-call, and alknet-vault crate specs are in draft. +**Pre-implementation.** The project has completed a pivot from a three-layer model to an ALPN-as-service model. The greenfield workspace contains only `alknet-vault` (stable — implementation exists) and research/reference material. Foundational ADRs (001–020) are in place, including the BiStream type definition (ADR-007), vault integration (ADR-008), ALPN router/endpoint (ADR-010), AuthContext structure (ADR-011), call protocol stream model (ADR-012), Rust as canonical implementation language (ADR-013), secret material flow with capability injection (ADR-014), privilege model with authority context (ADR-015), abort cascade for nested calls (ADR-016), call protocol client and adapter contract (ADR-017), vault standalone crate (ADR-018), vault assembly-layer-only access (ADR-019), and HD derivation for encryption keys (ADR-020). The alknet-core, alknet-call, and alknet-vault crate specs are in draft. -**Next step**: Review the vault spec documents (newly added), then begin implementation. All open questions for the core and call crates are resolved; the vault crate has three open/deferred OQs (OQ-20, OQ-21, OQ-22) that do not block implementation. +**Next step**: Review the vault spec documents, then begin implementation. All open questions for the core and call crates are resolved; the vault crate has two open/deferred OQs (OQ-21, OQ-22) that do not block implementation. ## Architecture Documents @@ -54,6 +54,7 @@ last_updated: 2026-06-19 | [017](decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | Accepted | | [018](decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Accepted | | [019](decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | Accepted | +| [020](decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | Accepted | ## Open Questions @@ -79,9 +80,9 @@ See [open-questions.md](open-questions.md) for the full tracker. - **OQ-13**: Operation path format — `/{service}/{op}` is the correct design for alknet-call, not a simplification - **OQ-14**: Batch operation semantics — multiple correlated `call.requested` events is the correct protocol design, not a simplification - **OQ-19**: Session-scoped registries — agent-written operations via `OperationEnv` trait layering; protocol doesn't need changes; `OperationEnv` must remain a trait +- **OQ-20**: Encryption key derivation — HD derivation from BIP39 seed, not PBKDF2; salt field unused in v2 (wire-format compat) (ADR-020) **Open (low priority, does not block implementation):** -- **OQ-20**: Salt/KDF Phase B — the `EncryptedData.salt` field is reserved; v1 does not use it. Two-way door. - **OQ-22**: Key rotation mechanism — key versioning is in place; the rotation workflow is not specced. Two-way door. **Deferred (not active):** diff --git a/docs/architecture/crates/vault/README.md b/docs/architecture/crates/vault/README.md index a834762..c839067 100644 --- a/docs/architecture/crates/vault/README.md +++ b/docs/architecture/crates/vault/README.md @@ -29,7 +29,7 @@ cross the network. | Document | Status | Description | |----------|--------|-------------| | [mnemonic-derivation.md](mnemonic-derivation.md) | draft | BIP39, SLIP-0010, BIP-0032, derivation paths, key types | -| [encryption.md](encryption.md) | draft | AES-256-GCM, EncryptedData, key versioning, salt (Phase B reserved) | +| [encryption.md](encryption.md) | draft | AES-256-GCM, EncryptedData, key versioning, HD derivation (ADR-020) | | [service.md](service.md) | draft | VaultServiceHandle lifecycle, actor dispatch, cache, error model | | [protocol.md](protocol.md) | draft | VaultProtocol irpc messages, DerivedKey redaction, serialization | @@ -43,12 +43,13 @@ cross the network. | [014](../../decisions/014-secret-material-flow-and-capability-injection.md) | Secret Material Flow and Capability Injection | Capabilities carry vault-derived material | | [018](../../decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Zero alknet crate dependencies | | [019](../../decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | The assembly layer is the sole caller | +| [020](../../decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | SLIP-0010 derivation, not PBKDF2; salt unused in v2 | ## Relevant Open Questions | OQ | Title | Status | Relevance | |----|-------|--------|-----------| -| OQ-20 | Salt/KDF Phase B | open | Salt field is reserved; v1 does not use it | +| OQ-20 | Encryption key derivation | resolved (ADR-020) | HD derivation from seed; salt field unused in v2 | | OQ-21 | Remote vault administration | deferred | Network unlock not supported; needs ADR if ever needed | | OQ-22 | Key rotation mechanism | open | Key versioning is in place; rotation workflow is not specced | diff --git a/docs/architecture/crates/vault/encryption.md b/docs/architecture/crates/vault/encryption.md index 1198db5..a44f427 100644 --- a/docs/architecture/crates/vault/encryption.md +++ b/docs/architecture/crates/vault/encryption.md @@ -36,6 +36,27 @@ dangerous) plaintext. GCM is also hardware-accelerated on modern CPUs (AES-NI), making it fast enough that encryption is never a bottleneck. +## Key Derivation: HD, Not PBKDF2 + +The encryption key is derived from the BIP39 seed via SLIP-0010 HD +derivation at path `m/74'/2'/0'/0'` (`PATHS::ENCRYPTION`). This is a +deliberate choice over the PBKDF2 approach used by the TypeScript +predecessor (`@alkdev/storage/src/graphs/crypto.ts`). See ADR-020 for the +full rationale. + +| Aspect | TS predecessor (PBKDF2) | Vault (HD derivation) | +|--------|--------------------------|----------------------| +| Secret input | Password (user-provided) | BIP39 seed (64 bytes) | +| Salt role | Load-bearing — part of key derivation | Unused — stored for wire-format compat | +| Derivation | PBKDF2 (100k iterations) | SLIP-0010 (a few HMACs) | +| Speed | Intentionally slow | Instant | +| Reproducible | Only with exact password | Deterministic from mnemonic | +| key_version | 1 | 2 | + +Data encrypted by the TS implementation (PBKDF2, key_version=1) **cannot be +decrypted by the vault** — the keys are different even if the password +equals the mnemonic. Migration is a one-time re-encryption (see ADR-020). + ## Encryption Key The encryption key is derived from the seed at path `m/74'/2'/0'/0'` @@ -77,7 +98,7 @@ changing it breaks both `alknet-storage` and the TypeScript consumer. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] pub struct EncryptedData { pub key_version: u32, // rotation tracking - pub salt: String, // base64, 32 bytes — reserved for Phase B (see OQ-20) + pub salt: String, // base64, 32 bytes — unused in v2 (wire-format compat, see ADR-020) pub iv: String, // base64, 12 bytes — AES-GCM nonce pub data: String, // base64 — ciphertext + auth tag } @@ -88,23 +109,23 @@ compatibility. The `iv` is 12 bytes (the standard GCM nonce size). The `data` field includes the GCM authentication tag appended to the ciphertext (the `aes-gcm` crate handles this). -### Salt field (reserved for Phase B) +### Salt field (unused in v2 — reserved for future KDF) -The `salt` field is **reserved for future KDF-based key derivation** (Phase -B, OQ-20). In v1, the encryption key is derived directly from the seed at -path `m/74'/2'/0'/0'` **without using the salt**. The salt is generated -randomly (32 bytes) and stored in `EncryptedData.salt` for forward -compatibility, but it plays no role in the v1 key derivation process. +The `salt` field is **unused for key derivation in v2** (HD derivation +doesn't need a salt — the derivation path provides domain separation). The +salt is generated randomly (32 bytes) and stored for wire-format +compatibility with the TypeScript `EncryptedDataSchema`, but it plays no +cryptographic role. -When key rotation is implemented in Phase B, the salt will be used as -input to HKDF or PBKDF2 for stretch-based key derivation, allowing the -same seed to produce different encryption keys without changing the -derivation path. The wire format does not need to change — the `salt` -field is already present and populated. +In the TypeScript predecessor, the salt was load-bearing — it was part of +the PBKDF2 key derivation. The vault's HD derivation doesn't use it, but the +field is kept in the wire format so the struct doesn't need to change if a +future KDF-based derivation is added. -This is a deliberate forward-compatibility decision: the field exists in -v1 so that v2 can use it without a format migration. The cost is 32 extra -bytes per `EncryptedData`; the benefit is no future format break. +If KDF-based key derivation is ever implemented (using HKDF or PBKDF2 with +the salt as input), it would be a new `key_version` and would not affect +existing v2 data. This is additive — see OQ-22 (key rotation) and ADR-020 +(HD derivation decision). ## Encrypt and Decrypt @@ -131,18 +152,25 @@ constraint — see below. ## Key Versioning -`CURRENT_KEY_VERSION` is `1`. Key versioning allows re-encryption when the -encryption key is rotated: +`CURRENT_KEY_VERSION` is `2`. Version `1` is reserved for the TypeScript +predecessor's PBKDF2-encrypted data (see ADR-020). Key versioning allows +re-encryption when the encryption key is rotated: 1. Derive a new key from a new derivation path or new seed -2. Decrypt all existing `EncryptedData` with key version 1 -3. Re-encrypt with key version 2 +2. Decrypt all existing `EncryptedData` with key version 2 +3. Re-encrypt with key version 3 4. Update storage The key version is stored in `EncryptedData.key_version` so decryption can select the right key. The rotation workflow itself is not specced — see OQ-22. +**The current source uses `CURRENT_KEY_VERSION = 1` with HD derivation.** +This is a drift from the spec — the source's v1 is HD-derived, but the TS +v1 is PBKDF2-derived. Same version number, different derivation. The source +must be updated to `CURRENT_KEY_VERSION = 2` to distinguish vault-encrypted +data from TS-encrypted data. See ADR-020. + ## Errors ```rust @@ -150,7 +178,7 @@ pub enum EncryptionError { Encryption(String), // encryption failed Decryption(String), // decryption failed (wrong key, tampered data, bad UTF-8) Decoding(String), // base64 decoding failed - KeyVersionMismatch { expected: u32, actual: u32 }, // reserved for Phase B + KeyVersionMismatch { expected: u32, actual: u32 }, // reserved for future rotation (OQ-22) } ``` @@ -158,29 +186,30 @@ Decryption failures are intentionally generic — they don't distinguish "wrong key" from "tampered data" from "corrupted storage" to avoid leaking information to an attacker. -`KeyVersionMismatch` is **defined but unused in v1** — neither `encrypt()` -nor `decrypt()` returns it. It is reserved for Phase B key rotation (OQ-22), -where the vault may enforce version matching before decrypting. In v1, the -`key_version` is stamped onto `EncryptedData` and `EncryptionKey` for -forward compatibility but does not gate decryption. An implementer should -not expect this variant to fire in v1. +`KeyVersionMismatch` is **defined but unused in v2** — neither `encrypt()` +nor `decrypt()` returns it. It is reserved for future key rotation +enforcement (OQ-22), where the vault may enforce version matching before +decrypting. In v2, the `key_version` is stamped onto `EncryptedData` and +`EncryptionKey` for forward compatibility but does not gate decryption. An +implementer should not expect this variant to fire in v2. ## Design Decisions | Decision | ADR | Summary | |----------|-----|---------| | AES-256-GCM for credential encryption | — | Authenticated encryption, hardware-accelerated | -| Salt reserved for Phase B (OQ-20) | — | Forward-compatible wire format; v1 doesn't use salt | +| HD derivation, not PBKDF2 | [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) | Seed-derived key; no password; deterministic | +| Salt unused in v2 (wire-format compat) | [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) | Kept for TS compat; not used in key derivation | | Key derived at `m/74'/2'/0'/0'` | — | Dedicated account for encryption keys | -| Key versioning | — | Rotation support without format break | +| Key versioning (v1=TS PBKDF2, v2=vault HD) | [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) | Distinguishes derivation methods | | All fields base64-encoded | — | JSON serialization compatibility | ## Open Questions See [open-questions.md](../../open-questions.md) for full details. -- **OQ-20** (open): Salt/KDF Phase B — when and how to use the reserved - `salt` field for KDF-based key derivation. +- **OQ-20** (resolved by ADR-020): Salt/KDF — HD derivation is the method; + the salt field is unused in v2 (wire-format compatibility only). - **OQ-22** (open): Key rotation mechanism — the key versioning is in place, but the rotation workflow (re-encrypt all data, update storage) is not specced. diff --git a/docs/architecture/crates/vault/mnemonic-derivation.md b/docs/architecture/crates/vault/mnemonic-derivation.md index 1778416..6e45f26 100644 --- a/docs/architecture/crates/vault/mnemonic-derivation.md +++ b/docs/architecture/crates/vault/mnemonic-derivation.md @@ -283,8 +283,9 @@ assembly-layer concern. See [open-questions.md](../../open-questions.md) for full details. -- **OQ-20** (open): Salt/KDF Phase B — the `EncryptedData.salt` field is - reserved; v1 does not use it. See [encryption.md](encryption.md). +- **OQ-20** (resolved by ADR-020): Encryption key derivation — HD derivation + from seed, not PBKDF2. The salt field is unused in v2. See + [encryption.md](encryption.md). ## References diff --git a/docs/architecture/crates/vault/service.md b/docs/architecture/crates/vault/service.md index 23de4de..030a3a3 100644 --- a/docs/architecture/crates/vault/service.md +++ b/docs/architecture/crates/vault/service.md @@ -302,9 +302,10 @@ error types — the CLI binary converts at the assembly boundary (ADR-018). | Decision | ADR | Summary | |----------|-----|---------| | Assembly layer is the sole caller | [ADR-019](../../decisions/019-vault-assembly-layer-only.md) | Handlers never hold a vault reference | +| Encryption key via HD derivation | [ADR-020](../../decisions/020-hd-derivation-for-encryption-keys.md) | Seed-derived key at `m/74'/2'/0'/0'`, not PBKDF2 | | RwLock for thread safety | — | Multiple readers (derive), exclusive writer (unlock/lock) | | TTL + LRU cache | — | Bounded memory, fresh keys, zeroized eviction | -| Actor for in-cluster dispatch | [ADR-005](../../decisions/005-irpc-as-call-protocol-foundation.md) | irpc message dispatch; not on the call protocol | +| Actor for in-process irpc dispatch | [ADR-005](../../decisions/005-irpc-as-call-protocol-foundation.md) | irpc message dispatch; not on the call protocol | | `derive_password` not cached | — | One-shot; caching grows cache with no reuse | ## Open Questions diff --git a/docs/architecture/decisions/020-hd-derivation-for-encryption-keys.md b/docs/architecture/decisions/020-hd-derivation-for-encryption-keys.md new file mode 100644 index 0000000..2ca5386 --- /dev/null +++ b/docs/architecture/decisions/020-hd-derivation-for-encryption-keys.md @@ -0,0 +1,217 @@ +# ADR-020: HD Derivation for Encryption Keys + +## Status + +Accepted + +## Context + +The vault encrypts external credentials (API keys, OAuth tokens) that cannot +be derived from the BIP39 seed — they're arbitrary bytes. The encryption +key for AES-256-GCM must come from somewhere. Two approaches exist: + +### The TypeScript predecessor + +The `@alkdev/storage` library (`/workspace/@alkdev/storage/src/graphs/crypto.ts`) +implemented credential encryption before the vault existed. It uses +**PBKDF2** (Password-Based Key Derivation Function 2) with a password and +salt: + +``` +key = PBKDF2(password, salt, iterations=100_000, hash=SHA-256, output=32 bytes) +``` + +- The **password** is the secret (a user-provided string, not a BIP39 seed) +- The **salt** is 16 bytes, randomly generated per encryption, and is + load-bearing — it participates in key derivation +- **Iterations**: 100,000 for key_version=1, 200,000 for key_version=2 +- The resulting key is used for AES-256-GCM encryption + +This was the right design before the vault existed: without a BIP39 seed, +PBKDF2 from a password was the only option. The salt prevents rainbow-table +attacks, and the iteration count slows brute-force. + +### The vault's approach + +The vault derives the encryption key from the BIP39 seed via **SLIP-0010 +HD derivation** at path `m/74'/2'/0'/0'`: + +``` +seed → SLIP-0010 derive(m/74'/2'/0'/0') → first 32 bytes → AES-256-GCM key +``` + +- The **seed** is the secret (64 bytes, derived from the BIP39 mnemonic) +- The **salt** is generated (32 bytes) but **not used** in key derivation — + it's stored in `EncryptedData.salt` for forward compatibility +- No PBKDF2, no iteration count, no password stretching +- The key is deterministic: the same mnemonic + path always produces the + same key + +### Why HD derivation is better now + +With the vault in place, HD derivation is strictly better than PBKDF2 for +credential encryption: + +1. **No password to manage.** The BIP39 mnemonic is already the root of + trust. PBKDF2 requires a separate password — another secret to manage, + lose, or have stolen. HD derivation uses the seed that already exists. + +2. **Deterministic and reproducible.** The same mnemonic always produces the + same encryption key at the same path. A backup node derives the same key. + PBKDF2 with a different password produces a different key — there's no + way to reproduce the key without the exact password. + +3. **No iteration overhead.** PBKDF2 with 100k iterations is intentionally + slow (that's the point — it slows brute-force). HD derivation is a few + HMAC operations — effectively instant. This matters when encrypting or + decrypting multiple credentials at startup. + +4. **Domain separation via paths.** Different encryption purposes can use + different derivation paths (`m/74'/2'/0'/0'` for v1, `m/74'/2'/1'/0'` + for a future v2). PBKDF2 has no equivalent — the only versioning knob is + the iteration count or the password. + +5. **The salt becomes unnecessary for key derivation.** HD derivation + doesn't need a salt — the path provides domain separation. The salt + field in `EncryptedData` is kept for wire-format compatibility but does + not participate in key derivation. + +### The compatibility problem + +The `EncryptedData` wire format is the same across both implementations +(`keyVersion`, `salt`, `iv`, `data` — all base64-encoded strings). But the +key derivation is different: + +- **TS v1**: PBKDF2(password, salt, 100k iterations) → key +- **Rust v1**: SLIP-0010(seed, `m/74'/2'/0'/0'`) → key + +Data encrypted by the TS implementation **cannot be decrypted by the vault** +— the keys are different even if the password equals the mnemonic. This is a +hard incompatibility at the crypto layer, not a format issue. + +## Decision + +### 1. HD derivation is the vault's encryption key derivation method + +The vault uses SLIP-0010 HD derivation from the BIP39 seed at path +`m/74'/2'/0'/0'` (`PATHS::ENCRYPTION`) to produce the AES-256-GCM +encryption key. No PBKDF2. No password-based key derivation. The seed is +the sole secret input. + +### 2. The salt field is unused in vault-encrypted data + +The `EncryptedData.salt` field exists in the wire format for compatibility +with the TS `EncryptedDataSchema`, but the vault does not use it for key +derivation. The vault generates a random salt and stores it (for wire-format +consistency), but it plays no cryptographic role. If a future KDF-based +derivation is needed (see "Future KDF" below), the field is already present. + +### 3. key_version semantics + +| Version | Key derivation | Used by | Decryptable by vault? | +|---------|---------------|---------|----------------------| +| 1 | PBKDF2 (password + salt + 100k iterations) | TS `@alkdev/storage` | No — different key | +| 2 | SLIP-0010 HD derivation (seed → `m/74'/2'/0'/0'`) | Rust vault | Yes | + +The vault stamps `key_version: 2` on new encryptions. `CURRENT_KEY_VERSION` +is `2`. + +**The current source uses `CURRENT_KEY_VERSION = 1` with HD derivation.** +This is a drift from the spec — the source's v1 is HD-derived, but the TS +v1 is PBKDF2-derived. Same version number, different derivation. The source +must be updated to use `key_version: 2` for HD-derived data, reserving v1 +for the TS PBKDF2 legacy. + +### 4. Migration path: TS → vault + +TS-encrypted credentials (PBKDF2, key_version=1) are migrated to vault- +encrypted credentials (HD derivation, key_version=2) through a one-time +re-encryption: + +1. Decrypt the TS-encrypted data with the original password and PBKDF2 + (using the TS `@alkdev/storage` `decrypt()` function or a migration + tool that implements PBKDF2) +2. Re-encrypt the plaintext with the vault at `key_version: 2` +3. Replace the old `EncryptedData` blob in storage + +The vault does **not** implement PBKDF2. The migration is performed by a +separate tool or script that has access to both the TS `decrypt()` function +and the vault's `encrypt()`. This is a one-time migration — once all data +is at key_version=2, PBKDF2 is no longer needed. + +### 5. No PBKDF2 in the vault + +The vault does not implement PBKDF2 and does not support decrypting +key_version=1 (TS PBKDF2) data. The vault's `decrypt()` method derives the +key via HD derivation and attempts decryption. If the data was encrypted +with PBKDF2 (TS), decryption fails (wrong key) — this is correct behavior, +not a bug. The migration tool handles the TS→vault transition. + +### 6. Future KDF (not v2) + +If a future use case requires KDF-based key derivation (e.g., stretching a +key derived from a non-seed source, or using a salt for additional domain +separation), it would be a new key_version with its own derivation method. +The `salt` field is available for this. This is additive — it doesn't +change v2 data. See OQ-22 (key rotation). + +## Consequences + +**Positive:** +- One secret (the BIP39 seed) is the root of trust for both derived keys + and encryption keys. No separate password to manage. +- Encryption key derivation is instant (HD derivation) vs. slow (PBKDF2 + 100k iterations). Startup with many credentials is fast. +- The encryption key is reproducible — a backup node with the same mnemonic + derives the same key and can decrypt the same credentials. +- Domain separation via paths — future encryption purposes can use + different paths without changing the wire format. +- Clean break from the TS approach. No PBKDF2 code in the vault. The vault + is smaller and simpler. + +**Negative:** +- TS-encrypted data cannot be decrypted by the vault. Migration requires a + separate tool with access to both the TS `decrypt()` and the vault's + `encrypt()`. This is expected — the TS implementation is being replaced, + not integrated. +- The `salt` field is unused in v2. It occupies 44 bytes (base64-encoded 32 + bytes) per `EncryptedData` blob for no cryptographic purpose. This is the + cost of wire-format compatibility — keeping the field means the struct + doesn't need to change if a future KDF uses it. +- `CURRENT_KEY_VERSION` must change from 1 to 2 in the source. If any + vault-encrypted data already exists at key_version=1 (with HD derivation), + it would need re-encryption at key_version=2. In practice, the vault is + pre-production, so this is a source change, not a data migration. + +## Assumptions + +1. **The TS `@alkdev/storage` encrypted data is the only legacy.** If other + systems produce PBKDF2-encrypted `EncryptedData` blobs, they need the same + migration treatment. The assumption is that `@alkdev/storage` is the only + consumer. + +2. **The vault is pre-production.** No significant amount of vault-encrypted + data (HD derivation, key_version=1) exists in production. Bumping to + key_version=2 is a source change, not a data migration. If vault-encrypted + data does exist, it needs re-encryption at key_version=2 (decrypt with HD + key at v1 path, re-encrypt at v2 — same key, just version bump). + +3. **The migration is one-time and one-directional.** Once data is at + key_version=2, there's no path back to PBKDF2. The TS `@alkdev/storage` + crypto module becomes legacy after migration. + +4. **The `salt` field's forward compatibility is worth the 44 bytes.** If a + future KDF is never needed, the salt field is wasted space. The assumption + is that the cost is negligible (credentials are small, not bulk data) and + the flexibility is worth it. + +## References + +- ADR-018: Vault as standalone crate +- ADR-019: Vault assembly-layer-only access +- [encryption.md](../crates/vault/encryption.md) — AES-256-GCM, EncryptedData +- [mnemonic-derivation.md](../crates/vault/mnemonic-derivation.md) — SLIP-0010, PATHS::ENCRYPTION +- OQ-20: Salt/KDF Phase B (resolved by this ADR) +- OQ-22: Key rotation mechanism (still open — this ADR defines v2 but not the rotation workflow) +- TypeScript predecessor: `/workspace/@alkdev/storage/src/graphs/crypto.ts` +- TypeScript secret graph: `/workspace/@alkdev/storage/src/graphs/modules/secret-graph.ts` \ No newline at end of file diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 0f9568c..b94d577 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -240,14 +240,14 @@ These questions are acknowledged but not active. They will be promoted to open w ## Theme: alknet-vault -### OQ-20: Salt/KDF Phase B +### OQ-20: Salt/KDF and Encryption Key Derivation Method - **Origin**: [encryption.md](crates/vault/encryption.md) -- **Status**: open -- **Door type**: Two-way -- **Priority**: low -- **Resolution**: The `EncryptedData.salt` field is reserved for future KDF-based key derivation. In v1, the encryption key is derived directly from the seed at path `m/74'/2'/0'/0'` without using the salt. The salt is generated and stored for forward compatibility but does not participate in key derivation. When KDF-based key derivation (HKDF or PBKDF2 using the salt) is implemented, the wire format does not need to change — the `salt` field is already present. The question of *when* to implement Phase B and *which* KDF to use is open but low-priority: v1's direct derivation is secure; the salt is a forward-compatibility hedge, not a gap. Two-way door — the salt is additive; implementing KDF usage doesn't break v1 data. -- **Cross-references**: [encryption.md](crates/vault/encryption.md) +- **Status**: resolved +- **Door type**: One-way (key derivation method), two-way (salt field usage) +- **Priority**: high +- **Resolution**: The vault uses SLIP-0010 HD derivation from the BIP39 seed at path `m/74'/2'/0'/0'` to produce the AES-256-GCM encryption key — not PBKDF2. The `salt` field in `EncryptedData` is unused for key derivation (kept for wire-format compatibility with the TS predecessor). The TypeScript `@alkdev/storage` crypto module used PBKDF2 with a password + salt; data encrypted by that method (key_version=1) cannot be decrypted by the vault and must be migrated via one-time re-encryption to key_version=2. See ADR-020 for the full rationale and migration path. +- **Cross-references**: ADR-020, [encryption.md](crates/vault/encryption.md) ### OQ-21: Remote Vault Administration diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 251dc8c..bf5cf21 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -210,6 +210,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/). | [017](decisions/017-call-protocol-client-and-adapter-contract.md) | Call Protocol Client and Adapter Contract | `CallClient` opens connections; `from_call` imports remote ops; connection direction independent of call direction | | [018](decisions/018-vault-standalone-crate.md) | Vault as Standalone Crate | Zero alknet crate dependencies; vault defines own types and errors | | [019](decisions/019-vault-assembly-layer-only.md) | Vault Assembly-Layer-Only Access | The assembly layer (CLI binary) is the sole direct caller; handlers never hold a vault reference | +| [020](decisions/020-hd-derivation-for-encryption-keys.md) | HD Derivation for Encryption Keys | SLIP-0010 derivation from seed, not PBKDF2; salt field unused in v2 | ## Open Questions @@ -221,7 +222,7 @@ Open questions are tracked in [open-questions.md](open-questions.md). Key questi - **OQ-04**: Dynamic handler registration at runtime vs static at startup (two-way door, defer to implementation) - **OQ-08**: Vault integration point (resolved: CLI-embedded, assembly-layer only — see ADR-008, ADR-014, ADR-018, ADR-019) - **OQ-16**: Safe vault operations for call protocol exposure (resolved: none for now — see ADR-014) -- **OQ-20**: Salt/KDF Phase B (open: reserved field, v1 doesn't use it — see [encryption.md](crates/vault/encryption.md)) +- **OQ-20**: Encryption key derivation (resolved: HD derivation, not PBKDF2 — see ADR-020) - **OQ-21**: Remote vault administration (deferred: network unlock not supported — see ADR-019) - **OQ-22**: Key rotation mechanism (open: versioning in place, workflow not specced — see [encryption.md](crates/vault/encryption.md))