keyfork-shard: move to keyfork-prompt

This commit is contained in:
Ryan Heywood 2023-12-21 15:01:59 -05:00
parent be74cd8ad1
commit dc1b36a92c
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
7 changed files with 96 additions and 115 deletions

2
Cargo.lock generated
View File

@ -1111,7 +1111,7 @@ dependencies = [
"card-backend", "card-backend",
"card-backend-pcsc", "card-backend-pcsc",
"keyfork-derive-openpgp", "keyfork-derive-openpgp",
"keyfork-pinentry", "keyfork-prompt",
"openpgp-card", "openpgp-card",
"openpgp-card-sequoia", "openpgp-card-sequoia",
"sequoia-openpgp", "sequoia-openpgp",

View File

@ -52,8 +52,18 @@ where
let mut terminal = AlternateScreen::new(&mut self.write)?; let mut terminal = AlternateScreen::new(&mut self.write)?;
terminal terminal
.queue(terminal::Clear(terminal::ClearType::All))? .queue(terminal::Clear(terminal::ClearType::All))?
.queue(Print(prompt))? .queue(cursor::MoveTo(0, 0))?;
.flush()?; 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(); let mut line = String::new();
self.read.read_line(&mut line)?; self.read.read_line(&mut line)?;
Ok(line) Ok(line)
@ -63,10 +73,21 @@ where
pub fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> { pub fn prompt_passphrase(&mut self, prompt: &str) -> Result<String> {
let mut terminal = AlternateScreen::new(&mut self.write)?; let mut terminal = AlternateScreen::new(&mut self.write)?;
let mut terminal = RawMode::new(&mut terminal)?; let mut terminal = RawMode::new(&mut terminal)?;
terminal terminal
.queue(terminal::Clear(terminal::ClearType::All))? .queue(terminal::Clear(terminal::ClearType::All))?
.queue(Print(prompt))? .queue(cursor::MoveTo(0, 0))?;
.flush()?; 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(); let mut passphrase = String::new();
loop { loop {
match read()? { match read()? {
@ -75,6 +96,15 @@ where
passphrase.push('\n'); passphrase.push('\n');
break; break;
} }
KeyCode::Backspace => {
if passphrase.pop().is_some() {
terminal
.queue(cursor::MoveLeft(1))?
.queue(Print(" "))?
.queue(cursor::MoveLeft(1))?
.flush()?;
}
}
KeyCode::Char(c) => { KeyCode::Char(c) => {
terminal.queue(Print("*"))?.flush()?; terminal.queue(Print("*"))?.flush()?;
passphrase.push(c); passphrase.push(c);
@ -90,14 +120,26 @@ where
pub fn prompt_message(&mut self, prompt: &str) -> Result<()> { pub fn prompt_message(&mut self, prompt: &str) -> Result<()> {
let mut terminal = AlternateScreen::new(&mut self.write)?; let mut terminal = AlternateScreen::new(&mut self.write)?;
let mut terminal = RawMode::new(&mut terminal)?; let mut terminal = RawMode::new(&mut terminal)?;
terminal terminal
.queue(terminal::Clear(terminal::ClearType::All))? .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::DisableBlinking)?
.queue(cursor::MoveDown(1))? .queue(cursor::MoveDown(1))?
.queue(cursor::MoveToColumn(0))? .queue(cursor::MoveToColumn(0))?
.queue(PrintStyledContent(" OK ".negative()))? .queue(PrintStyledContent(" OK ".negative()))?
.flush()?; .flush()?;
loop { loop {
match read()? { match read()? {
Event::Key(k) => match k.code { Event::Key(k) => match k.code {

View File

@ -10,7 +10,7 @@ license = "AGPL-3.0-only"
default = ["openpgp", "openpgp-card"] default = ["openpgp", "openpgp-card"]
openpgp = ["sequoia-openpgp", "prompt"] openpgp = ["sequoia-openpgp", "prompt"]
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"] openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"]
prompt = ["keyfork-pinentry"] prompt = ["keyfork-prompt"]
[dependencies] [dependencies]
anyhow = "1.0.75" anyhow = "1.0.75"
@ -18,7 +18,6 @@ bincode = "1.3.3"
card-backend = { version = "0.2.0", optional = true } card-backend = { version = "0.2.0", optional = true }
card-backend-pcsc = { version = "0.5.0", optional = true } card-backend-pcsc = { version = "0.5.0", optional = true }
keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" } 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-sequoia = { version = "0.2.0", optional = true }
openpgp-card = { version = "0.4.0", optional = true } openpgp-card = { version = "0.4.0", optional = true }
sequoia-openpgp = { version = "1.16.1", optional = true } sequoia-openpgp = { version = "1.16.1", optional = true }
@ -26,3 +25,4 @@ serde = "1.0.188"
sharks = "0.5.0" sharks = "0.5.0"
smex = { version = "0.1.0", path = "../smex" } smex = { version = "0.1.0", path = "../smex" }
thiserror = "1.0.50" thiserror = "1.0.50"
keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", optional = true }

View File

@ -1,5 +1,2 @@
#[cfg(feature = "openpgp")] #[cfg(feature = "openpgp")]
pub mod openpgp; pub mod openpgp;
#[cfg(feature = "prompt")]
mod prompt_manager;

View File

@ -1,4 +1,6 @@
use keyfork_pinentry::ExposeSecret; use std::fs::File;
use keyfork_prompt::{Error as PromptError, PromptManager};
use super::openpgp::{ use super::openpgp::{
self, self,
@ -9,8 +11,6 @@ use super::openpgp::{
KeyHandle, KeyID, KeyHandle, KeyID,
}; };
use crate::prompt_manager::{PinentryError, PromptManager};
use anyhow::Context; use anyhow::Context;
#[derive(thiserror::Error, Debug)] #[derive(thiserror::Error, Debug)]
@ -18,8 +18,14 @@ pub enum Error {
#[error("Secret key was not found")] #[error("Secret key was not found")]
SecretKeyNotFound, SecretKeyNotFound,
#[error("Could not find TTY when prompting")]
NoTTY,
#[error("Could not open TTY: {0}")]
Io(#[from] std::io::Error),
#[error("Prompt failed: {0}")] #[error("Prompt failed: {0}")]
Prompt(#[from] PinentryError), Prompt(#[from] PromptError),
} }
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -27,15 +33,20 @@ pub type Result<T, E = Error> = std::result::Result<T, E>;
pub struct Keyring { pub struct Keyring {
full_certs: Vec<Cert>, full_certs: Vec<Cert>,
root: Option<Cert>, root: Option<Cert>,
pm: PromptManager, pm: PromptManager<File, File>,
} }
impl Keyring { impl Keyring {
pub fn new(certs: impl AsRef<[Cert]>) -> Result<Self> { pub fn new(certs: impl AsRef<[Cert]>) -> Result<Self> {
let tty = std::env::vars()
.filter(|(k, _v)| k.as_str() == "GPG_TTY")
.next()
.ok_or(Error::NoTTY)?
.1;
Ok(Self { Ok(Self {
full_certs: certs.as_ref().to_vec(), full_certs: certs.as_ref().to_vec(),
root: Default::default(), 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> { pub fn get_cert_for_primary_keyid<'a>(&'a self, keyid: &KeyID) -> Option<&'a Cert> {
self.full_certs.iter().find(|cert| &cert.keyid() == keyid) 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<Item = &Cert> + '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 { impl VerificationHelper for &mut Keyring {
@ -117,7 +120,10 @@ impl DecryptionHelper for &mut Keyring {
let null = NullPolicy::new(); let null = NullPolicy::new();
// unoptimized route: use all locally stored certs // unoptimized route: use all locally stored certs
for pkesk in pkesks { 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)] #[allow(deprecated, clippy::map_flatten)]
let name = cert let name = cert
.userids() .userids()
@ -140,16 +146,16 @@ impl DecryptionHelper for &mut Keyring {
.context("Has unencrypted secret")? .context("Has unencrypted secret")?
} else { } else {
let message = if let Some(name) = name.as_ref() { 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 { } else {
format!("Decryption key for: {}", secret_key.keyid()) format!("Decryption key for {}: ", secret_key.keyid())
}; };
let passphrase = self let passphrase = self
.pm .pm
.prompt_passphrase("Decryption passphrase", message) .prompt_passphrase(&message)
.context("Decryption passphrase")?; .context("Decryption passphrase")?;
secret_key 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")? .context("has_unencrypted_secret is false, could not decrypt secret")?
.into_keypair() .into_keypair()
.context("just-decrypted key")? .context("just-decrypted key")?

View File

@ -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::{ use super::openpgp::{
self, self,
@ -9,7 +9,6 @@ use super::openpgp::{
parse::stream::{DecryptionHelper, MessageLayer, MessageStructure, VerificationHelper}, parse::stream::{DecryptionHelper, MessageLayer, MessageStructure, VerificationHelper},
Fingerprint, Fingerprint,
}; };
use crate::prompt_manager::{PinentryError, PromptManager};
use anyhow::Context; use anyhow::Context;
use card_backend_pcsc::PcscBackend; use card_backend_pcsc::PcscBackend;
@ -45,8 +44,14 @@ pub enum Error {
#[error("Invalid PIN entered too many times")] #[error("Invalid PIN entered too many times")]
InvalidPIN, InvalidPIN,
#[error("Could not find TTY when prompting")]
NoTTY,
#[error("Could not open TTY: {0}")]
Io(#[from] std::io::Error),
#[error("Prompt failed: {0}")] #[error("Prompt failed: {0}")]
Prompt(#[from] PinentryError), Prompt(#[from] PromptError),
} }
pub type Result<T, E = Error> = std::result::Result<T, E>; pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -65,15 +70,20 @@ fn format_name(input: impl AsRef<str>) -> String {
pub struct SmartcardManager { pub struct SmartcardManager {
current_card: Option<Card<Open>>, current_card: Option<Card<Open>>,
root: Option<Cert>, root: Option<Cert>,
pm: PromptManager, pm: PromptManager<File, File>,
} }
impl SmartcardManager { impl SmartcardManager {
pub fn new() -> Result<Self> { pub fn new() -> Result<Self> {
let tty = std::env::vars()
.filter(|(k, _v)| k.as_str() == "GPG_TTY")
.next()
.ok_or(Error::NoTTY)?
.1;
Ok(Self { Ok(Self {
current_card: None, current_card: None,
root: 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(); .err_count_pw1();
let rpea = "Remaining PIN entry attempts"; let rpea = "Remaining PIN entry attempts";
let message = if cardholder_name.is_empty() { 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 { } 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 = 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 { match verification_status {
Ok(_) => { Ok(_) => {
pin.replace(temp_pin); pin.replace(temp_pin);
@ -252,7 +262,7 @@ impl DecryptionHelper for &mut SmartcardManager {
} }
let pin = pin.ok_or(Error::InvalidPIN)?; let pin = pin.ok_or(Error::InvalidPIN)?;
let mut user = transaction 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")?; .context("Could not load user smartcard from PIN")?;
let mut decryptor = user let mut decryptor = user
.decryptor(&|| eprintln!("Touch confirmation needed for decryption")) .decryptor(&|| eprintln!("Touch confirmation needed for decryption"))

View File

@ -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<T, E = PinentryError> = std::result::Result<T, E>;
/// 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<String>,
pinentry_binary: impl Into<Option<PathBuf>>,
) -> Result<Self> {
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<str>) -> Result<bool> {
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<str>) -> 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<str>,
description: impl Into<Option<String>>,
) -> Result<SecretString> {
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()),
}
}
}