Compare commits

...

16 Commits

Author SHA1 Message Date
Ryan Heywood 6a3018e5e8
keyfork-shard: bump after mnemonic refactor 2024-02-19 05:41:37 -05:00
Ryan Heywood d51ee36ace
keyfork-shard: fixup usage of smex 2024-02-19 05:40:43 -05:00
Ryan Heywood b75d45876a
keyfork-shard: refactor key discovery mechanisms 2024-02-19 05:36:27 -05:00
Ryan Heywood 2541d49fb8
keyfork-shard: add shard_and_encrypt 2024-02-19 05:36:26 -05:00
Ryan Heywood 3b5c1340db
keyfork-shard: add new methods to trait to support split() 2024-02-19 05:36:24 -05:00
Ryan Heywood 3c1d8e9784
cleanup use of keyfork-shard deprecated functions 2024-02-19 05:36:20 -05:00
Ryan Heywood 6093cf9be4
keyfork-shard: traitify functionality 2024-02-19 05:35:01 -05:00
Ryan Heywood dfcf4b1740
keyfork-mnemonic-util: reduce amount of generics for validated functions 2024-02-19 05:32:24 -05:00
Ryan Heywood 44d8cf2098
keyfork-mnemonic-util: major refactor of Mnemonic type, remove cloned Wordlist 2024-02-19 05:20:33 -05:00
Ryan Heywood ed61d0685a
keyfork-bin: initial commit 2024-02-18 19:19:04 -05:00
Ryan Heywood d481c7e164
keyfork-mnemonic-util: deprecate from{_raw,}_bytes 2024-02-18 18:14:50 -05:00
Ryan Heywood 31e51f65a5
keyfork-mnemonic-util: optimize Default::default() for Wordlist 2024-02-18 18:01:51 -05:00
Ryan Heywood 883e0cdf65
keyfork-mnemonic-util: deprecate seed() in favor of generate_seed() 2024-02-18 18:01:18 -05:00
Ryan Heywood 9cb953414f
tests, examples: make clippy happy 2024-02-18 17:59:23 -05:00
Ryan Heywood ece9f435d2
Clarify documentation and add more examples
Note: The type signature of smex::encode and smex::decode has changed,
but will still accept values that were previously passed in.
2024-02-18 17:57:24 -05:00
Ryan Heywood 33405ee4fc
keyfork-derive-openpgp: add KEYFORK_OPENPGP_EXPIRE env var 2024-02-12 12:17:14 -05:00
36 changed files with 1436 additions and 452 deletions

8
Cargo.lock generated
View File

