keyfork-pinentry: remove

This commit is contained in:
Ryan Heywood 2023-12-21 15:02:32 -05:00
parent dc1b36a92c
commit 30a582ed8c
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
7 changed files with 0 additions and 888 deletions

48
Cargo.lock generated
View File

@ -860,15 +860,6 @@ dependencies = [
"digest", "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]] [[package]]
name = "iana-time-zone" name = "iana-time-zone"
version = "0.1.58" version = "0.1.58"
@ -1074,18 +1065,6 @@ dependencies = [
"sha2", "sha2",
] ]
[[package]]
name = "keyfork-pinentry"
version = "0.5.0"
dependencies = [
"nom",
"percent-encoding",
"secrecy",
"thiserror",
"which",
"zeroize",
]
[[package]] [[package]]
name = "keyfork-plumbing" name = "keyfork-plumbing"
version = "0.1.0" version = "0.1.0"
@ -1549,12 +1528,6 @@ dependencies = [
"base64ct", "base64ct",
] ]
[[package]]
name = "percent-encoding"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
[[package]] [[package]]
name = "petgraph" name = "petgraph"
version = "0.6.4" version = "0.6.4"
@ -1944,15 +1917,6 @@ dependencies = [
"zeroize", "zeroize",
] ]
[[package]]
name = "secrecy"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e"
dependencies = [
"zeroize",
]
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.18" version = "1.0.18"
@ -2565,18 +2529,6 @@ version = "0.2.87"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1" 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]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@ -9,7 +9,6 @@ members = [
"keyfork-derive-util", "keyfork-derive-util",
"keyfork-frame", "keyfork-frame",
"keyfork-mnemonic-util", "keyfork-mnemonic-util",
"keyfork-pinentry",
"keyfork-prompt", "keyfork-prompt",
"keyfork-plumbing", "keyfork-plumbing",
"keyfork-shard", "keyfork-shard",

View File

@ -1,23 +0,0 @@
[package]
# name = "pinentry"
name = "keyfork-pinentry"
description = "API for interacting with pinentry binaries"
version = "0.5.0"
# authors = ["Jack Grigg <thestr4d@gmail.com>"]
authors = ["Ryan Heywood <ryan@distrust.co>"]
# 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"

View File

@ -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.

View File

@ -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<String>),
/// Request could not be fulfilled. The possible error codes are defined by
/// `libgpg-error`.
Err {
code: u16,
description: Option<String>,
},
/// Informational output by the server, which is still processing the request.
Information {
keyword: String,
status: Option<String>,
},
/// 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<String>,
},
}
pub struct Connection {
output: ChildStdin,
input: BufReader<ChildStdout>,
}
impl Connection {
pub fn open(name: &Path) -> Result<Self> {
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<Option<SecretString>> {
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<Option<SecretString>> {
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)
}
}

View File

@ -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 <https://github.com/gpg/libgpg-error/blob/master/src/err-codes.h.in> for the
/// mapping from error code to GPG error type.
code: u16,
/// A description of the error, if available.
///
/// See <https://github.com/gpg/libgpg-error/blob/master/src/err-codes.h.in> for the
/// likely descriptions.
description: Option<String>,
}
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<String>) -> 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<io::Error> for Error {
fn from(e: io::Error) -> Self {
Error::Io(e)
}
}
impl From<std::str::Utf8Error> for Error {
fn from(e: std::str::Utf8Error) -> Self {
Error::Encoding(e)
}
}
*/
impl Error {
pub(crate) fn from_parts(code: u16, description: Option<String>) -> Self {
match code {
GPG_ERR_TIMEOUT => Error::Timeout,
GPG_ERR_CANCELED => Error::Cancelled,
_ => Error::Gpg(GpgError::new(code, description)),
}
}
}

View File

@ -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<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(|_| ())
}
}