From 03d5c8020ed3c87a6f6e49caf731f9bfd2719ef8 Mon Sep 17 00:00:00 2001 From: Awiteb Date: Sun, 27 Jul 2025 00:47:18 +0000 Subject: [PATCH] feat: Keyring the secret key `n34 config keyring --enable` Signed-off-by: Awiteb --- CHANGELOG.md | 1 + src/cli/commands/config/keyring.rs | 63 ++++++++++++++++++++++++++++++ src/cli/commands/config/mod.rs | 8 +++- src/cli/commands/mod.rs | 10 ++++- src/cli/config.rs | 13 +++--- src/cli/mod.rs | 45 +++++++++++++++++---- src/error.rs | 5 +++ src/nostr_utils/traits.rs | 9 +++++ 8 files changed, 139 insertions(+), 15 deletions(-) create mode 100644 src/cli/commands/config/keyring.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 89ce01c..1437b8d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Support signing using NIP-46 bunker - by Awiteb +- Keyring the secret key `n34 config keyring --enable` - by Awiteb ### Dependencies diff --git a/src/cli/commands/config/keyring.rs b/src/cli/commands/config/keyring.rs new file mode 100644 index 0000000..6e27acc --- /dev/null +++ b/src/cli/commands/config/keyring.rs @@ -0,0 +1,63 @@ +// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr +// Copyright (C) 2025 Awiteb +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clap::{ArgGroup, Args}; + +use crate::{ + cli::{Cli, CliOptions, traits::CommandRunner}, + error::N34Result, +}; + +#[derive(Args, Debug)] +#[clap( + group( + ArgGroup::new("options") + .required(true) + ) +)] +pub struct KeyringArgs { + /// Turns on secret key keyring. Requires entering the key once when + /// enabled. + #[arg(long, group = "options")] + enable: bool, + /// Turns off secret key keyring. Removes any existing key and prevents + /// storing new ones. + #[arg(long, group = "options")] + disable: bool, + /// Deletes current key and stores the next provided key. + #[arg(long, group = "options")] + reset: bool, +} + +impl CommandRunner for KeyringArgs { + const NEED_SIGNER: bool = false; + + async fn run(self, mut options: CliOptions) -> N34Result<()> { + let keyring = nostr_keyring::NostrKeyring::new(Cli::N34_KEYRING_SERVICE_NAME); + + if self.enable { + options.config.keyring_secret_key = true; + } else if self.disable { + options.config.keyring_secret_key = false; + } + + if self.reset || self.disable { + let _ = keyring.delete(Cli::USER_KEY_PAIR_ENTRY); + } + + options.config.dump() + } +} diff --git a/src/cli/commands/config/mod.rs b/src/cli/commands/config/mod.rs index 2cb2c45..24968be 100644 --- a/src/cli/commands/config/mod.rs +++ b/src/cli/commands/config/mod.rs @@ -16,6 +16,8 @@ /// `config bunker` subcommand mod bunker; +/// `config keyring` subcommand +mod keyring; /// `config pow` subcommand mod pow; /// `config relays` subcommand @@ -24,6 +26,7 @@ mod relays; use clap::Subcommand; use self::bunker::BunkerArgs; +use self::keyring::KeyringArgs; use self::pow::PowArgs; use self::relays::RelaysArgs; use super::CliOptions; @@ -39,10 +42,13 @@ pub enum ConfigSubcommands { Relays(RelaysArgs), /// Sets a URL of NIP-46 bunker server used for signing events. Bunker(BunkerArgs), + /// Managing the secret key keyring, including enabling, disabling, or + /// resetting it. + Keyring(KeyringArgs), } impl CommandRunner for ConfigSubcommands { async fn run(self, options: CliOptions) -> N34Result<()> { - crate::run_command!(self, options, & Pow Relays Bunker) + crate::run_command!(self, options, & Pow Relays Bunker Keyring) } } diff --git a/src/cli/commands/mod.rs b/src/cli/commands/mod.rs index b27692c..5fa3341 100644 --- a/src/cli/commands/mod.rs +++ b/src/cli/commands/mod.rs @@ -137,9 +137,16 @@ impl CliOptions { /// Returns the signer pub fn signer(&self) -> N34Result>> { + if self.config.keyring_secret_key { + return Ok(Some( + Cli::user_keypair(self.secret_key.clone())?.into_nostr_signer(), + )); + } + if let Some(sk) = &self.secret_key { return Ok(Some(Keys::new(sk.clone()).into_nostr_signer())); } + if let Some(ref bunker_url) = self.bunker_url { let mut nostrconnect = NostrConnect::new( bunker_url.clone(), @@ -165,7 +172,8 @@ impl CliOptions { /// Returns an error if there are no signers pub fn ensure_signer(&self) -> N34Result<()> { - if self.secret_key.is_none() && self.bunker_url.is_none() { + if !self.config.keyring_secret_key && self.secret_key.is_none() && self.bunker_url.is_none() + { return Err(N34Error::SignerRequired); } Ok(()) diff --git a/src/cli/config.rs b/src/cli/config.rs index 4e4b882..9e70476 100644 --- a/src/cli/config.rs +++ b/src/cli/config.rs @@ -52,16 +52,16 @@ pub enum ConfigError { pub struct CliConfig { /// Path to the configuration file (not serialized) #[serde(skip)] - path: PathBuf, + path: PathBuf, /// Groups of repositories and relays. #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub sets: Vec, + pub sets: Vec, /// The default PoW difficulty #[serde(skip_serializing_if = "Option::is_none")] - pub pow: Option, + pub pow: Option, /// List of fallback relays used if no fallback relays was provided. #[serde(skip_serializing_if = "Option::is_none")] - pub fallback_relays: Option>, + pub fallback_relays: Option>, /// Default Nostr bunker URL used for signing events. #[serde( default, @@ -69,7 +69,10 @@ pub struct CliConfig { deserialize_with = "super::parsers::de_bunker_url", serialize_with = "super::parsers::ser_bunker_url" )] - pub bunker_url: Option, + pub bunker_url: Option, + /// Whether to use the system keyring to store the secret key. + #[serde(default)] + pub keyring_secret_key: bool, } /// A named group of repositories and relays. diff --git a/src/cli/mod.rs b/src/cli/mod.rs index f9215f1..4539c3b 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -34,6 +34,7 @@ pub mod types; use clap::Parser; use clap_verbosity_flag::Verbosity; use nostr::key::Keys; +use nostr::key::SecretKey; use nostr_keyring::NostrKeyring; use types::RelayOrSet; @@ -42,6 +43,7 @@ pub use self::config::*; use self::traits::CommandRunner; use crate::error::N34Error; use crate::error::N34Result; +use crate::nostr_utils::traits::NostrKeyringErrorUtils; /// Header message, used in the help message const HEADER: &str = r#"Copyright (C) 2025 Awiteb @@ -54,11 +56,6 @@ Git repository: https://git.4rs.nl/awiteb/n34.git"#; /// Footer message, used in the help message const FOOTER: &str = r#"Please report bugs to ."#; -/// Keyring service name of n34 -const N34_KEYRING_SERVICE_NAME: &str = "n34"; - -/// Keyring entry name for the n34 keypair -const N34_KEY_PAIR_ENTRY: &str = "n34_keypair"; /// Name of the file storing the repository address pub const NOSTR_ADDRESS_FILE: &str = "nostr-address"; @@ -80,6 +77,13 @@ pub struct Cli { impl Cli { + /// Keyring service name of n34 + pub const N34_KEYRING_SERVICE_NAME: &str = "n34"; + /// Keyring entry name of the n34 keypair + pub const N34_KEY_PAIR_ENTRY: &str = "n34_keypair"; + /// Keyring entry name of the user secret key + pub const USER_KEY_PAIR_ENTRY: &str = "user_keypair"; + /// Executes the command pub async fn run(self) -> N34Result<()> { self.command.run(self.options).await @@ -88,18 +92,43 @@ impl Cli { /// Gets the n34 keypair from the keyring or generates and stores a new one /// if none exists. pub fn n34_keypair() -> N34Result { - let keyring = NostrKeyring::new(N34_KEYRING_SERVICE_NAME); + let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME); - match keyring.get(N34_KEY_PAIR_ENTRY) { + match keyring.get(Self::N34_KEY_PAIR_ENTRY) { Ok(keys) => Ok(keys), Err(nostr_keyring::Error::Keyring(keyring::Error::NoEntry)) => { let new_keys = Keys::generate(); - keyring.set(N34_KEY_PAIR_ENTRY, &new_keys)?; + keyring.set(Self::N34_KEY_PAIR_ENTRY, &new_keys)?; Ok(new_keys) } Err(err) => Err(N34Error::Keyring(err)), } } + + /// Retrieves the user's keypair from the keyring. If no key exists and one + /// is provided, stores and returns it. If no key exists and none is + /// provided, returns an error. + pub fn user_keypair(secret_key: Option) -> N34Result { + let keyring = NostrKeyring::new(Self::N34_KEYRING_SERVICE_NAME); + let keyring_key = keyring.get(Self::USER_KEY_PAIR_ENTRY); + + if let Err(ref err) = keyring_key + && err.is_keyring_no_entry() + && let Some(secret_key) = secret_key + { + let keypair = Keys::new(secret_key); + keyring.set(Self::USER_KEY_PAIR_ENTRY, &keypair)?; + return Ok(keypair); + } + + keyring_key.map_err(|err| { + if err.is_keyring_no_entry() { + N34Error::SecretKeyKeyringWithoutEntry + } else { + N34Error::Keyring(err) + } + }) + } } /// Processes the CLI configuration by applying fallback values from config if diff --git a/src/error.rs b/src/error.rs index fbaedd5..ddac1c1 100644 --- a/src/error.rs +++ b/src/error.rs @@ -134,6 +134,11 @@ pub enum N34Error { InvalidStatus(String), #[error("Not valid bunker URL")] NotBunkerUrl, + #[error( + "No secret key found in the keyring. Please use the secret key at least once while \ + keyring is enabled to store it" + )] + SecretKeyKeyringWithoutEntry, } impl N34Error { diff --git a/src/nostr_utils/traits.rs b/src/nostr_utils/traits.rs index 7fb853f..712a10b 100644 --- a/src/nostr_utils/traits.rs +++ b/src/nostr_utils/traits.rs @@ -300,3 +300,12 @@ impl Event { .join(", ") } } + +#[easy_ext::ext(NostrKeyringErrorUtils)] +impl nostr_keyring::Error { + /// Checks if the error indicates a missing keyring entry. + #[inline] + pub fn is_keyring_no_entry(&self) -> bool { + matches!(self, nostr_keyring::Error::Keyring(keyring::Error::NoEntry)) + } +}