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 2f12e58..6b49714 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 b4805d2..1749833 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 6907a2c..84e869d 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)?; @@ -935,6 +969,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, @@ -1022,6 +1057,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(())