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
33 changed files with 646 additions and 275 deletions

8
Cargo.lock generated
View File

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

View File

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

View File

@ -40,6 +40,25 @@
//! # keyforkd::test_util::Infallible::Ok(()) //! # keyforkd::test_util::Infallible::Ok(())
//! # }).unwrap(); //! # }).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; pub use std::os::unix::net::UnixStream;
use std::{collections::HashMap, path::PathBuf}; use std::{collections::HashMap, path::PathBuf};

View File

@ -18,7 +18,7 @@ fn secp256k1_test_suite() {
let seed = seed_test.seed; let seed = seed_test.seed;
run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> { run_test(&seed, move |socket_path| -> Result<(), Box<dyn std::error::Error + Send>> {
for test in seed_test.tests { 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 mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap(); let chain = DerivationPath::from_str(test.chain).unwrap();
let chain_len = chain.len(); 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 // key using an XPrv, for all but the last XPrv, which is verified after this
for i in 2..chain_len { for i in 2..chain_len {
// FIXME: Keyfork will only allow one request per session // 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 mut client = Client::new(socket);
let path = DerivationPath::from_str(test.chain).unwrap(); let path = DerivationPath::from_str(test.chain).unwrap();
let left_path = path.inner()[..i] let left_path = path.inner()[..i]
@ -40,7 +40,7 @@ fn secp256k1_test_suite() {
.fold(DerivationPath::default(), |p, i| p.chain_push(i.clone())); .fold(DerivationPath::default(), |p, i| p.chain_push(i.clone()));
let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap(); let xprv = dbg!(client.request_xprv::<SecretKey>(&left_path)).unwrap();
let derived_xprv = xprv.derive_path(&right_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 mut client = Client::new(socket);
let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap(); let keyforkd_xprv = client.request_xprv::<SecretKey>(&path).unwrap();
assert_eq!( assert_eq!(
@ -73,7 +73,7 @@ fn ed25519_test_suite() {
let seed = seed_test.seed; let seed = seed_test.seed;
run_test(&seed, move |socket_path| { run_test(&seed, move |socket_path| {
for test in seed_test.tests { 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 mut client = Client::new(socket);
let chain = DerivationPath::from_str(test.chain).unwrap(); let chain = DerivationPath::from_str(test.chain).unwrap();
let chain_len = chain.len(); 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 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 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. 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() let service = ServiceBuilder::new()
.layer(middleware::BincodeLayer::new()) .layer(middleware::BincodeLayer::new())
// TODO: passphrase support and/or store passphrase with mnemonic // 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) { let mut server = match UnixServer::bind(socket_path) {
Ok(s) => s, Ok(s) => s,

View File

@ -25,14 +25,25 @@ pub struct InfallibleError {
/// ``` /// ```
pub type Infallible<T> = std::result::Result<T, 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 /// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
/// provided as the only argument to the closure. The closure is expected to return a Result; the /// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
/// Error field of the Result may be an error returned by a test. /// 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 /// # 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 /// # Examples
/// loose" wih the usage of [`Result::expect`]. In normal usage, these should never be an issue. /// ```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)] #[allow(clippy::missing_errors_doc)]
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E> pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
where where

View File

@ -46,7 +46,7 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
let mut client = Client::discover_socket()?; let mut client = Client::discover_socket()?;
let request = DerivationRequest::new(algo, &path); let request = DerivationRequest::new(algo, &path);
let response = client.request(&request.into())?; let response = client.request(&request.into())?;
println!("{}", smex::encode(&DerivationResponse::try_from(response)?.data)); println!("{}", smex::encode(DerivationResponse::try_from(response)?.data));
Ok(()) 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 derive_util::{DerivationIndex, ExtendedPrivateKey, IndexError, PrivateKey};
use ed25519_dalek::SigningKey; 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 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 // Create certificate with initial key and signature
let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?; 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 // Sign and attach primary key and primary userid
let builder = SignatureBuilder::new(SignatureType::PositiveCertification) 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_signature_creation_time(epoch)?
.set_key_flags(primary_key_flags.clone())?; .set_key_flags(primary_key_flags.clone())?;
let binding = userid.bind(&mut primary_key.clone().into_keypair()?, &cert, builder)?; 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 // Set certificate expiration to one day
let mut keypair = primary_key.clone().into_keypair()?; 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 cert = cert.insert_packets(signatures)?;
let mut cert = cert; let mut cert = cert;
@ -127,7 +148,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
SignatureBuilder::new(SignatureType::SubkeyBinding) SignatureBuilder::new(SignatureType::SubkeyBinding)
.set_key_flags(subkey_flags.clone())? .set_key_flags(subkey_flags.clone())?
.set_signature_creation_time(epoch)? .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( .set_embedded_signature(
SignatureBuilder::new(SignatureType::PrimaryKeyBinding) SignatureBuilder::new(SignatureType::PrimaryKeyBinding)
.set_signature_creation_time(epoch)? .set_signature_creation_time(epoch)?
@ -141,7 +162,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
SignatureBuilder::new(SignatureType::SubkeyBinding) SignatureBuilder::new(SignatureType::SubkeyBinding)
.set_key_flags(subkey_flags.clone())? .set_key_flags(subkey_flags.clone())?
.set_signature_creation_time(epoch)? .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 // 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> { pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
// TODO: passphrase support and/or store passphrase within mnemonic // 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. /// Derive an [`ExtendedPrivateKey`] using the given seed.

View File

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

View File

@ -33,7 +33,7 @@ fn run() -> Result<()> {
let openpgp = OpenPGP; let openpgp = OpenPGP;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?; let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?;
print!("{}", smex::encode(&bytes)); print!("{}", smex::encode(bytes));
Ok(()) Ok(())
} }

View File

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

View File

@ -47,7 +47,7 @@ fn run() -> Result<()> {
let Some(line) = stdin().lines().next() else { let Some(line) = stdin().lines().next() else {
return Err(Error::Input.into()); return Err(Error::Input.into());
}; };
smex::decode(&line?)? smex::decode(line?)?
}; };
let openpgp = OpenPGP; let openpgp = OpenPGP;

View File

@ -7,7 +7,7 @@ use aes_gcm::{
Aes256Gcm, KeyInit, Nonce, Aes256Gcm, KeyInit, Nonce,
}; };
use hkdf::Hkdf; use hkdf::Hkdf;
use keyfork_mnemonic_util::{Mnemonic, Wordlist}; use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{ use keyfork_prompt::{
validators::{mnemonic::MnemonicSetValidator, Validator}, validators::{mnemonic::MnemonicSetValidator, Validator},
Message as PromptMessage, PromptHandler, Terminal, Message as PromptMessage, PromptHandler, Terminal,
@ -173,7 +173,6 @@ pub trait Format {
reader: impl Read + Send + Sync, reader: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> { ) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?; let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
// parse input // parse input
let private_keys = private_key_discovery let private_keys = private_key_discovery
@ -207,12 +206,12 @@ pub trait Format {
let validator = MnemonicSetValidator { let validator = MnemonicSetValidator {
word_lengths: [9, 24], word_lengths: [9, 24],
}; };
let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist( let [nonce_mnemonic, pubkey_mnemonic] = pm
QRCODE_COULDNT_READ, .prompt_validated_wordlist::<English, _>(
&wordlist, QRCODE_COULDNT_READ,
3, 3,
validator.to_fn(), validator.to_fn(),
)?; )?;
let nonce = nonce_mnemonic let nonce = nonce_mnemonic
.as_bytes() .as_bytes()
@ -228,8 +227,7 @@ pub trait Format {
// create our shared key // create our shared key
let our_key = EphemeralSecret::random(); let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic = let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let shared_secret = our_key let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_pubkey)) .diffie_hellman(&PublicKey::from(their_pubkey))
.to_bytes(); .to_bytes();
@ -279,8 +277,7 @@ pub trait Format {
} }
// safety: size of out_bytes is constant and always % 4 == 0 // safety: size of out_bytes is constant and always % 4 == 0
let payload_mnemonic = let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
#[cfg(feature = "qrcode")] #[cfg(feature = "qrcode")]
{ {
@ -395,7 +392,6 @@ const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry
/// incompatible with the currently running version. /// incompatible with the currently running version.
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> { pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?; let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let mut iter_count = None; let mut iter_count = None;
let mut shares = vec![]; let mut shares = vec![];
@ -406,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) { while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
iter += 1; iter += 1;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng); let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic = let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
let our_key = EphemeralSecret::random(); let our_key = EphemeralSecret::random();
let key_mnemonic = let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
#[cfg(feature = "qrcode")] #[cfg(feature = "qrcode")]
{ {
@ -464,12 +458,12 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
word_lengths: [24, 48], word_lengths: [24, 48],
}; };
let [pubkey_mnemonic, payload_mnemonic] = pm.prompt_validated_wordlist( let [pubkey_mnemonic, payload_mnemonic] = pm
QRCODE_COULDNT_READ, .prompt_validated_wordlist::<English, _>(
&wordlist, QRCODE_COULDNT_READ,
3, 3,
validator.to_fn(), validator.to_fn(),
)?; )?;
let pubkey = pubkey_mnemonic let pubkey = pubkey_mnemonic
.as_bytes() .as_bytes()
.try_into() .try_into()

View File

@ -17,7 +17,7 @@ use keyfork_derive_openpgp::{
derive_util::{DerivationPath, PathError, VariableLengthSeed}, derive_util::{DerivationPath, PathError, VariableLengthSeed},
XPrv, XPrv,
}; };
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist}; use keyfork_mnemonic_util::{English, Mnemonic, MnemonicFromStrError, MnemonicGenerationError};
use keyfork_prompt::{ use keyfork_prompt::{
validators::{mnemonic::MnemonicSetValidator, Validator}, validators::{mnemonic::MnemonicSetValidator, Validator},
Error as PromptError, Message as PromptMessage, PromptHandler, Terminal, Error as PromptError, Message as PromptMessage, PromptHandler, Terminal,
@ -854,7 +854,6 @@ pub fn decrypt(
encrypted_messages: &[EncryptedMessage], encrypted_messages: &[EncryptedMessage],
) -> Result<()> { ) -> Result<()> {
let mut pm = Terminal::new(stdin(), stdout())?; let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let mut nonce_data: Option<[u8; 12]> = None; let mut nonce_data: Option<[u8; 12]> = None;
let mut pubkey_data: Option<[u8; 32]> = None; let mut pubkey_data: Option<[u8; 32]> = None;
@ -879,8 +878,11 @@ pub fn decrypt(
let validator = MnemonicSetValidator { let validator = MnemonicSetValidator {
word_lengths: [9, 24], word_lengths: [9, 24],
}; };
let [nonce_mnemonic, pubkey_mnemonic] = let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist::<English, _>(
pm.prompt_validated_wordlist(QRCODE_COULDNT_READ, &wordlist, 3, validator.to_fn())?; QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let nonce = nonce_mnemonic let nonce = nonce_mnemonic
.as_bytes() .as_bytes()
@ -897,8 +899,7 @@ pub fn decrypt(
let nonce = Nonce::<U12>::from_slice(&nonce); let nonce = Nonce::<U12>::from_slice(&nonce);
let our_key = EphemeralSecret::random(); let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic = let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes(); let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
@ -943,7 +944,7 @@ pub fn decrypt(
} }
// safety: size of out_bytes is constant and always % 4 == 0 // 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")] #[cfg(feature = "qrcode")]
{ {

View File

@ -43,3 +43,4 @@ openpgp-card-sequoia = { version = "0.2.0", default-features = false }
openpgp-card = "0.4.1" openpgp-card = "0.4.1"
clap_complete = { version = "4.4.6", optional = true } clap_complete = { version = "4.4.6", optional = true }
sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] } 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::Tarot => todo!(),
MnemonicSeedSource::Dice => 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()) Ok(mnemonic.to_string())
} }
} }

View File

@ -2,7 +2,7 @@ use super::Keyfork;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use std::path::PathBuf; use std::path::PathBuf;
use keyfork_mnemonic_util::Mnemonic; use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_shard::{remote_decrypt, Format}; use keyfork_shard::{remote_decrypt, Format};
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>; type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
@ -61,9 +61,8 @@ impl RecoverSubcommands {
let validator = MnemonicChoiceValidator { let validator = MnemonicChoiceValidator {
word_lengths: [WordLength::Count(12), WordLength::Count(24)], word_lengths: [WordLength::Count(12), WordLength::Count(24)],
}; };
let mnemonic = term.prompt_validated_wordlist( let mnemonic = term.prompt_validated_wordlist::<English, _>(
"Mnemonic: ", "Mnemonic: ",
&Default::default(),
3, 3,
validator.to_fn(), validator.to_fn(),
)?; )?;
@ -82,7 +81,7 @@ pub struct Recover {
impl Recover { impl Recover {
pub fn handle(&self, _k: &Keyfork) -> Result<()> { pub fn handle(&self, _k: &Keyfork) -> Result<()> {
let seed = self.command.handle()?; 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() tokio::runtime::Builder::new_multi_thread()
.enable_all() .enable_all()
.build() .build()

View File

@ -76,7 +76,7 @@ impl ShardExec for OpenPGP {
{ {
let openpgp = keyfork_shard::openpgp::OpenPGP; let openpgp = keyfork_shard::openpgp::OpenPGP;
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?; let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?;
write!(output, "{}", smex::encode(&bytes))?; write!(output, "{}", smex::encode(bytes))?;
Ok(()) Ok(())
} }

View File

@ -6,21 +6,16 @@ use std::process::ExitCode;
use clap::Parser; use clap::Parser;
use keyfork_bin::{Bin, ClosureBin};
mod cli; mod cli;
mod config; mod config;
fn main() -> ExitCode { fn main() -> ExitCode {
let opts = cli::Keyfork::parse(); let bin = ClosureBin::new(|| {
let opts = cli::Keyfork::parse();
opts.command.handle(&opts)
});
if let Err(e) = opts.command.handle(&opts) { bin.main()
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
} }

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"); /// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
/// keyfork_entropy::ensure_safe(); /// 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() { pub fn ensure_safe() {
if !std::env::vars() if !std::env::vars().any(|(name, value)| {
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED") (name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
{ && value != "test-must-fail"
}) {
ensure_safe_kernel_version(); ensure_safe_kernel_version();
ensure_offline(); 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)?; let entropy = keyfork_entropy::generate_entropy_of_size(bit_size / 8)?;
println!("{}", smex::encode(&entropy)); println!("{}", smex::encode(entropy));
Ok(()) Ok(())
} }

View File

@ -66,6 +66,11 @@ pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
/// # Errors /// # Errors
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a /// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
/// constraint on a protocol level. /// 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> { pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
let mut output = vec![]; let mut output = vec![];
try_encode_to(data, &mut output)?; try_encode_to(data, &mut output)?;
@ -77,6 +82,12 @@ pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
/// # Errors /// # Errors
/// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer /// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer
/// is unable to write data. /// 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> { pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
let hash = hash(data); let hash = hash(data);
let len = hash.len() + data.len(); 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 enough data to parse a length,
/// * The given `data` does not contain the given length's worth of data, /// * 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 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> { pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
try_decode_from(&mut &data[..]) try_decode_from(&mut &data[..])
} }
/// Read and decode a framed message into a `Vec<u8>`. /// 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 /// # Errors
/// An error may be returned if: /// An error may be returned if:
/// * The given `data` does not contain enough data to parse a length, /// * 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` 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 given `data` has a checksum that does not match what we build locally.
/// * The source for the data returned an error. /// * 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> { pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
let mut bytes = 0u32.to_be_bytes(); let mut bytes = 0u32.to_be_bytes();
readable.read_exact(&mut 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)?; input.read_line(&mut line)?;
let decoded = smex::decode(line.trim())?; 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}"); 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 hmac::Hmac;
use pbkdf2::pbkdf2; use pbkdf2::pbkdf2;
@ -39,93 +93,65 @@ impl Display for MnemonicGenerationError {
impl Error for MnemonicGenerationError {} impl Error for MnemonicGenerationError {}
/// A BIP-0039 compatible list of words. /// A trait representing a BIP-0039 wordlist, of 2048 words, with each word having a unique first
#[derive(Debug, Clone)] /// three letters.
pub struct Wordlist(Vec<String>); 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 { /// Return a representation of the words in the wordlist as an array of [`str`].
/// Returns the English wordlist in the Bitcoin BIP-0039 specification. fn to_str_array(&self) -> [&str; 2048];
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(),
)
}
} }
impl Wordlist { /// A wordlist for the English language, from the BIP-0039 dataset.
/// Return an Arced version of the Wordlist #[derive(Debug)]
#[allow(clippy::must_use_candidate)] pub struct English {
pub fn arc(self) -> Arc<Self> { words: [String; 2048],
Arc::new(self) }
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. fn to_str_array(&self) -> [&str; 2048] {
pub fn contains(&self, word: &str) -> bool { std::array::from_fn(|i| self.words[i].as_str())
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
} }
} }
/// A BIP-0039 mnemonic with reference to a [`Wordlist`]. /// A BIP-0039 mnemonic with reference to a [`Wordlist`].
#[derive(Debug, Clone)] #[derive(Debug, Clone, PartialEq, Eq)]
pub struct Mnemonic { pub struct MnemonicBase<W: Wordlist> {
entropy: Vec<u8>, data: Vec<u8>,
// words: Vec<usize>, marker: PhantomData<W>,
wordlist: Arc<Wordlist>,
} }
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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let bit_count = self.entropy.len() * 8; let wordlist = W::get_singleton();
let mut bits = vec![false; bit_count + bit_count / 32]; let words = wordlist.to_str_array();
for byte_index in 0..bit_count / 8 { let mut iter = self
for bit_index in 0..8 { .words()
bits[byte_index * 8 + bit_index] = .into_iter()
(self.entropy[byte_index] & (1 << (7 - bit_index))) > 0; .filter_map(|word| words.get(word))
}
}
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))
.peekable(); .peekable();
while let Some(word) = iter.next() { while let Some(word) = iter.next() {
f.write_str(word)?; f.write_str(word)?;
@ -170,17 +196,20 @@ impl Display for MnemonicFromStrError {
impl Error for MnemonicFromStrError {} impl Error for MnemonicFromStrError {}
impl FromStr for Mnemonic { impl<W> FromStr for MnemonicBase<W>
where
W: Wordlist,
{
type Err = MnemonicFromStrError; type Err = MnemonicFromStrError;
fn from_str(s: &str) -> Result<Self, Self::Err> { 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 words: Vec<_> = s.split_whitespace().collect();
let mut usize_words = vec![]; let mut usize_words = vec![];
let wordlist = Wordlist::default().arc();
let mut bits = vec![false; words.len() * 11]; let mut bits = vec![false; words.len() * 11];
for (index, word) in words.iter().enumerate() { for (index, word) in words.iter().enumerate() {
let word = wordlist let word = wordlist_words
.0
.iter() .iter()
.position(|w| w == word) .position(|w| w == word)
.ok_or(MnemonicFromStrError::InvalidWord(index))?; .ok_or(MnemonicFromStrError::InvalidWord(index))?;
@ -197,7 +226,7 @@ impl FromStr for Mnemonic {
bits.truncate(bits.len() * 32 / 33); bits.truncate(bits.len() * 32 / 33);
// bits.truncate(bits.len() - bits.len() % 32); // bits.truncate(bits.len() - bits.len() % 32);
let entropy: Vec<u8> = bits let data: Vec<u8> = bits
.chunks_exact(8) .chunks_exact(8)
.map(|chunk| { .map(|chunk| {
let mut num = 0u8; let mut num = 0u8;
@ -209,7 +238,7 @@ impl FromStr for Mnemonic {
.collect(); .collect();
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(&entropy); hasher.update(&data);
let hash = hasher.finalize().to_vec(); let hash = hasher.finalize().to_vec();
for (i, bit) in checksum_bits.iter().enumerate() { for (i, bit) in checksum_bits.iter().enumerate() {
@ -218,23 +247,27 @@ impl FromStr for Mnemonic {
} }
} }
Ok(Mnemonic { Ok(MnemonicBase { data, marker: PhantomData })
entropy,
// words: usize_words,
wordlist,
})
} }
} }
impl Mnemonic { impl<W> MnemonicBase<W>
/// Generate a [`Mnemonic`] from the provided entropy and [`Wordlist`]. 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 /// # Errors
/// An error may be returned if the entropy is not within the acceptable lengths. /// An error may be returned if the data is not within the expected lengths.
pub fn from_entropy( ///
bytes: &[u8], /// # Examples
wordlist: Arc<Wordlist>, /// ```rust
) -> Result<Mnemonic, MnemonicGenerationError> { /// 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; let bit_count = bytes.len() * 8;
if bit_count % 32 != 0 { if bit_count % 32 != 0 {
@ -245,88 +278,150 @@ impl Mnemonic {
return Err(MnemonicGenerationError::InvalidByteLength(bit_count)); 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 /// # Safety
/// ///
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't /// 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 /// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
/// == 0`. /// == 0`. If the assumption is incorrect, code may panic.
pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc<Wordlist>) -> Mnemonic { ///
Mnemonic { /// # Examples
entropy: bytes.to_vec(), /// ```rust
wordlist, /// 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] { 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> { 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> { 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"] #[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
pub fn entropy(&self) -> Vec<u8> { 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. /// Create a BIP-0032 seed from the provided data and an optional passphrase.
/// ///
/// # Errors /// # Errors
/// The method may return an error if the pbkdf2 function returns an invalid length, but this /// The method should not return an error.
/// case should not be reached. #[deprecated = "Use generate_seed() instead"]
pub fn seed<'a>( pub fn seed<'a>(
&self, &self,
passphrase: impl Into<Option<&'a str>>, passphrase: impl Into<Option<&'a str>>,
) -> Result<Vec<u8>, MnemonicGenerationError> { ) -> 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 passphrase = passphrase.into();
let mut seed = [0u8; 64]; let mut seed = [0u8; 64];
let mnemonic = self.to_string(); let mnemonic = self.to_string();
let salt = ["mnemonic", passphrase.unwrap_or("")].join(""); let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed) pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
.map_err(|_| MnemonicGenerationError::InvalidPbkdf2Length)?; .expect("HmacSha512 InvalidLength should be infallible");
Ok(seed.to_vec()) seed.to_vec()
} }
/// Encode the mnemonic into a list of wordlist indexes. /// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a
pub fn words(self) -> (Vec<usize>, Arc<Wordlist>) { /// BIP-0039 wordlist.
let bit_count = self.entropy.len() * 8; pub fn words(&self) -> Vec<usize> {
let bit_count = self.data.len() * 8;
let mut bits = vec![false; bit_count + bit_count / 32]; let mut bits = vec![false; bit_count + bit_count / 32];
for byte_index in 0..bit_count / 8 { for byte_index in 0..bit_count / 8 {
for bit_index in 0..8 { for bit_index in 0..8 {
bits[byte_index * 8 + bit_index] = 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(); let mut hasher = Sha256::new();
hasher.update(&self.entropy); hasher.update(&self.data);
let hash = hasher.finalize().to_vec(); let hash = hasher.finalize().to_vec();
for check_bit in 0..bit_count / 32 { for check_bit in 0..bit_count / 32 {
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0; 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; let mut num = 0usize;
for i in 0..11 { for i in 0..11 {
num += usize::from(chunk[10 - i]) << i; num += usize::from(chunk[10 - i]) << i;
} }
num num
}); }).collect()
(words.collect(), self.wordlist.clone())
} }
} }
@ -337,13 +432,8 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn wordlist_word_count_correct() { fn can_load_wordlist() {
let wordlist = Wordlist::default().into_inner(); let _wordlist = English::get_singleton();
assert_eq!(
wordlist.len(),
2usize.pow(11),
"Wordlist did not include correct word count"
);
} }
#[test] #[test]
@ -351,8 +441,7 @@ mod tests {
let mut random_handle = File::open("/dev/random").unwrap(); let mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8]; let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap(); random_handle.read_exact(&mut entropy[..]).unwrap();
let wordlist = Wordlist::default().arc(); let mnemonic = super::Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
let new_entropy = mnemonic.as_bytes(); let new_entropy = mnemonic.as_bytes();
assert_eq!(new_entropy, entropy); assert_eq!(new_entropy, entropy);
} }
@ -361,7 +450,6 @@ mod tests {
fn conforms_to_trezor_tests() { fn conforms_to_trezor_tests() {
let content = include_str!("data/vectors.json"); let content = include_str!("data/vectors.json");
let jsonobj: serde_json::Value = serde_json::from_str(content).unwrap(); let jsonobj: serde_json::Value = serde_json::from_str(content).unwrap();
let wordlist = Wordlist::default().arc();
for test in jsonobj["english"].as_array().unwrap() { for test in jsonobj["english"].as_array().unwrap() {
let [ref hex_, ref seed, ..] = test.as_array().unwrap()[..] else { 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 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()); 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 mut random_handle = File::open("/dev/random").unwrap();
let entropy = &mut [0u8; 256 / 8]; let entropy = &mut [0u8; 256 / 8];
random_handle.read_exact(&mut entropy[..]).unwrap(); random_handle.read_exact(&mut entropy[..]).unwrap();
let wordlist = Wordlist::default().arc(); let my_mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let my_mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
let their_mnemonic = bip39::Mnemonic::from_entropy(&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.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!( assert_eq!(
my_mnemonic.seed("testing").unwrap(), my_mnemonic.generate_seed("testing"),
their_mnemonic.to_seed("testing") their_mnemonic.to_seed("testing")
); );
assert_ne!( assert_ne!(
my_mnemonic.seed("test1").unwrap(), my_mnemonic.generate_seed("test1"),
their_mnemonic.to_seed("test2") their_mnemonic.to_seed("test2")
); );
} }
@ -400,14 +487,13 @@ mod tests {
let tests = 100_000; let tests = 100_000;
let mut count = 0.; let mut count = 0.;
let entropy = &mut [0u8; 256 / 8]; let entropy = &mut [0u8; 256 / 8];
let wordlist = Wordlist::default().arc();
let mut random = std::fs::File::open("/dev/urandom").unwrap(); let mut random = std::fs::File::open("/dev/urandom").unwrap();
let mut hs = HashSet::<usize>::with_capacity(24); let mut hs = HashSet::<usize>::with_capacity(24);
for _ in 0..tests { for _ in 0..tests {
random.read_exact(&mut entropy[..]).unwrap(); random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], wordlist.clone()).unwrap(); let mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
let (words, _) = mnemonic.words(); let words = mnemonic.words();
hs.clear(); hs.clear();
hs.extend(words); hs.extend(words);
if hs.len() != 24 { if hs.len() != 24 {
@ -435,11 +521,10 @@ mod tests {
#[test] #[test]
fn can_do_up_to_1024_bits() { fn can_do_up_to_1024_bits() {
let entropy = &mut [0u8; 128]; let entropy = &mut [0u8; 128];
let wordlist = Wordlist::default().arc();
let mut random = std::fs::File::open("/dev/urandom").unwrap(); let mut random = std::fs::File::open("/dev/urandom").unwrap();
random.read_exact(&mut entropy[..]).unwrap(); random.read_exact(&mut entropy[..]).unwrap();
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&entropy[..], wordlist.clone()) }; let mnemonic = unsafe { Mnemonic::from_raw_bytes(&entropy[..]) };
let (words, _) = mnemonic.words(); let words = mnemonic.words();
assert!(words.len() == 96); assert!(words.len() == 96);
} }
} }

View File

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

View File

@ -51,31 +51,31 @@ pub trait PromptHandler {
/// could not be read. /// could not be read.
fn prompt_input(&mut self, prompt: &str) -> Result<String>; 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 /// # Errors
/// The method may return an error if the message was not able to be displayed or if the input /// The method may return an error if the message was not able to be displayed or if the input
/// could not be read. /// could not be read.
#[cfg(feature = "mnemonic")] #[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 /// 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 /// # Errors
/// The method may return an error if the message was not able to be displayed, if the input /// 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. /// could not be read, or if the parser returned an error.
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<V, F, E>( fn prompt_validated_wordlist<X, V>(
&mut self, &mut self,
prompt: &str, prompt: &str,
wordlist: &Wordlist,
retries: u8, retries: u8,
validator_fn: F, validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error> ) -> Result<V, Error>
where where
F: Fn(String) -> Result<V, E>, X: Wordlist;
E: std::error::Error;
/// Prompt the user for a passphrase, which is hidden while typing. /// Prompt the user for a passphrase, which is hidden while typing.
/// ///
@ -90,15 +90,12 @@ pub trait PromptHandler {
/// # Errors /// # Errors
/// The method may return an error if the message was not able to be displayed, if the input /// 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. /// 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, &mut self,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
validator_fn: F, validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
) -> Result<V, Error> ) -> Result<V, Error>;
where
F: Fn(String) -> Result<V, E>,
E: std::error::Error;
/// Prompt the user with a [`Message`]. /// Prompt the user with a [`Message`].
/// ///

View File

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

View File

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

View File

@ -28,7 +28,15 @@ impl std::fmt::Display for DecodeError {
impl std::error::Error for DecodeError {} impl std::error::Error for DecodeError {}
/// Encode a given input as a hex string. /// 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(); let mut s = String::new();
for byte in input { for byte in input {
write!(s, "{byte:02x}").unwrap(); write!(s, "{byte:02x}").unwrap();
@ -50,7 +58,26 @@ fn val(c: u8) -> Result<u8, DecodeError> {
/// # Errors /// # Errors
/// The function may error if a non-hex character is encountered or if the character count is not /// The function may error if a non-hex character is encountered or if the character count is not
/// evenly divisible by two. /// 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(); let len = input.len();
if len % 2 != 0 { if len % 2 != 0 {
return Err(DecodeError::InvalidCharacterCount(len)); return Err(DecodeError::InvalidCharacterCount(len));