diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs index 46378cac..2f7f98e5 100644 --- a/crates/client/src/config.rs +++ b/crates/client/src/config.rs @@ -126,6 +126,10 @@ pub struct Config { /// Disable interactive prompts. #[serde(default)] pub disable_interactive: bool, + + /// Use the specified backend for keyring access. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub keyring_backend: Option, } impl Config { @@ -207,6 +211,7 @@ impl Config { ignore_federation_hints: self.ignore_federation_hints, auto_accept_federation_hints: self.auto_accept_federation_hints, disable_interactive: self.disable_interactive, + keyring_backend: self.keyring_backend.clone(), }; serde_json::to_writer_pretty( diff --git a/crates/client/src/keyring.rs b/crates/client/src/keyring.rs deleted file mode 100644 index d815e940..00000000 --- a/crates/client/src/keyring.rs +++ /dev/null @@ -1,201 +0,0 @@ -//! Utilities for interacting with keyring and performing signing operations. - -use crate::RegistryUrl; -use anyhow::{bail, Context, Result}; -use indexmap::IndexSet; -use secrecy::Secret; -use warg_crypto::signing::PrivateKey; - -/// Gets the auth token entry for the given registry and key name. -pub fn get_auth_token_entry(registry_url: &RegistryUrl) -> Result { - let label = format!("warg-auth-token:{}", registry_url.safe_label()); - keyring::Entry::new(&label, ®istry_url.safe_label()).context("failed to get keyring entry") -} - -/// Gets the auth token -pub fn get_auth_token(registry_url: &RegistryUrl) -> Result>> { - let entry = get_auth_token_entry(registry_url)?; - match entry.get_password() { - Ok(secret) => Ok(Some(Secret::from(secret))), - Err(keyring::Error::NoEntry) => Ok(None), - Err(keyring::Error::Ambiguous(_)) => { - bail!("more than one auth token for registry `{registry_url}`"); - } - Err(e) => { - bail!("failed to get auth token for registry `{registry_url}`: {e}"); - } - } -} - -/// Deletes the auth token -pub fn delete_auth_token(registry_url: &RegistryUrl) -> Result<()> { - let entry = get_auth_token_entry(registry_url)?; - match entry.delete_password() { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => { - bail!("no auth token found for registry `{registry_url}`"); - } - Err(keyring::Error::Ambiguous(_)) => { - bail!("more than one auth token found for registry `{registry_url}`"); - } - Err(e) => { - bail!("failed to delete auth torkn for registry `{registry_url}`: {e}"); - } - } -} - -/// Sets the auth token -pub fn set_auth_token(registry_url: &RegistryUrl, token: &str) -> Result<()> { - let entry = get_auth_token_entry(registry_url)?; - match entry.set_password(token) { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => { - bail!("no auth token found for registry `{registry_url}`"); - } - Err(keyring::Error::Ambiguous(_)) => { - bail!("more than one auth token for registry `{registry_url}`"); - } - Err(e) => { - bail!("failed to set auth token for registry `{registry_url}`: {e}"); - } - } -} - -/// Gets the signing key entry for the given registry and key name. -pub fn get_signing_key_entry( - registry_url: Option<&str>, - keys: &IndexSet, - home_url: Option<&str>, -) -> Result { - if let Some(registry_url) = registry_url { - if keys.contains(registry_url) { - keyring::Entry::new("warg-signing-key", registry_url) - .context("failed to get keyring entry") - } else { - keyring::Entry::new("warg-signing-key", "default") - .context("failed to get keyring entry") - } - } else { - if let Some(url) = home_url { - if keys.contains(url) { - return keyring::Entry::new( - "warg-signing-key", - &RegistryUrl::new(url)?.safe_label(), - ) - .context("failed to get keyring entry"); - } - } - if keys.contains("default") { - keyring::Entry::new("warg-signing-key", "default") - .context("failed to get keyring entry") - } else { - bail!( - "error: Please set a default signing key by typing `warg key set ` or `warg key new`" - ) - } - } -} - -/// Gets the signing key for the given registry registry_label and key name. -pub fn get_signing_key( - // If being called by a cli key command, this will always be a cli flag - // If being called by a client publish command, this could also be supplied by namespace map config - registry_url: Option<&str>, - keys: &IndexSet, - home_url: Option<&str>, -) -> Result { - let entry = get_signing_key_entry(registry_url, keys, home_url)?; - - match entry.get_password() { - Ok(secret) => PrivateKey::decode(secret).context("failed to parse signing key"), - Err(keyring::Error::NoEntry) => { - if let Some(registry_url) = registry_url { - bail!("no signing key found for registry `{registry_url}`"); - } else { - bail!("no signing key found"); - } - } - Err(keyring::Error::Ambiguous(_)) => { - if let Some(registry_url) = registry_url { - bail!("more than one signing key found for registry `{registry_url}`"); - } else { - bail!("more than one signing key found"); - } - } - Err(e) => { - if let Some(registry_url) = registry_url { - bail!("failed to get signing key for registry `{registry_url}`: {e}"); - } else { - bail!("failed to get signing key`"); - } - } - } -} - -/// Sets the signing key for the given registry host and key name. -pub fn set_signing_key( - registry_url: Option<&str>, - key: &PrivateKey, - keys: &mut IndexSet, - home_url: Option<&str>, -) -> Result<()> { - let entry = get_signing_key_entry(registry_url, keys, home_url)?; - match entry.set_password(&key.encode()) { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => { - if let Some(registry_url) = registry_url { - bail!("no signing key found for registry `{registry_url}`"); - } else { - bail!("no signing key found`"); - } - } - Err(keyring::Error::Ambiguous(_)) => { - if let Some(registry_url) = registry_url { - bail!("more than one signing key found for registry `{registry_url}`"); - } else { - bail!("more than one signing key found"); - } - } - Err(e) => { - if let Some(registry_url) = registry_url { - bail!("failed to get signing key for registry `{registry_url}`: {e}"); - } else { - bail!("failed to get signing: {e}"); - } - } - } -} - -/// Deletes the signing key for the given registry host and key name. -pub fn delete_signing_key( - registry_url: Option<&str>, - keys: &IndexSet, - home_url: Option<&str>, -) -> Result<()> { - let entry = get_signing_key_entry(registry_url, keys, home_url)?; - - match entry.delete_password() { - Ok(()) => Ok(()), - Err(keyring::Error::NoEntry) => { - if let Some(registry_url) = registry_url { - bail!("no signing key found for registry `{registry_url}`"); - } else { - bail!("no signing key found"); - } - } - Err(keyring::Error::Ambiguous(_)) => { - if let Some(registry_url) = registry_url { - bail!("more than one signing key found for registry `{registry_url}`"); - } else { - bail!("more than one signing key found`"); - } - } - Err(e) => { - if let Some(registry_url) = registry_url { - bail!("failed to delete signing key for registry `{registry_url}`: {e}"); - } else { - bail!("failed to delete signing key"); - } - } - } -} diff --git a/crates/client/src/keyring/error.rs b/crates/client/src/keyring/error.rs new file mode 100644 index 00000000..594766f6 --- /dev/null +++ b/crates/client/src/keyring/error.rs @@ -0,0 +1,176 @@ +use std::fmt::Display; + +/// Error returned when a keyring operation fails. +#[derive(Debug)] +pub struct KeyringError(KeyringErrorImpl); + +#[derive(Debug)] +enum KeyringErrorImpl { + UnknownBackend { + backend: String, + }, + NoDefaultSigningKey { + backend: &'static str, + }, + AccessError { + backend: &'static str, + entry: KeyringEntry, + action: KeyringAction, + cause: KeyringErrorCause, + }, +} + +#[derive(Debug)] +pub(super) enum KeyringErrorCause { + Backend(keyring::Error), + Other(anyhow::Error), +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum KeyringEntry { + AuthToken { registry: String }, + SigningKey { registry: Option }, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(super) enum KeyringAction { + Open, + Get, + Set, + Delete, +} + +impl From for KeyringErrorCause { + fn from(error: keyring::Error) -> Self { + KeyringErrorCause::Backend(error) + } +} + +impl From for KeyringErrorCause { + fn from(error: anyhow::Error) -> Self { + KeyringErrorCause::Other(error) + } +} + +impl KeyringError { + pub(super) fn unknown_backend(backend: String) -> Self { + KeyringError(KeyringErrorImpl::UnknownBackend { backend }) + } + + pub(super) fn no_default_signing_key(backend: &'static str) -> Self { + KeyringError(KeyringErrorImpl::NoDefaultSigningKey { backend }) + } + + pub(super) fn auth_token_access_error( + backend: &'static str, + registry: &(impl Display + ?Sized), + action: KeyringAction, + cause: impl Into, + ) -> Self { + KeyringError(KeyringErrorImpl::AccessError { + backend, + entry: KeyringEntry::AuthToken { + registry: registry.to_string(), + }, + action, + cause: cause.into(), + }) + } + + pub(super) fn signing_key_access_error( + backend: &'static str, + registry: Option<&(impl Display + ?Sized)>, + action: KeyringAction, + cause: impl Into, + ) -> Self { + KeyringError(KeyringErrorImpl::AccessError { + backend, + entry: KeyringEntry::SigningKey { + registry: registry.map(|s| s.to_string()), + }, + action, + cause: cause.into(), + }) + } +} + +impl std::fmt::Display for KeyringErrorCause { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + KeyringErrorCause::Backend(e) => e.fmt(f), + KeyringErrorCause::Other(e) => e.fmt(f), + } + } +} + +impl std::fmt::Display for KeyringError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "keyring error: ")?; + match &self.0 { + KeyringErrorImpl::UnknownBackend { backend } => { + write!(f, "unknown backend '{backend}'. Run `warg config --keyring_backend ` to configure a keyring backend supported on this platform.") + } + KeyringErrorImpl::NoDefaultSigningKey { backend } => { + let _ = backend; + write!(f, "no default signing key is set. Please create one by running `warg key set ` or `warg key new`") + } + KeyringErrorImpl::AccessError { + backend, + entry, + action, + cause, + } => { + match *action { + KeyringAction::Open => write!(f, "failed to open ")?, + KeyringAction::Get => write!(f, "failed to read ")?, + KeyringAction::Set => write!(f, "failed to set ")?, + KeyringAction::Delete => write!(f, "failed to delete ")?, + }; + match entry { + KeyringEntry::AuthToken { registry } => { + write!(f, "auth token for registry <{registry}>.")? + } + KeyringEntry::SigningKey { + registry: Some(registry), + } => write!(f, "signing key for registry <{registry}>.")?, + KeyringEntry::SigningKey { registry: None } => { + write!(f, "default signing key.")? + } + }; + + if *backend == "secret-service" + && matches!( + cause, + KeyringErrorCause::Backend(keyring::Error::PlatformFailure(_)) + ) + { + write!( + f, + concat!(" Since you are using the 'secret-service' backend, ", + "the likely cause of this error is that no secret service ", + "implementation, such as GNOME Keyring or KWallet, is installed, ", + "or one is installed but not correctly configured. Consult your OS ", + "distribution's documentation for instructions on setting it up, or run ", + "`warg config --keyring_backend ` to use a different backend.") + )?; + } + + // The above will be followed by further information returned + // from `self.source()`. + Ok(()) + } + } + } +} + +impl std::error::Error for KeyringError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match &self.0 { + KeyringErrorImpl::AccessError { cause, .. } => match cause { + KeyringErrorCause::Backend(e) => Some(e), + KeyringErrorCause::Other(e) => Some(e.as_ref()), + }, + _ => None, + } + } +} diff --git a/crates/client/src/keyring/mod.rs b/crates/client/src/keyring/mod.rs new file mode 100644 index 00000000..55eb58d7 --- /dev/null +++ b/crates/client/src/keyring/mod.rs @@ -0,0 +1,312 @@ +//! Utilities for interacting with keyring and performing signing operations. + +use crate::config::Config; +use crate::RegistryUrl; +use indexmap::IndexSet; +use secrecy::Secret; +use warg_crypto::signing::PrivateKey; + +mod error; +use error::KeyringAction; +pub use error::KeyringError; + +/// Interface to a pluggable keyring backend +#[derive(Debug)] +pub struct Keyring { + imp: Box, + name: &'static str, +} + +/// Result type for keyring errors. +pub type Result = std::result::Result; + +impl Keyring { + #[cfg(target_os = "linux")] + /// List of supported credential store backends + pub const SUPPORTED_BACKENDS: &'static [&'static str] = + &["secret-service", "linux-keyutils", "mock"]; + #[cfg(any(target_os = "freebsd", target_os = "openbsd"))] + /// List of supported credential store backends + pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["secret-service", "mock"]; + #[cfg(target_os = "windows")] + /// List of supported credential store backends + pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["windows", "mock"]; + #[cfg(target_os = "macos")] + /// List of supported credential store backends + pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["macos", "mock"]; + #[cfg(target_os = "ios")] + /// List of supported credential store backends + pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["ios", "mock"]; + #[cfg(not(any( + target_os = "linux", + target_os = "freebsd", + target_os = "openbsd", + target_os = "macos", + target_os = "ios", + target_os = "windows", + )))] + /// List of supported credential store backends + pub const SUPPORTED_BACKENDS: &'static [&'static str] = &["mock"]; + + /// The default backend when no configuration option is set + pub const DEFAULT_BACKEND: &'static str = Self::SUPPORTED_BACKENDS[0]; + + /// Returns a human-readable description of a keyring backend. + pub fn describe_backend(backend: &str) -> &'static str { + match backend { + "secret-service" => "Freedesktop.org secret service (GNOME Keyring or KWallet)", + "linux-keyutils" => "Linux kernel memory-based keystore (lacks persistence, not suitable for desktop use)", + "windows" => "Windows Credential Manager", + "macos" => "MacOS Keychain", + "ios" => "Apple iOS Keychain", + "mock" => "Mock credential store with no persistence (for testing only)", + _ => "(no description available)" + } + } + + fn load_backend(backend: &str) -> Option> { + if !Self::SUPPORTED_BACKENDS.contains(&backend) { + return None; + } + + #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "openbsd"))] + if backend == "secret-service" { + return Some(keyring::secret_service::default_credential_builder()); + } + + #[cfg(target_os = "linux")] + if backend == "linux-keyutils" { + return Some(keyring::keyutils::default_credential_builder()); + } + + #[cfg(target_os = "macos")] + if backend == "macos" { + return Some(keyring::macos::default_credential_builder()); + } + + #[cfg(target_os = "ios")] + if backend == "ios" { + return Some(keyring::ios::default_credential_builder()); + } + + #[cfg(target_os = "windows")] + if backend == "windows" { + return Some(keyring::windows::default_credential_builder()); + } + + if backend == "mock" { + return Some(keyring::mock::default_credential_builder()); + } + + unreachable!("missing logic for backend {backend}") + } + + /// Instantiate a new keyring. + /// + /// The argument should be an element of [Self::SUPPORTED_BACKENDS]. + pub fn new(backend: &str) -> Result { + Self::load_backend(backend) + .ok_or_else(|| KeyringError::unknown_backend(backend.to_string())) + .map(|imp| Self { + imp, + // Get an equivalent &'static str from our &str + name: Self::SUPPORTED_BACKENDS + .iter() + .find(|s| **s == backend) + .expect("successfully-loaded backend should be found in SUPPORTED_BACKENDS"), + }) + } + + /// Instantiate a new keyring using the backend specified in a configuration file. + pub fn from_config(config: &Config) -> Result { + if let Some(ref backend) = config.keyring_backend { + Self::new(backend.as_str()) + } else { + Self::new(Self::SUPPORTED_BACKENDS[0]) + } + } + + /// Gets the auth token entry for the given registry and key name. + pub fn get_auth_token_entry(&self, registry_url: &RegistryUrl) -> Result { + let label = format!("warg-auth-token:{}", registry_url.safe_label()); + let cred = self + .imp + .build(None, &label, ®istry_url.safe_label()) + .map_err(|e| { + KeyringError::auth_token_access_error( + self.name, + registry_url, + KeyringAction::Open, + e, + ) + })?; + Ok(keyring::Entry::new_with_credential(cred)) + } + + /// Gets the auth token + pub fn get_auth_token(&self, registry_url: &RegistryUrl) -> Result>> { + let entry = self.get_auth_token_entry(registry_url)?; + match entry.get_password() { + Ok(secret) => Ok(Some(Secret::from(secret))), + Err(keyring::Error::NoEntry) => Ok(None), + Err(e) => Err(KeyringError::auth_token_access_error( + self.name, + registry_url, + KeyringAction::Get, + e, + )), + } + } + + /// Deletes the auth token + pub fn delete_auth_token(&self, registry_url: &RegistryUrl) -> Result<()> { + let entry = self.get_auth_token_entry(registry_url)?; + entry.delete_password().map_err(|e| { + KeyringError::auth_token_access_error(self.name, registry_url, KeyringAction::Delete, e) + }) + } + + /// Sets the auth token + pub fn set_auth_token(&self, registry_url: &RegistryUrl, token: &str) -> Result<()> { + let entry = self.get_auth_token_entry(registry_url)?; + entry.set_password(token).map_err(|e| { + KeyringError::auth_token_access_error(self.name, registry_url, KeyringAction::Set, e) + }) + } + + /// Gets the signing key entry for the given registry and key name. + pub fn get_signing_key_entry( + &self, + registry_url: Option<&str>, + keys: &IndexSet, + home_url: Option<&str>, + ) -> Result { + if let Some(registry_url) = registry_url { + let user = if keys.contains(registry_url) { + registry_url + } else { + "default" + }; + let cred = self + .imp + .build(None, "warg-signing-key", user) + .map_err(|e| { + KeyringError::signing_key_access_error( + self.name, + Some(registry_url), + KeyringAction::Open, + e, + ) + })?; + Ok(keyring::Entry::new_with_credential(cred)) + } else { + if let Some(url) = home_url { + if keys.contains(url) { + let cred = self + .imp + .build( + None, + "warg-signing-key", + &RegistryUrl::new(url) + .map_err(|e| { + KeyringError::signing_key_access_error( + self.name, + Some(url), + KeyringAction::Open, + e, + ) + })? + .safe_label(), + ) + .map_err(|e| { + KeyringError::signing_key_access_error( + self.name, + Some(url), + KeyringAction::Open, + e, + ) + })?; + return Ok(keyring::Entry::new_with_credential(cred)); + } + } + + if keys.contains("default") { + let cred = self + .imp + .build(None, "warg-signing-key", "default") + .map_err(|e| { + KeyringError::signing_key_access_error( + self.name, + None::<&str>, + KeyringAction::Open, + e, + ) + })?; + return Ok(keyring::Entry::new_with_credential(cred)); + } + + Err(KeyringError::no_default_signing_key(self.name)) + } + } + + /// Gets the signing key for the given registry registry_label and key name. + pub fn get_signing_key( + &self, + // If being called by a cli key command, this will always be a cli flag + // If being called by a client publish command, this could also be supplied by namespace map config + registry_url: Option<&str>, + keys: &IndexSet, + home_url: Option<&str>, + ) -> Result { + let entry = self.get_signing_key_entry(registry_url, keys, home_url)?; + + match entry.get_password() { + Ok(secret) => PrivateKey::decode(secret).map_err(|e| { + KeyringError::signing_key_access_error( + self.name, + registry_url, + KeyringAction::Get, + anyhow::Error::from(e), + ) + }), + Err(e) => Err(KeyringError::signing_key_access_error( + self.name, + registry_url, + KeyringAction::Get, + e, + )), + } + } + + /// Sets the signing key for the given registry host and key name. + pub fn set_signing_key( + &self, + registry_url: Option<&str>, + key: &PrivateKey, + keys: &mut IndexSet, + home_url: Option<&str>, + ) -> Result<()> { + let entry = self.get_signing_key_entry(registry_url, keys, home_url)?; + entry.set_password(&key.encode()).map_err(|e| { + KeyringError::signing_key_access_error(self.name, registry_url, KeyringAction::Set, e) + }) + } + + /// Deletes the signing key for the given registry host and key name. + pub fn delete_signing_key( + &self, + registry_url: Option<&str>, + keys: &IndexSet, + home_url: Option<&str>, + ) -> Result<()> { + let entry = self.get_signing_key_entry(registry_url, keys, home_url)?; + entry.delete_password().map_err(|e| { + KeyringError::signing_key_access_error( + self.name, + registry_url, + KeyringAction::Delete, + e, + ) + }) + } +} diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 1dfd8369..337aa4df 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -1346,7 +1346,7 @@ impl FileSystemClient { #[cfg(feature = "keyring")] if auth_token.is_none() && config.keyring_auth { - auth_token = crate::keyring::get_auth_token(&url)? + auth_token = crate::keyring::Keyring::from_config(config)?.get_auth_token(&url)? } Ok(StorageLockResult::Acquired(Self::new( @@ -1401,7 +1401,8 @@ impl FileSystemClient { #[cfg(feature = "keyring")] if auth_token.is_none() && config.keyring_auth { - auth_token = crate::keyring::get_auth_token(®istry_url)? + auth_token = + crate::keyring::Keyring::from_config(config)?.get_auth_token(®istry_url)? } Self::new( @@ -1635,6 +1636,10 @@ pub enum ClientError { log_length: RegistryLen, }, + /// An error occurred while accessing the keyring. + #[error(transparent)] + Keyring(#[from] crate::keyring::KeyringError), + /// An error occurred during an API operation. #[error(transparent)] Api(#[from] api::ClientError), diff --git a/src/commands.rs b/src/commands.rs index cc159d16..07be59b0 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -3,7 +3,7 @@ use anyhow::Result; use clap::Args; use std::path::PathBuf; -use warg_client::keyring::get_signing_key; +use warg_client::keyring::Keyring; use warg_client::storage::RegistryDomain; use warg_client::{ClientError, Config, FileSystemClient, StorageLockResult}; use warg_crypto::signing::PrivateKey; @@ -88,10 +88,11 @@ impl CommonOptions { registry_domain: Option<&RegistryDomain>, ) -> Result { let config = self.read_config()?; - get_signing_key( + let key = Keyring::from_config(&config)?.get_signing_key( registry_domain.map(|domain| domain.to_string()).as_deref(), &config.keys, config.home_url.as_deref(), - ) + )?; + Ok(key) } } diff --git a/src/commands/config.rs b/src/commands/config.rs index c584f57f..31103da3 100644 --- a/src/commands/config.rs +++ b/src/commands/config.rs @@ -2,7 +2,7 @@ use super::CommonOptions; use anyhow::{bail, Context, Result}; use clap::Args; use std::path::PathBuf; -use warg_client::{Config, RegistryUrl}; +use warg_client::{keyring::Keyring, Config, RegistryUrl}; /// Creates a new warg configuration file. #[derive(Args)] @@ -40,6 +40,10 @@ pub struct ConfigCommand { /// The path to the namespace map #[clap(long, value_name = "NAMESPACE_PATH")] pub namespace_path: Option, + + /// The backend to use for keyring access + #[clap(long, value_name = "KEYRING_BACKEND", value_parser = keyring_backend_parser, long_help = keyring_backend_help())] + pub keyring_backend: Option, } impl ConfigCommand { @@ -81,6 +85,7 @@ impl ConfigCommand { ignore_federation_hints: self.ignore_federation_hints, auto_accept_federation_hints: self.auto_accept_federation_hints, disable_interactive: false, + keyring_backend: self.keyring_backend.clone(), }; config.write_to_file(&path)?; @@ -98,3 +103,40 @@ impl ConfigCommand { Ok(()) } } + +fn keyring_backend_parser(s: &str) -> Result { + if Keyring::SUPPORTED_BACKENDS.contains(&s) { + Ok(s.to_string()) + } else { + Err(format!("`{s}` is not a supported keyring backend.")) + } +} + +fn keyring_backend_help() -> clap::builder::StyledStr { + use std::fmt::Write as _; + + let mut help = String::new(); + + writeln!( + &mut help, + "The backend to use for keyring access. The following options are supported:\n" + ) + .unwrap(); + for backend in Keyring::SUPPORTED_BACKENDS { + writeln!( + &mut help, + "{:16} {}", + backend, + Keyring::describe_backend(backend) + ) + .unwrap(); + } + writeln!( + &mut help, + "\nThe default is `{}`.", + Keyring::DEFAULT_BACKEND + ) + .unwrap(); + + help.into() +} diff --git a/src/commands/key.rs b/src/commands/key.rs index e63d8a8c..8d80ade3 100644 --- a/src/commands/key.rs +++ b/src/commands/key.rs @@ -3,10 +3,8 @@ use clap::{Args, Subcommand}; use dialoguer::{theme::ColorfulTheme, Confirm, Password}; use p256::ecdsa::SigningKey; use rand_core::OsRng; -use warg_client::{ - keyring::{delete_signing_key, get_signing_key, set_signing_key}, - Config, -}; +use warg_client::keyring::Keyring; +use warg_client::Config; use warg_crypto::signing::PrivateKey; use super::CommonOptions; @@ -62,7 +60,7 @@ impl KeyNewCommand { } else { config.keys.insert("default".to_string()); } - set_signing_key( + Keyring::from_config(config)?.set_signing_key( self.common.registry.as_deref(), &key, &mut config.keys, @@ -88,7 +86,7 @@ impl KeyInfoCommand { /// Executes the command. pub async fn exec(self) -> Result<()> { let config = &self.common.read_config()?; - let private_key = get_signing_key( + let private_key = Keyring::from_config(config)?.get_signing_key( self.common.registry.as_deref(), &config.keys, config.home_url.as_deref(), @@ -119,7 +117,7 @@ impl KeySetCommand { PrivateKey::decode(key_str).context("signing key is not in the correct format")?; let config = &mut self.common.read_config()?; - set_signing_key( + Keyring::from_config(config)?.set_signing_key( self.common.registry.as_deref(), &key, &mut config.keys, @@ -150,7 +148,7 @@ impl KeyDeleteCommand { .with_prompt("are you sure you want to delete your signing key") .interact()? { - delete_signing_key( + Keyring::from_config(config)?.delete_signing_key( self.common.registry.as_deref(), &config.keys, config.home_url.as_deref(), diff --git a/src/commands/login.rs b/src/commands/login.rs index 977113bb..fe218c98 100644 --- a/src/commands/login.rs +++ b/src/commands/login.rs @@ -3,10 +3,8 @@ use clap::Args; use dialoguer::{theme::ColorfulTheme, Password}; use p256::ecdsa::SigningKey; use rand_core::OsRng; -use warg_client::{ - keyring::{set_auth_token, set_signing_key}, - Config, RegistryUrl, -}; +use warg_client::keyring::Keyring; +use warg_client::{Config, RegistryUrl}; use super::CommonOptions; @@ -38,14 +36,15 @@ struct KeyringEntryArgs { } impl KeyringEntryArgs { - fn set_entry(&self, home_url: Option, token: &str) -> Result<()> { + fn set_entry(&self, keyring: &Keyring, home_url: Option, token: &str) -> Result<()> { if let Some(url) = &self.url { - set_auth_token(url, token) + keyring.set_auth_token(url, token)?; } else if let Some(url) = &home_url { - set_auth_token(&RegistryUrl::new(url)?, token) + keyring.set_auth_token(&RegistryUrl::new(url)?, token)?; } else { bail!("Please configure your home registry: warg config --registry ") } + Ok(()) } } @@ -62,6 +61,7 @@ impl LoginCommand { let mut config = self.common.read_config()?; config.ignore_federation_hints = self.ignore_federation_hints; config.auto_accept_federation_hints = self.auto_accept_federation_hints; + let keyring = Keyring::from_config(&config)?; if home_url.is_some() { config.home_url.clone_from(home_url); @@ -78,14 +78,14 @@ impl LoginCommand { if config.keys.is_empty() { config.keys.insert("default".to_string()); let key = SigningKey::random(&mut OsRng).into(); - set_signing_key(None, &key, &mut config.keys, config.home_url.as_deref())?; + keyring.set_signing_key(None, &key, &mut config.keys, config.home_url.as_deref())?; let public_key = key.public_key(); let token = Password::with_theme(&ColorfulTheme::default()) .with_prompt("Enter auth token") .interact() .context("failed to read token")?; self.keyring_entry - .set_entry(self.common.read_config()?.home_url, &token)?; + .set_entry(&keyring, self.common.read_config()?.home_url, &token)?; config.write_to_file(&Config::default_config_path()?)?; println!("auth token was set successfully, and generated default key",); println!("Public Key: {public_key}"); @@ -97,7 +97,7 @@ impl LoginCommand { .interact() .context("failed to read token")?; self.keyring_entry - .set_entry(self.common.read_config()?.home_url, &token)?; + .set_entry(&keyring, self.common.read_config()?.home_url, &token)?; config.write_to_file(&Config::default_config_path()?)?; println!("auth token was set successfully",); Ok(()) diff --git a/src/commands/logout.rs b/src/commands/logout.rs index cfb435ad..4d0c99b0 100644 --- a/src/commands/logout.rs +++ b/src/commands/logout.rs @@ -1,6 +1,7 @@ use anyhow::{bail, Result}; use clap::Args; -use warg_client::{keyring::delete_auth_token, Config, RegistryUrl}; +use warg_client::{Config, RegistryUrl}; +use warg_credentials::keyring::Keyring; use super::CommonOptions; @@ -23,23 +24,25 @@ struct KeyringEntryArgs { } impl KeyringEntryArgs { - fn delete_entry(&self, home_url: Option) -> Result<()> { + fn delete_entry(&self, keyring: &Keyring, home_url: Option) -> Result<()> { if let Some(url) = &self.url { - delete_auth_token(url) + keyring.delete_auth_token(url)?; } else if let Some(url) = &home_url { - delete_auth_token(&RegistryUrl::new(url)?) + keyring.delete_auth_token(&RegistryUrl::new(url)?)?; } else { bail!("Please configure your home registry: warg config --registry ") } + Ok(()) } } impl LogoutCommand { /// Executes the command. pub async fn exec(self) -> Result<()> { - self.keyring_entry - .delete_entry(self.common.read_config()?.home_url)?; let mut config = self.common.read_config()?; + let keyring = Keyring::from_config(&config)?; + self.keyring_entry + .delete_entry(&keyring, config.home_url.clone())?; config.keyring_auth = false; config.write_to_file(&Config::default_config_path()?)?; println!("auth token was deleted successfully",); diff --git a/tests/support/mod.rs b/tests/support/mod.rs index b5c787b6..a2954537 100644 --- a/tests/support/mod.rs +++ b/tests/support/mod.rs @@ -170,6 +170,7 @@ pub async fn spawn_server( ignore_federation_hints: false, auto_accept_federation_hints: false, disable_interactive: true, + keyring_backend: None, }; Ok((instance, config))