From 579ddfe72535fb65ac4c5cdddf66d30c683badf6 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Sat, 2 Sep 2023 13:56:56 +0200 Subject: [PATCH] cli: Give passphrase error on prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Example: ✗ Invalid passphrase, please try again ? Passphrase: *** --- radicle-cli/src/commands/auth.rs | 3 +- radicle-cli/src/commands/node/control.rs | 3 +- radicle-cli/src/terminal/io.rs | 36 ++++++++++++++++++++++-- radicle-crypto/src/ssh/keystore.rs | 13 +++++++++ radicle-term/src/io.rs | 9 ++++-- 5 files changed, 58 insertions(+), 6 deletions(-) diff --git a/radicle-cli/src/commands/auth.rs b/radicle-cli/src/commands/auth.rs index ed289a90..87f3c506 100644 --- a/radicle-cli/src/commands/auth.rs +++ b/radicle-cli/src/commands/auth.rs @@ -177,10 +177,11 @@ pub fn authenticate(options: Options, profile: &Profile) -> anyhow::Result<()> { term::format::Identity::new(profile).styled() ); + let validator = term::io::PassphraseValidator::new(profile.keystore.clone()); let passphrase = if options.stdin { term::passphrase_stdin()? } else { - term::passphrase(RAD_PASSPHRASE)? + term::passphrase(RAD_PASSPHRASE, validator)? }; register(&mut agent, profile, passphrase)?; diff --git a/radicle-cli/src/commands/node/control.rs b/radicle-cli/src/commands/node/control.rs index e4183f5c..c6118ae3 100644 --- a/radicle-cli/src/commands/node/control.rs +++ b/radicle-cli/src/commands/node/control.rs @@ -30,7 +30,8 @@ pub fn start( let envs = if profile.keystore.is_encrypted()? { // Ask passphrase here, otherwise it'll be a fatal error when running the daemon // without `RAD_PASSPHRASE`. - let Ok(passphrase) = term::io::passphrase(profile::env::RAD_PASSPHRASE) else { + let validator = term::io::PassphraseValidator::new(profile.keystore.clone()); + let Ok(passphrase) = term::io::passphrase(profile::env::RAD_PASSPHRASE, validator) else { anyhow::bail!("your radicle passphrase is required to start your node"); }; Some((profile::env::RAD_PASSPHRASE, passphrase)) diff --git a/radicle-cli/src/terminal/io.rs b/radicle-cli/src/terminal/io.rs index 5774e711..ccb24faf 100644 --- a/radicle-cli/src/terminal/io.rs +++ b/radicle-cli/src/terminal/io.rs @@ -1,19 +1,51 @@ use radicle::cob::issue::Issue; use radicle::cob::thread::{Comment, CommentId}; use radicle::crypto::ssh::keystore::MemorySigner; -use radicle::crypto::Signer; +use radicle::crypto::{ssh::Keystore, Signer}; use radicle::profile::env::RAD_PASSPHRASE; use radicle::profile::Profile; pub use radicle_term::io::*; pub use radicle_term::spinner; +use inquire::validator; + +/// Validates secret key passphrases. +#[derive(Clone)] +pub struct PassphraseValidator { + keystore: Keystore, +} + +impl PassphraseValidator { + /// Create a new validator. + pub fn new(keystore: Keystore) -> Self { + Self { keystore } + } +} + +impl inquire::validator::StringValidator for PassphraseValidator { + fn validate( + &self, + input: &str, + ) -> Result { + let passphrase = Passphrase::from(input.to_owned()); + if self.keystore.is_valid_passphrase(&passphrase)? { + Ok(validator::Validation::Valid) + } else { + Ok(validator::Validation::Invalid( + validator::ErrorMessage::from("Invalid passphrase, please try again"), + )) + } + } +} + /// Get the signer. First we try getting it from ssh-agent, otherwise we prompt the user. pub fn signer(profile: &Profile) -> anyhow::Result> { if let Ok(signer) = profile.signer() { return Ok(signer); } - let passphrase = passphrase(RAD_PASSPHRASE)?; + let validator = PassphraseValidator::new(profile.keystore.clone()); + let passphrase = passphrase(RAD_PASSPHRASE, validator)?; let spinner = spinner("Unsealing key..."); let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?; diff --git a/radicle-crypto/src/ssh/keystore.rs b/radicle-crypto/src/ssh/keystore.rs index b29ee938..d45d41c9 100644 --- a/radicle-crypto/src/ssh/keystore.rs +++ b/radicle-crypto/src/ssh/keystore.rs @@ -138,6 +138,19 @@ impl Keystore { } } + /// Check that the passphrase is valid. + pub fn is_valid_passphrase(&self, passphrase: &Passphrase) -> Result { + let path = self.path.join("radicle"); + if !path.exists() { + return Err(Error::Io(io::ErrorKind::NotFound.into())); + } + + let secret = ssh_key::PrivateKey::read_openssh_file(&path)?; + let valid = secret.decrypt(passphrase).is_ok(); + + Ok(valid) + } + /// Check whether the secret key is encrypted. pub fn is_encrypted(&self) -> Result { let path = self.path.join("radicle"); diff --git a/radicle-term/src/io.rs b/radicle-term/src/io.rs index 5c450656..13612009 100644 --- a/radicle-term/src/io.rs +++ b/radicle-term/src/io.rs @@ -2,6 +2,7 @@ use std::ffi::OsStr; use std::{env, fmt, io, process}; use inquire::ui::{ErrorMessageRenderConfig, StyleSheet, Styled}; +use inquire::validator; use inquire::InquireError; use inquire::{ui::Color, ui::RenderConfig, Confirm, CustomType, Password}; use once_cell::sync::Lazy; @@ -11,7 +12,7 @@ use crate::command; use crate::format; use crate::{style, Paint}; -// TODO: Try not to export this. +pub use inquire; pub use inquire::Select; pub const ERROR_PREFIX: Paint<&str> = Paint::red("✗"); @@ -199,7 +200,10 @@ where Ok(value) } -pub fn passphrase>(var: K) -> Result { +pub fn passphrase, V: validator::StringValidator + 'static>( + var: K, + validate: V, +) -> Result { if let Ok(p) = env::var(var) { Ok(Passphrase::from(p)) } else { @@ -208,6 +212,7 @@ pub fn passphrase>(var: K) -> Result