keyfork-shard: add keyfork-pinentry

This commit is contained in:
Ryan Heywood 2023-11-05 00:45:47 -05:00
parent 7f90e4ada4
commit c206800ad2
Signed by: ryan
GPG Key ID: 8E401478A3FBEF72
14 changed files with 1069 additions and 188 deletions

222
Cargo.lock generated
View File

@ -233,9 +233,9 @@ dependencies = [
[[package]]
name = "buffered-reader"
version = "1.2.0"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66d3bea5bcc3ecc38fe5388e6bc35e6fe7bd665eb3ae9a44283e15b91ad3867d"
checksum = "2b9b0a25eb06e83579bc985d836e1e3b957a7201301b48538764d2b2e78090d4"
dependencies = [
"bzip2",
"flate2",
@ -478,7 +478,7 @@ dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest 0.10.7",
"digest",
"fiat-crypto",
"platforms",
"rustc_version",
@ -524,15 +524,6 @@ version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
[[package]]
name = "digest"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
dependencies = [
"generic-array",
]
[[package]]
name = "digest"
version = "0.10.7"
@ -580,19 +571,10 @@ checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4"
dependencies = [
"der 0.7.8",
"elliptic-curve",
"signature 2.1.0",
"signature",
"spki 0.7.2",
]
[[package]]
name = "ed25519"
version = "1.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7"
dependencies = [
"signature 1.6.4",
]
[[package]]
name = "ed25519"
version = "2.2.2"
@ -600,7 +582,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60f6d271ca33075c88028be6f04d502853d63a5ece419d269c15315d4fc1cf1d"
dependencies = [
"pkcs8 0.10.2",
"signature 2.1.0",
"signature",
]
[[package]]
@ -610,7 +592,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980"
dependencies = [
"curve25519-dalek",
"ed25519 2.2.2",
"ed25519",
"serde",
"sha2",
"zeroize",
@ -630,7 +612,7 @@ checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b"
dependencies = [
"base16ct",
"crypto-bigint",
"digest 0.10.7",
"digest",
"ff",
"generic-array",
"group",
@ -762,19 +744,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "getrandom"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
"wasm-bindgen",
]
[[package]]
name = "getrandom"
version = "0.2.10"
@ -784,7 +753,7 @@ dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"wasm-bindgen",
]
@ -862,7 +831,16 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
dependencies = [
"digest 0.10.7",
"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]]
@ -890,9 +868,9 @@ dependencies = [
[[package]]
name = "idna"
version = "0.3.0"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c"
dependencies = [
"unicode-bidi",
"unicode-normalization",
@ -1033,7 +1011,7 @@ dependencies = [
name = "keyfork-derive-util"
version = "0.1.0"
dependencies = [
"digest 0.10.7",
"digest",
"ed25519-dalek",
"hex-literal",
"hmac",
@ -1069,6 +1047,18 @@ 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"
@ -1085,11 +1075,13 @@ dependencies = [
"bincode",
"card-backend-pcsc",
"keyfork-derive-openpgp",
"keyfork-pinentry",
"openpgp-card-sequoia",
"sequoia-openpgp",
"serde",
"sharks",
"smex",
"thiserror",
]
[[package]]
@ -1135,9 +1127,9 @@ dependencies = [
[[package]]
name = "lalrpop"
version = "0.19.12"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a1cbf952127589f2851ab2046af368fd20645491bb4b376f04b7f94d7a9837b"
checksum = "da4081d44f4611b66c6dd725e6de3169f9f63905421e8626fcb86b6a898998b8"
dependencies = [
"ascii-canvas",
"bit-set",
@ -1148,7 +1140,7 @@ dependencies = [
"lalrpop-util",
"petgraph",
"regex",
"regex-syntax 0.6.29",
"regex-syntax 0.7.4",
"string_cache",
"term",
"tiny-keccak",
@ -1157,9 +1149,9 @@ dependencies = [
[[package]]
name = "lalrpop-util"
version = "0.19.12"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3c48237b9604c5a4702de6b824e02006c3214327564636aef27c1028a8fa0ed"
checksum = "3f35c735096c0293d313e8f2a641627472b83d01b937177fe76e5e2708d31e0d"
[[package]]
name = "lazy_static"
@ -1275,7 +1267,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"wasi",
"windows-sys 0.48.0",
]
@ -1285,7 +1277,7 @@ version = "7.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9fdccf3eae7b161910d2daa2f0155ca35041322e8fe5c5f1f2c9d0b12356336"
dependencies = [
"getrandom 0.2.10",
"getrandom",
"libc",
"nettle-sys",
"thiserror",
@ -1470,7 +1462,7 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest 0.10.7",
"digest",
"hmac",
]
@ -1508,6 +1500,12 @@ 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"
@ -1646,19 +1644,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "rand"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03"
dependencies = [
"getrandom 0.1.16",
"libc",
"rand_chacha 0.2.2",
"rand_core 0.5.1",
"rand_hc",
]
[[package]]
name = "rand"
version = "0.8.5"
@ -1666,20 +1651,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha 0.3.1",
"rand_chacha",
"rand_core 0.6.4",
]
[[package]]
name = "rand_chacha"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402"
dependencies = [
"ppv-lite86",
"rand_core 0.5.1",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
@ -1705,31 +1680,13 @@ version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc"
[[package]]
name = "rand_core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
dependencies = [
"getrandom 0.1.16",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom 0.2.10",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c"
dependencies = [
"rand_core 0.5.1",
"getrandom",
]
[[package]]
@ -1765,7 +1722,7 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
dependencies = [
"getrandom 0.2.10",
"getrandom",
"redox_syscall 0.2.16",
"thiserror",
]
@ -1814,6 +1771,12 @@ version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2"
[[package]]
name = "regex-syntax"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
[[package]]
name = "remove_dir_all"
version = "0.5.3"
@ -1829,7 +1792,7 @@ version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f"
dependencies = [
"digest 0.10.7",
"digest",
]
[[package]]
@ -1839,7 +1802,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55a77d189da1fee555ad95b7e50e7457d91c0e089ec68ca69ad2989413bbdab4"
dependencies = [
"byteorder",
"digest 0.10.7",
"digest",
"num-bigint-dig",
"num-integer",
"num-iter",
@ -1847,7 +1810,7 @@ dependencies = [
"pkcs1",
"pkcs8 0.9.0",
"rand_core 0.6.4",
"signature 2.1.0",
"signature",
"subtle",
"zeroize",
]
@ -1932,6 +1895,15 @@ 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"
@ -1940,9 +1912,9 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
[[package]]
name = "sequoia-openpgp"
version = "1.16.1"
version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a16854c0f6297de6db4df195e28324dfbc2429802f0e48cd04007db8e3049709"
checksum = "2ea026cf8a70d331c742e3ad7e68fd405d0743ff86630fb4334a1bf8d0e194c7"
dependencies = [
"anyhow",
"base64",
@ -1950,9 +1922,9 @@ dependencies = [
"bzip2",
"chrono",
"dyn-clone",
"ed25519 1.5.3",
"ed25519",
"flate2",
"getrandom 0.2.10",
"getrandom",
"idna",
"lalrpop",
"lalrpop-util",
@ -1961,9 +1933,9 @@ dependencies = [
"memsec",
"nettle",
"once_cell",
"rand 0.7.3",
"rand 0.8.5",
"regex",
"regex-syntax 0.6.29",
"regex-syntax 0.8.2",
"sha1collisiondetection",
"thiserror",
"xxhash-rust",
@ -2002,11 +1974,11 @@ dependencies = [
[[package]]
name = "sha1collisiondetection"
version = "0.2.7"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b20793cf8330b2c7da4c438116660fed24e380bcb8a1bcfff2581b5593a0b38e"
checksum = "31c0b86a052106b16741199985c9ec2bf501f619f70c48fa479b44b093ad9a68"
dependencies = [
"digest 0.9.0",
"digest",
"generic-array",
]
@ -2018,7 +1990,7 @@ checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
dependencies = [
"cfg-if",
"cpufeatures",
"digest 0.10.7",
"digest",
]
[[package]]
@ -2056,19 +2028,13 @@ dependencies = [
"libc",
]
[[package]]
name = "signature"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c"
[[package]]
name = "signature"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
dependencies = [
"digest 0.10.7",
"digest",
"rand_core 0.6.4",
]
@ -2223,18 +2189,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.49"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1177e8c6d7ede7afde3585fd2513e611227efd6481bd78d2e82ba1ce16557ed4"
checksum = "f9a7210f5c9a7156bb50aa36aed4c95afb51df0df00713949448cf9e97d382d2"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.49"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10712f02019e9288794769fba95cd6847df9874d49d871d062172f9dd41bc4cc"
checksum = "266b2e40bc00e5a6c09c3584011e08b06f123c00362c92b975ba9843aaaa14b8"
dependencies = [
"proc-macro2",
"quote",
@ -2469,12 +2435,6 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "wasi"
version = "0.9.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -2535,6 +2495,18 @@ 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"

View File

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

View File

@ -0,0 +1,22 @@
[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"
[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

@ -0,0 +1,22 @@
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

@ -0,0 +1,225 @@
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

@ -0,0 +1,114 @@
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)),
}
}
}

437
keyfork-pinentry/src/lib.rs Normal file
View File

@ -0,0 +1,437 @@
//! `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)
}
/// 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(desc))?;
}
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(|_| ())
}
}

View File

@ -7,16 +7,19 @@ edition = "2021"
[features]
default = ["openpgp", "openpgp-card"]
openpgp = ["sequoia-openpgp"]
openpgp = ["sequoia-openpgp", "prompt"]
openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc"]
prompt = ["keyfork-pinentry"]
[dependencies]
anyhow = "1.0.75"
bincode = "1.3.3"
card-backend-pcsc = { version = "0.5.0", optional = true }
keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" }
keyfork-pinentry = { version = "0.5.0", path = "../keyfork-pinentry", optional = true }
openpgp-card-sequoia = { version = "0.2.0", optional = true }
sequoia-openpgp = { version = "1.16.1", optional = true }
serde = "1.0.188"
sharks = "0.5.0"
smex = { version = "0.1.0", path = "../smex" }
thiserror = "1.0.50"

View File

@ -1,11 +1,4 @@
use std::{
env,
fs::File,
io::stdout,
path::PathBuf,
process::ExitCode,
str::FromStr,
};
use std::{env, fs::File, io::stdout, path::PathBuf, process::ExitCode, str::FromStr};
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages};

View File

@ -1,2 +1,5 @@
#[cfg(feature = "openpgp")]
pub mod openpgp;
#[cfg(feature = "prompt")]
mod prompt_manager;

View File

@ -19,7 +19,7 @@ use openpgp::{
},
policy::{NullPolicy, Policy, StandardPolicy},
serialize::{
stream::{ArbitraryWriter, Encryptor, LiteralWriter, Message, Recipient, Signer},
stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer},
Marshal,
},
types::KeyFlags,
@ -176,8 +176,8 @@ pub fn combine(
// We don't want to invalidate someone's keys just because the old sig expired.
let policy = NullPolicy::new();
let mut keyring = Keyring::new(certs);
let mut manager = SmartcardManager::new();
let mut keyring = Keyring::new(certs)?;
let mut manager = SmartcardManager::new()?;
let content = if keyring.is_empty() {
let card_fp = manager.load_any_card()?;
eprintln!("key discovery is empty, using hardware smartcard: {card_fp}");
@ -351,7 +351,7 @@ pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write)
let mut message_output = vec![];
let message = Message::new(&mut message_output);
let message = Encryptor::for_recipients(
let message = Encryptor2::for_recipients(
message,
encryption_keys
.iter()
@ -398,7 +398,7 @@ pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write)
// metadata
let mut message_output = vec![];
let message = Message::new(&mut message_output);
let message = Encryptor::for_recipients(message, total_recipients).build()?;
let message = Encryptor2::for_recipients(message, total_recipients).build()?;
let mut message = LiteralWriter::new(message).build()?;
message.write_all(&pp)?;
message.finalize()?;

View File

@ -1,11 +1,16 @@
use keyfork_pinentry::ExposeSecret;
use super::openpgp::{
self,
cert::Cert,
packet::{PKESK, SKESK},
parse::stream::{DecryptionHelper, MessageLayer, MessageStructure, VerificationHelper},
policy::NullPolicy,
KeyHandle, KeyID,
};
use crate::prompt_manager::PromptManager;
#[derive(Clone, Debug)]
pub enum KeyringFailure {
SecretKeyNotFound,
@ -26,18 +31,19 @@ impl std::fmt::Display for KeyringFailure {
impl std::error::Error for KeyringFailure {}
#[derive(Clone, Debug)]
pub struct Keyring {
full_certs: Vec<Cert>,
root: Option<Cert>,
pm: PromptManager,
}
impl Keyring {
pub fn new(certs: impl AsRef<[Cert]>) -> Self {
Self {
pub fn new(certs: impl AsRef<[Cert]>) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
full_certs: certs.as_ref().to_vec(),
root: Default::default(),
}
pm: PromptManager::new("keyfork-shard", None)?,
})
}
pub fn is_empty(&self) -> bool {
@ -114,14 +120,39 @@ impl DecryptionHelper for &mut Keyring {
where
D: FnMut(openpgp::types::SymmetricAlgorithm, &openpgp::crypto::SessionKey) -> bool,
{
let null = NullPolicy::new();
// unoptimized route: use all locally stored certs
for pkesk in pkesks {
for cert in self.get_certs_for_pkesk(pkesk) {
for key in cert.keys().secret() {
#[allow(deprecated, clippy::map_flatten)]
let name = cert
.userids()
.next()
.map(|userid| userid.userid().name().transpose())
.flatten()
.transpose()?;
for key in cert
.keys()
.with_policy(&null, None)
.for_storage_encryption()
.secret()
{
let secret_key = key.key().clone();
// NOTE: Returns an error if using an encrypted secret key.
// TODO: support skipping or validating encrypted secret keys.
let mut keypair = secret_key.into_keypair()?;
let mut keypair = if secret_key.has_unencrypted_secret() {
secret_key.into_keypair()?
} else {
let message = if let Some(name) = name.as_ref() {
format!("Decryption key for: {} ({name})", secret_key.keyid())
} else {
format!("Decryption key for: {}", secret_key.keyid())
};
let passphrase = self
.pm
.prompt_passphrase("Decryption passphrase", message)?;
let key = secret_key
.decrypt_secret(&passphrase.expose_secret().as_str().into())?;
key.into_keypair()?
};
if pkesk
.decrypt(&mut keypair, sym_algo)
.map(|(algo, sk)| decrypt(algo, &sk))

View File

@ -1,5 +1,7 @@
use std::collections::HashSet;
use keyfork_pinentry::ExposeSecret;
use super::openpgp::{
self,
cert::Cert,
@ -7,6 +9,7 @@ use super::openpgp::{
parse::stream::{DecryptionHelper, MessageLayer, MessageStructure, VerificationHelper},
Fingerprint,
};
use crate::prompt_manager::PromptManager;
use card_backend_pcsc::PcscBackend;
use openpgp_card_sequoia::{state::Open, Card};
@ -28,15 +31,19 @@ impl std::fmt::Display for SmartcardFailure {
impl std::error::Error for SmartcardFailure {}
#[derive(Default)]
pub struct SmartcardManager {
current_card: Option<Card<Open>>,
root: Option<Cert>,
pm: PromptManager,
}
impl SmartcardManager {
pub fn new() -> Self {
Default::default()
pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
current_card: None,
root: None,
pm: PromptManager::new("keyfork-shard", None)?,
})
}
// Sets the root cert, returning the old cert
@ -46,40 +53,6 @@ impl SmartcardManager {
cert
}
/// Utility function to prompt for a newline from standard input.
pub fn prompt(&self, prompt: impl std::fmt::Display) -> std::io::Result<()> {
eprint!("{prompt}: ");
std::io::stdin().read_line(&mut String::new()).map(|_| ())
}
/// Utility function to obtain a prompt response from the command line.
pub fn prompt_input(&self, prompt: impl std::fmt::Display) -> std::io::Result<String> {
eprint!("{prompt}: ");
let mut output = String::new();
std::io::stdin().read_line(&mut output)?;
Ok(output)
}
/*
/// Return all [`Fingerprint`] for the currently accessible backends.
///
/// NOTE: Only implemented for decryption keys.
pub fn iter_fingerprints() -> impl Iterator<Item = Fingerprint> {
PcscBackend::cards(None).into_iter().flat_map(|iter| {
iter.filter_map(|backend| {
let backend = backend.ok()?;
let mut card = Card::<Open>::new(backend).ok()?;
let transaction = card.transaction().ok()?;
transaction
.fingerprints()
.ok()?
.decryption()
.map(|fp| Fingerprint::from_bytes(fp.as_bytes()))
})
})
}
*/
/// Load any backend.
pub fn load_any_card(&mut self) -> Result<Fingerprint, Box<dyn std::error::Error>> {
PcscBackend::cards(None)?
@ -141,7 +114,8 @@ impl SmartcardManager {
}
eprintln!("No matching smartcard detected.");
self.prompt("Please plug in a smart card and press enter")?;
self.pm
.prompt_message("Please plug in a smart card and press enter")?;
}
Ok(None)
@ -201,8 +175,18 @@ impl DecryptionHelper for &mut SmartcardManager {
.fingerprints()?
.decryption()
.map(|fp| Fingerprint::from_bytes(fp.as_bytes()));
let pin = self.prompt_input("User PIN")?;
let mut user = transaction.to_user_card(pin.as_str().trim())?;
let Some(fp) = fp else {
return Err(SmartcardFailure::SmartCardHasNoDecrypt.into());
};
let cardholder_name = transaction.cardholder_name()?;
let card_id = transaction.application_identifier()?.ident();
let message = if cardholder_name.is_empty() {
format!("Unlock card {card_id}")
} else {
format!("Unlock card {card_id} ({cardholder_name})")
};
let pin = self.pm.prompt_passphrase("Smartcard User PIN", message)?;
let mut user = transaction.to_user_card(pin.expose_secret().as_str().trim())?;
let mut decryptor =
user.decryptor(&|| eprintln!("Touch confirmation needed for decryption"))?;
for pkesk in pkesks {
@ -211,7 +195,7 @@ impl DecryptionHelper for &mut SmartcardManager {
.map(|(algo, sk)| decrypt(algo, &sk))
.unwrap_or(false)
{
return Ok(fp);
return Ok(Some(fp));
}
}

View File

@ -0,0 +1,74 @@
use std::path::PathBuf;
use keyfork_pinentry::{
self, default_binary, ConfirmationDialog, MessageDialog, PassphraseInput, SecretString,
};
#[derive(thiserror::Error, Debug)]
pub enum PinentryError {
#[error("No pinentry binary found")]
NoPinentryFound,
#[error("{0}")]
Internal(#[from] keyfork_pinentry::Error),
}
pub type Result<T, E = PinentryError> = std::result::Result<T, E>;
/// Display message dialogues, confirmation prompts, and passphrase inputs with keyfork-pinentry.
pub struct PromptManager {
program_title: String,
pinentry_binary: PathBuf,
}
impl PromptManager {
pub fn new(
program_title: impl Into<String>,
pinentry_binary: impl Into<Option<PathBuf>>,
) -> Result<Self> {
let path = match pinentry_binary.into() {
Some(p) => p,
None => default_binary()?,
};
std::fs::metadata(&path).map_err(|_| PinentryError::NoPinentryFound)?;
Ok(Self {
program_title: program_title.into(),
pinentry_binary: path,
})
}
#[allow(dead_code)]
pub fn prompt_confirmation(&self, prompt: impl AsRef<str>) -> Result<bool> {
ConfirmationDialog::with_binary(self.pinentry_binary.clone())
.with_title(&self.program_title)
.confirm(prompt.as_ref())
.map_err(|e| e.into())
}
pub fn prompt_message(&self, prompt: impl AsRef<str>) -> Result<()> {
MessageDialog::with_binary(self.pinentry_binary.clone())
.with_title(&self.program_title)
.show_message(prompt.as_ref())
.map_err(|e| e.into())
}
pub fn prompt_passphrase(
&self,
prompt: impl AsRef<str>,
description: impl Into<Option<String>>,
) -> Result<SecretString> {
match description.into() {
Some(desc) => PassphraseInput::with_binary(self.pinentry_binary.clone())
.with_title(&self.program_title)
.with_prompt(prompt.as_ref())
.with_description(&desc)
.interact()
.map_err(|e| e.into()),
None => PassphraseInput::with_binary(self.pinentry_binary.clone())
.with_title(&self.program_title)
.with_prompt(prompt.as_ref())
.interact()
.map_err(|e| e.into()),
}
}
}