vault: replace unwrap() on RwLock with poisoned-lock recovery (task: vault/poisoned-lock-recovery)
Drift item #2: replace all .read().unwrap()/.write().unwrap() calls in VaultServiceHandle with .unwrap_or_else(|e| e.into_inner()) to recover from poisoned locks instead of bricking the vault. Added test_poisoned_lock_recovery that poisons the lock via a panicking thread and verifies the vault remains usable. Refs: docs/architecture/crates/vault/README.md drift #2 Implements: ADR-025 # Conflicts: # crates/alknet-vault/src/service.rs
This commit is contained in:
@@ -130,7 +130,7 @@ impl VaultServiceHandle {
|
|||||||
/// The passphrase is the BIP39 password (may be empty string for none).
|
/// The passphrase is the BIP39 password (may be empty string for none).
|
||||||
/// After unlocking, derive and encrypt/decrypt operations are available.
|
/// After unlocking, derive and encrypt/decrypt operations are available.
|
||||||
pub fn unlock(&self, phrase: &str, passphrase: Option<&str>) -> Result<(), VaultServiceError> {
|
pub fn unlock(&self, phrase: &str, passphrase: Option<&str>) -> Result<(), VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
|
||||||
if inner.unlocked {
|
if inner.unlocked {
|
||||||
return Err(VaultServiceError::AlreadyUnlocked);
|
return Err(VaultServiceError::AlreadyUnlocked);
|
||||||
}
|
}
|
||||||
@@ -149,7 +149,7 @@ impl VaultServiceHandle {
|
|||||||
/// Returns the generated mnemonic phrase. Store this phrase securely —
|
/// Returns the generated mnemonic phrase. Store this phrase securely —
|
||||||
/// it is the root of trust for all derived keys.
|
/// it is the root of trust for all derived keys.
|
||||||
pub fn unlock_new(&self, word_count: usize) -> Result<Zeroizing<String>, VaultServiceError> {
|
pub fn unlock_new(&self, word_count: usize) -> Result<Zeroizing<String>, VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
|
||||||
if inner.unlocked {
|
if inner.unlocked {
|
||||||
return Err(VaultServiceError::AlreadyUnlocked);
|
return Err(VaultServiceError::AlreadyUnlocked);
|
||||||
}
|
}
|
||||||
@@ -170,7 +170,7 @@ impl VaultServiceHandle {
|
|||||||
/// until `unlock` is called again. Calls `zeroize()` on all sensitive
|
/// until `unlock` is called again. Calls `zeroize()` on all sensitive
|
||||||
/// material per ADR-038.
|
/// material per ADR-038.
|
||||||
pub fn lock(&self) {
|
pub fn lock(&self) {
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
|
||||||
inner.cache.clear();
|
inner.cache.clear();
|
||||||
inner.seed = None;
|
inner.seed = None;
|
||||||
inner.mnemonic = None;
|
inner.mnemonic = None;
|
||||||
@@ -179,12 +179,15 @@ impl VaultServiceHandle {
|
|||||||
|
|
||||||
/// Check whether the service is currently unlocked.
|
/// Check whether the service is currently unlocked.
|
||||||
pub fn is_unlocked(&self) -> bool {
|
pub fn is_unlocked(&self) -> bool {
|
||||||
self.inner.read().unwrap().unlocked
|
self.inner
|
||||||
|
.read()
|
||||||
|
.unwrap_or_else(|e| e.into_inner())
|
||||||
|
.unlocked
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive an Ed25519 keypair at the given path.
|
/// Derive an Ed25519 keypair at the given path.
|
||||||
pub fn derive_ed25519(&self, path: &str) -> Result<DerivedKey, VaultServiceError> {
|
pub fn derive_ed25519(&self, path: &str) -> Result<DerivedKey, VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(VaultServiceError::VaultLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
@@ -212,7 +215,7 @@ impl VaultServiceHandle {
|
|||||||
|
|
||||||
/// Derive an AES-256-GCM encryption key at the given path.
|
/// Derive an AES-256-GCM encryption key at the given path.
|
||||||
pub fn derive_encryption_key(&self, path: &str) -> Result<DerivedKey, VaultServiceError> {
|
pub fn derive_encryption_key(&self, path: &str) -> Result<DerivedKey, VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(VaultServiceError::VaultLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
@@ -246,7 +249,7 @@ impl VaultServiceHandle {
|
|||||||
pub fn derive_ethereum_key(&self, path: &str) -> Result<DerivedKey, VaultServiceError> {
|
pub fn derive_ethereum_key(&self, path: &str) -> Result<DerivedKey, VaultServiceError> {
|
||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
{
|
{
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(VaultServiceError::VaultLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
@@ -289,7 +292,7 @@ impl VaultServiceHandle {
|
|||||||
plaintext: &str,
|
plaintext: &str,
|
||||||
key_version: u32,
|
key_version: u32,
|
||||||
) -> Result<EncryptedData, VaultServiceError> {
|
) -> Result<EncryptedData, VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(VaultServiceError::VaultLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
@@ -313,7 +316,7 @@ impl VaultServiceHandle {
|
|||||||
|
|
||||||
/// Decrypt an EncryptedData blob using the derived encryption key.
|
/// Decrypt an EncryptedData blob using the derived encryption key.
|
||||||
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();
|
let mut inner = self.inner.write().unwrap_or_else(|e| e.into_inner());
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(VaultServiceError::VaultLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
@@ -404,6 +407,28 @@ mod tests {
|
|||||||
assert!(service.derive_ed25519(PATHS::IDENTITY).is_err());
|
assert!(service.derive_ed25519(PATHS::IDENTITY).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_poisoned_lock_recovery() {
|
||||||
|
let service = VaultServiceHandle::new();
|
||||||
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
|
let inner_arc = service.inner.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _guard = inner_arc.write().unwrap();
|
||||||
|
panic!("simulated panic while holding write lock");
|
||||||
|
})
|
||||||
|
.join()
|
||||||
|
.expect_err("thread must panic to poison the lock");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
service.is_unlocked(),
|
||||||
|
"vault must remain usable after a poisoned lock"
|
||||||
|
);
|
||||||
|
|
||||||
|
let key = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
||||||
|
assert!(!key.private_key.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_unlock_with_known_phrase() {
|
fn test_unlock_with_known_phrase() {
|
||||||
let service = VaultServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
|
|||||||
Reference in New Issue
Block a user