diff --git a/keyfork-shard/Cargo.toml b/keyfork-shard/Cargo.toml index 4bdcc9b..5181296 100644 --- a/keyfork-shard/Cargo.toml +++ b/keyfork-shard/Cargo.toml @@ -28,4 +28,4 @@ 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" +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-openpgp.rs new file mode 100644 index 0000000..881fb1e --- /dev/null +++ b/keyfork-shard/src/bin/keyfork-shard-remote-openpgp.rs @@ -0,0 +1,65 @@ +use std::{ + env, + fs::File, + path::{Path, PathBuf}, + process::ExitCode, +}; + +use keyfork_shard::openpgp::{remote_decrypt, 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"); + + remote_decrypt( + &cert_list, + encrypted_metadata, + encrypted_messages.make_contiguous(), + )?; + + 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 dea5ade..2c9df57 100644 --- a/keyfork-shard/src/openpgp.rs +++ b/keyfork-shard/src/openpgp.rs @@ -6,8 +6,8 @@ use std::{ }; use aes_gcm::{ - aead::{consts::U12, Aead}, - Aes256Gcm, KeyInit, Nonce, + aead::{consts::U12, Aead, AeadCore, OsRng}, + Aes256Gcm, KeyInit, Nonce, Error as AesError }; use keyfork_derive_openpgp::derive_util::{ request::{DerivationAlgorithm, DerivationRequest}, @@ -48,11 +48,16 @@ use x25519_dalek::{EphemeralSecret, PublicKey}; const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_OFFSET: usize = 2; +const ENC_LEN: u8 = 24 * 4; + #[derive(Debug, thiserror::Error)] pub enum Error { #[error("Error with creating Share: {0}")] Share(String), + #[error("Error decrypting share: {0}")] + SymDecryptShare(#[from] AesError), + #[error("Error combining shares: {0}")] CombineShares(String), @@ -343,7 +348,7 @@ fn decrypt_one( messages: Vec, certs: &[Cert], metadata: EncryptedMessage, -) -> Result> { +) -> Result<(Vec, u8, Cert)> { let policy = NullPolicy::new(); let mut keyring = Keyring::new(certs)?; @@ -351,10 +356,10 @@ fn decrypt_one( let content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?; - let (_threshold, root_cert, certs) = decode_metadata_v1(&content)?; + let (threshold, root_cert, certs) = decode_metadata_v1(&content)?; keyring.set_root_cert(root_cert.clone()); - manager.set_root_cert(root_cert); + manager.set_root_cert(root_cert.clone()); let mut messages: HashMap = HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages)); @@ -362,13 +367,13 @@ fn decrypt_one( let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; if let Some(message) = decrypted_messages.into_values().next() { - return Ok(message); + return Ok((message, threshold, root_cert)); } 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); + return Ok((message, threshold, root_cert)); } unreachable!("smartcard manager should always decrypt") @@ -413,7 +418,7 @@ pub fn decrypt( .diffie_hellman(&PublicKey::from(their_key)) .to_bytes(); - let share = decrypt_one(encrypted_messages.to_vec(), &certs, metadata)?; + let (share, ..) = decrypt_one(encrypted_messages.to_vec(), &certs, metadata)?; assert!(share.len() <= 65, "invalid share length (too long)"); const LEN: u8 = 24 * 3; let mut encrypted_payload = [(LEN - share.len() as u8); LEN as usize]; @@ -421,16 +426,27 @@ pub fn decrypt( let shared_key = Aes256Gcm::new_from_slice(&shared_secret).expect("Invalid length of constant key size"); - let bytes = shared_key.encrypt(their_nonce, share.as_slice()).unwrap(); + let bytes = shared_key.encrypt(their_nonce, share.as_slice())?; + shared_key.decrypt(their_nonce, &bytes[..])?; - // NOTE: Padding length is less than u8::MAX because 24 * 4 < u8::MAX - const ENC_LEN: u8 = 24 * 4; - let mut out_bytes = [(ENC_LEN - bytes.len() as u8); ENC_LEN as usize]; + // NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX + // NOTE: This previously used a single 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 done below by having sequentially increasing numbers up to but not + // including the last byte. + assert!(ENC_LEN < u8::MAX, "padding byte can be u8"); + let mut out_bytes = [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); + for (i, byte) in (&mut out_bytes[bytes.len()..(ENC_LEN as usize - 1)]) + .into_iter() + .enumerate() + { + *byte = (i % u8::MAX as usize) as u8; + } // safety: size of out_bytes is constant and always % 4 == 0 let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; @@ -440,6 +456,80 @@ pub fn decrypt( Ok(()) } +pub fn remote_decrypt( + certs: &[Cert], + metadata: EncryptedMessage, + encrypted_messages: &[EncryptedMessage], +) -> Result<()> { + let mut pm = PromptManager::new(stdin(), stdout())?; + let wordlist = Wordlist::default(); + + // Get our cert so we know our metadata + let (share, threshold, root_cert) = decrypt_one(encrypted_messages.to_vec(), &certs, metadata)?; + let mut shares = Vec::with_capacity(threshold as usize); + shares.push(share); + + for _ in 1..threshold { + 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 pubkey_mnemonic = pm.prompt_wordlist("Their words: ", &wordlist)?; + 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 = pm.prompt_wordlist("Their payload: ", &wordlist)?; + let payload = Mnemonic::from_str(&payload_mnemonic)?.entropy(); + + let decrypted_share = shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?; + shares.push(decrypted_share); + } + + 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()))?; + + 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, @@ -457,7 +547,7 @@ pub fn combine( let (threshold, root_cert, certs) = decode_metadata_v1(&content)?; keyring.set_root_cert(root_cert.clone()); - manager.set_root_cert(root_cert); + 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. @@ -492,6 +582,7 @@ pub fn combine( .recover(&shares) .map_err(|e| Error::CombineShares(e.to_string()))?; + // TODO: extract as function let userid = UserID::from("keyfork-sss"); let kdr = DerivationRequest::new( DerivationAlgorithm::Ed25519, @@ -506,10 +597,7 @@ pub fn combine( // NOTE: Signatures on certs will be different. Compare fingerprints instead. let derived_fp = derived_cert.fingerprint(); - let expected_fp = keyring - .root_cert() - .expect("cert was previously set") - .fingerprint(); + let expected_fp = root_cert.fingerprint(); if derived_fp != expected_fp { return Err(Error::InvalidSecret(derived_fp, expected_fp)); }