From 6093cf9be43ecc41f27067235eb1d49a03a8cdf3 Mon Sep 17 00:00:00 2001 From: ryan Date: Sat, 20 Jan 2024 01:20:04 -0500 Subject: [PATCH 1/8] keyfork-shard: traitify functionality --- .../src/bin/keyfork-shard-combine-openpgp.rs | 29 +- .../src/bin/keyfork-shard-decrypt-openpgp.rs | 40 +-- crates/keyfork-shard/src/lib.rs | 287 +++++++++++++++++- crates/keyfork-shard/src/openpgp.rs | 278 +++++++++++++++++ 4 files changed, 581 insertions(+), 53 deletions(-) diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs index 6554ca5..5e5338f 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs @@ -7,50 +7,33 @@ use std::{ process::ExitCode, }; -use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages}; +use keyfork_shard::{Format, openpgp::OpenPGP}; type Result> = std::result::Result; fn validate( shard: impl AsRef, key_discovery: Option<&str>, -) -> Result<(File, Vec)> { +) -> Result<(File, Option)> { let key_discovery = key_discovery.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(shard)?, certs)) + Ok((File::open(shard)?, key_discovery)) } 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() { + let (messages_file, key_discovery) = match args.as_slice() { [shard, key_discovery] => validate(shard, Some(key_discovery))?, [shard] => validate(shard, None)?, _ => panic!("Usage: {program_name} [key_discovery]"), }; - let mut encrypted_messages = parse_messages(messages_file)?; + let openpgp = OpenPGP; - let encrypted_metadata = encrypted_messages - .pop_front() - .expect("any pgp encrypted message"); - - let mut bytes = vec![]; - - combine( - cert_list, - &encrypted_metadata, - encrypted_messages.into(), - &mut bytes, - )?; + let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, messages_file)?; print!("{}", smex::encode(&bytes)); diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs index 748b353..2393a1b 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs @@ -7,47 +7,33 @@ use std::{ process::ExitCode, }; -use keyfork_shard::openpgp::{decrypt, discover_certs, openpgp::Cert, parse_messages}; +use keyfork_shard::{Format, openpgp::OpenPGP}; 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); +fn validate( + shard: impl AsRef, + key_discovery: Option<&str>, +) -> Result<(File, Option)> { + let key_discovery = key_discovery.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)) + Ok((File::open(shard)?, key_discovery)) } 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 (messages_file, key_discovery) = match args.as_slice() { + [shard, key_discovery] => validate(shard, Some(key_discovery))?, + [shard] => validate(shard, None)?, + _ => panic!("Usage: {program_name} [key_discovery]"), }; - let mut encrypted_messages = parse_messages(messages_file)?; + let openpgp = OpenPGP; - let encrypted_metadata = encrypted_messages - .pop_front() - .expect("any pgp encrypted message"); - - decrypt( - &cert_list, - &encrypted_metadata, - encrypted_messages.make_contiguous(), - )?; + openpgp.decrypt_one_shard_for_transport(key_discovery, messages_file)?; Ok(()) } diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 01e01d1..047ee09 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -1,10 +1,13 @@ #![doc = include_str!("../README.md")] -use std::io::{stdin, stdout, Write}; +use std::{ + io::{stdin, stdout, Read, Write}, + path::Path, +}; use aes_gcm::{ - aead::{Aead, AeadCore, OsRng}, - Aes256Gcm, KeyInit, + aead::{consts::U12, Aead, AeadCore, OsRng}, + Aes256Gcm, KeyInit, Nonce, }; use hkdf::Hkdf; use keyfork_mnemonic_util::{English, Mnemonic}; @@ -16,9 +19,287 @@ use sha2::Sha256; use sharks::{Share, Sharks}; use x25519_dalek::{EphemeralSecret, PublicKey}; +// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size +const ENC_LEN: u8 = 4 * 16; + #[cfg(feature = "openpgp")] pub mod openpgp; +/// A format to use for splitting and combining secrets. +pub trait Format { + /// The error type returned from any failed operations. + type Error: std::error::Error + 'static; + + /// A type encapsulating the public key recipients of shards. + type PublicKeyData; + + /// A type encapsulating the private key recipients of shards. + type PrivateKeyData; + + /// A type representing the parsed, but encrypted, Shard data. + type ShardData; + + /// A type representing a Signer derived from the secret. + type Signer; + + /// Parse the public key data from a readable type. + /// + /// # Errors + /// The method may return an error if private key data could not be properly parsed from the + /// path. + /// occurred while parsing the public key data. + fn parse_public_key_data( + &self, + key_data_path: impl AsRef, + ) -> Result; + + /// Parse the private key data from a readable type. The private key may not be accessible (it + /// may be hardware only, such as a smartcard), for which this method may return None. + /// + /// # Errors + /// The method may return an error if private key data could not be properly parsed from the + /// path. + fn parse_private_key_data( + &self, + key_data_path: impl AsRef, + ) -> Result; + + /// Parse the Shard file into a processable type. + /// + /// # Errors + /// The method may return an error if the Shard file could not be read from or if the Shard + /// file could not be properly parsed. + fn parse_shard_file( + &self, + shard_file: impl Read + Send + Sync, + ) -> Result; + + /// Write the Shard data to a Shard file. + /// + /// # Errors + /// The method may return an error if the Shard data could not be properly serialized or if the + /// Shard file could not be written to. + fn format_shard_file( + &self, + shard_data: Self::ShardData, + shard_file: impl Write, + ) -> Result<(), Self::Error>; + + /// Derive a Signer from the secret. + /// + /// # Errors + /// This function may return an error if a Signer could not be properly created. + fn derive_signer(&self, secret: &[u8]) -> Result; + + /// Encrypt multiple shares to public keys. + /// + /// # Errors + /// The method may return an error if the share could not be encrypted to a public key or if + /// the ShardData could not be created. + fn generate_shard_data( + &self, + shares: &[Share], + signer: &Self::Signer, + public_keys: Self::PublicKeyData, + ) -> Result; + + /// Decrypt shares and associated metadata from a readable input. For the current version of + /// Keyfork, the only associated metadata is a u8 representing the threshold to combine + /// secrets. + /// + /// # Errors + /// The method may return an error if the shardfile couldn't be read from, if all shards + /// could not be decrypted, or if a shard could not be parsed from the decrypted data. + fn decrypt_all_shards( + &self, + private_keys: Option, + shard_data: Self::ShardData, + ) -> Result<(Vec, u8), Self::Error>; + + /// Decrypt a single share and associated metadata from a reaable input. For the current + /// version of Keyfork, the only associated metadata is a u8 representing the threshold to + /// combine secrets. + /// + /// # Errors + /// The method may return an error if the shardfile couldn't be read from, if a shard could not + /// be decrypted, or if a shard could not be parsed from the decrypted data. + fn decrypt_one_shard( + &self, + private_keys: Option, + shard_data: Self::ShardData, + ) -> Result<(Share, u8), Self::Error>; + + /// Decrypt multiple shares and combine them to recreate a secret. + /// + /// # Errors + /// The method may return an error if the shares can't be decrypted or if the shares can't + /// be combined into a secret. + fn decrypt_all_shards_to_secret( + &self, + private_key_data_path: Option>, + reader: impl Read + Send + Sync, + ) -> Result, Box> { + let private_keys = private_key_data_path + .map(|p| self.parse_private_key_data(p)) + .transpose()?; + let shard_data = self.parse_shard_file(reader)?; + let (shares, threshold) = self.decrypt_all_shards(private_keys, shard_data)?; + + let secret = Sharks(threshold) + .recover(&shares) + .map_err(|e| SharksError::CombineShare(e.to_string()))?; + + Ok(secret) + } + + /// Establish an AES-256-GCM transport key using ECDH, decrypt a single shard, and encrypt the + /// shard to the AES key. + /// + /// # Errors + /// The method may return an error if a share can't be decrypted. The method will not return an + /// error if the camera is inaccessible or if a hardware error is encountered while scanning a + /// QR code; instead, a mnemonic prompt will be used. + fn decrypt_one_shard_for_transport( + &self, + private_key_data_path: Option>, + reader: impl Read + Send + Sync, + ) -> Result<(), Box> { + let mut pm = Terminal::new(stdin(), stdout())?; + let wordlist = Wordlist::default(); + + // parse input + let private_keys = private_key_data_path + .map(|p| self.parse_private_key_data(p)) + .transpose()?; + let shard_data = self.parse_shard_file(reader)?; + + // establish AES-256-GCM key via ECDH + let mut nonce_data: Option<[u8; 12]> = None; + let mut pubkey_data: Option<[u8; 32]> = None; + + // receive remote data via scanning QR code from camera + #[cfg(feature = "qrcode")] + { + pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?; + if let Ok(Some(hex)) = + keyfork_qrcode::scan_camera(std::time::Duration::from_secs(30), 0) + { + let decoded_data = smex::decode(&hex)?; + nonce_data = Some(decoded_data[..12].try_into().map_err(|_| InvalidData)?); + pubkey_data = Some(decoded_data[12..].try_into().map_err(|_| InvalidData)?) + } else { + pm.prompt_message(PromptMessage::Text(QRCODE_ERROR.to_string()))?; + }; + } + + // if QR code scanning failed or was unavailable, read from a set of mnemonics + let (nonce, their_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( + QRCODE_COULDNT_READ, + &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) + } + }; + + // create our shared key + 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(their_pubkey)) + .to_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)?; + + // decrypt a single shard and create the payload + let (share, threshold) = self.decrypt_one_shard(private_keys, shard_data)?; + let mut payload = Vec::from(&share); + payload.insert(0, HUNK_VERSION); + payload.insert(1, threshold); + assert!( + payload.len() <= ENC_LEN as usize, + "invalid share length (too long, max {ENC_LEN} bytes)" + ); + + // encrypt data + let nonce = Nonce::::from_slice(&nonce); + let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?; + + // convert data to a static-size payload + // NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX + #[allow(clippy::assertions_on_constants)] + { + assert!(ENC_LEN < u8::MAX, "padding byte can be u8"); + } + #[allow(clippy::cast_possible_truncation)] + let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize]; + assert!( + payload_bytes.len() < out_bytes.len(), + "encrypted payload larger than acceptable limit" + ); + out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes); + + // NOTE: This previously used a single repeated 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 resolved below by having sequentially increasing numbers up to + // but not including the last byte. + #[allow(clippy::cast_possible_truncation)] + for (i, byte) in (out_bytes[payload_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::Highest) { + pm.prompt_message(PromptMessage::Text( + concat!( + "A QR code will be displayed after this prompt. ", + "Send the QR code back to the operator combining the shards. ", + "Nobody else should scan this QR code." + ) + .to_string(), + ))?; + pm.prompt_message(PromptMessage::Data(qrcode))?; + } + } + + pm.prompt_message(PromptMessage::Text(format!( + "Upon request, these words should be sent: {our_pubkey_mnemonic} {payload_mnemonic}" + )))?; + + Ok(()) + } +} + /// Errors encountered while creating or combining shares using Shamir's Secret Sharing. #[derive(thiserror::Error, Debug)] pub enum SharksError { diff --git a/crates/keyfork-shard/src/openpgp.rs b/crates/keyfork-shard/src/openpgp.rs index 1135265..efe099e 100644 --- a/crates/keyfork-shard/src/openpgp.rs +++ b/crates/keyfork-shard/src/openpgp.rs @@ -59,6 +59,7 @@ const SHARD_METADATA_OFFSET: usize = 2; use super::{ InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, QRCODE_PROMPT, QRCODE_TIMEOUT, + Format, }; // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding @@ -163,6 +164,18 @@ impl EncryptedMessage { } } + /// Serialize all contents of the message to a writer. + /// + /// # Errors + /// The function may error for any condition in Sequoia's Serialize trait. + pub fn serialize(&self, o: &mut dyn std::io::Write) -> openpgp::Result<()> { + for pkesk in &self.pkesks { + pkesk.serialize(o)?; + } + self.message.serialize(o)?; + Ok(()) + } + /// Decrypt the message with a Sequoia policy and decryptor. /// /// This method creates a container containing the packets and passes the serialized container @@ -207,12 +220,273 @@ impl EncryptedMessage { } } +/// +pub struct OpenPGP; + +impl OpenPGP { + /// 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) + } + } +} + +impl Format for OpenPGP { + type Error = Error; + type PublicKeyData = Vec; + type PrivateKeyData = Vec; + type ShardData = Vec; + type Signer = openpgp::crypto::KeyPair; + + fn parse_public_key_data( + &self, + key_data_path: impl AsRef, + ) -> std::result::Result { + Self::discover_certs(key_data_path) + } + + fn parse_private_key_data( + &self, + key_data_path: impl AsRef, + ) -> std::result::Result { + Self::discover_certs(key_data_path) + } + + fn parse_shard_file( + &self, + shard_file: impl Read + Send + Sync, + ) -> Result { + let mut pkesks = Vec::new(); + let mut encrypted_messages = vec![]; + + for packet in PacketPile::from_reader(shard_file) + .map_err(Error::Sequoia)? + .into_children() + { + match packet { + Packet::PKESK(p) => pkesks.push(p), + Packet::SEIP(s) => { + encrypted_messages.push(EncryptedMessage::new(&mut pkesks, s)); + } + s => { + panic!("Invalid variant found: {}", s.tag()); + } + } + } + + Ok(encrypted_messages) + } + + fn derive_signer(&self, secret: &[u8]) -> Result { + let userid = UserID::from("keyfork-sss"); + let path = DerivationPath::from_str("m/7366512'/0'")?; + let seed = VariableLengthSeed::new(secret); + let xprv = XPrv::new(seed).derive_path(&path)?; + let derived_cert = keyfork_derive_openpgp::derive( + xprv, + &[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)?; + + Ok(signing_key) + } + + fn format_shard_file( + &self, + shard_data: Self::ShardData, + shard_file: impl Write, + ) -> Result<(), Self::Error> { + let mut writer = Writer::new(shard_file, Kind::Message).map_err(Error::SequoiaIo)?; + for message in shard_data { + message.serialize(&mut writer).map_err(Error::Sequoia)?; + } + writer.finalize().map_err(Error::SequoiaIo)?; + Ok(()) + } + + fn generate_shard_data( + &self, + shares: &[Share], + signer: &Self::Signer, + public_keys: Self::PublicKeyData, + ) -> std::result::Result { + let policy = StandardPolicy::new(); + let mut total_recipients = vec![]; + let mut messages = vec![]; + + for (share, cert) in shares.iter().zip(public_keys) { + 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, signer.clone()) + .build() + .map_err(Error::Sequoia)?; + let mut message = LiteralWriter::new(message) + .build() + .map_err(Error::Sequoia)?; + // NOTE: This shouldn't be an alloc, but it's a minor alloc, so it's fine. + message + .write_all(&Vec::from(share)) + .map_err(Error::SequoiaIo)?; + message.finalize().map_err(Error::Sequoia)?; + + messages.push(message_output); + } + + // A little bit of back and forth, we're going to parse the messages just to serialize them + // later. + let message = messages.into_iter().flatten().collect::>(); + let data = self.parse_shard_file(message.as_slice())?; + Ok(data) + } + + fn decrypt_all_shards( + &self, + private_keys: Option, + mut shard_data: Self::ShardData, + ) -> std::result::Result<(Vec, u8), Self::Error> { + // 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(private_keys.unwrap_or_default())?; + let mut manager = SmartcardManager::new()?; + + let metadata = shard_data.remove(0); + let metadata_content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?; + + let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_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. + + // TODO: remove alloc, convert EncryptedMessage to &EncryptedMessage + let mut messages: HashMap = certs + .iter() + .map(Cert::keyid) + .zip(shard_data) + .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()))?; + Ok((shares, threshold)) + } + + fn decrypt_one_shard( + &self, + private_keys: Option, + mut shard_data: Self::ShardData, + ) -> std::result::Result<(Share, u8), Self::Error> { + let policy = NullPolicy::new(); + let mut keyring = Keyring::new(private_keys.unwrap_or_default())?; + let mut manager = SmartcardManager::new()?; + + let metadata = shard_data.remove(0); + let metadata_content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?; + + let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_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(shard_data) + .collect(); + + let decrypted_messages = + decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; + + if let Some(message) = decrypted_messages.into_values().next() { + let share = Share::try_from(message.as_slice()).map_err(|e| SharksError::Share(e.to_string()))?; + return Ok((share, threshold)); + } + + let decrypted_messages = + decrypt_with_manager(1, &mut messages, &certs, &policy, &mut manager)?; + + if let Some(message) = decrypted_messages.into_values().next() { + let share = Share::try_from(message.as_slice()).map_err(|e| SharksError::Share(e.to_string()))?; + return Ok((share, threshold)); + } + + panic!("unable to decrypt shard"); + } +} + /// 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. +#[deprecated] pub fn discover_certs(path: impl AsRef) -> Result> { let path = path.as_ref(); @@ -245,6 +519,7 @@ pub fn discover_certs(path: impl AsRef) -> Result> { /// # 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. +#[deprecated] pub fn parse_messages(reader: impl Read + Send + Sync) -> Result> { let mut pkesks = Vec::new(); let mut encrypted_messages = VecDeque::new(); @@ -416,6 +691,7 @@ fn decrypt_metadata( }) } +#[deprecated] fn decrypt_one( messages: Vec, certs: &[Cert], @@ -465,6 +741,8 @@ fn decrypt_one( /// 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`]. +#[deprecated] +#[allow(deprecated)] pub fn decrypt( certs: &[Cert], metadata: &EncryptedMessage, -- 2.40.1 From 3c1d8e978419c59f02ede147dc830a2f33e6c158 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 12 Feb 2024 09:09:00 -0500 Subject: [PATCH 2/8] cleanup use of keyfork-shard deprecated functions --- .../src/bin/keyfork-shard-combine-openpgp.rs | 2 - crates/keyfork/src/cli/recover.rs | 18 +++------ crates/keyfork/src/cli/shard.rs | 39 +++---------------- 3 files changed, 10 insertions(+), 49 deletions(-) diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs index 5e5338f..f910647 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs @@ -32,9 +32,7 @@ fn run() -> Result<()> { }; let openpgp = OpenPGP; - let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, messages_file)?; - print!("{}", smex::encode(&bytes)); Ok(()) diff --git a/crates/keyfork/src/cli/recover.rs b/crates/keyfork/src/cli/recover.rs index d68a193..3b766b2 100644 --- a/crates/keyfork/src/cli/recover.rs +++ b/crates/keyfork/src/cli/recover.rs @@ -3,10 +3,7 @@ use clap::{Parser, Subcommand}; use std::path::PathBuf; use keyfork_mnemonic_util::{English, Mnemonic}; -use keyfork_shard::{ - openpgp::{combine, discover_certs, parse_messages}, - remote_decrypt, -}; +use keyfork_shard::{remote_decrypt, Format}; type Result> = std::result::Result; @@ -37,15 +34,10 @@ impl RecoverSubcommands { } => { let content = std::fs::read_to_string(shard_file)?; if content.contains("BEGIN PGP MESSAGE") { - let certs = key_discovery - .as_ref() - .map(discover_certs) - .transpose()? - .unwrap_or(vec![]); - let mut messages = parse_messages(content.as_bytes())?; - let metadata = messages.pop_front().expect("any pgp encrypted message"); - let mut seed = vec![]; - combine(certs, &metadata, messages.into(), &mut seed)?; + let openpgp = keyfork_shard::openpgp::OpenPGP; + // TODO: remove .clone() by making handle() consume self + let seed = openpgp + .decrypt_all_shards_to_secret(key_discovery.clone(), content.as_bytes())?; Ok(seed) } else { panic!("unknown format of shard file"); diff --git a/crates/keyfork/src/cli/shard.rs b/crates/keyfork/src/cli/shard.rs index 6439b5f..ee8d741 100644 --- a/crates/keyfork/src/cli/shard.rs +++ b/crates/keyfork/src/cli/shard.rs @@ -1,5 +1,6 @@ use super::Keyfork; use clap::{builder::PossibleValue, Parser, Subcommand, ValueEnum}; +use keyfork_shard::Format as _; use std::{ io::{stdin, stdout, Read, Write}, path::{Path, PathBuf}, @@ -86,25 +87,8 @@ impl ShardExec for OpenPGP { where T: AsRef, { - let certs = key_discovery - .map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref())) - .transpose()? - .unwrap_or(vec![]); - - let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?; - let encrypted_metadata = encrypted_messages - .pop_front() - .expect("any pgp encrypted message"); - - let mut bytes = vec![]; - - keyfork_shard::openpgp::combine( - certs, - &encrypted_metadata, - encrypted_messages.into(), - &mut bytes, - )?; - + let openpgp = keyfork_shard::openpgp::OpenPGP; + let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?; write!(output, "{}", smex::encode(&bytes))?; Ok(()) @@ -118,21 +102,8 @@ impl ShardExec for OpenPGP { where T: AsRef, { - let certs = key_discovery - .map(|kd| keyfork_shard::openpgp::discover_certs(kd.as_ref())) - .transpose()? - .unwrap_or(vec![]); - - let mut encrypted_messages = keyfork_shard::openpgp::parse_messages(input)?; - let encrypted_metadata = encrypted_messages - .pop_front() - .expect("any pgp encrypted message"); - - keyfork_shard::openpgp::decrypt( - &certs, - &encrypted_metadata, - encrypted_messages.make_contiguous(), - )?; + let openpgp = keyfork_shard::openpgp::OpenPGP; + openpgp.decrypt_one_shard_for_transport(key_discovery, input)?; Ok(()) } } -- 2.40.1 From 3b5c1340db2a1372f2cd2713cd0f2847f9e356d7 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 12 Feb 2024 11:51:49 -0500 Subject: [PATCH 3/8] keyfork-shard: add new methods to trait to support split() --- crates/keyfork-shard/src/lib.rs | 59 ++++--- crates/keyfork-shard/src/openpgp.rs | 237 ++++++++++++++++++---------- 2 files changed, 188 insertions(+), 108 deletions(-) diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 047ee09..5135cbc 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -31,17 +31,20 @@ pub trait Format { type Error: std::error::Error + 'static; /// A type encapsulating the public key recipients of shards. - type PublicKeyData; + type PublicKeyData: IntoIterator; + + /// A type encapsulating a single public key recipient. + type PublicKey; /// A type encapsulating the private key recipients of shards. type PrivateKeyData; + /// A type representing a Signer derived from the secret. + type SigningKey; + /// A type representing the parsed, but encrypted, Shard data. type ShardData; - /// A type representing a Signer derived from the secret. - type Signer; - /// Parse the public key data from a readable type. /// /// # Errors @@ -53,6 +56,36 @@ pub trait Format { key_data_path: impl AsRef, ) -> Result; + /// Derive a signer + fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey; + + /// Format a header containing necessary metadata. Such metadata contains a version byte, a + /// threshold byte, a public version of the [`Format::SigningKey`], and the public keys used to + /// encrypt shards. The public keys must be kept _in order_ to the encrypted shards. Keyfork + /// will use the same key_data for both, ensuring an iteration of this method will match with + /// iterations in methods called later. + /// + /// # Errors + /// The method may return an error if encryption to any of the public keys fails. + fn format_encrypted_header( + &self, + signing_key: &Self::SigningKey, + key_data: &Self::PublicKeyData, + threshold: u8, + ) -> Result, Self::Error>; + + /// Format a shard encrypted to the given public key, signing with the private key. + /// + /// # Errors + /// The method may return an error if the public key used to encrypt the shard is unsuitable + /// for encryption, or if an error occurs while encrypting. + fn encrypt_shard( + &self, + shard: &[u8], + public_key: &Self::PublicKey, + signing_key: &mut Self::SigningKey, + ) -> Result, Self::Error>; + /// Parse the private key data from a readable type. The private key may not be accessible (it /// may be hardware only, such as a smartcard), for which this method may return None. /// @@ -85,24 +118,6 @@ pub trait Format { shard_file: impl Write, ) -> Result<(), Self::Error>; - /// Derive a Signer from the secret. - /// - /// # Errors - /// This function may return an error if a Signer could not be properly created. - fn derive_signer(&self, secret: &[u8]) -> Result; - - /// Encrypt multiple shares to public keys. - /// - /// # Errors - /// The method may return an error if the share could not be encrypted to a public key or if - /// the ShardData could not be created. - fn generate_shard_data( - &self, - shares: &[Share], - signer: &Self::Signer, - public_keys: Self::PublicKeyData, - ) -> Result; - /// Decrypt shares and associated metadata from a readable input. For the current version of /// Keyfork, the only associated metadata is a u8 representing the threshold to combine /// secrets. diff --git a/crates/keyfork-shard/src/openpgp.rs b/crates/keyfork-shard/src/openpgp.rs index efe099e..d58f91a 100644 --- a/crates/keyfork-shard/src/openpgp.rs +++ b/crates/keyfork-shard/src/openpgp.rs @@ -57,9 +57,8 @@ const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_OFFSET: usize = 2; use super::{ - InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, QRCODE_PROMPT, - QRCODE_TIMEOUT, - Format, + Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, + QRCODE_PROMPT, QRCODE_TIMEOUT, }; // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding @@ -256,10 +255,11 @@ impl OpenPGP { impl Format for OpenPGP { type Error = Error; + type PublicKey = Cert; type PublicKeyData = Vec; type PrivateKeyData = Vec; + type SigningKey = Cert; type ShardData = Vec; - type Signer = openpgp::crypto::KeyPair; fn parse_public_key_data( &self, @@ -268,6 +268,145 @@ impl Format for OpenPGP { Self::discover_certs(key_data_path) } + /// Derive an OpenPGP Shard certificate from the given seed. + fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey { + let seed = VariableLengthSeed::new(seed); + // build cert to sign encrypted shares + let userid = UserID::from("keyfork-sss"); + let path = DerivationPath::from_str("m/7366512'/0'").expect("valid derivation path"); + let xprv = XPrv::new(seed) + .derive_path(&path) + .expect("valid derivation"); + keyfork_derive_openpgp::derive( + xprv, + &[KeyFlags::empty().set_certification().set_signing()], + &userid, + ) + .expect("valid cert creation") + } + + fn format_encrypted_header( + &self, + signing_key: &Self::SigningKey, + key_data: &Self::PublicKeyData, + threshold: u8, + ) -> Result, Self::Error> { + let policy = StandardPolicy::new(); + let mut pp = vec![SHARD_METADATA_VERSION, threshold]; + // Note: Sequoia does not export private keys on a Cert, only on a TSK + signing_key + .serialize(&mut pp) + .expect("serialize cert into bytes"); + for cert in key_data { + cert.serialize(&mut pp) + .expect("serialize pubkey into bytes"); + } + + // verify packet pile + let mut iter = openpgp::cert::CertParser::from_bytes(&pp[SHARD_METADATA_OFFSET..]) + .expect("should have certs"); + let first_cert = iter.next().transpose().ok().flatten().expect("first cert"); + assert_eq!(signing_key, &first_cert); + + for (packet_cert, cert) in iter.zip(key_data) { + assert_eq!( + &packet_cert.expect("parsed packet cert"), + cert, + "packet pile could not recreate cert: {}", + cert.fingerprint(), + ); + } + + let valid_certs = key_data + .iter() + .map(|c| c.with_policy(&policy, None)) + .collect::>>() + .map_err(Error::Sequoia)?; + let recipients = valid_certs.iter().flat_map(|vc| { + get_encryption_keys(vc).map(|key| Recipient::new(KeyID::wildcard(), key.key())) + }); + + // Process is as follows: + // * Any OpenPGP message + // * An encrypted message + // * A literal message + // * The packet pile + // + // When decrypting, OpenPGP will see: + // * A message, and parse it + // * An encrypted message, and decrypt it + // * A literal message, and extract it + // * The packet pile + let mut output = vec![]; + let message = Message::new(&mut output); + let encrypted_message = Encryptor2::for_recipients(message, recipients) + .build() + .map_err(Error::Sequoia)?; + let mut literal_message = LiteralWriter::new(encrypted_message) + .build() + .map_err(Error::Sequoia)?; + literal_message.write_all(&pp).map_err(Error::SequoiaIo)?; + literal_message.finalize().map_err(Error::Sequoia)?; + + Ok(output) + } + + fn encrypt_shard( + &self, + shard: &[u8], + public_key: &Cert, + signing_key: &mut Self::SigningKey, + ) -> Result> { + let policy = StandardPolicy::new(); + let valid_cert = public_key + .with_policy(&policy, None) + .map_err(Error::Sequoia)?; + let encryption_keys = get_encryption_keys(&valid_cert).collect::>(); + + let signing_key = signing_key + .primary_key() + .parts_into_secret() + .map_err(Error::Sequoia)? + .key() + .clone() + .into_keypair() + .map_err(Error::Sequoia)?; + + // Process is as follows: + // * Any OpenPGP message + // * An encrypted message + // * A signed message + // * A literal message + // * The shard itself + // + // When decrypting, OpenPGP will see: + // * A message, and parse it + // * An encrypted message, and decrypt it + // * A signed message, and verify it + // * A literal message, and extract it + // * The shard itself + let mut message_output = vec![]; + let message = Message::new(&mut message_output); + let encrypted_message = Encryptor2::for_recipients( + message, + encryption_keys + .iter() + .map(|k| Recipient::new(KeyID::wildcard(), k.key())), + ) + .build() + .map_err(Error::Sequoia)?; + let signed_message = Signer::new(encrypted_message, signing_key) + .build() + .map_err(Error::Sequoia)?; + let mut message = LiteralWriter::new(signed_message) + .build() + .map_err(Error::Sequoia)?; + message.write_all(shard).map_err(Error::SequoiaIo)?; + message.finalize().map_err(Error::Sequoia)?; + + Ok(message_output) + } + fn parse_private_key_data( &self, key_data_path: impl AsRef, @@ -300,29 +439,6 @@ impl Format for OpenPGP { Ok(encrypted_messages) } - fn derive_signer(&self, secret: &[u8]) -> Result { - let userid = UserID::from("keyfork-sss"); - let path = DerivationPath::from_str("m/7366512'/0'")?; - let seed = VariableLengthSeed::new(secret); - let xprv = XPrv::new(seed).derive_path(&path)?; - let derived_cert = keyfork_derive_openpgp::derive( - xprv, - &[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)?; - - Ok(signing_key) - } - fn format_shard_file( &self, shard_data: Self::ShardData, @@ -336,53 +452,6 @@ impl Format for OpenPGP { Ok(()) } - fn generate_shard_data( - &self, - shares: &[Share], - signer: &Self::Signer, - public_keys: Self::PublicKeyData, - ) -> std::result::Result { - let policy = StandardPolicy::new(); - let mut total_recipients = vec![]; - let mut messages = vec![]; - - for (share, cert) in shares.iter().zip(public_keys) { - 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, signer.clone()) - .build() - .map_err(Error::Sequoia)?; - let mut message = LiteralWriter::new(message) - .build() - .map_err(Error::Sequoia)?; - // NOTE: This shouldn't be an alloc, but it's a minor alloc, so it's fine. - message - .write_all(&Vec::from(share)) - .map_err(Error::SequoiaIo)?; - message.finalize().map_err(Error::Sequoia)?; - - messages.push(message_output); - } - - // A little bit of back and forth, we're going to parse the messages just to serialize them - // later. - let message = messages.into_iter().flatten().collect::>(); - let data = self.parse_shard_file(message.as_slice())?; - Ok(data) - } - fn decrypt_all_shards( &self, private_keys: Option, @@ -406,11 +475,8 @@ impl Format for OpenPGP { // because we control the order packets are encrypted and certificates are stored. // TODO: remove alloc, convert EncryptedMessage to &EncryptedMessage - let mut messages: HashMap = certs - .iter() - .map(Cert::keyid) - .zip(shard_data) - .collect(); + let mut messages: HashMap = + certs.iter().map(Cert::keyid).zip(shard_data).collect(); let mut decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; @@ -454,17 +520,15 @@ impl Format for OpenPGP { keyring.set_root_cert(root_cert.clone()); manager.set_root_cert(root_cert.clone()); - let mut messages: HashMap = certs - .iter() - .map(Cert::keyid) - .zip(shard_data) - .collect(); + let mut messages: HashMap = + certs.iter().map(Cert::keyid).zip(shard_data).collect(); let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; if let Some(message) = decrypted_messages.into_values().next() { - let share = Share::try_from(message.as_slice()).map_err(|e| SharksError::Share(e.to_string()))?; + let share = Share::try_from(message.as_slice()) + .map_err(|e| SharksError::Share(e.to_string()))?; return Ok((share, threshold)); } @@ -472,7 +536,8 @@ impl Format for OpenPGP { decrypt_with_manager(1, &mut messages, &certs, &policy, &mut manager)?; if let Some(message) = decrypted_messages.into_values().next() { - let share = Share::try_from(message.as_slice()).map_err(|e| SharksError::Share(e.to_string()))?; + let share = Share::try_from(message.as_slice()) + .map_err(|e| SharksError::Share(e.to_string()))?; return Ok((share, threshold)); } -- 2.40.1 From 2541d49fb8a3da7e255654c352086c2b740775f5 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 15 Feb 2024 03:01:23 -0500 Subject: [PATCH 4/8] keyfork-shard: add shard_and_encrypt --- .../src/bin/keyfork-shard-split-openpgp.rs | 33 +--- crates/keyfork-shard/src/lib.rs | 74 +++++++-- crates/keyfork-shard/src/openpgp.rs | 156 +++++++++++------- crates/keyfork/src/cli/shard.rs | 15 +- crates/keyfork/src/cli/wizard.rs | 2 + 5 files changed, 165 insertions(+), 115 deletions(-) diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs index 29d6f0a..dc2cdd7 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs @@ -2,14 +2,12 @@ use std::{env, path::PathBuf, process::ExitCode, str::FromStr}; -use keyfork_shard::openpgp::{discover_certs, openpgp::Cert, split}; +use keyfork_shard::{Format, openpgp::OpenPGP}; #[derive(Clone, Debug)] enum Error { Usage(String), Input, - Threshold(u8, u8), - InvalidCertCount(usize, u8), } impl std::fmt::Display for Error { @@ -19,15 +17,6 @@ impl std::fmt::Display for Error { write!(f, "Usage: {program_name} threshold max key_discovery") } Error::Input => f.write_str("Expected hex encoded input"), - Error::Threshold(threshold, max) => { - write!( - f, - "Invalid threshold: 0 < threshold {threshold} <= max {max} < 256" - ) - } - Error::InvalidCertCount(count, max) => { - write!(f, "Invalid cert count: count {count} != max {max}") - } } } } @@ -36,31 +25,20 @@ impl std::error::Error for Error {} type Result> = std::result::Result; -fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, Vec)> { +fn validate(threshold: &str, max: &str, key_discovery: &str) -> Result<(u8, u8, PathBuf)> { let threshold = u8::from_str(threshold)?; let max = u8::from_str(max)?; let key_discovery = PathBuf::from(key_discovery); - if threshold > max { - return Err(Error::Threshold(threshold, max).into()); - } - - // Verify path exists std::fs::metadata(&key_discovery)?; - // Load certs from path - let certs = discover_certs(key_discovery)?; - if certs.len() != max.into() { - return Err(Error::InvalidCertCount(certs.len(), max).into()); - } - - Ok((threshold, certs)) + Ok((threshold, max, key_discovery)) } fn run() -> Result<()> { let mut args = env::args(); let program_name = args.next().expect("program name"); let args = args.collect::>(); - let (threshold, cert_list) = match args.as_slice() { + let (threshold, max, key_discovery) = match args.as_slice() { [threshold, max, key_discovery] => validate(threshold, max, key_discovery)?, _ => return Err(Error::Usage(program_name).into()), }; @@ -72,8 +50,9 @@ fn run() -> Result<()> { smex::decode(line?)? }; - split(threshold, cert_list, &input, std::io::stdout())?; + let openpgp = OpenPGP; + openpgp.shard_and_encrypt(threshold, max, &input, key_discovery, std::io::stdout())?; Ok(()) } diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 5135cbc..0fa7d22 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -30,9 +30,6 @@ pub trait Format { /// The error type returned from any failed operations. type Error: std::error::Error + 'static; - /// A type encapsulating the public key recipients of shards. - type PublicKeyData: IntoIterator; - /// A type encapsulating a single public key recipient. type PublicKey; @@ -43,7 +40,7 @@ pub trait Format { type SigningKey; /// A type representing the parsed, but encrypted, Shard data. - type ShardData; + type EncryptedData; /// Parse the public key data from a readable type. /// @@ -54,7 +51,7 @@ pub trait Format { fn parse_public_key_data( &self, key_data_path: impl AsRef, - ) -> Result; + ) -> Result, Self::Error>; /// Derive a signer fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey; @@ -70,9 +67,9 @@ pub trait Format { fn format_encrypted_header( &self, signing_key: &Self::SigningKey, - key_data: &Self::PublicKeyData, + key_data: &[Self::PublicKey], threshold: u8, - ) -> Result, Self::Error>; + ) -> Result; /// Format a shard encrypted to the given public key, signing with the private key. /// @@ -84,7 +81,7 @@ pub trait Format { shard: &[u8], public_key: &Self::PublicKey, signing_key: &mut Self::SigningKey, - ) -> Result, Self::Error>; + ) -> Result; /// Parse the private key data from a readable type. The private key may not be accessible (it /// may be hardware only, such as a smartcard), for which this method may return None. @@ -105,7 +102,7 @@ pub trait Format { fn parse_shard_file( &self, shard_file: impl Read + Send + Sync, - ) -> Result; + ) -> Result, Self::Error>; /// Write the Shard data to a Shard file. /// @@ -114,8 +111,8 @@ pub trait Format { /// Shard file could not be written to. fn format_shard_file( &self, - shard_data: Self::ShardData, - shard_file: impl Write, + encrypted_data: &[Self::EncryptedData], + shard_file: impl Write + Send + Sync, ) -> Result<(), Self::Error>; /// Decrypt shares and associated metadata from a readable input. For the current version of @@ -128,7 +125,7 @@ pub trait Format { fn decrypt_all_shards( &self, private_keys: Option, - shard_data: Self::ShardData, + encrypted_messages: &[Self::EncryptedData], ) -> Result<(Vec, u8), Self::Error>; /// Decrypt a single share and associated metadata from a reaable input. For the current @@ -141,7 +138,7 @@ pub trait Format { fn decrypt_one_shard( &self, private_keys: Option, - shard_data: Self::ShardData, + encrypted_data: &[Self::EncryptedData], ) -> Result<(Share, u8), Self::Error>; /// Decrypt multiple shares and combine them to recreate a secret. @@ -157,8 +154,8 @@ pub trait Format { let private_keys = private_key_data_path .map(|p| self.parse_private_key_data(p)) .transpose()?; - let shard_data = self.parse_shard_file(reader)?; - let (shares, threshold) = self.decrypt_all_shards(private_keys, shard_data)?; + let encrypted_messages = self.parse_shard_file(reader)?; + let (shares, threshold) = self.decrypt_all_shards(private_keys, &encrypted_messages)?; let secret = Sharks(threshold) .recover(&shares) @@ -186,7 +183,7 @@ pub trait Format { let private_keys = private_key_data_path .map(|p| self.parse_private_key_data(p)) .transpose()?; - let shard_data = self.parse_shard_file(reader)?; + let encrypted_messages = self.parse_shard_file(reader)?; // establish AES-256-GCM key via ECDH let mut nonce_data: Option<[u8; 12]> = None; @@ -246,7 +243,7 @@ pub trait Format { let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?; // decrypt a single shard and create the payload - let (share, threshold) = self.decrypt_one_shard(private_keys, shard_data)?; + let (share, threshold) = self.decrypt_one_shard(private_keys, &encrypted_messages)?; let mut payload = Vec::from(&share); payload.insert(0, HUNK_VERSION); payload.insert(1, threshold); @@ -313,6 +310,49 @@ pub trait Format { Ok(()) } + + /// Split a secret into a shard for every shard in keys, with the given Shamir's Secret Sharing + /// threshold. + /// + /// # Errors + /// The method may return an error if the shares can't be encrypted. + fn shard_and_encrypt( + &self, + threshold: u8, + max: u8, + secret: &[u8], + public_key_data_path: impl AsRef, + writer: impl Write + Send + Sync, + ) -> Result<(), Box> { + let mut signing_key = self.derive_signing_key(secret); + + let sharks = Sharks(threshold); + let dealer = sharks.dealer(secret); + + let public_keys = self.parse_public_key_data(public_key_data_path)?; + assert!( + public_keys.len() < u8::MAX as usize, + "must have less than u8::MAX public keys" + ); + assert_eq!( + max, + public_keys.len() as u8, + "max must be equal to amount of public keys" + ); + let max = public_keys.len() as u8; + assert!(max >= threshold, "threshold must not exceed max keys"); + + let header = self.format_encrypted_header(&signing_key, &public_keys, threshold)?; + let mut messages = vec![header]; + for (pk, share) in public_keys.iter().zip(dealer) { + let shard = Vec::from(&share); + messages.push(self.encrypt_shard(&shard, pk, &mut signing_key)?); + } + + self.format_shard_file(&messages, writer)?; + + Ok(()) + } } /// Errors encountered while creating or combining shares using Shamir's Secret Sharing. diff --git a/crates/keyfork-shard/src/openpgp.rs b/crates/keyfork-shard/src/openpgp.rs index d58f91a..a6c356c 100644 --- a/crates/keyfork-shard/src/openpgp.rs +++ b/crates/keyfork-shard/src/openpgp.rs @@ -163,15 +163,52 @@ impl EncryptedMessage { } } + /// Parse OpenPGP packets for encrypted messages. + pub fn from_reader(input: impl Read + Send + Sync) -> openpgp::Result> { + let mut pkesks = Vec::new(); + let mut encrypted_messages = vec![]; + + for packet in PacketPile::from_reader(input) + .map_err(Error::Sequoia)? + .into_children() + { + match packet { + Packet::PKESK(p) => pkesks.push(p), + Packet::SEIP(s) => { + encrypted_messages.push(EncryptedMessage::new(&mut pkesks, s)); + } + s => { + panic!("Invalid variant found: {}", s.tag()); + } + } + } + + Ok(encrypted_messages) + } + /// Serialize all contents of the message to a writer. /// /// # Errors /// The function may error for any condition in Sequoia's Serialize trait. - pub fn serialize(&self, o: &mut dyn std::io::Write) -> openpgp::Result<()> { + fn serialize(&self, mut o: impl std::io::Write + Send + Sync) -> openpgp::Result<()> { for pkesk in &self.pkesks { - pkesk.serialize(o)?; + let mut packet = vec![]; + pkesk.serialize(&mut packet).map_err(Error::Sequoia)?; + let message = Message::new(&mut o); + 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)?; } - self.message.serialize(o)?; + let mut packet = vec![]; + self.message + .serialize(&mut packet) + .map_err(Error::Sequoia)?; + + let message = Message::new(&mut o); + 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)?; + Ok(()) } @@ -188,23 +225,8 @@ impl EncryptedMessage { 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) + self.serialize(&mut packets) .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)? @@ -256,15 +278,14 @@ impl OpenPGP { impl Format for OpenPGP { type Error = Error; type PublicKey = Cert; - type PublicKeyData = Vec; type PrivateKeyData = Vec; type SigningKey = Cert; - type ShardData = Vec; + type EncryptedData = EncryptedMessage; fn parse_public_key_data( &self, key_data_path: impl AsRef, - ) -> std::result::Result { + ) -> std::result::Result, Self::Error> { Self::discover_certs(key_data_path) } @@ -288,9 +309,9 @@ impl Format for OpenPGP { fn format_encrypted_header( &self, signing_key: &Self::SigningKey, - key_data: &Self::PublicKeyData, + key_data: &[Self::PublicKey], threshold: u8, - ) -> Result, Self::Error> { + ) -> Result { let policy = StandardPolicy::new(); let mut pp = vec![SHARD_METADATA_VERSION, threshold]; // Note: Sequoia does not export private keys on a Cert, only on a TSK @@ -348,7 +369,22 @@ impl Format for OpenPGP { literal_message.write_all(&pp).map_err(Error::SequoiaIo)?; literal_message.finalize().map_err(Error::Sequoia)?; - Ok(output) + // Parse it into an EncryptedMessage. Yes, this takes a serialized message + // and deserializes it. Don't think about it too hard. It's easier this way. + + let mut pkesks = vec![]; + for packet in PacketPile::from_reader(output.as_slice()) + .map_err(Error::Sequoia)? + .into_children() + { + match packet { + Packet::PKESK(p) => pkesks.push(p), + Packet::SEIP(s) => return Ok(EncryptedMessage::new(&mut pkesks, s)), + s => panic!("Invalid variant found: {}", s.tag()), + } + } + + panic!("Unable to build EncryptedMessage from PacketPile"); } fn encrypt_shard( @@ -356,7 +392,7 @@ impl Format for OpenPGP { shard: &[u8], public_key: &Cert, signing_key: &mut Self::SigningKey, - ) -> Result> { + ) -> Result { let policy = StandardPolicy::new(); let valid_cert = public_key .with_policy(&policy, None) @@ -404,7 +440,13 @@ impl Format for OpenPGP { message.write_all(shard).map_err(Error::SequoiaIo)?; message.finalize().map_err(Error::Sequoia)?; - Ok(message_output) + let message = EncryptedMessage::from_reader(message_output.as_slice()) + .map_err(Error::Sequoia)? + .into_iter() + .next() + .expect("serialized message should be parseable"); + + Ok(message) } fn parse_private_key_data( @@ -417,35 +459,17 @@ impl Format for OpenPGP { fn parse_shard_file( &self, shard_file: impl Read + Send + Sync, - ) -> Result { - let mut pkesks = Vec::new(); - let mut encrypted_messages = vec![]; - - for packet in PacketPile::from_reader(shard_file) - .map_err(Error::Sequoia)? - .into_children() - { - match packet { - Packet::PKESK(p) => pkesks.push(p), - Packet::SEIP(s) => { - encrypted_messages.push(EncryptedMessage::new(&mut pkesks, s)); - } - s => { - panic!("Invalid variant found: {}", s.tag()); - } - } - } - - Ok(encrypted_messages) + ) -> Result, Self::Error> { + EncryptedMessage::from_reader(shard_file).map_err(Error::Sequoia) } fn format_shard_file( &self, - shard_data: Self::ShardData, - shard_file: impl Write, + encrypted_data: &[Self::EncryptedData], + shard_file: impl Write + Send + Sync, ) -> Result<(), Self::Error> { let mut writer = Writer::new(shard_file, Kind::Message).map_err(Error::SequoiaIo)?; - for message in shard_data { + for message in encrypted_data { message.serialize(&mut writer).map_err(Error::Sequoia)?; } writer.finalize().map_err(Error::SequoiaIo)?; @@ -455,7 +479,7 @@ impl Format for OpenPGP { fn decrypt_all_shards( &self, private_keys: Option, - mut shard_data: Self::ShardData, + encrypted_data: &[Self::EncryptedData], ) -> std::result::Result<(Vec, u8), Self::Error> { // Be as liberal as possible when decrypting. // We don't want to invalidate someone's keys just because the old sig expired. @@ -463,8 +487,10 @@ impl Format for OpenPGP { let mut keyring = Keyring::new(private_keys.unwrap_or_default())?; let mut manager = SmartcardManager::new()?; - let metadata = shard_data.remove(0); - let metadata_content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?; + let mut encrypted_messages = encrypted_data.iter(); + + let metadata = encrypted_messages.next().expect("metdata"); + let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?; let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?; @@ -475,8 +501,11 @@ impl Format for OpenPGP { // because we control the order packets are encrypted and certificates are stored. // TODO: remove alloc, convert EncryptedMessage to &EncryptedMessage - let mut messages: HashMap = - certs.iter().map(Cert::keyid).zip(shard_data).collect(); + let mut messages: HashMap = certs + .iter() + .map(Cert::keyid) + .zip(encrypted_messages.cloned()) + .collect(); let mut decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; @@ -507,21 +536,26 @@ impl Format for OpenPGP { fn decrypt_one_shard( &self, private_keys: Option, - mut shard_data: Self::ShardData, + encrypted_data: &[Self::EncryptedData], ) -> std::result::Result<(Share, u8), Self::Error> { let policy = NullPolicy::new(); let mut keyring = Keyring::new(private_keys.unwrap_or_default())?; let mut manager = SmartcardManager::new()?; - let metadata = shard_data.remove(0); - let metadata_content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?; + let mut encrypted_messages = encrypted_data.iter(); + + let metadata = encrypted_messages.next().expect("metadata"); + let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?; let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_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(shard_data).collect(); + let mut messages: HashMap = certs + .iter() + .map(Cert::keyid) + .zip(encrypted_messages.cloned()) + .collect(); let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; @@ -936,6 +970,7 @@ pub fn decrypt( /// # Errors /// The function may return an error if an error occurs while decrypting shards, parsing shards, or /// combining the shards into a secret. +#[deprecated] pub fn combine( certs: Vec, metadata: &EncryptedMessage, @@ -1023,6 +1058,7 @@ pub fn combine( /// /// The function may panic if the metadata can't properly store the certificates used to generate /// the encrypted shares. +#[deprecated] pub fn split(threshold: u8, certs: Vec, secret: &[u8], output: impl Write) -> Result<()> { let seed = VariableLengthSeed::new(secret); // build cert to sign encrypted shares diff --git a/crates/keyfork/src/cli/shard.rs b/crates/keyfork/src/cli/shard.rs index ee8d741..dc91468 100644 --- a/crates/keyfork/src/cli/shard.rs +++ b/crates/keyfork/src/cli/shard.rs @@ -34,7 +34,7 @@ trait ShardExec { max: u8, key_discovery: impl AsRef, secret: &[u8], - output: &mut impl Write, + output: &mut (impl Write + Send + Sync), ) -> Result<(), Box>; fn combine( @@ -65,17 +65,10 @@ impl ShardExec for OpenPGP { max: u8, key_discovery: impl AsRef, secret: &[u8], - output: &mut impl Write, + output: &mut (impl Write + Send + Sync), ) -> Result<(), Box> { - // Get certs and input - let certs = keyfork_shard::openpgp::discover_certs(key_discovery.as_ref())?; - assert_eq!( - certs.len(), - max.into(), - "cert count {} != max {max}", - certs.len() - ); - keyfork_shard::openpgp::split(threshold, certs, secret, output).map_err(Into::into) + let opgp = keyfork_shard::openpgp::OpenPGP; + opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output) } fn combine( diff --git a/crates/keyfork/src/cli/wizard.rs b/crates/keyfork/src/cli/wizard.rs index 3c6e1db..e7a7b79 100644 --- a/crates/keyfork/src/cli/wizard.rs +++ b/crates/keyfork/src/cli/wizard.rs @@ -165,8 +165,10 @@ fn generate_shard_secret( if let Some(output_file) = output_file { let output = File::create(output_file)?; + #[allow(deprecated)] keyfork_shard::openpgp::split(threshold, certs, &seed, output)?; } else { + #[allow(deprecated)] keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?; } Ok(()) -- 2.40.1 From b75d45876aa10ebe3ea8d5782c67dbd6d17679ff Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 18 Feb 2024 20:19:29 -0500 Subject: [PATCH 5/8] keyfork-shard: refactor key discovery mechanisms --- .../src/bin/keyfork-shard-combine-openpgp.rs | 4 +- .../src/bin/keyfork-shard-decrypt-openpgp.rs | 2 +- .../src/bin/keyfork-shard-split-openpgp.rs | 2 +- crates/keyfork-shard/src/lib.rs | 64 +++++++++---------- crates/keyfork-shard/src/openpgp.rs | 36 ++++++----- crates/keyfork-shard/src/openpgp/keyring.rs | 4 +- crates/keyfork/src/cli/recover.rs | 2 +- crates/keyfork/src/cli/shard.rs | 36 ++++------- crates/keyfork/src/cli/wizard.rs | 10 +-- 9 files changed, 77 insertions(+), 83 deletions(-) diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs index f910647..82eb167 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs @@ -7,7 +7,7 @@ use std::{ process::ExitCode, }; -use keyfork_shard::{Format, openpgp::OpenPGP}; +use keyfork_shard::{openpgp::OpenPGP, Format}; type Result> = std::result::Result; @@ -32,7 +32,7 @@ fn run() -> Result<()> { }; let openpgp = OpenPGP; - let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, messages_file)?; + let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?; print!("{}", smex::encode(&bytes)); Ok(()) diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs index 2393a1b..3b06219 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs @@ -33,7 +33,7 @@ fn run() -> Result<()> { let openpgp = OpenPGP; - openpgp.decrypt_one_shard_for_transport(key_discovery, messages_file)?; + openpgp.decrypt_one_shard_for_transport(key_discovery.as_deref(), messages_file)?; Ok(()) } diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs index dc2cdd7..3423ef6 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs @@ -52,7 +52,7 @@ fn run() -> Result<()> { let openpgp = OpenPGP; - openpgp.shard_and_encrypt(threshold, max, &input, key_discovery, std::io::stdout())?; + openpgp.shard_and_encrypt(threshold, max, &input, key_discovery.as_path(), std::io::stdout())?; Ok(()) } diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 0fa7d22..4fbecaa 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -1,9 +1,6 @@ #![doc = include_str!("../README.md")] -use std::{ - io::{stdin, stdout, Read, Write}, - path::Path, -}; +use std::io::{stdin, stdout, Read, Write}; use aes_gcm::{ aead::{consts::U12, Aead, AeadCore, OsRng}, @@ -25,6 +22,27 @@ const ENC_LEN: u8 = 4 * 16; #[cfg(feature = "openpgp")] pub mod openpgp; +/// A trait to specify where keys can be discovered from, such as a Rust-native type or a path on +/// the filesystem that keys may be read from. +pub trait KeyDiscovery { + /// Discover public keys for the associated format. + /// + /// # Errors + /// The method may return an error if public keys could not be loaded from the given discovery + /// mechanism. A discovery mechanism _must_ be able to detect public keys. + fn discover_public_keys(&self) -> Result, F::Error>; + + /// Discover private keys for the associated format. + /// + /// # Errors + /// The method may return an error if private keys could not be loaded from the given + /// discovery mechanism. Keys may exist off-system (such as with smartcards), in which case the + /// PrivateKeyData type of the asssociated format should be either `()` (if the keys may never + /// exist on-system) or an empty container (such as an empty Vec); in either case, this method + /// _must not_ return an error if keys are accessible but can't be transferred into memory. + fn discover_private_keys(&self) -> Result; +} + /// A format to use for splitting and combining secrets. pub trait Format { /// The error type returned from any failed operations. @@ -42,17 +60,6 @@ pub trait Format { /// A type representing the parsed, but encrypted, Shard data. type EncryptedData; - /// Parse the public key data from a readable type. - /// - /// # Errors - /// The method may return an error if private key data could not be properly parsed from the - /// path. - /// occurred while parsing the public key data. - fn parse_public_key_data( - &self, - key_data_path: impl AsRef, - ) -> Result, Self::Error>; - /// Derive a signer fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey; @@ -83,17 +90,6 @@ pub trait Format { signing_key: &mut Self::SigningKey, ) -> Result; - /// Parse the private key data from a readable type. The private key may not be accessible (it - /// may be hardware only, such as a smartcard), for which this method may return None. - /// - /// # Errors - /// The method may return an error if private key data could not be properly parsed from the - /// path. - fn parse_private_key_data( - &self, - key_data_path: impl AsRef, - ) -> Result; - /// Parse the Shard file into a processable type. /// /// # Errors @@ -148,11 +144,11 @@ pub trait Format { /// be combined into a secret. fn decrypt_all_shards_to_secret( &self, - private_key_data_path: Option>, + private_key_discovery: Option>, reader: impl Read + Send + Sync, ) -> Result, Box> { - let private_keys = private_key_data_path - .map(|p| self.parse_private_key_data(p)) + let private_keys = private_key_discovery + .map(|p| p.discover_private_keys()) .transpose()?; let encrypted_messages = self.parse_shard_file(reader)?; let (shares, threshold) = self.decrypt_all_shards(private_keys, &encrypted_messages)?; @@ -173,15 +169,15 @@ pub trait Format { /// QR code; instead, a mnemonic prompt will be used. fn decrypt_one_shard_for_transport( &self, - private_key_data_path: Option>, + private_key_discovery: Option>, reader: impl Read + Send + Sync, ) -> Result<(), Box> { let mut pm = Terminal::new(stdin(), stdout())?; let wordlist = Wordlist::default(); // parse input - let private_keys = private_key_data_path - .map(|p| self.parse_private_key_data(p)) + let private_keys = private_key_discovery + .map(|p| p.discover_private_keys()) .transpose()?; let encrypted_messages = self.parse_shard_file(reader)?; @@ -321,7 +317,7 @@ pub trait Format { threshold: u8, max: u8, secret: &[u8], - public_key_data_path: impl AsRef, + public_key_discovery: impl KeyDiscovery, writer: impl Write + Send + Sync, ) -> Result<(), Box> { let mut signing_key = self.derive_signing_key(secret); @@ -329,7 +325,7 @@ pub trait Format { let sharks = Sharks(threshold); let dealer = sharks.dealer(secret); - let public_keys = self.parse_public_key_data(public_key_data_path)?; + let public_keys = public_key_discovery.discover_public_keys()?; assert!( public_keys.len() < u8::MAX as usize, "must have less than u8::MAX public keys" diff --git a/crates/keyfork-shard/src/openpgp.rs b/crates/keyfork-shard/src/openpgp.rs index a6c356c..5d5d061 100644 --- a/crates/keyfork-shard/src/openpgp.rs +++ b/crates/keyfork-shard/src/openpgp.rs @@ -58,7 +58,7 @@ const SHARD_METADATA_OFFSET: usize = 2; use super::{ Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, - QRCODE_PROMPT, QRCODE_TIMEOUT, + QRCODE_PROMPT, QRCODE_TIMEOUT, KeyDiscovery }; // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding @@ -282,13 +282,6 @@ impl Format for OpenPGP { type SigningKey = Cert; type EncryptedData = EncryptedMessage; - fn parse_public_key_data( - &self, - key_data_path: impl AsRef, - ) -> std::result::Result, Self::Error> { - Self::discover_certs(key_data_path) - } - /// Derive an OpenPGP Shard certificate from the given seed. fn derive_signing_key(&self, seed: &[u8]) -> Self::SigningKey { let seed = VariableLengthSeed::new(seed); @@ -449,13 +442,6 @@ impl Format for OpenPGP { Ok(message) } - fn parse_private_key_data( - &self, - key_data_path: impl AsRef, - ) -> std::result::Result { - Self::discover_certs(key_data_path) - } - fn parse_shard_file( &self, shard_file: impl Read + Send + Sync, @@ -579,6 +565,26 @@ impl Format for OpenPGP { } } +impl KeyDiscovery for &Path { + fn discover_public_keys(&self) -> Result::PublicKey>> { + OpenPGP::discover_certs(self) + } + + fn discover_private_keys(&self) -> Result<::PrivateKeyData> { + todo!() + } +} + +impl KeyDiscovery for &[Cert] { + fn discover_public_keys(&self) -> Result::PublicKey>> { + Ok(self.to_vec()) + } + + fn discover_private_keys(&self) -> Result<::PrivateKeyData> { + Ok(self.to_vec()) + } +} + /// 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. /// diff --git a/crates/keyfork-shard/src/openpgp/keyring.rs b/crates/keyfork-shard/src/openpgp/keyring.rs index fd01d89..6cd571a 100644 --- a/crates/keyfork-shard/src/openpgp/keyring.rs +++ b/crates/keyfork-shard/src/openpgp/keyring.rs @@ -111,12 +111,10 @@ impl DecryptionHelper for &mut Keyring { pkesk.recipient().is_wildcard() || cert.keys().any(|k| &k.keyid() == pkesk.recipient()) }) { - #[allow(deprecated, clippy::map_flatten)] let name = cert .userids() .next() - .map(|userid| userid.userid().name().transpose()) - .flatten() + .and_then(|userid| userid.userid().name2().transpose()) .transpose() .ok() .flatten(); diff --git a/crates/keyfork/src/cli/recover.rs b/crates/keyfork/src/cli/recover.rs index 3b766b2..2a0339a 100644 --- a/crates/keyfork/src/cli/recover.rs +++ b/crates/keyfork/src/cli/recover.rs @@ -37,7 +37,7 @@ impl RecoverSubcommands { let openpgp = keyfork_shard::openpgp::OpenPGP; // TODO: remove .clone() by making handle() consume self let seed = openpgp - .decrypt_all_shards_to_secret(key_discovery.clone(), content.as_bytes())?; + .decrypt_all_shards_to_secret(key_discovery.as_deref(), content.as_bytes())?; Ok(seed) } else { panic!("unknown format of shard file"); diff --git a/crates/keyfork/src/cli/shard.rs b/crates/keyfork/src/cli/shard.rs index dc91468..2d441b8 100644 --- a/crates/keyfork/src/cli/shard.rs +++ b/crates/keyfork/src/cli/shard.rs @@ -32,27 +32,23 @@ trait ShardExec { &self, threshold: u8, max: u8, - key_discovery: impl AsRef, + key_discovery: &Path, secret: &[u8], output: &mut (impl Write + Send + Sync), ) -> Result<(), Box>; - fn combine( + fn combine( &self, - key_discovery: Option, + key_discovery: Option<&Path>, input: impl Read + Send + Sync, output: &mut impl Write, - ) -> Result<(), Box> - where - T: AsRef; + ) -> Result<(), Box>; - fn decrypt( + fn decrypt( &self, - key_discovery: Option, + key_discovery: Option<&Path>, input: impl Read + Send + Sync, - ) -> Result<(), Box> - where - T: AsRef; + ) -> Result<(), Box>; } #[derive(Clone, Debug)] @@ -63,7 +59,7 @@ impl ShardExec for OpenPGP { &self, threshold: u8, max: u8, - key_discovery: impl AsRef, + key_discovery: &Path, secret: &[u8], output: &mut (impl Write + Send + Sync), ) -> Result<(), Box> { @@ -71,14 +67,12 @@ impl ShardExec for OpenPGP { opgp.shard_and_encrypt(threshold, max, secret, key_discovery, output) } - fn combine( + fn combine( &self, - key_discovery: Option, + key_discovery: Option<&Path>, input: impl Read + Send + Sync, output: &mut impl Write, ) -> Result<(), Box> - where - T: AsRef, { let openpgp = keyfork_shard::openpgp::OpenPGP; let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?; @@ -87,13 +81,11 @@ impl ShardExec for OpenPGP { Ok(()) } - fn decrypt( + fn decrypt( &self, - key_discovery: Option, + key_discovery: Option<&Path>, input: impl Read + Send + Sync, ) -> Result<(), Box> - where - T: AsRef, { let openpgp = keyfork_shard::openpgp::OpenPGP; openpgp.decrypt_one_shard_for_transport(key_discovery, input)?; @@ -189,7 +181,7 @@ impl ShardSubcommands { match format { Some(Format::OpenPGP(o)) => { - o.decrypt(key_discovery.as_ref(), shard_content.as_bytes()) + o.decrypt(key_discovery.as_deref(), shard_content.as_bytes()) } Some(Format::P256(_p)) => todo!(), None => panic!("{COULD_NOT_DETERMINE_FORMAT}"), @@ -206,7 +198,7 @@ impl ShardSubcommands { match format { Some(Format::OpenPGP(o)) => o.combine( - key_discovery.as_ref(), + key_discovery.as_deref(), shard_content.as_bytes(), &mut stdout, ), diff --git a/crates/keyfork/src/cli/wizard.rs b/crates/keyfork/src/cli/wizard.rs index e7a7b79..3509c62 100644 --- a/crates/keyfork/src/cli/wizard.rs +++ b/crates/keyfork/src/cli/wizard.rs @@ -15,6 +15,8 @@ use keyfork_prompt::{ Message, PromptHandler, Terminal, }; +use keyfork_shard::{Format, openpgp::OpenPGP}; + #[derive(thiserror::Error, Debug)] #[error("Invalid PIN length: {0}")] pub struct PinLength(usize); @@ -163,13 +165,13 @@ fn generate_shard_secret( certs.push(cert); } + let opgp = OpenPGP; + if let Some(output_file) = output_file { let output = File::create(output_file)?; - #[allow(deprecated)] - keyfork_shard::openpgp::split(threshold, certs, &seed, output)?; + opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?; } else { - #[allow(deprecated)] - keyfork_shard::openpgp::split(threshold, certs, &seed, std::io::stdout())?; + opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], std::io::stdout())?; } Ok(()) } -- 2.40.1 From d51ee36ace2b2b9d5e6675150d6fe137c9f5e78b Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 19 Feb 2024 05:40:43 -0500 Subject: [PATCH 6/8] keyfork-shard: fixup usage of smex --- crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs | 2 +- crates/keyfork-shard/src/bin/keyfork-shard-remote.rs | 2 +- crates/keyfork/src/cli/shard.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs index 82eb167..7a78cd5 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs @@ -33,7 +33,7 @@ fn run() -> Result<()> { let openpgp = OpenPGP; let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery.as_deref(), messages_file)?; - print!("{}", smex::encode(&bytes)); + print!("{}", smex::encode(bytes)); Ok(()) } diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-remote.rs b/crates/keyfork-shard/src/bin/keyfork-shard-remote.rs index f187176..66f7a29 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-remote.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-remote.rs @@ -20,7 +20,7 @@ fn run() -> Result<()> { let mut bytes = vec![]; remote_decrypt(&mut bytes)?; - print!("{}", smex::encode(&bytes)); + print!("{}", smex::encode(bytes)); Ok(()) } diff --git a/crates/keyfork/src/cli/shard.rs b/crates/keyfork/src/cli/shard.rs index 2d441b8..0fcc04e 100644 --- a/crates/keyfork/src/cli/shard.rs +++ b/crates/keyfork/src/cli/shard.rs @@ -76,7 +76,7 @@ impl ShardExec for OpenPGP { { let openpgp = keyfork_shard::openpgp::OpenPGP; let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?; - write!(output, "{}", smex::encode(&bytes))?; + write!(output, "{}", smex::encode(bytes))?; Ok(()) } -- 2.40.1 From 6a3018e5e860dc71834c0b8701ded1f99733cc31 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 19 Feb 2024 05:41:37 -0500 Subject: [PATCH 7/8] keyfork-shard: bump after mnemonic refactor --- crates/keyfork-shard/src/lib.rs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 4fbecaa..d930bef 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -173,7 +173,6 @@ pub trait Format { reader: impl Read + Send + Sync, ) -> Result<(), Box> { let mut pm = Terminal::new(stdin(), stdout())?; - let wordlist = Wordlist::default(); // parse input let private_keys = private_key_discovery @@ -207,12 +206,12 @@ pub trait Format { let validator = MnemonicSetValidator { word_lengths: [9, 24], }; - let [nonce_mnemonic, pubkey_mnemonic] = pm.prompt_validated_wordlist( - QRCODE_COULDNT_READ, - &wordlist, - 3, - validator.to_fn(), - )?; + let [nonce_mnemonic, pubkey_mnemonic] = pm + .prompt_validated_wordlist::( + QRCODE_COULDNT_READ, + 3, + validator.to_fn(), + )?; let nonce = nonce_mnemonic .as_bytes() @@ -228,8 +227,7 @@ pub trait Format { // create our shared key let our_key = EphemeralSecret::random(); - let our_pubkey_mnemonic = - Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; + let our_pubkey_mnemonic = Mnemonic::from_bytes(PublicKey::from(&our_key).as_bytes())?; let shared_secret = our_key .diffie_hellman(&PublicKey::from(their_pubkey)) .to_bytes(); @@ -279,8 +277,7 @@ pub trait Format { } // safety: size of out_bytes is constant and always % 4 == 0 - let payload_mnemonic = - unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; + let payload_mnemonic = unsafe { Mnemonic::from_raw_bytes(&out_bytes) }; #[cfg(feature = "qrcode")] { -- 2.40.1 From 425aa30aa66aa5b89edfff1bc217cf7e33c0703a Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 19 Feb 2024 05:49:43 -0500 Subject: [PATCH 8/8] keyfork-shard: remove old code! --- crates/keyfork-shard/src/openpgp.rs | 534 +--------------------------- 1 file changed, 13 insertions(+), 521 deletions(-) diff --git a/crates/keyfork-shard/src/openpgp.rs b/crates/keyfork-shard/src/openpgp.rs index 5d5d061..f689959 100644 --- a/crates/keyfork-shard/src/openpgp.rs +++ b/crates/keyfork-shard/src/openpgp.rs @@ -1,27 +1,16 @@ //! OpenPGP Shard functionality. use std::{ - collections::{HashMap, VecDeque}, - io::{stdin, stdout, Read, Write}, + collections::HashMap, + io::{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::{DerivationPath, PathError, VariableLengthSeed}, + derive_util::{DerivationPath, VariableLengthSeed}, XPrv, }; -use keyfork_mnemonic_util::{English, Mnemonic, MnemonicFromStrError, MnemonicGenerationError}; -use keyfork_prompt::{ - validators::{mnemonic::MnemonicSetValidator, Validator}, - Error as PromptError, Message as PromptMessage, PromptHandler, Terminal, -}; use openpgp::{ armor::{Kind, Writer}, cert::{Cert, CertParser, ValidCert}, @@ -36,12 +25,10 @@ use openpgp::{ Marshal, }, types::KeyFlags, - Fingerprint, KeyID, PacketPile, + KeyID, PacketPile, }; pub use sequoia_openpgp as openpgp; -use sha2::Sha256; -use sharks::{Share, Sharks}; -use x25519_dalek::{EphemeralSecret, PublicKey}; +use sharks::Share; mod keyring; use keyring::Keyring; @@ -56,13 +43,7 @@ use smartcard::SmartcardManager; const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_OFFSET: usize = 2; -use super::{ - Format, InvalidData, SharksError, HUNK_VERSION, QRCODE_COULDNT_READ, QRCODE_ERROR, - QRCODE_PROMPT, QRCODE_TIMEOUT, KeyDiscovery -}; - -// 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding -const ENC_LEN: u8 = 4 * 16; +use super::{Format, KeyDiscovery, SharksError}; /// Errors encountered while performing operations using OpenPGP. #[derive(Debug, thiserror::Error)] @@ -71,22 +52,6 @@ pub enum Error { #[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), @@ -103,45 +68,9 @@ pub enum Error { #[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 deriving data. - #[error("Derivation: {0}")] - Derivation(#[from] keyfork_derive_openpgp::derive_util::extended_key::private_key::Error), - - /// An error occurred while parsing a derivation path. - #[error("Derivation path: {0}")] - DerivationPath(#[from] PathError), - - /// 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)] @@ -164,6 +93,12 @@ impl EncryptedMessage { } /// Parse OpenPGP packets for encrypted messages. + /// + /// # Errors + /// The function may return an error if Sequoia is unable to parse packets. + /// + /// # Panics + /// The function may panic if an unexpected packet is encountered. pub fn from_reader(input: impl Read + Send + Sync) -> openpgp::Result> { let mut pkesks = Vec::new(); let mut encrypted_messages = vec![]; @@ -225,8 +160,7 @@ impl EncryptedMessage { H: VerificationHelper + DecryptionHelper, { let mut packets = vec![]; - self.serialize(&mut packets) - .map_err(Error::Sequoia)?; + self.serialize(&mut packets).map_err(Error::Sequoia)?; let mut decryptor = DecryptorBuilder::from_bytes(&packets) .map_err(Error::Sequoia)? @@ -585,68 +519,6 @@ impl KeyDiscovery for &[Cert] { } } -/// 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. -#[deprecated] -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. -#[deprecated] -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< @@ -795,383 +667,3 @@ fn decrypt_metadata( message.decrypt_with(policy, keyring)? }) } - -#[deprecated] -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`]. -#[deprecated] -#[allow(deprecated)] -pub fn decrypt( - certs: &[Cert], - metadata: &EncryptedMessage, - encrypted_messages: &[EncryptedMessage], -) -> Result<()> { - let mut pm = Terminal::new(stdin(), stdout())?; - - let mut nonce_data: Option<[u8; 12]> = None; - let mut pubkey_data: Option<[u8; 32]> = None; - - #[cfg(feature = "qrcode")] - { - pm.prompt_message(PromptMessage::Text(QRCODE_PROMPT.to_string()))?; - if let Ok(Some(hex)) = - keyfork_qrcode::scan_camera(std::time::Duration::from_secs(QRCODE_TIMEOUT), 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(QRCODE_ERROR.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::( - QRCODE_COULDNT_READ, - 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_bytes(PublicKey::from(&our_key).as_bytes())?; - - 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_bytes(&out_bytes) }; - - #[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::Highest) { - pm.prompt_message(PromptMessage::Text( - concat!( - "A QR code will be displayed after this prompt. ", - "Send the QR code back to the operator combining the shards. ", - "Nobody else should scan this QR code." - ) - .to_string(), - ))?; - pm.prompt_message(PromptMessage::Data(qrcode))?; - } - } - - pm.prompt_message(PromptMessage::Text(format!( - "Upon request, these words should be sent: {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. -#[deprecated] -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 path = DerivationPath::from_str("m/7366512'/0'")?; - let seed = VariableLengthSeed::new(&secret); - let xprv = XPrv::new(seed).derive_path(&path)?; - let derived_cert = keyfork_derive_openpgp::derive( - xprv, - &[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. -#[deprecated] -pub fn split(threshold: u8, certs: Vec, secret: &[u8], output: impl Write) -> Result<()> { - let seed = VariableLengthSeed::new(secret); - // build cert to sign encrypted shares - let userid = UserID::from("keyfork-sss"); - let path = DerivationPath::from_str("m/7366512'/0'")?; - let xprv = XPrv::new(seed).derive_path(&path)?; - let derived_cert = keyfork_derive_openpgp::derive( - xprv, - &[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(()) -} -- 2.40.1