vault: version-indexed encryption key paths, bump CURRENT_KEY_VERSION to 2, add rotate (task: vault/key-versioning-rotation)

Drift items #3, #9, #10: implement the version-indexed key rotation mechanism
(ADR-021). Bump CURRENT_KEY_VERSION to 2 (HD-derived per ADR-020). Add
encryption_path_for_version in derivation.rs, derive_encryption_key_for_version
+ version-aware encrypt/decrypt + rotate method on VaultServiceHandle. Each
version maps to a distinct derivation path; the blob carries its own version.

Refs: docs/architecture/crates/vault/README.md drift #3, #9, #10
Implements: ADR-020, ADR-021

# Conflicts:
#	crates/alknet-vault/src/derivation.rs
#	crates/alknet-vault/src/service.rs
This commit is contained in:
2026-06-23 13:39:05 +00:00
4 changed files with 208 additions and 54 deletions

View File

@@ -51,6 +51,21 @@ pub fn device_path(index: u32) -> String {
format!("m/74'/0'/0'/{}'", index)
}
/// Construct the version-indexed encryption key derivation path (ADR-021).
///
/// Maps a key version to its derivation path: v2 → `m/74'/2'/0'/0'`
/// (which is `PATHS::ENCRYPTION`), v3 → `m/74'/2'/0'/1'`, etc. Returns
/// `DerivationError::InvalidPath` for `version < 2` — v1 is reserved for
/// the TypeScript PBKDF2 legacy (ADR-020), which the vault cannot derive,
/// and v0 is meaningless.
pub fn encryption_path_for_version(version: u32) -> Result<String, DerivationError> {
if version < 2 {
return Err(DerivationError::InvalidPath(format!(
"key version {version} has no derivable path (v1 is TS PBKDF2 legacy)"
)));
}
Ok(format!("m/74'/2'/0'/{}'", version - 2))
}
/// A derived extended private key with its public key.
///
/// Contains the private key bytes and public key bytes from
@@ -240,6 +255,37 @@ mod tests {
assert_eq!(device_path(1), "m/74'/0'/0'/1'");
}
#[test]
fn test_encryption_path_for_version_v2() {
assert_eq!(encryption_path_for_version(2).unwrap(), PATHS::ENCRYPTION);
}
#[test]
fn test_encryption_path_for_version_v3() {
assert_eq!(encryption_path_for_version(3).unwrap(), "m/74'/2'/0'/1'");
}
#[test]
fn test_encryption_path_for_version_v4() {
assert_eq!(encryption_path_for_version(4).unwrap(), "m/74'/2'/0'/2'");
}
#[test]
fn test_encryption_path_for_version_rejects_v1() {
assert!(matches!(
encryption_path_for_version(1),
Err(DerivationError::InvalidPath(_))
));
}
#[test]
fn test_encryption_path_for_version_rejects_v0() {
assert!(matches!(
encryption_path_for_version(0),
Err(DerivationError::InvalidPath(_))
));
}
#[test]
fn test_derive_master_key_from_seed() {
// Use a known 64-byte seed