keyfork/keyfork-pinentry/src/lib.rs

456 lines
16 KiB
Rust

//! `keyfork_pinentry` is a library for interacting with the pinentry binaries available on
//! various platforms.
//!
//! # Examples
//!
//! ## Request passphrase or PIN
//!
//! ```no_run
//! use keyfork_pinentry::PassphraseInput;
//! use secrecy::SecretString;
//!
//! let passphrase = if let Ok(mut input) = PassphraseInput::with_default_binary() {
//! // pinentry binary is available!
//! input
//! .with_description("Enter new passphrase for FooBar")
//! .with_prompt("Passphrase:")
//! .with_confirmation("Confirm passphrase:", "Passphrases do not match")
//! .interact()
//! } else {
//! // Fall back to some other passphrase entry method.
//! Ok(SecretString::new("a better passphrase than this".to_owned()))
//! }?;
//! # Ok::<(), keyfork_pinentry::Error>(())
//! ```
//!
//! ## Ask user for confirmation
//!
//! ```no_run
//! use keyfork_pinentry::ConfirmationDialog;
//!
//! if let Ok(mut input) = ConfirmationDialog::with_default_binary() {
//! input
//! .with_ok("Definitely!")
//! .with_not_ok("No thanks")
//! .with_cancel("Maybe later")
//! .confirm("Would you like to play a game?")?;
//! };
//! # Ok::<(), keyfork_pinentry::Error>(())
//! ```
//!
//! ## Display a message
//!
//! ```no_run
//! use keyfork_pinentry::MessageDialog;
//!
//! if let Ok(mut input) = MessageDialog::with_default_binary() {
//! input.with_ok("Got it!").show_message("This will be shown with a single button.")?;
//! };
//! # Ok::<(), keyfork_pinentry::Error>(())
//! ```
// Catch documentation errors caused by code changes.
#![deny(rustdoc::broken_intra_doc_links)]
#![deny(missing_docs)]
pub use secrecy::{ExposeSecret, SecretString};
use std::path::PathBuf;
mod assuan;
mod error;
pub use error::{Error, GpgError};
/// Result type for the `keyfork_pinentry` crate.
pub type Result<T> = std::result::Result<T, Error>;
/// Find the expected default pinentry binary
pub fn default_binary() -> Result<PathBuf> {
which::which("pinentry-curses").map_err(Into::into)
}
fn convert_multiline(line: &str) -> String {
// convert into multiline
let mut converted_line = String::new();
let mut last_end = 0;
for (start, part) in line.match_indices(&['\n', '\r', '%']) {
converted_line.push_str(line.get(last_end..start).unwrap());
converted_line.push_str(match part {
"\n" => "%0A",
"\r" => "%0D",
"%" => "%25",
fb => panic!("expected index given to match_indices, got: {fb}"),
});
last_end = start + part.len();
}
converted_line.push_str(line.get(last_end..line.len()).unwrap());
converted_line
}
/// A dialog for requesting a passphrase from the user.
pub struct PassphraseInput<'a> {
binary: PathBuf,
required: Option<&'a str>,
title: Option<&'a str>,
description: Option<&'a str>,
error: Option<&'a str>,
prompt: Option<&'a str>,
confirmation: Option<(&'a str, &'a str)>,
ok: Option<&'a str>,
cancel: Option<&'a str>,
timeout: Option<u16>,
}
impl<'a> PassphraseInput<'a> {
/// Creates a new PassphraseInput using the binary named `keyfork_pinentry`.
///
/// Returns `Err` if `default_binary()` cannot be found in `PATH`.
pub fn with_default_binary() -> Result<Self> {
default_binary().map(Self::with_binary)
}
/// Creates a new PassphraseInput using the given path to, or name of, a `pinentry`
/// binary.
pub fn with_binary(binary: PathBuf) -> Self {
PassphraseInput {
binary,
required: None,
title: None,
description: None,
error: None,
prompt: None,
confirmation: None,
ok: None,
cancel: None,
timeout: None,
}
}
/// Prevents the user from submitting an empty passphrase.
///
/// The provided error text will be displayed if the user submits an empty passphrase.
/// The dialog will remain open until the user either submits a non-empty passphrase,
/// or selects the "Cancel" button.
pub fn required(&mut self, empty_error: &'a str) -> &mut Self {
self.required = Some(empty_error);
self
}
/// Sets the window title.
///
/// When using this feature you should take care that the window is still identifiable
/// as the pinentry.
pub fn with_title(&mut self, title: &'a str) -> &mut Self {
self.title = Some(title);
self
}
/// Sets the descriptive text to display.
pub fn with_description(&mut self, description: &'a str) -> &mut Self {
self.description = Some(description);
self
}
/// Sets the error text to display.
///
/// This is used to display an error message, for example on a second interaction if
/// the first passphrase was invalid.
pub fn with_error(&mut self, error: &'a str) -> &mut Self {
self.error = Some(error);
self
}
/// Sets the prompt to show.
///
/// When asking for a passphrase or PIN, this sets the text just before the widget for
/// passphrase entry.
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_prompt(&mut self, prompt: &'a str) -> &mut Self {
self.prompt = Some(prompt);
self
}
/// Enables confirmation prompting.
///
/// When asking for a passphrase or PIN, this sets the text just before the widget for
/// the passphrase confirmation entry.
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_confirmation(
&mut self,
confirmation_prompt: &'a str,
mismatch_error: &'a str,
) -> &mut Self {
self.confirmation = Some((confirmation_prompt, mismatch_error));
self
}
/// Sets the text for the button signalling confirmation (the "OK" button).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_ok(&mut self, ok: &'a str) -> &mut Self {
self.ok = Some(ok);
self
}
/// Sets the text for the button signaling cancellation or disagreement (the "Cancel"
/// button).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_cancel(&mut self, cancel: &'a str) -> &mut Self {
self.cancel = Some(cancel);
self
}
/// Sets the timeout (in seconds) before returning an error.
pub fn with_timeout(&mut self, timeout: u16) -> &mut Self {
self.timeout = Some(timeout);
self
}
/// Asks for a passphrase or PIN.
pub fn interact(&self) -> Result<SecretString> {
let mut pinentry = assuan::Connection::open(&self.binary)?;
if let Some(title) = &self.title {
pinentry.send_request("SETTITLE", Some(title))?;
}
if let Some(desc) = &self.description {
pinentry.send_request("SETDESC", Some(convert_multiline(desc).as_ref()))?;
}
if let Some(error) = &self.error {
pinentry.send_request("SETERROR", Some(error))?;
}
if let Some(prompt) = &self.prompt {
pinentry.send_request("SETPROMPT", Some(prompt))?;
}
if let Some(ok) = &self.ok {
pinentry.send_request("SETOK", Some(ok))?;
}
if let Some(cancel) = &self.cancel {
pinentry.send_request("SETCANCEL", Some(cancel))?;
}
if let Some((confirmation_prompt, mismatch_error)) = &self.confirmation {
pinentry.send_request("SETREPEAT", Some(confirmation_prompt))?;
pinentry.send_request("SETREPEATERROR", Some(mismatch_error))?;
}
if let Some(timeout) = self.timeout {
pinentry.send_request("SETTIMEOUT", Some(&format!("{}", timeout)))?;
}
loop {
match (pinentry.send_request("GETPIN", None)?, self.required) {
// If the user provides an empty passphrase, GETPIN returns no data.
(None, None) => return Ok(SecretString::new(String::new())),
(Some(passphrase), _) => return Ok(passphrase),
(_, Some(empty_error)) => {
// SETERROR is cleared by GETPIN, so we reset it on each loop.
pinentry.send_request("SETERROR", Some(empty_error))?;
}
}
}
}
}
/// A dialog for requesting a confirmation from the user.
pub struct ConfirmationDialog<'a> {
binary: PathBuf,
title: Option<&'a str>,
ok: Option<&'a str>,
cancel: Option<&'a str>,
not_ok: Option<&'a str>,
timeout: Option<u16>,
}
impl<'a> ConfirmationDialog<'a> {
/// Creates a new ConfirmationDialog using the binary named `pinentry`.
///
/// Returns `Err` if `pinentry` cannot be found in `PATH`.
pub fn with_default_binary() -> Result<Self> {
default_binary().map(Self::with_binary)
}
/// Creates a new ConfirmationDialog using the given path to, or name of, a `pinentry`
/// binary.
pub fn with_binary(binary: PathBuf) -> Self {
ConfirmationDialog {
binary,
title: None,
ok: None,
cancel: None,
not_ok: None,
timeout: None,
}
}
/// Sets the window title.
///
/// When using this feature you should take care that the window is still identifiable
/// as the pinentry.
pub fn with_title(&mut self, title: &'a str) -> &mut Self {
self.title = Some(title);
self
}
/// Sets the text for the button signalling confirmation (the "OK" button).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_ok(&mut self, ok: &'a str) -> &mut Self {
self.ok = Some(ok);
self
}
/// Sets the text for the button signaling cancellation or disagreement (the "Cancel"
/// button).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_cancel(&mut self, cancel: &'a str) -> &mut Self {
self.cancel = Some(cancel);
self
}
/// Enables the third non-affirmative response button (the "Not OK" button).
///
/// This can be used in case three buttons are required (to distinguish between
/// cancellation and disagreement).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_not_ok(&mut self, not_ok: &'a str) -> &mut Self {
self.not_ok = Some(not_ok);
self
}
/// Sets the timeout (in seconds) before returning an error.
pub fn with_timeout(&mut self, timeout: u16) -> &mut Self {
self.timeout = Some(timeout);
self
}
/// Asks for confirmation.
///
/// Returns:
/// - `Ok(true)` if the "OK" button is selected.
/// - `Ok(false)` if:
/// - the "Cancel" button is selected and the "Not OK" button is disabled.
/// - the "Not OK" button is enabled and selected.
/// - `Err(Error::Cancelled)` if the "Cancel" button is selected and the "Not OK"
/// button is enabled.
pub fn confirm(&self, query: &str) -> Result<bool> {
let mut pinentry = assuan::Connection::open(&self.binary)?;
pinentry.send_request("SETDESC", Some(query))?;
if let Some(ok) = &self.ok {
pinentry.send_request("SETOK", Some(ok))?;
}
if let Some(cancel) = &self.cancel {
pinentry.send_request("SETCANCEL", Some(cancel))?;
}
if let Some(not_ok) = &self.not_ok {
pinentry.send_request("SETNOTOK", Some(not_ok))?;
}
if let Some(timeout) = self.timeout {
pinentry.send_request("SETTIMEOUT", Some(&format!("{}", timeout)))?;
}
pinentry
.send_request("CONFIRM", None)
.map(|_| true)
.or_else(|e| match (&e, self.not_ok.is_some()) {
(Error::Cancelled, false) => Ok(false),
(Error::Gpg(gpg), true) if gpg.code() == error::GPG_ERR_NOT_CONFIRMED => Ok(false),
_ => Err(e),
})
}
}
/// A dialog for showing a message to the user.
pub struct MessageDialog<'a> {
binary: PathBuf,
title: Option<&'a str>,
ok: Option<&'a str>,
timeout: Option<u16>,
}
impl<'a> MessageDialog<'a> {
/// Creates a new MessageDialog using the binary named `pinentry`.
///
/// Returns `Err` if `pinentry` cannot be found in `PATH`.
pub fn with_default_binary() -> Result<Self> {
default_binary().map(Self::with_binary)
}
/// Creates a new MessageDialog using the given path to, or name of, a `pinentry`
/// binary.
pub fn with_binary(binary: PathBuf) -> Self {
MessageDialog {
binary,
title: None,
ok: None,
timeout: None,
}
}
/// Sets the window title.
///
/// When using this feature you should take care that the window is still identifiable
/// as the pinentry.
pub fn with_title(&mut self, title: &'a str) -> &mut Self {
self.title = Some(title);
self
}
/// Sets the text for the button signalling confirmation (the "OK" button).
///
/// You should use an underscore in the text only if you know that a modern version of
/// pinentry is used. Modern versions underline the next character after the
/// underscore and use the first such underlined character as a keyboard accelerator.
/// Use a double underscore to escape an underscore.
pub fn with_ok(&mut self, ok: &'a str) -> &mut Self {
self.ok = Some(ok);
self
}
/// Sets the timeout (in seconds) before returning an error.
pub fn with_timeout(&mut self, timeout: u16) -> &mut Self {
self.timeout = Some(timeout);
self
}
/// Shows a message.
pub fn show_message(&self, message: &str) -> Result<()> {
let mut pinentry = assuan::Connection::open(&self.binary)?;
pinentry.send_request("SETDESC", Some(message))?;
if let Some(ok) = &self.ok {
pinentry.send_request("SETOK", Some(ok))?;
}
if let Some(timeout) = self.timeout {
pinentry.send_request("SETTIMEOUT", Some(&format!("{}", timeout)))?;
}
pinentry.send_request("MESSAGE", None).map(|_| ())
}
}