refactor: rename alknet-secret to alknet-vault
Rename the crate from alknet-secret to alknet-vault to better reflect its purpose as a local key vault (seed management, key derivation, encryption) rather than a network service. Symbol renames: - SecretService → VaultService - SecretServiceHandle → VaultServiceHandle - SecretServiceActor → VaultServiceActor - SecretServiceError → VaultServiceError - SecretProtocol → VaultProtocol - SecretMessage → VaultMessage - ServiceLocked → VaultLocked - alknet_secret → alknet_vault (crate name) Update ADR-008 with vault access pattern: the vault is a capability source, not a service endpoint. The CLI injects derived/decrypted material into operation contexts — handlers never hold vault references.
This commit is contained in:
2
Cargo.lock
generated
2
Cargo.lock
generated
@@ -47,7 +47,7 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "alknet-secret"
|
name = "alknet-vault"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"aes-gcm",
|
"aes-gcm",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"crates/alknet-secret",
|
"crates/alknet-vault",
|
||||||
]
|
]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ A self-hostable networking toolkit built on QUIC+TLS with ALPN-based protocol di
|
|||||||
|
|
||||||
| Crate | Status | Description |
|
| Crate | Status | Description |
|
||||||
|-------|--------|-------------|
|
|-------|--------|-------------|
|
||||||
| `alknet-secret` | stable | BIP39/SLIP-0010/AES-GCM key derivation and encryption |
|
| `alknet-vault` | stable | Local key vault: BIP39/SLIP-0010/AES-GCM key derivation and encryption |
|
||||||
| `alknet-core` | planned | ProtocolHandler trait, ALPN router, auth/identity, config |
|
| `alknet-core` | planned | ProtocolHandler trait, ALPN router, auth/identity, config |
|
||||||
| `alknet-ssh` | planned | SSH handler (russh), SOCKS5, port forwarding |
|
| `alknet-ssh` | planned | SSH handler (russh), SOCKS5, port forwarding |
|
||||||
| `alknet-call` | planned | JSON-RPC call protocol (EventEnvelope framing) |
|
| `alknet-call` | planned | JSON-RPC call protocol (EventEnvelope framing) |
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "alknet-secret"
|
name = "alknet-vault"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description = "BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM encryption, and SecretProtocol irpc service for alknet"
|
description = "Local key vault: BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM encryption for securing provider keys, credentials, and identity material"
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "alknet_secret"
|
name = "alknet_vault"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = []
|
default = []
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
//! TTL-based key cache with LRU eviction for SecretService.
|
//! TTL-based key cache with LRU eviction for VaultService.
|
||||||
//!
|
//!
|
||||||
//! The `KeyCache` stores derived key material keyed by derivation path. Entries
|
//! The `KeyCache` stores derived key material keyed by derivation path. Entries
|
||||||
//! expire after a configurable TTL (default: 1 hour) and are evicted lazily on
|
//! expire after a configurable TTL (default: 1 hour) and are evicted lazily on
|
||||||
@@ -24,7 +24,7 @@ type HmacSha512 = Hmac<Sha512>;
|
|||||||
|
|
||||||
/// Well-known derivation path constants for alknet key material.
|
/// Well-known derivation path constants for alknet key material.
|
||||||
///
|
///
|
||||||
/// These paths are defined once and referenced by both the secret service and
|
/// These paths are defined once and referenced by both the vault service and
|
||||||
/// external consumers that need to request specific key types.
|
/// external consumers that need to request specific key types.
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
pub mod PATHS {
|
pub mod PATHS {
|
||||||
@@ -101,8 +101,8 @@ impl ExtendedPrivKey {
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// use alknet_secret::derivation::{derive_path_from_seed, PATHS};
|
/// use alknet_vault::derivation::{derive_path_from_seed, PATHS};
|
||||||
/// use alknet_secret::mnemonic::Mnemonic;
|
/// use alknet_vault::mnemonic::Mnemonic;
|
||||||
///
|
///
|
||||||
/// let mnemonic = Mnemonic::generate(24).unwrap();
|
/// let mnemonic = Mnemonic::generate(24).unwrap();
|
||||||
/// let seed = mnemonic.to_seed(None);
|
/// let seed = mnemonic.to_seed(None);
|
||||||
@@ -52,7 +52,7 @@ pub const CURRENT_KEY_VERSION: u32 = 1;
|
|||||||
///
|
///
|
||||||
/// The Rust `EncryptedData` is a superset of the TypeScript `EncryptedDataSchema`
|
/// The Rust `EncryptedData` is a superset of the TypeScript `EncryptedDataSchema`
|
||||||
/// from `@alkdev/storage`. Migration path: re-encrypt TypeScript-encrypted data
|
/// from `@alkdev/storage`. Migration path: re-encrypt TypeScript-encrypted data
|
||||||
/// using the Rust secret service with a new key version.
|
/// using the Rust vault with a new key version.
|
||||||
///
|
///
|
||||||
/// See OQ-SVC-03 for the compatibility tracking.
|
/// See OQ-SVC-03 for the compatibility tracking.
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
@@ -138,8 +138,8 @@ fn derive_child(
|
|||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// use alknet_secret::ethereum::derive_secp256k1_path;
|
/// use alknet_vault::ethereum::derive_secp256k1_path;
|
||||||
/// use alknet_secret::derivation::PATHS;
|
/// use alknet_vault::derivation::PATHS;
|
||||||
///
|
///
|
||||||
/// let key = derive_secp256k1_path(seed, PATHS::ETHEREUM).unwrap();
|
/// let key = derive_secp256k1_path(seed, PATHS::ETHEREUM).unwrap();
|
||||||
/// assert_eq!(key.private_key().len(), 32);
|
/// assert_eq!(key.private_key().len(), 32);
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
//! # alknet-secret
|
//! # alknet-vault
|
||||||
//!
|
//!
|
||||||
//! BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM
|
//! Local key vault: BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation,
|
||||||
//! encryption for external credentials, and the `SecretProtocol` irpc service.
|
//! AES-256-GCM encryption for securing provider keys, credentials, and identity material.
|
||||||
//!
|
//!
|
||||||
//! This crate is the only component that holds the master seed phrase. All other
|
//! This crate is the only component that holds the master seed phrase. The CLI binary
|
||||||
//! crates request derived keys through the `SecretProtocol` irpc service or the
|
//! unlocks the vault at startup and injects derived/decrypted material into operation
|
||||||
//! `SecretServiceHandle` local API.
|
//! contexts. Other crates never access the vault directly — they receive keys through
|
||||||
|
//! their operation context or via the call protocol.
|
||||||
//!
|
//!
|
||||||
//! ## Crate Independence
|
//! ## Crate Independence
|
||||||
//!
|
//!
|
||||||
//! alknet-secret does **not** depend on alknet-core or alknet-storage. Per ADR-027,
|
//! alknet-vault does **not** depend on alknet-core or any other alknet crate. It is
|
||||||
//! it is fully independent. The `EncryptedData` wire format is shared with
|
//! fully independent and usable in contexts where QUIC networking doesn't exist (CLI
|
||||||
//! alknet-storage by type-level compatibility, not a crate dependency.
|
//! tools, test harnesses, WASM key derivation).
|
||||||
//!
|
//!
|
||||||
//! ## Security Model
|
//! ## Security Model
|
||||||
//!
|
//!
|
||||||
@@ -24,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`] — `SecretProtocol` irpc service enum, `DerivedKey`, `KeyType`
|
//! - [`protocol`] — `VaultProtocol` irpc message enum, `DerivedKey`, `KeyType`
|
||||||
//! - [`service`] — `SecretService` implementation with Unlock/Lock lifecycle
|
//! - [`service`] — `VaultService` implementation 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;
|
||||||
@@ -43,5 +44,5 @@ pub use cache::CacheConfig;
|
|||||||
pub use derivation::{DerivationError, ExtendedPrivKey, PATHS};
|
pub use derivation::{DerivationError, ExtendedPrivKey, PATHS};
|
||||||
pub use encryption::{EncryptedData, EncryptionError};
|
pub use encryption::{EncryptedData, EncryptionError};
|
||||||
pub use mnemonic::{Language, Mnemonic, Seed};
|
pub use mnemonic::{Language, Mnemonic, Seed};
|
||||||
pub use protocol::{DerivedKey, KeyType, SecretMessage, SecretProtocol};
|
pub use protocol::{DerivedKey, KeyType, VaultMessage, VaultProtocol};
|
||||||
pub use service::{SecretService, SecretServiceActor, SecretServiceError, SecretServiceHandle};
|
pub use service::{VaultService, VaultServiceActor, VaultServiceError, VaultServiceHandle};
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
//! SecretProtocol irpc service definition and associated types.
|
//! VaultProtocol irpc message definition and associated types.
|
||||||
//!
|
//!
|
||||||
//! This module defines the `SecretProtocol` enum for irpc-based inter-service
|
//! This module defines the `VaultProtocol` enum for irpc-based message dispatch.
|
||||||
//! communication. The protocol supports unlock/lock lifecycle, key derivation,
|
//! The protocol supports unlock/lock lifecycle, key derivation,
|
||||||
//! and encryption/decryption operations.
|
//! and encryption/decryption operations.
|
||||||
//!
|
//!
|
||||||
//! # Protocol Operation
|
//! # Protocol Operation
|
||||||
//!
|
//!
|
||||||
//! The SecretProtocol follows a lifecycle: the service starts in a **locked**
|
//! The VaultProtocol follows a lifecycle: the vault starts in a **locked**
|
||||||
//! state where no derivation or encryption operations are possible. The `Unlock`
|
//! state where no derivation or encryption operations are possible. The `Unlock`
|
||||||
//! call loads the seed into memory (derived from the mnemonic passphrase). After
|
//! call loads the seed into memory (derived from the mnemonic passphrase). After
|
||||||
//! that, derive and encrypt/decrypt operations are available. The `Lock` call
|
//! that, derive and encrypt/decrypt operations are available. The `Lock` call
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
//!
|
//!
|
||||||
//! For local (in-process) calls, the protocol uses tokio channels directly.
|
//! For local (in-process) calls, the protocol uses tokio channels directly.
|
||||||
//! For remote (in-cluster) calls, the protocol is serialized with postcard.
|
//! For remote (in-cluster) calls, the protocol is serialized with postcard.
|
||||||
//! For cross-node (call protocol) exposure, the service is wrapped in an
|
//! For cross-node (call protocol) exposure, the vault is wrapped in an
|
||||||
//! operation that serializes to JSON.
|
//! operation that serializes to JSON.
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
@@ -98,27 +98,27 @@ impl Serialize for DerivedKey {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SecretProtocol service definition.
|
/// VaultProtocol message definition.
|
||||||
///
|
///
|
||||||
/// This is the irpc protocol enum that defines all secret service operations.
|
/// This is the irpc protocol enum that defines all vault operations.
|
||||||
/// The `#[rpc_requests]` macro generates:
|
/// The `#[rpc_requests]` macro generates:
|
||||||
/// - **`SecretMessage`**: message enum with `WithChannels` wrappers for each variant
|
/// - **`VaultMessage`**: message enum with `WithChannels` wrappers for each variant
|
||||||
/// - **`Channels<SecretProtocol>`** impls for each wrapper type
|
/// - **`Channels<VaultProtocol>`** impls for each wrapper type
|
||||||
/// - **`From`** impls for protocol enum and message enum conversions
|
/// - **`From`** impls for protocol enum and message enum conversions
|
||||||
/// - **`Service`** and **`RemoteService`** trait impls for remote dispatch
|
/// - **`Service`** and **`RemoteService`** trait impls for remote dispatch
|
||||||
///
|
///
|
||||||
/// # State Requirements
|
/// # State Requirements
|
||||||
///
|
///
|
||||||
/// All operations except `Unlock` require the service to be in an **unlocked**
|
/// All operations except `Unlock` require the vault to be in an **unlocked**
|
||||||
/// state. Calling derive/encrypt/decrypt on a locked service returns an error.
|
/// state. Calling derive/encrypt/decrypt on a locked vault returns an error.
|
||||||
#[rpc_requests(message = SecretMessage, no_spans)]
|
#[rpc_requests(message = VaultMessage, no_spans)]
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub enum SecretProtocol {
|
pub enum VaultProtocol {
|
||||||
/// Derive an Ed25519 keypair at the given path.
|
/// Derive an Ed25519 keypair at the given path.
|
||||||
///
|
///
|
||||||
/// Path format: `m/74'/0'/0'/0'` (SLIP-0010 hardened-only notation).
|
/// Path format: `m/74'/0'/0'/0'` (SLIP-0010 hardened-only notation).
|
||||||
/// Returns a `DerivedKey` with `KeyType::Ed25519`.
|
/// Returns a `DerivedKey` with `KeyType::Ed25519`.
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<DerivedKey, crate::service::SecretServiceError>>)]
|
#[rpc(tx = irpc::channel::oneshot::Sender<Result<DerivedKey, crate::service::VaultServiceError>>)]
|
||||||
#[wrap(DeriveEd25519)]
|
#[wrap(DeriveEd25519)]
|
||||||
DeriveEd25519 {
|
DeriveEd25519 {
|
||||||
/// SLIP-0010 derivation path (e.g., "m/74'/0'/0'/0'").
|
/// SLIP-0010 derivation path (e.g., "m/74'/0'/0'/0'").
|
||||||
@@ -129,7 +129,7 @@ pub enum SecretProtocol {
|
|||||||
///
|
///
|
||||||
/// The default encryption path is `m/74'/2'/0'/0'`.
|
/// The default encryption path is `m/74'/2'/0'/0'`.
|
||||||
/// Returns a `DerivedKey` with `KeyType::Aes256Gcm`.
|
/// Returns a `DerivedKey` with `KeyType::Aes256Gcm`.
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<DerivedKey, crate::service::SecretServiceError>>)]
|
#[rpc(tx = irpc::channel::oneshot::Sender<Result<DerivedKey, crate::service::VaultServiceError>>)]
|
||||||
#[wrap(DeriveEncryptionKey)]
|
#[wrap(DeriveEncryptionKey)]
|
||||||
DeriveEncryptionKey {
|
DeriveEncryptionKey {
|
||||||
/// SLIP-0010 derivation path for the encryption key.
|
/// SLIP-0010 derivation path for the encryption key.
|
||||||
@@ -140,7 +140,7 @@ pub enum SecretProtocol {
|
|||||||
///
|
///
|
||||||
/// The default Ethereum path is `m/44'/60'/0'/0/0`.
|
/// The default Ethereum path is `m/44'/60'/0'/0/0`.
|
||||||
/// Returns a `DerivedKey` with `KeyType::Secp256k1`.
|
/// Returns a `DerivedKey` with `KeyType::Secp256k1`.
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<DerivedKey, crate::service::SecretServiceError>>)]
|
#[rpc(tx = irpc::channel::oneshot::Sender<Result<DerivedKey, crate::service::VaultServiceError>>)]
|
||||||
#[wrap(DeriveEthereumKey)]
|
#[wrap(DeriveEthereumKey)]
|
||||||
DeriveEthereumKey {
|
DeriveEthereumKey {
|
||||||
/// BIP-0032 derivation path (e.g., "m/44'/60'/0'/0/0").
|
/// BIP-0032 derivation path (e.g., "m/44'/60'/0'/0/0").
|
||||||
@@ -151,7 +151,7 @@ pub enum SecretProtocol {
|
|||||||
///
|
///
|
||||||
/// Path format: `m/74'/1'/0'/{hash}'` (SLIP-0010 hardened notation).
|
/// Path format: `m/74'/1'/0'/{hash}'` (SLIP-0010 hardened notation).
|
||||||
/// The `length` parameter controls the output length.
|
/// The `length` parameter controls the output length.
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<Vec<u8>, crate::service::SecretServiceError>>)]
|
#[rpc(tx = irpc::channel::oneshot::Sender<Result<Vec<u8>, crate::service::VaultServiceError>>)]
|
||||||
#[wrap(DerivePassword)]
|
#[wrap(DerivePassword)]
|
||||||
DerivePassword {
|
DerivePassword {
|
||||||
/// SLIP-0010 derivation path for the password.
|
/// SLIP-0010 derivation path for the password.
|
||||||
@@ -164,7 +164,7 @@ pub enum SecretProtocol {
|
|||||||
///
|
///
|
||||||
/// The key is derived at the path `m/74'/2'/0'/0'` with the given version.
|
/// The key is derived at the path `m/74'/2'/0'/0'` with the given version.
|
||||||
/// Returns an `EncryptedData` blob suitable for storage.
|
/// Returns an `EncryptedData` blob suitable for storage.
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<EncryptedData, crate::service::SecretServiceError>>)]
|
#[rpc(tx = irpc::channel::oneshot::Sender<Result<EncryptedData, crate::service::VaultServiceError>>)]
|
||||||
#[wrap(Encrypt)]
|
#[wrap(Encrypt)]
|
||||||
Encrypt {
|
Encrypt {
|
||||||
/// The plaintext string to encrypt.
|
/// The plaintext string to encrypt.
|
||||||
@@ -176,7 +176,7 @@ pub enum SecretProtocol {
|
|||||||
/// Decrypt an `EncryptedData` blob back to plaintext.
|
/// Decrypt an `EncryptedData` blob back to plaintext.
|
||||||
///
|
///
|
||||||
/// The key is derived from the seed at the path indicated by the key version.
|
/// 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::SecretServiceError>>)]
|
#[rpc(tx = irpc::channel::oneshot::Sender<Result<String, crate::service::VaultServiceError>>)]
|
||||||
#[wrap(Decrypt)]
|
#[wrap(Decrypt)]
|
||||||
Decrypt {
|
Decrypt {
|
||||||
/// The encrypted data blob to decrypt.
|
/// The encrypted data blob to decrypt.
|
||||||
@@ -188,7 +188,7 @@ pub enum SecretProtocol {
|
|||||||
/// After locking, no derive/encrypt/decrypt operations are possible
|
/// After locking, no derive/encrypt/decrypt operations are possible
|
||||||
/// until `Unlock` is called again. Calls `zeroize()` on all sensitive
|
/// until `Unlock` is called again. Calls `zeroize()` on all sensitive
|
||||||
/// material (ADR-038).
|
/// material (ADR-038).
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<(), crate::service::SecretServiceError>>)]
|
#[rpc(tx = irpc::channel::oneshot::Sender<Result<(), crate::service::VaultServiceError>>)]
|
||||||
#[wrap(Lock)]
|
#[wrap(Lock)]
|
||||||
Lock,
|
Lock,
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ pub enum SecretProtocol {
|
|||||||
/// The mnemonic is the space-separated BIP39 word list. The passphrase is
|
/// The mnemonic is the space-separated BIP39 word list. The passphrase is
|
||||||
/// the optional BIP39 password extension (the "25th word"). After unlocking,
|
/// the optional BIP39 password extension (the "25th word"). After unlocking,
|
||||||
/// derive and encrypt/decrypt operations are available.
|
/// derive and encrypt/decrypt operations are available.
|
||||||
#[rpc(tx = irpc::channel::oneshot::Sender<Result<(), crate::service::SecretServiceError>>)]
|
#[rpc(tx = irpc::channel::oneshot::Sender<Result<(), crate::service::VaultServiceError>>)]
|
||||||
#[wrap(Unlock)]
|
#[wrap(Unlock)]
|
||||||
Unlock {
|
Unlock {
|
||||||
/// The BIP39 mnemonic phrase (space-separated word list).
|
/// The BIP39 mnemonic phrase (space-separated word list).
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! SecretService implementation with Unlock/Lock lifecycle.
|
//! VaultService implementation with Unlock/Lock lifecycle.
|
||||||
//!
|
//!
|
||||||
//! The `SecretService` is the primary runtime interface for key management.
|
//! The `VaultService` is the primary runtime interface for key management.
|
||||||
//! It holds the master seed in `Zeroize`-protected memory and provides methods
|
//! It holds the master seed in `Zeroize`-protected memory and provides methods
|
||||||
//! for the Unlock/Lock lifecycle, key derivation, and encryption/decryption.
|
//! for the Unlock/Lock lifecycle, key derivation, and encryption/decryption.
|
||||||
//!
|
//!
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
//! → cache empty (keys derived on demand)
|
//! → cache empty (keys derived on demand)
|
||||||
//!
|
//!
|
||||||
//! DeriveEd25519/DeriveEncryptionKey/Encrypt/Decrypt
|
//! DeriveEd25519/DeriveEncryptionKey/Encrypt/Decrypt
|
||||||
//! → require unlocked state (ServiceLocked error if locked)
|
//! → require unlocked state (VaultLocked error if locked)
|
||||||
//! → derive key, return result
|
//! → derive key, return result
|
||||||
//! → optionally cache derived key
|
//! → optionally cache derived key
|
||||||
//!
|
//!
|
||||||
@@ -22,24 +22,24 @@
|
|||||||
//! → zeroize all cached derived keys
|
//! → zeroize all cached derived keys
|
||||||
//! → zeroize seed
|
//! → zeroize seed
|
||||||
//! → drop all sensitive material
|
//! → drop all sensitive material
|
||||||
//! → service returns to locked state
|
//! → vault returns to locked state
|
||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Dispatch Paths
|
//! # Dispatch Paths
|
||||||
//!
|
//!
|
||||||
//! There are two ways to interact with the secret service:
|
//! There are two ways to interact with the vault:
|
||||||
//!
|
//!
|
||||||
//! 1. **Local (in-process)**: `SecretServiceHandle` wraps `SecretServiceInner`
|
//! 1. **Local (in-process)**: `VaultServiceHandle` wraps `VaultServiceInner`
|
||||||
//! behind `Arc<RwLock<>>` and provides direct method calls without serialization.
|
//! behind `Arc<RwLock<>>` and provides direct method calls without serialization.
|
||||||
//! 2. **Remote (in-cluster)**: `SecretServiceActor` processes `SecretMessage`
|
//! 2. **Remote (in-cluster)**: `VaultServiceActor` processes `VaultMessage`
|
||||||
//! variants from an mpsc channel and dispatches to the handle methods.
|
//! variants from an mpsc channel and dispatches to the handle methods.
|
||||||
//!
|
//!
|
||||||
//! # Assembly
|
//! # Assembly
|
||||||
//!
|
//!
|
||||||
//! The `SecretService` is assembled by the CLI binary or NAPI layer. Per ADR-027,
|
//! The `VaultService` is assembled by the CLI binary. The CLI unlocks the vault
|
||||||
//! alknet-core never sees the secret service directly — it is wired through the
|
//! at startup and injects derived/decrypted material into operation contexts.
|
||||||
//! `OperationEnv` dispatch mechanism. For minimal deployments, no secret service
|
//! No handler crate accesses the vault directly — they receive keys through
|
||||||
//! is available (the `SecretStoreCredentialProvider` returns `None`).
|
//! their operation context or via the call protocol.
|
||||||
|
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
@@ -54,21 +54,21 @@ use crate::encryption::{self, EncryptedData, EncryptionKey};
|
|||||||
use crate::mnemonic::{Language, Mnemonic, Seed};
|
use crate::mnemonic::{Language, Mnemonic, Seed};
|
||||||
use crate::protocol::{
|
use crate::protocol::{
|
||||||
Decrypt, DeriveEd25519, DeriveEncryptionKey, DeriveEthereumKey, DerivePassword, Encrypt,
|
Decrypt, DeriveEd25519, DeriveEncryptionKey, DeriveEthereumKey, DerivePassword, Encrypt,
|
||||||
SecretMessage, SecretProtocol, Unlock,
|
VaultMessage, VaultProtocol, Unlock,
|
||||||
};
|
};
|
||||||
use crate::protocol::{DerivedKey, KeyType};
|
use crate::protocol::{DerivedKey, KeyType};
|
||||||
|
|
||||||
/// Handle to a running SecretService for local (in-process) use.
|
/// Handle to a running VaultService for local (in-process) use.
|
||||||
///
|
///
|
||||||
/// This is the primary API for local secret operations. It wraps the
|
/// This is the primary API for local secret operations. It wraps the
|
||||||
/// service state in an `Arc<RwLock<>>` for thread-safe access.
|
/// service state in an `Arc<RwLock<>>` for thread-safe access.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct SecretServiceHandle {
|
pub struct VaultServiceHandle {
|
||||||
inner: Arc<RwLock<SecretServiceInner>>,
|
inner: Arc<RwLock<VaultServiceInner>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Internal state of the secret service.
|
/// Internal state of the secret service.
|
||||||
struct SecretServiceInner {
|
struct VaultServiceInner {
|
||||||
/// The mnemonic phrase, if unlocked. None if locked.
|
/// The mnemonic phrase, if unlocked. None if locked.
|
||||||
mnemonic: Option<Mnemonic>,
|
mnemonic: Option<Mnemonic>,
|
||||||
/// The master seed, if unlocked. None if locked.
|
/// The master seed, if unlocked. None if locked.
|
||||||
@@ -79,12 +79,12 @@ struct SecretServiceInner {
|
|||||||
cache: KeyCache,
|
cache: KeyCache,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors that can occur during secret service operations.
|
/// Errors that can occur during vault operations.
|
||||||
#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
|
#[derive(Debug, thiserror::Error, Serialize, Deserialize)]
|
||||||
pub enum SecretServiceError {
|
pub enum VaultServiceError {
|
||||||
#[error("service is locked; call Unlock first")]
|
#[error("vault is locked; call Unlock first")]
|
||||||
ServiceLocked,
|
VaultLocked,
|
||||||
#[error("service is already unlocked")]
|
#[error("vault is already unlocked")]
|
||||||
AlreadyUnlocked,
|
AlreadyUnlocked,
|
||||||
#[error("mnemonic error: {0}")]
|
#[error("mnemonic error: {0}")]
|
||||||
Mnemonic(String),
|
Mnemonic(String),
|
||||||
@@ -98,34 +98,34 @@ pub enum SecretServiceError {
|
|||||||
UnsupportedKeyType,
|
UnsupportedKeyType,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<crate::mnemonic::MnemonicError> for SecretServiceError {
|
impl From<crate::mnemonic::MnemonicError> for VaultServiceError {
|
||||||
fn from(e: crate::mnemonic::MnemonicError) -> Self {
|
fn from(e: crate::mnemonic::MnemonicError) -> Self {
|
||||||
SecretServiceError::Mnemonic(e.to_string())
|
VaultServiceError::Mnemonic(e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DerivationError> for SecretServiceError {
|
impl From<DerivationError> for VaultServiceError {
|
||||||
fn from(e: DerivationError) -> Self {
|
fn from(e: DerivationError) -> Self {
|
||||||
SecretServiceError::Derivation(e.to_string())
|
VaultServiceError::Derivation(e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<encryption::EncryptionError> for SecretServiceError {
|
impl From<encryption::EncryptionError> for VaultServiceError {
|
||||||
fn from(e: encryption::EncryptionError) -> Self {
|
fn from(e: encryption::EncryptionError) -> Self {
|
||||||
SecretServiceError::Encryption(e.to_string())
|
VaultServiceError::Encryption(e.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SecretServiceHandle {
|
impl VaultServiceHandle {
|
||||||
/// Create a new SecretServiceHandle in the locked state with default cache config.
|
/// Create a new VaultServiceHandle in the locked state with default cache config.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self::with_cache_config(CacheConfig::default())
|
Self::with_cache_config(CacheConfig::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new SecretServiceHandle with the given cache configuration.
|
/// Create a new VaultServiceHandle with the given cache configuration.
|
||||||
pub fn with_cache_config(config: CacheConfig) -> Self {
|
pub fn with_cache_config(config: CacheConfig) -> Self {
|
||||||
Self {
|
Self {
|
||||||
inner: Arc::new(RwLock::new(SecretServiceInner {
|
inner: Arc::new(RwLock::new(VaultServiceInner {
|
||||||
mnemonic: None,
|
mnemonic: None,
|
||||||
seed: None,
|
seed: None,
|
||||||
unlocked: false,
|
unlocked: false,
|
||||||
@@ -138,10 +138,10 @@ impl SecretServiceHandle {
|
|||||||
///
|
///
|
||||||
/// 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<(), SecretServiceError> {
|
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();
|
||||||
if inner.unlocked {
|
if inner.unlocked {
|
||||||
return Err(SecretServiceError::AlreadyUnlocked);
|
return Err(VaultServiceError::AlreadyUnlocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mnemonic = Mnemonic::from_phrase(phrase, Language::English)?;
|
let mnemonic = Mnemonic::from_phrase(phrase, Language::English)?;
|
||||||
@@ -157,10 +157,10 @@ impl SecretServiceHandle {
|
|||||||
///
|
///
|
||||||
/// 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<String, SecretServiceError> {
|
pub fn unlock_new(&self, word_count: usize) -> Result<String, VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap();
|
||||||
if inner.unlocked {
|
if inner.unlocked {
|
||||||
return Err(SecretServiceError::AlreadyUnlocked);
|
return Err(VaultServiceError::AlreadyUnlocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mnemonic = Mnemonic::generate(word_count)?;
|
let mnemonic = Mnemonic::generate(word_count)?;
|
||||||
@@ -192,10 +192,10 @@ impl SecretServiceHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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, SecretServiceError> {
|
pub fn derive_ed25519(&self, path: &str) -> Result<DerivedKey, VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap();
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(SecretServiceError::ServiceLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(cached) = inner.cache.get(path) {
|
if let Some(cached) = inner.cache.get(path) {
|
||||||
@@ -209,7 +209,7 @@ impl SecretServiceHandle {
|
|||||||
let seed = inner
|
let seed = inner
|
||||||
.seed
|
.seed
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(SecretServiceError::ServiceLocked)?;
|
.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();
|
||||||
@@ -223,10 +223,10 @@ impl SecretServiceHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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, SecretServiceError> {
|
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();
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(SecretServiceError::ServiceLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(cached) = inner.cache.get(path) {
|
if let Some(cached) = inner.cache.get(path) {
|
||||||
@@ -240,7 +240,7 @@ impl SecretServiceHandle {
|
|||||||
let seed = inner
|
let seed = inner
|
||||||
.seed
|
.seed
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(SecretServiceError::ServiceLocked)?;
|
.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();
|
||||||
@@ -258,12 +258,12 @@ impl SecretServiceHandle {
|
|||||||
/// Uses BIP-0032 derivation (HMAC-SHA512 with "Bitcoin seed") when the
|
/// Uses BIP-0032 derivation (HMAC-SHA512 with "Bitcoin seed") when the
|
||||||
/// `secp256k1` feature is enabled. Returns `UnsupportedKeyType` when the
|
/// `secp256k1` feature is enabled. Returns `UnsupportedKeyType` when the
|
||||||
/// feature is disabled.
|
/// feature is disabled.
|
||||||
pub fn derive_ethereum_key(&self, path: &str) -> Result<DerivedKey, SecretServiceError> {
|
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();
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(SecretServiceError::ServiceLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(cached) = inner.cache.get(path) {
|
if let Some(cached) = inner.cache.get(path) {
|
||||||
@@ -277,7 +277,7 @@ impl SecretServiceHandle {
|
|||||||
let seed = inner
|
let seed = inner
|
||||||
.seed
|
.seed
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(SecretServiceError::ServiceLocked)?;
|
.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();
|
||||||
@@ -295,7 +295,7 @@ impl SecretServiceHandle {
|
|||||||
#[cfg(not(feature = "secp256k1"))]
|
#[cfg(not(feature = "secp256k1"))]
|
||||||
{
|
{
|
||||||
let _ = path;
|
let _ = path;
|
||||||
Err(SecretServiceError::UnsupportedKeyType)
|
Err(VaultServiceError::UnsupportedKeyType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -303,15 +303,15 @@ impl SecretServiceHandle {
|
|||||||
&self,
|
&self,
|
||||||
path: &str,
|
path: &str,
|
||||||
length: usize,
|
length: usize,
|
||||||
) -> Result<Vec<u8>, SecretServiceError> {
|
) -> Result<Vec<u8>, VaultServiceError> {
|
||||||
let inner = self.inner.read().unwrap();
|
let inner = self.inner.read().unwrap();
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(SecretServiceError::ServiceLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
let seed = inner
|
let seed = inner
|
||||||
.seed
|
.seed
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(SecretServiceError::ServiceLocked)?;
|
.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();
|
||||||
@@ -324,7 +324,7 @@ impl SecretServiceHandle {
|
|||||||
&self,
|
&self,
|
||||||
path: &str,
|
path: &str,
|
||||||
length: usize,
|
length: usize,
|
||||||
) -> Result<String, SecretServiceError> {
|
) -> Result<String, VaultServiceError> {
|
||||||
let bytes = self.derive_password(path, length)?;
|
let bytes = self.derive_password(path, length)?;
|
||||||
Ok(URL_SAFE_NO_PAD.encode(&bytes))
|
Ok(URL_SAFE_NO_PAD.encode(&bytes))
|
||||||
}
|
}
|
||||||
@@ -336,10 +336,10 @@ impl SecretServiceHandle {
|
|||||||
&self,
|
&self,
|
||||||
plaintext: &str,
|
plaintext: &str,
|
||||||
key_version: u32,
|
key_version: u32,
|
||||||
) -> Result<EncryptedData, SecretServiceError> {
|
) -> Result<EncryptedData, VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap();
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(SecretServiceError::ServiceLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
let private_key = if let Some(cached) = inner.cache.get(PATHS::ENCRYPTION) {
|
let private_key = if let Some(cached) = inner.cache.get(PATHS::ENCRYPTION) {
|
||||||
@@ -348,7 +348,7 @@ impl SecretServiceHandle {
|
|||||||
let seed = inner
|
let seed = inner
|
||||||
.seed
|
.seed
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(SecretServiceError::ServiceLocked)?;
|
.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();
|
||||||
@@ -363,10 +363,10 @@ impl SecretServiceHandle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 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, SecretServiceError> {
|
pub fn decrypt(&self, encrypted: &EncryptedData) -> Result<String, VaultServiceError> {
|
||||||
let mut inner = self.inner.write().unwrap();
|
let mut inner = self.inner.write().unwrap();
|
||||||
if !inner.unlocked {
|
if !inner.unlocked {
|
||||||
return Err(SecretServiceError::ServiceLocked);
|
return Err(VaultServiceError::VaultLocked);
|
||||||
}
|
}
|
||||||
|
|
||||||
let private_key = if let Some(cached) = inner.cache.get(PATHS::ENCRYPTION) {
|
let private_key = if let Some(cached) = inner.cache.get(PATHS::ENCRYPTION) {
|
||||||
@@ -375,7 +375,7 @@ impl SecretServiceHandle {
|
|||||||
let seed = inner
|
let seed = inner
|
||||||
.seed
|
.seed
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.ok_or(SecretServiceError::ServiceLocked)?;
|
.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();
|
||||||
@@ -390,42 +390,42 @@ impl SecretServiceHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SecretServiceHandle {
|
impl Default for VaultServiceHandle {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The SecretService manages the lifecycle of the master seed and provides
|
/// The VaultService manages the lifecycle of the master seed and provides
|
||||||
/// secret operations. This is the type used by the irpc service handler.
|
/// secret operations. This is the type used by the irpc service handler.
|
||||||
///
|
///
|
||||||
/// For local (in-process) use, prefer `SecretServiceHandle` which wraps
|
/// For local (in-process) use, prefer `VaultServiceHandle` which wraps
|
||||||
/// this in thread-safe locks.
|
/// this in thread-safe locks.
|
||||||
pub struct SecretService {
|
pub struct VaultService {
|
||||||
handle: SecretServiceHandle,
|
handle: VaultServiceHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SecretService {
|
impl VaultService {
|
||||||
/// Create a new SecretService in the locked state.
|
/// Create a new VaultService in the locked state.
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
handle: SecretServiceHandle::new(),
|
handle: VaultServiceHandle::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a handle for local (in-process) use.
|
/// Get a handle for local (in-process) use.
|
||||||
pub fn handle(&self) -> &SecretServiceHandle {
|
pub fn handle(&self) -> &VaultServiceHandle {
|
||||||
&self.handle
|
&self.handle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SecretService {
|
impl Default for VaultService {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Actor that processes `SecretMessage` variants and dispatches to `SecretServiceHandle`.
|
/// Actor that processes `VaultMessage` variants and dispatches to `VaultServiceHandle`.
|
||||||
///
|
///
|
||||||
/// The actor runs as a `tokio::task`, receives messages from an mpsc channel,
|
/// The actor runs as a `tokio::task`, receives messages from an mpsc channel,
|
||||||
/// dispatches to the handle methods, and sends responses through oneshot channels.
|
/// dispatches to the handle methods, and sends responses through oneshot channels.
|
||||||
@@ -433,40 +433,40 @@ impl Default for SecretService {
|
|||||||
/// # Usage
|
/// # Usage
|
||||||
///
|
///
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// let handle = SecretServiceHandle::new();
|
/// let handle = VaultServiceHandle::new();
|
||||||
/// let (client, actor) = SecretServiceActor::spawn(handle);
|
/// let (client, actor) = VaultServiceActor::spawn(handle);
|
||||||
/// tokio::task::spawn(actor.run(rx));
|
/// tokio::task::spawn(actor.run(rx));
|
||||||
/// // Use client to send messages
|
/// // Use client to send messages
|
||||||
/// ```
|
/// ```
|
||||||
pub struct SecretServiceActor {
|
pub struct VaultServiceActor {
|
||||||
handle: SecretServiceHandle,
|
handle: VaultServiceHandle,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SecretServiceActor {
|
impl VaultServiceActor {
|
||||||
/// Create a new actor wrapping the given handle.
|
/// Create a new actor wrapping the given handle.
|
||||||
pub fn new(handle: SecretServiceHandle) -> Self {
|
pub fn new(handle: VaultServiceHandle) -> Self {
|
||||||
Self { handle }
|
Self { handle }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the actor message loop, processing `SecretMessage` variants.
|
/// Run the actor message loop, processing `VaultMessage` variants.
|
||||||
///
|
///
|
||||||
/// This method runs until the receiver channel is closed. Each message
|
/// This method runs until the receiver channel is closed. Each message
|
||||||
/// variant is dispatched to the corresponding `SecretServiceHandle` method
|
/// variant is dispatched to the corresponding `VaultServiceHandle` method
|
||||||
/// and the response is sent through the oneshot channel embedded in the message.
|
/// 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<SecretMessage>) {
|
pub async fn run(mut self, mut rx: tokio::sync::mpsc::Receiver<VaultMessage>) {
|
||||||
while let Some(msg) = rx.recv().await {
|
while let Some(msg) = rx.recv().await {
|
||||||
self.handle_message(msg);
|
self.handle_message(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Spawn the actor as a `tokio::task` and return a `Client<SecretProtocol>` for sending messages.
|
/// 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 actor runs on a tokio task and processes messages from the mpsc channel.
|
||||||
/// The returned `Client<SecretProtocol>` can be used to send `SecretMessage` variants
|
/// The returned `Client<VaultProtocol>` can be used to send `VaultMessage` variants
|
||||||
/// to the actor.
|
/// to the actor.
|
||||||
pub fn spawn(
|
pub fn spawn(
|
||||||
handle: SecretServiceHandle,
|
handle: VaultServiceHandle,
|
||||||
) -> (irpc::Client<SecretProtocol>, SecretServiceActor) {
|
) -> (irpc::Client<VaultProtocol>, VaultServiceActor) {
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
||||||
let client = irpc::Client::local(tx);
|
let client = irpc::Client::local(tx);
|
||||||
let actor = Self::new(handle.clone());
|
let actor = Self::new(handle.clone());
|
||||||
@@ -474,10 +474,10 @@ impl SecretServiceActor {
|
|||||||
(client, Self::new(handle))
|
(client, Self::new(handle))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a single `SecretMessage` by dispatching to the appropriate handle method.
|
/// Handle a single `VaultMessage` by dispatching to the appropriate handle method.
|
||||||
fn handle_message(&mut self, msg: SecretMessage) {
|
fn handle_message(&mut self, msg: VaultMessage) {
|
||||||
match msg {
|
match msg {
|
||||||
SecretMessage::DeriveEd25519(msg) => {
|
VaultMessage::DeriveEd25519(msg) => {
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
let WithChannels { inner, tx, .. } = msg;
|
||||||
let DeriveEd25519 { path } = inner;
|
let DeriveEd25519 { path } = inner;
|
||||||
let result = self.handle.derive_ed25519(&path);
|
let result = self.handle.derive_ed25519(&path);
|
||||||
@@ -485,7 +485,7 @@ impl SecretServiceActor {
|
|||||||
let _ = tx.send(result).await;
|
let _ = tx.send(result).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
SecretMessage::DeriveEncryptionKey(msg) => {
|
VaultMessage::DeriveEncryptionKey(msg) => {
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
let WithChannels { inner, tx, .. } = msg;
|
||||||
let DeriveEncryptionKey { path } = inner;
|
let DeriveEncryptionKey { path } = inner;
|
||||||
let result = self.handle.derive_encryption_key(&path);
|
let result = self.handle.derive_encryption_key(&path);
|
||||||
@@ -493,7 +493,7 @@ impl SecretServiceActor {
|
|||||||
let _ = tx.send(result).await;
|
let _ = tx.send(result).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
SecretMessage::DeriveEthereumKey(msg) => {
|
VaultMessage::DeriveEthereumKey(msg) => {
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
let WithChannels { inner, tx, .. } = msg;
|
||||||
let DeriveEthereumKey { path } = inner;
|
let DeriveEthereumKey { path } = inner;
|
||||||
let result = self.handle.derive_ethereum_key(&path);
|
let result = self.handle.derive_ethereum_key(&path);
|
||||||
@@ -501,7 +501,7 @@ impl SecretServiceActor {
|
|||||||
let _ = tx.send(result).await;
|
let _ = tx.send(result).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
SecretMessage::DerivePassword(msg) => {
|
VaultMessage::DerivePassword(msg) => {
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
let WithChannels { inner, tx, .. } = msg;
|
||||||
let DerivePassword { path, length } = inner;
|
let DerivePassword { path, length } = inner;
|
||||||
let result = self.handle.derive_password(&path, length);
|
let result = self.handle.derive_password(&path, length);
|
||||||
@@ -509,7 +509,7 @@ impl SecretServiceActor {
|
|||||||
let _ = tx.send(result).await;
|
let _ = tx.send(result).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
SecretMessage::Encrypt(msg) => {
|
VaultMessage::Encrypt(msg) => {
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
let WithChannels { inner, tx, .. } = msg;
|
||||||
let Encrypt {
|
let Encrypt {
|
||||||
plaintext,
|
plaintext,
|
||||||
@@ -520,7 +520,7 @@ impl SecretServiceActor {
|
|||||||
let _ = tx.send(result).await;
|
let _ = tx.send(result).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
SecretMessage::Decrypt(msg) => {
|
VaultMessage::Decrypt(msg) => {
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
let WithChannels { inner, tx, .. } = msg;
|
||||||
let Decrypt { encrypted } = inner;
|
let Decrypt { encrypted } = inner;
|
||||||
let result = self.handle.decrypt(&encrypted);
|
let result = self.handle.decrypt(&encrypted);
|
||||||
@@ -528,14 +528,14 @@ impl SecretServiceActor {
|
|||||||
let _ = tx.send(result).await;
|
let _ = tx.send(result).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
SecretMessage::Lock(msg) => {
|
VaultMessage::Lock(msg) => {
|
||||||
let WithChannels { inner: _, tx, .. } = msg;
|
let WithChannels { inner: _, tx, .. } = msg;
|
||||||
self.handle.lock();
|
self.handle.lock();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let _ = tx.send(Ok(())).await;
|
let _ = tx.send(Ok(())).await;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
SecretMessage::Unlock(msg) => {
|
VaultMessage::Unlock(msg) => {
|
||||||
let WithChannels { inner, tx, .. } = msg;
|
let WithChannels { inner, tx, .. } = msg;
|
||||||
let Unlock {
|
let Unlock {
|
||||||
mnemonic,
|
mnemonic,
|
||||||
@@ -559,13 +559,13 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_service_starts_locked() {
|
fn test_service_starts_locked() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
assert!(!service.is_unlocked());
|
assert!(!service.is_unlocked());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_unlock_new_generates_mnemonic() {
|
fn test_unlock_new_generates_mnemonic() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
let phrase = service.unlock_new(24).unwrap();
|
let phrase = service.unlock_new(24).unwrap();
|
||||||
assert!(!phrase.is_empty());
|
assert!(!phrase.is_empty());
|
||||||
assert!(service.is_unlocked());
|
assert!(service.is_unlocked());
|
||||||
@@ -573,7 +573,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lock_purges_state() {
|
fn test_lock_purges_state() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
assert!(service.is_unlocked());
|
assert!(service.is_unlocked());
|
||||||
|
|
||||||
@@ -583,21 +583,21 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_derive_on_locked_fails() {
|
fn test_derive_on_locked_fails() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
let result = service.derive_ed25519(PATHS::IDENTITY);
|
let result = service.derive_ed25519(PATHS::IDENTITY);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypt_on_locked_fails() {
|
fn test_encrypt_on_locked_fails() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
let result = service.encrypt("secret", 1);
|
let result = service.encrypt("secret", 1);
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_full_lifecycle() {
|
fn test_full_lifecycle() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
|
|
||||||
assert!(!service.is_unlocked());
|
assert!(!service.is_unlocked());
|
||||||
|
|
||||||
@@ -617,7 +617,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_unlock_with_known_phrase() {
|
fn test_unlock_with_known_phrase() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
|
|
||||||
let phrase = service.unlock_new(24).unwrap();
|
let phrase = service.unlock_new(24).unwrap();
|
||||||
service.lock();
|
service.lock();
|
||||||
@@ -628,7 +628,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_double_unlock_fails() {
|
fn test_double_unlock_fails() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let result = service.unlock_new(12);
|
let result = service.unlock_new(12);
|
||||||
@@ -637,7 +637,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypt_decrypt_lifecycle() {
|
fn test_encrypt_decrypt_lifecycle() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let plaintext = "my-api-key-12345";
|
let plaintext = "my-api-key-12345";
|
||||||
@@ -651,7 +651,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_derive_password_deterministic() {
|
fn test_derive_password_deterministic() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let path = "m/74'/1'/0'/12345'";
|
let path = "m/74'/1'/0'/12345'";
|
||||||
@@ -662,7 +662,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_derive_password_different_paths() {
|
fn test_derive_password_different_paths() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let pw_a = service.derive_password("m/74'/1'/0'/100'", 16).unwrap();
|
let pw_a = service.derive_password("m/74'/1'/0'/100'", 16).unwrap();
|
||||||
@@ -675,7 +675,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_derive_password_length_truncation() {
|
fn test_derive_password_length_truncation() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let path = "m/74'/1'/0'/999'";
|
let path = "m/74'/1'/0'/999'";
|
||||||
@@ -693,14 +693,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_derive_password_locked_error() {
|
fn test_derive_password_locked_error() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
let result = service.derive_password("m/74'/1'/0'/1'", 16);
|
let result = service.derive_password("m/74'/1'/0'/1'", 16);
|
||||||
assert!(matches!(result, Err(SecretServiceError::ServiceLocked)));
|
assert!(matches!(result, Err(VaultServiceError::VaultLocked)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_derive_password_string_base64url() {
|
fn test_derive_password_string_base64url() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let path = "m/74'/1'/0'/42'";
|
let path = "m/74'/1'/0'/42'";
|
||||||
@@ -722,7 +722,7 @@ mod tests {
|
|||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_derive_ethereum_key_bip32() {
|
fn test_derive_ethereum_key_bip32() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let key = service.derive_ethereum_key(PATHS::ETHEREUM).unwrap();
|
let key = service.derive_ethereum_key(PATHS::ETHEREUM).unwrap();
|
||||||
@@ -734,7 +734,7 @@ mod tests {
|
|||||||
#[cfg(feature = "secp256k1")]
|
#[cfg(feature = "secp256k1")]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ethereum_key_differs_from_ed25519() {
|
fn test_ethereum_key_differs_from_ed25519() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let eth_key = service.derive_ethereum_key(PATHS::ETHEREUM).unwrap();
|
let eth_key = service.derive_ethereum_key(PATHS::ETHEREUM).unwrap();
|
||||||
@@ -746,19 +746,19 @@ mod tests {
|
|||||||
#[cfg(not(feature = "secp256k1"))]
|
#[cfg(not(feature = "secp256k1"))]
|
||||||
#[test]
|
#[test]
|
||||||
fn test_derive_ethereum_key_unsupported_without_feature() {
|
fn test_derive_ethereum_key_unsupported_without_feature() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
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,
|
result,
|
||||||
Err(SecretServiceError::UnsupportedKeyType)
|
Err(VaultServiceError::UnsupportedKeyType)
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cache_hit_avoids_re_derivation() {
|
fn test_cache_hit_avoids_re_derivation() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let key1 = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
let key1 = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
||||||
@@ -773,7 +773,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cache_miss_derives_and_caches() {
|
fn test_cache_miss_derives_and_caches() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
assert_eq!(service.inner.read().unwrap().cache.len(), 0);
|
assert_eq!(service.inner.read().unwrap().cache.len(), 0);
|
||||||
@@ -786,7 +786,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_expired_entry_evicted_on_access() {
|
fn test_expired_entry_evicted_on_access() {
|
||||||
let config = crate::cache::CacheConfig::new(std::time::Duration::from_millis(5), 64);
|
let config = crate::cache::CacheConfig::new(std::time::Duration::from_millis(5), 64);
|
||||||
let service = SecretServiceHandle::with_cache_config(config);
|
let service = VaultServiceHandle::with_cache_config(config);
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let key1 = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
let key1 = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
||||||
@@ -802,7 +802,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_lru_eviction_when_over_max_entries() {
|
fn test_lru_eviction_when_over_max_entries() {
|
||||||
let config = crate::cache::CacheConfig::new(std::time::Duration::from_secs(3600), 2);
|
let config = crate::cache::CacheConfig::new(std::time::Duration::from_secs(3600), 2);
|
||||||
let service = SecretServiceHandle::with_cache_config(config);
|
let service = VaultServiceHandle::with_cache_config(config);
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
||||||
@@ -820,7 +820,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lock_clears_all_cache_entries() {
|
fn test_lock_clears_all_cache_entries() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
||||||
@@ -834,7 +834,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypt_decrypt_uses_cached_encryption_key() {
|
fn test_encrypt_decrypt_uses_cached_encryption_key() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let plaintext = "cached-encryption-test";
|
let plaintext = "cached-encryption-test";
|
||||||
@@ -849,13 +849,13 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_actor_unlock_responds_successfully() {
|
async fn test_actor_unlock_responds_successfully() {
|
||||||
let handle = SecretServiceHandle::new();
|
let handle = VaultServiceHandle::new();
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
||||||
let actor = SecretServiceActor::new(handle);
|
let actor = VaultServiceActor::new(handle);
|
||||||
tokio::task::spawn(actor.run(rx));
|
tokio::task::spawn(actor.run(rx));
|
||||||
|
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
let msg = SecretMessage::Unlock(WithChannels::from((
|
let msg = VaultMessage::Unlock(WithChannels::from((
|
||||||
Unlock {
|
Unlock {
|
||||||
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
|
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about".to_string(),
|
||||||
passphrase: None,
|
passphrase: None,
|
||||||
@@ -870,14 +870,14 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_actor_derive_ed25519_returns_key() {
|
async fn test_actor_derive_ed25519_returns_key() {
|
||||||
let handle = SecretServiceHandle::new();
|
let handle = VaultServiceHandle::new();
|
||||||
handle.unlock_new(24).unwrap();
|
handle.unlock_new(24).unwrap();
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
||||||
let actor = SecretServiceActor::new(handle);
|
let actor = VaultServiceActor::new(handle);
|
||||||
tokio::task::spawn(actor.run(rx));
|
tokio::task::spawn(actor.run(rx));
|
||||||
|
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
let msg = SecretMessage::DeriveEd25519(WithChannels::from((
|
let msg = VaultMessage::DeriveEd25519(WithChannels::from((
|
||||||
DeriveEd25519 {
|
DeriveEd25519 {
|
||||||
path: PATHS::IDENTITY.to_string(),
|
path: PATHS::IDENTITY.to_string(),
|
||||||
},
|
},
|
||||||
@@ -897,15 +897,15 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_actor_lock_clears_state() {
|
async fn test_actor_lock_clears_state() {
|
||||||
let handle = SecretServiceHandle::new();
|
let handle = VaultServiceHandle::new();
|
||||||
handle.unlock_new(24).unwrap();
|
handle.unlock_new(24).unwrap();
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
||||||
let actor = SecretServiceActor::new(handle.clone());
|
let actor = VaultServiceActor::new(handle.clone());
|
||||||
tokio::task::spawn(actor.run(rx));
|
tokio::task::spawn(actor.run(rx));
|
||||||
|
|
||||||
let (resp_tx, resp_rx): (oneshot::Sender<Result<(), SecretServiceError>>, _) =
|
let (resp_tx, resp_rx): (oneshot::Sender<Result<(), VaultServiceError>>, _) =
|
||||||
oneshot::channel();
|
oneshot::channel();
|
||||||
let msg = SecretMessage::Lock(WithChannels::from((Lock, resp_tx)));
|
let msg = VaultMessage::Lock(WithChannels::from((Lock, resp_tx)));
|
||||||
tx.send(msg).await.unwrap();
|
tx.send(msg).await.unwrap();
|
||||||
|
|
||||||
let result = resp_rx.await.unwrap();
|
let result = resp_rx.await.unwrap();
|
||||||
@@ -915,8 +915,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_unlock_with_passphrase_produces_different_seed() {
|
fn test_unlock_with_passphrase_produces_different_seed() {
|
||||||
let service_a = SecretServiceHandle::new();
|
let service_a = VaultServiceHandle::new();
|
||||||
let service_b = SecretServiceHandle::new();
|
let service_b = VaultServiceHandle::new();
|
||||||
|
|
||||||
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
||||||
|
|
||||||
@@ -946,15 +946,15 @@ mod tests {
|
|||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_actor_unlock_with_passphrase() {
|
async fn test_actor_unlock_with_passphrase() {
|
||||||
let handle = SecretServiceHandle::new();
|
let handle = VaultServiceHandle::new();
|
||||||
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
let (tx, rx) = tokio::sync::mpsc::channel(64);
|
||||||
let actor = SecretServiceActor::new(handle);
|
let actor = VaultServiceActor::new(handle);
|
||||||
tokio::task::spawn(actor.run(rx));
|
tokio::task::spawn(actor.run(rx));
|
||||||
|
|
||||||
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
let mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
|
||||||
|
|
||||||
let (resp_tx, resp_rx) = oneshot::channel();
|
let (resp_tx, resp_rx) = oneshot::channel();
|
||||||
let msg = SecretMessage::Unlock(WithChannels::from((
|
let msg = VaultMessage::Unlock(WithChannels::from((
|
||||||
Unlock {
|
Unlock {
|
||||||
mnemonic: mnemonic.to_string(),
|
mnemonic: mnemonic.to_string(),
|
||||||
passphrase: Some("TREZOR".to_string()),
|
passphrase: Some("TREZOR".to_string()),
|
||||||
@@ -3,33 +3,33 @@
|
|||||||
//! These tests verify that SLIP-0010 derivation produces correct results
|
//! These tests verify that SLIP-0010 derivation produces correct results
|
||||||
//! against known test vectors and that path constants produce expected key types.
|
//! against known test vectors and that path constants produce expected key types.
|
||||||
|
|
||||||
use alknet_secret::derivation::PATHS;
|
use alknet_vault::derivation::PATHS;
|
||||||
use alknet_secret::service::SecretServiceHandle;
|
use alknet_vault::service::VaultServiceHandle;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_identity_key_derivation() {
|
fn test_identity_key_derivation() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
let _phrase = service.unlock_new(24).unwrap();
|
let _phrase = service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let key = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
let key = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
||||||
assert_eq!(key.key_type, alknet_secret::protocol::KeyType::Ed25519);
|
assert_eq!(key.key_type, alknet_vault::protocol::KeyType::Ed25519);
|
||||||
assert!(!key.private_key.is_empty());
|
assert!(!key.private_key.is_empty());
|
||||||
assert!(!key.public_key.is_empty());
|
assert!(!key.public_key.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encryption_key_derivation() {
|
fn test_encryption_key_derivation() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let key = service.derive_encryption_key(PATHS::ENCRYPTION).unwrap();
|
let key = service.derive_encryption_key(PATHS::ENCRYPTION).unwrap();
|
||||||
assert_eq!(key.key_type, alknet_secret::protocol::KeyType::Aes256Gcm);
|
assert_eq!(key.key_type, alknet_vault::protocol::KeyType::Aes256Gcm);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_deterministic_derivation() {
|
fn test_deterministic_derivation() {
|
||||||
// Same seed + same path = same key
|
// Same seed + same path = same key
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
let phrase = service.unlock_new(24).unwrap();
|
let phrase = service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let key1 = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
let key1 = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
||||||
@@ -46,7 +46,7 @@ fn test_deterministic_derivation() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_different_paths_different_keys() {
|
fn test_different_paths_different_keys() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let identity_key = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
let identity_key = service.derive_ed25519(PATHS::IDENTITY).unwrap();
|
||||||
@@ -3,12 +3,12 @@
|
|||||||
//! These tests verify round-trip encryption, key version handling,
|
//! These tests verify round-trip encryption, key version handling,
|
||||||
//! and wire format compatibility.
|
//! and wire format compatibility.
|
||||||
|
|
||||||
use alknet_secret::encryption::CURRENT_KEY_VERSION;
|
use alknet_vault::encryption::CURRENT_KEY_VERSION;
|
||||||
use alknet_secret::service::SecretServiceHandle;
|
use alknet_vault::service::VaultServiceHandle;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypt_decrypt_round_trip_via_service() {
|
fn test_encrypt_decrypt_round_trip_via_service() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let plaintext = "sk-proj-abc123xyz789";
|
let plaintext = "sk-proj-abc123xyz789";
|
||||||
@@ -21,7 +21,7 @@ fn test_encrypt_decrypt_round_trip_via_service() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypt_produces_different_ciphertext_each_time() {
|
fn test_encrypt_produces_different_ciphertext_each_time() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let plaintext = "same input different ciphertexts";
|
let plaintext = "same input different ciphertexts";
|
||||||
@@ -38,7 +38,7 @@ fn test_encrypt_produces_different_ciphertext_each_time() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypted_data_serialization() {
|
fn test_encrypted_data_serialization() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let plaintext = "test serialization";
|
let plaintext = "test serialization";
|
||||||
@@ -52,7 +52,7 @@ fn test_encrypted_data_serialization() {
|
|||||||
assert!(json.contains("data"));
|
assert!(json.contains("data"));
|
||||||
|
|
||||||
// Verify round-trip through JSON
|
// Verify round-trip through JSON
|
||||||
let deserialized: alknet_secret::encryption::EncryptedData =
|
let deserialized: alknet_vault::encryption::EncryptedData =
|
||||||
serde_json::from_str(&json).unwrap();
|
serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(deserialized, encrypted);
|
assert_eq!(deserialized, encrypted);
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,21 @@
|
|||||||
//! Integration tests for the SecretService lifecycle.
|
//! Integration tests for the VaultService lifecycle.
|
||||||
//!
|
//!
|
||||||
//! These tests verify the unlock/lock lifecycle, error conditions,
|
//! These tests verify the unlock/lock lifecycle, error conditions,
|
||||||
//! and that the service correctly manages state transitions.
|
//! and that the vault correctly manages state transitions.
|
||||||
|
|
||||||
use alknet_secret::derivation::PATHS;
|
use alknet_vault::derivation::PATHS;
|
||||||
use alknet_secret::service::{SecretServiceError, SecretServiceHandle};
|
use alknet_vault::service::{VaultServiceError, VaultServiceHandle};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_full_lifecycle() {
|
fn test_full_lifecycle() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
|
|
||||||
// Starts locked
|
// Starts locked
|
||||||
assert!(!service.is_unlocked());
|
assert!(!service.is_unlocked());
|
||||||
|
|
||||||
// Can't derive while locked
|
// Can't derive while locked
|
||||||
let result = service.derive_ed25519(PATHS::IDENTITY);
|
let result = service.derive_ed25519(PATHS::IDENTITY);
|
||||||
assert!(matches!(result, Err(SecretServiceError::ServiceLocked)));
|
assert!(matches!(result, Err(VaultServiceError::VaultLocked)));
|
||||||
|
|
||||||
// Unlock
|
// Unlock
|
||||||
let phrase = service.unlock_new(24).unwrap();
|
let phrase = service.unlock_new(24).unwrap();
|
||||||
@@ -32,12 +32,12 @@ fn test_full_lifecycle() {
|
|||||||
|
|
||||||
// Can't derive again
|
// Can't derive again
|
||||||
let result = service.derive_ed25519(PATHS::IDENTITY);
|
let result = service.derive_ed25519(PATHS::IDENTITY);
|
||||||
assert!(matches!(result, Err(SecretServiceError::ServiceLocked)));
|
assert!(matches!(result, Err(VaultServiceError::VaultLocked)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_unlock_with_known_phrase() {
|
fn test_unlock_with_known_phrase() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
|
|
||||||
// Generate a phrase
|
// Generate a phrase
|
||||||
let phrase = service.unlock_new(24).unwrap();
|
let phrase = service.unlock_new(24).unwrap();
|
||||||
@@ -53,16 +53,16 @@ fn test_unlock_with_known_phrase() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_double_unlock_fails() {
|
fn test_double_unlock_fails() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let result = service.unlock_new(12);
|
let result = service.unlock_new(12);
|
||||||
assert!(matches!(result, Err(SecretServiceError::AlreadyUnlocked)));
|
assert!(matches!(result, Err(VaultServiceError::AlreadyUnlocked)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lock_when_already_locked_is_noop() {
|
fn test_lock_when_already_locked_is_noop() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
assert!(!service.is_unlocked());
|
assert!(!service.is_unlocked());
|
||||||
|
|
||||||
// Lock on already-locked service is a no-op
|
// Lock on already-locked service is a no-op
|
||||||
@@ -72,7 +72,7 @@ fn test_lock_when_already_locked_is_noop() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_encrypt_decrypt_lifecycle() {
|
fn test_encrypt_decrypt_lifecycle() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
let plaintext = "my-api-key-12345";
|
let plaintext = "my-api-key-12345";
|
||||||
@@ -83,12 +83,12 @@ fn test_encrypt_decrypt_lifecycle() {
|
|||||||
// After lock, can't decrypt
|
// After lock, can't decrypt
|
||||||
service.lock();
|
service.lock();
|
||||||
let result = service.decrypt(&encrypted);
|
let result = service.decrypt(&encrypted);
|
||||||
assert!(matches!(result, Err(SecretServiceError::ServiceLocked)));
|
assert!(matches!(result, Err(VaultServiceError::VaultLocked)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_multiple_derive_paths_succeed() {
|
fn test_multiple_derive_paths_succeed() {
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
service.unlock_new(24).unwrap();
|
service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
// All standard paths should work
|
// All standard paths should work
|
||||||
@@ -17,10 +17,10 @@
|
|||||||
//! byte-for-byte matching against SLIP-0010 raw hex, since the crate's internal
|
//! byte-for-byte matching against SLIP-0010 raw hex, since the crate's internal
|
||||||
//! representation handles clamping differently.
|
//! representation handles clamping differently.
|
||||||
|
|
||||||
use alknet_secret::derivation::{derive_path_from_seed, PATHS};
|
use alknet_vault::derivation::{derive_path_from_seed, PATHS};
|
||||||
use alknet_secret::encryption::{decrypt, encrypt, EncryptionKey, CURRENT_KEY_VERSION};
|
use alknet_vault::encryption::{decrypt, encrypt, EncryptionKey, CURRENT_KEY_VERSION};
|
||||||
use alknet_secret::mnemonic::{Language, Mnemonic};
|
use alknet_vault::mnemonic::{Language, Mnemonic};
|
||||||
use alknet_secret::protocol::KeyType;
|
use alknet_vault::protocol::KeyType;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// BIP39 Test Vectors
|
// BIP39 Test Vectors
|
||||||
@@ -291,7 +291,7 @@ fn test_aes256gcm_known_key_encrypt_decrypt() {
|
|||||||
];
|
];
|
||||||
let nonce = Nonce::from_slice(&nonce_bytes);
|
let nonce = Nonce::from_slice(&nonce_bytes);
|
||||||
|
|
||||||
let plaintext = b"hello, alknet secret service!";
|
let plaintext = b"hello, alknet vault!";
|
||||||
|
|
||||||
// Encrypt with known key and nonce
|
// Encrypt with known key and nonce
|
||||||
let ciphertext = cipher.encrypt(nonce, plaintext.as_ref()).unwrap();
|
let ciphertext = cipher.encrypt(nonce, plaintext.as_ref()).unwrap();
|
||||||
@@ -396,13 +396,13 @@ fn test_alknet_encryption_path_regression() {
|
|||||||
assert_ne!(key.private_key(), identity.private_key());
|
assert_ne!(key.private_key(), identity.private_key());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify that the SecretServiceHandle produces keys consistent with
|
/// Verify that the VaultServiceHandle produces keys consistent with
|
||||||
/// direct derivation (integration test).
|
/// direct derivation (integration test).
|
||||||
#[test]
|
#[test]
|
||||||
fn test_service_derive_matches_direct_derivation() {
|
fn test_service_derive_matches_direct_derivation() {
|
||||||
use alknet_secret::service::SecretServiceHandle;
|
use alknet_vault::service::VaultServiceHandle;
|
||||||
|
|
||||||
let service = SecretServiceHandle::new();
|
let service = VaultServiceHandle::new();
|
||||||
let phrase = service.unlock_new(24).unwrap();
|
let phrase = service.unlock_new(24).unwrap();
|
||||||
|
|
||||||
// Derive via service (which uses Mnemonic + Seed internally)
|
// Derive via service (which uses Mnemonic + Seed internally)
|
||||||
@@ -7,7 +7,7 @@ last_updated: 2026-06-16
|
|||||||
|
|
||||||
## Current State
|
## Current State
|
||||||
|
|
||||||
**Pre-implementation.** The project has completed a pivot from a three-layer model to an ALPN-as-service model. The greenfield workspace contains only `alknet-secret` (stable) and research/reference material. Foundational ADRs (001–009) are in place, including the BiStream type definition (ADR-007), secret service integration (ADR-008), and the one-way door decision framework (ADR-009). Architecture specs are ready for Phase 1 implementation planning.
|
**Pre-implementation.** The project has completed a pivot from a three-layer model to an ALPN-as-service model. The greenfield workspace contains only `alknet-vault` (stable) and research/reference material. Foundational ADRs (001–009) are in place, including the BiStream type definition (ADR-007), vault integration (ADR-008), and the one-way door decision framework (ADR-009). Architecture specs are ready for Phase 1 implementation planning.
|
||||||
|
|
||||||
**Next step**: Resolve remaining two-way-door questions during implementation. Start with alknet-core (ProtocolHandler trait, Connection, endpoint, router, auth types, config).
|
**Next step**: Resolve remaining two-way-door questions during implementation. Start with alknet-core (ProtocolHandler trait, Connection, endpoint, router, auth types, config).
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ Crate-specific specs will be created when each crate is ready for Phase 1 archit
|
|||||||
| [005](decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | Accepted |
|
| [005](decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | Accepted |
|
||||||
| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention and Connection Model | Accepted |
|
| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention and Connection Model | Accepted |
|
||||||
| [007](decisions/007-bistream-type-definition.md) | BiStream Type Definition | Accepted |
|
| [007](decisions/007-bistream-type-definition.md) | BiStream Type Definition | Accepted |
|
||||||
| [008](decisions/008-secret-service-integration.md) | Secret Service Integration Point | Accepted |
|
| [008](decisions/008-secret-service-integration.md) | Vault Integration Point | Accepted |
|
||||||
| [009](decisions/009-one-way-door-decision-framework.md) | One-Way Door Decision Framework | Accepted |
|
| [009](decisions/009-one-way-door-decision-framework.md) | One-Way Door Decision Framework | Accepted |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
@@ -43,7 +43,7 @@ See [open-questions.md](open-questions.md) for the full tracker.
|
|||||||
- **OQ-02**: AuthContext timing — hybrid model (ADR-004)
|
- **OQ-02**: AuthContext timing — hybrid model (ADR-004)
|
||||||
- **OQ-03**: ALPN naming — `alknet/` prefix, no version (ADR-006)
|
- **OQ-03**: ALPN naming — `alknet/` prefix, no version (ADR-006)
|
||||||
- **OQ-06**: ALPN per connection, not per stream (ADR-006)
|
- **OQ-06**: ALPN per connection, not per stream (ADR-006)
|
||||||
- **OQ-08**: Secret service — CLI-embedded via call protocol (ADR-008)
|
- **OQ-08**: Vault integration — CLI-embedded via call protocol (ADR-008)
|
||||||
|
|
||||||
**Two-way doors (deferred to implementation):**
|
**Two-way doors (deferred to implementation):**
|
||||||
- **OQ-04**: Dynamic handler registration — start static, add ArcSwap later
|
- **OQ-04**: Dynamic handler registration — start static, add ArcSwap later
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ The new ALPN dispatch model eliminates the need for a shared interface layer. Ea
|
|||||||
|
|
||||||
Key constraints:
|
Key constraints:
|
||||||
- Protocol crates must depend on alknet-core for auth/identity/config — but not on each other
|
- Protocol crates must depend on alknet-core for auth/identity/config — but not on each other
|
||||||
- alknet-secret is already standalone (no alknet-core dependency) and must remain so
|
- alknet-secret is already standalone (no alknet-core dependency) and must remain so (renamed to alknet-vault — see ADR-008)
|
||||||
- The CLI binary assembles everything — it's the only crate that depends on all handler crates
|
- The CLI binary assembles everything — it's the only crate that depends on all handler crates
|
||||||
- Some handlers (SFTP, call protocol) need to compile to WASM for browser/client use
|
- Some handlers (SFTP, call protocol) need to compile to WASM for browser/client use
|
||||||
- irpc is the foundation for the call protocol — it provides the operation registry, framing, and pub/sub patterns
|
- irpc is the foundation for the call protocol — it provides the operation registry, framing, and pub/sub patterns
|
||||||
@@ -24,7 +24,7 @@ The workspace decomposes into the following crates:
|
|||||||
| Crate | Responsibility | Depends on |
|
| Crate | Responsibility | Depends on |
|
||||||
|-------|---------------|------------|
|
|-------|---------------|------------|
|
||||||
| `alknet-core` | ProtocolHandler trait, ALPN router, endpoint, BiStream, AuthContext, IdentityProvider, config, ArcSwap dynamic config | tokio, quinn, rustls, irpc |
|
| `alknet-core` | ProtocolHandler trait, ALPN router, endpoint, BiStream, AuthContext, IdentityProvider, config, ArcSwap dynamic config | tokio, quinn, rustls, irpc |
|
||||||
| `alknet-secret` | BIP39/SLIP-0010/AES-GCM key derivation and encryption, SecretProtocol service | (standalone, no alknet-core) |
|
| `alknet-vault` | Local key vault: BIP39/SLIP-0010/AES-GCM key derivation, encryption, VaultProtocol dispatch | (standalone, no alknet-core) |
|
||||||
| `alknet-ssh` | SshAdapter (russh, SOCKS5, port forwarding) | alknet-core, russh |
|
| `alknet-ssh` | SshAdapter (russh, SOCKS5, port forwarding) | alknet-core, russh |
|
||||||
| `alknet-call` | CallAdapter (JSON-RPC via irpc, operation registry, pub/sub, access control) | alknet-core, irpc |
|
| `alknet-call` | CallAdapter (JSON-RPC via irpc, operation registry, pub/sub, access control) | alknet-core, irpc |
|
||||||
| `alknet-git` | GitAdapter (gix, pkt-line protocol) | alknet-core, gix |
|
| `alknet-git` | GitAdapter (gix, pkt-line protocol) | alknet-core, gix |
|
||||||
@@ -37,7 +37,7 @@ The workspace decomposes into the following crates:
|
|||||||
|
|
||||||
Dependency flow:
|
Dependency flow:
|
||||||
```
|
```
|
||||||
alknet-secret (standalone)
|
alknet-vault (standalone)
|
||||||
alknet-core ← all handler crates ← alknet (CLI)
|
alknet-core ← all handler crates ← alknet (CLI)
|
||||||
alknet-call ← alknet-napi
|
alknet-call ← alknet-napi
|
||||||
```
|
```
|
||||||
@@ -49,7 +49,7 @@ No handler crate depends on another handler crate. Cross-handler communication g
|
|||||||
**Positive:**
|
**Positive:**
|
||||||
- Each handler can be developed, tested, and versioned independently
|
- Each handler can be developed, tested, and versioned independently
|
||||||
- WASM-compatible handlers (sftp, call) don't pull in heavy dependencies (russh, axum)
|
- WASM-compatible handlers (sftp, call) don't pull in heavy dependencies (russh, axum)
|
||||||
- alknet-secret remains standalone — no circular dependency risk
|
- alknet-vault remains standalone — no circular dependency risk
|
||||||
- New handlers are added by creating a crate and registering it with the endpoint
|
- New handlers are added by creating a crate and registering it with the endpoint
|
||||||
- Clean separation of concerns — each crate has one job
|
- Clean separation of concerns — each crate has one job
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# ADR-008: Secret Service Integration Point
|
# ADR-008: Vault Integration Point
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
@@ -6,19 +6,25 @@ Accepted
|
|||||||
|
|
||||||
## Context
|
## Context
|
||||||
|
|
||||||
alknet-secret is a standalone crate with zero alknet crate dependencies. It provides BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM encryption, and an irpc-based `SecretProtocol` service. It is already implemented and stable.
|
alknet-vault (formerly alknet-secret) is a standalone crate with zero alknet crate dependencies. It provides BIP39 mnemonic generation, SLIP-0010 Ed25519 HD key derivation, AES-256-GCM encryption, and an irpc-based `VaultProtocol` for message dispatch. It is already implemented and stable.
|
||||||
|
|
||||||
The question (OQ-08) is: how does the rest of the alknet system access alknet-secret's capabilities? The options are:
|
The question (OQ-08) was: how does the rest of the alknet system access alknet-vault's capabilities? The options were:
|
||||||
|
|
||||||
1. **irpc service over `alknet/call`**: Other services call SecretProtocol operations through the call protocol on `alknet/call`. The secret service is just another set of operations registered in the call protocol's operation registry.
|
1. **irpc service over `alknet/call`**: Other services call vault operations through the call protocol.
|
||||||
|
2. **ALPN handler on `alknet/secret`**: alknet-vault implements ProtocolHandler and gets its own ALPN.
|
||||||
|
3. **Direct library dependency**: alknet-core or handler crates depend on alknet-vault directly, breaking its independence.
|
||||||
|
4. **CLI-embedded with call protocol exposure**: The CLI binary instantiates VaultServiceHandle locally and registers vault operations in the call protocol's registry.
|
||||||
|
|
||||||
2. **ALPN handler on `alknet/secret`**: alknet-secret implements `ProtocolHandler` and gets its own ALPN. Remote nodes call it over a dedicated QUIC connection.
|
This is a one-way door because if alknet-vault gets pulled into alknet-core as a dependency, its independence is permanently lost. The standalone property is valuable — alknet-vault has no QUIC, no tokio runtime requirement (the handle works without it), and no alknet crate dependencies. It can be used in contexts where QUIC networking doesn't exist (CLI tools, test harnesses, WASM key derivation).
|
||||||
|
|
||||||
3. **Direct library dependency**: alknet-core or handler crates depend on alknet-secret directly, breaking its independence.
|
Beyond the integration point, there's a question of access patterns. The vault holds the master seed and can derive keys and encrypt/decrypt arbitrary data. This is used for:
|
||||||
|
|
||||||
4. **CLI-embedded with call protocol exposure**: The CLI binary instantiates SecretServiceHandle locally and registers secret operations in the call protocol's registry.
|
- Identity key derivation (SSH host keys, node identity)
|
||||||
|
- Provider API key storage (Vast.ai, LLM providers) — encrypted at rest, decrypted on demand
|
||||||
|
- Credential encryption for storage
|
||||||
|
- Multi-tenant key derivation (different derivation paths per tenant)
|
||||||
|
|
||||||
This is a one-way door because if alknet-secret gets pulled into alknet-core as a dependency, its independence is permanently lost. The standalone property is valuable — alknet-secret has no QUIC, no tokio runtime requirement (the handle works without it), and no alknet crate dependencies. It can be used in contexts where QUIC networking doesn't exist (CLI tools, test harnesses, WASM key derivation).
|
The vault is a capability source, not a service endpoint. Operations that need provider keys don't hold a reference to the vault — they receive the derived/decrypted material through their operation context. The vault is unlocked at startup by the CLI, and the CLI injects material into operation contexts as needed.
|
||||||
|
|
||||||
## Decision
|
## Decision
|
||||||
|
|
||||||
@@ -26,38 +32,37 @@ This is a one-way door because if alknet-secret gets pulled into alknet-core as
|
|||||||
|
|
||||||
The CLI binary (the `alknet` crate) is the integration point. It:
|
The CLI binary (the `alknet` crate) is the integration point. It:
|
||||||
|
|
||||||
1. Instantiates `SecretServiceHandle` locally at startup (or on-demand with Unlock/Lock lifecycle).
|
1. Instantiates `VaultServiceHandle` locally at startup (or on-demand with Unlock/Lock lifecycle).
|
||||||
2. Registers secret operations (DeriveEd25519, DeriveEncryptionKey, Encrypt, Decrypt, etc.) in the call protocol's operation registry.
|
2. Registers vault operations (DeriveEd25519, DeriveEncryptionKey, Encrypt, Decrypt, etc.) in the call protocol's operation registry.
|
||||||
3. Other handlers access secret capabilities by calling operations on `alknet/call` — they don't import alknet-secret directly.
|
3. Other handlers access vault capabilities by calling operations on `alknet/call` — they don't import alknet-vault directly.
|
||||||
|
|
||||||
alknet-secret remains standalone with no alknet crate dependencies. Its `SecretServiceHandle` is used directly (in-process, no serialization) by the CLI binary. Its `SecretProtocol` irpc service is available for remote access through the call protocol.
|
**alknet-vault does NOT get its own ALPN.** Key derivation is a local operation — the master seed never crosses the network. If a remote node needs derived public keys (e.g., for identity verification), they're shared through the call protocol, not through direct vault access.
|
||||||
|
|
||||||
**alknet-secret does NOT get its own ALPN.** Here's why:
|
**The vault is accessed at the assembly layer, not by individual handlers.** The CLI (or a configuration middleware it sets up) is the only component that talks to the vault directly. Derived keys and decrypted credentials are injected into operation contexts — handlers receive the material they need, not a vault reference.
|
||||||
|
|
||||||
- `alknet/secret` as a separate ALPN would mean a remote node opens a QUIC connection to access key derivation — this is architecturally wrong. Key derivation is a local operation that should never cross the network in its raw form.
|
This is analogous to the reverse-proxy admin key pattern (ADR-028 in the reverse-proxy project): the proxy reads the key file once at startup, hashes it, and individual handlers never see the file. Here, the CLI unlocks the vault once at startup, and individual handlers receive the results of vault operations through their contexts.
|
||||||
- If a remote node needs derived keys (e.g., for end-to-end encryption), the local node derives them and sends only the public component over `alknet/call` — never the seed or private key.
|
|
||||||
- The secret service's Unlock/Lock lifecycle (holding the master seed in RAM) is inherently local. There's no safe way to expose Unlock/Lock over the network.
|
|
||||||
|
|
||||||
**What if a handler needs a key at runtime?** The handler calls through the call protocol. The CLI registers secret operations in the call registry at startup. The call protocol routes the request to the locally-running SecretServiceHandle. No handler crate depends on alknet-secret.
|
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
**Positive:**
|
**Positive:**
|
||||||
- alknet-secret remains fully standalone — no QUIC dependency, no tokio runtime requirement for the handle
|
- alknet-vault remains fully standalone — no QUIC dependency, no tokio runtime requirement for the handle
|
||||||
- Key derivation and encryption are local-only by default — the master seed never leaves the node
|
- Key derivation and encryption are local-only by default — the master seed never leaves the node
|
||||||
- Remote access to public key material (not secrets) flows through the existing call protocol — no separate ALPN needed
|
- Remote access to public key material (not secrets) flows through the existing call protocol — no separate ALPN needed
|
||||||
- The CLI binary is the single integration point — clean dependency graph, no circular dependencies
|
- The CLI binary is the single integration point — clean dependency graph, no circular dependencies
|
||||||
- The `SecretServiceHandle` is used in-process with zero serialization overhead — direct method calls, not irpc messages
|
- The `VaultServiceHandle` is used in-process with zero serialization overhead — direct method calls, not irpc messages
|
||||||
- Test harnesses can use `SecretServiceHandle` directly without any QUIC infrastructure
|
- Test harnesses can use `VaultServiceHandle` directly without any QUIC infrastructure
|
||||||
|
- Access pattern is clear: vault → CLI → operation contexts, not vault ← handlers
|
||||||
|
|
||||||
**Negative:**
|
**Negative:**
|
||||||
- Handlers that need keys must go through the call protocol — this adds a hop even for local calls (mitigated: local call protocol calls can be short-circuited to direct method calls via irpc's local dispatch)
|
- Handlers that need keys must receive them through their operation context — this requires the CLI or call protocol to mediate
|
||||||
- The CLI binary has a larger dependency tree since it imports both alknet-call and alknet-secret (expected: the CLI assembles everything)
|
- The CLI binary has a larger dependency tree since it imports both alknet-call and alknet-vault (expected: the CLI assembles everything)
|
||||||
- If the call protocol is not yet running when a handler needs a key, the handler must wait for initialization (mitigated: the CLI starts SecretServiceHandle before accepting connections)
|
- If the call protocol is not yet running when a handler needs a key, the handler must wait for initialization (mitigated: the CLI starts VaultServiceHandle before accepting connections)
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
- ADR-003: Crate decomposition (alknet-secret is standalone)
|
- ADR-003: Crate decomposition (alknet-vault is standalone)
|
||||||
- ADR-005: irpc as call protocol foundation
|
- ADR-005: irpc as call protocol foundation
|
||||||
|
- ADR-009: One-way door decision framework
|
||||||
- OQ-08: Secret service integration point (resolved by this ADR)
|
- OQ-08: Secret service integration point (resolved by this ADR)
|
||||||
- alknet-secret implementation: `crates/alknet-secret/`
|
- alknet-vault implementation: `crates/alknet-vault/`
|
||||||
|
- Reverse-proxy ADR-028: Admin HTTP API (analogous key management pattern)
|
||||||
@@ -20,7 +20,7 @@ Every architectural decision is classified as one of:
|
|||||||
|
|
||||||
**One-way door** — Reversing this decision requires rewriting significant code across multiple crates or permanently closes a capability door. Examples:
|
**One-way door** — Reversing this decision requires rewriting significant code across multiple crates or permanently closes a capability door. Examples:
|
||||||
- BiStream as a concrete quinn type (closes WASM door permanently)
|
- BiStream as a concrete quinn type (closes WASM door permanently)
|
||||||
- alknet-secret pulled into alknet-core as a dependency (loses standalone property permanently)
|
- alknet-vault pulled into alknet-core as a dependency (loses standalone property permanently)
|
||||||
- ProtocolHandler signature changes (every handler must be rewritten)
|
- ProtocolHandler signature changes (every handler must be rewritten)
|
||||||
|
|
||||||
**Two-way door** — Reversing this decision is cheap or additive. Examples:
|
**Two-way door** — Reversing this decision is cheap or additive. Examples:
|
||||||
|
|||||||
@@ -84,13 +84,13 @@ Door type classifications follow ADR-009:
|
|||||||
|
|
||||||
## Theme: Security
|
## Theme: Security
|
||||||
|
|
||||||
### OQ-08: Secret Service Integration Point
|
### OQ-08: Vault Integration Point
|
||||||
|
|
||||||
- **Origin**: [overview.md](overview.md)
|
- **Origin**: [overview.md](overview.md)
|
||||||
- **Status**: resolved
|
- **Status**: resolved
|
||||||
- **Door type**: One-way
|
- **Door type**: One-way
|
||||||
- **Priority**: medium
|
- **Priority**: medium
|
||||||
- **Resolution**: CLI-embedded with call protocol exposure. The CLI binary instantiates `SecretServiceHandle` locally and registers secret operations in the call protocol's operation registry. alknet-secret has no ALPN and no alknet-core dependency. Key derivation is local-only; only public key material crosses the network via `alknet/call`. See ADR-008.
|
- **Resolution**: CLI-embedded with call protocol exposure. The CLI binary instantiates `VaultServiceHandle` locally and registers vault operations in the call protocol's operation registry. alknet-vault has no ALPN and no alknet-core dependency. Key derivation is local-only; only public key material crosses the network via `alknet/call`. The vault is a capability source — derived keys and decrypted credentials are injected into operation contexts at the assembly layer, not passed as vault references to handlers. See ADR-008.
|
||||||
- **Cross-references**: ADR-003, ADR-005, ADR-008
|
- **Cross-references**: ADR-003, ADR-005, ADR-008
|
||||||
|
|
||||||
## Deferred Questions
|
## Deferred Questions
|
||||||
|
|||||||
@@ -25,12 +25,12 @@ See [ADR-001](decisions/001-alpn-protocol-dispatch.md) for the full rationale.
|
|||||||
## Crate Graph
|
## Crate Graph
|
||||||
|
|
||||||
```
|
```
|
||||||
alknet-secret (standalone, no alknet-core dependency)
|
alknet-vault (standalone, no alknet-core dependency)
|
||||||
│
|
│
|
||||||
alknet-core
|
alknet-core
|
||||||
│ ├── ProtocolHandler trait
|
│ ├── ProtocolHandler trait
|
||||||
│ ├── ALPN router / endpoint
|
│ ├── ALPN router / endpoint
|
||||||
│ ├── BiStream type
|
│ ├── BiStream trait, Connection type
|
||||||
│ ├── AuthContext, IdentityProvider
|
│ ├── AuthContext, IdentityProvider
|
||||||
│ └── StaticConfig, DynamicConfig (ArcSwap)
|
│ └── StaticConfig, DynamicConfig (ArcSwap)
|
||||||
│
|
│
|
||||||
@@ -44,15 +44,15 @@ alknet-core
|
|||||||
│
|
│
|
||||||
├── alknet-napi (depends on alknet-call, napi-rs)
|
├── alknet-napi (depends on alknet-call, napi-rs)
|
||||||
│
|
│
|
||||||
└── alknet (CLI binary, depends on all handler crates)
|
└── alknet (CLI binary, depends on all handler crates + alknet-vault)
|
||||||
```
|
```
|
||||||
|
|
||||||
Dependency rules:
|
Dependency rules:
|
||||||
- No handler crate depends on another handler crate
|
- No handler crate depends on another handler crate
|
||||||
- All handler crates depend on alknet-core
|
- All handler crates depend on alknet-core
|
||||||
- alknet-secret has zero alknet crate dependencies
|
- alknet-vault has zero alknet crate dependencies
|
||||||
- alknet-napi depends only on alknet-call (call protocol client)
|
- alknet-napi depends only on alknet-call (call protocol client)
|
||||||
- alknet (CLI) is the only crate that depends on all handler crates
|
- alknet (CLI) is the only crate that depends on all handler crates and alknet-vault
|
||||||
|
|
||||||
See [ADR-003](decisions/003-crate-decomposition.md) for the full decomposition rationale.
|
See [ADR-003](decisions/003-crate-decomposition.md) for the full decomposition rationale.
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ See [ADR-002](decisions/002-protocol-handler-trait.md) and [ADR-007](decisions/0
|
|||||||
| `h3` | HttpAdapter (WebTransport upgrade) | Browser-compatible WebTransport, then ALPN upgrade |
|
| `h3` | HttpAdapter (WebTransport upgrade) | Browser-compatible WebTransport, then ALPN upgrade |
|
||||||
| `h2` / `http/1.1` | HttpAdapter | Standard HTTP for browsers, curl |
|
| `h2` / `http/1.1` | HttpAdapter | Standard HTTP for browsers, curl |
|
||||||
|
|
||||||
> **Note**: `alknet/secret` is not in the ALPN registry. alknet-secret is a standalone crate with no alknet-core dependency. The CLI binary embeds it and exposes its operations through `alknet/call`. See ADR-008 for the integration rationale.
|
> **Note**: `alknet/vault` is not in the ALPN registry. alknet-vault is a standalone local key vault with no alknet-core dependency. The CLI binary embeds it and exposes its operations through `alknet/call`. The vault is a capability source — derived keys and decrypted credentials are injected into operation contexts at the assembly layer, not passed as vault references to handlers. See ADR-008 for the integration rationale.
|
||||||
|
|
||||||
## Authentication
|
## Authentication
|
||||||
|
|
||||||
@@ -181,7 +181,7 @@ All design decisions are documented as ADRs in [decisions/](decisions/).
|
|||||||
| [005](decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | Call protocol uses irpc for registry, framing, dispatch |
|
| [005](decisions/005-irpc-as-call-protocol-foundation.md) | irpc as Call Protocol Foundation | Call protocol uses irpc for registry, framing, dispatch |
|
||||||
| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention and Connection Model | `alknet/` prefix, one ALPN per connection |
|
| [006](decisions/006-alpn-convention-and-connection-model.md) | ALPN String Convention and Connection Model | `alknet/` prefix, one ALPN per connection |
|
||||||
| [007](decisions/007-bistream-type-definition.md) | BiStream Type Definition | BiStream is a trait, handlers receive Connection not BiStream |
|
| [007](decisions/007-bistream-type-definition.md) | BiStream Type Definition | BiStream is a trait, handlers receive Connection not BiStream |
|
||||||
| [008](decisions/008-secret-service-integration.md) | Secret Service Integration Point | CLI-embedded, exposed via call protocol, no ALPN for secrets |
|
| [008](decisions/008-secret-service-integration.md) | Vault Integration Point | CLI-embedded, exposed via call protocol, vault is a capability source |
|
||||||
| [009](decisions/009-one-way-door-decision-framework.md) | One-Way Door Decision Framework | Classify decisions by reversal cost; one-way doors need ADRs |
|
| [009](decisions/009-one-way-door-decision-framework.md) | One-Way Door Decision Framework | Classify decisions by reversal cost; one-way doors need ADRs |
|
||||||
|
|
||||||
## Open Questions
|
## Open Questions
|
||||||
@@ -192,7 +192,7 @@ Open questions are tracked in [open-questions.md](open-questions.md). Key questi
|
|||||||
- **OQ-02**: AuthContext resolution timing (resolved: hybrid — see ADR-004)
|
- **OQ-02**: AuthContext resolution timing (resolved: hybrid — see ADR-004)
|
||||||
- **OQ-03**: ALPN string naming convention (resolved: see ADR-006)
|
- **OQ-03**: ALPN string naming convention (resolved: see ADR-006)
|
||||||
- **OQ-04**: Dynamic handler registration at runtime vs static at startup (two-way door, defer to implementation)
|
- **OQ-04**: Dynamic handler registration at runtime vs static at startup (two-way door, defer to implementation)
|
||||||
- **OQ-08**: Secret service integration point (resolved: CLI-embedded via call protocol — see ADR-008)
|
- **OQ-08**: Vault integration point (resolved: CLI-embedded via call protocol — see ADR-008)
|
||||||
|
|
||||||
## Failure Modes
|
## Failure Modes
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user