//! OpenPGP Shard functionality. use std::{ collections::{HashMap, VecDeque}, io::{stdin, stdout, Read, Write}, path::Path, str::FromStr, }; use aes_gcm::{ aead::{consts::U12, Aead}, aes::cipher::InvalidLength, Aes256Gcm, Error as AesError, KeyInit, Nonce, }; use hkdf::{Hkdf, InvalidLength as HkdfInvalidLength}; use keyfork_derive_openpgp::derive_util::{ request::{DerivationAlgorithm, DerivationRequest}, DerivationPath, }; use keyfork_mnemonic_util::{Mnemonic, MnemonicFromStrError, MnemonicGenerationError, Wordlist}; use keyfork_prompt::{ validators::{mnemonic::MnemonicSetValidator, Validator}, Error as PromptError, Message as PromptMessage, PromptHandler, Terminal, }; use openpgp::{ armor::{Kind, Writer}, cert::{Cert, CertParser, ValidCert}, packet::{Packet, Tag, UserID, PKESK, SEIP}, parse::{ stream::{DecryptionHelper, DecryptorBuilder, VerificationHelper}, Parse, }, policy::{NullPolicy, Policy, StandardPolicy}, serialize::{ stream::{ArbitraryWriter, Encryptor2, LiteralWriter, Message, Recipient, Signer}, Marshal, }, types::KeyFlags, Fingerprint, KeyID, PacketPile, }; pub use sequoia_openpgp as openpgp; use sha2::Sha256; use sharks::{Share, Sharks}; use x25519_dalek::{EphemeralSecret, PublicKey}; mod keyring; use keyring::Keyring; mod smartcard; use smartcard::SmartcardManager; /// Shard metadata verson 1: /// 1 byte: Version /// 1 byte: Threshold /// Packet Pile of Certs const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_OFFSET: usize = 2; use super::{InvalidData, SharksError, HUNK_VERSION}; // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding const ENC_LEN: u8 = 4 * 16; /// Errors encountered while performing operations using OpenPGP. #[derive(Debug, thiserror::Error)] pub enum Error { /// Errors encountered while creating or combining shares. #[error("{0}")] Sharks(#[from] SharksError), /// Unable to decrypt a share. #[error("Error decrypting share: {0}")] SymDecryptShare(#[from] AesError), /// The generated AES key is of an invalid length. #[error("Invalid length of AES key: {0}")] AesLength(#[from] InvalidLength), /// The HKDF function was given an input of an invalid length. #[error("Invalid KDF length: {0}")] HkdfLength(#[from] HkdfInvalidLength), /// The secret did not match the previously-known secret fingerprint. #[error("Derived secret hash {0} != expected {1}")] InvalidSecret(Fingerprint, Fingerprint), /// An error occurred while performing an OpenPGP operation. #[error("OpenPGP error: {0}")] Sequoia(#[source] anyhow::Error), /// An IO error occurred while performing an OpenPGP operation. #[error("OpenPGP IO error: {0}")] SequoiaIo(#[source] std::io::Error), /// An error occurred while using a keyring. #[error("Keyring error: {0}")] Keyring(#[from] keyring::Error), /// An error occurred while using a smartcard. #[error("Smartcard error: {0}")] Smartcard(#[from] smartcard::Error), /// An error occurred while displaying a prompt. #[error("Prompt error: {0}")] Prompt(#[from] PromptError), /// An error occurred while generating a mnemonic. #[error("Mnemonic generation error: {0}")] MnemonicGeneration(#[from] MnemonicGenerationError), /// An error occurred while parsing a mnemonic. #[error("Mnemonic parse error: {0}")] MnemonicFromStr(#[from] MnemonicFromStrError), /// An error occurred while converting mnemonic data. #[error("{0}")] InvalidMnemonicData(#[from] InvalidData), /// An IO error occurred. #[error("IO error: {0}")] Io(#[source] std::io::Error), /// An error occurred while parsing a derivation path. #[error("Derivation path: {0}")] DerivationPath(#[from] keyfork_derive_openpgp::derive_util::path::Error), /// An error occurred while requesting derivation. #[error("Derivation request: {0}")] DerivationRequest(#[from] keyfork_derive_openpgp::derive_util::request::DerivationError), /// An error occurred while decoding hex. #[error("Unable to decode hex: {0}")] HexDecode(#[from] smex::DecodeError), /// An error occurred while creating an OpenPGP cert. #[error("Keyfork OpenPGP: {0}")] KeyforkOpenPGP(#[from] keyfork_derive_openpgp::Error), } #[allow(missing_docs)] pub type Result = std::result::Result; /// An OpenPGP encrypted message and public-key-encrypted-secret-key packets. #[derive(Debug, Clone)] pub struct EncryptedMessage { pkesks: Vec, message: SEIP, } impl EncryptedMessage { /// Create a new EncryptedMessage from known parts. pub fn new(pkesks: &mut Vec, seip: SEIP) -> Self { Self { pkesks: std::mem::take(pkesks), message: seip, } } /// Decrypt the message with a Sequoia policy and decryptor. /// /// This method creates a container containing the packets and passes the serialized container /// to a DecryptorBuilder, which is used to decrypt the message. /// /// # Errors /// The method may return an error if it is unable to rebuild the message to decrypt or if it /// is unable to decrypt the message. pub fn decrypt_with(&self, policy: &'_ dyn Policy, decryptor: H) -> Result> where H: VerificationHelper + DecryptionHelper, { let mut packets = vec![]; for pkesk in &self.pkesks { let mut packet = vec![]; pkesk.serialize(&mut packet).map_err(Error::Sequoia)?; let message = Message::new(&mut packets); let mut message = ArbitraryWriter::new(message, Tag::PKESK).map_err(Error::Sequoia)?; message.write_all(&packet).map_err(Error::SequoiaIo)?; message.finalize().map_err(Error::Sequoia)?; } let mut packet = vec![]; self.message .serialize(&mut packet) .map_err(Error::Sequoia)?; let message = Message::new(&mut packets); let mut message = ArbitraryWriter::new(message, Tag::SEIP).map_err(Error::Sequoia)?; message.write_all(&packet).map_err(Error::SequoiaIo)?; message.finalize().map_err(Error::Sequoia)?; let mut decryptor = DecryptorBuilder::from_bytes(&packets) .map_err(Error::Sequoia)? .with_policy(policy, None, decryptor) .map_err(Error::Sequoia)?; let mut content = vec![]; decryptor .read_to_end(&mut content) .map_err(Error::SequoiaIo)?; Ok(content) } } /// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read /// from a file, or from files one level deep in a directory. /// /// # Errors /// The function may return an error if it is unable to read the directory or if Sequoia is unable /// to load certificates from the file. pub fn discover_certs(path: impl AsRef) -> Result> { let path = path.as_ref(); if path.is_file() { let mut vec = vec![]; for cert in CertParser::from_file(path).map_err(Error::Sequoia)? { vec.push(cert.map_err(Error::Sequoia)?); } Ok(vec) } else { let mut vec = vec![]; for entry in path .read_dir() .map_err(Error::Io)? .filter_map(Result::ok) .filter(|p| p.path().is_file()) { vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?); } Ok(vec) } } /// Parse messages from a type implementing [`Read`] and store them as [`EncryptedMessage`]. /// /// # Errors /// The function may return an error if the reader has run out of data or if the data is not /// properly formatted OpenPGP messages. /// /// # Panics /// When given packets that are not a list of PKESK packets and SEIP packets, the function panics. /// The `split` utility should never give packets that are not in this format. pub fn parse_messages(reader: impl Read + Send + Sync) -> Result> { let mut pkesks = Vec::new(); let mut encrypted_messages = VecDeque::new(); for packet in PacketPile::from_reader(reader) .map_err(Error::Sequoia)? .into_children() { match packet { Packet::PKESK(p) => pkesks.push(p), Packet::SEIP(s) => { encrypted_messages.push_back(EncryptedMessage::new(&mut pkesks, s)); } s => { panic!("Invalid variant found: {}", s.tag()); } } } Ok(encrypted_messages) } fn get_encryption_keys<'a>( cert: &'a ValidCert, ) -> openpgp::cert::prelude::ValidKeyAmalgamationIter< 'a, openpgp::packet::key::PublicParts, openpgp::packet::key::UnspecifiedRole, > { cert.keys() .alive() .revoked(false) .supported() .for_storage_encryption() } fn get_decryption_keys<'a>( cert: &'a ValidCert, ) -> openpgp::cert::prelude::ValidKeyAmalgamationIter< 'a, openpgp::packet::key::SecretParts, openpgp::packet::key::UnspecifiedRole, > { cert.keys() /* .alive() .revoked(false) .supported() */ .for_storage_encryption() .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, messages: &mut HashMap, certs: &[Cert], policy: &dyn Policy, 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( messages: &mut HashMap, certs: &[Cert], policy: &NullPolicy, keyring: &mut Keyring, ) -> 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) } fn decrypt_metadata( message: &EncryptedMessage, policy: &NullPolicy, keyring: &mut Keyring, manager: &mut SmartcardManager, ) -> Result> { Ok(if keyring.is_empty() { manager.load_any_card()?; message.decrypt_with(policy, manager)? } else { message.decrypt_with(policy, keyring)? }) } fn decrypt_one( messages: Vec, certs: &[Cert], metadata: &EncryptedMessage, ) -> Result<(Vec, u8, Cert)> { let policy = NullPolicy::new(); 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)?; keyring.set_root_cert(root_cert.clone()); manager.set_root_cert(root_cert.clone()); let mut messages: HashMap = certs.iter().map(Cert::keyid).zip(messages).collect(); let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; if let Some(message) = decrypted_messages.into_values().next() { 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, threshold, root_cert)); } unreachable!("smartcard manager should always decrypt") } /// Decrypt a single shard, encrypt to a remote operator, and present the transport shard as a QR /// code and mnemonic to be sent to the remote operator. /// /// # Errors /// /// The function may error if an error occurs while displaying a prompt or while decrypting the /// shard. An error will not be returned if the camera has a hardware error while scanning a QR /// code; instead, a mnemonic prompt will be used. /// /// # Panics /// /// The function may panic if a share is decrypted but has a length larger than 256 bits. This is /// atypical usage and should not be encountered in normal usage, unless something that is not a /// Keyfork seed has been fed into [`split`]. pub fn decrypt( certs: &[Cert], metadata: &EncryptedMessage, encrypted_messages: &[EncryptedMessage], ) -> Result<()> { let mut pm = Terminal::new(stdin(), stdout())?; let wordlist = Wordlist::default(); let mut nonce_data: Option<[u8; 12]> = None; let mut pubkey_data: Option<[u8; 32]> = None; #[cfg(feature = "qrcode")] { pm.prompt_message(PromptMessage::Text( "Press enter, then present QR code to camera".to_string(), ))?; if let Ok(Some(hex)) = keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0) { let decoded_data = smex::decode(&hex)?; let _ = nonce_data.insert(decoded_data[..12].try_into().map_err(|_| InvalidData)?); let _ = pubkey_data.insert(decoded_data[12..].try_into().map_err(|_| InvalidData)?); } else { pm.prompt_message(PromptMessage::Text( "Unable to detect QR code, falling back to text".to_string(), ))?; }; } let (nonce, pubkey) = match (nonce_data, pubkey_data) { (Some(nonce), Some(pubkey)) => (nonce, pubkey), _ => { let validator = MnemonicSetValidator { word_lengths: [9, 24], }; let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist("Their words: ", &wordlist, 3, validator.to_fn())?; let nonce = nonce_mnemonic .as_bytes() .try_into() .map_err(|_| InvalidData)?; let pubkey = pubkey_mnemonic .as_bytes() .try_into() .map_err(|_| InvalidData)?; (nonce, pubkey) } }; let nonce = Nonce::::from_slice(&nonce); let our_key = EphemeralSecret::random(); let our_pubkey_mnemonic = Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; let shared_secret = our_key.diffie_hellman(&PublicKey::from(pubkey)).to_bytes(); 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 hkdf = Hkdf::::new(None, &shared_secret); let mut hkdf_output = [0u8; 256 / 8]; hkdf.expand(&[], &mut hkdf_output)?; let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?; let bytes = shared_key.encrypt(nonce, share.as_slice())?; shared_key.decrypt(nonce, &bytes[..])?; // 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. #[allow(clippy::assertions_on_constants)] { assert!(ENC_LEN < u8::MAX, "padding byte can be u8"); } #[allow(clippy::cast_possible_truncation)] 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); #[allow(clippy::cast_possible_truncation)] for (i, byte) in (out_bytes[bytes.len()..(ENC_LEN as usize - 1)]) .iter_mut() .enumerate() { *byte = (i % u8::MAX as usize) as u8; } // safety: size of out_bytes is constant and always % 4 == 0 let payload_mnemonic = unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; #[cfg(feature = "qrcode")] { use keyfork_qrcode::{qrencode, ErrorCorrection}; let mut qrcode_data = our_pubkey_mnemonic.to_bytes(); qrcode_data.extend(payload_mnemonic.as_bytes()); if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Lowest) { pm.prompt_message(PromptMessage::Data(qrcode))?; } } pm.prompt_message(PromptMessage::Text(format!( "Our words: {our_pubkey_mnemonic} {payload_mnemonic}" )))?; Ok(()) } /// Combine mulitple shards into a secret. /// /// # Errors /// The function may return an error if an error occurs while decrypting shards, parsing shards, or /// combining the shards into a secret. pub fn combine( certs: Vec, metadata: &EncryptedMessage, messages: Vec, mut output: impl Write, ) -> Result<()> { // Be as liberal as possible when decrypting. // We don't want to invalidate someone's keys just because the old sig expired. let policy = NullPolicy::new(); 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)?; keyring.set_root_cert(root_cert.clone()); 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. let mut messages: HashMap = certs.iter().map(Cert::keyid).zip(messages).collect(); let mut decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; // 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 { #[allow(clippy::cast_possible_truncation)] let new_messages = decrypt_with_manager( left_from_threshold as u8, &mut messages, &certs, &policy, &mut manager, )?; decrypted_messages.extend(new_messages); } let shares = decrypted_messages .values() .map(|message| Share::try_from(message.as_slice())) .collect::, &str>>() .map_err(|e| SharksError::Share(e.to_string()))?; let secret = Sharks(threshold) .recover(&shares) .map_err(|e| SharksError::CombineShare(e.to_string()))?; // TODO: extract as function let userid = UserID::from("keyfork-sss"); let kdr = DerivationRequest::new( DerivationAlgorithm::Ed25519, &DerivationPath::from_str("m/7366512'/0'")?, ) .derive_with_master_seed(secret.clone())?; 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)); } output.write_all(&secret).map_err(Error::Io)?; Ok(()) } /// Split a secret into an OpenPGP formatted Shard file. /// /// # Errors /// /// The function may return an error if the shards can't be encrypted to the provided OpenPGP /// certs or if an error happens while writing the Shard file. /// /// # Panics /// /// The function may panic if the metadata can't properly store the certificates used to generate /// the encrypted shares. pub fn split(threshold: u8, certs: Vec, secret: &[u8], output: impl Write) -> Result<()> { // build cert to sign encrypted shares 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, )?; let signing_key = derived_cert .primary_key() .parts_into_secret() .map_err(Error::Sequoia)? .key() .clone() .into_keypair() .map_err(Error::Sequoia)?; let sharks = Sharks(threshold); let dealer = sharks.dealer(secret); let generated_shares = dealer.map(|s| Vec::from(&s)).collect::>(); let policy = StandardPolicy::new(); let mut writer = Writer::new(output, Kind::Message).map_err(Error::SequoiaIo)?; let mut total_recipients = vec![]; let mut messages = vec![]; for (share, cert) in generated_shares.iter().zip(certs) { total_recipients.push(cert.clone()); let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?; let encryption_keys = get_encryption_keys(&valid_cert).collect::>(); let mut message_output = vec![]; let message = Message::new(&mut message_output); let message = Encryptor2::for_recipients( message, encryption_keys .iter() .map(|k| Recipient::new(KeyID::wildcard(), k.key())), ) .build() .map_err(Error::Sequoia)?; let message = Signer::new(message, signing_key.clone()) .build() .map_err(Error::Sequoia)?; let mut message = LiteralWriter::new(message) .build() .map_err(Error::Sequoia)?; message.write_all(share).map_err(Error::SequoiaIo)?; message.finalize().map_err(Error::Sequoia)?; messages.push(message_output); } let mut pp = vec![SHARD_METADATA_VERSION, threshold]; // store derived cert to verify provided shares derived_cert.serialize(&mut pp).map_err(Error::Sequoia)?; for recipient in &total_recipients { recipient.serialize(&mut pp).map_err(Error::Sequoia)?; } // verify packet pile for (packet_cert, cert) in openpgp::cert::CertParser::from_bytes(&pp[2..]) .map_err(Error::Sequoia)? .skip(1) .zip(total_recipients.iter()) { assert_eq!( &packet_cert.map_err(Error::Sequoia)?, cert, "packet pile could not recreate cert: {}", cert.fingerprint() ); } let valid_certs = total_recipients .iter() .map(|c| c.with_policy(&policy, None)) .collect::>>() .map_err(Error::Sequoia)?; let total_recipients = valid_certs.iter().flat_map(|vc| { get_encryption_keys(vc).map(|key| Recipient::new(KeyID::wildcard(), key.key())) }); // metadata let mut message_output = vec![]; let message = Message::new(&mut message_output); let message = Encryptor2::for_recipients(message, total_recipients) .build() .map_err(Error::Sequoia)?; let mut message = LiteralWriter::new(message) .build() .map_err(Error::Sequoia)?; message.write_all(&pp).map_err(Error::SequoiaIo)?; message.finalize().map_err(Error::Sequoia)?; writer .write_all(&message_output) .map_err(Error::SequoiaIo)?; for message in messages { writer.write_all(&message).map_err(Error::SequoiaIo)?; } writer.finalize().map_err(Error::SequoiaIo)?; Ok(()) }