feat: Keyring the secret key n34 config keyring --enable

Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
Awiteb
2025-07-27 00:47:18 +00:00
parent c3aef2e0ad
commit 03d5c8020e
8 changed files with 139 additions and 15 deletions

View File

@@ -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

View File

@@ -0,0 +1,63 @@
// n34 - A CLI to interact with NIP-34 and other stuff related to codes in nostr
// Copyright (C) 2025 Awiteb <a@4rs.nl>
//
// 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 <https://gnu.org/licenses/gpl-3.0.html>.
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()
}
}

View File

@@ -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)
}
}

View File

@@ -137,9 +137,16 @@ impl CliOptions {
/// Returns the signer
pub fn signer(&self) -> N34Result<Option<Arc<dyn NostrSigner + 'static>>> {
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(())

View File

@@ -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<RepoRelaySet>,
pub sets: Vec<RepoRelaySet>,
/// The default PoW difficulty
#[serde(skip_serializing_if = "Option::is_none")]
pub pow: Option<u8>,
pub pow: Option<u8>,
/// List of fallback relays used if no fallback relays was provided.
#[serde(skip_serializing_if = "Option::is_none")]
pub fallback_relays: Option<Vec<RelayUrl>>,
pub fallback_relays: Option<Vec<RelayUrl>>,
/// 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<NostrConnectURI>,
pub bunker_url: Option<NostrConnectURI>,
/// 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.

View File

@@ -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 <a@4rs.nl>
@@ -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 <naddr1qqpkuve5qgsqqqqqq9g9uljgjfcyd6dm4fegk8em2yfz0c3qp3tc6mntkrrhawgrqsqqqauesksc39>."#;
/// 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<Keys> {
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<SecretKey>) -> N34Result<Keys> {
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

View File

@@ -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 {

View File

@@ -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))
}
}