From 3240ab9e1f8848bb340dd62acd89c133f54b2651 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 3 Jan 2024 19:58:39 -0500 Subject: [PATCH] keyfork-shard: decrypt only in `decrypt` command The way this previously runs, the combining operator would be *required* to decrypt a share. This was not ideal for enclaves, where the process should just send out public keys and read in public keys and payloads. This is now resolved. --- .../src/bin/keyfork-shard-remote-openpgp.rs | 39 ++------ keyfork-shard/src/openpgp.rs | 89 ++++++++++++++----- 2 files changed, 70 insertions(+), 58 deletions(-) diff --git a/keyfork-shard/src/bin/keyfork-shard-remote-openpgp.rs b/keyfork-shard/src/bin/keyfork-shard-remote-openpgp.rs index 881fb1e..cc5311f 100644 --- a/keyfork-shard/src/bin/keyfork-shard-remote-openpgp.rs +++ b/keyfork-shard/src/bin/keyfork-shard-remote-openpgp.rs @@ -1,51 +1,22 @@ use std::{ env, - fs::File, - path::{Path, PathBuf}, process::ExitCode, }; -use keyfork_shard::openpgp::{remote_decrypt, discover_certs, openpgp::Cert, parse_messages}; +use keyfork_shard::openpgp::remote_decrypt; 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]"), + match args.as_slice() { + [] => (), + _ => panic!("Usage: {program_name}"), }; - 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(), - )?; + remote_decrypt()?; Ok(()) } diff --git a/keyfork-shard/src/openpgp.rs b/keyfork-shard/src/openpgp.rs index 2c9df57..9ee6bdc 100644 --- a/keyfork-shard/src/openpgp.rs +++ b/keyfork-shard/src/openpgp.rs @@ -7,7 +7,7 @@ use std::{ use aes_gcm::{ aead::{consts::U12, Aead, AeadCore, OsRng}, - Aes256Gcm, KeyInit, Nonce, Error as AesError + Aes256Gcm, Error as AesError, KeyInit, Nonce, }; use keyfork_derive_openpgp::derive_util::{ request::{DerivationAlgorithm, DerivationRequest}, @@ -48,7 +48,15 @@ use x25519_dalek::{EphemeralSecret, PublicKey}; const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_OFFSET: usize = 2; -const ENC_LEN: u8 = 24 * 4; +/// Decrypt hunk version 1: +/// 1 byte: Version +/// 1 byte: Threshold +/// Data: &[u8] +const HUNK_VERSION: u8 = 1; +const HUNK_OFFSET: usize = 2; + +// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding +const ENC_LEN: u8 = 4 * 16; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -411,22 +419,23 @@ pub fn decrypt( 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(PromptMessage::Text(format!("Our words: {our_mnemonic}")))?; let shared_secret = our_key .diffie_hellman(&PublicKey::from(their_key)) .to_bytes(); - 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]; - encrypted_payload[..share.len()].copy_from_slice(&share); + let (mut share, threshold, ..) = decrypt_one(encrypted_messages.to_vec(), &certs, metadata)?; + share.insert(0, HUNK_VERSION); + share.insert(1, threshold); + assert!( + share.len() <= ENC_LEN as usize, + "invalid share length (too long, max {ENC_LEN} bytes)" + ); 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())?; + dbg!(bytes.len()); shared_key.decrypt(their_nonce, &bytes[..])?; // NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX @@ -451,25 +460,23 @@ pub fn decrypt( // safety: size of out_bytes is constant and always % 4 == 0 let mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; - pm.prompt_message(PromptMessage::Text(format!("Our payload: {mnemonic}")))?; + pm.prompt_message(PromptMessage::Text(format!( + "Our words: {our_mnemonic} {mnemonic}" + )))?; Ok(()) } -pub fn remote_decrypt( - certs: &[Cert], - metadata: EncryptedMessage, - encrypted_messages: &[EncryptedMessage], -) -> Result<()> { +pub fn remote_decrypt() -> 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); + let mut iter_count = None; + let mut shares = vec![]; - for _ in 1..threshold { + 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()) }; @@ -480,7 +487,25 @@ pub fn remote_decrypt( "Our words: {nonce_mnemonic} {key_mnemonic}" )))?; - let pubkey_mnemonic = pm.prompt_wordlist("Their words: ", &wordlist)?; + 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"); @@ -490,11 +515,24 @@ pub fn remote_decrypt( 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 payload = + shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?; + assert_eq!(HUNK_VERSION, payload[0], "Incompatible hunk version"); - let decrypted_share = shared_key.decrypt(&nonce, &payload[..payload[payload.len() - 1] as usize])?; - shares.push(decrypted_share); + 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 @@ -506,6 +544,8 @@ pub fn remote_decrypt( .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, @@ -524,6 +564,7 @@ pub fn remote_decrypt( if derived_fp != expected_fp { return Err(Error::InvalidSecret(derived_fp, expected_fp)); } + */ print!("{}", smex::encode(&secret));