diff --git a/Cargo.lock b/Cargo.lock index a8fc834..ddf3a12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -860,15 +860,6 @@ dependencies = [ "digest", ] -[[package]] -name = "home" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" -dependencies = [ - "windows-sys 0.48.0", -] - [[package]] name = "iana-time-zone" version = "0.1.58" @@ -1074,18 +1065,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "keyfork-pinentry" -version = "0.5.0" -dependencies = [ - "nom", - "percent-encoding", - "secrecy", - "thiserror", - "which", - "zeroize", -] - [[package]] name = "keyfork-plumbing" version = "0.1.0" @@ -1549,12 +1528,6 @@ dependencies = [ "base64ct", ] -[[package]] -name = "percent-encoding" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" - [[package]] name = "petgraph" version = "0.6.4" @@ -1944,15 +1917,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "secrecy" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" -dependencies = [ - "zeroize", -] - [[package]] name = "semver" version = "1.0.18" @@ -2565,18 +2529,6 @@ version = "0.2.87" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" -[[package]] -name = "which" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" -dependencies = [ - "either", - "home", - "once_cell", - "rustix 0.38.13", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/Cargo.toml b/Cargo.toml index 8b20f82..b53e93d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ members = [ "keyfork-derive-util", "keyfork-frame", "keyfork-mnemonic-util", - "keyfork-pinentry", "keyfork-prompt", "keyfork-plumbing", "keyfork-shard", diff --git a/keyfork-pinentry/Cargo.toml b/keyfork-pinentry/Cargo.toml deleted file mode 100644 index 0beb7fe..0000000 --- a/keyfork-pinentry/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -# name = "pinentry" -name = "keyfork-pinentry" -description = "API for interacting with pinentry binaries" -version = "0.5.0" -# authors = ["Jack Grigg "] -authors = ["Ryan Heywood "] -# repository = "https://github.com/str4d/pinentry-rs" -# readme = "README.md" -# keywords = ["passphrase", "password"] -# categories = ["api-bindings", "command-line-interface"] -# license = "MIT OR Apache-2.0" -license = "MIT" -# edition = "2018" -edition = "2021" - -[dependencies] -nom = { version = "7", default-features = false } -percent-encoding = "2.1" -secrecy = "0.8" -thiserror = "1.0.50" -which = { version = "4", default-features = false } -zeroize = "1" diff --git a/keyfork-pinentry/LICENSE.md b/keyfork-pinentry/LICENSE.md deleted file mode 100644 index b1a5332..0000000 --- a/keyfork-pinentry/LICENSE.md +++ /dev/null @@ -1,22 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2020 Jack Grigg -Copyright (c) 2023 Distrust - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/keyfork-pinentry/src/assuan.rs b/keyfork-pinentry/src/assuan.rs deleted file mode 100644 index dbfbe60..0000000 --- a/keyfork-pinentry/src/assuan.rs +++ /dev/null @@ -1,225 +0,0 @@ -use percent_encoding::percent_decode_str; -use secrecy::{ExposeSecret, SecretString}; -use std::borrow::Cow; -use std::io::{self, BufRead, BufReader, Write}; -use std::path::Path; -use std::process::{ChildStdin, ChildStdout}; -use std::process::{Command, Stdio}; -use zeroize::Zeroize; - -use crate::{Error, Result}; - -/// Possible response lines from an Assuan server. -/// -/// Reference: https://gnupg.org/documentation/manuals/assuan/Server-responses.html -#[allow(dead_code)] -#[derive(Debug)] -enum Response { - /// Request was successful. - Ok(Option), - /// Request could not be fulfilled. The possible error codes are defined by - /// `libgpg-error`. - Err { - code: u16, - description: Option, - }, - /// Informational output by the server, which is still processing the request. - Information { - keyword: String, - status: Option, - }, - /// Comment line issued only for debugging purposes. - Comment(String), - /// Raw data returned to client. - DataLine(SecretString), - /// The server needs further information from the client. - Inquire { - keyword: String, - parameters: Option, - }, -} - -pub struct Connection { - output: ChildStdin, - input: BufReader, -} - -impl Connection { - pub fn open(name: &Path) -> Result { - let process = Command::new(name) - .stdin(Stdio::piped()) - .stdout(Stdio::piped()) - .spawn()?; - let output = process.stdin.expect("could open stdin"); - let input = BufReader::new(process.stdout.expect("could open stdin")); - - let mut conn = Connection { output, input }; - // There is always an initial OK server response - conn.read_response()?; - - #[cfg(unix)] - { - conn.send_request("OPTION", Some("ttyname=/dev/tty"))?; - conn.send_request( - "OPTION", - Some(&format!( - "ttytype={}", - std::env::var("TERM") - .as_ref() - .map(|s| s.as_str()) - .unwrap_or("xterm-256color") - )), - )?; - } - - Ok(conn) - } - - pub fn send_request( - &mut self, - command: &str, - parameters: Option<&str>, - ) -> Result> { - self.output.write_all(command.as_bytes())?; - if let Some(p) = parameters { - self.output.write_all(b" ")?; - self.output.write_all(p.as_bytes())?; - } - self.output.write_all(b"\n")?; - self.read_response() - } - - fn read_response(&mut self) -> Result> { - let mut line = String::new(); - let mut data = None; - - // We loop until we find an OK or ERR response. This is probably sufficient for - // pinentry, but other Assuan protocols might rely on INQUIRE, which needs - // intermediate completion states or callbacks. - loop { - line.zeroize(); - self.input.read_line(&mut line)?; - match read::server_response(&line) - .map(|(_, r)| r) - .map_err(|e| io::Error::new(io::ErrorKind::InvalidData, format!("{}", e)))? - { - Response::Ok(_info) => { - /* - if let Some(info) = info { - debug!("< OK {}", info); - } - */ - line.zeroize(); - return Ok(data.map(SecretString::new)); - } - Response::Err { code, description } => { - line.zeroize(); - if let Some(mut buf) = data { - buf.zeroize(); - } - return Err(Error::from_parts(code, description)); - } - Response::Comment(_comment) => { - // debug!("< # {}", comment) - } - Response::DataLine(data_line) => { - let buf = data.take(); - let data_line_decoded = - percent_decode_str(data_line.expose_secret()).decode_utf8()?; - data = Some(buf.unwrap_or_else(String::new) + &data_line_decoded); - if let Cow::Owned(mut data_line_decoded) = data_line_decoded { - data_line_decoded.zeroize(); - } - } - _res => { - // info!("< {:?}", res) - } - } - } - } -} - -impl Drop for Connection { - fn drop(&mut self) { - let _ = self.send_request("BYE", None); - } -} - -mod read { - use nom::{ - branch::alt, - bytes::complete::{is_not, tag}, - character::complete::{digit1, line_ending}, - combinator::{map, opt}, - sequence::{pair, preceded, terminated}, - IResult, - }; - use secrecy::SecretString; - - use super::Response; - - fn gpg_error_code(input: &str) -> IResult<&str, u16> { - map(digit1, |code| { - #[allow(clippy::from_str_radix_10)] - let full = u32::from_str_radix(code, 10).expect("have decimal digits"); - // gpg uses the lowest 16 bits for error codes. - full as u16 - })(input) - } - - pub(super) fn server_response(input: &str) -> IResult<&str, Response> { - terminated( - alt(( - preceded( - tag("OK"), - map(opt(preceded(tag(" "), is_not("\r\n"))), |params| { - Response::Ok(params.map(String::from)) - }), - ), - preceded( - tag("ERR "), - map( - pair(gpg_error_code, opt(preceded(tag(" "), is_not("\r\n")))), - |(code, description)| Response::Err { - code, - description: description.map(String::from), - }, - ), - ), - preceded( - tag("S "), - map( - pair(is_not(" \r\n"), opt(preceded(tag(" "), is_not("\r\n")))), - |(keyword, status): (&str, _)| Response::Information { - keyword: keyword.to_owned(), - status: status.map(String::from), - }, - ), - ), - preceded( - tag("# "), - map(is_not("\r\n"), |comment: &str| { - Response::Comment(comment.to_owned()) - }), - ), - preceded( - tag("D "), - map(is_not("\r\n"), |data: &str| { - Response::DataLine(SecretString::new(data.to_owned())) - }), - ), - preceded( - tag("INQUIRE "), - map( - pair(is_not(" \r\n"), opt(preceded(tag(" "), is_not("\r\n")))), - |(keyword, parameters): (&str, _)| Response::Inquire { - keyword: keyword.to_owned(), - parameters: parameters.map(String::from), - }, - ), - ), - )), - line_ending, - )(input) - } -} diff --git a/keyfork-pinentry/src/error.rs b/keyfork-pinentry/src/error.rs deleted file mode 100644 index e05c91c..0000000 --- a/keyfork-pinentry/src/error.rs +++ /dev/null @@ -1,114 +0,0 @@ -use std::{fmt, io}; - -pub(crate) const GPG_ERR_TIMEOUT: u16 = 62; -pub(crate) const GPG_ERR_CANCELED: u16 = 99; -pub(crate) const GPG_ERR_NOT_CONFIRMED: u16 = 114; - -/// An uncommon or unexpected GPG error. -/// -/// `pinentry` is built on top of Assuan, which inherits all of GPG's error codes. Only -/// some of these error codes are actually used by the common `pinentry` implementations, -/// but it's possible to receive any of them. -#[derive(Debug)] -pub struct GpgError { - /// The GPG error code. - /// - /// See for the - /// mapping from error code to GPG error type. - code: u16, - - /// A description of the error, if available. - /// - /// See for the - /// likely descriptions. - description: Option, -} - -impl fmt::Display for GpgError { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Code {}", self.code)?; - if let Some(desc) = &self.description { - write!(f, ": {}", desc)?; - } - Ok(()) - } -} - -impl std::error::Error for GpgError {} - -impl GpgError { - pub(super) fn new(code: u16, description: Option) -> Self { - GpgError { code, description } - } - - /// Returns the GPG code for this error. - pub fn code(&self) -> u16 { - self.code - } -} - -/// Errors that may be returned while interacting with `pinentry` binaries. -#[derive(thiserror::Error, Debug)] -pub enum Error { - /// The user cancelled the operation. - #[error("The user cancelled the operation")] - Cancelled, - - /// Operation timed out waiting for the user to respond. - #[error("Operation timed out waiting for the user to respond")] - Timeout, - - /// An error occurred while finding the `pinentry` binary. - #[error("{0}")] - Which(#[from] which::Error), - - /// An I/O error occurred while communicating with the `pinentry` binary. - #[error("{0}")] - Io(#[from] io::Error), - - /// An uncommon or unexpected GPG error. - #[error("{0}")] - Gpg(#[from] GpgError), - - /// The user's input doesn't decode to valid UTF-8. - #[error("{0}")] - Encoding(#[from] std::str::Utf8Error), -} - -/* -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Error::Timeout => write!(f, "Operation timed out"), - Error::Cancelled => write!(f, "Operation cancelled"), - Error::Gpg(e) => e.fmt(f), - Error::Io(e) => e.fmt(f), - Error::Encoding(e) => e.fmt(f), - } - } -} -*/ - -/* -impl From for Error { - fn from(e: io::Error) -> Self { - Error::Io(e) - } -} - -impl From for Error { - fn from(e: std::str::Utf8Error) -> Self { - Error::Encoding(e) - } -} -*/ - -impl Error { - pub(crate) fn from_parts(code: u16, description: Option) -> Self { - match code { - GPG_ERR_TIMEOUT => Error::Timeout, - GPG_ERR_CANCELED => Error::Cancelled, - _ => Error::Gpg(GpgError::new(code, description)), - } - } -} diff --git a/keyfork-pinentry/src/lib.rs b/keyfork-pinentry/src/lib.rs deleted file mode 100644 index ce930ee..0000000 --- a/keyfork-pinentry/src/lib.rs +++ /dev/null @@ -1,455 +0,0 @@ -//! `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(|_| ()) - } -}