diff --git a/Cargo.lock b/Cargo.lock index 6ca9a25..335eb99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -132,9 +132,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.75" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" +checksum = "080e9890a082662b09c1ad45f567faeeb47f22b5fb23895fbe1e651e718e25ca" [[package]] name = "ascii-canvas" @@ -1162,7 +1162,6 @@ version = "0.1.0" dependencies = [ "aes-gcm", "anyhow", - "bincode", "card-backend", "card-backend-pcsc", "keyfork-derive-openpgp", @@ -1171,7 +1170,6 @@ dependencies = [ "openpgp-card", "openpgp-card-sequoia", "sequoia-openpgp", - "serde", "sharks", "smex", "thiserror", diff --git a/keyfork-shard/Cargo.toml b/keyfork-shard/Cargo.toml index 5181296..36bab88 100644 --- a/keyfork-shard/Cargo.toml +++ b/keyfork-shard/Cargo.toml @@ -8,24 +8,26 @@ license = "AGPL-3.0-only" [features] default = ["openpgp", "openpgp-card"] -openpgp = ["sequoia-openpgp", "prompt"] +openpgp = ["sequoia-openpgp", "prompt", "anyhow"] openpgp-card = ["openpgp-card-sequoia", "card-backend-pcsc", "card-backend", "dep:openpgp-card"] prompt = ["keyfork-prompt"] [dependencies] -anyhow = "1.0.75" -bincode = "1.3.3" +sharks = "0.5.0" +smex = { version = "0.1.0", path = "../smex" } +thiserror = "1.0.50" + +# Remote operator mode +keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" } +x25519-dalek = { version = "2.0.0", features = ["getrandom"] } +aes-gcm = { version = "0.10.3", features = ["std"] } + +# OpenPGP +anyhow = { version = "1.0.79", optional = true } card-backend = { version = "0.2.0", optional = true } card-backend-pcsc = { version = "0.5.0", optional = true } keyfork-derive-openpgp = { version = "0.1.0", path = "../keyfork-derive-openpgp" } openpgp-card-sequoia = { version = "0.2.0", optional = true } openpgp-card = { version = "0.4.0", optional = true } sequoia-openpgp = { version = "1.16.1", optional = true } -serde = "1.0.188" -sharks = "0.5.0" -smex = { version = "0.1.0", path = "../smex" } -thiserror = "1.0.50" keyfork-prompt = { version = "0.1.0", path = "../keyfork-prompt", optional = true } -x25519-dalek = { version = "2.0.0", features = ["getrandom"] } -keyfork-mnemonic-util = { version = "0.1.0", path = "../keyfork-mnemonic-util" } -aes-gcm = { version = "0.10.3", features = ["std"] } diff --git a/keyfork-shard/src/bin/keyfork-shard-remote-openpgp.rs b/keyfork-shard/src/bin/keyfork-shard-remote.rs similarity index 94% rename from keyfork-shard/src/bin/keyfork-shard-remote-openpgp.rs rename to keyfork-shard/src/bin/keyfork-shard-remote.rs index cc5311f..d03cf9d 100644 --- a/keyfork-shard/src/bin/keyfork-shard-remote-openpgp.rs +++ b/keyfork-shard/src/bin/keyfork-shard-remote.rs @@ -3,7 +3,7 @@ use std::{ process::ExitCode, }; -use keyfork_shard::openpgp::remote_decrypt; +use keyfork_shard::remote_decrypt; type Result> = std::result::Result; diff --git a/keyfork-shard/src/lib.rs b/keyfork-shard/src/lib.rs index 9c60f1d..31fcb87 100644 --- a/keyfork-shard/src/lib.rs +++ b/keyfork-shard/src/lib.rs @@ -1,2 +1,127 @@ +use std::{ + io::{stdin, stdout}, + str::FromStr, +}; + +use aes_gcm::{ + aead::{Aead, AeadCore, OsRng}, + Aes256Gcm, KeyInit, +}; +use keyfork_mnemonic_util::{Mnemonic, Wordlist}; +use keyfork_prompt::{Message as PromptMessage, PromptManager}; +use sharks::{Share, Sharks}; +use x25519_dalek::{EphemeralSecret, PublicKey}; + #[cfg(feature = "openpgp")] pub mod openpgp; + +/// Decrypt hunk version 1: +/// 1 byte: Version +/// 1 byte: Threshold +/// Data: &[u8] +pub(crate) const HUNK_VERSION: u8 = 1; +pub(crate) const HUNK_OFFSET: usize = 2; + +pub fn remote_decrypt() -> Result<(), Box> { + let mut pm = PromptManager::new(stdin(), stdout())?; + let wordlist = Wordlist::default(); + + let mut iter_count = None; + let mut shares = vec![]; + + let mut threshold = 0; + + while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) { + let nonce = Aes256Gcm::generate_nonce(&mut OsRng); + let nonce_mnemonic = + unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) }; + let our_key = EphemeralSecret::random(); + let key_mnemonic = + Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; + pm.prompt_message(PromptMessage::Text(format!( + "Our words: {nonce_mnemonic} {key_mnemonic}" + )))?; + + let their_words = pm.prompt_wordlist("Their words: ", &wordlist)?; + + let mut pubkey_words = their_words.split_whitespace().take(24).peekable(); + let mut payload_words = their_words.split_whitespace().skip(24).take(48).peekable(); + let mut pubkey_mnemonic = String::new(); + let mut payload_mnemonic = String::new(); + while let Some(word) = pubkey_words.next() { + pubkey_mnemonic.push_str(word); + if pubkey_words.peek().is_some() { + pubkey_mnemonic.push(' '); + } + } + while let Some(word) = payload_words.next() { + payload_mnemonic.push_str(word); + if payload_words.peek().is_some() { + payload_mnemonic.push(' '); + } + } + + let their_key = Mnemonic::from_str(&pubkey_mnemonic)?.entropy(); + let their_key: [u8; 32] = their_key.try_into().expect("24 words"); + + let shared_secret = our_key + .diffie_hellman(&PublicKey::from(their_key)) + .to_bytes(); + let shared_key = + Aes256Gcm::new_from_slice(&shared_secret).expect("Invalid length of constant key size"); + + let payload = Mnemonic::from_str(&payload_mnemonic)?.entropy(); + let payload = + shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?; + assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version"); + + match &mut iter_count { + Some(n) => { + // Must be > 0 to start loop, can't go lower + *n -= 1; + } + None => { + // NOTE: Should always be >= 1, < 256 due to Shamir constraints + threshold = payload[1]; + let _ = iter_count.insert(threshold - 1); + } + } + + shares.push(payload[HUNK_OFFSET..].to_vec()); + } + + let shares = shares + .into_iter() + .map(|s| Share::try_from(s.as_slice())) + .collect::, &str>>() + .map_err(|e| anyhow::anyhow!("{e}"))?; + let secret = Sharks(threshold) + .recover(&shares) + .map_err(|e| anyhow::anyhow!("{e}"))?; + + /* + * Verification would take up too much size, mnemonic would be very large + let userid = UserID::from("keyfork-sss"); + let kdr = DerivationRequest::new( + DerivationAlgorithm::Ed25519, + &DerivationPath::from_str("m/7366512'/0'")?, + ) + .derive_with_master_seed(secret.to_vec())?; + let derived_cert = keyfork_derive_openpgp::derive( + kdr, + &[KeyFlags::empty().set_certification().set_signing()], + userid, + )?; + + // NOTE: Signatures on certs will be different. Compare fingerprints instead. + let derived_fp = derived_cert.fingerprint(); + let expected_fp = root_cert.fingerprint(); + if derived_fp != expected_fp { + return Err(Error::InvalidSecret(derived_fp, expected_fp)); + } + */ + + print!("{}", smex::encode(&secret)); + + Ok(()) +} diff --git a/keyfork-shard/src/openpgp.rs b/keyfork-shard/src/openpgp.rs index 9ee6bdc..4cb37e3 100644 --- a/keyfork-shard/src/openpgp.rs +++ b/keyfork-shard/src/openpgp.rs @@ -6,7 +6,7 @@ use std::{ }; use aes_gcm::{ - aead::{consts::U12, Aead, AeadCore, OsRng}, + aead::{consts::U12, Aead}, Aes256Gcm, Error as AesError, KeyInit, Nonce, }; use keyfork_derive_openpgp::derive_util::{ @@ -33,13 +33,13 @@ use openpgp::{ }; pub use sequoia_openpgp as openpgp; use sharks::{Share, Sharks}; +use x25519_dalek::{EphemeralSecret, PublicKey}; mod keyring; use keyring::Keyring; mod smartcard; use smartcard::SmartcardManager; -use x25519_dalek::{EphemeralSecret, PublicKey}; /// Shard metadata verson 1: /// 1 byte: Version @@ -48,12 +48,7 @@ use x25519_dalek::{EphemeralSecret, PublicKey}; const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_OFFSET: usize = 2; -/// Decrypt hunk version 1: -/// 1 byte: Version -/// 1 byte: Threshold -/// Data: &[u8] -const HUNK_VERSION: u8 = 1; -const HUNK_OFFSET: usize = 2; +use super::HUNK_VERSION; // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding const ENC_LEN: u8 = 4 * 16; @@ -467,110 +462,6 @@ pub fn decrypt( Ok(()) } -pub fn remote_decrypt() -> Result<()> { - let mut pm = PromptManager::new(stdin(), stdout())?; - let wordlist = Wordlist::default(); - - let mut iter_count = None; - let mut shares = vec![]; - - let mut threshold = 0; - - while iter_count.is_none() || iter_count.is_some_and(|i| i > 0) { - let nonce = Aes256Gcm::generate_nonce(&mut OsRng); - let nonce_mnemonic = - unsafe { Mnemonic::from_raw_entropy(nonce.as_slice(), Default::default()) }; - let our_key = EphemeralSecret::random(); - let key_mnemonic = - Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; - pm.prompt_message(PromptMessage::Text(format!( - "Our words: {nonce_mnemonic} {key_mnemonic}" - )))?; - - let their_words = pm.prompt_wordlist("Their words: ", &wordlist)?; - - let mut pubkey_words = their_words.split_whitespace().take(24).peekable(); - let mut payload_words = their_words.split_whitespace().skip(24).take(48).peekable(); - let mut pubkey_mnemonic = String::new(); - let mut payload_mnemonic = String::new(); - while let Some(word) = pubkey_words.next() { - pubkey_mnemonic.push_str(word); - if pubkey_words.peek().is_some() { - pubkey_mnemonic.push(' '); - } - } - while let Some(word) = payload_words.next() { - payload_mnemonic.push_str(word); - if payload_words.peek().is_some() { - payload_mnemonic.push(' '); - } - } - - let their_key = Mnemonic::from_str(&pubkey_mnemonic)?.entropy(); - let their_key: [u8; 32] = their_key.try_into().expect("24 words"); - - let shared_secret = our_key - .diffie_hellman(&PublicKey::from(their_key)) - .to_bytes(); - let shared_key = - Aes256Gcm::new_from_slice(&shared_secret).expect("Invalid length of constant key size"); - - let payload = Mnemonic::from_str(&payload_mnemonic)?.entropy(); - let payload = - shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?; - assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version"); - - match &mut iter_count { - Some(n) => { - // Must be > 0 to start loop, can't go lower - *n -= 1; - } - None => { - // NOTE: Should always be >= 1, < 256 due to Shamir constraints - threshold = payload[1]; - let _ = iter_count.insert(threshold - 1); - } - } - - shares.push(payload[HUNK_OFFSET..].to_vec()); - } - - let shares = shares - .into_iter() - .map(|s| Share::try_from(s.as_slice())) - .collect::, &str>>() - .map_err(|e| Error::Share(e.to_string()))?; - let secret = Sharks(threshold) - .recover(&shares) - .map_err(|e| Error::CombineShares(e.to_string()))?; - - /* - * Verification would take up too much size, mnemonic would be very large - let userid = UserID::from("keyfork-sss"); - let kdr = DerivationRequest::new( - DerivationAlgorithm::Ed25519, - &DerivationPath::from_str("m/7366512'/0'")?, - ) - .derive_with_master_seed(secret.to_vec())?; - let derived_cert = keyfork_derive_openpgp::derive( - kdr, - &[KeyFlags::empty().set_certification().set_signing()], - userid, - )?; - - // NOTE: Signatures on certs will be different. Compare fingerprints instead. - let derived_fp = derived_cert.fingerprint(); - let expected_fp = root_cert.fingerprint(); - if derived_fp != expected_fp { - return Err(Error::InvalidSecret(derived_fp, expected_fp)); - } - */ - - print!("{}", smex::encode(&secret)); - - Ok(()) -} - pub fn combine( certs: Vec, metadata: EncryptedMessage,