docs(architecture): add ADR-020, resolve OQ-20 — HD derivation for encryption keys

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.
This commit is contained in:
2026-06-19 09:49:06 +00:00
parent dd1ca1de70
commit 6e9414bc81
8 changed files with 296 additions and 45 deletions

View File

@@ -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 (001019) 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 (001020) 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):**

View File

@@ -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 |

View File

@@ -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.

View File

@@ -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

View File

@@ -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

View File

@@ -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`

View File

@@ -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

View File

@@ -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))