refactor(vault): remove irpc actor dispatch — direct method calls on VaultServiceHandle (ADR-025)
Drop the irpc-based actor dispatch path from alknet-vault and convert to direct method calls on VaultServiceHandle (drift item #4, ADR-025). Removed: - VaultProtocol enum with #[rpc_requests] derive from protocol.rs - VaultServiceActor (mpsc + oneshot dispatch loop) from service.rs - VaultService wrapper struct (only the handle is needed) - Client<VaultProtocol> usage - irpc, irpc-derive, tokio from [dependencies] - postcard from [dev-dependencies] - VaultMessage/VaultProtocol/VaultServiceActor re-exports from lib.rs - Serialize/Deserialize derives from VaultServiceError - postcard round-trip tests from protocol.rs - actor tokio::test tests from service.rs The vault now has zero async runtime dependency and zero RPC framework dependency — it is local-only by construction. VaultServiceHandle is the sole API: Arc<std::sync::RwLock<VaultServiceInner>> with synchronous methods. lib.rs re-exports match the vault README Public API section. Also fixes pre-existing clippy field_reassign_with_default warnings in cache.rs tests so cargo clippy -- -D warnings passes.
This commit is contained in:
1591
Cargo.lock
generated
1591
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -25,11 +25,7 @@ zeroize = { version = "1", features = ["derive"] }
|
|||||||
hmac = "0.12"
|
hmac = "0.12"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
irpc = { workspace = true }
|
|
||||||
irpc-derive = { workspace = true }
|
|
||||||
tokio = { version = "1", features = ["sync", "rt", "macros"] }
|
|
||||||
secp256k1 = { version = "0.29", optional = true }
|
secp256k1 = { version = "0.29", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
hex = "0.4"
|
hex = "0.4"
|
||||||
postcard = { version = "1", features = ["alloc"] }
|
|
||||||
@@ -231,8 +231,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cache_expired_entry_evicted_on_access() {
|
fn test_cache_expired_entry_evicted_on_access() {
|
||||||
let mut config = CacheConfig::default();
|
let config = CacheConfig {
|
||||||
config.ttl = Duration::from_millis(1);
|
ttl: Duration::from_millis(1),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
let mut cache = KeyCache::new(config);
|
let mut cache = KeyCache::new(config);
|
||||||
cache.insert("m/74'/0'/0'/0'", make_cached_key(KeyType::Ed25519));
|
cache.insert("m/74'/0'/0'/0'", make_cached_key(KeyType::Ed25519));
|
||||||
@@ -245,8 +247,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cache_lru_eviction() {
|
fn test_cache_lru_eviction() {
|
||||||
let mut config = CacheConfig::default();
|
let config = CacheConfig {
|
||||||
config.max_entries = 3;
|
max_entries: 3,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
let mut cache = KeyCache::new(config);
|
let mut cache = KeyCache::new(config);
|
||||||
|
|
||||||
@@ -267,8 +271,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cache_lru_access_reorders() {
|
fn test_cache_lru_access_reorders() {
|
||||||
let mut config = CacheConfig::default();
|
let config = CacheConfig {
|
||||||
config.max_entries = 3;
|
max_entries: 3,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
let mut cache = KeyCache::new(config);
|
let mut cache = KeyCache::new(config);
|
||||||
|
|
||||||
@@ -303,8 +309,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_evict_expired_removes_only_expired() {
|
fn test_evict_expired_removes_only_expired() {
|
||||||
let mut config = CacheConfig::default();
|
let config = CacheConfig {
|
||||||
config.ttl = Duration::from_millis(10);
|
ttl: Duration::from_millis(10),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
let mut cache = KeyCache::new(config);
|
let mut cache = KeyCache::new(config);
|
||||||
cache.insert("path1", make_cached_key(KeyType::Ed25519));
|
cache.insert("path1", make_cached_key(KeyType::Ed25519));
|
||||||
|
|||||||
@@ -25,8 +25,8 @@
|
|||||||
//! - [`mnemonic`] — BIP39 mnemonic generation, validation, and seed derivation
|
//! - [`mnemonic`] — BIP39 mnemonic generation, validation, and seed derivation
|
||||||
//! - [`derivation`] — SLIP-0010 Ed25519 HD key derivation and path constants
|
//! - [`derivation`] — SLIP-0010 Ed25519 HD key derivation and path constants
|
||||||
//! - [`encryption`] — AES-256-GCM encrypt/decrypt and `EncryptedData` type
|
//! - [`encryption`] — AES-256-GCM encrypt/decrypt and `EncryptedData` type
|
||||||
//! - [`protocol`] — `VaultProtocol` irpc message enum, `DerivedKey`, `KeyType`
|
//! - [`protocol`] — `DerivedKey` and `KeyType` (return types from vault methods)
|
||||||
//! - [`service`] — `VaultService` implementation with Unlock/Lock lifecycle
|
//! - [`service`] — `VaultServiceHandle` runtime API with Unlock/Lock lifecycle
|
||||||
//! - [`ethereum`] — BIP-0032 secp256k1 HD key derivation (behind `secp256k1` feature)
|
//! - [`ethereum`] — BIP-0032 secp256k1 HD key derivation (behind `secp256k1` feature)
|
||||||
|
|
||||||
pub mod cache;
|
pub mod cache;
|
||||||
@@ -42,7 +42,8 @@ pub mod ethereum;
|
|||||||
// Re-export primary public API
|
// Re-export primary public API
|
||||||
pub use cache::CacheConfig;
|
pub use cache::CacheConfig;
|
||||||
pub use derivation::{DerivationError, ExtendedPrivKey, PATHS};
|
pub use derivation::{DerivationError, ExtendedPrivKey, PATHS};
|
||||||
pub use encryption::{EncryptedData, EncryptionError};
|
pub use encryption::CURRENT_KEY_VERSION;
|
||||||
|
pub use encryption::{EncryptedData, EncryptionError, EncryptionKey};
|
||||||
pub use mnemonic::{Language, Mnemonic, Seed};
|
pub use mnemonic::{Language, Mnemonic, Seed};
|
||||||
pub use protocol::{DerivedKey, KeyType, VaultMessage, VaultProtocol};
|
pub use protocol::{DerivedKey, KeyType};
|
||||||
pub use service::{VaultService, VaultServiceActor, VaultServiceError, VaultServiceHandle};
|
pub use service::{VaultServiceError, VaultServiceHandle};
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
//! VaultProtocol irpc message definition and associated types.
|
//! Vault key types: `DerivedKey` and `KeyType`.
|
||||||
//!
|
//!
|
||||||
//! This module defines the `VaultProtocol` enum for irpc-based message dispatch.
|
//! The vault's dispatch is direct method calls on `VaultServiceHandle`
|
||||||
//! The protocol supports unlock/lock lifecycle, key derivation,
|
//! (ADR-025). The types defined here — `DerivedKey`, `KeyType` — are the
|
||||||
//! and encryption/decryption operations.
|
//! return types from those methods. There is no `VaultProtocol` enum, no
|
||||||
|
//! `VaultMessage`, no `VaultServiceActor`, and no remote dispatch capability.
|
||||||
//!
|
//!
|
||||||
//! # Protocol Operation
|
//! The vault is **local-only by construction**. If remote vault access is
|
||||||
//!
|
//! ever needed, it requires a separate crate that wraps the vault and adds
|
||||||
//! The VaultProtocol follows a lifecycle: the vault starts in a **locked**
|
//! remote transport + auth (ADR-025, OQ-021).
|
||||||
//! state where no derivation or encryption operations are possible. The `Unlock`
|
|
||||||
//! call loads the seed into memory (derived from the mnemonic passphrase). After
|
|
||||||
//! that, derive and encrypt/decrypt operations are available. The `Lock` call
|
|
||||||
//! purges the seed and all cached keys.
|
|
||||||
//!
|
|
||||||
//! # Wire Format
|
|
||||||
//!
|
|
||||||
//! For local (in-process) calls, the protocol uses tokio channels directly.
|
|
||||||
//! For remote (in-cluster) calls, the protocol is serialized with postcard.
|
|
||||||
//! For cross-node (call protocol) exposure, the vault is wrapped in an
|
|
||||||
//! operation that serializes to JSON.
|
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use irpc::rpc_requests;
|
|
||||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use zeroize::Zeroize;
|
use zeroize::Zeroize;
|
||||||
|
|
||||||
use crate::encryption::EncryptedData;
|
|
||||||
|
|
||||||
/// The type of a derived key.
|
/// The type of a derived key.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub enum KeyType {
|
pub enum KeyType {
|
||||||
@@ -46,10 +33,8 @@ pub enum KeyType {
|
|||||||
/// by `#[zeroize(drop)]`).
|
/// by `#[zeroize(drop)]`).
|
||||||
///
|
///
|
||||||
/// Serialization redacts the `private_key` field for human-readable formats
|
/// Serialization redacts the `private_key` field for human-readable formats
|
||||||
/// (JSON) for safety, showing `"[REDACTED]"` instead of the key bytes. For
|
/// (JSON) for safety, showing `"[REDACTED]"` instead of the key bytes.
|
||||||
/// binary formats (postcard, used by irpc), the actual bytes are serialized
|
/// Deserialization always reads the full bytes.
|
||||||
/// so that remote communication works correctly. Deserialization always reads
|
|
||||||
/// the full bytes.
|
|
||||||
#[derive(Zeroize, Deserialize)]
|
#[derive(Zeroize, Deserialize)]
|
||||||
#[zeroize(drop)]
|
#[zeroize(drop)]
|
||||||
pub struct DerivedKey {
|
pub struct DerivedKey {
|
||||||
@@ -98,115 +83,6 @@ impl Serialize for DerivedKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// VaultProtocol message definition.
|
|
||||||
///
|
|
||||||
/// This is the irpc protocol enum that defines all vault operations.
|
|
||||||
/// The `#[rpc_requests]` macro generates:
|
|
||||||
/// - **`VaultMessage`**: message enum with `WithChannels` wrappers for each variant
|
|
||||||
/// - **`Channels<VaultProtocol>`** impls for each wrapper type
|
|
||||||
/// - **`From`** impls for protocol enum and message enum conversions
|
|
||||||
/// - **`Service`** and **`RemoteService`** trait impls for remote dispatch
|
|
||||||
///
|
|
||||||
/// # State Requirements
|
|
||||||
///
|
|
||||||
/// All operations except `Unlock` require the vault to be in an **unlocked**
|
|
||||||
/// state. Calling derive/encrypt/decrypt on a locked vault returns an error.
|
|
||||||
#[rpc_requests(message = VaultMessage, no_spans)]
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
|
||||||
pub enum VaultProtocol {
|
|
||||||
/// Derive an Ed25519 keypair at the given path.
|
|
||||||
///
|
|
||||||
/// Path format: `m/74'/0'/0'/0'` (SLIP-0010 hardened-only notation).
|
|
||||||
/// Returns a `DerivedKey` with `KeyType::Ed25519`.
|
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<DerivedKey, crate::service::VaultServiceError>>)]
|
|
||||||
#[wrap(DeriveEd25519)]
|
|
||||||
DeriveEd25519 {
|
|
||||||
/// SLIP-0010 derivation path (e.g., "m/74'/0'/0'/0'").
|
|
||||||
path: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Derive an AES-256-GCM encryption key at the given path.
|
|
||||||
///
|
|
||||||
/// The default encryption path is `m/74'/2'/0'/0'`.
|
|
||||||
/// Returns a `DerivedKey` with `KeyType::Aes256Gcm`.
|
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<DerivedKey, crate::service::VaultServiceError>>)]
|
|
||||||
#[wrap(DeriveEncryptionKey)]
|
|
||||||
DeriveEncryptionKey {
|
|
||||||
/// SLIP-0010 derivation path for the encryption key.
|
|
||||||
path: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Derive a secp256k1 (Ethereum) keypair at the given path.
|
|
||||||
///
|
|
||||||
/// The default Ethereum path is `m/44'/60'/0'/0/0`.
|
|
||||||
/// Returns a `DerivedKey` with `KeyType::Secp256k1`.
|
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<DerivedKey, crate::service::VaultServiceError>>)]
|
|
||||||
#[wrap(DeriveEthereumKey)]
|
|
||||||
DeriveEthereumKey {
|
|
||||||
/// BIP-0032 derivation path (e.g., "m/44'/60'/0'/0/0").
|
|
||||||
path: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Derive a deterministic password at the given path.
|
|
||||||
///
|
|
||||||
/// Path format: `m/74'/1'/0'/{hash}'` (SLIP-0010 hardened notation).
|
|
||||||
/// The `length` parameter controls the output length.
|
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<Vec<u8>, crate::service::VaultServiceError>>)]
|
|
||||||
#[wrap(DerivePassword)]
|
|
||||||
DerivePassword {
|
|
||||||
/// SLIP-0010 derivation path for the password.
|
|
||||||
path: String,
|
|
||||||
/// Desired password length in bytes.
|
|
||||||
length: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Encrypt plaintext using a derived encryption key.
|
|
||||||
///
|
|
||||||
/// The key is derived at the path `m/74'/2'/0'/0'` with the given version.
|
|
||||||
/// Returns an `EncryptedData` blob suitable for storage.
|
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<EncryptedData, crate::service::VaultServiceError>>)]
|
|
||||||
#[wrap(Encrypt)]
|
|
||||||
Encrypt {
|
|
||||||
/// The plaintext string to encrypt.
|
|
||||||
plaintext: String,
|
|
||||||
/// The key version for rotation tracking.
|
|
||||||
key_version: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Decrypt an `EncryptedData` blob back to plaintext.
|
|
||||||
///
|
|
||||||
/// The key is derived from the seed at the path indicated by the key version.
|
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<String, crate::service::VaultServiceError>>)]
|
|
||||||
#[wrap(Decrypt)]
|
|
||||||
Decrypt {
|
|
||||||
/// The encrypted data blob to decrypt.
|
|
||||||
encrypted: EncryptedData,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Lock the service, purging the seed and all cached derived keys.
|
|
||||||
///
|
|
||||||
/// After locking, no derive/encrypt/decrypt operations are possible
|
|
||||||
/// until `Unlock` is called again. Calls `zeroize()` on all sensitive
|
|
||||||
/// material (ADR-038).
|
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<(), crate::service::VaultServiceError>>)]
|
|
||||||
#[wrap(Lock)]
|
|
||||||
Lock,
|
|
||||||
|
|
||||||
/// Unlock the service with a BIP39 mnemonic and optional passphrase.
|
|
||||||
///
|
|
||||||
/// The mnemonic is the space-separated BIP39 word list. The passphrase is
|
|
||||||
/// the optional BIP39 password extension (the "25th word"). After unlocking,
|
|
||||||
/// derive and encrypt/decrypt operations are available.
|
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<(), crate::service::VaultServiceError>>)]
|
|
||||||
#[wrap(Unlock)]
|
|
||||||
Unlock {
|
|
||||||
/// The BIP39 mnemonic phrase (space-separated word list).
|
|
||||||
mnemonic: String,
|
|
||||||
/// Optional BIP39 passphrase (the "25th word" password extension).
|
|
||||||
passphrase: Option<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -249,35 +125,6 @@ mod tests {
|
|||||||
assert!(json.contains("Ed25519"), "JSON must contain key_type");
|
assert!(json.contains("Ed25519"), "JSON must contain key_type");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_derived_key_serialize_preserves_bytes_postcard() {
|
|
||||||
let key = make_test_key();
|
|
||||||
let bytes = postcard::to_allocvec(&key).unwrap();
|
|
||||||
let restored: DerivedKey = postcard::from_bytes(&bytes).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
restored.private_key,
|
|
||||||
vec![0xABu8; 32],
|
|
||||||
"postcard must preserve private_key bytes"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
restored.public_key,
|
|
||||||
vec![0xCDu8; 32],
|
|
||||||
"postcard must preserve public_key bytes"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_derived_key_deserialize_preserves_bytes() {
|
|
||||||
let key = make_test_key();
|
|
||||||
let bytes = postcard::to_allocvec(&key.private_key).unwrap();
|
|
||||||
let restored: Vec<u8> = postcard::from_bytes(&bytes).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
restored,
|
|
||||||
vec![0xABu8; 32],
|
|
||||||
"Deserialization must preserve private_key bytes"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_derived_key_zeroize_on_drop() {
|
fn test_derived_key_zeroize_on_drop() {
|
||||||
let key = DerivedKey {
|
let key = DerivedKey {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//! VaultService implementation with Unlock/Lock lifecycle.
|
//! VaultServiceHandle — the sole runtime API for the vault.
|
||||||
//!
|
//!
|
||||||
//! The `VaultService` is the primary runtime interface for key management.
|
//! The `VaultServiceHandle` wraps the vault's state in an
|
||||||
//! It holds the master seed in `Zeroize`-protected memory and provides methods
|
//! `Arc<std::sync::RwLock<>>` and provides direct, synchronous method calls
|
||||||
//! for the Unlock/Lock lifecycle, key derivation, and encryption/decryption.
|
//! for the unlock/lock lifecycle, key derivation, and encryption/decryption.
|
||||||
//!
|
//!
|
||||||
//! # Lifecycle
|
//! # Lifecycle
|
||||||
//!
|
//!
|
||||||
@@ -25,37 +25,30 @@
|
|||||||
//! → vault returns to locked state
|
//! → vault returns to locked state
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Dispatch Paths
|
//! # Dispatch
|
||||||
//!
|
//!
|
||||||
//! There are two ways to interact with the vault:
|
//! The vault uses **direct method calls** on `VaultServiceHandle` — no actor,
|
||||||
//!
|
//! no message enum, no channels, no serialization (ADR-025). The handle is
|
||||||
//! 1. **Local (in-process)**: `VaultServiceHandle` wraps `VaultServiceInner`
|
//! `Arc<std::sync::RwLock<VaultServiceInner>>` — clone it, share it, call
|
||||||
//! behind `Arc<RwLock<>>` and provides direct method calls without serialization.
|
//! methods directly. All methods are synchronous (no `async`, no `.await`).
|
||||||
//! 2. **Remote (in-cluster)**: `VaultServiceActor` processes `VaultMessage`
|
//! The vault does not depend on `tokio` (ADR-025).
|
||||||
//! variants from an mpsc channel and dispatches to the handle methods.
|
|
||||||
//!
|
//!
|
||||||
//! # Assembly
|
//! # Assembly
|
||||||
//!
|
//!
|
||||||
//! The `VaultService` is assembled by the CLI binary. The CLI unlocks the vault
|
//! The `VaultServiceHandle` is assembled by the CLI binary. The CLI unlocks
|
||||||
//! at startup and injects derived/decrypted material into operation contexts.
|
//! the vault at startup and injects derived/decrypted material into operation
|
||||||
//! No handler crate accesses the vault directly — they receive keys through
|
//! contexts. No handler crate accesses the vault directly — they receive keys
|
||||||
//! their operation context or via the call protocol.
|
//! through their operation context or via the call protocol.
|
||||||
|
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use irpc::WithChannels;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
use crate::cache::{CacheConfig, CachedKey, KeyCache};
|
use crate::cache::{CacheConfig, CachedKey, KeyCache};
|
||||||
use crate::derivation::{self, DerivationError, PATHS};
|
use crate::derivation::{self, DerivationError, PATHS};
|
||||||
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::{
|
|
||||||
Decrypt, DeriveEd25519, DeriveEncryptionKey, DeriveEthereumKey, DerivePassword, Encrypt,
|
|
||||||
VaultMessage, VaultProtocol, Unlock,
|
|
||||||
};
|
|
||||||
use crate::protocol::{DerivedKey, KeyType};
|
use crate::protocol::{DerivedKey, KeyType};
|
||||||
|
|
||||||
/// Handle to a running VaultService for local (in-process) use.
|
/// Handle to a running VaultService for local (in-process) use.
|
||||||
@@ -80,7 +73,7 @@ struct VaultServiceInner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Errors that can occur during vault operations.
|
/// Errors that can occur during vault operations.
|
||||||
#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum VaultServiceError {
|
pub enum VaultServiceError {
|
||||||
#[error("vault is locked; call Unlock first")]
|
#[error("vault is locked; call Unlock first")]
|
||||||
VaultLocked,
|
VaultLocked,
|
||||||
@@ -206,10 +199,7 @@ impl VaultServiceHandle {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let seed = inner
|
let seed = inner.seed.as_ref().ok_or(VaultServiceError::VaultLocked)?;
|
||||||
.seed
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(VaultServiceError::VaultLocked)?;
|
|
||||||
let key = derivation::derive_path_from_seed(seed.as_bytes(), path)?;
|
let key = derivation::derive_path_from_seed(seed.as_bytes(), path)?;
|
||||||
let private_key = key.private_key().to_vec();
|
let private_key = key.private_key().to_vec();
|
||||||
let public_key = key.public_key().to_vec();
|
let public_key = key.public_key().to_vec();
|
||||||
@@ -237,10 +227,7 @@ impl VaultServiceHandle {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let seed = inner
|
let seed = inner.seed.as_ref().ok_or(VaultServiceError::VaultLocked)?;
|
||||||
.seed
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(VaultServiceError::VaultLocked)?;
|
|
||||||
let key = derivation::derive_path_from_seed(seed.as_bytes(), path)?;
|
let key = derivation::derive_path_from_seed(seed.as_bytes(), path)?;
|
||||||
let private_key = key.private_key().to_vec();
|
let private_key = key.private_key().to_vec();
|
||||||
let public_key = key.public_key().to_vec();
|
let public_key = key.public_key().to_vec();
|
||||||
@@ -274,10 +261,7 @@ impl VaultServiceHandle {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let seed = inner
|
let seed = inner.seed.as_ref().ok_or(VaultServiceError::VaultLocked)?;
|
||||||
.seed
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(VaultServiceError::VaultLocked)?;
|
|
||||||
|
|
||||||
let key = crate::ethereum::derive_secp256k1_path(seed.as_bytes(), path)?;
|
let key = crate::ethereum::derive_secp256k1_path(seed.as_bytes(), path)?;
|
||||||
let private_key = key.private_key().to_vec();
|
let private_key = key.private_key().to_vec();
|
||||||
@@ -299,19 +283,12 @@ impl VaultServiceHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn derive_password(
|
pub fn derive_password(&self, path: &str, length: usize) -> Result<Vec<u8>, VaultServiceError> {
|
||||||
&self,
|
|
||||||
path: &str,
|
|
||||||
length: usize,
|
|
||||||
) -> Result<Vec<u8>, VaultServiceError> {
|
|
||||||
let inner = self.inner.read().unwrap();
|
let inner = self.inner.read().unwrap();
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(VaultServiceError::VaultLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
let seed = inner
|
let seed = inner.seed.as_ref().ok_or(VaultServiceError::VaultLocked)?;
|
||||||
.seed
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(VaultServiceError::VaultLocked)?;
|
|
||||||
|
|
||||||
let key = derivation::derive_path_from_seed(seed.as_bytes(), path)?;
|
let key = derivation::derive_path_from_seed(seed.as_bytes(), path)?;
|
||||||
let private_key = key.private_key();
|
let private_key = key.private_key();
|
||||||
@@ -345,10 +322,7 @@ impl VaultServiceHandle {
|
|||||||
let private_key = if let Some(cached) = inner.cache.get(PATHS::ENCRYPTION) {
|
let private_key = if let Some(cached) = inner.cache.get(PATHS::ENCRYPTION) {
|
||||||
cached.private_key.clone()
|
cached.private_key.clone()
|
||||||
} else {
|
} else {
|
||||||
let seed = inner
|
let seed = inner.seed.as_ref().ok_or(VaultServiceError::VaultLocked)?;
|
||||||
.seed
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(VaultServiceError::VaultLocked)?;
|
|
||||||
let derived = derivation::derive_path_from_seed(seed.as_bytes(), PATHS::ENCRYPTION)?;
|
let derived = derivation::derive_path_from_seed(seed.as_bytes(), PATHS::ENCRYPTION)?;
|
||||||
let pk = derived.private_key().to_vec();
|
let pk = derived.private_key().to_vec();
|
||||||
let pubk = derived.public_key().to_vec();
|
let pubk = derived.public_key().to_vec();
|
||||||
@@ -372,10 +346,7 @@ impl VaultServiceHandle {
|
|||||||
let private_key = if let Some(cached) = inner.cache.get(PATHS::ENCRYPTION) {
|
let private_key = if let Some(cached) = inner.cache.get(PATHS::ENCRYPTION) {
|
||||||
cached.private_key.clone()
|
cached.private_key.clone()
|
||||||
} else {
|
} else {
|
||||||
let seed = inner
|
let seed = inner.seed.as_ref().ok_or(VaultServiceError::VaultLocked)?;
|
||||||
.seed
|
|
||||||
.as_ref()
|
|
||||||
.ok_or(VaultServiceError::VaultLocked)?;
|
|
||||||
let derived = derivation::derive_path_from_seed(seed.as_bytes(), PATHS::ENCRYPTION)?;
|
let derived = derivation::derive_path_from_seed(seed.as_bytes(), PATHS::ENCRYPTION)?;
|
||||||
let pk = derived.private_key().to_vec();
|
let pk = derived.private_key().to_vec();
|
||||||
let pubk = derived.public_key().to_vec();
|
let pubk = derived.public_key().to_vec();
|
||||||
@@ -396,166 +367,9 @@ impl Default for VaultServiceHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The VaultService manages the lifecycle of the master seed and provides
|
|
||||||
/// secret operations. This is the type used by the irpc service handler.
|
|
||||||
///
|
|
||||||
/// For local (in-process) use, prefer `VaultServiceHandle` which wraps
|
|
||||||
/// this in thread-safe locks.
|
|
||||||
pub struct VaultService {
|
|
||||||
handle: VaultServiceHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VaultService {
|
|
||||||
/// Create a new VaultService in the locked state.
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
handle: VaultServiceHandle::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a handle for local (in-process) use.
|
|
||||||
pub fn handle(&self) -> &VaultServiceHandle {
|
|
||||||
&self.handle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for VaultService {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Actor that processes `VaultMessage` variants and dispatches to `VaultServiceHandle`.
|
|
||||||
///
|
|
||||||
/// The actor runs as a `tokio::task`, receives messages from an mpsc channel,
|
|
||||||
/// dispatches to the handle methods, and sends responses through oneshot channels.
|
|
||||||
///
|
|
||||||
/// # Usage
|
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
/// let handle = VaultServiceHandle::new();
|
|
||||||
/// let (client, actor) = VaultServiceActor::spawn(handle);
|
|
||||||
/// tokio::task::spawn(actor.run(rx));
|
|
||||||
/// // Use client to send messages
|
|
||||||
/// ```
|
|
||||||
pub struct VaultServiceActor {
|
|
||||||
handle: VaultServiceHandle,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VaultServiceActor {
|
|
||||||
/// Create a new actor wrapping the given handle.
|
|
||||||
pub fn new(handle: VaultServiceHandle) -> Self {
|
|
||||||
Self { handle }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run the actor message loop, processing `VaultMessage` variants.
|
|
||||||
///
|
|
||||||
/// This method runs until the receiver channel is closed. Each message
|
|
||||||
/// variant is dispatched to the corresponding `VaultServiceHandle` method
|
|
||||||
/// and the response is sent through the oneshot channel embedded in the message.
|
|
||||||
pub async fn run(mut self, mut rx: tokio::sync::mpsc::Receiver<VaultMessage>) {
|
|
||||||
while let Some(msg) = rx.recv().await {
|
|
||||||
self.handle_message(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Spawn the actor as a `tokio::task` and return a `Client<VaultProtocol>` for sending messages.
|
|
||||||
///
|
|
||||||
/// The actor runs on a tokio task and processes messages from the mpsc channel.
|
|
||||||
/// The returned `Client<VaultProtocol>` can be used to send `VaultMessage` variants
|
|
||||||
/// to the actor.
|
|
||||||
pub fn spawn(
|
|
||||||
handle: VaultServiceHandle,
|
|
||||||
) -> (irpc::Client<VaultProtocol>, VaultServiceActor) {
|
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
|
||||||
let client = irpc::Client::local(tx);
|
|
||||||
let actor = Self::new(handle.clone());
|
|
||||||
tokio::task::spawn(actor.run(rx));
|
|
||||||
(client, Self::new(handle))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a single `VaultMessage` by dispatching to the appropriate handle method.
|
|
||||||
fn handle_message(&mut self, msg: VaultMessage) {
|
|
||||||
match msg {
|
|
||||||
VaultMessage::DeriveEd25519(msg) => {
|
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
|
||||||
let DeriveEd25519 { path } = inner;
|
|
||||||
let result = self.handle.derive_ed25519(&path);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = tx.send(result).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
VaultMessage::DeriveEncryptionKey(msg) => {
|
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
|
||||||
let DeriveEncryptionKey { path } = inner;
|
|
||||||
let result = self.handle.derive_encryption_key(&path);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = tx.send(result).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
VaultMessage::DeriveEthereumKey(msg) => {
|
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
|
||||||
let DeriveEthereumKey { path } = inner;
|
|
||||||
let result = self.handle.derive_ethereum_key(&path);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = tx.send(result).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
VaultMessage::DerivePassword(msg) => {
|
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
|
||||||
let DerivePassword { path, length } = inner;
|
|
||||||
let result = self.handle.derive_password(&path, length);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = tx.send(result).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
VaultMessage::Encrypt(msg) => {
|
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
|
||||||
let Encrypt {
|
|
||||||
plaintext,
|
|
||||||
key_version,
|
|
||||||
} = inner;
|
|
||||||
let result = self.handle.encrypt(&plaintext, key_version);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = tx.send(result).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
VaultMessage::Decrypt(msg) => {
|
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
|
||||||
let Decrypt { encrypted } = inner;
|
|
||||||
let result = self.handle.decrypt(&encrypted);
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = tx.send(result).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
VaultMessage::Lock(msg) => {
|
|
||||||
let WithChannels { inner: _, tx, .. } = msg;
|
|
||||||
self.handle.lock();
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = tx.send(Ok(())).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
VaultMessage::Unlock(msg) => {
|
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
|
||||||
let Unlock {
|
|
||||||
mnemonic,
|
|
||||||
passphrase,
|
|
||||||
} = inner;
|
|
||||||
let result = self.handle.unlock(&mnemonic, passphrase.as_deref());
|
|
||||||
tokio::spawn(async move {
|
|
||||||
let _ = tx.send(result).await;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::protocol::Lock;
|
|
||||||
use irpc::channel::oneshot;
|
|
||||||
use irpc::WithChannels;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_service_starts_locked() {
|
fn test_service_starts_locked() {
|
||||||
@@ -750,10 +564,7 @@ mod tests {
|
|||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let result = service.derive_ethereum_key(PATHS::ETHEREUM);
|
let result = service.derive_ethereum_key(PATHS::ETHEREUM);
|
||||||
assert!(matches!(
|
assert!(matches!(result, Err(VaultServiceError::UnsupportedKeyType)));
|
||||||
result,
|
|
||||||
Err(VaultServiceError::UnsupportedKeyType)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -847,72 +658,6 @@ mod tests {
|
|||||||
assert_eq!(service.inner.read().unwrap().cache.len(), 1);
|
assert_eq!(service.inner.read().unwrap().cache.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_actor_unlock_responds_successfully() {
|
|
||||||
let handle = VaultServiceHandle::new();
|
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
|
||||||
let actor = VaultServiceActor::new(handle);
|
|
||||||
tokio::task::spawn(actor.run(rx));
|
|
||||||
|
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
|
||||||
let msg = VaultMessage::Unlock(WithChannels::from((
|
|
||||||
Unlock {
|
|
||||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
|
|
||||||
passphrase: None,
|
|
||||||
},
|
|
||||||
resp_tx,
|
|
||||||
)));
|
|
||||||
tx.send(msg).await.unwrap();
|
|
||||||
|
|
||||||
let result = resp_rx.await.unwrap();
|
|
||||||
assert!(result.is_ok(), "Unlock via actor must succeed");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_actor_derive_ed25519_returns_key() {
|
|
||||||
let handle = VaultServiceHandle::new();
|
|
||||||
handle.unlock_new(24).unwrap();
|
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
|
||||||
let actor = VaultServiceActor::new(handle);
|
|
||||||
tokio::task::spawn(actor.run(rx));
|
|
||||||
|
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
|
||||||
let msg = VaultMessage::DeriveEd25519(WithChannels::from((
|
|
||||||
DeriveEd25519 {
|
|
||||||
path: PATHS::IDENTITY.to_string(),
|
|
||||||
},
|
|
||||||
resp_tx,
|
|
||||||
)));
|
|
||||||
tx.send(msg).await.unwrap();
|
|
||||||
|
|
||||||
let result = resp_rx.await.unwrap();
|
|
||||||
assert!(result.is_ok(), "DeriveEd25519 via actor must succeed");
|
|
||||||
let key = result.unwrap();
|
|
||||||
assert!(
|
|
||||||
!key.private_key.is_empty(),
|
|
||||||
"DerivedKey must have private_key"
|
|
||||||
);
|
|
||||||
assert_eq!(key.key_type, KeyType::Ed25519);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_actor_lock_clears_state() {
|
|
||||||
let handle = VaultServiceHandle::new();
|
|
||||||
handle.unlock_new(24).unwrap();
|
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
|
||||||
let actor = VaultServiceActor::new(handle.clone());
|
|
||||||
tokio::task::spawn(actor.run(rx));
|
|
||||||
|
|
||||||
let (resp_tx, resp_rx): (oneshot::Sender<Result<(), VaultServiceError>>, _) =
|
|
||||||
oneshot::channel();
|
|
||||||
let msg = VaultMessage::Lock(WithChannels::from((Lock, resp_tx)));
|
|
||||||
tx.send(msg).await.unwrap();
|
|
||||||
|
|
||||||
let result = resp_rx.await.unwrap();
|
|
||||||
assert!(result.is_ok(), "Lock via actor must succeed");
|
|
||||||
assert!(!handle.is_unlocked(), "Handle must be locked after Lock");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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();
|
||||||
@@ -943,30 +688,4 @@ mod tests {
|
|||||||
"Unlock with None passphrase must produce same seed as another None passphrase unlock"
|
"Unlock with None passphrase must produce same seed as another None passphrase unlock"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_actor_unlock_with_passphrase() {
|
|
||||||
let handle = VaultServiceHandle::new();
|
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
|
||||||
let actor = VaultServiceActor::new(handle);
|
|
||||||
tokio::task::spawn(actor.run(rx));
|
|
||||||
|
|
||||||
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
|
||||||
|
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
|
||||||
let msg = VaultMessage::Unlock(WithChannels::from((
|
|
||||||
Unlock {
|
|
||||||
mnemonic: mnemonic.to_string(),
|
|
||||||
passphrase: Some("TREZOR".to_string()),
|
|
||||||
},
|
|
||||||
resp_tx,
|
|
||||||
)));
|
|
||||||
tx.send(msg).await.unwrap();
|
|
||||||
|
|
||||||
let result = resp_rx.await.unwrap();
|
|
||||||
assert!(
|
|
||||||
result.is_ok(),
|
|
||||||
"Unlock with passphrase via actor must succeed"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user