diff --git a/Cargo.lock b/Cargo.lock index cb97533..6ca9a25 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.4.7" @@ -341,6 +376,16 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clang-sys" version = "1.6.1" @@ -481,9 +526,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "curve25519-dalek" version = "4.1.0" @@ -783,6 +838,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.28.0" @@ -903,6 +968,15 @@ dependencies = [ "hashbrown 0.14.1", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "insta" version = "1.31.0" @@ -1086,11 +1160,13 @@ dependencies = [ name = "keyfork-shard" version = "0.1.0" dependencies = [ + "aes-gcm", "anyhow", "bincode", "card-backend", "card-backend-pcsc", "keyfork-derive-openpgp", + "keyfork-mnemonic-util", "keyfork-prompt", "openpgp-card", "openpgp-card-sequoia", @@ -1099,6 +1175,7 @@ dependencies = [ "sharks", "smex", "thiserror", + "x25519-dalek", ] [[package]] @@ -1426,6 +1503,12 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + [[package]] name = "openpgp-card" version = "0.4.0" @@ -1624,6 +1707,18 @@ version = "3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4503fa043bf02cee09a9582e9554b4c6403b2ef55e4612e96561d294419429f8" +[[package]] +name = "polyval" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -2440,6 +2535,16 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -2693,6 +2798,18 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "x25519-dalek" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "serde", + "zeroize", +] + [[package]] name = "xxhash-rust" version = "0.8.7" diff --git a/keyfork-shard/Cargo.toml b/keyfork-shard/Cargo.toml index 516311a..4bdcc9b 100644 --- a/keyfork-shard/Cargo.toml +++ b/keyfork-shard/Cargo.toml @@ -26,3 +26,6 @@ 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 = "0.10.3" diff --git a/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs b/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs new file mode 100644 index 0000000..381b8db --- /dev/null +++ b/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs @@ -0,0 +1,128 @@ +use std::{ + env, + fs::File, + io::{stdin, stdout}, + path::{Path, PathBuf}, + process::ExitCode, + str::FromStr, +}; + +use aes_gcm::{aead::Aead, aes::cipher::consts::U12, Aes256Gcm, KeyInit, Nonce}; +use x25519_dalek::{EphemeralSecret, PublicKey}; + +use keyfork_mnemonic_util::{Mnemonic, Wordlist}; +use keyfork_prompt::PromptManager; +use keyfork_shard::openpgp::{decrypt_one, discover_certs, openpgp::Cert, parse_messages}; + +type Result> = std::result::Result; + +fn validate<'a>( + messages_file: impl AsRef, + key_discovery: impl Into>, +) -> Result<(File, Vec)> { + let key_discovery = key_discovery.into().map(PathBuf::from); + key_discovery.as_ref().map(std::fs::metadata).transpose()?; + + // Load certs from path + let certs = key_discovery + .map(discover_certs) + .transpose()? + .unwrap_or(vec![]); + + Ok((File::open(messages_file)?, certs)) +} + +fn run() -> Result<()> { + let mut args = env::args(); + let program_name = args.next().expect("program name"); + let args = args.collect::>(); + let (messages_file, cert_list) = match args.as_slice() { + [messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?, + [messages_file] => validate(messages_file, None)?, + _ => panic!("Usage: {program_name} messages_file [key_discovery]"), + }; + + let mut encrypted_messages = parse_messages(messages_file)?; + + let encrypted_metadata = encrypted_messages + .pop_front() + .expect("any pgp encrypted message"); + + // Receive their key + + let mut pm = PromptManager::new(stdin(), stdout())?; + let wordlist = Wordlist::default(); + let their_words = pm.prompt_wordlist("Their words: ", &wordlist)?; + let mut nonce_words = their_words + .split_whitespace() + .take(9) + .peekable(); + let mut pubkey_words = their_words + .split_whitespace() + .skip(9) + .take(24) + .peekable(); + let mut nonce_mnemonic = String::new(); + let mut pubkey_mnemonic = String::new(); + while let Some(word) = nonce_words.next() { + nonce_mnemonic.push_str(word); + if nonce_words.peek().is_some() { + nonce_mnemonic.push(' '); + } + } + while let Some(word) = pubkey_words.next() { + pubkey_mnemonic.push_str(word); + if pubkey_words.peek().is_some() { + pubkey_mnemonic.push(' '); + } + } + let their_key = Mnemonic::from_str(&pubkey_mnemonic)?.entropy(); + let their_key: [u8; 32] = their_key.try_into().expect("24 words"); + let their_nonce = Mnemonic::from_str(&nonce_mnemonic)?.entropy(); + let their_nonce = Nonce::::from_slice(&their_nonce); + + let our_key = EphemeralSecret::random(); + let our_mnemonic = + Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; + // TODO: Encode using `qrencode -t ansiutf8 -m 2` + pm.prompt_message(&format!("Our words: {our_mnemonic}"))?; + + let shared_secret = our_key + .diffie_hellman(&PublicKey::from(their_key)) + .to_bytes(); + + let share = decrypt_one(encrypted_messages.into(), &cert_list, encrypted_metadata)?; + assert_eq!(share.len(), 65, "non-constant share length"); + const LEN: u8 = 24 * 3; + let mut encrypted_payload = [(LEN - share.len() as u8); LEN as usize]; + encrypted_payload[..share.len()].copy_from_slice(&share); + + let shared_key = Aes256Gcm::new_from_slice(&shared_secret)?; + let bytes = shared_key.encrypt(their_nonce, share.as_slice()).unwrap(); + + const ENC_LEN: u8 = 24 * 4; + let mut out_bytes = [(ENC_LEN - bytes.len() as u8); ENC_LEN as usize]; + assert!(bytes.len() < out_bytes.len(), "encrypted payload larger than acceptable limit"); + out_bytes[..bytes.len()].clone_from_slice(&bytes); + + // safety: size of out_bytes is immutable and always % 32 == 0 + let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; + + pm.prompt_message(&format!("Our payload: {mnemonic}"))?; + + Ok(()) +} + +fn main() -> ExitCode { + let result = run(); + if let Err(e) = result { + eprintln!("Error: {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 +} diff --git a/keyfork-shard/src/openpgp.rs b/keyfork-shard/src/openpgp.rs index be74ba9..020a29c 100644 --- a/keyfork-shard/src/openpgp.rs +++ b/keyfork-shard/src/openpgp.rs @@ -323,15 +323,17 @@ fn decrypt_metadata( }) } -fn decrypt_one( +pub fn decrypt_one( messages: Vec, - keyring: &mut Keyring, - manager: &mut SmartcardManager, + certs: &[Cert], metadata: EncryptedMessage, ) -> Result> { let policy = NullPolicy::new(); - let content = decrypt_metadata(&metadata, &policy, &mut *keyring, &mut *manager)?; + let mut keyring = Keyring::new(certs)?; + let mut manager = SmartcardManager::new()?; + + let content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?; let (_threshold, root_cert, certs) = decode_metadata_v1(&content)?; @@ -341,13 +343,13 @@ fn decrypt_one( let mut messages: HashMap = HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages)); - let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, keyring)?; + let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; if let Some(message) = decrypted_messages.into_values().next() { return Ok(message); } - let decrypted_messages = decrypt_with_manager(1, &mut messages, &certs, policy, manager)?; + let decrypted_messages = decrypt_with_manager(1, &mut messages, &certs, policy, &mut manager)?; if let Some(message) = decrypted_messages.into_values().next() { return Ok(message);