feat(vault): version-indexed encryption key paths, CURRENT_KEY_VERSION=2, rotate method (ADR-021)
- Bump CURRENT_KEY_VERSION from 1 to 2 (v1 reserved for TS PBKDF2 legacy per ADR-020)
- Add derivation::encryption_path_for_version(version) -> m/74'/2'/0'/{version-2}', returns InvalidPath for version < 2
- Add VaultServiceHandle::derive_encryption_key_for_version(version), cached by path, returns InvalidPath for version < 2
- encrypt/decrypt now derive at encryption_path_for_version(key_version) instead of fixed PATHS::ENCRYPTION
- Add VaultServiceHandle::rotate(encrypted, to_version): decrypt old, re-encrypt new
- Update existing tests to use v2; add round-trip, rotation, partial-rotation, and invalid-version tests
Task: vault/key-versioning-rotation
This commit is contained in:
@@ -59,6 +59,22 @@ pub fn site_password_path(site_hash: &str) -> String {
|
||||
format!("m/74'/1'/0'/{}'", site_hash)
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -253,6 +269,37 @@ mod tests {
|
||||
assert_eq!(site_password_path("abc123"), "m/74'/1'/0'/abc123'");
|
||||
}
|
||||
|
||||
#[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
|
||||
|
||||
Reference in New Issue
Block a user