keyfork-pinentry: remove
This commit is contained in:
parent
dc1b36a92c
commit
30a582ed8c
|
@ -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"
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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"
|
|
|
@ -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.
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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(|_| ())
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue