diff --git a/keyfork-shard/src/openpgp.rs b/keyfork-shard/src/openpgp.rs index 42697ca..7160e26 100644 --- a/keyfork-shard/src/openpgp.rs +++ b/keyfork-shard/src/openpgp.rs @@ -205,6 +205,110 @@ fn get_decryption_keys<'a>( .secret() } +fn decode_metadata_v1(buf: &[u8]) -> Result<(u8, Cert, Vec)> { + assert_eq!( + SHARD_METADATA_VERSION, buf[0], + "Incompatible metadata version" + ); + let threshold = buf[1]; + + let mut cert_parser = + CertParser::from_bytes(&buf[SHARD_METADATA_OFFSET..]).map_err(Error::Sequoia)?; + + let root_cert = match cert_parser.next() { + Some(Ok(c)) => c, + Some(Err(e)) => return Err(Error::Sequoia(e)), + None => panic!("No data found"), + }; + + let certs = cert_parser + .collect::>>() + .map_err(Error::Sequoia)?; + + Ok((threshold, root_cert, certs)) +} + +// NOTE: When using single-decryptor mechanism, use this method with `threshold = 1` to return a +// single message. +fn decrypt_with_manager( + threshold: u8, + certs: &[Cert], + messages: &mut HashMap, + policy: NullPolicy, + manager: &mut SmartcardManager, +) -> Result>> { + let mut decrypted_messages = HashMap::new(); + + while threshold as usize - decrypted_messages.len() > 0 { + // Build list of fingerprints that haven't yet been used for decrypting + let mut cert_by_fingerprint = HashMap::new(); + let mut unused_fingerprints = vec![]; + for valid_cert in certs + .iter() + .filter(|cert| !decrypted_messages.contains_key(&cert.keyid())) + .map(|cert| cert.with_policy(&policy, None)) + { + let valid_cert = valid_cert.map_err(Error::Sequoia)?; + let fp = valid_cert + .keys() + .for_storage_encryption() + .map(|k| k.fingerprint()) + .collect::>(); + for fp in &fp { + cert_by_fingerprint.insert(fp.clone(), valid_cert.keyid()); + } + unused_fingerprints.extend(fp.into_iter()); + } + + // Iterate over all fingerprints and use key_by_fingerprints to assoc with Enc. Message + if let Some(fp) = manager.load_any_fingerprint(unused_fingerprints)? { + let cert_keyid = cert_by_fingerprint.get(&fp).unwrap().clone(); + if let Some(message) = messages.remove(&cert_keyid) { + let message = message.decrypt_with(&policy, &mut *manager)?; + decrypted_messages.insert(cert_keyid, message); + } + } + } + + Ok(decrypted_messages) +} + +// NOTE: When using single-decryptor mechanism, only a single key should be provided in Keyring to +// decrypt messages with. +fn decrypt_with_keyring( + certs: &[Cert], + policy: &NullPolicy, + keyring: &mut Keyring, + messages: &mut HashMap, +) -> Result>, Error> { + let mut decrypted_messages = HashMap::new(); + + for valid_cert in certs.iter().map(|cert| cert.with_policy(policy, None)) { + let valid_cert = valid_cert.map_err(Error::Sequoia)?; + let Some(secret_cert) = keyring.get_cert_for_primary_keyid(&valid_cert.keyid()) else { + continue; + }; + let secret_cert = secret_cert + .with_policy(policy, None) + .map_err(Error::Sequoia)?; + let keys = get_decryption_keys(&secret_cert).collect::>(); + if !keys.is_empty() { + if let Some(message) = messages.get_mut(&valid_cert.keyid()) { + for (pkesk, key) in message.pkesks.iter_mut().zip(keys) { + pkesk.set_recipient(key.keyid()); + } + // we have a pkesk, decrypt via keyring + decrypted_messages.insert( + valid_cert.keyid(), + message.decrypt_with(policy, &mut *keyring)?, + ); + } + } + } + + Ok(decrypted_messages) +} + pub fn combine( certs: Vec, metadata: EncryptedMessage, @@ -220,103 +324,41 @@ pub fn combine( let content = if keyring.is_empty() { // NOTE: Any card plugged in that can't decrypt, will raise issues. // This should not be used on a system where OpenPGP cards are available that shouldn't be - // used, due to the nature of how wildcard decryption works. + // used. The manager will try every wildcard packet for the card on the system, and once no + // matching PKESK packet has been found, will return an error. manager.load_any_card()?; metadata.decrypt_with(&policy, &mut manager)? } else { metadata.decrypt_with(&policy, &mut keyring)? }; - assert_eq!( - SHARD_METADATA_VERSION, content[0], - "incompatible metadata version" - ); - let threshold = content[1]; + let (threshold, root_cert, certs) = decode_metadata_v1(&content)?; - let mut cert_parser = - CertParser::from_bytes(&content[SHARD_METADATA_OFFSET..]).map_err(Error::Sequoia)?; - let root_cert = match cert_parser.next() { - Some(Ok(c)) => c, - Some(Err(e)) => panic!("Could not find root (first) certificate: {e}"), - None => panic!("No certs found in cert parser"), - }; - let certs = cert_parser - .collect::>>() - .map_err(Error::Sequoia)?; keyring.set_root_cert(root_cert.clone()); manager.set_root_cert(root_cert); + + // Generate a controlled binding from certificates to encrypted messages. This is stable + // because we control the order packets are encrypted and certificates are stored. + let mut messages: HashMap = HashMap::from_iter(certs.iter().map(|c| c.keyid()).zip(messages)); - let mut decrypted_messages: HashMap> = HashMap::new(); - // NOTE: This is ONLY stable because we control the generation of PKESK packets and - // encode the policy to ourselves. - for valid_cert in certs.iter().map(|cert| cert.with_policy(&policy, None)) { - let valid_cert = valid_cert.map_err(Error::Sequoia)?; - // get keys from keyring for cert - let Some(secret_cert) = keyring.get_cert_for_primary_keyid(&valid_cert.keyid()) else { - continue; - }; - let secret_cert = secret_cert - .with_policy(&policy, None) - .map_err(Error::Sequoia)?; - let keys = get_decryption_keys(&secret_cert).collect::>(); - if !keys.is_empty() { - if let Some(message) = messages.get_mut(&valid_cert.keyid()) { - for (pkesk, key) in message.pkesks.iter_mut().zip(keys) { - pkesk.set_recipient(key.keyid()); - } - // we have a pkesk, decrypt via keyring - decrypted_messages.insert( - valid_cert.keyid(), - message.decrypt_with(&policy, &mut keyring)?, - ); - } - } - } + let mut decrypted_messages = + decrypt_with_keyring(&certs, &policy, &mut keyring, &mut messages)?; // clean decrypted messages from encrypted messages messages.retain(|k, _v| !decrypted_messages.contains_key(k)); let left_from_threshold = threshold as usize - decrypted_messages.len(); if left_from_threshold > 0 { - let mut remaining_usable_certs = certs - .iter() - .filter(|cert| messages.contains_key(&cert.keyid())) - .collect::>(); - - while threshold as usize - decrypted_messages.len() > 0 { - remaining_usable_certs.retain(|cert| messages.contains_key(&cert.keyid())); - let mut key_by_fingerprints = HashMap::new(); - let mut total_fingerprints = vec![]; - for valid_cert in remaining_usable_certs - .iter() - .map(|cert| cert.with_policy(&policy, None)) - { - let valid_cert = valid_cert.map_err(Error::Sequoia)?; - let fp = valid_cert - .keys() - .for_storage_encryption() - .map(|k| k.fingerprint()) - .collect::>(); - for fp in &fp { - key_by_fingerprints.insert(fp.clone(), valid_cert.keyid()); - } - total_fingerprints.extend(fp.iter().cloned()); - } - - // Iterate over all fingerprints and use key_by_fingerprints to assoc with Enc. Message - if let Some(fp) = manager.load_any_fingerprint(total_fingerprints)? { - // soundness: `key_by_fingerprints` is extended by the same fps that are then - // inserted into `total_fingerprints` - let cert_keyid = key_by_fingerprints.get(&fp).unwrap().clone(); - let message = messages.remove(&cert_keyid); - if let Some(message) = message { - let message = message.decrypt_with(&policy, &mut manager)?; - decrypted_messages.insert(cert_keyid, message); - } - } - } + let new_messages = decrypt_with_manager( + left_from_threshold as u8, + &certs, + &mut messages, + policy, + &mut manager, + )?; + decrypted_messages.extend(new_messages.into_iter()); } let shares = decrypted_messages