cli: Give passphrase error on prompt

Example:

    ✗ Invalid passphrase, please try again
    ? Passphrase: ***
This commit is contained in:
Alexis Sellier 2023-09-02 13:56:56 +02:00 committed by cloudhead
parent 5040f33025
commit 579ddfe725
No known key found for this signature in database
5 changed files with 58 additions and 6 deletions

View File

@ -177,10 +177,11 @@ pub fn authenticate(options: Options, profile: &Profile) -> anyhow::Result<()> {
term::format::Identity::new(profile).styled() term::format::Identity::new(profile).styled()
); );
let validator = term::io::PassphraseValidator::new(profile.keystore.clone());
let passphrase = if options.stdin { let passphrase = if options.stdin {
term::passphrase_stdin()? term::passphrase_stdin()?
} else { } else {
term::passphrase(RAD_PASSPHRASE)? term::passphrase(RAD_PASSPHRASE, validator)?
}; };
register(&mut agent, profile, passphrase)?; register(&mut agent, profile, passphrase)?;

View File

@ -30,7 +30,8 @@ pub fn start(
let envs = if profile.keystore.is_encrypted()? { let envs = if profile.keystore.is_encrypted()? {
// Ask passphrase here, otherwise it'll be a fatal error when running the daemon // Ask passphrase here, otherwise it'll be a fatal error when running the daemon
// without `RAD_PASSPHRASE`. // 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"); anyhow::bail!("your radicle passphrase is required to start your node");
}; };
Some((profile::env::RAD_PASSPHRASE, passphrase)) Some((profile::env::RAD_PASSPHRASE, passphrase))

View File

@ -1,19 +1,51 @@
use radicle::cob::issue::Issue; use radicle::cob::issue::Issue;
use radicle::cob::thread::{Comment, CommentId}; use radicle::cob::thread::{Comment, CommentId};
use radicle::crypto::ssh::keystore::MemorySigner; 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::env::RAD_PASSPHRASE;
use radicle::profile::Profile; use radicle::profile::Profile;
pub use radicle_term::io::*; pub use radicle_term::io::*;
pub use radicle_term::spinner; 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<validator::Validation, inquire::error::CustomUserError> {
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. /// Get the signer. First we try getting it from ssh-agent, otherwise we prompt the user.
pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> { pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
if let Ok(signer) = profile.signer() { if let Ok(signer) = profile.signer() {
return Ok(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 spinner = spinner("Unsealing key...");
let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?; let signer = MemorySigner::load(&profile.keystore, Some(passphrase))?;

View File

@ -138,6 +138,19 @@ impl Keystore {
} }
} }
/// Check that the passphrase is valid.
pub fn is_valid_passphrase(&self, passphrase: &Passphrase) -> Result<bool, Error> {
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. /// Check whether the secret key is encrypted.
pub fn is_encrypted(&self) -> Result<bool, Error> { pub fn is_encrypted(&self) -> Result<bool, Error> {
let path = self.path.join("radicle"); let path = self.path.join("radicle");

View File

@ -2,6 +2,7 @@ use std::ffi::OsStr;
use std::{env, fmt, io, process}; use std::{env, fmt, io, process};
use inquire::ui::{ErrorMessageRenderConfig, StyleSheet, Styled}; use inquire::ui::{ErrorMessageRenderConfig, StyleSheet, Styled};
use inquire::validator;
use inquire::InquireError; use inquire::InquireError;
use inquire::{ui::Color, ui::RenderConfig, Confirm, CustomType, Password}; use inquire::{ui::Color, ui::RenderConfig, Confirm, CustomType, Password};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
@ -11,7 +12,7 @@ use crate::command;
use crate::format; use crate::format;
use crate::{style, Paint}; use crate::{style, Paint};
// TODO: Try not to export this. pub use inquire;
pub use inquire::Select; pub use inquire::Select;
pub const ERROR_PREFIX: Paint<&str> = Paint::red(""); pub const ERROR_PREFIX: Paint<&str> = Paint::red("");
@ -199,7 +200,10 @@ where
Ok(value) Ok(value)
} }
pub fn passphrase<K: AsRef<OsStr>>(var: K) -> Result<Passphrase, inquire::InquireError> { pub fn passphrase<K: AsRef<OsStr>, V: validator::StringValidator + 'static>(
var: K,
validate: V,
) -> Result<Passphrase, inquire::InquireError> {
if let Ok(p) = env::var(var) { if let Ok(p) = env::var(var) {
Ok(Passphrase::from(p)) Ok(Passphrase::from(p))
} else { } else {
@ -208,6 +212,7 @@ pub fn passphrase<K: AsRef<OsStr>>(var: K) -> Result<Passphrase, inquire::Inquir
.with_render_config(*CONFIG) .with_render_config(*CONFIG)
.with_display_mode(inquire::PasswordDisplayMode::Masked) .with_display_mode(inquire::PasswordDisplayMode::Masked)
.without_confirmation() .without_confirmation()
.with_validator(validate)
.prompt()?, .prompt()?,
)) ))
} }