cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

commit d1c18025bf516b6dbf74c13fcdb4cd6abc5666c9
parent dba81a9bd83d63b29cb2a21b115c80f35e4ca2c8
Author: triesap <tyson@radroots.org>
Date:   Sun, 12 Apr 2026 18:03:31 +0000

accounts: remove cli plaintext backend

Diffstat:
MCargo.lock | 1+
MCargo.toml | 2+-
Msrc/render/mod.rs | 2--
Msrc/runtime/accounts.rs | 157+++++++++++++++++--------------------------------------------------------------
Msrc/runtime/config.rs | 7++-----
Mtests/identity_commands.rs | 6++++--
6 files changed, 42 insertions(+), 133 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -1823,6 +1823,7 @@ version = "0.1.0-alpha.2" dependencies = [ "radroots_identity", "radroots_nostr_signer", + "radroots_protected_store", "radroots_runtime", "radroots_secret_vault", "serde", diff --git a/Cargo.toml b/Cargo.toml @@ -26,7 +26,7 @@ radroots_events = { path = "../lib/crates/events" } radroots_events_codec = { path = "../lib/crates/events_codec", features = ["serde_json"] } radroots_identity = { path = "../lib/crates/identity" } radroots_log = { path = "../lib/crates/log" } -radroots_nostr_accounts = { path = "../lib/crates/nostr_accounts" } +radroots_nostr_accounts = { path = "../lib/crates/nostr_accounts", features = ["os-keyring"] } radroots_nostr_signer = { path = "../lib/crates/nostr_signer" } radroots_protected_store = { path = "../lib/crates/protected_store", features = ["std"] } radroots_replica_db = { path = "../lib/crates/replica_db" } diff --git a/src/render/mod.rs b/src/render/mod.rs @@ -2641,7 +2641,6 @@ mod tests { "host_vault".into(), "encrypted_file".into(), "memory".into(), - "plaintext_file".into(), ], host_vault_policy: Some("desktop".into()), uses_protected_store: true, @@ -2792,7 +2791,6 @@ mod tests { "host_vault".into(), "encrypted_file".into(), "memory".into(), - "plaintext_file".into(), ], host_vault_policy: Some("desktop".into()), uses_protected_store: true, diff --git a/src/runtime/accounts.rs b/src/runtime/accounts.rs @@ -1,15 +1,9 @@ -use std::fs; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - use radroots_nostr_accounts::prelude::{ - RadrootsNostrAccountRecord, RadrootsNostrAccountsManager, RadrootsNostrSecretVaultMemory, - RadrootsNostrSelectedAccountStatus, + RadrootsNostrAccountRecord, RadrootsNostrAccountsManager, RadrootsNostrSelectedAccountStatus, }; -use radroots_protected_store::RadrootsProtectedFileSecretVault; use radroots_secret_vault::{ - RadrootsHostVaultCapabilities, RadrootsResolvedSecretBackend, RadrootsSecretBackend, - RadrootsSecretBackendSelection, RadrootsSecretVault, RadrootsSecretVaultAccessError, + RadrootsHostVaultCapabilities, RadrootsResolvedSecretBackend, + RadrootsSecretBackendAvailability, RadrootsSecretBackendSelection, RadrootsSecretVault, RadrootsSecretVaultError, RadrootsSecretVaultOsKeyring, }; @@ -19,7 +13,6 @@ use crate::runtime::config::RuntimeConfig; const HOST_VAULT_AVAILABILITY_OVERRIDE_ENV: &str = "RADROOTS_ACCOUNT_HOST_VAULT_AVAILABLE"; const HOST_VAULT_SERVICE_NAME: &str = "org.radroots.cli.local-account"; const HOST_VAULT_PROBE_SLOT: &str = "__radroots_cli_host_vault_probe__"; -const PLAINTEXT_FILE_SECRET_SUFFIX: &str = ".secret"; pub const SHARED_ACCOUNT_STORE_SOURCE: &str = "shared account store ยท local first"; #[derive(Debug, Clone)] @@ -232,30 +225,14 @@ fn find_by_selector<'a>( } fn account_manager(config: &RuntimeConfig) -> Result<RadrootsNostrAccountsManager, RuntimeError> { - let vault = secret_vault(config)?; - Ok(RadrootsNostrAccountsManager::new_file_backed( + let (manager, _) = RadrootsNostrAccountsManager::new_local_file_backed( config.account.store_path.as_path(), - vault, - )?) -} - -fn secret_vault(config: &RuntimeConfig) -> Result<Arc<dyn RadrootsSecretVault>, RuntimeError> { - let resolved = resolve_secret_backend(config).map_err(secret_backend_runtime_error)?; - match resolved.backend { - RadrootsSecretBackend::HostVault(_) => Ok(Arc::new(RadrootsSecretVaultOsKeyring::new( - HOST_VAULT_SERVICE_NAME, - ))), - RadrootsSecretBackend::EncryptedFile => Ok(Arc::new( - RadrootsProtectedFileSecretVault::new(config.account.secrets_dir.as_path()), - )), - RadrootsSecretBackend::Memory => Ok(Arc::new(RadrootsNostrSecretVaultMemory::new())), - RadrootsSecretBackend::PlaintextFile => Ok(Arc::new(CliPlaintextFileSecretVault::new( - config.account.secrets_dir.as_path(), - ))), - RadrootsSecretBackend::ExternalCommand => Err(RuntimeError::Config( - "external_command secret backend is not supported for local cli accounts".to_owned(), - )), - } + config.account.secrets_dir.as_path(), + account_secret_backend_selection(config), + secret_backend_availability()?, + HOST_VAULT_SERVICE_NAME, + )?; + Ok(manager) } fn resolve_secret_backend( @@ -264,35 +241,35 @@ fn resolve_secret_backend( let availability = secret_backend_availability().map_err(|error| { SecretBackendResolutionError::Invalid(format!("account secret backend: {error}")) })?; - let selection = RadrootsSecretBackendSelection { + RadrootsNostrAccountsManager::resolve_local_backend( + account_secret_backend_selection(config), + availability, + ) + .map_err(|error| match error { + RadrootsSecretVaultError::BackendUnavailable { .. } + | RadrootsSecretVaultError::FallbackUnavailable { .. } => { + SecretBackendResolutionError::Unavailable(format!("account secret backend: {error}")) + } + RadrootsSecretVaultError::FallbackDisallowed { .. } + | RadrootsSecretVaultError::HostVaultPolicyUnsupported { .. } => { + SecretBackendResolutionError::Invalid(format!("account secret backend: {error}")) + } + }) +} + +fn account_secret_backend_selection(config: &RuntimeConfig) -> RadrootsSecretBackendSelection { + RadrootsSecretBackendSelection { primary: config.account.secret_backend, fallback: config.account.secret_fallback, - }; - - selection - .resolve(availability) - .map_err(|error| match error { - RadrootsSecretVaultError::BackendUnavailable { .. } - | RadrootsSecretVaultError::FallbackUnavailable { .. } => { - SecretBackendResolutionError::Unavailable(format!( - "account secret backend: {error}" - )) - } - RadrootsSecretVaultError::FallbackDisallowed { .. } - | RadrootsSecretVaultError::HostVaultPolicyUnsupported { .. } => { - SecretBackendResolutionError::Invalid(format!("account secret backend: {error}")) - } - }) + } } -fn secret_backend_availability() --> Result<radroots_secret_vault::RadrootsSecretBackendAvailability, RuntimeError> { - Ok(radroots_secret_vault::RadrootsSecretBackendAvailability { +fn secret_backend_availability() -> Result<RadrootsSecretBackendAvailability, RuntimeError> { + Ok(RadrootsSecretBackendAvailability { host_vault: host_vault_capabilities()?, encrypted_file: true, external_command: false, memory: true, - plaintext_file: true, }) } @@ -329,84 +306,18 @@ fn parse_bool_value(key: &str, value: &str) -> Result<bool, RuntimeError> { } } -fn secret_backend_runtime_error(error: SecretBackendResolutionError) -> RuntimeError { - match error { - SecretBackendResolutionError::Unavailable(message) - | SecretBackendResolutionError::Invalid(message) => RuntimeError::Config(message), - } -} - #[derive(Debug, Clone)] enum SecretBackendResolutionError { Unavailable(String), Invalid(String), } -#[derive(Debug, Clone)] -struct CliPlaintextFileSecretVault { - secrets_dir: PathBuf, -} - -impl CliPlaintextFileSecretVault { - fn new(path: impl AsRef<Path>) -> Self { - Self { - secrets_dir: path.as_ref().to_path_buf(), - } - } - - fn secret_file_path(&self, slot: &str) -> PathBuf { - self.secrets_dir - .join(format!("{slot}{PLAINTEXT_FILE_SECRET_SUFFIX}")) - } -} - -impl RadrootsSecretVault for CliPlaintextFileSecretVault { - fn store_secret(&self, slot: &str, secret: &str) -> Result<(), RadrootsSecretVaultAccessError> { - fs::create_dir_all(&self.secrets_dir).map_err(io_backend_error)?; - let path = self.secret_file_path(slot); - fs::write(&path, secret.as_bytes()).map_err(io_backend_error)?; - set_secret_permissions(&path)?; - Ok(()) - } - - fn load_secret(&self, slot: &str) -> Result<Option<String>, RadrootsSecretVaultAccessError> { - match fs::read_to_string(self.secret_file_path(slot)) { - Ok(contents) => Ok(Some(contents.trim().to_owned())), - Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(source) => Err(io_backend_error(source)), - } - } - - fn remove_secret(&self, slot: &str) -> Result<(), RadrootsSecretVaultAccessError> { - match fs::remove_file(self.secret_file_path(slot)) { - Ok(()) => Ok(()), - Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()), - Err(source) => Err(io_backend_error(source)), - } - } -} - -fn io_backend_error(source: std::io::Error) -> RadrootsSecretVaultAccessError { - RadrootsSecretVaultAccessError::Backend(source.to_string()) -} - -#[cfg(unix)] -fn set_secret_permissions(path: &Path) -> Result<(), RadrootsSecretVaultAccessError> { - use std::os::unix::fs::PermissionsExt; - - let mut permissions = fs::metadata(path).map_err(io_backend_error)?.permissions(); - permissions.set_mode(0o600); - fs::set_permissions(path, permissions).map_err(io_backend_error) -} - -#[cfg(not(unix))] -fn set_secret_permissions(_path: &Path) -> Result<(), RadrootsSecretVaultAccessError> { - Ok(()) -} - #[cfg(test)] mod tests { use super::*; + use radroots_protected_store::RadrootsProtectedFileSecretVault; + use radroots_secret_vault::RadrootsSecretVault; + use std::fs; use tempfile::tempdir; #[test] diff --git a/src/runtime/config.rs b/src/runtime/config.rs @@ -28,8 +28,7 @@ const DEFAULT_RPC_URL: &str = "http://127.0.0.1:7070"; const CLI_HOST_VAULT_POLICY: &str = "desktop"; const CLI_DEFAULT_SECRET_BACKEND: &str = "host_vault"; const CLI_DEFAULT_SECRET_FALLBACK: &str = "encrypted_file"; -const CLI_ALLOWED_SHARED_SECRET_BACKENDS: &[&str] = - &["host_vault", "encrypted_file", "memory", "plaintext_file"]; +const CLI_ALLOWED_SHARED_SECRET_BACKENDS: &[&str] = &["host_vault", "encrypted_file", "memory"]; const CLI_USES_PROTECTED_STORE: bool = true; const ENV_FILE_PATH: &str = "RADROOTS_ENV_FILE"; const ENV_OUTPUT: &str = "RADROOTS_OUTPUT"; @@ -1286,9 +1285,8 @@ fn parse_account_secret_backend( )), "encrypted_file" => Ok(RadrootsSecretBackend::EncryptedFile), "memory" => Ok(RadrootsSecretBackend::Memory), - "plaintext_file" => Ok(RadrootsSecretBackend::PlaintextFile), other => Err(RuntimeError::Config(format!( - "{key} must be `host_vault`, `encrypted_file`, `memory`, `plaintext_file`, or `none` for fallback, got `{other}`" + "{key} must be `host_vault`, `encrypted_file`, `memory`, or `none` for fallback, got `{other}`" ))), } } @@ -1469,7 +1467,6 @@ mod tests { "host_vault".to_owned(), "encrypted_file".to_owned(), "memory".to_owned(), - "plaintext_file".to_owned(), ], host_vault_policy: Some("desktop".to_owned()), uses_protected_store: true, diff --git a/tests/identity_commands.rs b/tests/identity_commands.rs @@ -129,7 +129,7 @@ fn account_new_rejects_dry_run_without_creating_store_state() { } #[test] -fn account_new_rejects_plaintext_fallback_downgrade() { +fn account_new_rejects_plaintext_fallback_backend() { let dir = tempdir().expect("tempdir"); let output = cli_command_in(dir.path()) @@ -141,7 +141,9 @@ fn account_new_rejects_plaintext_fallback_downgrade() { assert_eq!(output.status.code(), Some(2)); let stderr = String::from_utf8(output.stderr).expect("utf8 stderr"); - assert!(stderr.contains("may not silently downgrade to plaintext_file")); + assert!( + stderr.contains("must be `host_vault`, `encrypted_file`, `memory`, or `none` for fallback") + ); } #[test]