//! `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 = std::result::Result; /// Find the expected default pinentry binary pub fn default_binary() -> Result { 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, } 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 { 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 { 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, } 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 { 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 { 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, } 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 { 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(|_| ()) } }