Compare commits
16 Commits
41fc804764
...
6a3018e5e8
Author | SHA1 | Date |
---|---|---|
Ryan Heywood | 6a3018e5e8 | |
Ryan Heywood | d51ee36ace | |
Ryan Heywood | b75d45876a | |
Ryan Heywood | 2541d49fb8 | |
Ryan Heywood | 3b5c1340db | |
Ryan Heywood | 3c1d8e9784 | |
Ryan Heywood | 6093cf9be4 | |
Ryan Heywood | dfcf4b1740 | |
Ryan Heywood | 44d8cf2098 | |
Ryan Heywood | ed61d0685a | |
Ryan Heywood | d481c7e164 | |
Ryan Heywood | 31e51f65a5 | |
Ryan Heywood | 883e0cdf65 | |
Ryan Heywood | 9cb953414f | |
Ryan Heywood | ece9f435d2 | |
Ryan Heywood | 33405ee4fc |
|
@ -1674,6 +1674,7 @@ dependencies = [
|
||||||
"card-backend-pcsc",
|
"card-backend-pcsc",
|
||||||
"clap",
|
"clap",
|
||||||
"clap_complete",
|
"clap_complete",
|
||||||
|
"keyfork-bin",
|
||||||
"keyfork-derive-openpgp",
|
"keyfork-derive-openpgp",
|
||||||
"keyfork-derive-util",
|
"keyfork-derive-util",
|
||||||
"keyfork-entropy",
|
"keyfork-entropy",
|
||||||
|
@ -1692,6 +1693,13 @@ dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "keyfork-bin"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyfork-crossterm"
|
name = "keyfork-crossterm"
|
||||||
version = "0.27.1"
|
version = "0.27.1"
|
||||||
|
|
|
@ -14,6 +14,7 @@ members = [
|
||||||
"crates/qrcode/keyfork-qrcode",
|
"crates/qrcode/keyfork-qrcode",
|
||||||
"crates/qrcode/keyfork-zbar",
|
"crates/qrcode/keyfork-zbar",
|
||||||
"crates/qrcode/keyfork-zbar-sys",
|
"crates/qrcode/keyfork-zbar-sys",
|
||||||
|
"crates/util/keyfork-bin",
|
||||||
"crates/util/keyfork-crossterm",
|
"crates/util/keyfork-crossterm",
|
||||||
"crates/util/keyfork-entropy",
|
"crates/util/keyfork-entropy",
|
||||||
"crates/util/keyfork-frame",
|
"crates/util/keyfork-frame",
|
||||||
|
|
|
@ -40,6 +40,25 @@
|
||||||
//! # keyforkd::test_util::Infallible::Ok(())
|
//! # keyforkd::test_util::Infallible::Ok(())
|
||||||
//! # }).unwrap();
|
//! # }).unwrap();
|
||||||
//! ```
|
//! ```
|
||||||
|
//!
|
||||||
|
//! In tests, the Keyforkd test_util module and TestPrivateKeys can be used.
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use std::str::FromStr;
|
||||||
|
//!
|
||||||
|
//! use keyforkd_client::Client;
|
||||||
|
//! use keyfork_derive_util::DerivationPath;
|
||||||
|
//! use keyfork_derive_util::private_key::TestPrivateKey as PrivateKey;
|
||||||
|
//!
|
||||||
|
//! let seed = b"funky accordion noises";
|
||||||
|
//! keyforkd::test_util::run_test(seed, |socket_path| {
|
||||||
|
//! std::env::set_var("KEYFORKD_SOCKET_PATH", socket_path);
|
||||||
|
//! let derivation_path = DerivationPath::from_str("m/44'/0'").unwrap();
|
||||||
|
//! let mut client = Client::discover_socket().unwrap();
|
||||||
|
//! let xprv = client.request_xprv::<PrivateKey>(&derivation_path).unwrap();
|
||||||
|
//! keyforkd::test_util::Infallible::Ok(())
|
||||||
|
//! }).unwrap();
|
||||||
|
//! ```
|
||||||
|
|
||||||
pub use std::os::unix::net::UnixStream;
|
pub use std::os::unix::net::UnixStream;
|
||||||
use std::{collections::HashMap, path::PathBuf};
|
use std::{collections::HashMap, path::PathBuf};
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -34,3 +34,15 @@ request, as well as its best-effort guess on what path is being derived (using
|
||||||
the `keyfork-derive-path-data` crate), to inform the user of what keys are
|
the `keyfork-derive-path-data` crate), to inform the user of what keys are
|
||||||
requested. Once the server sends the client the new extended private key, the
|
requested. Once the server sends the client the new extended private key, the
|
||||||
client can then choose to use the key as-is, or derive further keys.
|
client can then choose to use the key as-is, or derive further keys.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
A Keyfork server can be automatically started by using [`test_util::run_test`].
|
||||||
|
The function accepts a closure, starting the server before the closure is run,
|
||||||
|
and closing the server after the closure has completed. This may be useful for
|
||||||
|
people writing software that interacts with the Keyfork server, such as a
|
||||||
|
deriver or a provisioner. A test seed must be provided, but can be any content.
|
||||||
|
The closure accepts one argument, the path of the UNIX socket from which the
|
||||||
|
server can be accessed.
|
||||||
|
|
||||||
|
Examples of the test utility can be seen in the `keyforkd-client` crate.
|
||||||
|
|
|
@ -57,7 +57,7 @@ pub async fn start_and_run_server_on(
|
||||||
let service = ServiceBuilder::new()
|
let service = ServiceBuilder::new()
|
||||||
.layer(middleware::BincodeLayer::new())
|
.layer(middleware::BincodeLayer::new())
|
||||||
// TODO: passphrase support and/or store passphrase with mnemonic
|
// TODO: passphrase support and/or store passphrase with mnemonic
|
||||||
.service(Keyforkd::new(mnemonic.seed(None)?));
|
.service(Keyforkd::new(mnemonic.generate_seed(None)));
|
||||||
|
|
||||||
let mut server = match UnixServer::bind(socket_path) {
|
let mut server = match UnixServer::bind(socket_path) {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
|
|
|
@ -25,14 +25,25 @@ pub struct InfallibleError {
|
||||||
/// ```
|
/// ```
|
||||||
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
|
pub type Infallible<T> = std::result::Result<T, InfallibleError>;
|
||||||
|
|
||||||
/// Run a test making use of a Keyforkd server. The path to the socket of the Keyforkd server is
|
/// Run a test making use of a Keyforkd server. The test may use a seed (the first argument) from a
|
||||||
/// provided as the only argument to the closure. The closure is expected to return a Result; the
|
/// test suite, or (as shown in the example below) a simple seed may be used solely to ensure
|
||||||
/// Error field of the Result may be an error returned by a test.
|
/// the server is capable of being interacted with. The test is in the form of a closure, expected
|
||||||
|
/// to return a [`Result`] where success is a unit type (test passed) and the error is any error
|
||||||
|
/// that happened during the test (alternatively, a panic may be used, and will be returned as an
|
||||||
|
/// error).
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
|
/// The function may panic if any errors arise while configuring and using the Tokio multithreaded
|
||||||
|
/// runtime.
|
||||||
///
|
///
|
||||||
/// The function is not expected to run in production; therefore, the function plays "fast and
|
/// # Examples
|
||||||
/// loose" wih the usage of [`Result::expect`]. In normal usage, these should never be an issue.
|
/// ```rust
|
||||||
|
/// use std::os::unix::net::UnixStream;
|
||||||
|
/// let seed = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
/// keyforkd::test_util::run_test(seed.as_slice(), |path| {
|
||||||
|
/// UnixStream::connect(&path).map(|_| ())
|
||||||
|
/// }).unwrap();
|
||||||
|
/// ```
|
||||||
#[allow(clippy::missing_errors_doc)]
|
#[allow(clippy::missing_errors_doc)]
|
||||||
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
|
pub fn run_test<F, E>(seed: &[u8], closure: F) -> Result<(), E>
|
||||||
where
|
where
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
//! Creation of OpenPGP certificates from BIP-0032 derived data.
|
//! Creation of OpenPGP Transferable Secret Keys from BIP-0032 derived data.
|
||||||
|
|
||||||
use std::time::{Duration, SystemTime, SystemTimeError};
|
use std::{
|
||||||
|
str::FromStr,
|
||||||
|
time::{Duration, SystemTime, SystemTimeError},
|
||||||
|
};
|
||||||
|
|
||||||
use derive_util::{DerivationIndex, ExtendedPrivateKey, IndexError, PrivateKey};
|
use derive_util::{DerivationIndex, ExtendedPrivateKey, IndexError, PrivateKey};
|
||||||
use ed25519_dalek::SigningKey;
|
use ed25519_dalek::SigningKey;
|
||||||
|
@ -68,7 +71,24 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||||
};
|
};
|
||||||
|
|
||||||
let epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
|
let epoch = SystemTime::UNIX_EPOCH + Duration::from_secs(1);
|
||||||
let one_day = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
|
let expiration_date = match std::env::var("KEYFORK_OPENPGP_EXPIRE").as_mut() {
|
||||||
|
Ok(var) => {
|
||||||
|
let ch = var.pop();
|
||||||
|
match (ch, u64::from_str(var)) {
|
||||||
|
(Some(ch @ ('d' | 'm' | 'y')), Ok(expire)) => {
|
||||||
|
let multiplier = match ch {
|
||||||
|
'd' => 1,
|
||||||
|
'm' => 30,
|
||||||
|
'y' => 365,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
SystemTime::now() + Duration::from_secs(60 * 60 * 24 * expire * multiplier)
|
||||||
|
}
|
||||||
|
_ => SystemTime::now() + Duration::from_secs(60 * 60 * 24),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(_) => SystemTime::now() + Duration::from_secs(60 * 60 * 24),
|
||||||
|
};
|
||||||
|
|
||||||
// Create certificate with initial key and signature
|
// Create certificate with initial key and signature
|
||||||
let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?;
|
let derived_primary_key = xprv.derive_child(&DerivationIndex::new(0, true)?)?;
|
||||||
|
@ -80,7 +100,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||||
|
|
||||||
// Sign and attach primary key and primary userid
|
// Sign and attach primary key and primary userid
|
||||||
let builder = SignatureBuilder::new(SignatureType::PositiveCertification)
|
let builder = SignatureBuilder::new(SignatureType::PositiveCertification)
|
||||||
.set_key_validity_period(one_day.duration_since(epoch)?)?
|
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
|
||||||
.set_signature_creation_time(epoch)?
|
.set_signature_creation_time(epoch)?
|
||||||
.set_key_flags(primary_key_flags.clone())?;
|
.set_key_flags(primary_key_flags.clone())?;
|
||||||
let binding = userid.bind(&mut primary_key.clone().into_keypair()?, &cert, builder)?;
|
let binding = userid.bind(&mut primary_key.clone().into_keypair()?, &cert, builder)?;
|
||||||
|
@ -89,7 +109,8 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||||
|
|
||||||
// Set certificate expiration to one day
|
// Set certificate expiration to one day
|
||||||
let mut keypair = primary_key.clone().into_keypair()?;
|
let mut keypair = primary_key.clone().into_keypair()?;
|
||||||
let signatures = cert.set_expiration_time(&policy, None, &mut keypair, Some(one_day))?;
|
let signatures =
|
||||||
|
cert.set_expiration_time(&policy, None, &mut keypair, Some(expiration_date))?;
|
||||||
let cert = cert.insert_packets(signatures)?;
|
let cert = cert.insert_packets(signatures)?;
|
||||||
|
|
||||||
let mut cert = cert;
|
let mut cert = cert;
|
||||||
|
@ -127,7 +148,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||||
SignatureBuilder::new(SignatureType::SubkeyBinding)
|
SignatureBuilder::new(SignatureType::SubkeyBinding)
|
||||||
.set_key_flags(subkey_flags.clone())?
|
.set_key_flags(subkey_flags.clone())?
|
||||||
.set_signature_creation_time(epoch)?
|
.set_signature_creation_time(epoch)?
|
||||||
.set_key_validity_period(one_day.duration_since(epoch)?)?
|
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
|
||||||
.set_embedded_signature(
|
.set_embedded_signature(
|
||||||
SignatureBuilder::new(SignatureType::PrimaryKeyBinding)
|
SignatureBuilder::new(SignatureType::PrimaryKeyBinding)
|
||||||
.set_signature_creation_time(epoch)?
|
.set_signature_creation_time(epoch)?
|
||||||
|
@ -141,7 +162,7 @@ pub fn derive(xprv: XPrv, keys: &[KeyFlags], userid: &UserID) -> Result<Cert> {
|
||||||
SignatureBuilder::new(SignatureType::SubkeyBinding)
|
SignatureBuilder::new(SignatureType::SubkeyBinding)
|
||||||
.set_key_flags(subkey_flags.clone())?
|
.set_key_flags(subkey_flags.clone())?
|
||||||
.set_signature_creation_time(epoch)?
|
.set_signature_creation_time(epoch)?
|
||||||
.set_key_validity_period(one_day.duration_since(epoch)?)?
|
.set_key_validity_period(expiration_date.duration_since(epoch)?)?
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sign subkey with primary key and attach to cert
|
// Sign subkey with primary key and attach to cert
|
||||||
|
|
|
@ -209,7 +209,7 @@ impl DerivationRequest {
|
||||||
/// # }
|
/// # }
|
||||||
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
|
pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result<DerivationResponse> {
|
||||||
// TODO: passphrase support and/or store passphrase within mnemonic
|
// TODO: passphrase support and/or store passphrase within mnemonic
|
||||||
self.derive_with_master_seed(&mnemonic.seed(None)?)
|
self.derive_with_master_seed(&mnemonic.generate_seed(None))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Derive an [`ExtendedPrivateKey`] using the given seed.
|
/// Derive an [`ExtendedPrivateKey`] using the given seed.
|
||||||
|
|
|
@ -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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,52 +7,33 @@ use std::{
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages};
|
use keyfork_shard::{openpgp::OpenPGP, Format};
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
fn validate(
|
fn validate(
|
||||||
shard: impl AsRef<Path>,
|
shard: impl AsRef<Path>,
|
||||||
key_discovery: Option<&str>,
|
key_discovery: Option<&str>,
|
||||||
) -> Result<(File, Vec<Cert>)> {
|
) -> Result<(File, Option<PathBuf>)> {
|
||||||
let key_discovery = key_discovery.map(PathBuf::from);
|
let key_discovery = key_discovery.map(PathBuf::from);
|
||||||
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
||||||
|
|
||||||
// Load certs from path
|
Ok((File::open(shard)?, key_discovery))
|
||||||
let certs = key_discovery
|
|
||||||
.map(discover_certs)
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or(vec![]);
|
|
||||||
|
|
||||||
Ok((File::open(shard)?, certs))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
let program_name = args.next().expect("program name");
|
let program_name = args.next().expect("program name");
|
||||||
let args = args.collect::<Vec<_>>();
|
let args = args.collect::<Vec<_>>();
|
||||||
let (messages_file, cert_list) = match args.as_slice() {
|
let (messages_file, key_discovery) = match args.as_slice() {
|
||||||
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
|
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
|
||||||
[shard] => validate(shard, None)?,
|
[shard] => validate(shard, None)?,
|
||||||
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut encrypted_messages = parse_messages(messages_file)?;
|
let openpgp = OpenPGP;
|
||||||
|
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?;
|
||||||
let encrypted_metadata = encrypted_messages
|
print!("{}", smex::encode(bytes));
|
||||||
.pop_front()
|
|
||||||
.expect("any pgp encrypted message");
|
|
||||||
|
|
||||||
let mut bytes = vec![];
|
|
||||||
|
|
||||||
combine(
|
|
||||||
cert_list,
|
|
||||||
&encrypted_metadata,
|
|
||||||
encrypted_messages.into(),
|
|
||||||
&mut bytes,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
print!("{}", smex::encode(&bytes));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,47 +7,33 @@ use std::{
|
||||||
process::ExitCode,
|
process::ExitCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_shard::openpgp::{decrypt, discover_certs, openpgp::Cert, parse_messages};
|
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
fn validate<'a>(
|
fn validate(
|
||||||
messages_file: impl AsRef<Path>,
|
shard: impl AsRef<Path>,
|
||||||
key_discovery: impl Into<Option<&'a str>>,
|
key_discovery: Option<&str>,
|
||||||
) -> Result<(File, Vec<Cert>)> {
|
) -> Result<(File, Option<PathBuf>)> {
|
||||||
let key_discovery = key_discovery.into().map(PathBuf::from);
|
let key_discovery = key_discovery.map(PathBuf::from);
|
||||||
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
key_discovery.as_ref().map(std::fs::metadata).transpose()?;
|
||||||
|
|
||||||
// Load certs from path
|
Ok((File::open(shard)?, key_discovery))
|
||||||
let certs = key_discovery
|
|
||||||
.map(discover_certs)
|
|
||||||
.transpose()?
|
|
||||||
.unwrap_or(vec![]);
|
|
||||||
|
|
||||||
Ok((File::open(messages_file)?, certs))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
let program_name = args.next().expect("program name");
|
let program_name = args.next().expect("program name");
|
||||||
let args = args.collect::<Vec<_>>();
|
let args = args.collect::<Vec<_>>();
|
||||||
let (messages_file, cert_list) = match args.as_slice() {
|
let (messages_file, key_discovery) = match args.as_slice() {
|
||||||
[messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?,
|
[shard, key_discovery] => validate(shard, Some(key_discovery))?,
|
||||||
[messages_file] => validate(messages_file, None)?,
|
[shard] => validate(shard, None)?,
|
||||||
_ => panic!("Usage: {program_name} messages_file [key_discovery]"),
|
_ => panic!("Usage: {program_name} <shard> [key_discovery]"),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut encrypted_messages = parse_messages(messages_file)?;
|
let openpgp = OpenPGP;
|
||||||
|
|
||||||
let encrypted_metadata = encrypted_messages
|
openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file)?;
|
||||||
.pop_front()
|
|
||||||
.expect("any pgp encrypted message");
|
|
||||||
|
|
||||||
decrypt(
|
|
||||||
&cert_list,
|
|
||||||
&encrypted_metadata,
|
|
||||||
encrypted_messages.make_contiguous(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,14 +2,12 @@
|
||||||
|
|
||||||
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
use std::{env, path::PathBuf, process::ExitCode, str::FromStr};
|
||||||
|
|
||||||
use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split};
|
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum Error {
|
enum Error {
|
||||||
Usage(String),
|
Usage(String),
|
||||||
Input,
|
Input,
|
||||||
Threshold(u8, u8),
|
|
||||||
InvalidCertCount(usize, u8),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for Error {
|
impl std::fmt::Display for Error {
|
||||||
|
@ -19,15 +17,6 @@ impl std::fmt::Display for Error {
|
||||||
write!(f, "Usage: {program_name} threshold max key_discovery")
|
write!(f, "Usage: {program_name} threshold max key_discovery")
|
||||||
}
|
}
|
||||||
Error::Input => f.write_str("Expected hex encoded input"),
|
Error::Input => f.write_str("Expected hex encoded input"),
|
||||||
Error::Threshold(threshold, max) => {
|
|
||||||
write!(
|
|
||||||
f,
|
|
||||||
"Invalid threshold: 0 < threshold {threshold} <= max {max} < 256"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Error::InvalidCertCount(count, max) => {
|
|
||||||
write!(f, "Invalid cert count: count {count} != max {max}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,31 +25,20 @@ impl std::error::Error for Error {}
|
||||||
|
|
||||||
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
type Result<T, E = Box<dyn std::error::Error>> = std::result::Result<T, E>;
|
||||||
|
|
||||||
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, Vec<Cert>)> {
|
fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, u8, PathBuf)> {
|
||||||
let threshold = u8::from_str(threshold)?;
|
let threshold = u8::from_str(threshold)?;
|
||||||
let max = u8::from_str(max)?;
|
let max = u8::from_str(max)?;
|
||||||
let key_discovery = PathBuf::from(key_discovery);
|
let key_discovery = PathBuf::from(key_discovery);
|
||||||
if threshold > max {
|
|
||||||
return Err(Error::Threshold(threshold, max).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify path exists
|
|
||||||
std::fs::metadata(&key_discovery)?;
|
std::fs::metadata(&key_discovery)?;
|
||||||
|
|
||||||
// Load certs from path
|
Ok((threshold, max, key_discovery))
|
||||||
let certs = discover_certs(key_discovery)?;
|
|
||||||
if certs.len() != max.into() {
|
|
||||||
return Err(Error::InvalidCertCount(certs.len(), max).into());
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok((threshold, certs))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run() -> Result<()> {
|
fn run() -> Result<()> {
|
||||||
let mut args = env::args();
|
let mut args = env::args();
|
||||||
let program_name = args.next().expect("program name");
|
let program_name = args.next().expect("program name");
|
||||||
let args = args.collect::<Vec<_>>();
|
let args = args.collect::<Vec<_>>();
|
||||||
let (threshold, cert_list) = match args.as_slice() {
|
let (threshold, max, key_discovery) = match args.as_slice() {
|
||||||
[threshold, max, key_discovery] => validate(threshold, max, key_discovery)?,
|
[threshold, max, key_discovery] => validate(threshold, max, key_discovery)?,
|
||||||
_ => return Err(Error::Usage(program_name).into()),
|
_ => return Err(Error::Usage(program_name).into()),
|
||||||
};
|
};
|
||||||
|
@ -69,11 +47,12 @@ 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?)?
|
||||||
};
|
};
|
||||||
|
|
||||||
split(threshold, cert_list, &input, std::io::stdout())?;
|
let openpgp = OpenPGP;
|
||||||
|
|
||||||
|
openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
|
||||||
use std::io::{stdin, stdout, Write};
|
use std::io::{stdin, stdout, Read, Write};
|
||||||
|
|
||||||
use aes_gcm::{
|
use aes_gcm::{
|
||||||
aead::{Aead, AeadCore, OsRng},
|
aead::{consts::U12, Aead, AeadCore, OsRng},
|
||||||
Aes256Gcm, KeyInit,
|
Aes256Gcm, KeyInit, Nonce,
|
||||||
};
|
};
|
||||||
use hkdf::Hkdf;
|
use hkdf::Hkdf;
|
||||||
use keyfork_mnemonic_util::{Mnemonic, Wordlist};
|
use keyfork_mnemonic_util::{English, Mnemonic};
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
validators::{mnemonic::MnemonicSetValidator, Validator},
|
validators::{mnemonic::MnemonicSetValidator, Validator},
|
||||||
Message as PromptMessage, PromptHandler, Terminal,
|
Message as PromptMessage, PromptHandler, Terminal,
|
||||||
|
@ -16,9 +16,338 @@ use sha2::Sha256;
|
||||||
use sharks::{Share, Sharks};
|
use sharks::{Share, Sharks};
|
||||||
use x25519_dalek::{EphemeralSecret, PublicKey};
|
use x25519_dalek::{EphemeralSecret, PublicKey};
|
||||||
|
|
||||||
|
// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size
|
||||||
|
const ENC_LEN: u8 = 4 * 16;
|
||||||
|
|
||||||
#[cfg(feature = "openpgp")]
|
#[cfg(feature = "openpgp")]
|
||||||
pub mod openpgp;
|
pub mod openpgp;
|
||||||
|
|
||||||
|
/// A trait to specify where keys can be discovered from, such as a Rust-native type or a path on
|
||||||
|
/// the filesystem that keys may be read from.
|
||||||
|
pub trait KeyDiscovery<F: Format + ?Sized> {
|
||||||
|
/// Discover public keys for the associated format.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if public keys could not be loaded from the given discovery
|
||||||
|
/// mechanism. A discovery mechanism _must_ be able to detect public keys.
|
||||||
|
fn discover_public_keys(&self) -> Result<Vec<F::PublicKey>, F::Error>;
|
||||||
|
|
||||||
|
/// Discover private keys for the associated format.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if private keys could not be loaded from the given
|
||||||
|
/// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the
|
||||||
|
/// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never
|
||||||
|
/// exist on-system) or an empty container (such as an empty Vec); in either case, this method
|
||||||
|
/// _must not_ return an error if keys are accessible but can't be transferred into memory.
|
||||||
|
fn discover_private_keys(&self) -> Result<F::PrivateKeyData, F::Error>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A format to use for splitting and combining secrets.
|
||||||
|
pub trait Format {
|
||||||
|
/// The error type returned from any failed operations.
|
||||||
|
type Error: std::error::Error + 'static;
|
||||||
|
|
||||||
|
/// A type encapsulating a single public key recipient.
|
||||||
|
type PublicKey;
|
||||||
|
|
||||||
|
/// A type encapsulating the private key recipients of shards.
|
||||||
|
type PrivateKeyData;
|
||||||
|
|
||||||
|
/// A type representing a Signer derived from the secret.
|
||||||
|
type SigningKey;
|
||||||
|
|
||||||
|
/// A type representing the parsed, but encrypted, Shard data.
|
||||||
|
type EncryptedData;
|
||||||
|
|
||||||
|
/// Derive a signer
|
||||||
|
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey;
|
||||||
|
|
||||||
|
/// Format a header containing necessary metadata. Such metadata contains a version byte, a
|
||||||
|
/// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to
|
||||||
|
/// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork
|
||||||
|
/// will use the same key_data for both, ensuring an iteration of this method will match with
|
||||||
|
/// iterations in methods called later.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if encryption to any of the public keys fails.
|
||||||
|
fn format_encrypted_header(
|
||||||
|
&self,
|
||||||
|
signing_key: &Self::SigningKey,
|
||||||
|
key_data: &[Self::PublicKey],
|
||||||
|
threshold: u8,
|
||||||
|
) -> Result<Self::EncryptedData, Self::Error>;
|
||||||
|
|
||||||
|
/// Format a shard encrypted to the given public key, signing with the private key.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the public key used to encrypt the shard is unsuitable
|
||||||
|
/// for encryption, or if an error occurs while encrypting.
|
||||||
|
fn encrypt_shard(
|
||||||
|
&self,
|
||||||
|
shard: &[u8],
|
||||||
|
public_key: &Self::PublicKey,
|
||||||
|
signing_key: &mut Self::SigningKey,
|
||||||
|
) -> Result<Self::EncryptedData, Self::Error>;
|
||||||
|
|
||||||
|
/// Parse the Shard file into a processable type.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the Shard file could not be read from or if the Shard
|
||||||
|
/// file could not be properly parsed.
|
||||||
|
fn parse_shard_file(
|
||||||
|
&self,
|
||||||
|
shard_file: impl Read + Send + Sync,
|
||||||
|
) -> Result<Vec<Self::EncryptedData>, Self::Error>;
|
||||||
|
|
||||||
|
/// Write the Shard data to a Shard file.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the Shard data could not be properly serialized or if the
|
||||||
|
/// Shard file could not be written to.
|
||||||
|
fn format_shard_file(
|
||||||
|
&self,
|
||||||
|
encrypted_data: &[Self::EncryptedData],
|
||||||
|
shard_file: impl Write + Send + Sync,
|
||||||
|
) -> Result<(), Self::Error>;
|
||||||
|
|
||||||
|
/// Decrypt shares and associated metadata from a readable input. For the current version of
|
||||||
|
/// Keyfork, the only associated metadata is a u8 representing the threshold to combine
|
||||||
|
/// secrets.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the shardfile couldn't be read from, if all shards
|
||||||
|
/// could not be decrypted, or if a shard could not be parsed from the decrypted data.
|
||||||
|
fn decrypt_all_shards(
|
||||||
|
&self,
|
||||||
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
|
encrypted_messages: &[Self::EncryptedData],
|
||||||
|
) -> Result<(Vec<Share>, u8), Self::Error>;
|
||||||
|
|
||||||
|
/// Decrypt a single share and associated metadata from a reaable input. For the current
|
||||||
|
/// version of Keyfork, the only associated metadata is a u8 representing the threshold to
|
||||||
|
/// combine secrets.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the shardfile couldn't be read from, if a shard could not
|
||||||
|
/// be decrypted, or if a shard could not be parsed from the decrypted data.
|
||||||
|
fn decrypt_one_shard(
|
||||||
|
&self,
|
||||||
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
|
encrypted_data: &[Self::EncryptedData],
|
||||||
|
) -> Result<(Share, u8), Self::Error>;
|
||||||
|
|
||||||
|
/// Decrypt multiple shares and combine them to recreate a secret.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the shares can't be decrypted or if the shares can't
|
||||||
|
/// be combined into a secret.
|
||||||
|
fn decrypt_all_shards_to_secret(
|
||||||
|
&self,
|
||||||
|
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||||
|
reader: impl Read + Send + Sync,
|
||||||
|
) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
|
||||||
|
let private_keys = private_key_discovery
|
||||||
|
.map(|p| p.discover_private_keys())
|
||||||
|
.transpose()?;
|
||||||
|
let encrypted_messages = self.parse_shard_file(reader)?;
|
||||||
|
let (shares, threshold) = self.decrypt_all_shards(private_keys, &encrypted_messages)?;
|
||||||
|
|
||||||
|
let secret = Sharks(threshold)
|
||||||
|
.recover(&shares)
|
||||||
|
.map_err(|e| SharksError::CombineShare(e.to_string()))?;
|
||||||
|
|
||||||
|
Ok(secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Establish an AES-256-GCM transport key using ECDH, decrypt a single shard, and encrypt the
|
||||||
|
/// shard to the AES key.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if a share can't be decrypted. The method will not return an
|
||||||
|
/// error if the camera is inaccessible or if a hardware error is encountered while scanning a
|
||||||
|
/// QR code; instead, a mnemonic prompt will be used.
|
||||||
|
fn decrypt_one_shard_for_transport(
|
||||||
|
&self,
|
||||||
|
private_key_discovery: Option<impl KeyDiscovery<Self>>,
|
||||||
|
reader: impl Read + Send + Sync,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut pm = Terminal::new(stdin(), stdout())?;
|
||||||
|
|
||||||
|
// parse input
|
||||||
|
let private_keys = private_key_discovery
|
||||||
|
.map(|p| p.discover_private_keys())
|
||||||
|
.transpose()?;
|
||||||
|
let encrypted_messages = self.parse_shard_file(reader)?;
|
||||||
|
|
||||||
|
// establish AES-256-GCM key via ECDH
|
||||||
|
let mut nonce_data: Option<[u8; 12]> = None;
|
||||||
|
let mut pubkey_data: Option<[u8; 32]> = None;
|
||||||
|
|
||||||
|
// receive remote data via scanning QR code from camera
|
||||||
|
#[cfg(feature = "qrcode")]
|
||||||
|
{
|
||||||
|
pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?;
|
||||||
|
if let Ok(Some(hex)) =
|
||||||
|
keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0)
|
||||||
|
{
|
||||||
|
let decoded_data = smex::decode(&hex)?;
|
||||||
|
nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?);
|
||||||
|
pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?)
|
||||||
|
} else {
|
||||||
|
pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// if QR code scanning failed or was unavailable, read from a set of mnemonics
|
||||||
|
let (nonce, their_pubkey) = match (nonce_data, pubkey_data) {
|
||||||
|
(Some(nonce), Some(pubkey)) => (nonce, pubkey),
|
||||||
|
_ => {
|
||||||
|
let validator = MnemonicSetValidator {
|
||||||
|
word_lengths: [9, 24],
|
||||||
|
};
|
||||||
|
let [nonce_mnemonic, pubkey_mnemonic] = pm
|
||||||
|
.prompt_validated_wordlist::<English, _>(
|
||||||
|
QRCODE_COULDNT_READ,
|
||||||
|
3,
|
||||||
|
validator.to_fn(),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let nonce = nonce_mnemonic
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| InvalidData)?;
|
||||||
|
let pubkey = pubkey_mnemonic
|
||||||
|
.as_bytes()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| InvalidData)?;
|
||||||
|
(nonce, pubkey)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// create our shared key
|
||||||
|
let our_key = EphemeralSecret::random();
|
||||||
|
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
||||||
|
let shared_secret = our_key
|
||||||
|
.diffie_hellman(&PublicKey::from(their_pubkey))
|
||||||
|
.to_bytes();
|
||||||
|
let hkdf = Hkdf::<Sha256>::new(None, &shared_secret);
|
||||||
|
let mut hkdf_output = [0u8; 256 / 8];
|
||||||
|
hkdf.expand(&[], &mut hkdf_output)?;
|
||||||
|
let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?;
|
||||||
|
|
||||||
|
// decrypt a single shard and create the payload
|
||||||
|
let (share, threshold) = self.decrypt_one_shard(private_keys, &encrypted_messages)?;
|
||||||
|
let mut payload = Vec::from(&share);
|
||||||
|
payload.insert(0, HUNK_VERSION);
|
||||||
|
payload.insert(1, threshold);
|
||||||
|
assert!(
|
||||||
|
payload.len() <= ENC_LEN as usize,
|
||||||
|
"invalid share length (too long, max {ENC_LEN} bytes)"
|
||||||
|
);
|
||||||
|
|
||||||
|
// encrypt data
|
||||||
|
let nonce = Nonce::<U12>::from_slice(&nonce);
|
||||||
|
let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?;
|
||||||
|
|
||||||
|
// convert data to a static-size payload
|
||||||
|
// NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX
|
||||||
|
#[allow(clippy::assertions_on_constants)]
|
||||||
|
{
|
||||||
|
assert!(ENC_LEN < u8::MAX, "padding byte can be u8");
|
||||||
|
}
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize];
|
||||||
|
assert!(
|
||||||
|
payload_bytes.len() < out_bytes.len(),
|
||||||
|
"encrypted payload larger than acceptable limit"
|
||||||
|
);
|
||||||
|
out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes);
|
||||||
|
|
||||||
|
// NOTE: This previously used a single repeated value as the padding byte, but resulted in
|
||||||
|
// difficulty when entering in prompts manually, as one's place could be lost due to
|
||||||
|
// repeated keywords. This is resolved below by having sequentially increasing numbers up to
|
||||||
|
// but not including the last byte.
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)])
|
||||||
|
.iter_mut()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
*byte = (i % u8::MAX as usize) as u8;
|
||||||
|
}
|
||||||
|
|
||||||
|
// safety: size of out_bytes is constant and always % 4 == 0
|
||||||
|
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
|
||||||
|
|
||||||
|
#[cfg(feature = "qrcode")]
|
||||||
|
{
|
||||||
|
use keyfork_qrcode::{qrencode, ErrorCorrection};
|
||||||
|
let mut qrcode_data = our_pubkey_mnemonic.to_bytes();
|
||||||
|
qrcode_data.extend(payload_mnemonic.as_bytes());
|
||||||
|
if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Highest) {
|
||||||
|
pm.prompt_message(PromptMessage::Text(
|
||||||
|
concat!(
|
||||||
|
"A QR code will be displayed after this prompt. ",
|
||||||
|
"Send the QR code back to the operator combining the shards. ",
|
||||||
|
"Nobody else should scan this QR code."
|
||||||
|
)
|
||||||
|
.to_string(),
|
||||||
|
))?;
|
||||||
|
pm.prompt_message(PromptMessage::Data(qrcode))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pm.prompt_message(PromptMessage::Text(format!(
|
||||||
|
"Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}"
|
||||||
|
)))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Split a secret into a shard for every shard in keys, with the given Shamir's Secret Sharing
|
||||||
|
/// threshold.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The method may return an error if the shares can't be encrypted.
|
||||||
|
fn shard_and_encrypt(
|
||||||
|
&self,
|
||||||
|
threshold: u8,
|
||||||
|
max: u8,
|
||||||
|
secret: &[u8],
|
||||||
|
public_key_discovery: impl KeyDiscovery<Self>,
|
||||||
|
writer: impl Write + Send + Sync,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
let mut signing_key = self.derive_signing_key(secret);
|
||||||
|
|
||||||
|
let sharks = Sharks(threshold);
|
||||||
|
let dealer = sharks.dealer(secret);
|
||||||
|
|
||||||
|
let public_keys = public_key_discovery.discover_public_keys()?;
|
||||||
|
assert!(
|
||||||
|
public_keys.len() < u8::MAX as usize,
|
||||||
|
"must have less than u8::MAX public keys"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
max,
|
||||||
|
public_keys.len() as u8,
|
||||||
|
"max must be equal to amount of public keys"
|
||||||
|
);
|
||||||
|
let max = public_keys.len() as u8;
|
||||||
|
assert!(max >= threshold, "threshold must not exceed max keys");
|
||||||
|
|
||||||
|
let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?;
|
||||||
|
let mut messages = vec![header];
|
||||||
|
for (pk, share) in public_keys.iter().zip(dealer) {
|
||||||
|
let shard = Vec::from(&share);
|
||||||
|
messages.push(self.encrypt_shard(&shard, pk, &mut signing_key)?);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.format_shard_file(&messages, writer)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
|
/// Errors encountered while creating or combining shares using Shamir's Secret Sharing.
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
pub enum SharksError {
|
pub enum SharksError {
|
||||||
|
@ -63,7 +392,6 @@ const QRCODE_ERROR: &str = "Unable to scan a QR code. Falling back to text entry
|
||||||
/// incompatible with the currently running version.
|
/// 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![];
|
||||||
|
@ -74,11 +402,9 @@ pub fn remote_decrypt(w: &mut impl Write) -> Result<(), Box<dyn std::error::Erro
|
||||||
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
|
while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) {
|
||||||
iter += 1;
|
iter += 1;
|
||||||
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
let nonce = Aes256Gcm::generate_nonce(&mut OsRng);
|
||||||
let nonce_mnemonic =
|
let nonce_mnemonic = unsafe { Mnemonic::from_raw_bytes(nonce.as_slice()) };
|
||||||
unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) };
|
|
||||||
let our_key = EphemeralSecret::random();
|
let our_key = EphemeralSecret::random();
|
||||||
let key_mnemonic =
|
let key_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
||||||
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
|
|
||||||
|
|
||||||
#[cfg(feature = "qrcode")]
|
#[cfg(feature = "qrcode")]
|
||||||
{
|
{
|
||||||
|
@ -132,9 +458,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.prompt_validated_wordlist(
|
let [pubkey_mnemonic, payload_mnemonic] = pm
|
||||||
|
.prompt_validated_wordlist::<English, _>(
|
||||||
QRCODE_COULDNT_READ,
|
QRCODE_COULDNT_READ,
|
||||||
&wordlist,
|
|
||||||
3,
|
3,
|
||||||
validator.to_fn(),
|
validator.to_fn(),
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -17,7 +17,7 @@ use keyfork_derive_openpgp::{
|
||||||
derive_util::{DerivationPath, PathError, VariableLengthSeed},
|
derive_util::{DerivationPath, PathError, VariableLengthSeed},
|
||||||
XPrv,
|
XPrv,
|
||||||
};
|
};
|
||||||
use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist};
|
use keyfork_mnemonic_util::{English, Mnemonic, MnemonicFromStrError, MnemonicGenerationError};
|
||||||
use keyfork_prompt::{
|
use keyfork_prompt::{
|
||||||
validators::{mnemonic::MnemonicSetValidator, Validator},
|
validators::{mnemonic::MnemonicSetValidator, Validator},
|
||||||
Error as PromptError, Message as PromptMessage, PromptHandler, Terminal,
|
Error as PromptError, Message as PromptMessage, PromptHandler, Terminal,
|
||||||
|
@ -57,8 +57,8 @@ const SHARD_METADATA_VERSION: u8 = 1;
|
||||||
const SHARD_METADATA_OFFSET: usize = 2;
|
const SHARD_METADATA_OFFSET: usize = 2;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, QRCODE_PROMPT,
|
Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR,
|
||||||
QRCODE_TIMEOUT,
|
QRCODE_PROMPT, QRCODE_TIMEOUT, KeyDiscovery
|
||||||
};
|
};
|
||||||
|
|
||||||
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
|
// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding
|
||||||
|
@ -163,6 +163,55 @@ impl EncryptedMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse OpenPGP packets for encrypted messages.
|
||||||
|
pub fn from_reader(input: impl Read + Send + Sync) -> openpgp::Result<Vec<Self>> {
|
||||||
|
let mut pkesks = Vec::new();
|
||||||
|
let mut encrypted_messages = vec![];
|
||||||
|
|
||||||
|
for packet in PacketPile::from_reader(input)
|
||||||
|
.map_err(Error::Sequoia)?
|
||||||
|
.into_children()
|
||||||
|
{
|
||||||
|
match packet {
|
||||||
|
Packet::PKESK(p) => pkesks.push(p),
|
||||||
|
Packet::SEIP(s) => {
|
||||||
|
encrypted_messages.push(EncryptedMessage::new(&mut pkesks, s));
|
||||||
|
}
|
||||||
|
s => {
|
||||||
|
panic!("Invalid variant found: {}", s.tag());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(encrypted_messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Serialize all contents of the message to a writer.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The function may error for any condition in Sequoia's Serialize trait.
|
||||||
|
fn serialize(&self, mut o: impl std::io::Write + Send + Sync) -> openpgp::Result<()> {
|
||||||
|
for pkesk in &self.pkesks {
|
||||||
|
let mut packet = vec![];
|
||||||
|
pkesk.serialize(&mut packet).map_err(Error::Sequoia)?;
|
||||||
|
let message = Message::new(&mut o);
|
||||||
|
let mut message = ArbitraryWriter::new(message, Tag::PKESK).map_err(Error::Sequoia)?;
|
||||||
|
message.write_all(&packet).map_err(Error::SequoiaIo)?;
|
||||||
|
message.finalize().map_err(Error::Sequoia)?;
|
||||||
|
}
|
||||||
|
let mut packet = vec![];
|
||||||
|
self.message
|
||||||
|
.serialize(&mut packet)
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
|
||||||
|
let message = Message::new(&mut o);
|
||||||
|
let mut message = ArbitraryWriter::new(message, Tag::SEIP).map_err(Error::Sequoia)?;
|
||||||
|
message.write_all(&packet).map_err(Error::SequoiaIo)?;
|
||||||
|
message.finalize().map_err(Error::Sequoia)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Decrypt the message with a Sequoia policy and decryptor.
|
/// Decrypt the message with a Sequoia policy and decryptor.
|
||||||
///
|
///
|
||||||
/// This method creates a container containing the packets and passes the serialized container
|
/// This method creates a container containing the packets and passes the serialized container
|
||||||
|
@ -176,23 +225,8 @@ impl EncryptedMessage {
|
||||||
H: VerificationHelper + DecryptionHelper,
|
H: VerificationHelper + DecryptionHelper,
|
||||||
{
|
{
|
||||||
let mut packets = vec![];
|
let mut packets = vec![];
|
||||||
|
self.serialize(&mut packets)
|
||||||
for pkesk in &self.pkesks {
|
|
||||||
let mut packet = vec![];
|
|
||||||
pkesk.serialize(&mut packet).map_err(Error::Sequoia)?;
|
|
||||||
let message = Message::new(&mut packets);
|
|
||||||
let mut message = ArbitraryWriter::new(message, Tag::PKESK).map_err(Error::Sequoia)?;
|
|
||||||
message.write_all(&packet).map_err(Error::SequoiaIo)?;
|
|
||||||
message.finalize().map_err(Error::Sequoia)?;
|
|
||||||
}
|
|
||||||
let mut packet = vec![];
|
|
||||||
self.message
|
|
||||||
.serialize(&mut packet)
|
|
||||||
.map_err(Error::Sequoia)?;
|
.map_err(Error::Sequoia)?;
|
||||||
let message = Message::new(&mut packets);
|
|
||||||
let mut message = ArbitraryWriter::new(message, Tag::SEIP).map_err(Error::Sequoia)?;
|
|
||||||
message.write_all(&packet).map_err(Error::SequoiaIo)?;
|
|
||||||
message.finalize().map_err(Error::Sequoia)?;
|
|
||||||
|
|
||||||
let mut decryptor = DecryptorBuilder::from_bytes(&packets)
|
let mut decryptor = DecryptorBuilder::from_bytes(&packets)
|
||||||
.map_err(Error::Sequoia)?
|
.map_err(Error::Sequoia)?
|
||||||
|
@ -207,6 +241,10 @@ impl EncryptedMessage {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
///
|
||||||
|
pub struct OpenPGP;
|
||||||
|
|
||||||
|
impl OpenPGP {
|
||||||
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
|
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
|
||||||
/// from a file, or from files one level deep in a directory.
|
/// from a file, or from files one level deep in a directory.
|
||||||
///
|
///
|
||||||
|
@ -235,6 +273,347 @@ pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
||||||
Ok(vec)
|
Ok(vec)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Format for OpenPGP {
|
||||||
|
type Error = Error;
|
||||||
|
type PublicKey = Cert;
|
||||||
|
type PrivateKeyData = Vec<Cert>;
|
||||||
|
type SigningKey = Cert;
|
||||||
|
type EncryptedData = EncryptedMessage;
|
||||||
|
|
||||||
|
/// Derive an OpenPGP Shard certificate from the given seed.
|
||||||
|
fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey {
|
||||||
|
let seed = VariableLengthSeed::new(seed);
|
||||||
|
// build cert to sign encrypted shares
|
||||||
|
let userid = UserID::from("keyfork-sss");
|
||||||
|
let path = DerivationPath::from_str("m/7366512'/0'").expect("valid derivation path");
|
||||||
|
let xprv = XPrv::new(seed)
|
||||||
|
.derive_path(&path)
|
||||||
|
.expect("valid derivation");
|
||||||
|
keyfork_derive_openpgp::derive(
|
||||||
|
xprv,
|
||||||
|
&[KeyFlags::empty().set_certification().set_signing()],
|
||||||
|
&userid,
|
||||||
|
)
|
||||||
|
.expect("valid cert creation")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_encrypted_header(
|
||||||
|
&self,
|
||||||
|
signing_key: &Self::SigningKey,
|
||||||
|
key_data: &[Self::PublicKey],
|
||||||
|
threshold: u8,
|
||||||
|
) -> Result<Self::EncryptedData, Self::Error> {
|
||||||
|
let policy = StandardPolicy::new();
|
||||||
|
let mut pp = vec![SHARD_METADATA_VERSION, threshold];
|
||||||
|
// Note: Sequoia does not export private keys on a Cert, only on a TSK
|
||||||
|
signing_key
|
||||||
|
.serialize(&mut pp)
|
||||||
|
.expect("serialize cert into bytes");
|
||||||
|
for cert in key_data {
|
||||||
|
cert.serialize(&mut pp)
|
||||||
|
.expect("serialize pubkey into bytes");
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify packet pile
|
||||||
|
let mut iter = openpgp::cert::CertParser::from_bytes(&pp[SHARD_METADATA_OFFSET..])
|
||||||
|
.expect("should have certs");
|
||||||
|
let first_cert = iter.next().transpose().ok().flatten().expect("first cert");
|
||||||
|
assert_eq!(signing_key, &first_cert);
|
||||||
|
|
||||||
|
for (packet_cert, cert) in iter.zip(key_data) {
|
||||||
|
assert_eq!(
|
||||||
|
&packet_cert.expect("parsed packet cert"),
|
||||||
|
cert,
|
||||||
|
"packet pile could not recreate cert: {}",
|
||||||
|
cert.fingerprint(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let valid_certs = key_data
|
||||||
|
.iter()
|
||||||
|
.map(|c| c.with_policy(&policy, None))
|
||||||
|
.collect::<openpgp::Result<Vec<_>>>()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
let recipients = valid_certs.iter().flat_map(|vc| {
|
||||||
|
get_encryption_keys(vc).map(|key| Recipient::new(KeyID::wildcard(), key.key()))
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process is as follows:
|
||||||
|
// * Any OpenPGP message
|
||||||
|
// * An encrypted message
|
||||||
|
// * A literal message
|
||||||
|
// * The packet pile
|
||||||
|
//
|
||||||
|
// When decrypting, OpenPGP will see:
|
||||||
|
// * A message, and parse it
|
||||||
|
// * An encrypted message, and decrypt it
|
||||||
|
// * A literal message, and extract it
|
||||||
|
// * The packet pile
|
||||||
|
let mut output = vec![];
|
||||||
|
let message = Message::new(&mut output);
|
||||||
|
let encrypted_message = Encryptor2::for_recipients(message, recipients)
|
||||||
|
.build()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
let mut literal_message = LiteralWriter::new(encrypted_message)
|
||||||
|
.build()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
literal_message.write_all(&pp).map_err(Error::SequoiaIo)?;
|
||||||
|
literal_message.finalize().map_err(Error::Sequoia)?;
|
||||||
|
|
||||||
|
// Parse it into an EncryptedMessage. Yes, this takes a serialized message
|
||||||
|
// and deserializes it. Don't think about it too hard. It's easier this way.
|
||||||
|
|
||||||
|
let mut pkesks = vec![];
|
||||||
|
for packet in PacketPile::from_reader(output.as_slice())
|
||||||
|
.map_err(Error::Sequoia)?
|
||||||
|
.into_children()
|
||||||
|
{
|
||||||
|
match packet {
|
||||||
|
Packet::PKESK(p) => pkesks.push(p),
|
||||||
|
Packet::SEIP(s) => return Ok(EncryptedMessage::new(&mut pkesks, s)),
|
||||||
|
s => panic!("Invalid variant found: {}", s.tag()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("Unable to build EncryptedMessage from PacketPile");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encrypt_shard(
|
||||||
|
&self,
|
||||||
|
shard: &[u8],
|
||||||
|
public_key: &Cert,
|
||||||
|
signing_key: &mut Self::SigningKey,
|
||||||
|
) -> Result<EncryptedMessage> {
|
||||||
|
let policy = StandardPolicy::new();
|
||||||
|
let valid_cert = public_key
|
||||||
|
.with_policy(&policy, None)
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
let encryption_keys = get_encryption_keys(&valid_cert).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
let signing_key = signing_key
|
||||||
|
.primary_key()
|
||||||
|
.parts_into_secret()
|
||||||
|
.map_err(Error::Sequoia)?
|
||||||
|
.key()
|
||||||
|
.clone()
|
||||||
|
.into_keypair()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
|
||||||
|
// Process is as follows:
|
||||||
|
// * Any OpenPGP message
|
||||||
|
// * An encrypted message
|
||||||
|
// * A signed message
|
||||||
|
// * A literal message
|
||||||
|
// * The shard itself
|
||||||
|
//
|
||||||
|
// When decrypting, OpenPGP will see:
|
||||||
|
// * A message, and parse it
|
||||||
|
// * An encrypted message, and decrypt it
|
||||||
|
// * A signed message, and verify it
|
||||||
|
// * A literal message, and extract it
|
||||||
|
// * The shard itself
|
||||||
|
let mut message_output = vec![];
|
||||||
|
let message = Message::new(&mut message_output);
|
||||||
|
let encrypted_message = Encryptor2::for_recipients(
|
||||||
|
message,
|
||||||
|
encryption_keys
|
||||||
|
.iter()
|
||||||
|
.map(|k| Recipient::new(KeyID::wildcard(), k.key())),
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
let signed_message = Signer::new(encrypted_message, signing_key)
|
||||||
|
.build()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
let mut message = LiteralWriter::new(signed_message)
|
||||||
|
.build()
|
||||||
|
.map_err(Error::Sequoia)?;
|
||||||
|
message.write_all(shard).map_err(Error::SequoiaIo)?;
|
||||||
|
message.finalize().map_err(Error::Sequoia)?;
|
||||||
|
|
||||||
|
let message = EncryptedMessage::from_reader(message_output.as_slice())
|
||||||
|
.map_err(Error::Sequoia)?
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.expect("serialized message should be parseable");
|
||||||
|
|
||||||
|
Ok(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_shard_file(
|
||||||
|
&self,
|
||||||
|
shard_file: impl Read + Send + Sync,
|
||||||
|
) -> Result<Vec<Self::EncryptedData>, Self::Error> {
|
||||||
|
EncryptedMessage::from_reader(shard_file).map_err(Error::Sequoia)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_shard_file(
|
||||||
|
&self,
|
||||||
|
encrypted_data: &[Self::EncryptedData],
|
||||||
|
shard_file: impl Write + Send + Sync,
|
||||||
|
) -> Result<(), Self::Error> {
|
||||||
|
let mut writer = Writer::new(shard_file, Kind::Message).map_err(Error::SequoiaIo)?;
|
||||||
|
for message in encrypted_data {
|
||||||
|
message.serialize(&mut writer).map_err(Error::Sequoia)?;
|
||||||
|
}
|
||||||
|
writer.finalize().map_err(Error::SequoiaIo)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_all_shards(
|
||||||
|
&self,
|
||||||
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
|
encrypted_data: &[Self::EncryptedData],
|
||||||
|
) -> std::result::Result<(Vec<Share>, u8), Self::Error> {
|
||||||
|
// Be as liberal as possible when decrypting.
|
||||||
|
// We don't want to invalidate someone's keys just because the old sig expired.
|
||||||
|
let policy = NullPolicy::new();
|
||||||
|
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
|
||||||
|
let mut manager = SmartcardManager::new()?;
|
||||||
|
|
||||||
|
let mut encrypted_messages = encrypted_data.iter();
|
||||||
|
|
||||||
|
let metadata = encrypted_messages.next().expect("metdata");
|
||||||
|
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
|
||||||
|
|
||||||
|
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
|
||||||
|
|
||||||
|
keyring.set_root_cert(root_cert.clone());
|
||||||
|
manager.set_root_cert(root_cert.clone());
|
||||||
|
|
||||||
|
// Generate a controlled binding from certificates to encrypted messages. This is stable
|
||||||
|
// because we control the order packets are encrypted and certificates are stored.
|
||||||
|
|
||||||
|
// TODO: remove alloc, convert EncryptedMessage to &EncryptedMessage
|
||||||
|
let mut messages: HashMap<KeyID, EncryptedMessage> = certs
|
||||||
|
.iter()
|
||||||
|
.map(Cert::keyid)
|
||||||
|
.zip(encrypted_messages.cloned())
|
||||||
|
.collect();
|
||||||
|
let mut decrypted_messages =
|
||||||
|
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
|
||||||
|
|
||||||
|
// clean decrypted messages from encrypted messages
|
||||||
|
messages.retain(|k, _v| !decrypted_messages.contains_key(k));
|
||||||
|
|
||||||
|
let left_from_threshold = threshold as usize - decrypted_messages.len();
|
||||||
|
if left_from_threshold > 0 {
|
||||||
|
#[allow(clippy::cast_possible_truncation)]
|
||||||
|
let new_messages = decrypt_with_manager(
|
||||||
|
left_from_threshold as u8,
|
||||||
|
&mut messages,
|
||||||
|
&certs,
|
||||||
|
&policy,
|
||||||
|
&mut manager,
|
||||||
|
)?;
|
||||||
|
decrypted_messages.extend(new_messages);
|
||||||
|
}
|
||||||
|
|
||||||
|
let shares = decrypted_messages
|
||||||
|
.values()
|
||||||
|
.map(|message| Share::try_from(message.as_slice()))
|
||||||
|
.collect::<Result<Vec<_>, &str>>()
|
||||||
|
.map_err(|e| SharksError::Share(e.to_string()))?;
|
||||||
|
Ok((shares, threshold))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decrypt_one_shard(
|
||||||
|
&self,
|
||||||
|
private_keys: Option<Self::PrivateKeyData>,
|
||||||
|
encrypted_data: &[Self::EncryptedData],
|
||||||
|
) -> std::result::Result<(Share, u8), Self::Error> {
|
||||||
|
let policy = NullPolicy::new();
|
||||||
|
let mut keyring = Keyring::new(private_keys.unwrap_or_default())?;
|
||||||
|
let mut manager = SmartcardManager::new()?;
|
||||||
|
|
||||||
|
let mut encrypted_messages = encrypted_data.iter();
|
||||||
|
|
||||||
|
let metadata = encrypted_messages.next().expect("metadata");
|
||||||
|
let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?;
|
||||||
|
|
||||||
|
let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?;
|
||||||
|
|
||||||
|
keyring.set_root_cert(root_cert.clone());
|
||||||
|
manager.set_root_cert(root_cert.clone());
|
||||||
|
let mut messages: HashMap<KeyID, EncryptedMessage> = certs
|
||||||
|
.iter()
|
||||||
|
.map(Cert::keyid)
|
||||||
|
.zip(encrypted_messages.cloned())
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let decrypted_messages =
|
||||||
|
decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?;
|
||||||
|
|
||||||
|
if let Some(message) = decrypted_messages.into_values().next() {
|
||||||
|
let share = Share::try_from(message.as_slice())
|
||||||
|
.map_err(|e| SharksError::Share(e.to_string()))?;
|
||||||
|
return Ok((share, threshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
let decrypted_messages =
|
||||||
|
decrypt_with_manager(1, &mut messages, &certs, &policy, &mut manager)?;
|
||||||
|
|
||||||
|
if let Some(message) = decrypted_messages.into_values().next() {
|
||||||
|
let share = Share::try_from(message.as_slice())
|
||||||
|
.map_err(|e| SharksError::Share(e.to_string()))?;
|
||||||
|
return Ok((share, threshold));
|
||||||
|
}
|
||||||
|
|
||||||
|
panic!("unable to decrypt shard");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyDiscovery<OpenPGP> for &Path {
|
||||||
|
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
|
||||||
|
OpenPGP::discover_certs(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl KeyDiscovery<OpenPGP> for &[Cert] {
|
||||||
|
fn discover_public_keys(&self) -> Result<Vec<<OpenPGP as Format>::PublicKey>> {
|
||||||
|
Ok(self.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn discover_private_keys(&self) -> Result<<OpenPGP as Format>::PrivateKeyData> {
|
||||||
|
Ok(self.to_vec())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read
|
||||||
|
/// from a file, or from files one level deep in a directory.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// The function may return an error if it is unable to read the directory or if Sequoia is unable
|
||||||
|
/// to load certificates from the file.
|
||||||
|
#[deprecated]
|
||||||
|
pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
||||||
|
let path = path.as_ref();
|
||||||
|
|
||||||
|
if path.is_file() {
|
||||||
|
let mut vec = vec![];
|
||||||
|
for cert in CertParser::from_file(path).map_err(Error::Sequoia)? {
|
||||||
|
vec.push(cert.map_err(Error::Sequoia)?);
|
||||||
|
}
|
||||||
|
Ok(vec)
|
||||||
|
} else {
|
||||||
|
let mut vec = vec![];
|
||||||
|
for entry in path
|
||||||
|
.read_dir()
|
||||||
|
.map_err(Error::Io)?
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter(|p| p.path().is_file())
|
||||||
|
{
|
||||||
|
vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?);
|
||||||
|
}
|
||||||
|
Ok(vec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Parse messages from a type implementing [`Read`] and store them as [`EncryptedMessage`].
|
/// Parse messages from a type implementing [`Read`] and store them as [`EncryptedMessage`].
|
||||||
///
|
///
|
||||||
|
@ -245,6 +624,7 @@ pub fn discover_certs(path: impl AsRef<Path>) -> Result<Vec<Cert>> {
|
||||||
/// # Panics
|
/// # Panics
|
||||||
/// When given packets that are not a list of PKESK packets and SEIP packets, the function panics.
|
/// When given packets that are not a list of PKESK packets and SEIP packets, the function panics.
|
||||||
/// The `split` utility should never give packets that are not in this format.
|
/// The `split` utility should never give packets that are not in this format.
|
||||||
|
#[deprecated]
|
||||||
pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage>> {
|
pub fn parse_messages(reader: impl Read + Send + Sync) -> Result<VecDeque<EncryptedMessage>> {
|
||||||
let mut pkesks = Vec::new();
|
let mut pkesks = Vec::new();
|
||||||
let mut encrypted_messages = VecDeque::new();
|
let mut encrypted_messages = VecDeque::new();
|
||||||
|
@ -416,6 +796,7 @@ fn decrypt_metadata(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[deprecated]
|
||||||
fn decrypt_one(
|
fn decrypt_one(
|
||||||
messages: Vec<EncryptedMessage>,
|
messages: Vec<EncryptedMessage>,
|
||||||
certs: &[Cert],
|
certs: &[Cert],
|
||||||
|
@ -465,13 +846,14 @@ fn decrypt_one(
|
||||||
/// The function may panic if a share is decrypted but has a length larger than 256 bits. This is
|
/// The function may panic if a share is decrypted but has a length larger than 256 bits. This is
|
||||||
/// atypical usage and should not be encountered in normal usage, unless something that is not a
|
/// atypical usage and should not be encountered in normal usage, unless something that is not a
|
||||||
/// Keyfork seed has been fed into [`split`].
|
/// Keyfork seed has been fed into [`split`].
|
||||||
|
#[deprecated]
|
||||||
|
#[allow(deprecated)]
|
||||||
pub fn decrypt(
|
pub fn decrypt(
|
||||||
certs: &[Cert],
|
certs: &[Cert],
|
||||||
metadata: &EncryptedMessage,
|
metadata: &EncryptedMessage,
|
||||||
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;
|
||||||
|
@ -496,8 +878,11 @@ pub fn decrypt(
|
||||||
let validator = MnemonicSetValidator {
|
let validator = MnemonicSetValidator {
|
||||||
word_lengths: [9, 24],
|
word_lengths: [9, 24],
|
||||||
};
|
};
|
||||||
let [nonce_mnemonic, pubkey_mnemonic] =
|
let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist::<English, _>(
|
||||||
pm.prompt_validated_wordlist(QRCODE_COULDNT_READ, &wordlist, 3, validator.to_fn())?;
|
QRCODE_COULDNT_READ,
|
||||||
|
3,
|
||||||
|
validator.to_fn(),
|
||||||
|
)?;
|
||||||
|
|
||||||
let nonce = nonce_mnemonic
|
let nonce = nonce_mnemonic
|
||||||
.as_bytes()
|
.as_bytes()
|
||||||
|
@ -514,8 +899,7 @@ pub fn decrypt(
|
||||||
let nonce = Nonce::<U12>::from_slice(&nonce);
|
let nonce = Nonce::<U12>::from_slice(&nonce);
|
||||||
|
|
||||||
let our_key = EphemeralSecret::random();
|
let our_key = EphemeralSecret::random();
|
||||||
let our_pubkey_mnemonic =
|
let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?;
|
||||||
Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?;
|
|
||||||
|
|
||||||
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
|
let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes();
|
||||||
|
|
||||||
|
@ -560,7 +944,7 @@ pub fn decrypt(
|
||||||
}
|
}
|
||||||
|
|
||||||
// safety: size of out_bytes is constant and always % 4 == 0
|
// safety: size of out_bytes is constant and always % 4 == 0
|
||||||
let payload_mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) };
|
let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) };
|
||||||
|
|
||||||
#[cfg(feature = "qrcode")]
|
#[cfg(feature = "qrcode")]
|
||||||
{
|
{
|
||||||
|
@ -592,6 +976,7 @@ pub fn decrypt(
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The function may return an error if an error occurs while decrypting shards, parsing shards, or
|
/// The function may return an error if an error occurs while decrypting shards, parsing shards, or
|
||||||
/// combining the shards into a secret.
|
/// combining the shards into a secret.
|
||||||
|
#[deprecated]
|
||||||
pub fn combine(
|
pub fn combine(
|
||||||
certs: Vec<Cert>,
|
certs: Vec<Cert>,
|
||||||
metadata: &EncryptedMessage,
|
metadata: &EncryptedMessage,
|
||||||
|
@ -679,6 +1064,7 @@ pub fn combine(
|
||||||
///
|
///
|
||||||
/// The function may panic if the metadata can't properly store the certificates used to generate
|
/// The function may panic if the metadata can't properly store the certificates used to generate
|
||||||
/// the encrypted shares.
|
/// the encrypted shares.
|
||||||
|
#[deprecated]
|
||||||
pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write) -> Result<()> {
|
pub fn split(threshold: u8, certs: Vec<Cert>, secret: &[u8], output: impl Write) -> Result<()> {
|
||||||
let seed = VariableLengthSeed::new(secret);
|
let seed = VariableLengthSeed::new(secret);
|
||||||
// build cert to sign encrypted shares
|
// build cert to sign encrypted shares
|
||||||
|
|
|
@ -111,12 +111,10 @@ impl DecryptionHelper for &mut Keyring {
|
||||||
pkesk.recipient().is_wildcard()
|
pkesk.recipient().is_wildcard()
|
||||||
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient())
|
|| cert.keys().any(|k| &k.keyid() == pkesk.recipient())
|
||||||
}) {
|
}) {
|
||||||
#[allow(deprecated, clippy::map_flatten)]
|
|
||||||
let name = cert
|
let name = cert
|
||||||
.userids()
|
.userids()
|
||||||
.next()
|
.next()
|
||||||
.map(|userid| userid.userid().name().transpose())
|
.and_then(|userid| userid.userid().name2().transpose())
|
||||||
.flatten()
|
|
||||||
.transpose()
|
.transpose()
|
||||||
.ok()
|
.ok()
|
||||||
.flatten();
|
.flatten();
|
||||||
|
|
|
@ -43,3 +43,4 @@ openpgp-card-sequoia = { version = "0.2.0", default-features = false }
|
||||||
openpgp-card = "0.4.1"
|
openpgp-card = "0.4.1"
|
||||||
clap_complete = { version = "4.4.6", optional = true }
|
clap_complete = { version = "4.4.6", optional = true }
|
||||||
sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] }
|
sequoia-openpgp = { version = "1.17.0", default-features = false, features = ["compression"] }
|
||||||
|
keyfork-bin = { version = "0.1.0", path = "../util/keyfork-bin" }
|
||||||
|
|
|
@ -109,7 +109,7 @@ impl MnemonicSeedSource {
|
||||||
MnemonicSeedSource::Tarot => todo!(),
|
MnemonicSeedSource::Tarot => todo!(),
|
||||||
MnemonicSeedSource::Dice => todo!(),
|
MnemonicSeedSource::Dice => todo!(),
|
||||||
};
|
};
|
||||||
let mnemonic = keyfork_mnemonic_util::Mnemonic::from_entropy(&seed, Default::default())?;
|
let mnemonic = keyfork_mnemonic_util::Mnemonic::from_bytes(&seed)?;
|
||||||
Ok(mnemonic.to_string())
|
Ok(mnemonic.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,11 +2,8 @@ use super::Keyfork;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use keyfork_mnemonic_util::Mnemonic;
|
use keyfork_mnemonic_util::{English, Mnemonic};
|
||||||
use keyfork_shard::{
|
use keyfork_shard::{remote_decrypt, Format};
|
||||||
openpgp::{combine, discover_certs, parse_messages},
|
|
||||||
remote_decrypt,
|
|
||||||
};
|
|
||||||
|
|
||||||
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>;
|
||||||
|
|
||||||
|
@ -37,15 +34,10 @@ impl RecoverSubcommands {
|
||||||
} => {
|
} => {
|
||||||
let content = std::fs::read_to_string(shard_file)?;
|
let content = std::fs::read_to_string(shard_file)?;
|
||||||
if content.contains("BEGIN PGP MESSAGE") {
|
if content.contains("BEGIN PGP MESSAGE") {
|
||||||
let certs = key_discovery
|
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||||
.as_ref()
|
// TODO: remove .clone() by making handle() consume self
|
||||||
.map(discover_certs)
|
let seed = openpgp
|
||||||
.transpose()?
|
.decrypt_all_shards_to_secret(key_discovery.as_deref(), content.as_bytes())?;
|
||||||
.unwrap_or(vec![]);
|
|
||||||
let mut messages = parse_messages(content.as_bytes())?;
|
|
||||||
let metadata = messages.pop_front().expect("any pgp encrypted message");
|
|
||||||
let mut seed = vec![];
|
|
||||||
combine(certs, &metadata, messages.into(), &mut seed)?;
|
|
||||||
Ok(seed)
|
Ok(seed)
|
||||||
} else {
|
} else {
|
||||||
panic!("unknown format of shard file");
|
panic!("unknown format of shard file");
|
||||||
|
@ -69,9 +61,8 @@ impl RecoverSubcommands {
|
||||||
let validator = MnemonicChoiceValidator {
|
let validator = MnemonicChoiceValidator {
|
||||||
word_lengths: [WordLength::Count(12), WordLength::Count(24)],
|
word_lengths: [WordLength::Count(12), WordLength::Count(24)],
|
||||||
};
|
};
|
||||||
let mnemonic = term.prompt_validated_wordlist(
|
let mnemonic = term.prompt_validated_wordlist::<English, _>(
|
||||||
"Mnemonic: ",
|
"Mnemonic: ",
|
||||||
&Default::default(),
|
|
||||||
3,
|
3,
|
||||||
validator.to_fn(),
|
validator.to_fn(),
|
||||||
)?;
|
)?;
|
||||||
|
@ -90,7 +81,7 @@ pub struct Recover {
|
||||||
impl Recover {
|
impl Recover {
|
||||||
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
pub fn handle(&self, _k: &Keyfork) -> Result<()> {
|
||||||
let seed = self.command.handle()?;
|
let seed = self.command.handle()?;
|
||||||
let mnemonic = Mnemonic::from_entropy(&seed, Default::default())?;
|
let mnemonic = Mnemonic::from_bytes(&seed)?;
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
.enable_all()
|
.enable_all()
|
||||||
.build()
|
.build()
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
use super::Keyfork;
|
use super::Keyfork;
|
||||||
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum};
|
||||||
|
use keyfork_shard::Format as _;
|
||||||
use std::{
|
use std::{
|
||||||
io::{stdin, stdout, Read, Write},
|
io::{stdin, stdout, Read, Write},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
|
@ -31,27 +32,23 @@ trait ShardExec {
|
||||||
&self,
|
&self,
|
||||||
threshold: u8,
|
threshold: u8,
|
||||||
max: u8,
|
max: u8,
|
||||||
key_discovery: impl AsRef<Path>,
|
key_discovery: &Path,
|
||||||
secret: &[u8],
|
secret: &[u8],
|
||||||
|
output: &mut (impl Write + Send + Sync),
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
fn combine(
|
||||||
|
&self,
|
||||||
|
key_discovery: Option<&Path>,
|
||||||
|
input: impl Read + Send + Sync,
|
||||||
output: &mut impl Write,
|
output: &mut impl Write,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>;
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
fn combine<T>(
|
fn decrypt(
|
||||||
&self,
|
&self,
|
||||||
key_discovery: Option<T>,
|
key_discovery: Option<&Path>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
output: &mut impl Write,
|
) -> Result<(), Box<dyn std::error::Error>>;
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
|
||||||
where
|
|
||||||
T: AsRef<Path>;
|
|
||||||
|
|
||||||
fn decrypt<T>(
|
|
||||||
&self,
|
|
||||||
key_discovery: Option<T>,
|
|
||||||
input: impl Read + Send + Sync,
|
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
|
||||||
where
|
|
||||||
T: AsRef<Path>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
|
@ -62,77 +59,36 @@ impl ShardExec for OpenPGP {
|
||||||
&self,
|
&self,
|
||||||
threshold: u8,
|
threshold: u8,
|
||||||
max: u8,
|
max: u8,
|
||||||
key_discovery: impl AsRef<Path>,
|
key_discovery: &Path,
|
||||||
secret: &[u8],
|
secret: &[u8],
|
||||||
output: &mut impl Write,
|
output: &mut (impl Write + Send + Sync),
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
// Get certs and input
|
let opgp = keyfork_shard::openpgp::OpenPGP;
|
||||||
let certs = keyfork_shard::openpgp::discover_certs(key_discovery.as_ref())?;
|
opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output)
|
||||||
assert_eq!(
|
|
||||||
certs.len(),
|
|
||||||
max.into(),
|
|
||||||
"cert count {} != max {max}",
|
|
||||||
certs.len()
|
|
||||||
);
|
|
||||||
keyfork_shard::openpgp::split(threshold, certs, secret, output).map_err(Into::into)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn combine<T>(
|
fn combine(
|
||||||
&self,
|
&self,
|
||||||
key_discovery: Option<T>,
|
key_discovery: Option<&Path>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
output: &mut impl Write,
|
output: &mut impl Write,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
where
|
|
||||||
T: AsRef<Path>,
|
|
||||||
{
|
{
|
||||||
let certs = key_discovery
|
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||||
.map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref()))
|
let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?;
|
||||||
.transpose()?
|
write!(output, "{}", smex::encode(bytes))?;
|
||||||
.unwrap_or(vec![]);
|
|
||||||
|
|
||||||
let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?;
|
|
||||||
let encrypted_metadata = encrypted_messages
|
|
||||||
.pop_front()
|
|
||||||
.expect("any pgp encrypted message");
|
|
||||||
|
|
||||||
let mut bytes = vec![];
|
|
||||||
|
|
||||||
keyfork_shard::openpgp::combine(
|
|
||||||
certs,
|
|
||||||
&encrypted_metadata,
|
|
||||||
encrypted_messages.into(),
|
|
||||||
&mut bytes,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
write!(output, "{}", smex::encode(&bytes))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decrypt<T>(
|
fn decrypt(
|
||||||
&self,
|
&self,
|
||||||
key_discovery: Option<T>,
|
key_discovery: Option<&Path>,
|
||||||
input: impl Read + Send + Sync,
|
input: impl Read + Send + Sync,
|
||||||
) -> Result<(), Box<dyn std::error::Error>>
|
) -> Result<(), Box<dyn std::error::Error>>
|
||||||
where
|
|
||||||
T: AsRef<Path>,
|
|
||||||
{
|
{
|
||||||
let certs = key_discovery
|
let openpgp = keyfork_shard::openpgp::OpenPGP;
|
||||||
.map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref()))
|
openpgp.decrypt_one_shard_for_transport(key_discovery, input)?;
|
||||||
.transpose()?
|
|
||||||
.unwrap_or(vec![]);
|
|
||||||
|
|
||||||
let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?;
|
|
||||||
let encrypted_metadata = encrypted_messages
|
|
||||||
.pop_front()
|
|
||||||
.expect("any pgp encrypted message");
|
|
||||||
|
|
||||||
keyfork_shard::openpgp::decrypt(
|
|
||||||
&certs,
|
|
||||||
&encrypted_metadata,
|
|
||||||
encrypted_messages.make_contiguous(),
|
|
||||||
)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -225,7 +181,7 @@ impl ShardSubcommands {
|
||||||
|
|
||||||
match format {
|
match format {
|
||||||
Some(Format::OpenPGP(o)) => {
|
Some(Format::OpenPGP(o)) => {
|
||||||
o.decrypt(key_discovery.as_ref(), shard_content.as_bytes())
|
o.decrypt(key_discovery.as_deref(), shard_content.as_bytes())
|
||||||
}
|
}
|
||||||
Some(Format::P256(_p)) => todo!(),
|
Some(Format::P256(_p)) => todo!(),
|
||||||
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
None => panic!("{COULD_NOT_DETERMINE_FORMAT}"),
|
||||||
|
@ -242,7 +198,7 @@ impl ShardSubcommands {
|
||||||
|
|
||||||
match format {
|
match format {
|
||||||
Some(Format::OpenPGP(o)) => o.combine(
|
Some(Format::OpenPGP(o)) => o.combine(
|
||||||
key_discovery.as_ref(),
|
key_discovery.as_deref(),
|
||||||
shard_content.as_bytes(),
|
shard_content.as_bytes(),
|
||||||
&mut stdout,
|
&mut stdout,
|
||||||
),
|
),
|
||||||
|
|
|
@ -15,6 +15,8 @@ use keyfork_prompt::{
|
||||||
Message, PromptHandler, Terminal,
|
Message, PromptHandler, Terminal,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use keyfork_shard::{Format, openpgp::OpenPGP};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
#[error("Invalid PIN length: {0}")]
|
#[error("Invalid PIN length: {0}")]
|
||||||
pub struct PinLength(usize);
|
pub struct PinLength(usize);
|
||||||
|
@ -163,11 +165,13 @@ fn generate_shard_secret(
|
||||||
certs.push(cert);
|
certs.push(cert);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let opgp = OpenPGP;
|
||||||
|
|
||||||
if let Some(output_file) = output_file {
|
if let Some(output_file) = output_file {
|
||||||
let output = File::create(output_file)?;
|
let output = File::create(output_file)?;
|
||||||
keyfork_shard::openpgp::split(threshold, certs, &seed, output)?;
|
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?;
|
||||||
} else {
|
} else {
|
||||||
keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?;
|
opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], std::io::stdout())?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,21 +6,16 @@ use std::process::ExitCode;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
|
use keyfork_bin::{Bin, ClosureBin};
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
|
let bin = ClosureBin::new(|| {
|
||||||
let opts = cli::Keyfork::parse();
|
let opts = cli::Keyfork::parse();
|
||||||
|
opts.command.handle(&opts)
|
||||||
|
});
|
||||||
|
|
||||||
if let Err(e) = opts.command.handle(&opts) {
|
bin.main()
|
||||||
eprintln!("Unable to run command: {e}");
|
|
||||||
let mut source = e.source();
|
|
||||||
while let Some(new_error) = source.take() {
|
|
||||||
eprintln!("Source: {new_error}");
|
|
||||||
source = new_error.source();
|
|
||||||
}
|
|
||||||
return ExitCode::FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
ExitCode::SUCCESS
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
|
@ -57,10 +57,21 @@ fn ensure_offline() {
|
||||||
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
|
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1");
|
||||||
/// keyfork_entropy::ensure_safe();
|
/// keyfork_entropy::ensure_safe();
|
||||||
/// ```
|
/// ```
|
||||||
|
///
|
||||||
|
/// When running on a system that's online, or running an outdated kernel:
|
||||||
|
///
|
||||||
|
/// ```rust,should_panic
|
||||||
|
/// # // NOTE: sometimes, the environment variable is set, for testing purposes. I'm not sure how
|
||||||
|
/// # // to un-set it. Set it to a sentinel value.
|
||||||
|
/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "test-must-fail");
|
||||||
|
/// # std::env::set_var("INSECURE_HARDWARE_ALLOWED", "test-must-fail");
|
||||||
|
/// keyfork_entropy::ensure_safe();
|
||||||
|
/// ```
|
||||||
pub fn ensure_safe() {
|
pub fn ensure_safe() {
|
||||||
if !std::env::vars()
|
if !std::env::vars().any(|(name, value)| {
|
||||||
.any(|(name, _)| name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
|
(name == "SHOOT_SELF_IN_FOOT" || name == "INSECURE_HARDWARE_ALLOWED")
|
||||||
{
|
&& value != "test-must-fail"
|
||||||
|
}) {
|
||||||
ensure_safe_kernel_version();
|
ensure_safe_kernel_version();
|
||||||
ensure_offline();
|
ensure_offline();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,6 +66,11 @@ pub(crate) fn hash(data: &[u8]) -> Vec<u8> {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
|
/// An error may be returned if the given `data` is more than [`u32::MAX`] bytes. This is a
|
||||||
/// constraint on a protocol level.
|
/// constraint on a protocol level.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let data = keyfork_frame::try_encode(b"hello world!".as_slice()).unwrap();
|
||||||
|
/// ```
|
||||||
pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
|
pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
|
||||||
let mut output = vec![];
|
let mut output = vec![];
|
||||||
try_encode_to(data, &mut output)?;
|
try_encode_to(data, &mut output)?;
|
||||||
|
@ -77,6 +82,12 @@ pub fn try_encode(data: &[u8]) -> Result<Vec<u8>, EncodeError> {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer
|
/// An error may be returned if the givenu `data` is more than [`u32::MAX`] bytes, or if the writer
|
||||||
/// is unable to write data.
|
/// is unable to write data.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let mut output = vec![];
|
||||||
|
/// keyfork_frame::try_encode_to(b"hello world!".as_slice(), &mut output).unwrap();
|
||||||
|
/// ```
|
||||||
pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
|
pub fn try_encode_to(data: &[u8], writable: &mut impl Write) -> Result<(), EncodeError> {
|
||||||
let hash = hash(data);
|
let hash = hash(data);
|
||||||
let len = hash.len() + data.len();
|
let len = hash.len() + data.len();
|
||||||
|
@ -107,18 +118,40 @@ pub(crate) fn verify_checksum(data: &[u8]) -> Result<&[u8], DecodeError> {
|
||||||
/// * The given `data` does not contain enough data to parse a length,
|
/// * The given `data` does not contain enough data to parse a length,
|
||||||
/// * The given `data` does not contain the given length's worth of data,
|
/// * The given `data` does not contain the given length's worth of data,
|
||||||
/// * The given `data` has a checksum that does not match what we build locally.
|
/// * The given `data` has a checksum that does not match what we build locally.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let input = b"hello world!";
|
||||||
|
/// let encoded = keyfork_frame::try_encode(input.as_slice()).unwrap();
|
||||||
|
/// let decoded = keyfork_frame::try_decode(&encoded).unwrap();
|
||||||
|
/// assert_eq!(input.as_slice(), decoded.as_slice());
|
||||||
|
/// ```
|
||||||
pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
|
pub fn try_decode(data: &[u8]) -> Result<Vec<u8>, DecodeError> {
|
||||||
try_decode_from(&mut &data[..])
|
try_decode_from(&mut &data[..])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read and decode a framed message into a `Vec<u8>`.
|
/// Read and decode a framed message into a `Vec<u8>`.
|
||||||
///
|
///
|
||||||
|
/// Note that unlike [`try_encode_to`], this method does not allow writing to an object
|
||||||
|
/// implementing Write. This is because the data must be stored entirely in memory to allow
|
||||||
|
/// verifying the data. The data is then returned using the same in-memory representation as is
|
||||||
|
/// used in memory, and a caller may then choose to use `writable.write_all()`.
|
||||||
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// An error may be returned if:
|
/// An error may be returned if:
|
||||||
/// * The given `data` does not contain enough data to parse a length,
|
/// * The given `data` does not contain enough data to parse a length,
|
||||||
/// * The given `data` does not contain the given length's worth of data,
|
/// * The given `data` does not contain the given length's worth of data,
|
||||||
/// * The given `data` has a checksum that does not match what we build locally.
|
/// * The given `data` has a checksum that does not match what we build locally.
|
||||||
/// * The source for the data returned an error.
|
/// * The source for the data returned an error.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let input = b"hello world!";
|
||||||
|
/// let mut encoded = vec![];
|
||||||
|
/// keyfork_frame::try_encode_to(input.as_slice(), &mut encoded).unwrap();
|
||||||
|
/// let decoded = keyfork_frame::try_decode_from(&mut &encoded[..]).unwrap();
|
||||||
|
/// assert_eq!(input.as_slice(), decoded.as_slice());
|
||||||
|
/// ```
|
||||||
pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
|
pub fn try_decode_from(readable: &mut impl Read) -> Result<Vec<u8>, DecodeError> {
|
||||||
let mut bytes = 0u32.to_be_bytes();
|
let mut bytes = 0u32.to_be_bytes();
|
||||||
readable.read_exact(&mut bytes)?;
|
readable.read_exact(&mut bytes)?;
|
||||||
|
|
|
@ -8,7 +8,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
input.read_line(&mut line)?;
|
input.read_line(&mut line)?;
|
||||||
let decoded = smex::decode(line.trim())?;
|
let decoded = smex::decode(line.trim())?;
|
||||||
|
|
||||||
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&decoded, Default::default()) };
|
let mnemonic = unsafe { Mnemonic::from_raw_bytes(&decoded) };
|
||||||
|
|
||||||
println!("{mnemonic}");
|
println!("{mnemonic}");
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,60 @@
|
||||||
//! Zero-dependency Mnemonic encoding and decoding.
|
//! Zero-dependency mnemonic encoding and decoding of data.
|
||||||
|
//!
|
||||||
|
//! Mnemonics can be used to safely encode data of 32, 48, and 64 bytes as a phrase:
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
//! let data = b"Hello, world! I am a mnemonic :)";
|
||||||
|
//! assert_eq!(data.len(), 32);
|
||||||
|
//! let mnemonic = Mnemonic::from_bytes(data).unwrap();
|
||||||
|
//! println!("Our mnemonic is: {mnemonic}");
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! A mnemonic can also be parsed from a string:
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
//! use std::str::FromStr;
|
||||||
|
//!
|
||||||
|
//! let data = b"Hello, world! I am a mnemonic :)";
|
||||||
|
//! let words = "embody clock brand tattoo search desert saddle eternal
|
||||||
|
//! goddess animal banner dolphin bitter mother loyal asset
|
||||||
|
//! hover clock forward system normal mosquito trim credit";
|
||||||
|
//! let mnemonic = Mnemonic::from_str(words).unwrap();
|
||||||
|
//! assert_eq!(&data[..], mnemonic.as_bytes());
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Mnemonics can also be used to store data of other lengths, but such functionality is not
|
||||||
|
//! verified to be safe:
|
||||||
|
//!
|
||||||
|
//! ```rust
|
||||||
|
//! use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
|
||||||
|
//! let mnemonic_text = mnemonic.to_string();
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! If given an invalid length, undefined behavior may follow, or code may panic.
|
||||||
|
//!
|
||||||
|
//! ```rust,should_panic
|
||||||
|
//! use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
//! use std::str::FromStr;
|
||||||
|
//!
|
||||||
|
//! // NOTE: Data is of invalid length, 31
|
||||||
|
//! let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
//! let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
|
||||||
|
//! let mnemonic_text = mnemonic.to_string();
|
||||||
|
//! // NOTE: panic happens here
|
||||||
|
//! let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
|
||||||
|
//! ```
|
||||||
|
|
||||||
use std::{error::Error, fmt::Display, str::FromStr, sync::Arc};
|
use std::{
|
||||||
|
error::Error,
|
||||||
|
fmt::Display,
|
||||||
|
str::FromStr,
|
||||||
|
sync::OnceLock,
|
||||||
|
marker::PhantomData,
|
||||||
|
};
|
||||||
|
|
||||||
use hmac::Hmac;
|
use hmac::Hmac;
|
||||||
use pbkdf2::pbkdf2;
|
use pbkdf2::pbkdf2;
|
||||||
|
@ -39,93 +93,65 @@ impl Display for MnemonicGenerationError {
|
||||||
|
|
||||||
impl Error for MnemonicGenerationError {}
|
impl Error for MnemonicGenerationError {}
|
||||||
|
|
||||||
/// A BIP-0039 compatible list of words.
|
/// A trait representing a BIP-0039 wordlist, of 2048 words, with each word having a unique first
|
||||||
#[derive(Debug, Clone)]
|
/// three letters.
|
||||||
pub struct Wordlist(Vec<String>);
|
pub trait Wordlist: std::fmt::Debug {
|
||||||
|
/// Get a reference to a [`std::sync::OnceLock`] Self.
|
||||||
|
fn get_singleton<'a>() -> &'a Self;
|
||||||
|
|
||||||
impl Default for Wordlist {
|
/// Return a representation of the words in the wordlist as an array of [`str`].
|
||||||
/// Returns the English wordlist in the Bitcoin BIP-0039 specification.
|
fn to_str_array(&self) -> [&str; 2048];
|
||||||
fn default() -> Self {
|
}
|
||||||
// TODO: English is the only supported language.
|
|
||||||
|
/// 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");
|
||||||
Wordlist(
|
let mut words = wordlist_file
|
||||||
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());
|
||||||
.collect(),
|
English {
|
||||||
)
|
words: std::array::from_fn(|_| words.next().expect("wordlist has 2048 words")),
|
||||||
}
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Wordlist {
|
fn to_str_array(&self) -> [&str; 2048] {
|
||||||
/// Return an Arced version of the Wordlist
|
std::array::from_fn(|i| self.words[i].as_str())
|
||||||
#[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)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct Mnemonic {
|
pub struct MnemonicBase<W: Wordlist> {
|
||||||
entropy: Vec<u8>,
|
data: Vec<u8>,
|
||||||
// words: Vec<usize>,
|
marker: PhantomData<W>,
|
||||||
wordlist: Arc<Wordlist>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Mnemonic {
|
/// A default Mnemonic using the English language.
|
||||||
|
pub type Mnemonic = MnemonicBase<English>;
|
||||||
|
|
||||||
|
impl<W> Display for MnemonicBase<W>
|
||||||
|
where
|
||||||
|
W: Wordlist,
|
||||||
|
{
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let bit_count = self.entropy.len() * 8;
|
let wordlist = W::get_singleton();
|
||||||
let mut bits = vec![false; bit_count + bit_count / 32];
|
let words = wordlist.to_str_array();
|
||||||
|
|
||||||
for byte_index in 0..bit_count / 8 {
|
let mut iter = self
|
||||||
for bit_index in 0..8 {
|
.words()
|
||||||
bits[byte_index * 8 + bit_index] =
|
.into_iter()
|
||||||
(self.entropy[byte_index] & (1 << (7 - bit_index))) > 0;
|
.filter_map(|word| words.get(word))
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(&self.entropy);
|
|
||||||
let hash = hasher.finalize().to_vec();
|
|
||||||
for check_bit in 0..bit_count / 32 {
|
|
||||||
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut iter = bits
|
|
||||||
.chunks_exact(11)
|
|
||||||
.peekable()
|
|
||||||
.map(|chunk| {
|
|
||||||
let mut num = 0usize;
|
|
||||||
for i in 0..11 {
|
|
||||||
num += usize::from(chunk[10 - i]) << i;
|
|
||||||
}
|
|
||||||
num
|
|
||||||
})
|
|
||||||
.filter_map(|word| self.wordlist.get_word(word))
|
|
||||||
.peekable();
|
.peekable();
|
||||||
while let Some(word) = iter.next() {
|
while let Some(word) = iter.next() {
|
||||||
f.write_str(word)?;
|
f.write_str(word)?;
|
||||||
|
@ -170,17 +196,20 @@ impl Display for MnemonicFromStrError {
|
||||||
|
|
||||||
impl Error for MnemonicFromStrError {}
|
impl Error for MnemonicFromStrError {}
|
||||||
|
|
||||||
impl FromStr for Mnemonic {
|
impl<W> FromStr for MnemonicBase<W>
|
||||||
|
where
|
||||||
|
W: Wordlist,
|
||||||
|
{
|
||||||
type Err = MnemonicFromStrError;
|
type Err = MnemonicFromStrError;
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let wordlist = W::get_singleton();
|
||||||
|
let wordlist_words = wordlist.to_str_array();
|
||||||
let words: Vec<_> = s.split_whitespace().collect();
|
let words: Vec<_> = s.split_whitespace().collect();
|
||||||
let mut usize_words = vec![];
|
let mut usize_words = vec![];
|
||||||
let wordlist = Wordlist::default().arc();
|
|
||||||
let mut bits = vec![false; words.len() * 11];
|
let mut bits = vec![false; words.len() * 11];
|
||||||
for (index, word) in words.iter().enumerate() {
|
for (index, word) in words.iter().enumerate() {
|
||||||
let word = wordlist
|
let word = wordlist_words
|
||||||
.0
|
|
||||||
.iter()
|
.iter()
|
||||||
.position(|w| w == word)
|
.position(|w| w == word)
|
||||||
.ok_or(MnemonicFromStrError::InvalidWord(index))?;
|
.ok_or(MnemonicFromStrError::InvalidWord(index))?;
|
||||||
|
@ -197,7 +226,7 @@ impl FromStr for Mnemonic {
|
||||||
bits.truncate(bits.len() * 32 / 33);
|
bits.truncate(bits.len() * 32 / 33);
|
||||||
// bits.truncate(bits.len() - bits.len() % 32);
|
// bits.truncate(bits.len() - bits.len() % 32);
|
||||||
|
|
||||||
let entropy: Vec<u8> = bits
|
let data: Vec<u8> = bits
|
||||||
.chunks_exact(8)
|
.chunks_exact(8)
|
||||||
.map(|chunk| {
|
.map(|chunk| {
|
||||||
let mut num = 0u8;
|
let mut num = 0u8;
|
||||||
|
@ -209,7 +238,7 @@ impl FromStr for Mnemonic {
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(&entropy);
|
hasher.update(&data);
|
||||||
let hash = hasher.finalize().to_vec();
|
let hash = hasher.finalize().to_vec();
|
||||||
|
|
||||||
for (i, bit) in checksum_bits.iter().enumerate() {
|
for (i, bit) in checksum_bits.iter().enumerate() {
|
||||||
|
@ -218,23 +247,27 @@ impl FromStr for Mnemonic {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Mnemonic {
|
Ok(MnemonicBase { data, marker: PhantomData })
|
||||||
entropy,
|
|
||||||
// words: usize_words,
|
|
||||||
wordlist,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mnemonic {
|
impl<W> MnemonicBase<W>
|
||||||
/// Generate a [`Mnemonic`] from the provided entropy and [`Wordlist`].
|
where
|
||||||
|
W: Wordlist,
|
||||||
|
{
|
||||||
|
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
|
||||||
|
/// of 128, 192, or 256 bits, as per BIP-0039.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// An error may be returned if the entropy is not within the acceptable lengths.
|
/// An error may be returned if the data is not within the expected lengths.
|
||||||
pub fn from_entropy(
|
///
|
||||||
bytes: &[u8],
|
/// # Examples
|
||||||
wordlist: Arc<Wordlist>,
|
/// ```rust
|
||||||
) -> Result<Mnemonic, MnemonicGenerationError> {
|
/// use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
/// let mnemonic = Mnemonic::from_bytes(data.as_slice()).unwrap();
|
||||||
|
/// ```
|
||||||
|
pub fn from_bytes(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
|
||||||
let bit_count = bytes.len() * 8;
|
let bit_count = bytes.len() * 8;
|
||||||
|
|
||||||
if bit_count % 32 != 0 {
|
if bit_count % 32 != 0 {
|
||||||
|
@ -245,88 +278,150 @@ impl Mnemonic {
|
||||||
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
|
return Err(MnemonicGenerationError::InvalidByteLength(bit_count));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(unsafe { Self::from_raw_entropy(bytes, wordlist) })
|
Ok(unsafe { Self::from_raw_bytes(bytes) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a [`Mnemonic`] from the provided data and [`Wordlist`]. The data is expected to be
|
||||||
|
/// of 128, 192, or 256 bits, as per BIP-0039.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// An error may be returned if the data is not within the expected lengths.
|
||||||
|
#[deprecated = "use Mnemonic::from_bytes"]
|
||||||
|
pub fn from_entropy(bytes: &[u8]) -> Result<MnemonicBase<W>, MnemonicGenerationError> {
|
||||||
|
MnemonicBase::from_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
|
||||||
|
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
|
||||||
|
///
|
||||||
/// # Safety
|
/// # Safety
|
||||||
///
|
///
|
||||||
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
|
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
|
||||||
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
|
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
|
||||||
/// == 0`.
|
/// == 0`. If the assumption is incorrect, code may panic.
|
||||||
pub unsafe fn from_raw_entropy(bytes: &[u8], wordlist: Arc<Wordlist>) -> Mnemonic {
|
///
|
||||||
Mnemonic {
|
/// # Examples
|
||||||
entropy: bytes.to_vec(),
|
/// ```rust
|
||||||
wordlist,
|
/// use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
|
||||||
|
/// let mnemonic_text = mnemonic.to_string();
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// If given an invalid length, undefined behavior may follow, or code may panic.
|
||||||
|
///
|
||||||
|
/// ```rust,should_panic
|
||||||
|
/// use keyfork_mnemonic_util::Mnemonic;
|
||||||
|
/// use std::str::FromStr;
|
||||||
|
///
|
||||||
|
/// // NOTE: Data is of invalid length, 31
|
||||||
|
/// let data = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||||
|
/// let mnemonic = unsafe { Mnemonic::from_raw_bytes(data.as_slice()) };
|
||||||
|
/// let mnemonic_text = mnemonic.to_string();
|
||||||
|
/// // NOTE: panic happens here
|
||||||
|
/// let new_mnemonic = Mnemonic::from_str(&mnemonic_text).unwrap();
|
||||||
|
/// ```
|
||||||
|
pub unsafe fn from_raw_bytes(bytes: &[u8]) -> MnemonicBase<W> {
|
||||||
|
MnemonicBase {
|
||||||
|
data: bytes.to_vec(),
|
||||||
|
marker: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The internal representation of the decoded data.
|
/// Create a Mnemonic using an arbitrary length of given data. The length does not need to
|
||||||
|
/// conform to BIP-0039 standards, but should be a multiple of 32 bits or 4 bytes.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
///
|
||||||
|
/// This function can potentially produce mnemonics that are not BIP-0039 compliant or can't
|
||||||
|
/// properly be encoded as a mnemonic. It is assumed the caller asserts the byte count is `% 4
|
||||||
|
/// == 0`. If the assumption is incorrect, code may panic.
|
||||||
|
#[deprecated = "use Mnemonic::from_raw_bytes"]
|
||||||
|
pub unsafe fn from_raw_entropy(bytes: &[u8]) -> MnemonicBase<W> {
|
||||||
|
MnemonicBase {
|
||||||
|
data: bytes.to_vec(),
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A view to internal representation of the decoded data.
|
||||||
pub fn as_bytes(&self) -> &[u8] {
|
pub fn as_bytes(&self) -> &[u8] {
|
||||||
&self.entropy
|
&self.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The internal representation of the decoded data, as a [`Vec<u8>`].
|
/// A clone of the internal representation of the decoded data.
|
||||||
pub fn to_bytes(&self) -> Vec<u8> {
|
pub fn to_bytes(&self) -> Vec<u8> {
|
||||||
self.entropy.to_vec()
|
self.data.to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop self, returning the decoded data.
|
/// Conver the Mnemonic into the internal representation of the decoded data.
|
||||||
pub fn into_bytes(self) -> Vec<u8> {
|
pub fn into_bytes(self) -> Vec<u8> {
|
||||||
self.entropy
|
self.data
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clone the existing entropy.
|
/// Clone the existing data.
|
||||||
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
|
#[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"]
|
||||||
pub fn entropy(&self) -> Vec<u8> {
|
pub fn entropy(&self) -> Vec<u8> {
|
||||||
self.entropy.clone()
|
self.data.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
|
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The method may return an error if the pbkdf2 function returns an invalid length, but this
|
/// The method should not return an error.
|
||||||
/// case should not be reached.
|
#[deprecated = "Use generate_seed() instead"]
|
||||||
pub fn seed<'a>(
|
pub fn seed<'a>(
|
||||||
&self,
|
&self,
|
||||||
passphrase: impl Into<Option<&'a str>>,
|
passphrase: impl Into<Option<&'a str>>,
|
||||||
) -> Result<Vec<u8>, MnemonicGenerationError> {
|
) -> Result<Vec<u8>, MnemonicGenerationError> {
|
||||||
|
Ok(self.generate_seed(passphrase))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a BIP-0032 seed from the provided data and an optional passphrase.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// The function may panic if the HmacSha512 function returns an error. The only error the
|
||||||
|
/// HmacSha512 function should return is an invalid length, which should not be possible.
|
||||||
|
///
|
||||||
|
pub fn generate_seed<'a>(&self, passphrase: impl Into<Option<&'a str>>) -> Vec<u8> {
|
||||||
let passphrase = passphrase.into();
|
let passphrase = passphrase.into();
|
||||||
|
|
||||||
let mut seed = [0u8; 64];
|
let mut seed = [0u8; 64];
|
||||||
let mnemonic = self.to_string();
|
let mnemonic = self.to_string();
|
||||||
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
|
let salt = ["mnemonic", passphrase.unwrap_or("")].join("");
|
||||||
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
|
pbkdf2::<Hmac<Sha512>>(mnemonic.as_bytes(), salt.as_bytes(), 2048, &mut seed)
|
||||||
.map_err(|_| MnemonicGenerationError::InvalidPbkdf2Length)?;
|
.expect("HmacSha512 InvalidLength should be infallible");
|
||||||
Ok(seed.to_vec())
|
seed.to_vec()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Encode the mnemonic into a list of wordlist indexes.
|
/// Encode the mnemonic into a list of integers 11 bits in length, matching the length of a
|
||||||
pub fn words(self) -> (Vec<usize>, Arc<Wordlist>) {
|
/// BIP-0039 wordlist.
|
||||||
let bit_count = self.entropy.len() * 8;
|
pub fn words(&self) -> Vec<usize> {
|
||||||
|
let bit_count = self.data.len() * 8;
|
||||||
let mut bits = vec![false; bit_count + bit_count / 32];
|
let mut bits = vec![false; bit_count + bit_count / 32];
|
||||||
|
|
||||||
for byte_index in 0..bit_count / 8 {
|
for byte_index in 0..bit_count / 8 {
|
||||||
for bit_index in 0..8 {
|
for bit_index in 0..8 {
|
||||||
bits[byte_index * 8 + bit_index] =
|
bits[byte_index * 8 + bit_index] =
|
||||||
(self.entropy[byte_index] & (1 << (7 - bit_index))) > 0;
|
(self.data[byte_index] & (1 << (7 - bit_index))) > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
hasher.update(&self.entropy);
|
hasher.update(&self.data);
|
||||||
let hash = hasher.finalize().to_vec();
|
let hash = hasher.finalize().to_vec();
|
||||||
for check_bit in 0..bit_count / 32 {
|
for check_bit in 0..bit_count / 32 {
|
||||||
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
|
bits[bit_count + check_bit] = (hash[check_bit / 8] & (1 << (7 - (check_bit % 8)))) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
let words = bits.chunks_exact(11).peekable().map(|chunk| {
|
// TODO: find a way to not have to collect to vec
|
||||||
|
bits.chunks_exact(11).peekable().map(|chunk| {
|
||||||
let mut num = 0usize;
|
let mut num = 0usize;
|
||||||
for i in 0..11 {
|
for i in 0..11 {
|
||||||
num += usize::from(chunk[10 - i]) << i;
|
num += usize::from(chunk[10 - i]) << i;
|
||||||
}
|
}
|
||||||
num
|
num
|
||||||
});
|
}).collect()
|
||||||
(words.collect(), self.wordlist.clone())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -337,13 +432,8 @@ mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn wordlist_word_count_correct() {
|
fn can_load_wordlist() {
|
||||||
let wordlist = Wordlist::default().into_inner();
|
let _wordlist = English::get_singleton();
|
||||||
assert_eq!(
|
|
||||||
wordlist.len(),
|
|
||||||
2usize.pow(11),
|
|
||||||
"Wordlist did not include correct word count"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -351,8 +441,7 @@ mod tests {
|
||||||
let mut random_handle = File::open("/dev/random").unwrap();
|
let mut random_handle = File::open("/dev/random").unwrap();
|
||||||
let entropy = &mut [0u8; 256 / 8];
|
let entropy = &mut [0u8; 256 / 8];
|
||||||
random_handle.read_exact(&mut entropy[..]).unwrap();
|
random_handle.read_exact(&mut entropy[..]).unwrap();
|
||||||
let wordlist = Wordlist::default().arc();
|
let mnemonic = super::Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
|
||||||
let mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
|
|
||||||
let new_entropy = mnemonic.as_bytes();
|
let new_entropy = mnemonic.as_bytes();
|
||||||
assert_eq!(new_entropy, entropy);
|
assert_eq!(new_entropy, entropy);
|
||||||
}
|
}
|
||||||
|
@ -361,7 +450,6 @@ mod tests {
|
||||||
fn conforms_to_trezor_tests() {
|
fn conforms_to_trezor_tests() {
|
||||||
let content = include_str!("data/vectors.json");
|
let content = include_str!("data/vectors.json");
|
||||||
let jsonobj: serde_json::Value = serde_json::from_str(content).unwrap();
|
let jsonobj: serde_json::Value = serde_json::from_str(content).unwrap();
|
||||||
let wordlist = Wordlist::default().arc();
|
|
||||||
|
|
||||||
for test in jsonobj["english"].as_array().unwrap() {
|
for test in jsonobj["english"].as_array().unwrap() {
|
||||||
let [ref hex_, ref seed, ..] = test.as_array().unwrap()[..] else {
|
let [ref hex_, ref seed, ..] = test.as_array().unwrap()[..] else {
|
||||||
|
@ -369,7 +457,7 @@ mod tests {
|
||||||
};
|
};
|
||||||
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
|
let hex = hex::decode(hex_.as_str().unwrap()).unwrap();
|
||||||
|
|
||||||
let mnemonic = Mnemonic::from_entropy(&hex, wordlist.clone()).unwrap();
|
let mnemonic = Mnemonic::from_bytes(&hex).unwrap();
|
||||||
|
|
||||||
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
|
assert_eq!(mnemonic.to_string(), seed.as_str().unwrap());
|
||||||
}
|
}
|
||||||
|
@ -380,17 +468,16 @@ mod tests {
|
||||||
let mut random_handle = File::open("/dev/random").unwrap();
|
let mut random_handle = File::open("/dev/random").unwrap();
|
||||||
let entropy = &mut [0u8; 256 / 8];
|
let entropy = &mut [0u8; 256 / 8];
|
||||||
random_handle.read_exact(&mut entropy[..]).unwrap();
|
random_handle.read_exact(&mut entropy[..]).unwrap();
|
||||||
let wordlist = Wordlist::default().arc();
|
let my_mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
|
||||||
let my_mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap();
|
|
||||||
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
|
let their_mnemonic = bip39::Mnemonic::from_entropy(&entropy[..256 / 8]).unwrap();
|
||||||
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
|
assert_eq!(my_mnemonic.to_string(), their_mnemonic.to_string());
|
||||||
assert_eq!(my_mnemonic.seed(None).unwrap(), their_mnemonic.to_seed(""));
|
assert_eq!(my_mnemonic.generate_seed(None), their_mnemonic.to_seed(""));
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
my_mnemonic.seed("testing").unwrap(),
|
my_mnemonic.generate_seed("testing"),
|
||||||
their_mnemonic.to_seed("testing")
|
their_mnemonic.to_seed("testing")
|
||||||
);
|
);
|
||||||
assert_ne!(
|
assert_ne!(
|
||||||
my_mnemonic.seed("test1").unwrap(),
|
my_mnemonic.generate_seed("test1"),
|
||||||
their_mnemonic.to_seed("test2")
|
their_mnemonic.to_seed("test2")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -400,14 +487,13 @@ mod tests {
|
||||||
let tests = 100_000;
|
let tests = 100_000;
|
||||||
let mut count = 0.;
|
let mut count = 0.;
|
||||||
let entropy = &mut [0u8; 256 / 8];
|
let entropy = &mut [0u8; 256 / 8];
|
||||||
let wordlist = Wordlist::default().arc();
|
|
||||||
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||||
let mut hs = HashSet::<usize>::with_capacity(24);
|
let mut hs = HashSet::<usize>::with_capacity(24);
|
||||||
|
|
||||||
for _ in 0..tests {
|
for _ in 0..tests {
|
||||||
random.read_exact(&mut entropy[..]).unwrap();
|
random.read_exact(&mut entropy[..]).unwrap();
|
||||||
let mnemonic = Mnemonic::from_entropy(&entropy[..256 / 8], wordlist.clone()).unwrap();
|
let mnemonic = Mnemonic::from_bytes(&entropy[..256 / 8]).unwrap();
|
||||||
let (words, _) = mnemonic.words();
|
let words = mnemonic.words();
|
||||||
hs.clear();
|
hs.clear();
|
||||||
hs.extend(words);
|
hs.extend(words);
|
||||||
if hs.len() != 24 {
|
if hs.len() != 24 {
|
||||||
|
@ -435,11 +521,10 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn can_do_up_to_1024_bits() {
|
fn can_do_up_to_1024_bits() {
|
||||||
let entropy = &mut [0u8; 128];
|
let entropy = &mut [0u8; 128];
|
||||||
let wordlist = Wordlist::default().arc();
|
|
||||||
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
let mut random = std::fs::File::open("/dev/urandom").unwrap();
|
||||||
random.read_exact(&mut entropy[..]).unwrap();
|
random.read_exact(&mut entropy[..]).unwrap();
|
||||||
let mnemonic = unsafe { Mnemonic::from_raw_entropy(&entropy[..], wordlist.clone()) };
|
let mnemonic = unsafe { Mnemonic::from_raw_bytes(&entropy[..]) };
|
||||||
let (words, _) = mnemonic.words();
|
let words = mnemonic.words();
|
||||||
assert!(words.len() == 96);
|
assert!(words.len() == 96);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,8 @@ use keyfork_prompt::{
|
||||||
Terminal, PromptHandler,
|
Terminal, PromptHandler,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use keyfork_mnemonic_util::English;
|
||||||
|
|
||||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let mut mgr = Terminal::new(stdin(), stdout())?;
|
let mut mgr = Terminal::new(stdin(), stdout())?;
|
||||||
let transport_validator = mnemonic::MnemonicSetValidator {
|
let transport_validator = mnemonic::MnemonicSetValidator {
|
||||||
|
@ -16,18 +18,16 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
word_lengths: [24, 48],
|
word_lengths: [24, 48],
|
||||||
};
|
};
|
||||||
|
|
||||||
let mnemonics = mgr.prompt_validated_wordlist(
|
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
|
||||||
"Enter a 9-word and 24-word mnemonic: ",
|
"Enter a 9-word and 24-word mnemonic: ",
|
||||||
&Default::default(),
|
|
||||||
3,
|
3,
|
||||||
transport_validator.to_fn(),
|
transport_validator.to_fn(),
|
||||||
)?;
|
)?;
|
||||||
assert_eq!(mnemonics[0].as_bytes().len(), 12);
|
assert_eq!(mnemonics[0].as_bytes().len(), 12);
|
||||||
assert_eq!(mnemonics[1].as_bytes().len(), 32);
|
assert_eq!(mnemonics[1].as_bytes().len(), 32);
|
||||||
|
|
||||||
let mnemonics = mgr.prompt_validated_wordlist(
|
let mnemonics = mgr.prompt_validated_wordlist::<English, _>(
|
||||||
"Enter a 24 and 48-word mnemonic: ",
|
"Enter a 24 and 48-word mnemonic: ",
|
||||||
&Default::default(),
|
|
||||||
3,
|
3,
|
||||||
combine_validator.to_fn(),
|
combine_validator.to_fn(),
|
||||||
)?;
|
)?;
|
||||||
|
|
|
@ -51,31 +51,31 @@ pub trait PromptHandler {
|
||||||
/// could not be read.
|
/// could not be read.
|
||||||
fn prompt_input(&mut self, prompt: &str) -> Result<String>;
|
fn prompt_input(&mut self, prompt: &str) -> Result<String>;
|
||||||
|
|
||||||
/// Prompt the user for input based on a wordlist.
|
/// Prompt the user for input based on a wordlist. A language must be specified as the generic
|
||||||
|
/// parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The method may return an error if the message was not able to be displayed or if the input
|
/// The method may return an error if the message was not able to be displayed or if the input
|
||||||
/// could not be read.
|
/// could not be read.
|
||||||
#[cfg(feature = "mnemonic")]
|
#[cfg(feature = "mnemonic")]
|
||||||
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String>;
|
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String> where X: Wordlist;
|
||||||
|
|
||||||
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
/// Prompt the user for input based on a wordlist, while validating the wordlist using a
|
||||||
/// provided parser function, returning the type from the parser.
|
/// provided parser function, returning the type from the parser. A language must be specified
|
||||||
|
/// as the generic parameter `X` (any type implementing [`Wordlist`]) when parsing a wordlist.
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The method may return an error if the message was not able to be displayed, if the input
|
/// The method may return an error if the message was not able to be displayed, if the input
|
||||||
/// could not be read, or if the parser returned an error.
|
/// could not be read, or if the parser returned an error.
|
||||||
#[cfg(feature = "mnemonic")]
|
#[cfg(feature = "mnemonic")]
|
||||||
fn prompt_validated_wordlist<V, F, E>(
|
fn prompt_validated_wordlist<X, V>(
|
||||||
&mut self,
|
&mut self,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
wordlist: &Wordlist,
|
|
||||||
retries: u8,
|
retries: u8,
|
||||||
validator_fn: F,
|
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
||||||
) -> Result<V, Error>
|
) -> Result<V, Error>
|
||||||
where
|
where
|
||||||
F: Fn(String) -> Result<V, E>,
|
X: Wordlist;
|
||||||
E: std::error::Error;
|
|
||||||
|
|
||||||
/// Prompt the user for a passphrase, which is hidden while typing.
|
/// Prompt the user for a passphrase, which is hidden while typing.
|
||||||
///
|
///
|
||||||
|
@ -90,15 +90,12 @@ pub trait PromptHandler {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The method may return an error if the message was not able to be displayed, if the input
|
/// The method may return an error if the message was not able to be displayed, if the input
|
||||||
/// could not be read, or if the parser returned an error.
|
/// could not be read, or if the parser returned an error.
|
||||||
fn prompt_validated_passphrase<V, F, E>(
|
fn prompt_validated_passphrase<V>(
|
||||||
&mut self,
|
&mut self,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
retries: u8,
|
retries: u8,
|
||||||
validator_fn: F,
|
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
||||||
) -> Result<V, Error>
|
) -> Result<V, Error>;
|
||||||
where
|
|
||||||
F: Fn(String) -> Result<V, E>,
|
|
||||||
E: std::error::Error;
|
|
||||||
|
|
||||||
/// Prompt the user with a [`Message`].
|
/// Prompt the user with a [`Message`].
|
||||||
///
|
///
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Borrow,
|
||||||
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
|
io::{stderr, stdin, BufRead, BufReader, Read, Stderr, Stdin, Write},
|
||||||
os::fd::AsRawFd, borrow::Borrow,
|
os::fd::AsRawFd,
|
||||||
};
|
};
|
||||||
|
|
||||||
use keyfork_crossterm::{
|
use keyfork_crossterm::{
|
||||||
|
@ -12,7 +13,7 @@ use keyfork_crossterm::{
|
||||||
ExecutableCommand, QueueableCommand,
|
ExecutableCommand, QueueableCommand,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{PromptHandler, Message, Wordlist, Error};
|
use crate::{Error, Message, PromptHandler, Wordlist};
|
||||||
|
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
pub type Result<T, E = Error> = std::result::Result<T, E>;
|
||||||
|
@ -155,11 +156,13 @@ where
|
||||||
fn lock(&mut self) -> TerminalGuard<'_, R, W> {
|
fn lock(&mut self) -> TerminalGuard<'_, R, W> {
|
||||||
TerminalGuard::new(&mut self.read, &mut self.write, &mut self.terminal)
|
TerminalGuard::new(&mut self.read, &mut self.write, &mut self.terminal)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + AsRawFd + Sized {
|
impl<R, W> PromptHandler for Terminal<R, W>
|
||||||
|
where
|
||||||
|
R: Read + Sized,
|
||||||
|
W: Write + AsRawFd + Sized,
|
||||||
|
{
|
||||||
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
|
fn prompt_input(&mut self, prompt: &str) -> Result<String> {
|
||||||
let mut terminal = self.lock().alternate_screen()?;
|
let mut terminal = self.lock().alternate_screen()?;
|
||||||
terminal
|
terminal
|
||||||
|
@ -182,20 +185,18 @@ impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + As
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "mnemonic")]
|
#[cfg(feature = "mnemonic")]
|
||||||
fn prompt_validated_wordlist<V, F, E>(
|
fn prompt_validated_wordlist<X, V>(
|
||||||
&mut self,
|
&mut self,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
wordlist: &Wordlist,
|
|
||||||
retries: u8,
|
retries: u8,
|
||||||
validator_fn: F,
|
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
||||||
) -> Result<V, Error>
|
) -> Result<V, Error>
|
||||||
where
|
where
|
||||||
F: Fn(String) -> Result<V, E>,
|
X: Wordlist,
|
||||||
E: std::error::Error,
|
|
||||||
{
|
{
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
for _ in 0..retries {
|
for _ in 0..retries {
|
||||||
let s = self.prompt_wordlist(prompt, wordlist)?;
|
let s = self.prompt_wordlist::<X>(prompt)?;
|
||||||
match validator_fn(s) {
|
match validator_fn(s) {
|
||||||
Ok(v) => return Ok(v),
|
Ok(v) => return Ok(v),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
@ -214,7 +215,13 @@ impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + As
|
||||||
|
|
||||||
#[cfg(feature = "mnemonic")]
|
#[cfg(feature = "mnemonic")]
|
||||||
#[allow(clippy::too_many_lines)]
|
#[allow(clippy::too_many_lines)]
|
||||||
fn prompt_wordlist(&mut self, prompt: &str, wordlist: &Wordlist) -> Result<String> {
|
fn prompt_wordlist<X>(&mut self, prompt: &str) -> Result<String>
|
||||||
|
where
|
||||||
|
X: Wordlist,
|
||||||
|
{
|
||||||
|
let wordlist = X::get_singleton();
|
||||||
|
let words = wordlist.to_str_array();
|
||||||
|
|
||||||
let mut terminal = self
|
let mut terminal = self
|
||||||
.lock()
|
.lock()
|
||||||
.alternate_screen()?
|
.alternate_screen()?
|
||||||
|
@ -316,7 +323,7 @@ impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + As
|
||||||
let mut iter = printable_input.split_whitespace().peekable();
|
let mut iter = printable_input.split_whitespace().peekable();
|
||||||
|
|
||||||
while let Some(word) = iter.next() {
|
while let Some(word) = iter.next() {
|
||||||
if wordlist.contains(word) {
|
if words.contains(&word) {
|
||||||
terminal.queue(PrintStyledContent(word.green()))?;
|
terminal.queue(PrintStyledContent(word.green()))?;
|
||||||
} else {
|
} else {
|
||||||
terminal.queue(PrintStyledContent(word.red()))?;
|
terminal.queue(PrintStyledContent(word.red()))?;
|
||||||
|
@ -337,16 +344,12 @@ impl<R, W> PromptHandler for Terminal<R, W> where R: Read + Sized, W: Write + As
|
||||||
Ok(input)
|
Ok(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_validated_passphrase<V, F, E>(
|
fn prompt_validated_passphrase<V>(
|
||||||
&mut self,
|
&mut self,
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
retries: u8,
|
retries: u8,
|
||||||
validator_fn: F,
|
validator_fn: impl Fn(String) -> Result<V, Box<dyn std::error::Error>>,
|
||||||
) -> Result<V, Error>
|
) -> Result<V, Error> {
|
||||||
where
|
|
||||||
F: Fn(String) -> Result<V, E>,
|
|
||||||
E: std::error::Error,
|
|
||||||
{
|
|
||||||
let mut last_error = None;
|
let mut last_error = None;
|
||||||
for _ in 0..retries {
|
for _ in 0..retries {
|
||||||
let s = self.prompt_passphrase(prompt)?;
|
let s = self.prompt_passphrase(prompt)?;
|
||||||
|
|
|
@ -12,7 +12,7 @@ pub trait Validator {
|
||||||
type Error;
|
type Error;
|
||||||
|
|
||||||
/// Create a validator function from the given parameters.
|
/// Create a validator function from the given parameters.
|
||||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>>;
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A PIN could not be validated from the given input.
|
/// A PIN could not be validated from the given input.
|
||||||
|
@ -48,7 +48,7 @@ impl Validator for PinValidator {
|
||||||
type Output = String;
|
type Output = String;
|
||||||
type Error = PinError;
|
type Error = PinError;
|
||||||
|
|
||||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, PinError>> {
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<String, Box<dyn std::error::Error>>> {
|
||||||
let min_len = self.min_length.unwrap_or(usize::MIN);
|
let min_len = self.min_length.unwrap_or(usize::MIN);
|
||||||
let max_len = self.max_length.unwrap_or(usize::MAX);
|
let max_len = self.max_length.unwrap_or(usize::MAX);
|
||||||
let range = self.range.clone().unwrap_or('0'..='9');
|
let range = self.range.clone().unwrap_or('0'..='9');
|
||||||
|
@ -56,14 +56,14 @@ impl Validator for PinValidator {
|
||||||
s.truncate(s.trim_end().len());
|
s.truncate(s.trim_end().len());
|
||||||
let len = s.len();
|
let len = s.len();
|
||||||
if len < min_len {
|
if len < min_len {
|
||||||
return Err(PinError::TooShort(len, min_len));
|
return Err(Box::new(PinError::TooShort(len, min_len)));
|
||||||
}
|
}
|
||||||
if len > max_len {
|
if len > max_len {
|
||||||
return Err(PinError::TooLong(len, max_len));
|
return Err(Box::new(PinError::TooLong(len, max_len)));
|
||||||
}
|
}
|
||||||
for (index, ch) in s.chars().enumerate() {
|
for (index, ch) in s.chars().enumerate() {
|
||||||
if !range.contains(&ch) {
|
if !range.contains(&ch) {
|
||||||
return Err(PinError::InvalidCharacters(ch, index));
|
return Err(Box::new(PinError::InvalidCharacters(ch, index)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(s)
|
Ok(s)
|
||||||
|
@ -123,13 +123,13 @@ pub mod mnemonic {
|
||||||
type Output = Mnemonic;
|
type Output = Mnemonic;
|
||||||
type Error = MnemonicValidationError;
|
type Error = MnemonicValidationError;
|
||||||
|
|
||||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Mnemonic, Self::Error>> {
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Mnemonic, Box<dyn std::error::Error>>> {
|
||||||
let word_length = self.word_length.clone();
|
let word_length = self.word_length.clone();
|
||||||
Box::new(move |s: String| match word_length.as_ref() {
|
Box::new(move |s: String| match word_length.as_ref() {
|
||||||
Some(wl) => {
|
Some(wl) => {
|
||||||
let count = s.split_whitespace().count();
|
let count = s.split_whitespace().count();
|
||||||
if !wl.matches(count) {
|
if !wl.matches(count) {
|
||||||
return Err(Self::Error::InvalidLength(count, wl.clone()));
|
return Err(Box::new(Self::Error::InvalidLength(count, wl.clone())));
|
||||||
}
|
}
|
||||||
let m = Mnemonic::from_str(&s)?;
|
let m = Mnemonic::from_str(&s)?;
|
||||||
Ok(m)
|
Ok(m)
|
||||||
|
@ -165,7 +165,7 @@ pub mod mnemonic {
|
||||||
type Output = Mnemonic;
|
type Output = Mnemonic;
|
||||||
type Error = MnemonicChoiceValidationError;
|
type Error = MnemonicChoiceValidationError;
|
||||||
|
|
||||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>> {
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>> {
|
||||||
let word_lengths = self.word_lengths.clone();
|
let word_lengths = self.word_lengths.clone();
|
||||||
Box::new(move |s: String| {
|
Box::new(move |s: String| {
|
||||||
let count = s.split_whitespace().count();
|
let count = s.split_whitespace().count();
|
||||||
|
@ -175,10 +175,10 @@ pub mod mnemonic {
|
||||||
return Ok(m);
|
return Ok(m);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(MnemonicChoiceValidationError::InvalidLength(
|
Err(Box::new(MnemonicChoiceValidationError::InvalidLength(
|
||||||
count,
|
count,
|
||||||
word_lengths.to_vec(),
|
word_lengths.to_vec(),
|
||||||
))
|
)))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -207,7 +207,7 @@ pub mod mnemonic {
|
||||||
type Output = [Mnemonic; N];
|
type Output = [Mnemonic; N];
|
||||||
type Error = MnemonicSetValidationError;
|
type Error = MnemonicSetValidationError;
|
||||||
|
|
||||||
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Self::Error>> {
|
fn to_fn(&self) -> Box<dyn Fn(String) -> Result<Self::Output, Box<dyn std::error::Error>>> {
|
||||||
let word_lengths = self.word_lengths;
|
let word_lengths = self.word_lengths;
|
||||||
Box::new(move |s: String| {
|
Box::new(move |s: String| {
|
||||||
let mut counter: usize = 0;
|
let mut counter: usize = 0;
|
||||||
|
@ -219,15 +219,17 @@ pub mod mnemonic {
|
||||||
.take(word_length)
|
.take(word_length)
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
if words.len() != word_length {
|
if words.len() != word_length {
|
||||||
return Err(MnemonicSetValidationError::InvalidSetLength(
|
return Err(Box::new(MnemonicSetValidationError::InvalidSetLength(
|
||||||
word_set,
|
word_set,
|
||||||
words.len(),
|
words.len(),
|
||||||
word_length,
|
word_length,
|
||||||
));
|
)));
|
||||||
}
|
}
|
||||||
let mnemonic = match Mnemonic::from_str(&words.join(" ")) {
|
let mnemonic = match Mnemonic::from_str(&words.join(" ")) {
|
||||||
Ok(m) => m,
|
Ok(m) => m,
|
||||||
Err(e) => return Err(Self::Error::MnemonicFromStrError(word_set, e)),
|
Err(e) => {
|
||||||
|
return Err(Box::new(Self::Error::MnemonicFromStrError(word_set, e)))
|
||||||
|
}
|
||||||
};
|
};
|
||||||
output.push(mnemonic);
|
output.push(mnemonic);
|
||||||
counter += word_length;
|
counter += word_length;
|
||||||
|
|
|
@ -28,7 +28,15 @@ impl std::fmt::Display for DecodeError {
|
||||||
impl std::error::Error for DecodeError {}
|
impl std::error::Error for DecodeError {}
|
||||||
|
|
||||||
/// Encode a given input as a hex string.
|
/// Encode a given input as a hex string.
|
||||||
pub fn encode(input: &[u8]) -> String {
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let data = b"hello world!";
|
||||||
|
/// let result = smex::encode(&data);
|
||||||
|
/// assert_eq!(result, "68656c6c6f20776f726c6421");
|
||||||
|
/// ```
|
||||||
|
pub fn encode(input: impl AsRef<[u8]>) -> String {
|
||||||
|
let input = input.as_ref();
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
for byte in input {
|
for byte in input {
|
||||||
write!(s, "{byte:02x}").unwrap();
|
write!(s, "{byte:02x}").unwrap();
|
||||||
|
@ -50,7 +58,26 @@ fn val(c: u8) -> Result<u8, DecodeError> {
|
||||||
/// # Errors
|
/// # Errors
|
||||||
/// The function may error if a non-hex character is encountered or if the character count is not
|
/// The function may error if a non-hex character is encountered or if the character count is not
|
||||||
/// evenly divisible by two.
|
/// evenly divisible by two.
|
||||||
pub fn decode(input: &str) -> Result<Vec<u8>, DecodeError> {
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```rust
|
||||||
|
/// let data = b"hello world!";
|
||||||
|
/// let encoded = smex::encode(&data);
|
||||||
|
/// let decoded = smex::decode(&encoded).unwrap();
|
||||||
|
/// assert_eq!(data.as_slice(), decoded.as_slice());
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The function may return an error if the given input is not valid hex.
|
||||||
|
///
|
||||||
|
/// ```rust,should_panic
|
||||||
|
/// let data = b"hello world!";
|
||||||
|
/// let mut encoded = smex::encode(&data);
|
||||||
|
/// encoded.push('G');
|
||||||
|
/// let decoded = smex::decode(&encoded).unwrap();
|
||||||
|
/// assert_eq!(data.as_slice(), decoded.as_slice());
|
||||||
|
/// ```
|
||||||
|
pub fn decode(input: impl AsRef<str>) -> Result<Vec<u8>, DecodeError> {
|
||||||
|
let input = input.as_ref();
|
||||||
let len = input.len();
|
let len = input.len();
|
||||||
if len % 2 != 0 {
|
if len % 2 != 0 {
|
||||||
return Err(DecodeError::InvalidCharacterCount(len));
|
return Err(DecodeError::InvalidCharacterCount(len));
|
||||||
|
|
Loading…
Reference in New Issue