Files
alknet/tasks/integration/phase3/secret-service/encryption-salt-kdf.md
glm-5.1 9ec7627d80 chore: add Phase 3 secret-service decomposition tasks
9 atomic tasks for alknet-secret spec conformance and gap closure,
derived from architect's implementation review. Dependencies form
a 5-generation graph starting with spec update, then parallel
implementation tasks, ending with a review gate.

Tasks address: DerivedKey zeroize security, key caching with TTL,
irpc protocol integration, password derivation, secp256k1/Ethereum
derivation, encryption salt/KDF, crypto test vectors, and final
spec conformance review.
2026-06-10 04:14:39 +00:00

103 lines
5.3 KiB
Markdown

---
id: encryption-salt-kdf
name: Clarify and fix EncryptedData salt usage — use HKDF for key derivation or document as reserved
status: pending
depends_on: [spec-update-secret-service]
scope: narrow
risk: low
impact: component
level: implementation
---
## Description
The `EncryptedData` struct has a `salt` field that is generated randomly during encryption but not used in the key derivation process. The current encryption flow is:
1. Derive key from seed at path `m/74'/2'/0'/0'`
2. Use first 32 bytes of derived private key as AES-256-GCM key
3. Generate random 12-byte IV
4. Generate random 32-byte salt (stored but NOT used in key derivation)
5. Encrypt with AES-256-GCM using the derived key + random IV
6. Store `{key_version, salt, iv, data}` as `EncryptedData`
The salt is stored but serves no purpose. This is a gap because:
- Without a KDF, the same derived key is used for every encryption operation (different IVs provide per-message randomness, but the key itself is static)
- Salt-based key derivation would add an additional security layer: even if the derivation path is known, the salt provides per-encryption diversity
- The `key_version` field exists for rotation but without KDF-based key derivation, there's no mechanism to rotate to a stronger key
**The spec update (spec-update-secret-service task) decides one of two paths:**
### Option A: Use HKDF for key derivation (recommended for v1)
Replace the direct "first 32 bytes of derived key" approach with:
1. Derive master key from seed at path `m/74'/2'/0'/0'`
2. Use HKDF-SHA256 with `salt` and `info = "alknet-encryption-v{key_version}"` to derive the actual AES-256-GCM key
3. This means: same seed + same path + different salt = different AES key
Benefits: Each encryption uses a unique derived key (even with the same master key), providing forward security and key diversity. The salt is now purposeful.
### Option B: Document salt as reserved (Phase B)
Keep the current approach (direct key from derivation path) and document the salt field as "reserved for future KDF-based key derivation." Add a comment explaining that v1 doesn't use the salt.
This is simpler in v1 but defers the security improvement.
**This task implements whichever option the spec update chooses.** If the spec says "use HKDF now," implement Option A. If it says "document as reserved," implement Option B.
**If Option A (HKDF):**
1. Add `hkdf` dependency to `Cargo.toml`
2. Modify `encryption::encrypt()`:
- Generate random salt (32 bytes)
- Use HKDF-SHA256 to derive AES key from: `master_key + salt + info`
- The `info` string includes the key version for forward compatibility
3. Modify `encryption::decrypt()`:
- Use HKDF-SHA256 with the stored salt to re-derive the AES key
- Decrypt ciphertext with the derived key + stored IV
4. **Backward compatibility**: Add an `EncryptedData::version` or check if salt is empty/all-zeros to detect v1 (direct key) vs v2 (HKDF) format. Or, since key_version=1 is already in use, bump key_version to 2 for HKDF-derived keys and support both in decrypt.
**If Option B (reserved):**
1. Add documentation/comments to `encryption.rs` and `EncryptedData` explaining that the salt is reserved for future KDF
2. Add a `// TODO(Phase B)` comment on the salt generation
3. No code behavior changes
## Acceptance Criteria
**If Option A (HKDF — recommended):**
- [ ] `hkdf` dependency added to `Cargo.toml`
- [ ] `encrypt()` uses HKDF-SHA256 with `salt + info = "alknet-encryption-v{key_version}"` to derive AES key
- [ ] `decrypt()` uses HKDF-SHA256 with stored `salt` to re-derive AES key
- [ ] `EncryptedData` with `key_version >= 2` uses HKDF
- [ ] `EncryptedData` with `key_version == 1` uses direct key (backward compat)
- [ ] Backward compatibility: data encrypted with v1 format can still be decrypted
- [ ] `CURRENT_KEY_VERSION` bumped to 2
- [ ] Unit test: encrypt/decrypt round-trip with HKDF (key_version 2)
- [ ] Unit test: decrypt v1-encrypted data (direct key) still works
- [ ] Unit test: different salts produce different ciphertext keys (even with same master key)
- [ ] `EncryptionKey` struct updated to carry HKDF info if needed
**If Option B (reserved):**
- [ ] `encryption.rs` has documentation explaining salt is reserved for future KDF
- [ ] `EncryptedData` struct has doc comment on `salt` field explaining reserved purpose
- [ ] `// TODO(Phase B)` comment on salt generation in `encrypt()`
- [ ] No behavior changes — existing tests pass unchanged
## References
- docs/architecture/secret-service.md — Encryption section (after spec update)
- crates/alknet-secret/src/encryption.rs — Current encrypt/decrypt implementation
- HKDF (RFC 5869): https://tools.ietf.org/html/rfc5869
## Notes
> My recommendation is Option A (HKDF). It's a small amount of additional code (the `hkdf` crate is tiny and well-tested), it makes the `salt` field purposeful, and it provides per-encryption key diversity. The backward compatibility concern is manageable: decrypt based on `key_version` (v1 = direct, v2 = HKDF).
> The architect's message specifically called out: "The EncryptedData struct has a salt field but the encryption function generates a random salt per encryption without using it for key derivation. Either the salt should be used in a KDF, or the field should be documented as reserved." This task resolves that ambiguity.
## Summary
> To be filled on completion