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",
"clap",
"clap_complete",
"keyfork-bin",
"keyfork-derive-openpgp",
"keyfork-derive-util",
"keyfork-entropy",
@ -1692,6 +1693,13 @@ dependencies = [
"tokio",
]
[[package]]
name = "keyfork-bin"
version = "0.1.0"
dependencies = [
"anyhow",
]
[[package]]
name = "keyfork-crossterm"
version = "0.27.1"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,7 +7,7 @@ use aes_gcm::{
Aes256Gcm, KeyInit, Nonce,
};
use hkdf::Hkdf;
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
use keyfork_mnemonic_util::{English, Mnemonic};
use keyfork_prompt::{
validators::{mnemonic::MnemonicSetValidator, Validator},
Message as PromptMessage, PromptHandler, Terminal,
@ -173,7 +173,6 @@ pub trait Format {
reader: impl Read + Send + Sync,
) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
// parse input
let private_keys = private_key_discovery
@ -207,12 +206,12 @@ pub trait Format {
let validator = MnemonicSetValidator {
word_lengths: [9, 24],
};
let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist(
QRCODE_COULDNT_READ,
&wordlist,
3,
validator.to_fn(),
)?;
let [nonce_mnemonic, pubkey_mnemonic] = pm
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let nonce = nonce_mnemonic
.as_bytes()
@ -228,8 +227,7 @@ pub trait Format {
// create our shared key
let our_key = EphemeralSecret::random();
let our_pubkey_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
let shared_secret = our_key
.diffie_hellman(&PublicKey::from(their_pubkey))
.to_bytes();
@ -279,8 +277,7 @@ pub trait Format {
}
// 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")]
{
@ -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.
pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Error>> {
let mut pm = Terminal::new(stdin(), stdout())?;
let wordlist = Wordlist::default();
let mut iter_count = None;
let mut shares = vec![];
@ -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) {
iter += 1;
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
let nonce_mnemonic =
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
let our_key = EphemeralSecret::random();
let key_mnemonic =
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
#[cfg(feature = "qrcode")]
{
@ -464,12 +458,12 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
word_lengths: [24, 48],
};
let [pubkey_mnemonic, payload_mnemonic] = pm.prompt_validated_wordlist(
QRCODE_COULDNT_READ,
&wordlist,
3,
validator.to_fn(),
)?;
let [pubkey_mnemonic, payload_mnemonic] = pm
.prompt_validated_wordlist::<English, _>(
QRCODE_COULDNT_READ,
3,
validator.to_fn(),
)?;
let pubkey = pubkey_mnemonic
.as_bytes()
.try_into()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,21 +6,16 @@ use std::process::ExitCode;
use clap::Parser;
use keyfork_bin::{Bin, ClosureBin};
mod cli;
mod config;
fn main() -> ExitCode {
let 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) {
eprintln!("Unable to run command: {e}");
let mut source = e.source();
while let Some(new_error) = source.take() {
eprintln!("Source: {new_error}");
source = new_error.source();
}
return ExitCode::FAILURE;
}
ExitCode::SUCCESS
bin.main()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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