diff --git a/crates/daemon/keyforkd-client/src/tests.rs b/crates/daemon/keyforkd-client/src/tests.rs index a3aabfb..d12f528 100644 --- a/crates/daemon/keyforkd-client/src/tests.rs +++ b/crates/daemon/keyforkd-client/src/tests.rs @@ -47,7 +47,7 @@ fn secp256k1() { ); let response = DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap(); - assert_eq!(response.data, test.private_key); + assert_eq!(&response.data, test.private_key.as_slice()); } handle.abort(); @@ -92,7 +92,7 @@ fn ed25519() { ); let response = DerivationResponse::try_from(client.request(&req.into()).unwrap()).unwrap(); - assert_eq!(response.data, test.private_key); + assert_eq!(&response.data, test.private_key.as_slice()); } handle.abort(); diff --git a/crates/daemon/keyforkd/src/service.rs b/crates/daemon/keyforkd/src/service.rs index 1b4058b..fee1cad 100644 --- a/crates/daemon/keyforkd/src/service.rs +++ b/crates/daemon/keyforkd/src/service.rs @@ -76,7 +76,7 @@ impl Service for Keyforkd { info!("Deriving path: {}", req.path()); } - req.derive_with_master_seed((*seed).clone()) + req.derive_with_master_seed(seed.as_ref()) .map(Response::Derivation) .map_err(|e| DerivationError::Derivation(e.to_string()).into()) }), @@ -120,7 +120,7 @@ mod tests { .unwrap() .try_into() .unwrap(); - assert_eq!(response.data, test.private_key); + assert_eq!(&response.data, test.private_key.as_slice()); assert_eq!(response.chain_code.as_slice(), test.chain_code); } } @@ -150,7 +150,7 @@ mod tests { .unwrap() .try_into() .unwrap(); - assert_eq!(response.data, test.private_key); + assert_eq!(&response.data, test.private_key.as_slice()); assert_eq!(response.chain_code.as_slice(), test.chain_code); } } diff --git a/crates/derive/keyfork-derive-util/README.md b/crates/derive/keyfork-derive-util/README.md index 20135ff..55fb1e1 100644 --- a/crates/derive/keyfork-derive-util/README.md +++ b/crates/derive/keyfork-derive-util/README.md @@ -40,7 +40,7 @@ fn main() -> Result<(), Box> { let key1 = request.derive_with_mnemonic(&mnemonic)?; let seed = mnemonic.seed(None)?; - let key2 = request.derive_with_master_seed(seed)?; + let key2 = request.derive_with_master_seed(&seed)?; assert_eq!(key1, key2); diff --git a/crates/derive/keyfork-derive-util/src/extended_key/private_key.rs b/crates/derive/keyfork-derive-util/src/extended_key/private_key.rs index f63846e..ad15994 100644 --- a/crates/derive/keyfork-derive-util/src/extended_key/private_key.rs +++ b/crates/derive/keyfork-derive-util/src/extended_key/private_key.rs @@ -31,6 +31,68 @@ type Result = std::result::Result; type ChainCode = [u8; 32]; type HmacSha512 = Hmac; +/// A reference to a variable-length seed. Keyfork automatically supports a seed of 128 bits, +/// 256 bits, or 512 bits, but because the master key is derived from a hashed seed, in theory +/// any amount of bytes could be used. It is not advised to use a variable-length seed longer +/// than 256 bits, as a brute-force attack on the master key could be performed in 2^256 +/// attempts. +/// +/// Mnemonics use a 512 bit seed, as knowledge of the mnemonics' words (such as through a side +/// channel attack) could leak which individual word is used, but not the order the words are +/// used in. Using a 512 bit hash to generate the seed results in a more computationally +/// expensive brute-force requirement. +pub struct VariableLengthSeed<'a> { + seed: &'a [u8], +} + +impl<'a> VariableLengthSeed<'a> { + /// Create a new VariableLengthSeed. + /// + /// # Examples + /// ```rust + /// use sha2::{Sha256, Digest}; + /// use keyfork_derive_util::VariableLengthSeed; + /// + /// let data = b"the missile is very eepy and wants to take a small sleeb"; + /// let seed = VariableLengthSeed::new(data); + /// ``` + pub fn new(seed: &'a [u8]) -> Self { + Self { seed } + } +} + +mod as_private_key { + use super::VariableLengthSeed; + + pub trait AsPrivateKey { + fn as_private_key(&self) -> &[u8]; + } + + impl AsPrivateKey for [u8; 16] { + fn as_private_key(&self) -> &[u8] { + self + } + } + + impl AsPrivateKey for [u8; 32] { + fn as_private_key(&self) -> &[u8] { + self + } + } + + impl AsPrivateKey for [u8; 64] { + fn as_private_key(&self) -> &[u8] { + self + } + } + + impl AsPrivateKey for VariableLengthSeed<'_> { + fn as_private_key(&self) -> &[u8] { + self.seed + } + } +} + /// Extended private keys derived using BIP-0032. /// /// Generic over types implementing [`PrivateKey`]. @@ -101,10 +163,10 @@ where /// # }; /// let seed: &[u8; 64] = // /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; - /// let xprv = ExtendedPrivateKey::::new(seed); + /// let xprv = ExtendedPrivateKey::::new(*seed); /// ``` - pub fn new(seed: impl AsRef<[u8]>) -> Self { - Self::new_internal(seed.as_ref()) + pub fn new(seed: impl as_private_key::AsPrivateKey) -> Self { + Self::new_internal(seed.as_private_key()) } fn new_internal(seed: &[u8]) -> Self { @@ -191,7 +253,7 @@ where /// # 102, 201, 210, 159, 219, 222, 42, 201, 44, 196, 27, /// # 90, 221, 80, 85, 135, 79, 39, 253, 223, 35, 251 /// # ]; - /// let xprv = ExtendedPrivateKey::::new(seed); + /// let xprv = ExtendedPrivateKey::::new(*seed); /// let xpub = xprv.extended_public_key(); /// assert_eq!(known_key, xpub.public_key().to_bytes()); /// # Ok(()) @@ -215,7 +277,7 @@ where /// # fn main() -> Result<(), Box> { /// let seed: &[u8; 64] = // /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; - /// let xprv = ExtendedPrivateKey::::new(seed); + /// let xprv = ExtendedPrivateKey::::new(*seed); /// let pubkey = xprv.public_key(); /// # Ok(()) /// # } @@ -280,7 +342,7 @@ where /// # fn main() -> Result<(), Box> { /// let seed: &[u8; 64] = // /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; - /// let root_xprv = ExtendedPrivateKey::::new(seed); + /// let root_xprv = ExtendedPrivateKey::::new(*seed); /// let path = DerivationPath::default() /// .chain_push(DerivationIndex::new(44, true)?) /// .chain_push(DerivationIndex::new(0, true)?) @@ -326,7 +388,7 @@ where /// # fn main() -> Result<(), Box> { /// let seed: &[u8; 64] = // /// # b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; - /// let root_xprv = ExtendedPrivateKey::::new(seed); + /// let root_xprv = ExtendedPrivateKey::::new(*seed); /// let bip44_wallet = DerivationPath::default() /// .chain_push(DerivationIndex::new(44, true)?) /// .chain_push(DerivationIndex::new(0, true)?) diff --git a/crates/derive/keyfork-derive-util/src/lib.rs b/crates/derive/keyfork-derive-util/src/lib.rs index ba170d4..ce2c1a7 100644 --- a/crates/derive/keyfork-derive-util/src/lib.rs +++ b/crates/derive/keyfork-derive-util/src/lib.rs @@ -17,7 +17,7 @@ pub mod public_key; mod tests; #[doc(inline)] -pub use crate::extended_key::{private_key::ExtendedPrivateKey, public_key::ExtendedPublicKey}; +pub use crate::extended_key::{private_key::{ExtendedPrivateKey, Error as XPrvError, VariableLengthSeed}, public_key::{ExtendedPublicKey, Error as XPubError}}; pub use crate::{ index::{DerivationIndex, Error as IndexError}, diff --git a/crates/derive/keyfork-derive-util/src/request.rs b/crates/derive/keyfork-derive-util/src/request.rs index cb5c63b..d6f5830 100644 --- a/crates/derive/keyfork-derive-util/src/request.rs +++ b/crates/derive/keyfork-derive-util/src/request.rs @@ -19,10 +19,11 @@ //! ``` use crate::{ - extended_key::private_key::Error as XPrvError, + extended_key::private_key::{Error as XPrvError, VariableLengthSeed}, private_key::{PrivateKey, TestPrivateKey}, DerivationPath, ExtendedPrivateKey, }; + use keyfork_mnemonic_util::{Mnemonic, MnemonicGenerationError}; use serde::{Deserialize, Serialize}; @@ -64,7 +65,8 @@ impl DerivationAlgorithm { /// /// # Errors /// The method may error if the derivation fails or if the algorithm is not supported. - fn derive(&self, seed: Vec, path: &DerivationPath) -> Result { + fn derive(&self, seed: &[u8], path: &DerivationPath) -> Result { + let seed = VariableLengthSeed::new(seed); match self { #[cfg(feature = "ed25519")] Self::Ed25519 => { @@ -207,7 +209,7 @@ impl DerivationRequest { /// # } pub fn derive_with_mnemonic(&self, mnemonic: &Mnemonic) -> Result { // TODO: passphrase support and/or store passphrase within mnemonic - self.derive_with_master_seed(mnemonic.seed(None)?) + self.derive_with_master_seed(&mnemonic.seed(None)?) } /// Derive an [`ExtendedPrivateKey`] using the given seed. @@ -231,10 +233,10 @@ impl DerivationRequest { /// let path: DerivationPath = // /// # DerivationPath::default(); /// let request = DerivationRequest::new(algo, &path); - /// let response = request.derive_with_master_seed(seed.to_vec())?; + /// let response = request.derive_with_master_seed(seed)?; /// # Ok(()) /// # } - pub fn derive_with_master_seed(&self, seed: Vec) -> Result { + pub fn derive_with_master_seed(&self, seed: &[u8]) -> Result { self.algorithm.derive(seed, &self.path) } } diff --git a/crates/derive/keyfork-derive-util/src/tests.rs b/crates/derive/keyfork-derive-util/src/tests.rs index eb9d6f4..3f2eba3 100644 --- a/crates/derive/keyfork-derive-util/src/tests.rs +++ b/crates/derive/keyfork-derive-util/src/tests.rs @@ -30,7 +30,8 @@ fn secp256k1() { } = test; // Tests for ExtendedPrivateKey - let xkey = ExtendedPrivateKey::::new(seed); + let varlen_seed = VariableLengthSeed::new(&seed); + let xkey = ExtendedPrivateKey::::new(varlen_seed); let derived_key = xkey.derive_path(&chain).unwrap(); assert_eq!( derived_key.chain_code().as_slice(), @@ -50,7 +51,7 @@ fn secp256k1() { // Tests for DerivationRequest let request = DerivationRequest::new(DerivationAlgorithm::Secp256k1, &chain); - let response = request.derive_with_master_seed(seed.clone()).unwrap(); + let response = request.derive_with_master_seed(&seed).unwrap(); assert_eq!(&response.data, private_key.as_slice(), "test: {chain}"); } } @@ -75,7 +76,8 @@ fn ed25519() { } = test; // Tests for ExtendedPrivateKey - let xkey = ExtendedPrivateKey::::new(seed); + let varlen_seed = VariableLengthSeed::new(&seed); + let xkey = ExtendedPrivateKey::::new(varlen_seed); let derived_key = xkey.derive_path(&chain).unwrap(); assert_eq!( derived_key.chain_code().as_slice(), @@ -95,7 +97,7 @@ fn ed25519() { // Tests for DerivationRequest let request = DerivationRequest::new(DerivationAlgorithm::Ed25519, &chain); - let response = request.derive_with_master_seed(seed.to_vec()).unwrap(); + let response = request.derive_with_master_seed(&seed).unwrap(); assert_eq!(&response.data, private_key.as_slice(), "test: {chain}"); } } diff --git a/crates/keyfork-shard/Cargo.toml b/crates/keyfork-shard/Cargo.toml index f1ae88b..8c2d75a 100644 --- a/crates/keyfork-shard/Cargo.toml +++ b/crates/keyfork-shard/Cargo.toml @@ -7,11 +7,10 @@ license = "AGPL-3.0-only" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = ["openpgp", "openpgp-card", "qrcode"] +default = ["openpgp", "openpgp-card", "qrcode", "sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"] openpgp = ["sequoia-openpgp", "anyhow"] openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"] qrcode = ["keyfork-qrcode"] -bin = ["sequoia-openpgp/crypto-nettle", "keyfork-qrcode/decode-backend-rqrr"] [dependencies] keyfork-prompt = { version = "0.1.0", path = "../util/keyfork-prompt", default-features = false, features = ["mnemonic"] } diff --git a/crates/keyfork-shard/src/openpgp.rs b/crates/keyfork-shard/src/openpgp.rs index c3c1aad..c07396a 100644 --- a/crates/keyfork-shard/src/openpgp.rs +++ b/crates/keyfork-shard/src/openpgp.rs @@ -14,7 +14,7 @@ use aes_gcm::{ }; use hkdf::{Hkdf, InvalidLength as HkdfInvalidLength}; use keyfork_derive_openpgp::{ - derive_util::{DerivationPath, PathError}, + derive_util::{DerivationPath, PathError, VariableLengthSeed}, XPrv, }; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist}; @@ -648,7 +648,8 @@ pub fn combine( // TODO: extract as function let userid = UserID::from("keyfork-sss"); let path = DerivationPath::from_str("m/7366512'/0'")?; - let xprv = XPrv::new(&secret).derive_path(&path)?; + let seed = VariableLengthSeed::new(&secret); + let xprv = XPrv::new(seed).derive_path(&path)?; let derived_cert = keyfork_derive_openpgp::derive( xprv, &[KeyFlags::empty().set_certification().set_signing()], @@ -679,10 +680,11 @@ pub fn combine( /// The function may panic if the metadata can't properly store the certificates used to generate /// the encrypted shares. pub fn split(threshold: u8, certs: Vec, secret: &[u8], output: impl Write) -> Result<()> { + let seed = VariableLengthSeed::new(secret); // build cert to sign encrypted shares let userid = UserID::from("keyfork-sss"); let path = DerivationPath::from_str("m/7366512'/0'")?; - let xprv = XPrv::new(&secret).derive_path(&path)?; + let xprv = XPrv::new(seed).derive_path(&path)?; let derived_cert = keyfork_derive_openpgp::derive( xprv, &[KeyFlags::empty().set_certification().set_signing()], diff --git a/crates/keyfork/src/cli/wizard.rs b/crates/keyfork/src/cli/wizard.rs index fb52abe..3c6e1db 100644 --- a/crates/keyfork/src/cli/wizard.rs +++ b/crates/keyfork/src/cli/wizard.rs @@ -21,7 +21,7 @@ pub struct PinLength(usize); type Result> = std::result::Result; -fn derive_key(seed: &[u8], index: u8) -> Result { +fn derive_key(seed: [u8; 32], index: u8) -> Result { let subkeys = vec![ KeyFlags::empty().set_certification(), KeyFlags::empty().set_signing(), @@ -102,7 +102,7 @@ fn generate_shard_secret( keys_per_shard: u8, output_file: &Option, ) -> Result<()> { - let seed = keyfork_entropy::generate_entropy_of_size(256 / 8)?; + let seed = keyfork_entropy::generate_entropy_of_const_size::<{256 / 8}>()?; let mut pm = Terminal::new(std::io::stdin(), std::io::stderr())?; let mut certs = vec![]; let mut seen_cards: HashSet = HashSet::new(); @@ -126,7 +126,7 @@ fn generate_shard_secret( .to_fn(); for index in 0..max { - let cert = derive_key(&seed, index)?; + let cert = derive_key(seed, index)?; for i in 0..keys_per_shard { pm.prompt_message(Message::Text(format!( "Please remove all keys and insert key #{} for user #{}", diff --git a/crates/util/keyfork-entropy/Cargo.toml b/crates/util/keyfork-entropy/Cargo.toml index 1d5a444..20a75c2 100644 --- a/crates/util/keyfork-entropy/Cargo.toml +++ b/crates/util/keyfork-entropy/Cargo.toml @@ -7,7 +7,7 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] -default = [] +default = ["bin"] bin = ["smex"] [dependencies] diff --git a/crates/util/keyfork-entropy/src/lib.rs b/crates/util/keyfork-entropy/src/lib.rs index d7512b5..8f1580f 100644 --- a/crates/util/keyfork-entropy/src/lib.rs +++ b/crates/util/keyfork-entropy/src/lib.rs @@ -1,6 +1,9 @@ //! Utilities for reading entropy from secure sources. -use std::{fs::{read_dir, read_to_string, File}, io::Read}; +use std::{ + fs::{read_dir, read_to_string, File}, + io::Read, +}; static WARNING_LINKS: [&str; 1] = ["https://lore.kernel.org/lkml/20211223141113.1240679-2-Jason@zx2c4.com/"]; @@ -84,3 +87,24 @@ pub fn generate_entropy_of_size(byte_count: usize) -> Result, std::io::E entropy_file.read_exact(&mut vec[..])?; Ok(vec) } + +/// Read system entropy of a constant size. +/// +/// # Errors +/// An error may be returned if an error occurred while reading from the random source. +/// +/// # Examples +/// ```rust,no_run +/// # fn main() -> Result<(), Box> { +/// # std::env::set_var("SHOOT_SELF_IN_FOOT", "1"); +/// let entropy = keyfork_entropy::generate_entropy_of_const_size::<64>()?; +/// assert_eq!(entropy.len(), 64); +/// # Ok(()) +/// # } +/// ``` +pub fn generate_entropy_of_const_size() -> Result<[u8; N], std::io::Error> { + let mut output = [0u8; N]; + let mut entropy_file = File::open("/dev/urandom")?; + entropy_file.read_exact(&mut output[..])?; + Ok(output) +} diff --git a/crates/util/keyfork-mnemonic-util/Cargo.toml b/crates/util/keyfork-mnemonic-util/Cargo.toml index 3707884..10c88dc 100644 --- a/crates/util/keyfork-mnemonic-util/Cargo.toml +++ b/crates/util/keyfork-mnemonic-util/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" license = "MIT" [features] -default = [] +default = ["bin"] bin = ["smex"] [dependencies] diff --git a/crates/util/keyfork-mnemonic-util/src/lib.rs b/crates/util/keyfork-mnemonic-util/src/lib.rs index c34e305..b1eb04b 100644 --- a/crates/util/keyfork-mnemonic-util/src/lib.rs +++ b/crates/util/keyfork-mnemonic-util/src/lib.rs @@ -276,7 +276,7 @@ impl Mnemonic { } /// Clone the existing entropy. - #[deprecated] + #[deprecated = "Use as_bytes(), to_bytes(), or into_bytes() instead"] pub fn entropy(&self) -> Vec { self.entropy.clone() } @@ -353,8 +353,8 @@ mod tests { random_handle.read_exact(&mut entropy[..]).unwrap(); let wordlist = Wordlist::default().arc(); let mnemonic = super::Mnemonic::from_entropy(&entropy[..256 / 8], wordlist).unwrap(); - let new_entropy = mnemonic.entropy(); - assert_eq!(&new_entropy, entropy); + let new_entropy = mnemonic.as_bytes(); + assert_eq!(new_entropy, entropy); } #[test] diff --git a/crates/util/keyfork-prompt/examples/test-basic-prompt.rs b/crates/util/keyfork-prompt/examples/test-basic-prompt.rs index fe4ad86..801c35c 100644 --- a/crates/util/keyfork-prompt/examples/test-basic-prompt.rs +++ b/crates/util/keyfork-prompt/examples/test-basic-prompt.rs @@ -22,8 +22,8 @@ fn main() -> Result<(), Box> { 3, transport_validator.to_fn(), )?; - assert_eq!(mnemonics[0].entropy().len(), 12); - assert_eq!(mnemonics[1].entropy().len(), 32); + assert_eq!(mnemonics[0].as_bytes().len(), 12); + assert_eq!(mnemonics[1].as_bytes().len(), 32); let mnemonics = mgr.prompt_validated_wordlist( "Enter a 24 and 48-word mnemonic: ", @@ -31,8 +31,8 @@ fn main() -> Result<(), Box> { 3, combine_validator.to_fn(), )?; - assert_eq!(mnemonics[0].entropy().len(), 32); - assert_eq!(mnemonics[1].entropy().len(), 64); + assert_eq!(mnemonics[0].as_bytes().len(), 32); + assert_eq!(mnemonics[1].as_bytes().len(), 64); Ok(()) }