commit d1c18025bf516b6dbf74c13fcdb4cd6abc5666c9
parent dba81a9bd83d63b29cb2a21b115c80f35e4ca2c8
Author: triesap <tyson@radroots.org>
Date: Sun, 12 Apr 2026 18:03:31 +0000
accounts: remove cli plaintext backend
Diffstat:
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]