Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Give passphrase error on prompt
Alexis Sellier committed 2 years ago
commit 579ddfe72535fb65ac4c5cdddf66d30c683badf6
parent 5040f33025d6ba7a68dca5f9e1eb8053888b623b
5 files changed +58 -6
modified 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)?;

modified 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))
modified 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<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.
pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
    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))?;

modified 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<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.
    pub fn is_encrypted(&self) -> Result<bool, Error> {
        let path = self.path.join("radicle");
modified 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<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) {
        Ok(Passphrase::from(p))
    } else {
@@ -208,6 +212,7 @@ pub fn passphrase<K: AsRef<OsStr>>(var: K) -> Result<Passphrase, inquire::Inquir
                .with_render_config(*CONFIG)
                .with_display_mode(inquire::PasswordDisplayMode::Masked)
                .without_confirmation()
+
                .with_validator(validate)
                .prompt()?,
        ))
    }