Break down the three initial crates (alknet-vault, alknet-core, alknet-call) into dependency-ordered task files for implementation agents. Structure: - tasks/vault/ (10 tasks) — drift fixes from ADR-025/026 refactor, review, spec sync. Vault is independent and can run fully in parallel with core/call. - tasks/core/ (6 tasks) — crate init, core types, config, auth, endpoint, review. Core is foundational; call depends on it. - tasks/call/ (12 tasks) — split into registry/ and protocol/ topic subdirs reflecting the two subsystems. CallAdapter is the merge point. Key decisions: - Drifts 3+9+10 grouped as one task (key-versioning-rotation) — the complete ADR-021 rotation feature that doesn't compile in pieces - Reviews injected at end of each crate phase (vault, core, call) - Vault spec-sync task removes the drift table and bumps doc status to stable - ACME deferred in core/endpoint (noted as TODO; X509 manual certs for now) - OperationEnv kept as a trait (load-bearing for ADR-024 layering) Validated: 28 tasks, no cycles, 11 generations of parallel work. Critical path runs through call (11 tasks). Vault completes by generation 4. 6 high-risk tasks identified (21%): irpc-removal, endpoint, operation-context, operation-env, call-adapter, abort-cascade.
127 lines
5.8 KiB
Markdown
127 lines
5.8 KiB
Markdown
---
|
|
id: vault/key-versioning-rotation
|
|
name: Implement version-indexed encryption key paths, bump CURRENT_KEY_VERSION to 2, and add rotate method
|
|
status: pending
|
|
depends_on: [vault/irpc-removal]
|
|
scope: moderate
|
|
risk: medium
|
|
impact: component
|
|
level: implementation
|
|
---
|
|
|
|
## Description
|
|
|
|
Fix drift items #3, #9, and #10 as one coherent feature: the version-indexed
|
|
key rotation mechanism from ADR-021. These three drifts are tightly coupled —
|
|
`CURRENT_KEY_VERSION = 2` (drift #3), version-aware `encrypt`/`decrypt` via
|
|
`encryption_path_for_version` (drift #9), and the `rotate` method (drift #10)
|
|
form the complete key rotation feature. Splitting them would produce tasks that
|
|
don't compile independently.
|
|
|
|
### Drift #3: Bump CURRENT_KEY_VERSION
|
|
|
|
Current: `CURRENT_KEY_VERSION = 1` (but the key is HD-derived, and v1 is
|
|
reserved for the TypeScript PBKDF2 legacy per ADR-020).
|
|
|
|
Target: `CURRENT_KEY_VERSION = 2` (HD-derived, per ADR-020).
|
|
|
|
Version semantics:
|
|
- v1: TypeScript predecessor's PBKDF2-encrypted data — the vault **cannot**
|
|
decrypt it (different key derivation). Migration is a one-time re-encryption.
|
|
- v2: HD-derived at `m/74'/2'/0'/0'` (PATHS::ENCRYPTION) — current.
|
|
- v3+: `m/74'/2'/0'/1'`, `m/74'/2'/0'/2'`, etc. — future rotation versions.
|
|
|
|
### Drift #9: Version-aware encrypt/decrypt
|
|
|
|
Current: `encrypt`/`decrypt` always derive at `PATHS::ENCRYPTION` regardless of
|
|
the `key_version` parameter.
|
|
|
|
Target:
|
|
- `encrypt(plaintext, key_version)`: derive the encryption key at
|
|
`encryption_path_for_version(key_version)`, stamp the same `key_version` on
|
|
the resulting `EncryptedData`.
|
|
- `decrypt(encrypted)`: derive the key at
|
|
`encryption_path_for_version(encrypted.key_version)` — the blob carries its
|
|
own version, and each version maps to a distinct derivation path.
|
|
|
|
This requires:
|
|
1. `encryption_path_for_version(version: u32) -> Result<String, DerivationError>`
|
|
already exists in `derivation.rs` — verify it returns `InvalidPath` for
|
|
`version < 2` (v1 is TS legacy, v0 is meaningless).
|
|
2. `derive_encryption_key_for_version(version: u32) -> Result<DerivedKey, VaultServiceError>`
|
|
— a new method on `VaultServiceHandle` that maps version → path → derive.
|
|
Cached by path (same cache as `derive_encryption_key`).
|
|
3. `encrypt` and `decrypt` use `derive_encryption_key_for_version` instead of
|
|
deriving at the fixed `PATHS::ENCRYPTION` path.
|
|
|
|
### Drift #10: Implement rotate
|
|
|
|
Current: no `rotate` method exists.
|
|
|
|
Target:
|
|
```rust
|
|
pub fn rotate(&self, encrypted: &EncryptedData, to_version: u32) -> Result<EncryptedData, VaultServiceError>;
|
|
```
|
|
|
|
Decrypts with the old version's key (from `encrypted.key_version`), re-encrypts
|
|
with the new version's key (`to_version`). Returns the new `EncryptedData` —
|
|
the caller replaces the blob in storage. No new mnemonic needed; the same seed
|
|
produces all version keys via different derivation paths (ADR-021).
|
|
|
|
### Implementation notes
|
|
|
|
- `derive_encryption_key(path)` (the path-based API) remains as-is for deriving
|
|
at arbitrary paths. `derive_encryption_key_for_version(version)` is the
|
|
version-aware API used by `encrypt`/`decrypt`. Both share the same cache
|
|
(keyed by derivation path).
|
|
- `encrypt` and `decrypt` extract the `EncryptionKey` from the `DerivedKey` via
|
|
`EncryptionKey::from_derived_bytes` (see encryption.md).
|
|
- `encryption_path_for_version` returns `InvalidPath` for `version < 2`.
|
|
`derive_encryption_key_for_version` propagates this as
|
|
`VaultServiceError::InvalidPath`.
|
|
|
|
### Scope
|
|
|
|
This task touches `encryption.rs` (CURRENT_KEY_VERSION), `service.rs` (encrypt,
|
|
decrypt, rotate, derive_encryption_key_for_version), and possibly `derivation.rs`
|
|
(verify `encryption_path_for_version`). It depends on the irpc removal task
|
|
(drift #4) because both modify `service.rs`.
|
|
|
|
## Acceptance Criteria
|
|
|
|
- [ ] `CURRENT_KEY_VERSION` is `2` in `encryption.rs`
|
|
- [ ] `derive_encryption_key_for_version(version)` method added to `VaultServiceHandle`
|
|
- [ ] `derive_encryption_key_for_version` returns `InvalidPath` for `version < 2`
|
|
- [ ] `encrypt(plaintext, key_version)` derives at `encryption_path_for_version(key_version)`
|
|
- [ ] `encrypt` stamps the passed `key_version` on the resulting `EncryptedData`
|
|
- [ ] `decrypt(encrypted)` derives at `encryption_path_for_version(encrypted.key_version)`
|
|
- [ ] `rotate(encrypted, to_version)` method implemented: decrypt old, re-encrypt new
|
|
- [ ] `rotate` returns `EncryptedData` with `key_version = to_version`
|
|
- [ ] Unit test: encrypt at v2, decrypt at v2 — round-trip succeeds
|
|
- [ ] Unit test: encrypt at v2, rotate to v3, decrypt at v3 — round-trip succeeds
|
|
- [ ] Unit test: decrypt v2 blob after rotation — old key still derivable (partial rotation safe)
|
|
- [ ] Unit test: `derive_encryption_key_for_version(1)` returns `InvalidPath`
|
|
- [ ] Unit test: `derive_encryption_key_for_version(0)` returns `InvalidPath`
|
|
- [ ] `cargo test` succeeds
|
|
- [ ] `cargo clippy` succeeds with no warnings
|
|
|
|
## References
|
|
|
|
- docs/architecture/crates/vault/README.md — Known Source Drift table items #3, #9, #10
|
|
- docs/architecture/crates/vault/encryption.md — Key Versioning, Rotation, EncryptionKey
|
|
- docs/architecture/crates/vault/service.md — encrypt, decrypt, rotate, derive_encryption_key_for_version
|
|
- docs/architecture/crates/vault/mnemonic-derivation.md — encryption_path_for_version, PATHS
|
|
- docs/architecture/decisions/020-hd-derivation-for-encryption-keys.md — ADR-020
|
|
- docs/architecture/decisions/021-key-rotation-via-version-indexed-paths.md — ADR-021
|
|
|
|
## Notes
|
|
|
|
> These three drifts are one feature: version-indexed key rotation (ADR-021).
|
|
> Splitting them would produce tasks that don't compile independently —
|
|
> bumping the version without version-aware encrypt/decrypt would make v2
|
|
> blobs undecryptable, and rotate without version-aware encrypt/decrypt has no
|
|
> keys to work with. Depends on irpc removal because both modify `service.rs`.
|
|
|
|
## Summary
|
|
|
|
> To be filled on completion |