feat: Keyring the secret key n34 config keyring --enable
Signed-off-by: Awiteb <a@4rs.nl>
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
63
src/cli/commands/config/keyring.rs
Normal file
63
src/cli/commands/config/keyring.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
|
||||
@@ -70,6 +70,9 @@ pub struct CliConfig {
|
||||
serialize_with = "super::parsers::ser_bunker_url"
|
||||
)]
|
||||
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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user