lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

commit cdd93d99ab1527565a013d01c6b2ce5570507648
parent fb14849faafd002aeb14ea3b19f84067a39b64e1
Author: triesap <tyson@radroots.org>
Date:   Sun, 12 Apr 2026 18:03:32 +0000

secret_vault: remove shared plaintext backend

Diffstat:
MCargo.lock | 1+
Mcrates/nostr_accounts/Cargo.toml | 4++++
Mcrates/nostr_accounts/src/manager.rs | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/secret_vault/src/backend.rs | 3---
Mcrates/secret_vault/src/error.rs | 1-
Mcrates/secret_vault/src/selection.rs | 61-------------------------------------------------------------
6 files changed, 119 insertions(+), 65 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -2432,6 +2432,7 @@ dependencies = [ "radroots_identity", "radroots_nostr_ndb", "radroots_nostr_signer", + "radroots_protected_store", "radroots_runtime", "radroots_secret_vault", "serde", diff --git a/crates/nostr_accounts/Cargo.toml b/crates/nostr_accounts/Cargo.toml @@ -19,6 +19,7 @@ std = [ "dep:serde_json", "dep:radroots_identity", "dep:radroots_nostr_signer", + "dep:radroots_protected_store", "dep:radroots_runtime", "dep:radroots_secret_vault", ] @@ -39,6 +40,9 @@ radroots_nostr_ndb = { workspace = true, optional = true, default-features = fal "giftwrap", "rt", ] } +radroots_protected_store = { workspace = true, optional = true, default-features = false, features = [ + "std", +] } radroots_secret_vault = { workspace = true, optional = true, default-features = false, features = [ "std", ] } diff --git a/crates/nostr_accounts/src/manager.rs b/crates/nostr_accounts/src/manager.rs @@ -7,12 +7,19 @@ use crate::store::RadrootsNostrMemoryAccountStore; use crate::store::{RadrootsNostrAccountStore, RadrootsNostrFileAccountStore}; #[cfg(feature = "memory-vault")] use crate::vault::RadrootsNostrSecretVaultMemory; +#[cfg(feature = "os-keyring")] +use crate::vault::RadrootsNostrSecretVaultOsKeyring; use crate::vault::{RadrootsSecretVault, account_secret_slot}; use radroots_identity::{RadrootsIdentity, RadrootsIdentityId, RadrootsIdentityPublic}; use radroots_nostr_signer::prelude::{ RadrootsNostrLocalSignerAvailability, RadrootsNostrLocalSignerCapability, RadrootsNostrSignerCapability, }; +use radroots_protected_store::RadrootsProtectedFileSecretVault; +use radroots_secret_vault::{ + RadrootsResolvedSecretBackend, RadrootsSecretBackend, RadrootsSecretBackendAvailability, + RadrootsSecretBackendSelection, RadrootsSecretVaultError, +}; use std::path::Path; use std::sync::{Arc, RwLock}; use std::time::{SystemTime, UNIX_EPOCH}; @@ -84,6 +91,31 @@ impl RadrootsNostrAccountsManager { Self::new_file_backed(path, Arc::new(vault)) } + pub fn resolve_local_backend( + selection: RadrootsSecretBackendSelection, + availability: RadrootsSecretBackendAvailability, + ) -> Result<RadrootsResolvedSecretBackend, RadrootsSecretVaultError> { + selection.resolve(availability) + } + + pub fn new_local_file_backed( + path: impl AsRef<Path>, + secrets_dir: impl AsRef<Path>, + selection: RadrootsSecretBackendSelection, + availability: RadrootsSecretBackendAvailability, + host_vault_service_name: impl Into<String>, + ) -> Result<(Self, RadrootsResolvedSecretBackend), RadrootsNostrAccountsError> { + let resolved = Self::resolve_local_backend(selection, availability) + .map_err(|error| RadrootsNostrAccountsError::Vault(error.to_string()))?; + let vault = local_file_backed_secret_vault( + resolved.backend, + secrets_dir.as_ref(), + host_vault_service_name.into(), + )?; + let manager = Self::new_file_backed(path, vault)?; + Ok((manager, resolved)) + } + pub fn list_accounts( &self, ) -> Result<Vec<RadrootsNostrAccountRecord>, RadrootsNostrAccountsError> { @@ -416,6 +448,35 @@ impl RadrootsNostrAccountsManager { } } +fn local_file_backed_secret_vault( + backend: RadrootsSecretBackend, + secrets_dir: &Path, + _host_vault_service_name: String, +) -> Result<Arc<dyn RadrootsSecretVault>, RadrootsNostrAccountsError> { + match backend { + #[cfg(feature = "os-keyring")] + RadrootsSecretBackend::HostVault(_) => Ok(Arc::new( + RadrootsNostrSecretVaultOsKeyring::new(_host_vault_service_name), + )), + #[cfg(not(feature = "os-keyring"))] + RadrootsSecretBackend::HostVault(_) => Err(RadrootsNostrAccountsError::Vault( + "host_vault backend requires radroots_nostr_accounts os-keyring support".into(), + )), + RadrootsSecretBackend::EncryptedFile => { + Ok(Arc::new(RadrootsProtectedFileSecretVault::new(secrets_dir))) + } + #[cfg(feature = "memory-vault")] + RadrootsSecretBackend::Memory => Ok(Arc::new(RadrootsNostrSecretVaultMemory::new())), + #[cfg(not(feature = "memory-vault"))] + RadrootsSecretBackend::Memory => Err(RadrootsNostrAccountsError::Vault( + "memory backend requires radroots_nostr_accounts memory-vault support".into(), + )), + RadrootsSecretBackend::ExternalCommand => Err(RadrootsNostrAccountsError::Vault( + "external_command secret backend is not supported for local accounts".into(), + )), + } +} + fn now_unix_secs() -> u64 { SystemTime::now() .duration_since(UNIX_EPOCH) @@ -432,6 +493,10 @@ mod tests { use crate::vault::RadrootsNostrSecretVaultMemory; use crate::vault::RadrootsSecretVault; use radroots_identity::RadrootsIdentityProfile; + use radroots_secret_vault::{ + RadrootsHostVaultCapabilities, RadrootsSecretBackend, RadrootsSecretBackendAvailability, + RadrootsSecretBackendSelection, + }; use std::sync::Arc; use std::sync::RwLock; use std::thread; @@ -687,6 +752,55 @@ mod tests { } #[test] + fn resolve_local_backend_applies_shared_fallback_policy() { + let resolved = RadrootsNostrAccountsManager::resolve_local_backend( + RadrootsSecretBackendSelection { + primary: RadrootsSecretBackend::HostVault( + radroots_secret_vault::RadrootsHostVaultPolicy::desktop(), + ), + fallback: Some(RadrootsSecretBackend::EncryptedFile), + }, + RadrootsSecretBackendAvailability { + host_vault: RadrootsHostVaultCapabilities::unavailable(), + encrypted_file: true, + external_command: false, + memory: false, + }, + ) + .expect("fallback resolves"); + + assert_eq!(resolved.backend, RadrootsSecretBackend::EncryptedFile); + assert!(resolved.used_fallback); + } + + #[test] + fn new_local_file_backed_rejects_external_command_backend() { + let temp = tempfile::tempdir().expect("tempdir"); + let err = RadrootsNostrAccountsManager::new_local_file_backed( + temp.path().join("accounts.json"), + temp.path().join("secrets"), + RadrootsSecretBackendSelection { + primary: RadrootsSecretBackend::ExternalCommand, + fallback: None, + }, + RadrootsSecretBackendAvailability { + host_vault: RadrootsHostVaultCapabilities::unavailable(), + encrypted_file: true, + external_command: true, + memory: false, + }, + "org.radroots.test.local-account", + ) + .err() + .expect("external command must be rejected"); + + assert_eq!( + err.to_string(), + "vault error: external_command secret backend is not supported for local accounts" + ); + } + + #[test] fn watch_only_account_has_no_signing_identity() { let temp = tempfile::tempdir().expect("tempdir"); let store = Arc::new(RadrootsNostrFileAccountStore::new( diff --git a/crates/secret_vault/src/backend.rs b/crates/secret_vault/src/backend.rs @@ -6,7 +6,6 @@ pub enum RadrootsSecretBackendKind { EncryptedFile, ExternalCommand, Memory, - PlaintextFile, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -15,7 +14,6 @@ pub enum RadrootsSecretBackend { EncryptedFile, ExternalCommand, Memory, - PlaintextFile, } impl RadrootsSecretBackend { @@ -26,7 +24,6 @@ impl RadrootsSecretBackend { Self::EncryptedFile => RadrootsSecretBackendKind::EncryptedFile, Self::ExternalCommand => RadrootsSecretBackendKind::ExternalCommand, Self::Memory => RadrootsSecretBackendKind::Memory, - Self::PlaintextFile => RadrootsSecretBackendKind::PlaintextFile, } } } diff --git a/crates/secret_vault/src/error.rs b/crates/secret_vault/src/error.rs @@ -80,7 +80,6 @@ impl fmt::Display for RadrootsSecretBackendKind { Self::EncryptedFile => "encrypted_file", Self::ExternalCommand => "external_command", Self::Memory => "memory", - Self::PlaintextFile => "plaintext_file", }; f.write_str(value) } diff --git a/crates/secret_vault/src/selection.rs b/crates/secret_vault/src/selection.rs @@ -14,7 +14,6 @@ pub struct RadrootsSecretBackendAvailability { pub encrypted_file: bool, pub external_command: bool, pub memory: bool, - pub plaintext_file: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -76,7 +75,6 @@ impl RadrootsSecretBackendAvailability { RadrootsSecretBackend::EncryptedFile if self.encrypted_file => Ok(()), RadrootsSecretBackend::ExternalCommand if self.external_command => Ok(()), RadrootsSecretBackend::Memory if self.memory => Ok(()), - RadrootsSecretBackend::PlaintextFile if self.plaintext_file => Ok(()), _ => Err(RadrootsSecretVaultError::BackendUnavailable { backend: backend.kind(), }), @@ -118,7 +116,6 @@ mod tests { encrypted_file: true, external_command: false, memory: false, - plaintext_file: false, }) .expect("host vault resolves"); @@ -144,7 +141,6 @@ mod tests { encrypted_file: true, external_command: false, memory: false, - plaintext_file: false, }) .expect("encrypted file fallback resolves"); @@ -170,7 +166,6 @@ mod tests { encrypted_file: true, external_command: false, memory: false, - plaintext_file: false, }) .expect_err("missing fallback must fail"); @@ -199,7 +194,6 @@ mod tests { encrypted_file: true, external_command: false, memory: false, - plaintext_file: false, }) .expect_err("unsupported host policy must fail"); @@ -212,32 +206,6 @@ mod tests { } #[test] - fn encrypted_file_may_not_downgrade_to_plaintext_file() { - let selection = RadrootsSecretBackendSelection { - primary: RadrootsSecretBackend::EncryptedFile, - fallback: Some(RadrootsSecretBackend::PlaintextFile), - }; - - let err = selection - .resolve(RadrootsSecretBackendAvailability { - host_vault: RadrootsHostVaultCapabilities::unavailable(), - encrypted_file: false, - external_command: false, - memory: false, - plaintext_file: true, - }) - .expect_err("plaintext downgrade must fail"); - - assert_eq!( - err, - RadrootsSecretVaultError::FallbackDisallowed { - primary: RadrootsSecretBackendKind::EncryptedFile, - fallback: RadrootsSecretBackendKind::PlaintextFile, - } - ); - } - - #[test] fn external_command_may_not_downgrade_to_encrypted_file() { let selection = RadrootsSecretBackendSelection { primary: RadrootsSecretBackend::ExternalCommand, @@ -250,7 +218,6 @@ mod tests { encrypted_file: true, external_command: false, memory: false, - plaintext_file: false, }) .expect_err("external command downgrade must fail"); @@ -264,32 +231,6 @@ mod tests { } #[test] - fn explicit_plaintext_file_selection_stays_explicit() { - let selection = RadrootsSecretBackendSelection { - primary: RadrootsSecretBackend::PlaintextFile, - fallback: None, - }; - - let resolved = selection - .resolve(RadrootsSecretBackendAvailability { - host_vault: RadrootsHostVaultCapabilities::unavailable(), - encrypted_file: false, - external_command: false, - memory: false, - plaintext_file: true, - }) - .expect("explicit plaintext file selection resolves"); - - assert_eq!( - resolved, - RadrootsResolvedSecretBackend { - backend: RadrootsSecretBackend::PlaintextFile, - used_fallback: false, - } - ); - } - - #[test] fn memory_backend_must_be_selected_explicitly() { let selection = RadrootsSecretBackendSelection { primary: RadrootsSecretBackend::Memory, @@ -302,7 +243,6 @@ mod tests { encrypted_file: false, external_command: false, memory: true, - plaintext_file: false, }) .expect("memory backend resolves"); @@ -328,7 +268,6 @@ mod tests { encrypted_file: false, external_command: false, memory: false, - plaintext_file: false, }) .expect_err("unavailable fallback must fail");