Compare commits

..

5 Commits

33 changed files with 275 additions and 646 deletions

8
Cargo.lock generated
View File

@ -1674,7 +1674,6 @@ 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",
@ -1693,13 +1692,6 @@ 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,7 +14,6 @@ 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,25 +40,6 @@
//! # 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,15 +34,3 @@ 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.generate_seed(None))); .service(Keyforkd::new(mnemonic.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,25 +25,14 @@ 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 test may use a seed (the first argument) from a /// Run a test making use of a Keyforkd server. The path to the socket of the Keyforkd server is
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure /// provided as the only argument to the closure. The closure is expected to return a Result; the
/// the server is capable of being interacted with. The test is in the form of a closure, expected /// Error field of the Result may be an error returned by a test.
/// 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.
/// ///
/// # Examples /// The function is not expected to run in production; therefore, the function plays "fast and
/// ```rust /// loose" wih the usage of [`Result::expect`]. In normal usage, these should never be an issue.
/// 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,9 +1,6 @@
//! Creation of OpenPGP Transferable Secret Keys from BIP-0032 derived data. //! Creation of OpenPGP certificates from BIP-0032 derived data.
use std::{ use std::time::{Duration, SystemTime, SystemTimeError};
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;
@ -71,24 +68,7 @@ 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 expiration_date = match std::env::var("KEYFORK_OPENPGP_EXPIRE").as_mut() { let one_day = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
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)?)?;
@ -100,7 +80,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(expiration_date.duration_since(epoch)?)? .set_key_validity_period(one_day.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)?;
@ -109,8 +89,7 @@ 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 = let signatures = cert.set_expiration_time(&policy, None, &mut keypair, Some(one_day))?;
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;
@ -148,7 +127,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(expiration_date.duration_since(epoch)?)? .set_key_validity_period(one_day.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)?
@ -162,7 +141,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(expiration_date.duration_since(epoch)?)? .set_key_validity_period(one_day.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.generate_seed(None)) self.derive_with_master_seed(&mnemonic.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::{English, Mnemonic}; use keyfork_mnemonic_util::{Mnemonic, Wordlist};
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,6 +173,7 @@ 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
@ -206,9 +207,9 @@ pub trait Format {
let validator = MnemonicSetValidator { let validator = MnemonicSetValidator {
word_lengths: [9, 24], word_lengths: [9, 24],
}; };
let [nonce_mnemonic, pubkey_mnemonic] = pm let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist(
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ, QRCODE_COULDNT_READ,
&wordlist,
3, 3,
validator.to_fn(), validator.to_fn(),
)?; )?;
@ -227,7 +228,8 @@ 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 = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?; let our_pubkey_mnemonic =
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();
@ -277,7 +279,8 @@ 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 = unsafe { Mnemonic::from_raw_bytes(&out_bytes) }; let payload_mnemonic =
unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
#[cfg(feature = "qrcode")] #[cfg(feature = "qrcode")]
{ {
@ -392,6 +395,7 @@ 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![];
@ -402,9 +406,11 @@ 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 = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) }; let nonce_mnemonic =
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
let our_key = EphemeralSecret::random(); let our_key = EphemeralSecret::random();
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?; let key_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
#[cfg(feature = "qrcode")] #[cfg(feature = "qrcode")]
{ {
@ -458,9 +464,9 @@ 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 let [pubkey_mnemonic, payload_mnemonic] = pm.prompt_validated_wordlist(
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ, QRCODE_COULDNT_READ,
&wordlist,
3, 3,
validator.to_fn(), validator.to_fn(),
)?; )?;

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::{English, Mnemonic, MnemonicFromStrError, MnemonicGenerationError}; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist};
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,6 +854,7 @@ 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;
@ -878,11 +879,8 @@ pub fn decrypt(
let validator = MnemonicSetValidator { let validator = MnemonicSetValidator {
word_lengths: [9, 24], word_lengths: [9, 24],
}; };
let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist::<English, _>( let [nonce_mnemonic, pubkey_mnemonic] =
QRCODE_COULDNT_READ, pm.prompt_validated_wordlist(QRCODE_COULDNT_READ, &wordlist, 3, validator.to_fn())?;
3,
validator.to_fn(),
)?;
let nonce = nonce_mnemonic let nonce = nonce_mnemonic
.as_bytes() .as_bytes()
@ -899,7 +897,8 @@ 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 = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?; let our_pubkey_mnemonic =
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();
@ -944,7 +943,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_bytes(&out_bytes) }; let payload_mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
#[cfg(feature = "qrcode")] #[cfg(feature = "qrcode")]
{ {

View File

@ -43,4 +43,3 @@ 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_bytes(&seed)?; let mnemonic = keyfork_mnemonic_util::Mnemonic::from_entropy(&seed, Default::default())?;
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::{English, Mnemonic}; use keyfork_mnemonic_util::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,8 +61,9 @@ 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::<English, _>( let mnemonic = term.prompt_validated_wordlist(
"Mnemonic: ", "Mnemonic: ",
&Default::default(),
3, 3,
validator.to_fn(), validator.to_fn(),
)?; )?;
@ -81,7 +82,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_bytes(&seed)?; let mnemonic = Mnemonic::from_entropy(&seed, Default::default())?;
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,16 +6,21 @@ 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 bin = ClosureBin::new(|| {
let opts = cli::Keyfork::parse(); let opts = cli::Keyfork::parse();
opts.command.handle(&opts)
});
bin.main() 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
} }

View File

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

@ -1,140 +0,0 @@
#![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,21 +57,10 @@ 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().any(|(name, value)| { if !std::env::vars()
(name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED") .any(|(name, _)| 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,11 +66,6 @@ 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)?;
@ -82,12 +77,6 @@ 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();
@ -118,40 +107,18 @@ 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_bytes(&decoded) }; let mnemonic = unsafe { Mnemonic::from_raw_entropy(&decoded, Default::default()) };
println!("{mnemonic}"); println!("{mnemonic}");

View File

@ -1,60 +1,6 @@
//! Zero-dependency mnemonic encoding and decoding of data. //! Zero-dependency Mnemonic encoding and decoding.
//!
//! 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::{ use std::{error::Error, fmt::Display, str::FromStr, sync::Arc};
error::Error,
fmt::Display,
str::FromStr,
sync::OnceLock,
marker::PhantomData,
};
use hmac::Hmac; use hmac::Hmac;
use pbkdf2::pbkdf2; use pbkdf2::pbkdf2;
@ -93,65 +39,93 @@ impl Display for MnemonicGenerationError {
impl Error for MnemonicGenerationError {} impl Error for MnemonicGenerationError {}
/// A trait representing a BIP-0039 wordlist, of 2048 words, with each word having a unique first /// A BIP-0039 compatible list of words.
/// three letters. #[derive(Debug, Clone)]
pub trait Wordlist: std::fmt::Debug { pub struct Wordlist(Vec<String>);
/// Get a reference to a [`std::sync::OnceLock`] Self.
fn get_singleton<'a>() -> &'a Self;
/// Return a representation of the words in the wordlist as an array of [`str`]. impl Default for Wordlist {
fn to_str_array(&self) -> [&str; 2048]; /// Returns the English wordlist in the Bitcoin BIP-0039 specification.
} fn default() -> Self {
// TODO: English is the only supported language.
/// 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 wordlist_file = include_str!("data/wordlist.txt");
let mut words = wordlist_file Wordlist(
wordlist_file
.lines() .lines()
// skip 1: comment at top of file to point to BIP-0039 source.
.skip(1) .skip(1)
.map(|x| x.trim().to_string()); .map(|x| x.trim().to_string())
English { .collect(),
words: std::array::from_fn(|_| words.next().expect("wordlist has 2048 words")), )
} }
})
} }
fn to_str_array(&self) -> [&str; 2048] { impl Wordlist {
std::array::from_fn(|i| self.words[i].as_str()) /// Return an Arced version of the Wordlist
#[allow(clippy::must_use_candidate)]
pub fn arc(self) -> Arc<Self> {
Arc::new(self)
}
/// 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
} }
} }
/// A BIP-0039 mnemonic with reference to a [`Wordlist`]. /// A BIP-0039 mnemonic with reference to a [`Wordlist`].
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone)]
pub struct MnemonicBase<W: Wordlist> { pub struct Mnemonic {
data: Vec<u8>, entropy: Vec<u8>,
marker: PhantomData<W>, // words: Vec<usize>,
wordlist: Arc<Wordlist>,
} }
/// A default Mnemonic using the English language. impl Display for Mnemonic {
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 wordlist = W::get_singleton(); let bit_count = self.entropy.len() * 8;
let words = wordlist.to_str_array(); let mut bits = vec![false; bit_count + bit_count / 32];
let mut iter = self for byte_index in 0..bit_count / 8 {
.words() for bit_index in 0..8 {
.into_iter() bits[byte_index * 8 + bit_index] =
.filter_map(|word| words.get(word)) (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))
.peekable(); .peekable();
while let Some(word) = iter.next() { while let Some(word) = iter.next() {
f.write_str(word)?; f.write_str(word)?;
@ -196,20 +170,17 @@ impl Display for MnemonicFromStrError {
impl Error for MnemonicFromStrError {} impl Error for MnemonicFromStrError {}
impl<W> FromStr for MnemonicBase<W> impl FromStr for Mnemonic {
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_words let word = wordlist
.0
.iter() .iter()
.position(|w| w == word) .position(|w| w == word)
.ok_or(MnemonicFromStrError::InvalidWord(index))?; .ok_or(MnemonicFromStrError::InvalidWord(index))?;
@ -226,7 +197,7 @@ where
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 data: Vec<u8> = bits let entropy: Vec<u8> = bits
.chunks_exact(8) .chunks_exact(8)
.map(|chunk| { .map(|chunk| {
let mut num = 0u8; let mut num = 0u8;
@ -238,7 +209,7 @@ where
.collect(); .collect();
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(&data); hasher.update(&entropy);
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() {
@ -247,27 +218,23 @@ where
} }
} }
Ok(MnemonicBase { data, marker: PhantomData }) Ok(Mnemonic {
entropy,
// words: usize_words,
wordlist,
})
} }
} }
impl<W> MnemonicBase<W> impl Mnemonic {
where /// Generate a [`Mnemonic`] from the provided entropy and [`Wordlist`].
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 data is not within the expected lengths. /// An error may be returned if the entropy is not within the acceptable lengths.
/// pub fn from_entropy(
/// # Examples bytes: &[u8],
/// ```rust wordlist: Arc<Wordlist>,
/// use keyfork_mnemonic_util::Mnemonic; ) -> Result<Mnemonic, MnemonicGenerationError> {
/// 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 {
@ -278,150 +245,88 @@ where
return Err(MnemonicGenerationError::InvalidByteLength(bit_count)); return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
} }
Ok(unsafe { Self::from_raw_bytes(bytes) }) Ok(unsafe { Self::from_raw_entropy(bytes, 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 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`. If the assumption is incorrect, code may panic. /// == 0`.
/// pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc<Wordlist>) -> Mnemonic {
/// # Examples Mnemonic {
/// ```rust entropy: bytes.to_vec(),
/// use keyfork_mnemonic_util::Mnemonic; wordlist,
/// 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,
} }
} }
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to /// The internal representation of the decoded data.
/// 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.data &self.entropy
} }
/// A clone of the internal representation of the decoded data. /// The internal representation of the decoded data, as a [`Vec<u8>`].
pub fn to_bytes(&self) -> Vec<u8> { pub fn to_bytes(&self) -> Vec<u8> {
self.data.to_vec() self.entropy.to_vec()
} }
/// Conver the Mnemonic into the internal representation of the decoded data. /// Drop self, returning the decoded data.
pub fn into_bytes(self) -> Vec<u8> { pub fn into_bytes(self) -> Vec<u8> {
self.data self.entropy
} }
/// Clone the existing data. /// Clone the existing entropy.
#[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.data.clone() self.entropy.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 should not return an error. /// The method may return an error if the pbkdf2 function returns an invalid length, but this
#[deprecated = "Use generate_seed() instead"] /// case should not be reached.
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)
.expect("HmacSha512 InvalidLength should be infallible"); .map_err(|_| MnemonicGenerationError::InvalidPbkdf2Length)?;
seed.to_vec() Ok(seed.to_vec())
} }
/// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a /// Encode the mnemonic into a list of wordlist indexes.
/// BIP-0039 wordlist. pub fn words(self) -> (Vec<usize>, Arc<Wordlist>) {
pub fn words(&self) -> Vec<usize> { let bit_count = self.entropy.len() * 8;
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.data[byte_index] & (1 << (7 - bit_index))) > 0; (self.entropy[byte_index] & (1 << (7 - bit_index))) > 0;
} }
} }
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(&self.data); hasher.update(&self.entropy);
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;
} }
// TODO: find a way to not have to collect to vec let words = bits.chunks_exact(11).peekable().map(|chunk| {
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())
} }
} }
@ -432,8 +337,13 @@ mod tests {
use super::*; use super::*;
#[test] #[test]
fn can_load_wordlist() { fn wordlist_word_count_correct() {
let _wordlist = English::get_singleton(); let wordlist = Wordlist::default().into_inner();
assert_eq!(
wordlist.len(),
2usize.pow(11),
"Wordlist did not include correct word count"
);
} }
#[test] #[test]
@ -441,7 +351,8 @@ 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 mnemonic = super::Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap(); let wordlist = Wordlist::default().arc();
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);
} }
@ -450,6 +361,7 @@ 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 {
@ -457,7 +369,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_bytes(&hex).unwrap(); let mnemonic = Mnemonic::from_entropy(&hex, wordlist.clone()).unwrap();
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap()); assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
} }
@ -468,16 +380,17 @@ 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 my_mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap(); let wordlist = Wordlist::default().arc();
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.generate_seed(None), their_mnemonic.to_seed("")); assert_eq!(my_mnemonic.seed(None).unwrap(), their_mnemonic.to_seed(""));
assert_eq!( assert_eq!(
my_mnemonic.generate_seed("testing"), my_mnemonic.seed("testing").unwrap(),
their_mnemonic.to_seed("testing") their_mnemonic.to_seed("testing")
); );
assert_ne!( assert_ne!(
my_mnemonic.generate_seed("test1"), my_mnemonic.seed("test1").unwrap(),
their_mnemonic.to_seed("test2") their_mnemonic.to_seed("test2")
); );
} }
@ -487,13 +400,14 @@ 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_bytes(&entropy[..256 / 8]).unwrap(); let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], wordlist.clone()).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 {
@ -521,10 +435,11 @@ 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_bytes(&entropy[..]) }; let mnemonic = unsafe { Mnemonic::from_raw_entropy(&entropy[..], wordlist.clone()) };
let words = mnemonic.words(); let (words, _) = mnemonic.words();
assert!(words.len() == 96); assert!(words.len() == 96);
} }
} }

