Compare commits
3 Commits
25327b41d4
...
968e3a09ee
| Author | SHA1 | Date | |
|---|---|---|---|
| 968e3a09ee | |||
| 9eab93100e | |||
| 55d356cb4e |
@@ -51,6 +51,21 @@ pub fn device_path(index: u32) -> String {
|
|||||||
format!("m/74'/0'/0'/{}'", index)
|
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.
|
/// A derived extended private key with its public key.
|
||||||
///
|
///
|
||||||
/// Contains the private key bytes and public key bytes from
|
/// 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'");
|
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]
|
#[test]
|
||||||
fn test_derive_master_key_from_seed() {
|
fn test_derive_master_key_from_seed() {
|
||||||
// Use a known 64-byte seed
|
// Use a known 64-byte seed
|
||||||
|
|||||||
@@ -7,10 +7,10 @@
|
|||||||
//! # Salt Field (Reserved for Future KDF-Based Key Derivation)
|
//! # Salt Field (Reserved for Future KDF-Based Key Derivation)
|
||||||
//!
|
//!
|
||||||
//! The `salt` field in `EncryptedData` is **reserved for future KDF-based key
|
//! The `salt` field in `EncryptedData` is **reserved for future KDF-based key
|
||||||
//! derivation** (Phase B). In v1, the encryption key is derived directly from the
|
//! derivation** (Phase B). In v2, the encryption key is derived directly from the
|
||||||
//! seed at path `m/74'/2'/0'/0'` without using the salt. The salt is generated
|
//! 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
|
//! randomly (32 bytes) and stored in `EncryptedData.salt` for forward
|
||||||
//! compatibility, but it plays no role in the v1 key derivation process.
|
//! compatibility, but it plays no role in the v2 key derivation process.
|
||||||
//!
|
//!
|
||||||
//! When key rotation is implemented in Phase B, the salt will be used as input to
|
//! 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
|
//! HKDF or PBKDF2 for stretch-based key derivation, allowing the same seed to
|
||||||
@@ -27,11 +27,14 @@
|
|||||||
//! # Key Versioning
|
//! # Key Versioning
|
||||||
//!
|
//!
|
||||||
//! Key versioning allows re-encryption when the encryption key is rotated. The
|
//! Key versioning allows re-encryption when the encryption key is rotated. The
|
||||||
//! current key version is `1`. To rotate:
|
//! current key version is `2` (HD-derived at `m/74'/2'/0'/0'`). Version `1` is
|
||||||
//! 1. Derive a new key from a new derivation path or new seed
|
//! reserved for the TypeScript predecessor's PBKDF2-encrypted data, which the
|
||||||
//! 2. Decrypt all existing `EncryptedData` with key version 1
|
//! vault cannot decrypt (different key derivation) — migration is a one-time
|
||||||
//! 3. Re-encrypt with key version 2
|
//! re-encryption. Each version maps to a unique derivation path
|
||||||
//! 4. Update storage
|
//! (`m/74'/2'/0'/{version-2}'`, see ADR-021). To rotate:
|
||||||
|
//! 1. Decrypt all existing `EncryptedData` with the old key version
|
||||||
|
//! 2. Re-encrypt with the new key version (via `VaultServiceHandle::rotate`)
|
||||||
|
//! 3. Update storage
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{Aead, KeyInit},
|
aead::{Aead, KeyInit},
|
||||||
@@ -42,7 +45,11 @@ use serde::{Deserialize, Serialize};
|
|||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
/// Current default key version for encryption.
|
/// Current default key version for encryption.
|
||||||
pub const CURRENT_KEY_VERSION: u32 = 1;
|
///
|
||||||
|
/// Version `2` is HD-derived at `m/74'/2'/0'/0'` (`PATHS::ENCRYPTION`) per
|
||||||
|
/// ADR-020. Version `1` is reserved for the TypeScript predecessor's
|
||||||
|
/// PBKDF2-encrypted data, which the vault cannot decrypt.
|
||||||
|
pub const CURRENT_KEY_VERSION: u32 = 2;
|
||||||
|
|
||||||
/// Encrypted data blob stored in the metagraph.
|
/// Encrypted data blob stored in the metagraph.
|
||||||
///
|
///
|
||||||
@@ -62,7 +69,7 @@ pub struct EncryptedData {
|
|||||||
pub key_version: u32,
|
pub key_version: u32,
|
||||||
/// Base64-encoded random salt.
|
/// Base64-encoded random salt.
|
||||||
///
|
///
|
||||||
/// **Reserved for future KDF-based key derivation (Phase B).** In v1, the
|
/// **Reserved for future KDF-based key derivation (Phase B).** In v2, the
|
||||||
/// encryption key is derived directly from the seed at path `m/74'/2'/0'/0'`
|
/// 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
|
/// without using the salt. The salt is generated and stored for forward
|
||||||
/// compatibility but does not participate in key derivation.
|
/// compatibility but does not participate in key derivation.
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crate::cache::{CacheConfig, CachedKey, KeyCache};
|
use crate::cache::{CacheConfig, CachedKey, KeyCache};
|
||||||
use crate::derivation::{self, DerivationError, PATHS};
|
use crate::derivation::{self, DerivationError};
|
||||||
use crate::encryption::{self, EncryptedData, EncryptionKey};
|
use crate::encryption::{self, EncryptedData, EncryptionKey};
|
||||||
use crate::mnemonic::{Language, Mnemonic, Seed};
|
use crate::mnemonic::{Language, Mnemonic, Seed};
|
||||||
use crate::protocol::{DerivedKey, KeyType};
|
use crate::protocol::{DerivedKey, KeyType};
|
||||||
@@ -241,6 +241,23 @@ impl VaultServiceHandle {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derive the encryption key for a specific key version (ADR-021).
|
||||||
|
///
|
||||||
|
/// Maps `version` to its derivation path via
|
||||||
|
/// `derivation::encryption_path_for_version` (v2 → `m/74'/2'/0'/0'`,
|
||||||
|
/// v3 → `m/74'/2'/0'/1'`, etc.) and derives the key. Cached by path
|
||||||
|
/// (same cache as `derive_encryption_key`). Returns
|
||||||
|
/// `VaultServiceError::InvalidPath` for `version < 2` (v1 is the TS
|
||||||
|
/// PBKDF2 legacy, which the vault cannot derive; v0 is meaningless).
|
||||||
|
pub fn derive_encryption_key_for_version(
|
||||||
|
&self,
|
||||||
|
version: u32,
|
||||||
|
) -> Result<DerivedKey, VaultServiceError> {
|
||||||
|
let path = derivation::encryption_path_for_version(version)
|
||||||
|
.map_err(|e| VaultServiceError::InvalidPath(e.to_string()))?;
|
||||||
|
self.derive_encryption_key(&path)
|
||||||
|
}
|
||||||
|
|
||||||
/// Derive a secp256k1 (Ethereum) keypair at the given path.
|
/// Derive a secp256k1 (Ethereum) keypair at the given path.
|
||||||
///
|
///
|
||||||
/// Uses BIP-0032 derivation (HMAC-SHA512 with "Bitcoin seed") when the
|
/// Uses BIP-0032 derivation (HMAC-SHA512 with "Bitcoin seed") when the
|
||||||
@@ -284,58 +301,48 @@ impl VaultServiceHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encrypt plaintext using the derived encryption key.
|
/// Encrypt plaintext using the encryption key derived for `key_version`.
|
||||||
///
|
///
|
||||||
/// Uses the key at path `m/74'/2'/0'/0'` (PATHS::ENCRYPTION) by default.
|
/// Derives the key at `encryption_path_for_version(key_version)` (ADR-021)
|
||||||
|
/// and stamps the same `key_version` on the resulting `EncryptedData`.
|
||||||
|
/// Returns `VaultServiceError::InvalidPath` for `version < 2`.
|
||||||
pub fn encrypt(
|
pub fn encrypt(
|
||||||
&self,
|
&self,
|
||||||
plaintext: &str,
|
plaintext: &str,
|
||||||
key_version: u32,
|
key_version: u32,
|
||||||
) -> Result<EncryptedData, VaultServiceError> {
|
) -> Result<EncryptedData, VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
|
let derived = self.derive_encryption_key_for_version(key_version)?;
|
||||||
if !inner.unlocked {
|
let enc_key = EncryptionKey::from_derived_bytes(&derived.private_key, key_version);
|
||||||
return Err(VaultServiceError::VaultLocked);
|
|
||||||
}
|
|
||||||
|
|
||||||
let private_key = if let Some(cached) = inner.cache.get(PATHS::ENCRYPTION) {
|
|
||||||
cached.private_key.clone()
|
|
||||||
} else {
|
|
||||||
let seed = inner.seed.as_ref().ok_or(VaultServiceError::VaultLocked)?;
|
|
||||||
let derived = derivation::derive_path_from_seed(seed.as_bytes(), PATHS::ENCRYPTION)?;
|
|
||||||
let pk = derived.private_key().to_vec();
|
|
||||||
let pubk = derived.public_key().to_vec();
|
|
||||||
let cached = CachedKey::new(KeyType::Aes256Gcm, pk.clone(), pubk);
|
|
||||||
inner.cache.insert(PATHS::ENCRYPTION, cached);
|
|
||||||
pk
|
|
||||||
};
|
|
||||||
|
|
||||||
let enc_key = EncryptionKey::from_derived_bytes(&private_key, key_version);
|
|
||||||
|
|
||||||
encryption::encrypt(plaintext, &enc_key).map_err(|e| e.into())
|
encryption::encrypt(plaintext, &enc_key).map_err(|e| e.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decrypt an EncryptedData blob using the derived encryption key.
|
/// Decrypt an `EncryptedData` blob using the key for its `key_version`.
|
||||||
|
///
|
||||||
|
/// Derives the key at `encryption_path_for_version(encrypted.key_version)`
|
||||||
|
/// (ADR-021). Each version maps to a distinct derivation path, so old and
|
||||||
|
/// new keys can coexist during partial rotation.
|
||||||
pub fn decrypt(&self, encrypted: &EncryptedData) -> Result<String, VaultServiceError> {
|
pub fn decrypt(&self, encrypted: &EncryptedData) -> Result<String, VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
|
let derived = self.derive_encryption_key_for_version(encrypted.key_version)?;
|
||||||
if !inner.unlocked {
|
let enc_key =
|
||||||
return Err(VaultServiceError::VaultLocked);
|
EncryptionKey::from_derived_bytes(&derived.private_key, encrypted.key_version);
|
||||||
|
encryption::decrypt(encrypted, &enc_key).map_err(|e| e.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
let private_key = if let Some(cached) = inner.cache.get(PATHS::ENCRYPTION) {
|
/// Re-encrypt an `EncryptedData` blob from its current version to
|
||||||
cached.private_key.clone()
|
/// `to_version` (ADR-021).
|
||||||
} else {
|
///
|
||||||
let seed = inner.seed.as_ref().ok_or(VaultServiceError::VaultLocked)?;
|
/// Decrypts with the old version's key (`encrypted.key_version`) and
|
||||||
let derived = derivation::derive_path_from_seed(seed.as_bytes(), PATHS::ENCRYPTION)?;
|
/// re-encrypts with the new version's key (`to_version`). Returns the new
|
||||||
let pk = derived.private_key().to_vec();
|
/// `EncryptedData` with `key_version = to_version` — the caller replaces
|
||||||
let pubk = derived.public_key().to_vec();
|
/// the blob in storage. No new mnemonic is needed; the same seed produces
|
||||||
let cached = CachedKey::new(KeyType::Aes256Gcm, pk.clone(), pubk);
|
/// all version keys via different derivation paths.
|
||||||
inner.cache.insert(PATHS::ENCRYPTION, cached);
|
pub fn rotate(
|
||||||
pk
|
&self,
|
||||||
};
|
encrypted: &EncryptedData,
|
||||||
|
to_version: u32,
|
||||||
let enc_key = EncryptionKey::from_derived_bytes(&private_key, encrypted.key_version);
|
) -> Result<EncryptedData, VaultServiceError> {
|
||||||
|
let plaintext = self.decrypt(encrypted)?;
|
||||||
encryption::decrypt(encrypted, &enc_key).map_err(|e| e.into())
|
self.encrypt(&plaintext, to_version)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -348,6 +355,7 @@ impl Default for VaultServiceHandle {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::derivation::PATHS;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_service_starts_locked() {
|
fn test_service_starts_locked() {
|
||||||
@@ -455,7 +463,7 @@ mod tests {
|
|||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let plaintext = "my-api-key-12345";
|
let plaintext = "my-api-key-12345";
|
||||||
let encrypted = service.encrypt(plaintext, 1).unwrap();
|
let encrypted = service.encrypt(plaintext, 2).unwrap();
|
||||||
let decrypted = service.decrypt(&encrypted).unwrap();
|
let decrypted = service.decrypt(&encrypted).unwrap();
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
|
|
||||||
@@ -579,7 +587,7 @@ mod tests {
|
|||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let plaintext = "cached-encryption-test";
|
let plaintext = "cached-encryption-test";
|
||||||
let encrypted = service.encrypt(plaintext, 1).unwrap();
|
let encrypted = service.encrypt(plaintext, 2).unwrap();
|
||||||
assert_eq!(service.inner.read().unwrap().cache.len(), 1);
|
assert_eq!(service.inner.read().unwrap().cache.len(), 1);
|
||||||
|
|
||||||
let decrypted = service.decrypt(&encrypted).unwrap();
|
let decrypted = service.decrypt(&encrypted).unwrap();
|
||||||
@@ -588,6 +596,99 @@ mod tests {
|
|||||||
assert_eq!(service.inner.read().unwrap().cache.len(), 1);
|
assert_eq!(service.inner.read().unwrap().cache.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypt_v2_round_trip() {
|
||||||
|
let service = VaultServiceHandle::new();
|
||||||
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
|
let plaintext = "v2 round trip secret";
|
||||||
|
let encrypted = service.encrypt(plaintext, 2).unwrap();
|
||||||
|
assert_eq!(encrypted.key_version, 2);
|
||||||
|
|
||||||
|
let decrypted = service.decrypt(&encrypted).unwrap();
|
||||||
|
assert_eq!(decrypted, plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rotate_v2_to_v3_round_trip() {
|
||||||
|
let service = VaultServiceHandle::new();
|
||||||
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
|
let plaintext = "rotated secret";
|
||||||
|
let encrypted_v2 = service.encrypt(plaintext, 2).unwrap();
|
||||||
|
assert_eq!(encrypted_v2.key_version, 2);
|
||||||
|
|
||||||
|
let encrypted_v3 = service.rotate(&encrypted_v2, 3).unwrap();
|
||||||
|
assert_eq!(encrypted_v3.key_version, 3);
|
||||||
|
assert_ne!(encrypted_v3.data, encrypted_v2.data);
|
||||||
|
|
||||||
|
let decrypted_v3 = service.decrypt(&encrypted_v3).unwrap();
|
||||||
|
assert_eq!(decrypted_v3, plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_rotate_old_key_still_derivable_after_rotation() {
|
||||||
|
let service = VaultServiceHandle::new();
|
||||||
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
|
let plaintext = "partial rotation safe";
|
||||||
|
let encrypted_v2 = service.encrypt(plaintext, 2).unwrap();
|
||||||
|
|
||||||
|
let _encrypted_v3 = service.rotate(&encrypted_v2, 3).unwrap();
|
||||||
|
|
||||||
|
let decrypted_v2 = service.decrypt(&encrypted_v2).unwrap();
|
||||||
|
assert_eq!(decrypted_v2, plaintext);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_derive_encryption_key_for_version_rejects_v1() {
|
||||||
|
let service = VaultServiceHandle::new();
|
||||||
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
|
let result = service.derive_encryption_key_for_version(1);
|
||||||
|
assert!(matches!(result, Err(VaultServiceError::InvalidPath(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_derive_encryption_key_for_version_rejects_v0() {
|
||||||
|
let service = VaultServiceHandle::new();
|
||||||
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
|
let result = service.derive_encryption_key_for_version(0);
|
||||||
|
assert!(matches!(result, Err(VaultServiceError::InvalidPath(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_encrypt_rejects_v1() {
|
||||||
|
let service = VaultServiceHandle::new();
|
||||||
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
|
let result = service.encrypt("secret", 1);
|
||||||
|
assert!(matches!(result, Err(VaultServiceError::InvalidPath(_))));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_derive_encryption_key_for_version_v2_matches_path() {
|
||||||
|
let service = VaultServiceHandle::new();
|
||||||
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
|
let by_version = service.derive_encryption_key_for_version(2).unwrap();
|
||||||
|
let by_path = service
|
||||||
|
.derive_encryption_key(crate::derivation::PATHS::ENCRYPTION)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(by_version.private_key, by_path.private_key);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_derive_encryption_key_for_version_v3_distinct_from_v2() {
|
||||||
|
let service = VaultServiceHandle::new();
|
||||||
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
|
let v2 = service.derive_encryption_key_for_version(2).unwrap();
|
||||||
|
let v3 = service.derive_encryption_key_for_version(3).unwrap();
|
||||||
|
assert_ne!(v2.private_key, v3.private_key);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_unlock_with_passphrase_produces_different_seed() {
|
fn test_unlock_with_passphrase_produces_different_seed() {
|
||||||
let service_a = VaultServiceHandle::new();
|
let service_a = VaultServiceHandle::new();
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ fn test_encrypt_decrypt_lifecycle() {
|
|||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let plaintext = "my-api-key-12345";
|
let plaintext = "my-api-key-12345";
|
||||||
let encrypted = service.encrypt(plaintext, 1).unwrap();
|
let encrypted = service.encrypt(plaintext, 2).unwrap();
|
||||||
let decrypted = service.decrypt(&encrypted).unwrap();
|
let decrypted = service.decrypt(&encrypted).unwrap();
|
||||||
assert_eq!(decrypted, plaintext);
|
assert_eq!(decrypted, plaintext);
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
---
|
---
|
||||||
id: vault/key-versioning-rotation
|
id: vault/key-versioning-rotation
|
||||||
name: Implement version-indexed encryption key paths, bump CURRENT_KEY_VERSION to 2, and add rotate method
|
name: Implement version-indexed encryption key paths, bump CURRENT_KEY_VERSION to 2, and add rotate method
|
||||||
status: pending
|
status: completed
|
||||||
depends_on: [vault/irpc-removal]
|
depends_on: [vault/irpc-removal]
|
||||||
scope: moderate
|
scope: moderate
|
||||||
risk: medium
|
risk: medium
|
||||||
@@ -124,4 +124,11 @@ decrypt, rotate, derive_encryption_key_for_version), and possibly `derivation.rs
|
|||||||
|
|
||||||
## Summary
|
## Summary
|
||||||
|
|
||||||
> To be filled on completion
|
Bumped `CURRENT_KEY_VERSION` to 2 (HD-derived per ADR-020). Added
|
||||||
|
`encryption_path_for_version` in derivation.rs (v2 → `m/74'/2'/0'/0'`, v3 →
|
||||||
|
`m/74'/2'/0'/1'`, rejects version < 2). Added `derive_encryption_key_for_version`
|
||||||
|
+ version-aware `encrypt`/`decrypt` + `rotate` method on `VaultServiceHandle`
|
||||||
|
(ADR-021). Each version maps to a distinct derivation path; the blob carries
|
||||||
|
its own version. 68 lib + 14 integration tests pass; clippy clean. Merged to
|
||||||
|
develop (resolved conflicts with remove-password-derivation and
|
||||||
|
poisoned-lock-recovery).
|
||||||
Reference in New Issue
Block a user