diff --git a/Cargo.lock b/Cargo.lock index 7ad02a4..a8fc834 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1111,7 +1111,7 @@ dependencies = [ "card-backend", "card-backend-pcsc", "keyfork-derive-openpgp", - "keyfork-pinentry", + "keyfork-prompt", "openpgp-card", "openpgp-card-sequoia", "sequoia-openpgp", diff --git a/keyfork-prompt/src/lib.rs b/keyfork-prompt/src/lib.rs index 9d30181..4ab5521 100644 --- a/keyfork-prompt/src/lib.rs +++ b/keyfork-prompt/src/lib.rs @@ -52,8 +52,18 @@ where let mut terminal = AlternateScreen::new(&mut self.write)?; terminal .queue(terminal::Clear(terminal::ClearType::All))? - .queue(Print(prompt))? - .flush()?; + .queue(cursor::MoveTo(0, 0))?; + let mut lines = prompt.lines().peekable(); + while let Some(line) = lines.next() { + terminal.queue(Print(line))?; + if lines.peek().is_some() { + terminal + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } + } + terminal.flush()?; + let mut line = String::new(); self.read.read_line(&mut line)?; Ok(line) @@ -63,10 +73,21 @@ where pub fn prompt_passphrase(&mut self, prompt: &str) -> Result { let mut terminal = AlternateScreen::new(&mut self.write)?; let mut terminal = RawMode::new(&mut terminal)?; + terminal .queue(terminal::Clear(terminal::ClearType::All))? - .queue(Print(prompt))? - .flush()?; + .queue(cursor::MoveTo(0, 0))?; + let mut lines = prompt.lines().peekable(); + while let Some(line) = lines.next() { + terminal.queue(Print(line))?; + if lines.peek().is_some() { + terminal + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } + } + terminal.flush()?; + let mut passphrase = String::new(); loop { match read()? { @@ -75,6 +96,15 @@ where passphrase.push('\n'); break; } + KeyCode::Backspace => { + if passphrase.pop().is_some() { + terminal + .queue(cursor::MoveLeft(1))? + .queue(Print(" "))? + .queue(cursor::MoveLeft(1))? + .flush()?; + } + } KeyCode::Char(c) => { terminal.queue(Print("*"))?.flush()?; passphrase.push(c); @@ -90,14 +120,26 @@ where pub fn prompt_message(&mut self, prompt: &str) -> Result<()> { let mut terminal = AlternateScreen::new(&mut self.write)?; let mut terminal = RawMode::new(&mut terminal)?; + terminal .queue(terminal::Clear(terminal::ClearType::All))? - .queue(Print(prompt))? + .queue(cursor::MoveTo(0, 0))?; + let mut lines = prompt.lines().peekable(); + while let Some(line) = lines.next() { + terminal.queue(Print(line))?; + if lines.peek().is_some() { + terminal + .queue(cursor::MoveDown(1))? + .queue(cursor::MoveToColumn(0))?; + } + } + terminal .queue(cursor::DisableBlinking)? .queue(cursor::MoveDown(1))? .queue(cursor::MoveToColumn(0))? .queue(PrintStyledContent(" OK ".negative()))? .flush()?; + loop { match read()? { Event::Key(k) => match k.code { diff --git a/keyfork-shard/Cargo.toml b/keyfork-shard/Cargo.toml index 6523d35..516311a 100644 --- a/keyfork-shard/Cargo.toml +++ b/keyfork-shard/Cargo.toml @@ -10,7 +10,7 @@ license = "AGPL-3.0-only" default = ["openpgp", "openpgp-card"] openpgp = ["sequoia-openpgp", "prompt"] openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"] -prompt = ["keyfork-pinentry"] +prompt = ["keyfork-prompt"] [dependencies] anyhow = "1.0.75" @@ -18,7 +18,6 @@ bincode = "1.3.3" card-backend = { version = "0.2.0", optional = true } card-backend-pcsc = { version = "0.5.0", optional = true } keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" } -keyfork-pinentry = { version = "0.5.0", path = "../keyfork-pinentry", optional = true } openpgp-card-sequoia = { version = "0.2.0", optional = true } openpgp-card = { version = "0.4.0", optional = true } sequoia-openpgp = { version = "1.16.1", optional = true } @@ -26,3 +25,4 @@ serde = "1.0.188" sharks = "0.5.0" smex = { version = "0.1.0", path = "../smex" } thiserror = "1.0.50" +keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", optional = true } diff --git a/keyfork-shard/src/lib.rs b/keyfork-shard/src/lib.rs index ec47690..9c60f1d 100644 --- a/keyfork-shard/src/lib.rs +++ b/keyfork-shard/src/lib.rs @@ -1,5 +1,2 @@ #[cfg(feature = "openpgp")] pub mod openpgp; - -#[cfg(feature = "prompt")] -mod prompt_manager; diff --git a/keyfork-shard/src/openpgp/keyring.rs b/keyfork-shard/src/openpgp/keyring.rs index d849420..69a9732 100644 --- a/keyfork-shard/src/openpgp/keyring.rs +++ b/keyfork-shard/src/openpgp/keyring.rs @@ -1,4 +1,6 @@ -use keyfork_pinentry::ExposeSecret; +use std::fs::File; + +use keyfork_prompt::{Error as PromptError, PromptManager}; use super::openpgp::{ self, @@ -9,8 +11,6 @@ use super::openpgp::{ KeyHandle, KeyID, }; -use crate::prompt_manager::{PinentryError, PromptManager}; - use anyhow::Context; #[derive(thiserror::Error, Debug)] @@ -18,8 +18,14 @@ pub enum Error { #[error("Secret key was not found")] SecretKeyNotFound, + #[error("Could not find TTY when prompting")] + NoTTY, + + #[error("Could not open TTY: {0}")] + Io(#[from] std::io::Error), + #[error("Prompt failed: {0}")] - Prompt(#[from] PinentryError), + Prompt(#[from] PromptError), } pub type Result = std::result::Result; @@ -27,15 +33,20 @@ pub type Result = std::result::Result; pub struct Keyring { full_certs: Vec, root: Option, - pm: PromptManager, + pm: PromptManager, } impl Keyring { pub fn new(certs: impl AsRef<[Cert]>) -> Result { + let tty = std::env::vars() + .filter(|(k, _v)| k.as_str() == "GPG_TTY") + .next() + .ok_or(Error::NoTTY)? + .1; Ok(Self { full_certs: certs.as_ref().to_vec(), root: Default::default(), - pm: PromptManager::new("keyfork-shard", None)?, + pm: PromptManager::new(File::open(&tty)?, File::options().write(true).open(&tty)?)?, }) } @@ -57,14 +68,6 @@ impl Keyring { pub fn get_cert_for_primary_keyid<'a>(&'a self, keyid: &KeyID) -> Option<&'a Cert> { self.full_certs.iter().find(|cert| &cert.keyid() == keyid) } - - // NOTE: This can't return an iterator because iterators are all different types - // and returning different types is naughty - fn get_certs_for_pkesk<'a>(&'a self, pkesk: &'a PKESK) -> impl Iterator + 'a { - self.full_certs.iter().filter(move |cert| { - pkesk.recipient().is_wildcard() || cert.keys().any(|k| &k.keyid() == pkesk.recipient()) - }) - } } impl VerificationHelper for &mut Keyring { @@ -117,7 +120,10 @@ impl DecryptionHelper for &mut Keyring { let null = NullPolicy::new(); // unoptimized route: use all locally stored certs for pkesk in pkesks { - for cert in self.get_certs_for_pkesk(pkesk) { + for cert in self.full_certs.iter().filter(|cert| { + pkesk.recipient().is_wildcard() + || cert.keys().any(|k| &k.keyid() == pkesk.recipient()) + }) { #[allow(deprecated, clippy::map_flatten)] let name = cert .userids() @@ -140,16 +146,16 @@ impl DecryptionHelper for &mut Keyring { .context("Has unencrypted secret")? } else { let message = if let Some(name) = name.as_ref() { - format!("Decryption key for: {} ({name})", secret_key.keyid()) + format!("Decryption key for {} ({name}): ", secret_key.keyid()) } else { - format!("Decryption key for: {}", secret_key.keyid()) + format!("Decryption key for {}: ", secret_key.keyid()) }; let passphrase = self .pm - .prompt_passphrase("Decryption passphrase", message) + .prompt_passphrase(&message) .context("Decryption passphrase")?; secret_key - .decrypt_secret(&passphrase.expose_secret().as_str().into()) + .decrypt_secret(&passphrase.as_str().into()) .context("has_unencrypted_secret is false, could not decrypt secret")? .into_keypair() .context("just-decrypted key")? diff --git a/keyfork-shard/src/openpgp/smartcard.rs b/keyfork-shard/src/openpgp/smartcard.rs index 6807832..fac0df6 100644 --- a/keyfork-shard/src/openpgp/smartcard.rs +++ b/keyfork-shard/src/openpgp/smartcard.rs @@ -1,6 +1,6 @@ -use std::collections::HashSet; +use std::{collections::HashSet, fs::File}; -use keyfork_pinentry::ExposeSecret; +use keyfork_prompt::{Error as PromptError, PromptManager}; use super::openpgp::{ self, @@ -9,7 +9,6 @@ use super::openpgp::{ parse::stream::{DecryptionHelper, MessageLayer, MessageStructure, VerificationHelper}, Fingerprint, }; -use crate::prompt_manager::{PinentryError, PromptManager}; use anyhow::Context; use card_backend_pcsc::PcscBackend; @@ -45,8 +44,14 @@ pub enum Error { #[error("Invalid PIN entered too many times")] InvalidPIN, + #[error("Could not find TTY when prompting")] + NoTTY, + + #[error("Could not open TTY: {0}")] + Io(#[from] std::io::Error), + #[error("Prompt failed: {0}")] - Prompt(#[from] PinentryError), + Prompt(#[from] PromptError), } pub type Result = std::result::Result; @@ -65,15 +70,20 @@ fn format_name(input: impl AsRef) -> String { pub struct SmartcardManager { current_card: Option>, root: Option, - pm: PromptManager, + pm: PromptManager, } impl SmartcardManager { pub fn new() -> Result { + let tty = std::env::vars() + .filter(|(k, _v)| k.as_str() == "GPG_TTY") + .next() + .ok_or(Error::NoTTY)? + .1; Ok(Self { current_card: None, root: None, - pm: PromptManager::new("keyfork-shard", None)?, + pm: PromptManager::new(File::open(&tty)?, File::options().write(true).open(&tty)?)?, }) } @@ -232,13 +242,13 @@ impl DecryptionHelper for &mut SmartcardManager { .err_count_pw1(); let rpea = "Remaining PIN entry attempts"; let message = if cardholder_name.is_empty() { - format!("Unlock card {card_id}\n\n{rpea}: {attempts}") + format!("Unlock card {card_id}\n{rpea}: {attempts}\n\nPIN: ") } else { - format!("Unlock card {card_id} ({cardholder_name})\n\n{rpea}: {attempts}") + format!("Unlock card {card_id} ({cardholder_name})\n{rpea}: {attempts}\n\nPIN: ") }; - let temp_pin = self.pm.prompt_passphrase("Smartcard User PIN", message)?; + let temp_pin = self.pm.prompt_passphrase(&message)?; let verification_status = - transaction.verify_user_pin(temp_pin.expose_secret().as_str().trim()); + transaction.verify_user_pin(temp_pin.as_str().trim()); match verification_status { Ok(_) => { pin.replace(temp_pin); @@ -252,7 +262,7 @@ impl DecryptionHelper for &mut SmartcardManager { } let pin = pin.ok_or(Error::InvalidPIN)?; let mut user = transaction - .to_user_card(pin.expose_secret().as_str().trim()) + .to_user_card(pin.as_str().trim()) .context("Could not load user smartcard from PIN")?; let mut decryptor = user .decryptor(&|| eprintln!("Touch confirmation needed for decryption")) diff --git a/keyfork-shard/src/prompt_manager.rs b/keyfork-shard/src/prompt_manager.rs deleted file mode 100644 index dc46fa3..0000000 --- a/keyfork-shard/src/prompt_manager.rs +++ /dev/null @@ -1,74 +0,0 @@ -use std::path::PathBuf; - -use keyfork_pinentry::{ - self, default_binary, ConfirmationDialog, MessageDialog, PassphraseInput, SecretString, -}; - -#[derive(thiserror::Error, Debug)] -pub enum PinentryError { - #[error("No pinentry binary found")] - NoPinentryFound, - - #[error("{0}")] - Internal(#[from] keyfork_pinentry::Error), -} - -pub type Result = std::result::Result; - -/// Display message dialogues, confirmation prompts, and passphrase inputs with keyfork-pinentry. -pub struct PromptManager { - program_title: String, - pinentry_binary: PathBuf, -} - -impl PromptManager { - pub fn new( - program_title: impl Into, - pinentry_binary: impl Into>, - ) -> Result { - let path = match pinentry_binary.into() { - Some(p) => p, - None => default_binary()?, - }; - std::fs::metadata(&path).map_err(|_| PinentryError::NoPinentryFound)?; - Ok(Self { - program_title: program_title.into(), - pinentry_binary: path, - }) - } - - #[allow(dead_code)] - pub fn prompt_confirmation(&self, prompt: impl AsRef) -> Result { - ConfirmationDialog::with_binary(self.pinentry_binary.clone()) - .with_title(&self.program_title) - .confirm(prompt.as_ref()) - .map_err(|e| e.into()) - } - - pub fn prompt_message(&self, prompt: impl AsRef) -> Result<()> { - MessageDialog::with_binary(self.pinentry_binary.clone()) - .with_title(&self.program_title) - .show_message(prompt.as_ref()) - .map_err(|e| e.into()) - } - - pub fn prompt_passphrase( - &self, - prompt: impl AsRef, - description: impl Into>, - ) -> Result { - match description.into() { - Some(desc) => PassphraseInput::with_binary(self.pinentry_binary.clone()) - .with_title(&self.program_title) - .with_prompt(prompt.as_ref()) - .with_description(&desc) - .interact() - .map_err(|e| e.into()), - None => PassphraseInput::with_binary(self.pinentry_binary.clone()) - .with_title(&self.program_title) - .with_prompt(prompt.as_ref()) - .interact() - .map_err(|e| e.into()), - } - } -}