View File

@ -7,8 +7,6 @@ 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 {
@ -18,16 +16,18 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
word_lengths: [24, 48], word_lengths: [24, 48],
}; };
let mnemonics = mgr.prompt_validated_wordlist::<English, _>( let mnemonics = mgr.prompt_validated_wordlist(
"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::<English, _>( let mnemonics = mgr.prompt_validated_wordlist(
"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. A language must be specified as the generic /// Prompt the user for input based on a wordlist.
/// 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<X>(&mut self, prompt: &str) -> Result<String> where X: Wordlist; fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String>;
/// 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. A language must be specified /// provided parser function, returning the type from the parser.
/// 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<X, V>( fn prompt_validated_wordlist<V, F, E>(
&mut self, &mut self,
prompt: &str, prompt: &str,
wordlist: &Wordlist,
retries: u8, retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>, validator_fn: F,
) -> Result<V, Error> ) -> Result<V, Error>
where where
X: Wordlist; F: Fn(String) -> Result<V, E>,
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,12 +90,15 @@ 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>( fn prompt_validated_passphrase<V, F, E>(
&mut self, &mut self,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>, validator_fn: F,
) -> 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,7 +1,6 @@
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, os::fd::AsRawFd, borrow::Borrow,
}; };
use keyfork_crossterm::{ use keyfork_crossterm::{
@ -13,7 +12,7 @@ use keyfork_crossterm::{
ExecutableCommand, QueueableCommand, ExecutableCommand, QueueableCommand,
}; };
use crate::{Error, Message, PromptHandler, Wordlist}; use crate::{PromptHandler, Message, Wordlist, Error};
#[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>;
@ -156,13 +155,11 @@ 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> impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + AsRawFd + Sized {
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
@ -185,18 +182,20 @@ where
} }
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
fn prompt_validated_wordlist<X, V>( fn prompt_validated_wordlist<V, F, E>(
&mut self, &mut self,
prompt: &str, prompt: &str,
wordlist: &Wordlist,
retries: u8, retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>, validator_fn: F,
) -> Result<V, Error> ) -> Result<V, Error>
where where
X: Wordlist, 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_wordlist::<X>(prompt)?; let s = self.prompt_wordlist(prompt, wordlist)?;
match validator_fn(s) { match validator_fn(s) {
Ok(v) => return Ok(v), Ok(v) => return Ok(v),
Err(e) => { Err(e) => {
@ -215,13 +214,7 @@ where
#[cfg(feature = "mnemonic")] #[cfg(feature = "mnemonic")]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String> fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> 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()?
@ -323,7 +316,7 @@ where
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 words.contains(&word) { if wordlist.contains(word) {
terminal.queue(PrintStyledContent(word.green()))?; terminal.queue(PrintStyledContent(word.green()))?;
} else { } else {
terminal.queue(PrintStyledContent(word.red()))?; terminal.queue(PrintStyledContent(word.red()))?;
@ -344,12 +337,16 @@ where
Ok(input) Ok(input)
} }
fn prompt_validated_passphrase<V>( fn prompt_validated_passphrase<V, F, E>(
&mut self, &mut self,
prompt: &str, prompt: &str,
retries: u8, retries: u8,
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>, validator_fn: F,
) -> 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, Box<dyn std::error::Error>>>; fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::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, Box<dyn std::error::Error>>> { fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, PinError>> {
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(Box::new(PinError::TooShort(len, min_len))); return Err(PinError::TooShort(len, min_len));
} }
if len > max_len { if len > max_len {
return Err(Box::new(PinError::TooLong(len, max_len))); return Err(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(Box::new(PinError::InvalidCharacters(ch, index))); return Err(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, Box<dyn std::error::Error>>> { fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Mnemonic, Self::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(Box::new(Self::Error::InvalidLength(count, wl.clone()))); return Err(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, Box<dyn std::error::Error>>> { fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::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(Box::new(MnemonicChoiceValidationError::InvalidLength( Err(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, Box<dyn std::error::Error>>> { fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::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,17 +219,15 @@ 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(Box::new(MnemonicSetValidationError::InvalidSetLength( return Err(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) => { Err(e) => return Err(Self::Error::MnemonicFromStrError(word_set, 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,15 +28,7 @@ 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();
@ -58,26 +50,7 @@ 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));