@ -1674,6 +1674,7 @@ dependencies = [
"card-backend-pcsc",
"clap",
"clap_complete",
"keyfork-bin",
"keyfork-derive-openpgp",
"keyfork-derive-util",
"keyfork-entropy",
@ -1692,6 +1693,13 @@ dependencies = [
"tokio",
]
[[package]]
name = "keyfork-bin"
version = "0.1.0"
dependencies = [
"anyhow",
]
[[package]]
name = "keyfork-crossterm"
version = "0.27.1"

View File

@ -14,6 +14,7 @@ members = [
"crates/qrcode/keyfork-qrcode",
"crates/qrcode/keyfork-zbar",
"crates/qrcode/keyfork-zbar-sys",
"crates/util/keyfork-bin",
"crates/util/keyfork-crossterm",
"crates/util/keyfork-entropy",
"crates/util/keyfork-frame",

View File

@ -40,6 +40,25 @@
//! # keyforkd::test_util::Infallible::Ok(())
//! # }).unwrap();
//! ```
//!
//! In tests, the Keyforkd test_util module and TestPrivateKeys can be used.
//!
//! ```rust
//! use std::str::FromStr;
//!
//! use keyforkd_client::Client;
//! use keyfork_derive_util::DerivationPath;
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
//!
//! let seed = b"funky accordion noises";
//! keyforkd::test_util::run_test(seed, |socket_path| {
//! std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
//! let mut client = Client::discover_socket().unwrap();
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
//! keyforkd::test_util::Infallible::Ok(())
//! }).unwrap();
//! ```
pub use std::os::unix::net::UnixStream;
use std::{collections::HashMap, path::PathBuf};

View File

@ -18,7 +18,7 @@ fn secp256k1_test_suite() {
let seed = seed_test.seed;
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
for test in seed_test.tests {
let socket = UnixStream::connect(&socket_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
let chain_len = chain.len();
@ -29,7 +29,7 @@ fn secp256k1_test_suite() {
// key using an XPrv, for all but the last XPrv, which is verified after this
for i in 2..chain_len {
// FIXME: Keyfork will only allow one request per session
let socket = UnixStream::connect(&socket_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let path = DerivationPath::from_str(test.chain).unwrap();
let left_path = path.inner()[..i]
@ -40,7 +40,7 @@ fn secp256k1_test_suite() {
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
let derived_xprv = xprv.derive_path(&right_path).unwrap();
let socket = UnixStream::connect(&socket_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
assert_eq!(
@ -73,7 +73,7 @@ fn ed25519_test_suite() {
let seed = seed_test.seed;
run_test(&seed, move |socket_path| {
for test in seed_test.tests {
let socket = UnixStream::connect(&socket_path).unwrap();
let socket = UnixStream::connect(socket_path).unwrap();
let mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap();
let chain_len = chain.len();

View File

@ -34,3 +34,15 @@ request, as well as its best-effort guess on what path is being derived (using
the `keyfork-derive-path-data` crate), to inform the user of what keys are
requested. Once the server sends the client the new extended private key, the
client can then choose to use the key as-is, or derive further keys.
## Testing
A Keyfork server can be automatically started by using [`test_util::run_test`].
The function accepts a closure, starting the server before the closure is run,
and closing the server after the closure has completed. This may be useful for
people writing software that interacts with the Keyfork server, such as a
deriver or a provisioner. A test seed must be provided, but can be any content.
The closure accepts one argument, the path of the UNIX socket from which the
server can be accessed.
Examples of the test utility can be seen in the `keyforkd-client` crate.

View File

@ -57,7 +57,7 @@ pub async fn start_and_run_server_on(
let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new())
// TODO: passphrase support and/or store passphrase with mnemonic
.service(Keyforkd::new(mnemonic.seed(None)?));
.service(Keyforkd::new(mnemonic.generate_seed(None)));
let mut server = match UnixServer::bind(socket_path) {
Ok(s) => s,

View File

@ -25,14 +25,25 @@ pub struct InfallibleError {
/// ```
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
/// Run a test making use of a Keyforkd server. The path to the socket of the Keyforkd server is
/// provided as the only argument to the closure. The closure is expected to return a Result; the
/// Error field of the Result may be an error returned by a test.
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
/// the server is capable of being interacted with. The test is in the form of a closure, expected
/// to return a [`Result`] where success is a unit type (test passed) and the error is any error
/// that happened during the test (alternatively, a panic may be used, and will be returned as an
/// error).
///
/// # Panics
/// The function may panic if any errors arise while configuring and using the Tokio multithreaded
/// runtime.
///
/// The function is not expected to run in production; therefore, the function plays "fast and
/// loose" wih the usage of [`Result::expect`]. In normal usage, these should never be an issue.
/// # Examples
/// ```rust
/// use std::os::unix::net::UnixStream;
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
/// UnixStream::connect(&path).map(|_| ())
/// }).unwrap();
/// ```
#[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
where

View File

@ -46,7 +46,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::discover_socket()?;
let request = DerivationRequest::new(algo, &path);
let response = client.request(&request.into())?;
println!("{}", smex::encode(&DerivationResponse::try_from(response)?.data));
println!("{}", smex::encode(DerivationResponse::try_from(response)?.data));
Ok(())
}

View File

@ -1,6 +1,9 @@
//! Creation of OpenPGP certificates from BIP-0032 derived data.
//! Creation of OpenPGP Transferable Secret Keys from BIP-0032 derived data.
use std::time::{Duration, SystemTime, SystemTimeError};
use std::{
str::FromStr,
time::{Duration, SystemTime, SystemTimeError},
};
use derive_util::{DerivationIndex, ExtendedPrivateKey, IndexError, PrivateKey};
use ed25519_dalek::SigningKey;
@ -68,7 +71,24 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
};
let epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
let one_day = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
let expiration_date = match std::env::var("KEYFORK_OPENPGP_EXPIRE").as_mut() {
Ok(var) => {
let ch = var.pop();
match (ch, u64::from_str(var)) {
(Some(ch @ ('d' | 'm' | 'y')), Ok(expire)) => {
let multiplier = match ch {
'd' => 1,
'm' => 30,
'y' => 365,
_ => unreachable!(),
};
SystemTime::now() + Duration::from_secs(60 * 60 * 24 * expire * multiplier)
}
_ => SystemTime::now() + Duration::from_secs(60 * 60 * 24),
}
}
Err(_) => SystemTime::now() + Duration::from_secs(60 * 60 * 24),
};
// Create certificate with initial key and signature
let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?;
@ -80,7 +100,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
// Sign and attach primary key and primary userid
let builder = SignatureBuilder::new(SignatureType::PositiveCertification)
.set_key_validity_period(one_day.duration_since(epoch)?)?
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
.set_signature_creation_time(epoch)?
.set_key_flags(primary_key_flags.clone())?;
let binding = userid.bind(&mut primary_key.clone().into_keypair()?, &cert, builder)?;
@ -89,7 +109,8 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
// Set certificate expiration to one day
let mut keypair = primary_key.clone().into_keypair()?;
let signatures = cert.set_expiration_time(&policy, None, &mut keypair, Some(one_day))?;
let signatures =
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;
let cert = cert.insert_packets(signatures)?;
let mut cert = cert;
@ -127,7 +148,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
SignatureBuilder::new(SignatureType::SubkeyBinding)
.set_key_flags(subkey_flags.clone())?
.set_signature_creation_time(epoch)?
.set_key_validity_period(one_day.duration_since(epoch)?)?
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
.set_embedded_signature(
SignatureBuilder::new(SignatureType::PrimaryKeyBinding)
.set_signature_creation_time(epoch)?
@ -141,7 +162,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
SignatureBuilder::new(SignatureType::SubkeyBinding)
.set_key_flags(subkey_flags.clone())?
.set_signature_creation_time(epoch)?
.set_key_validity_period(one_day.duration_since(epoch)?)?
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
};
// Sign subkey with primary key and attach to cert

View File

@ -209,7 +209,7 @@ impl DerivationRequest {
/// # }
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
// TODO: passphrase support and/or store passphrase within mnemonic
self.derive_with_master_seed(&mnemonic.seed(None)?)
self.derive_with_master_seed(&mnemonic.generate_seed(None))
}
/// Derive an [`ExtendedPrivateKey`] using the given seed.

View File

@ -30,7 +30,7 @@ fn secp256k1() {
} = test;
// Tests for ExtendedPrivateKey
let varlen_seed = VariableLengthSeed::new(&seed);
let varlen_seed = VariableLengthSeed::new(seed);
let xkey = ExtendedPrivateKey::<SecretKey>::new(varlen_seed);
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(
@ -51,7 +51,7 @@ fn secp256k1() {
// Tests for DerivationRequest
let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain);
let response = request.derive_with_master_seed(&seed).unwrap();
let response = request.derive_with_master_seed(seed).unwrap();
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
}
}
@ -76,7 +76,7 @@ fn ed25519() {
} = test;
// Tests for ExtendedPrivateKey
let varlen_seed = VariableLengthSeed::new(&seed);
let varlen_seed = VariableLengthSeed::new(seed);
let xkey = ExtendedPrivateKey::<SigningKey>::new(varlen_seed);
let derived_key = xkey.derive_path(&chain).unwrap();
assert_eq!(
@ -97,7 +97,7 @@ fn ed25519() {
// Tests for DerivationRequest
let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain);
let response = request.derive_with_master_seed(&seed).unwrap();
let response = request.derive_with_master_seed(seed).unwrap();
assert_eq!(&response.data, private_key.as_slice(), "test: {chain}");
}
}

View File

@ -7,52 +7,33 @@ use std::{
process::ExitCode,
};
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages};
use keyfork_shard::{openpgp::OpenPGP, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(
shard: impl AsRef<Path>,
key_discovery: Option<&str>,
) -> Result<(File, Vec<Cert>)> {
) -> Result<(File, Option<PathBuf>)> {
let key_discovery = key_discovery.map(PathBuf::from);
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
// Load certs from path
let certs = key_discovery
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
Ok((File::open(shard)?, certs))
Ok((File::open(shard)?, key_discovery))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (messages_file, cert_list) = match args.as_slice() {
let (messages_file, key_discovery) = match args.as_slice() {
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
[shard] => validate(shard, None)?,
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let mut encrypted_messages = parse_messages(messages_file)?;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
let mut bytes = vec![];
combine(
cert_list,
&encrypted_metadata,
encrypted_messages.into(),
&mut bytes,
)?;
print!("{}", smex::encode(&bytes));
let openpgp = OpenPGP;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?;
print!("{}", smex::encode(bytes));
Ok(())
}

View File

@ -7,47 +7,33 @@ use std::{
process::ExitCode,
};
use keyfork_shard::openpgp::{decrypt, discover_certs, openpgp::Cert, parse_messages};
use keyfork_shard::{Format, openpgp::OpenPGP};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate<'a>(
messages_file: impl AsRef<Path>,
key_discovery: impl Into<Option<&'a str>>,
) -> Result<(File, Vec<Cert>)> {
let key_discovery = key_discovery.into().map(PathBuf::from);
fn validate(
shard: impl AsRef<Path>,
key_discovery: Option<&str>,
) -> Result<(File, Option<PathBuf>)> {
let key_discovery = key_discovery.map(PathBuf::from);
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
// Load certs from path
let certs = key_discovery
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
Ok((File::open(messages_file)?, certs))
Ok((File::open(shard)?, key_discovery))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (messages_file, cert_list) = match args.as_slice() {
[messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?,
[messages_file] => validate(messages_file, None)?,
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
let (messages_file, key_discovery) = match args.as_slice() {
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
[shard] => validate(shard, None)?,
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
};
let mut encrypted_messages = parse_messages(messages_file)?;
let openpgp = OpenPGP;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
decrypt(
&cert_list,
&encrypted_metadata,
encrypted_messages.make_contiguous(),
)?;
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file)?;
Ok(())
}

View File

@ -20,7 +20,7 @@ fn run() -> Result<()> {
let mut bytes = vec![];
remote_decrypt(&mut bytes)?;
print!("{}", smex::encode(&bytes));
print!("{}", smex::encode(bytes));
Ok(())
}

View File

@ -2,14 +2,12 @@
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split};
use keyfork_shard::{Format, openpgp::OpenPGP};
#[derive(Clone, Debug)]
enum Error {
Usage(String),
Input,
Threshold(u8, u8),
InvalidCertCount(usize, u8),
}
impl std::fmt::Display for Error {
@ -19,15 +17,6 @@ impl std::fmt::Display for Error {
write!(f, "Usage: {program_name} threshold max key_discovery")
}
Error::Input => f.write_str("Expected hex encoded input"),
Error::Threshold(threshold, max) => {
write!(
f,
"Invalid threshold: 0 < threshold {threshold} <= max {max} < 256"
)
}
Error::InvalidCertCount(count, max) => {
write!(f, "Invalid cert count: count {count} != max {max}")
}
}
}
}
@ -36,31 +25,20 @@ impl std::error::Error for Error {}
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, Vec<Cert>)> {
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, u8, PathBuf)> {
let threshold = u8::from_str(threshold)?;
let max = u8::from_str(max)?;
let key_discovery = PathBuf::from(key_discovery);
if threshold > max {
return Err(Error::Threshold(threshold, max).into());
}
// Verify path exists
std::fs::metadata(&key_discovery)?;
// Load certs from path
let certs = discover_certs(key_discovery)?;
if certs.len() != max.into() {
return Err(Error::InvalidCertCount(certs.len(), max).into());
}
Ok((threshold, certs))
Ok((threshold, max, key_discovery))
}
fn run() -> Result<()> {
let mut args = env::args();
let program_name = args.next().expect("program name");
let args = args.collect::<Vec<_>>();
let (threshold, cert_list) = match args.as_slice() {
let (threshold, max, key_discovery) = match args.as_slice() {
[threshold, max, key_discovery] => validate(threshold, max, key_discovery)?,
_ => return Err(Error::Usage(program_name).into()),
};
@ -69,11 +47,12 @@ fn run() -> Result<()> {
let Some(line) = stdin().lines().next() else {
return Err(Error::Input.into());
};
smex::decode(&line?)?
smex::decode(line?)?
};
split(threshold, cert_list, &input, std::io::stdout())?;
let openpgp = OpenPGP;
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
Ok(())
}

View File

@ -1,13 +1,13 @@
#![doc = include_str!("../README.md")]
use std::io::{stdin, stdout, Write};
use std::io::{stdin, stdout, Read, Write};
use aes_gcm::{
aead::{Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit,
aead::{consts::U12, Aead, AeadCore, OsRng},
Aes256Gcm, KeyInit, Nonce,
};
use hkdf::Hkdf;
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{
validators::{mnemonic::MnemonicSetValidator, Validator},
Message as PromptMessage, PromptHandler, Terminal,
@ -16,9 +16,338 @@ use sha2::Sha256;
use sharks::{Share, Sharks};
use x25519_dalek::{EphemeralSecret, PublicKey};
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
const ENC_LEN: u8 = 4 * 16;
#[cfg(feature = "openpgp")]
pub mod openpgp;
/// A trait to specify where keys can be discovered from, such as a Rust-native type or a path on
/// the filesystem that keys may be read from.
pub trait KeyDiscovery<F: Format + ?Sized> {
/// Discover public keys for the associated format.
///
/// # Errors
/// The method may return an error if public keys could not be loaded from the given discovery
/// mechanism. A discovery mechanism _must_ be able to detect public keys.
fn discover_public_keys(&self) -> Result<Vec<F::PublicKey>, F::Error>;
/// Discover private keys for the associated format.
///
/// # Errors
/// The method may return an error if private keys could not be loaded from the given
/// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
}
/// A format to use for splitting and combining secrets.
pub trait Format {
/// The error type returned from any failed operations.
type Error: std::error::Error + 'static;
/// A type encapsulating a single public key recipient.
type PublicKey;
/// A type encapsulating the private key recipients of shards.
type PrivateKeyData;
/// A type representing a Signer derived from the secret.
type SigningKey;
/// A type representing the parsed, but encrypted, Shard data.
type EncryptedData;
/// Derive a signer
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
/// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to
/// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork
/// will use the same key_data for both, ensuring an iteration of this method will match with
/// iterations in methods called later.
///
/// # Errors
/// The method may return an error if encryption to any of the public keys fails.
fn format_encrypted_header(
&self,
signing_key: &Self::SigningKey,
key_data: &[Self::PublicKey],
threshold: u8,
) -> Result<Self::EncryptedData, Self::Error>;
/// Format a shard encrypted to the given public key, signing with the private key.
///
/// # Errors
/// The method may return an error if the public key used to encrypt the shard is unsuitable
/// for encryption, or if an error occurs while encrypting.
fn encrypt_shard(
&self,
shard: &[u8],
public_key: &Self::PublicKey,
signing_key: &mut Self::SigningKey,
) -> Result<Self::EncryptedData, Self::Error>;
/// Parse the Shard file into a processable type.
///
/// # Errors
/// The method may return an error if the Shard file could not be read from or if the Shard
/// file could not be properly parsed.
fn parse_shard_file(
&self,
shard_file: impl Read + Send + Sync,
) -> Result<Vec<Self::EncryptedData>, Self::Error>;
/// Write the Shard data to a Shard file.
///
/// # Errors
/// The method may return an error if the Shard data could not be properly serialized or if the
/// Shard file could not be written to.
fn format_shard_file(
&self,
encrypted_data: &[Self::EncryptedData],
shard_file: impl Write + Send + Sync,
) -> Result<(), Self::Error>;
/// Decrypt shares and associated metadata from a readable input. For the current version of
/// Keyfork, the only associated metadata is a u8 representing the threshold to combine
/// secrets.
///
/// # Errors
/// The method may return an error if the shardfile couldn't be read from, if all shards
/// could not be decrypted, or if a shard could not be parsed from the decrypted data.
fn decrypt_all_shards(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_messages: &[Self::EncryptedData],
) -> Result<(Vec<Share>, u8), Self::Error>;
/// Decrypt a single share and associated metadata from a reaable input. For the current
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
/// combine secrets.
///
/// # Errors
/// The method may return an error if the shardfile couldn't be read from, if a shard could not
/// be decrypted, or if a shard could not be parsed from the decrypted data.
fn decrypt_one_shard(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
) -> Result<(Share, u8), Self::Error>;
/// Decrypt multiple shares and combine them to recreate a secret.
///
/// # Errors
/// The method may return an error if the shares can't be decrypted or if the shares can't
/// be combined into a secret.
fn decrypt_all_shards_to_secret(
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
let (shares, threshold) = self.decrypt_all_shards(private_keys, &encrypted_messages)?;
let secret = Sharks(threshold)
.recover(&shares)
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
Ok(secret)
}
/// Establish an AES-256-GCM transport key using ECDH, decrypt a single shard, and encrypt the
/// shard to the AES key.
///
/// # Errors
/// The method may return an error if a share can't be decrypted. The method will not return an
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
/// QR code; instead, a mnemonic prompt will be used.
fn decrypt_one_shard_for_transport(
&self,
private_key_discovery: Option<impl KeyDiscovery<Self>>,
reader: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?;
// parse input
let private_keys = private_key_discovery
.map(|p| p.discover_private_keys())
.transpose()?;
let encrypted_messages = self.parse_shard_file(reader)?;
// establish AES-256-GCM key via ECDH
let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None;
// receive remote data via scanning QR code from camera
#[cfg(feature = "qrcode")]
{
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
if let Ok(Some(hex)) =
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
{
let decoded_data = smex::decode(&hex)?;
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
} else {
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
};
}
// if QR code scanning failed or was unavailable, read from a set of mnemonics
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
_ => {
let validator = MnemonicSetValidator {
word_lengths: [9, 24],
};
let [nonce_mnemonic, pubkey_mnemonic] = pm
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let nonce = nonce_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()
.map_err(|_| InvalidData)?;
(nonce, pubkey)
}
};
// create our shared key
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_pubkey))
.to_bytes();
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
let mut hkdf_output = [0u8; 256 / 8];
hkdf.expand(&[], &mut hkdf_output)?;
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
// decrypt a single shard and create the payload
let (share, threshold) = self.decrypt_one_shard(private_keys, &encrypted_messages)?;
let mut payload = Vec::from(&share);
payload.insert(0, HUNK_VERSION);
payload.insert(1, threshold);
assert!(
payload.len() <= ENC_LEN as usize,
"invalid share length (too long, max {ENC_LEN} bytes)"
);
// encrypt data
let nonce = Nonce::<U12>::from_slice(&nonce);
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
// convert data to a static-size payload
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
#[allow(clippy::assertions_on_constants)]
{
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
}
#[allow(clippy::cast_possible_truncation)]
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
assert!(
payload_bytes.len() < out_bytes.len(),
"encrypted payload larger than acceptable limit"
);
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
// difficulty when entering in prompts manually, as one's place could be lost due to
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
// but not including the last byte.
#[allow(clippy::cast_possible_truncation)]
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
.iter_mut()
.enumerate()
{
*byte = (i % u8::MAX as usize) as u8;
}
// safety: size of out_bytes is constant and always % 4 == 0
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
#[cfg(feature = "qrcode")]
{
use keyfork_qrcode::{qrencode, ErrorCorrection};
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
qrcode_data.extend(payload_mnemonic.as_bytes());
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
pm.prompt_message(PromptMessage::Text(
concat!(
"A QR code will be displayed after this prompt. ",
"Send the QR code back to the operator combining the shards. ",
"Nobody else should scan this QR code."
)
.to_string(),
))?;
pm.prompt_message(PromptMessage::Data(qrcode))?;
}
}
pm.prompt_message(PromptMessage::Text(format!(
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
)))?;
Ok(())
}
/// Split a secret into a shard for every shard in keys, with the given Shamir's Secret Sharing
/// threshold.
///
/// # Errors
/// The method may return an error if the shares can't be encrypted.
fn shard_and_encrypt(
&self,
threshold: u8,
max: u8,
secret: &[u8],
public_key_discovery: impl KeyDiscovery<Self>,
writer: impl Write + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let mut signing_key = self.derive_signing_key(secret);
let sharks = Sharks(threshold);
let dealer = sharks.dealer(secret);
let public_keys = public_key_discovery.discover_public_keys()?;
assert!(
public_keys.len() < u8::MAX as usize,
"must have less than u8::MAX public keys"
);
assert_eq!(
max,
public_keys.len() as u8,
"max must be equal to amount of public keys"
);
let max = public_keys.len() as u8;
assert!(max >= threshold, "threshold must not exceed max keys");
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
let mut messages = vec![header];
for (pk, share) in public_keys.iter().zip(dealer) {
let shard = Vec::from(&share);
messages.push(self.encrypt_shard(&shard, pk, &mut signing_key)?);
}
self.format_shard_file(&messages, writer)?;
Ok(())
}
}
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
#[derive(thiserror::Error, Debug)]
pub enum SharksError {
@ -63,7 +392,6 @@ const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry
/// incompatible with the currently running version.
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let mut iter_count = None;
let mut shares = vec![];
@ -74,11 +402,9 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
iter += 1;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic =
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
let our_key = EphemeralSecret::random();
let key_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
#[cfg(feature = "qrcode")]
{
@ -132,9 +458,9 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
word_lengths: [24, 48],
};
let [pubkey_mnemonic, payload_mnemonic] = pm.prompt_validated_wordlist(
let [pubkey_mnemonic, payload_mnemonic] = pm
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
&wordlist,
3,
validator.to_fn(),
)?;

View File

@ -17,7 +17,7 @@ use keyfork_derive_openpgp::{
derive_util::{DerivationPath, PathError, VariableLengthSeed},
XPrv,
};
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist};
use keyfork_mnemonic_util::{English, Mnemonic, MnemonicFromStrError, MnemonicGenerationError};
use keyfork_prompt::{
validators::{mnemonic::MnemonicSetValidator, Validator},
Error as PromptError, Message as PromptMessage, PromptHandler, Terminal,
@ -57,8 +57,8 @@ const SHARD_METADATA_VERSION: u8 = 1;
const SHARD_METADATA_OFFSET: usize = 2;
use super::{
InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, QRCODE_PROMPT,
QRCODE_TIMEOUT,
Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR,
QRCODE_PROMPT, QRCODE_TIMEOUT, KeyDiscovery
};
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
@ -163,6 +163,55 @@ impl EncryptedMessage {
}
}
/// Parse OpenPGP packets for encrypted messages.
pub fn from_reader(input: impl Read + Send + Sync) -> openpgp::Result<Vec<Self>> {
let mut pkesks = Vec::new();
let mut encrypted_messages = vec![];
for packet in PacketPile::from_reader(input)
.map_err(Error::Sequoia)?
.into_children()
{
match packet {
Packet::PKESK(p) => pkesks.push(p),
Packet::SEIP(s) => {
encrypted_messages.push(EncryptedMessage::new(&mut pkesks, s));
}
s => {
panic!("Invalid variant found: {}", s.tag());
}
}
}
Ok(encrypted_messages)
}
/// Serialize all contents of the message to a writer.
///
/// # Errors
/// The function may error for any condition in Sequoia's Serialize trait.
fn serialize(&self, mut o: impl std::io::Write + Send + Sync) -> openpgp::Result<()> {
for pkesk in &self.pkesks {
let mut packet = vec![];
pkesk.serialize(&mut packet).map_err(Error::Sequoia)?;
let message = Message::new(&mut o);
let mut message = ArbitraryWriter::new(message, Tag::PKESK).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
}
let mut packet = vec![];
self.message
.serialize(&mut packet)
.map_err(Error::Sequoia)?;
let message = Message::new(&mut o);
let mut message = ArbitraryWriter::new(message, Tag::SEIP).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
Ok(())
}
/// Decrypt the message with a Sequoia policy and decryptor.
///
/// This method creates a container containing the packets and passes the serialized container
@ -176,23 +225,8 @@ impl EncryptedMessage {
H: VerificationHelper + DecryptionHelper,
{
let mut packets = vec![];
for pkesk in &self.pkesks {
let mut packet = vec![];
pkesk.serialize(&mut packet).map_err(Error::Sequoia)?;
let message = Message::new(&mut packets);
let mut message = ArbitraryWriter::new(message, Tag::PKESK).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
}
let mut packet = vec![];
self.message
.serialize(&mut packet)
self.serialize(&mut packets)
.map_err(Error::Sequoia)?;
let message = Message::new(&mut packets);
let mut message = ArbitraryWriter::new(message, Tag::SEIP).map_err(Error::Sequoia)?;
message.write_all(&packet).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
let mut decryptor = DecryptorBuilder::from_bytes(&packets)
.map_err(Error::Sequoia)?
@ -207,12 +241,357 @@ impl EncryptedMessage {
}
}
///
pub struct OpenPGP;
impl OpenPGP {
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
/// from a file, or from files one level deep in a directory.
///
/// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// to load certificates from the file.
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref();
if path.is_file() {
let mut vec = vec![];
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
vec.push(cert.map_err(Error::Sequoia)?);
}
Ok(vec)
} else {
let mut vec = vec![];
for entry in path
.read_dir()
.map_err(Error::Io)?
.filter_map(Result::ok)
.filter(|p| p.path().is_file())
{
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
}
Ok(vec)
}
}
}
impl Format for OpenPGP {
type Error = Error;
type PublicKey = Cert;
type PrivateKeyData = Vec<Cert>;
type SigningKey = Cert;
type EncryptedData = EncryptedMessage;
/// Derive an OpenPGP Shard certificate from the given seed.
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey {
let seed = VariableLengthSeed::new(seed);
// build cert to sign encrypted shares
let userid = UserID::from("keyfork-sss");
let path = DerivationPath::from_str("m/7366512'/0'").expect("valid derivation path");
let xprv = XPrv::new(seed)
.derive_path(&path)
.expect("valid derivation");
keyfork_derive_openpgp::derive(
xprv,
&[KeyFlags::empty().set_certification().set_signing()],
&userid,
)
.expect("valid cert creation")
}
fn format_encrypted_header(
&self,
signing_key: &Self::SigningKey,
key_data: &[Self::PublicKey],
threshold: u8,
) -> Result<Self::EncryptedData, Self::Error> {
let policy = StandardPolicy::new();
let mut pp = vec![SHARD_METADATA_VERSION, threshold];
// Note: Sequoia does not export private keys on a Cert, only on a TSK
signing_key
.serialize(&mut pp)
.expect("serialize cert into bytes");
for cert in key_data {
cert.serialize(&mut pp)
.expect("serialize pubkey into bytes");
}
// verify packet pile
let mut iter = openpgp::cert::CertParser::from_bytes(&pp[SHARD_METADATA_OFFSET..])
.expect("should have certs");
let first_cert = iter.next().transpose().ok().flatten().expect("first cert");
assert_eq!(signing_key, &first_cert);
for (packet_cert, cert) in iter.zip(key_data) {
assert_eq!(
&packet_cert.expect("parsed packet cert"),
cert,
"packet pile could not recreate cert: {}",
cert.fingerprint(),
);
}
let valid_certs = key_data
.iter()
.map(|c| c.with_policy(&policy, None))
.collect::<openpgp::Result<Vec<_>>>()
.map_err(Error::Sequoia)?;
let recipients = valid_certs.iter().flat_map(|vc| {
get_encryption_keys(vc).map(|key| Recipient::new(KeyID::wildcard(), key.key()))
});
// Process is as follows:
// * Any OpenPGP message
// * An encrypted message
// * A literal message
// * The packet pile
//
// When decrypting, OpenPGP will see:
// * A message, and parse it
// * An encrypted message, and decrypt it
// * A literal message, and extract it
// * The packet pile
let mut output = vec![];
let message = Message::new(&mut output);
let encrypted_message = Encryptor2::for_recipients(message, recipients)
.build()
.map_err(Error::Sequoia)?;
let mut literal_message = LiteralWriter::new(encrypted_message)
.build()
.map_err(Error::Sequoia)?;
literal_message.write_all(&pp).map_err(Error::SequoiaIo)?;
literal_message.finalize().map_err(Error::Sequoia)?;
// Parse it into an EncryptedMessage. Yes, this takes a serialized message
// and deserializes it. Don't think about it too hard. It's easier this way.
let mut pkesks = vec![];
for packet in PacketPile::from_reader(output.as_slice())
.map_err(Error::Sequoia)?
.into_children()
{
match packet {
Packet::PKESK(p) => pkesks.push(p),
Packet::SEIP(s) => return Ok(EncryptedMessage::new(&mut pkesks, s)),
s => panic!("Invalid variant found: {}", s.tag()),
}
}
panic!("Unable to build EncryptedMessage from PacketPile");
}
fn encrypt_shard(
&self,
shard: &[u8],
public_key: &Cert,
signing_key: &mut Self::SigningKey,
) -> Result<EncryptedMessage> {
let policy = StandardPolicy::new();
let valid_cert = public_key
.with_policy(&policy, None)
.map_err(Error::Sequoia)?;
let encryption_keys = get_encryption_keys(&valid_cert).collect::<Vec<_>>();
let signing_key = signing_key
.primary_key()
.parts_into_secret()
.map_err(Error::Sequoia)?
.key()
.clone()
.into_keypair()
.map_err(Error::Sequoia)?;
// Process is as follows:
// * Any OpenPGP message
// * An encrypted message
// * A signed message
// * A literal message
// * The shard itself
//
// When decrypting, OpenPGP will see:
// * A message, and parse it
// * An encrypted message, and decrypt it
// * A signed message, and verify it
// * A literal message, and extract it
// * The shard itself
let mut message_output = vec![];
let message = Message::new(&mut message_output);
let encrypted_message = Encryptor2::for_recipients(
message,
encryption_keys
.iter()
.map(|k| Recipient::new(KeyID::wildcard(), k.key())),
)
.build()
.map_err(Error::Sequoia)?;
let signed_message = Signer::new(encrypted_message, signing_key)
.build()
.map_err(Error::Sequoia)?;
let mut message = LiteralWriter::new(signed_message)
.build()
.map_err(Error::Sequoia)?;
message.write_all(shard).map_err(Error::SequoiaIo)?;
message.finalize().map_err(Error::Sequoia)?;
let message = EncryptedMessage::from_reader(message_output.as_slice())
.map_err(Error::Sequoia)?
.into_iter()
.next()
.expect("serialized message should be parseable");
Ok(message)
}
fn parse_shard_file(
&self,
shard_file: impl Read + Send + Sync,
) -> Result<Vec<Self::EncryptedData>, Self::Error> {
EncryptedMessage::from_reader(shard_file).map_err(Error::Sequoia)
}
fn format_shard_file(
&self,
encrypted_data: &[Self::EncryptedData],
shard_file: impl Write + Send + Sync,
) -> Result<(), Self::Error> {
let mut writer = Writer::new(shard_file, Kind::Message).map_err(Error::SequoiaIo)?;
for message in encrypted_data {
message.serialize(&mut writer).map_err(Error::Sequoia)?;
}
writer.finalize().map_err(Error::SequoiaIo)?;
Ok(())
}
fn decrypt_all_shards(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
// Be as liberal as possible when decrypting.
// We don't want to invalidate someone's keys just because the old sig expired.
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
let mut manager = SmartcardManager::new()?;
let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages.next().expect("metdata");
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
keyring.set_root_cert(root_cert.clone());
manager.set_root_cert(root_cert.clone());
// Generate a controlled binding from certificates to encrypted messages. This is stable
// because we control the order packets are encrypted and certificates are stored.
// TODO: remove alloc, convert EncryptedMessage to &EncryptedMessage
let mut messages: HashMap<KeyID, EncryptedMessage> = certs
.iter()
.map(Cert::keyid)
.zip(encrypted_messages.cloned())
.collect();
let mut decrypted_messages =
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
// clean decrypted messages from encrypted messages
messages.retain(|k, _v| !decrypted_messages.contains_key(k));
let left_from_threshold = threshold as usize - decrypted_messages.len();
if left_from_threshold > 0 {
#[allow(clippy::cast_possible_truncation)]
let new_messages = decrypt_with_manager(
left_from_threshold as u8,
&mut messages,
&certs,
&policy,
&mut manager,
)?;
decrypted_messages.extend(new_messages);
}
let shares = decrypted_messages
.values()
.map(|message| Share::try_from(message.as_slice()))
.collect::<Result<Vec<_>, &str>>()
.map_err(|e| SharksError::Share(e.to_string()))?;
Ok((shares, threshold))
}
fn decrypt_one_shard(
&self,
private_keys: Option<Self::PrivateKeyData>,
encrypted_data: &[Self::EncryptedData],
) -> std::result::Result<(Share, u8), Self::Error> {
let policy = NullPolicy::new();
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
let mut manager = SmartcardManager::new()?;
let mut encrypted_messages = encrypted_data.iter();
let metadata = encrypted_messages.next().expect("metadata");
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
keyring.set_root_cert(root_cert.clone());
manager.set_root_cert(root_cert.clone());
let mut messages: HashMap<KeyID, EncryptedMessage> = certs
.iter()
.map(Cert::keyid)
.zip(encrypted_messages.cloned())
.collect();
let decrypted_messages =
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
if let Some(message) = decrypted_messages.into_values().next() {
let share = Share::try_from(message.as_slice())
.map_err(|e| SharksError::Share(e.to_string()))?;
return Ok((share, threshold));
}
let decrypted_messages =
decrypt_with_manager(1, &mut messages, &certs, &policy, &mut manager)?;
if let Some(message) = decrypted_messages.into_values().next() {
let share = Share::try_from(message.as_slice())
.map_err(|e| SharksError::Share(e.to_string()))?;
return Ok((share, threshold));
}
panic!("unable to decrypt shard");
}
}
impl KeyDiscovery<OpenPGP> for &Path {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
OpenPGP::discover_certs(self)
}
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
todo!()
}
}
impl KeyDiscovery<OpenPGP> for &[Cert] {
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
Ok(self.to_vec())
}
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
Ok(self.to_vec())
}
}
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
/// from a file, or from files one level deep in a directory.
///
/// # Errors
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
/// to load certificates from the file.
#[deprecated]
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
let path = path.as_ref();
@ -245,6 +624,7 @@ pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
/// # Panics
/// When given packets that are not a list of PKESK packets and SEIP packets, the function panics.
/// The `split` utility should never give packets that are not in this format.
#[deprecated]
pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage>> {
let mut pkesks = Vec::new();
let mut encrypted_messages = VecDeque::new();
@ -416,6 +796,7 @@ fn decrypt_metadata(
})
}
#[deprecated]
fn decrypt_one(
messages: Vec<EncryptedMessage>,
certs: &[Cert],
@ -465,13 +846,14 @@ fn decrypt_one(
/// The function may panic if a share is decrypted but has a length larger than 256 bits. This is
/// atypical usage and should not be encountered in normal usage, unless something that is not a
/// Keyfork seed has been fed into [`split`].
#[deprecated]
#[allow(deprecated)]
pub fn decrypt(
certs: &[Cert],
metadata: &EncryptedMessage,
encrypted_messages: &[EncryptedMessage],
) -> Result<()> {
let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None;
@ -496,8 +878,11 @@ pub fn decrypt(
let validator = MnemonicSetValidator {
word_lengths: [9, 24],
};
let [nonce_mnemonic, pubkey_mnemonic] =
pm.prompt_validated_wordlist(QRCODE_COULDNT_READ, &wordlist, 3, validator.to_fn())?;
let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let nonce = nonce_mnemonic
.as_bytes()
@ -514,8 +899,7 @@ pub fn decrypt(
let nonce = Nonce::<U12>::from_slice(&nonce);
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
@ -560,7 +944,7 @@ pub fn decrypt(
}
// safety: size of out_bytes is constant and always % 4 == 0
let payload_mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
#[cfg(feature = "qrcode")]
{
@ -592,6 +976,7 @@ pub fn decrypt(
/// # Errors
/// The function may return an error if an error occurs while decrypting shards, parsing shards, or
/// combining the shards into a secret.
#[deprecated]
pub fn combine(
certs: Vec<Cert>,
metadata: &EncryptedMessage,
@ -679,6 +1064,7 @@ pub fn combine(
///
/// The function may panic if the metadata can't properly store the certificates used to generate
/// the encrypted shares.
#[deprecated]
pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write) -> Result<()> {
let seed = VariableLengthSeed::new(secret);
// build cert to sign encrypted shares

View File

@ -111,12 +111,10 @@ impl DecryptionHelper for &mut Keyring {
pkesk.recipient().is_wildcard()
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient())
}) {
#[allow(deprecated, clippy::map_flatten)]
let name = cert
.userids()
.next()
.map(|userid| userid.userid().name().transpose())
.flatten()
.and_then(|userid| userid.userid().name2().transpose())
.transpose()
.ok()
.flatten();

View File

@ -43,3 +43,4 @@ openpgp-card-sequoia = { version = "0.2.0", default-features = false }
openpgp-card = "0.4.1"
clap_complete = { version = "4.4.6", optional = true }
sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] }
keyfork-bin = { version = "0.1.0", path = "../util/keyfork-bin" }

View File

@ -109,7 +109,7 @@ impl MnemonicSeedSource {
MnemonicSeedSource::Tarot => todo!(),
MnemonicSeedSource::Dice => todo!(),
};
let mnemonic = keyfork_mnemonic_util::Mnemonic::from_entropy(&seed, Default::default())?;
let mnemonic = keyfork_mnemonic_util::Mnemonic::from_bytes(&seed)?;
Ok(mnemonic.to_string())
}
}

View File

@ -2,11 +2,8 @@ use super::Keyfork;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
use keyfork_mnemonic_util::Mnemonic;
use keyfork_shard::{
openpgp::{combine, discover_certs, parse_messages},
remote_decrypt,
};
use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_shard::{remote_decrypt, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -37,15 +34,10 @@ impl RecoverSubcommands {
} => {
let content = std::fs::read_to_string(shard_file)?;
if content.contains("BEGIN PGP MESSAGE") {
let certs = key_discovery
.as_ref()
.map(discover_certs)
.transpose()?
.unwrap_or(vec![]);
let mut messages = parse_messages(content.as_bytes())?;
let metadata = messages.pop_front().expect("any pgp encrypted message");
let mut seed = vec![];
combine(certs, &metadata, messages.into(), &mut seed)?;
let openpgp = keyfork_shard::openpgp::OpenPGP;
// TODO: remove .clone() by making handle() consume self
let seed = openpgp
.decrypt_all_shards_to_secret(key_discovery.as_deref(), content.as_bytes())?;
Ok(seed)
} else {
panic!("unknown format of shard file");
@ -69,9 +61,8 @@ impl RecoverSubcommands {
let validator = MnemonicChoiceValidator {
word_lengths: [WordLength::Count(12), WordLength::Count(24)],
};
let mnemonic = term.prompt_validated_wordlist(
let mnemonic = term.prompt_validated_wordlist::<English, _>(
"Mnemonic: ",
&Default::default(),
3,
validator.to_fn(),
)?;
@ -90,7 +81,7 @@ pub struct Recover {
impl Recover {
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
let seed = self.command.handle()?;
let mnemonic = Mnemonic::from_entropy(&seed, Default::default())?;
let mnemonic = Mnemonic::from_bytes(&seed)?;
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()

View File

@ -1,5 +1,6 @@
use super::Keyfork;
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
use keyfork_shard::Format as _;
use std::{
io::{stdin, stdout, Read, Write},
path::{Path, PathBuf},
@ -31,27 +32,23 @@ trait ShardExec {
&self,
threshold: u8,
max: u8,
key_discovery: impl AsRef<Path>,
key_discovery: &Path,
secret: &[u8],
output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>>;
fn combine(
&self,
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>;
fn combine<T>(
fn decrypt(
&self,
key_discovery: Option<T>,
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>;
fn decrypt<T>(
&self,
key_discovery: Option<T>,
input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>;
) -> Result<(), Box<dyn std::error::Error>>;
}
#[derive(Clone, Debug)]
@ -62,77 +59,36 @@ impl ShardExec for OpenPGP {
&self,
threshold: u8,
max: u8,
key_discovery: impl AsRef<Path>,
key_discovery: &Path,
secret: &[u8],
output: &mut impl Write,
output: &mut (impl Write + Send + Sync),
) -> Result<(), Box<dyn std::error::Error>> {
// Get certs and input
let certs = keyfork_shard::openpgp::discover_certs(key_discovery.as_ref())?;
assert_eq!(
certs.len(),
max.into(),
"cert count {} != max {max}",
certs.len()
);
keyfork_shard::openpgp::split(threshold, certs, secret, output).map_err(Into::into)
let opgp = keyfork_shard::openpgp::OpenPGP;
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
}
fn combine<T>(
fn combine(
&self,
key_discovery: Option<T>,
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
output: &mut impl Write,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>,
{
let certs = key_discovery
.map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref()))
.transpose()?
.unwrap_or(vec![]);
let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
let mut bytes = vec![];
keyfork_shard::openpgp::combine(
certs,
&encrypted_metadata,
encrypted_messages.into(),
&mut bytes,
)?;
write!(output, "{}", smex::encode(&bytes))?;
let openpgp = keyfork_shard::openpgp::OpenPGP;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?;
write!(output, "{}", smex::encode(bytes))?;
Ok(())
}
fn decrypt<T>(
fn decrypt(
&self,
key_discovery: Option<T>,
key_discovery: Option<&Path>,
input: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>>
where
T: AsRef<Path>,
{
let certs = key_discovery
.map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref()))
.transpose()?
.unwrap_or(vec![]);
let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?;
let encrypted_metadata = encrypted_messages
.pop_front()
.expect("any pgp encrypted message");
keyfork_shard::openpgp::decrypt(
&certs,
&encrypted_metadata,
encrypted_messages.make_contiguous(),
)?;
let openpgp = keyfork_shard::openpgp::OpenPGP;
openpgp.decrypt_one_shard_for_transport(key_discovery, input)?;
Ok(())
}
}
@ -225,7 +181,7 @@ impl ShardSubcommands {
match format {
Some(Format::OpenPGP(o)) => {
o.decrypt(key_discovery.as_ref(), shard_content.as_bytes())
o.decrypt(key_discovery.as_deref(), shard_content.as_bytes())
}
Some(Format::P256(_p)) => todo!(),
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
@ -242,7 +198,7 @@ impl ShardSubcommands {
match format {
Some(Format::OpenPGP(o)) => o.combine(
key_discovery.as_ref(),
key_discovery.as_deref(),
shard_content.as_bytes(),
&mut stdout,
),

View File

@ -15,6 +15,8 @@ use keyfork_prompt::{
Message, PromptHandler, Terminal,
};
use keyfork_shard::{Format, openpgp::OpenPGP};
#[derive(thiserror::Error, Debug)]
#[error("Invalid PIN length: {0}")]
pub struct PinLength(usize);
@ -163,11 +165,13 @@ fn generate_shard_secret(
certs.push(cert);
}
let opgp = OpenPGP;
if let Some(output_file) = output_file {
let output = File::create(output_file)?;
keyfork_shard::openpgp::split(threshold, certs, &seed, output)?;
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
} else {
keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?;
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], std::io::stdout())?;
}
Ok(())
}

View File

@ -6,21 +6,16 @@ use std::process::ExitCode;
use clap::Parser;
use keyfork_bin::{Bin, ClosureBin};
mod cli;
mod config;
fn main() -> ExitCode {
let bin = ClosureBin::new(|| {
let opts = cli::Keyfork::parse();
opts.command.handle(&opts)
});
if let Err(e) = opts.command.handle(&opts) {
eprintln!("Unable to run command: {e}");
let mut source = e.source();
while let Some(new_error) = source.take() {
eprintln!("Source: {new_error}");
source = new_error.source();
}
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
bin.main()
}

View File

@ -0,0 +1,11 @@
[package]
name = "keyfork-bin"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
[dev-dependencies]
anyhow = "1.0.79"

View File

@ -0,0 +1,140 @@
#![allow(clippy::needless_doctest_main)]
//! A convenient trait for quickly writing binaries in a consistent pattern.
//!
//! # Examples
//! ```rust
//! use anyhow::anyhow;
//! use keyfork_bin::Bin;
//!
//! struct Main;
//!
//! impl Bin for Main {
//! type Args = (String, String);
//!
//! fn usage_hint(&self) -> Option<String> {
//! Some(String::from("<param1> <param2>"))
//! }
//!
//! fn validate_args(&self, mut args: impl Iterator<Item = String>) -> keyfork_bin::ProcessResult<Self::Args> {
//! let arg1 = args.next().ok_or(anyhow!("missing argument 1"))?;
//! let arg2 = args.next().ok_or(anyhow!("missing argument 2"))?;
//! Ok((arg1, arg2))
//! }
//!
//! fn run(&self, (arg1, arg2): Self::Args) -> keyfork_bin::ProcessResult {
//! println!("First argument: {arg1}");
//! println!("Second argument: {arg2}");
//! Ok(())
//! }
//!#
//!# fn main(&self) -> std::process::ExitCode {
//!# self.main_inner([String::from("hello"), String::from("world")].into_iter())
//!# }
//! }
//!
//! fn main() {
//! // Assume the program was called with something like "hello world"...
//! let bin = Main;
//! bin.main();
//! }
//! ```
use std::process::ExitCode;
/// A result that may contain any error.
pub type ProcessResult<T = ()> = Result<T, Box<dyn std::error::Error>>;
fn report_err(e: Box<dyn std::error::Error>) {
eprintln!("Unable to run command: {e}");
let mut source = e.source();
while let Some(new_error) = source.take() {
eprintln!("- Caused by: {new_error}");
source = new_error.source();
}
}
/// A trait for implementing the flow of a binary's execution.
pub trait Bin {
/// The type for command-line arguments required by the function.
type Args;
/// A usage hint for how the arguments should be provided to the program.
fn usage_hint(&self) -> Option<String> {
None
}
/// Validate the arguments provided by the user into types required by the binary.
#[allow(clippy::missing_errors_doc)]
fn validate_args(&self, args: impl Iterator<Item = String>) -> ProcessResult<Self::Args>;
/// Run the binary
#[allow(clippy::missing_errors_doc)]
fn run(&self, args: Self::Args) -> ProcessResult;
/// The default handler for running the binary and reporting any errors.
fn main(&self) -> ExitCode {
self.main_inner(std::env::args())
}
#[doc(hidden)]
fn main_inner(&self, mut args: impl Iterator<Item = String>) -> ExitCode {
let command = args.next();
let args = match self.validate_args(args) {
Ok(args) => args,
Err(e) => {
if let (Some(command), Some(hint)) = (command, self.usage_hint()) {
eprintln!("Usage: {command} {hint}");
}
report_err(e);
return ExitCode::FAILURE;
}
};
if let Err(e) = self.run(args) {
report_err(e);
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
}
}
/// A Bin that doesn't take any arguments.
pub struct ClosureBin<F: Fn() -> ProcessResult> {
closure: F
}
impl<F> ClosureBin<F> where F: Fn() -> ProcessResult {
/// Create a new Bin from a closure.
///
/// # Examples
/// ```rust
/// use keyfork_bin::{Bin, ClosureBin};
///
/// let bin = ClosureBin::new(|| {
/// println!("Hello, world!");
/// Ok(())
/// });
///
/// bin.main();
/// ```
pub fn new(closure: F) -> Self {
Self {
closure
}
}
}
impl<F> Bin for ClosureBin<F> where F: Fn() -> ProcessResult {
type Args = ();
fn validate_args(&self, _args: impl Iterator<Item = String>) -> ProcessResult<Self::Args> {
Ok(())
}
fn run(&self, _args: Self::Args) -> ProcessResult {
let c = &self.closure;
c()
}
}

View File

@ -57,10 +57,21 @@ fn ensure_offline() {
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// keyfork_entropy::ensure_safe();
/// ```
///
/// When running on a system that's online, or running an outdated kernel:
///
/// ```rust,should_panic
/// # // NOTE: sometimes, the environment variable is set, for testing purposes. I'm not sure how
/// # // to un-set it. Set it to a sentinel value.
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "test-must-fail");
/// # std::env::set_var("INSECURE_HARDWARE_ALLOWED", "test-must-fail");
/// keyfork_entropy::ensure_safe();
/// ```
pub fn ensure_safe() {
if !std::env::vars()
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
{
if !std::env::vars().any(|(name, value)| {
(name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
&& value != "test-must-fail"
}) {
ensure_safe_kernel_version();
ensure_offline();
}

View File

@ -16,7 +16,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
);
let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
println!("{}", smex::encode(&entropy));
println!("{}", smex::encode(entropy));
Ok(())
}

View File

@ -66,6 +66,11 @@ pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
/// # Errors
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
/// constraint on a protocol level.
///
/// # Examples
/// ```rust
/// let data = keyfork_frame::try_encode(b"hello world!".as_slice()).unwrap();
/// ```
pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
let mut output = vec![];
try_encode_to(data, &mut output)?;
@ -77,6 +82,12 @@ pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
/// # Errors
/// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer
/// is unable to write data.
///
/// # Examples
/// ```rust
/// let mut output = vec![];
/// keyfork_frame::try_encode_to(b"hello world!".as_slice(), &mut output).unwrap();
/// ```
pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
let hash = hash(data);
let len = hash.len() + data.len();
@ -107,18 +118,40 @@ pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> {
/// * The given `data` does not contain enough data to parse a length,
/// * The given `data` does not contain the given length's worth of data,
/// * The given `data` has a checksum that does not match what we build locally.
///
/// # Examples
/// ```rust
/// let input = b"hello world!";
/// let encoded = keyfork_frame::try_encode(input.as_slice()).unwrap();
/// let decoded = keyfork_frame::try_decode(&encoded).unwrap();
/// assert_eq!(input.as_slice(), decoded.as_slice());
/// ```
pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
try_decode_from(&mut &data[..])
}
/// Read and decode a framed message into a `Vec<u8>`.
///
/// Note that unlike [`try_encode_to`], this method does not allow writing to an object
/// implementing Write. This is because the data must be stored entirely in memory to allow
/// verifying the data. The data is then returned using the same in-memory representation as is
/// used in memory, and a caller may then choose to use `writable.write_all()`.
///
/// # Errors
/// An error may be returned if:
/// * The given `data` does not contain enough data to parse a length,
/// * The given `data` does not contain the given length's worth of data,
/// * The given `data` has a checksum that does not match what we build locally.
/// * The source for the data returned an error.
///
/// # Examples
/// ```rust
/// let input = b"hello world!";
/// let mut encoded = vec![];
/// keyfork_frame::try_encode_to(input.as_slice(), &mut encoded).unwrap();
/// let decoded = keyfork_frame::try_decode_from(&mut &encoded[..]).unwrap();
/// assert_eq!(input.as_slice(), decoded.as_slice());
/// ```
pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
let mut bytes = 0u32.to_be_bytes();
readable.read_exact(&mut bytes)?;

View File

@ -8,7 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
input.read_line(&mut line)?;
let decoded = smex::decode(line.trim())?;
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&decoded, Default::default()) };
let mnemonic = unsafe { Mnemonic::from_raw_bytes(&decoded) };
println!("{mnemonic}");

View File

@ -1,6 +1,60 @@
//! Zero-dependency Mnemonic encoding and decoding.
//! Zero-dependency mnemonic encoding and decoding of data.
//!
//! Mnemonics can be used to safely encode data of 32, 48, and 64 bytes as a phrase:
//!
//! ```rust
//! use keyfork_mnemonic_util::Mnemonic;
//! let data = b"Hello, world! I am a mnemonic :)";
//! assert_eq!(data.len(), 32);
//! let mnemonic = Mnemonic::from_bytes(data).unwrap();
//! println!("Our mnemonic is: {mnemonic}");
//! ```
//!
//! A mnemonic can also be parsed from a string:
//!
//! ```rust
//! use keyfork_mnemonic_util::Mnemonic;
//! use std::str::FromStr;
//!
//! let data = b"Hello, world! I am a mnemonic :)";
//! let words = "embody clock brand tattoo search desert saddle eternal
//! goddess animal banner dolphin bitter mother loyal asset
//! hover clock forward system normal mosquito trim credit";
//! let mnemonic = Mnemonic::from_str(words).unwrap();
//! assert_eq!(&data[..], mnemonic.as_bytes());
//! ```
//!
//! Mnemonics can also be used to store data of other lengths, but such functionality is not
//! verified to be safe:
//!
//! ```rust
//! use keyfork_mnemonic_util::Mnemonic;
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
//! let mnemonic_text = mnemonic.to_string();
//! ```
//!
//! If given an invalid length, undefined behavior may follow, or code may panic.
//!
//! ```rust,should_panic
//! use keyfork_mnemonic_util::Mnemonic;
//! use std::str::FromStr;
//!
//! // NOTE: Data is of invalid length, 31
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
//! let mnemonic_text = mnemonic.to_string();
//! // NOTE: panic happens here
//! let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
//! ```
use std::{error::Error, fmt::Display, str::FromStr, sync::Arc};
use std::{
error::Error,
fmt::Display,
str::FromStr,
sync::OnceLock,
marker::PhantomData,
};
use hmac::Hmac;
use pbkdf2::pbkdf2;
@ -39,93 +93,65 @@ impl Display for MnemonicGenerationError {
impl Error for MnemonicGenerationError {}
/// A BIP-0039 compatible list of words.
#[derive(Debug, Clone)]
pub struct Wordlist(Vec<String>);
/// A trait representing a BIP-0039 wordlist, of 2048 words, with each word having a unique first
/// three letters.
pub trait Wordlist: std::fmt::Debug {
/// Get a reference to a [`std::sync::OnceLock`] Self.
fn get_singleton<'a>() -> &'a Self;
impl Default for Wordlist {
/// Returns the English wordlist in the Bitcoin BIP-0039 specification.
fn default() -> Self {
// TODO: English is the only supported language.
let wordlist_file = include_str!("data/wordlist.txt");
Wordlist(
wordlist_file
.lines()
// skip 1: comment at top of file to point to BIP-0039 source.
.skip(1)
.map(|x| x.trim().to_string())
.collect(),
)
}
/// Return a representation of the words in the wordlist as an array of [`str`].
fn to_str_array(&self) -> [&str; 2048];
}
impl Wordlist {
/// Return an Arced version of the Wordlist
#[allow(clippy::must_use_candidate)]
pub fn arc(self) -> Arc<Self> {
Arc::new(self)
/// A wordlist for the English language, from the BIP-0039 dataset.
#[derive(Debug)]
pub struct English {
words: [String; 2048],
}
static ENGLISH: OnceLock<English> = OnceLock::new();
impl Wordlist for English {
fn get_singleton<'a>() -> &'a Self {
ENGLISH.get_or_init(|| {
let wordlist_file = include_str!("data/wordlist.txt");
let mut words = wordlist_file
.lines()
.skip(1)
.map(|x| x.trim().to_string());
English {
words: std::array::from_fn(|_| words.next().expect("wordlist has 2048 words")),
}
})
}
/// Determine whether the Wordlist contains a given word.
pub fn contains(&self, word: &str) -> bool {
self.0.iter().any(|w| w.as_str() == word)
}
/// Given an index, get a word from the wordlist.
pub fn get_word(&self, word: usize) -> Option<&String> {
self.0.get(word)
}
/*
fn inner(&self) -> &Vec<String> {
&self.0
}
*/
#[cfg(test)]
fn into_inner(self) -> Vec<String> {
self.0
fn to_str_array(&self) -> [&str; 2048] {
std::array::from_fn(|i| self.words[i].as_str())
}
}
/// A BIP-0039 mnemonic with reference to a [`Wordlist`].
#[derive(Debug, Clone)]
pub struct Mnemonic {
entropy: Vec<u8>,
// words: Vec<usize>,
wordlist: Arc<Wordlist>,
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MnemonicBase<W: Wordlist> {
data: Vec<u8>,
marker: PhantomData<W>,
}
impl Display for Mnemonic {
/// A default Mnemonic using the English language.
pub type Mnemonic = MnemonicBase<English>;
impl<W> Display for MnemonicBase<W>
where
W: Wordlist,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let bit_count = self.entropy.len() * 8;
let mut bits = vec![false; bit_count + bit_count / 32];
let wordlist = W::get_singleton();
let words = wordlist.to_str_array();
for byte_index in 0..bit_count / 8 {
for bit_index in 0..8 {
bits[byte_index * 8 + bit_index] =
(self.entropy[byte_index] & (1 << (7 - bit_index))) > 0;
}
}
let mut hasher = Sha256::new();
hasher.update(&self.entropy);
let hash = hasher.finalize().to_vec();
for check_bit in 0..bit_count / 32 {
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
}
let mut iter = bits
.chunks_exact(11)
.peekable()
.map(|chunk| {
let mut num = 0usize;
for i in 0..11 {
num += usize::from(chunk[10 - i]) << i;
}
num
})
.filter_map(|word| self.wordlist.get_word(word))
let mut iter = self
.words()
.into_iter()
.filter_map(|word| words.get(word))
.peekable();
while let Some(word) = iter.next() {
f.write_str(word)?;
@ -170,17 +196,20 @@ impl Display for MnemonicFromStrError {
impl Error for MnemonicFromStrError {}
impl FromStr for Mnemonic {
impl<W> FromStr for MnemonicBase<W>
where
W: Wordlist,
{
type Err = MnemonicFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let wordlist = W::get_singleton();
let wordlist_words = wordlist.to_str_array();
let words: Vec<_> = s.split_whitespace().collect();
let mut usize_words = vec![];
let wordlist = Wordlist::default().arc();
let mut bits = vec![false; words.len() * 11];
for (index, word) in words.iter().enumerate() {
let word = wordlist
.0
let word = wordlist_words
.iter()
.position(|w| w == word)
.ok_or(MnemonicFromStrError::InvalidWord(index))?;
@ -197,7 +226,7 @@ impl FromStr for Mnemonic {
bits.truncate(bits.len() * 32 / 33);
// bits.truncate(bits.len() - bits.len() % 32);
let entropy: Vec<u8> = bits
let data: Vec<u8> = bits
.chunks_exact(8)
.map(|chunk| {
let mut num = 0u8;
@ -209,7 +238,7 @@ impl FromStr for Mnemonic {
.collect();
let mut hasher = Sha256::new();
hasher.update(&entropy);
hasher.update(&data);
let hash = hasher.finalize().to_vec();
for (i, bit) in checksum_bits.iter().enumerate() {
@ -218,23 +247,27 @@ impl FromStr for Mnemonic {
}
}
Ok(Mnemonic {
entropy,
// words: usize_words,
wordlist,
})
Ok(MnemonicBase { data, marker: PhantomData })
}
}
impl Mnemonic {
/// Generate a [`Mnemonic`] from the provided entropy and [`Wordlist`].
impl<W> MnemonicBase<W>
where
W: Wordlist,
{
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of 128, 192, or 256 bits, as per BIP-0039.
///
/// # Errors
/// An error may be returned if the entropy is not within the acceptable lengths.
pub fn from_entropy(
bytes: &[u8],
wordlist: Arc<Wordlist>,
) -> Result<Mnemonic, MnemonicGenerationError> {
/// An error may be returned if the data is not within the expected lengths.
///
/// # Examples
/// ```rust
/// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = Mnemonic::from_bytes(data.as_slice()).unwrap();
/// ```
pub fn from_bytes(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
let bit_count = bytes.len() * 8;
if bit_count % 32 != 0 {
@ -245,88 +278,150 @@ impl Mnemonic {
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
}
Ok(unsafe { Self::from_raw_entropy(bytes, wordlist) })
Ok(unsafe { Self::from_raw_bytes(bytes) })
}
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
/// of 128, 192, or 256 bits, as per BIP-0039.
///
/// # Errors
/// An error may be returned if the data is not within the expected lengths.
#[deprecated = "use Mnemonic::from_bytes"]
pub fn from_entropy(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
MnemonicBase::from_bytes(bytes)
}
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
///
/// # Safety
///
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
/// == 0`.
pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc<Wordlist>) -> Mnemonic {
Mnemonic {
entropy: bytes.to_vec(),
wordlist,
/// == 0`. If the assumption is incorrect, code may panic.
///
/// # Examples
/// ```rust
/// use keyfork_mnemonic_util::Mnemonic;
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// let mnemonic_text = mnemonic.to_string();
/// ```
///
/// If given an invalid length, undefined behavior may follow, or code may panic.
///
/// ```rust,should_panic
/// use keyfork_mnemonic_util::Mnemonic;
/// use std::str::FromStr;
///
/// // NOTE: Data is of invalid length, 31
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
/// let mnemonic_text = mnemonic.to_string();
/// // NOTE: panic happens here
/// let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
/// ```
pub unsafe fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
MnemonicBase {
data: bytes.to_vec(),
marker: PhantomData,
}
}
/// The internal representation of the decoded data.
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
///
/// # Safety
///
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
/// == 0`. If the assumption is incorrect, code may panic.
#[deprecated = "use Mnemonic::from_raw_bytes"]
pub unsafe fn from_raw_entropy(bytes: &[u8]) -> MnemonicBase<W> {
MnemonicBase {
data: bytes.to_vec(),
marker: PhantomData,
}
}
/// A view to internal representation of the decoded data.
pub fn as_bytes(&self) -> &[u8] {
&self.entropy
&self.data
}
/// The internal representation of the decoded data, as a [`Vec<u8>`].
/// A clone of the internal representation of the decoded data.
pub fn to_bytes(&self) -> Vec<u8> {
self.entropy.to_vec()
self.data.to_vec()
}
/// Drop self, returning the decoded data.
/// Conver the Mnemonic into the internal representation of the decoded data.
pub fn into_bytes(self) -> Vec<u8> {
self.entropy
self.data
}
/// Clone the existing entropy.
/// Clone the existing data.
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> {
self.entropy.clone()
self.data.clone()
}
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
///
/// # Errors
/// The method may return an error if the pbkdf2 function returns an invalid length, but this
/// case should not be reached.
/// The method should not return an error.
#[deprecated = "Use generate_seed() instead"]
pub fn seed<'a>(
&self,
passphrase: impl Into<Option<&'a str>>,
) -> Result<Vec<u8>, MnemonicGenerationError> {
Ok(self.generate_seed(passphrase))
}
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
///
/// # Panics
/// The function may panic if the HmacSha512 function returns an error. The only error the
/// HmacSha512 function should return is an invalid length, which should not be possible.
///
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> Vec<u8> {
let passphrase = passphrase.into();
let mut seed = [0u8; 64];
let mnemonic = self.to_string();
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
.map_err(|_| MnemonicGenerationError::InvalidPbkdf2Length)?;
Ok(seed.to_vec())
.expect("HmacSha512 InvalidLength should be infallible");
seed.to_vec()
}
/// Encode the mnemonic into a list of wordlist indexes.
pub fn words(self) -> (Vec<usize>, Arc<Wordlist>) {
let bit_count = self.entropy.len() * 8;
/// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a
/// BIP-0039 wordlist.
pub fn words(&self) -> Vec<usize> {
let bit_count = self.data.len() * 8;
let mut bits = vec![false; bit_count + bit_count / 32];
for byte_index in 0..bit_count / 8 {
for bit_index in 0..8 {
bits[byte_index * 8 + bit_index] =
(self.entropy[byte_index] & (1 << (7 - bit_index))) > 0;
(self.data[byte_index] & (1 << (7 - bit_index))) > 0;
}
}
let mut hasher = Sha256::new();
hasher.update(&self.entropy);
hasher.update(&self.data);
let hash = hasher.finalize().to_vec();
for check_bit in 0..bit_count / 32 {
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
}
let words = bits.chunks_exact(11).peekable().map(|chunk| {
// TODO: find a way to not have to collect to vec
bits.chunks_exact(11).peekable().map(|chunk| {
let mut num = 0usize;
for i in 0..11 {
num += usize::from(chunk[10 - i]) << i;
}
num
});
(words.collect(), self.wordlist.clone())
}).collect()
}
}
@ -337,13 +432,8 @@ mod tests {
use super::*;
#[test]
fn wordlist_word_count_correct() {
let wordlist = Wordlist::default().into_inner();
assert_eq!(
wordlist.len(),
2usize.pow(11),
"Wordlist did not include correct word count"
);
fn can_load_wordlist() {
let _wordlist = English::get_singleton();
}
#[test]
@ -351,8 +441,7 @@ mod tests {
let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap();
let wordlist = Wordlist::default().arc();
let mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
let mnemonic = super::Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let new_entropy = mnemonic.as_bytes();
assert_eq!(new_entropy, entropy);
}
@ -361,7 +450,6 @@ mod tests {
fn conforms_to_trezor_tests() {
let content = include_str!("data/vectors.json");
let jsonobj: serde_json::Value = serde_json::from_str(content).unwrap();
let wordlist = Wordlist::default().arc();
for test in jsonobj["english"].as_array().unwrap() {
let [ref hex_, ref seed, ..] = test.as_array().unwrap()[..] else {
@ -369,7 +457,7 @@ mod tests {
};
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
let mnemonic = Mnemonic::from_entropy(&hex, wordlist.clone()).unwrap();
let mnemonic = Mnemonic::from_bytes(&hex).unwrap();
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
}
@ -380,17 +468,16 @@ mod tests {
let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap();
let wordlist = Wordlist::default().arc();
let my_mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
let my_mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
assert_eq!(my_mnemonic.seed(None).unwrap(), their_mnemonic.to_seed(""));
assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed(""));
assert_eq!(
my_mnemonic.seed("testing").unwrap(),
my_mnemonic.generate_seed("testing"),
their_mnemonic.to_seed("testing")
);
assert_ne!(
my_mnemonic.seed("test1").unwrap(),
my_mnemonic.generate_seed("test1"),
their_mnemonic.to_seed("test2")
);
}
@ -400,14 +487,13 @@ mod tests {
let tests = 100_000;
let mut count = 0.;
let entropy = &mut [0u8; 256 / 8];
let wordlist = Wordlist::default().arc();
let mut random = std::fs::File::open("/dev/urandom").unwrap();
let mut hs = HashSet::<usize>::with_capacity(24);
for _ in 0..tests {
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], wordlist.clone()).unwrap();
let (words, _) = mnemonic.words();
let mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let words = mnemonic.words();
hs.clear();
hs.extend(words);
if hs.len() != 24 {
@ -435,11 +521,10 @@ mod tests {
#[test]
fn can_do_up_to_1024_bits() {
let entropy = &mut [0u8; 128];
let wordlist = Wordlist::default().arc();
let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&entropy[..], wordlist.clone()) };
let (words, _) = mnemonic.words();
let mnemonic = unsafe { Mnemonic::from_raw_bytes(&entropy[..]) };
let words = mnemonic.words();
assert!(words.len() == 96);
}
}

View File

@ -7,6 +7,8 @@ use keyfork_prompt::{
Terminal, PromptHandler,
};
use keyfork_mnemonic_util::English;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut mgr = Terminal::new(stdin(), stdout())?;
let transport_validator = mnemonic::MnemonicSetValidator {
@ -16,18 +18,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
word_lengths: [24, 48],
};
let mnemonics = mgr.prompt_validated_wordlist(
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
"Enter a 9-word and 24-word mnemonic: ",
&Default::default(),
3,
transport_validator.to_fn(),
)?;
assert_eq!(mnemonics[0].as_bytes().len(), 12);
assert_eq!(mnemonics[1].as_bytes().len(), 32);
let mnemonics = mgr.prompt_validated_wordlist(
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
"Enter a 24 and 48-word mnemonic: ",
&Default::default(),
3,
combine_validator.to_fn(),
)?;

View File

@ -51,31 +51,31 @@ pub trait PromptHandler {
/// could not be read.
fn prompt_input(&mut self, prompt: &str) -> Result<String>;
/// Prompt the user for input based on a wordlist.
/// Prompt the user for input based on a wordlist. A language must be specified as the generic
/// parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed or if the input
/// could not be read.
#[cfg(feature = "mnemonic")]
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String>;
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String> where X: Wordlist;
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
/// provided parser function, returning the type from the parser.
/// provided parser function, returning the type from the parser. A language must be specified
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
///
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
#[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<V, F, E>(
fn prompt_validated_wordlist<X, V>(
&mut self,
prompt: &str,
wordlist: &Wordlist,
retries: u8,
validator_fn: F,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
E: std::error::Error;
X: Wordlist;
/// Prompt the user for a passphrase, which is hidden while typing.
///
@ -90,15 +90,12 @@ pub trait PromptHandler {
/// # Errors
/// The method may return an error if the message was not able to be displayed, if the input
/// could not be read, or if the parser returned an error.
fn prompt_validated_passphrase<V, F, E>(
fn prompt_validated_passphrase<V>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: F,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
E: std::error::Error;
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>;
/// Prompt the user with a [`Message`].
///

View File

@ -1,6 +1,7 @@
use std::{
borrow::Borrow,
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
os::fd::AsRawFd, borrow::Borrow,
os::fd::AsRawFd,
};
use keyfork_crossterm::{
@ -12,7 +13,7 @@ use keyfork_crossterm::{
ExecutableCommand, QueueableCommand,
};
use crate::{PromptHandler, Message, Wordlist, Error};
use crate::{Error, Message, PromptHandler, Wordlist};
#[allow(missing_docs)]
pub type Result<T, E = Error> = std::result::Result<T, E>;
@ -155,11 +156,13 @@ where
fn lock(&mut self) -> TerminalGuard<'_, R, W> {
TerminalGuard::new(&mut self.read, &mut self.write, &mut self.terminal)
}
}
impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + AsRawFd + Sized {
impl<R, W> PromptHandler for Terminal<R, W>
where
R: Read + Sized,
W: Write + AsRawFd + Sized,
{
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
let mut terminal = self.lock().alternate_screen()?;
terminal
@ -182,20 +185,18 @@ impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + As
}
#[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<V, F, E>(
fn prompt_validated_wordlist<X, V>(
&mut self,
prompt: &str,
wordlist: &Wordlist,
retries: u8,
validator_fn: F,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
E: std::error::Error,
X: Wordlist,
{
let mut last_error = None;
for _ in 0..retries {
let s = self.prompt_wordlist(prompt, wordlist)?;
let s = self.prompt_wordlist::<X>(prompt)?;
match validator_fn(s) {
Ok(v) => return Ok(v),
Err(e) => {
@ -214,7 +215,13 @@ impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + As
#[cfg(feature = "mnemonic")]
#[allow(clippy::too_many_lines)]
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String> {
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String>
where
X: Wordlist,
{
let wordlist = X::get_singleton();
let words = wordlist.to_str_array();
let mut terminal = self
.lock()
.alternate_screen()?
@ -316,7 +323,7 @@ impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + As
let mut iter = printable_input.split_whitespace().peekable();
while let Some(word) = iter.next() {
if wordlist.contains(word) {
if words.contains(&word) {
terminal.queue(PrintStyledContent(word.green()))?;
} else {
terminal.queue(PrintStyledContent(word.red()))?;
@ -337,16 +344,12 @@ impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + As
Ok(input)
}
fn prompt_validated_passphrase<V, F, E>(
fn prompt_validated_passphrase<V>(
&mut self,
prompt: &str,
retries: u8,
validator_fn: F,
) -> Result<V, Error>
where
F: Fn(String) -> Result<V, E>,
E: std::error::Error,
{
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error> {
let mut last_error = None;
for _ in 0..retries {
let s = self.prompt_passphrase(prompt)?;

View File

@ -12,7 +12,7 @@ pub trait Validator {
type Error;
/// Create a validator function from the given parameters.
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>>;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>>;
}
/// A PIN could not be validated from the given input.
@ -48,7 +48,7 @@ impl Validator for PinValidator {
type Output = String;
type Error = PinError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, PinError>> {
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>> {
let min_len = self.min_length.unwrap_or(usize::MIN);
let max_len = self.max_length.unwrap_or(usize::MAX);
let range = self.range.clone().unwrap_or('0'..='9');
@ -56,14 +56,14 @@ impl Validator for PinValidator {
s.truncate(s.trim_end().len());
let len = s.len();
if len < min_len {
return Err(PinError::TooShort(len, min_len));
return Err(Box::new(PinError::TooShort(len, min_len)));
}
if len > max_len {
return Err(PinError::TooLong(len, max_len));
return Err(Box::new(PinError::TooLong(len, max_len)));
}
for (index, ch) in s.chars().enumerate() {
if !range.contains(&ch) {
return Err(PinError::InvalidCharacters(ch, index));
return Err(Box::new(PinError::InvalidCharacters(ch, index)));
}
}
Ok(s)
@ -123,13 +123,13 @@ pub mod mnemonic {
type Output = Mnemonic;
type Error = MnemonicValidationError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Mnemonic, Self::Error>> {
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Mnemonic, Box<dyn std::error::Error>>> {
let word_length = self.word_length.clone();
Box::new(move |s: String| match word_length.as_ref() {
Some(wl) => {
let count = s.split_whitespace().count();
if !wl.matches(count) {
return Err(Self::Error::InvalidLength(count, wl.clone()));
return Err(Box::new(Self::Error::InvalidLength(count, wl.clone())));
}
let m = Mnemonic::from_str(&s)?;
Ok(m)
@ -165,7 +165,7 @@ pub mod mnemonic {
type Output = Mnemonic;
type Error = MnemonicChoiceValidationError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>> {
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>> {
let word_lengths = self.word_lengths.clone();
Box::new(move |s: String| {
let count = s.split_whitespace().count();
@ -175,10 +175,10 @@ pub mod mnemonic {
return Ok(m);
}
}
Err(MnemonicChoiceValidationError::InvalidLength(
Err(Box::new(MnemonicChoiceValidationError::InvalidLength(
count,
word_lengths.to_vec(),
))
)))
})
}
}
@ -207,7 +207,7 @@ pub mod mnemonic {
type Output = [Mnemonic; N];
type Error = MnemonicSetValidationError;
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>> {
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>> {
let word_lengths = self.word_lengths;
Box::new(move |s: String| {
let mut counter: usize = 0;
@ -219,15 +219,17 @@ pub mod mnemonic {
.take(word_length)
.collect::<Vec<_>>();
if words.len() != word_length {
return Err(MnemonicSetValidationError::InvalidSetLength(
return Err(Box::new(MnemonicSetValidationError::InvalidSetLength(
word_set,
words.len(),
word_length,
));
)));
}
let mnemonic = match Mnemonic::from_str(&words.join(" ")) {
Ok(m) => m,
Err(e) => return Err(Self::Error::MnemonicFromStrError(word_set, e)),
Err(e) => {
return Err(Box::new(Self::Error::MnemonicFromStrError(word_set, e)))
}
};
output.push(mnemonic);
counter += word_length;

View File

@ -28,7 +28,15 @@ impl std::fmt::Display for DecodeError {
impl std::error::Error for DecodeError {}
/// Encode a given input as a hex string.
pub fn encode(input: &[u8]) -> String {
///
/// # Examples
/// ```rust
/// let data = b"hello world!";
/// let result = smex::encode(&data);
/// assert_eq!(result, "68656c6c6f20776f726c6421");
/// ```
pub fn encode(input: impl AsRef<[u8]>) -> String {
let input = input.as_ref();
let mut s = String::new();
for byte in input {
write!(s, "{byte:02x}").unwrap();
@ -50,7 +58,26 @@ fn val(c: u8) -> Result<u8, DecodeError> {
/// # Errors
/// The function may error if a non-hex character is encountered or if the character count is not
/// evenly divisible by two.
pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
///
/// # Examples
/// ```rust
/// let data = b"hello world!";
/// let encoded = smex::encode(&data);
/// let decoded = smex::decode(&encoded).unwrap();
/// assert_eq!(data.as_slice(), decoded.as_slice());
/// ```
///
/// The function may return an error if the given input is not valid hex.
///
/// ```rust,should_panic
/// let data = b"hello world!";
/// let mut encoded = smex::encode(&data);
/// encoded.push('G');
/// let decoded = smex::decode(&encoded).unwrap();
/// assert_eq!(data.as_slice(), decoded.as_slice());
/// ```
pub fn decode(input: impl AsRef<str>) -> Result<Vec<u8>, DecodeError> {
let input = input.as_ref();
let len = input.len();
if len % 2 != 0 {
return Err(DecodeError::InvalidCharacterCount(len));