From c95ed0b72936013c0558b5a166251bf2c6c8f07b Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 24 Jan 2025 08:02:30 -0500 Subject: [PATCH] keyfork shard metadata: initial commit --- crates/keyfork-shard/src/lib.rs | 38 +++++++++++++++- crates/keyfork-shard/src/openpgp.rs | 20 +++++++++ crates/keyfork/src/cli/shard.rs | 68 +++++++++++++++++++++++++++++ 3 files changed, 124 insertions(+), 2 deletions(-) diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 252ac96..5f94be5 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -3,8 +3,8 @@ use std::{ io::{stdin, stdout, Read, Write}, - sync::Mutex, rc::Rc, + sync::Mutex, }; use aes_gcm::{ @@ -140,7 +140,7 @@ pub trait Format { prompt: Rc>>, ) -> Result<(Vec, u8), Self::Error>; - /// Decrypt a single share and associated metadata from a reaable input. For the current + /// Decrypt a single share 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. /// @@ -154,6 +154,40 @@ pub trait Format { prompt: Rc>>, ) -> Result<(Share, u8), Self::Error>; + /// Decrypt the public keys and metadata from encrypted data. + /// + /// # Errors + /// The method may return an error if hte shardfile couldn't be read from or if the metadata + /// could neither be encrypted nor parsed. + fn decrypt_metadata( + &self, + private_keys: Option, + encrypted_data: &[Self::EncryptedData], + prompt: Rc>>, + ) -> std::result::Result<(u8, Vec), Self::Error>; + + /// Decrypt the public keys and metadata from a Shardfile. + /// + /// # Errors + /// The method may return an error if hte shardfile couldn't be read from or if the metadata + /// could neither be encrypted nor parsed. + fn decrypt_metadata_from_file( + &self, + private_key_discovery: Option>, + reader: impl Read + Send + Sync, + prompt: Box, + ) -> Result<(u8, Vec), Self::Error> { + let private_keys = private_key_discovery + .map(|p| p.discover_private_keys()) + .transpose()?; + let encrypted_messages = self.parse_shard_file(reader)?; + self.decrypt_metadata( + private_keys, + &encrypted_messages, + Rc::new(Mutex::new(prompt)), + ) + } + /// Decrypt multiple shares and combine them to recreate a secret. /// /// # Errors diff --git a/crates/keyfork-shard/src/openpgp.rs b/crates/keyfork-shard/src/openpgp.rs index 024334f..cd917fc 100644 --- a/crates/keyfork-shard/src/openpgp.rs +++ b/crates/keyfork-shard/src/openpgp.rs @@ -549,6 +549,26 @@ impl Format for OpenPGP { panic!("unable to decrypt shard"); } + + fn decrypt_metadata( + &self, + private_keys: Option, + encrypted_data: &[Self::EncryptedData], + prompt: Rc>>, + ) -> std::result::Result<(u8, Vec), Self::Error> { + let policy = NullPolicy::new(); + let mut keyring = Keyring::new(private_keys.unwrap_or_default(), prompt.clone())?; + let mut manager = SmartcardManager::new(prompt.clone())?; + let mut encrypted_messages = encrypted_data.iter(); + + let metadata = encrypted_messages + .next() + .expect(bug!(METADATA_MESSAGE_MISSING)); + let metadata_content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?; + + let (threshold, _root_cert, certs) = decode_metadata_v1(&metadata_content)?; + Ok((threshold, certs)) + } } impl KeyDiscovery for &Path { diff --git a/crates/keyfork/src/cli/shard.rs b/crates/keyfork/src/cli/shard.rs index 3e3b045..8a85d8f 100644 --- a/crates/keyfork/src/cli/shard.rs +++ b/crates/keyfork/src/cli/shard.rs @@ -50,6 +50,14 @@ trait ShardExec { key_discovery: Option<&Path>, input: impl Read + Send + Sync, ) -> Result<(), Box>; + + fn metadata( + &self, + key_discovery: Option<&Path>, + input: impl Read + Send + Sync, + output_pubkeys: &mut impl Write, + output: &mut impl Write, + ) -> Result<(), Box>; } #[derive(Clone, Debug)] @@ -92,6 +100,31 @@ impl ShardExec for OpenPGP { openpgp.decrypt_one_shard_for_transport(key_discovery, input, prompt)?; Ok(()) } + + fn metadata( + &self, + key_discovery: Option<&Path>, + input: impl Read + Send + Sync, + output_pubkeys: &mut impl Write, + output: &mut impl Write, + ) -> Result<(), Box> { + use keyfork_derive_openpgp::openpgp::{ + serialize::Marshal, + armor::{Writer, Kind}, + }; + + let openpgp = keyfork_shard::openpgp::OpenPGP; + let prompt = default_handler()?; + + let (threshold, certs) = openpgp.decrypt_metadata_from_file(key_discovery, input, prompt)?; + let mut writer = Writer::new(output_pubkeys, Kind::PublicKey)?; + for cert in certs { + cert.serialize(&mut writer)?; + } + writer.finalize()?; + writeln!(output, "Threshold: {threshold}")?; + Ok(()) + } } #[derive(Clone, Debug)] @@ -141,6 +174,20 @@ pub enum ShardSubcommands { /// The path to discover private keys from. key_discovery: Option, }, + + /// Decrypt metadata for a shardfile, including the threshold and the public keys. Public keys + /// are serialized to a file. + Metadata { + /// The path to load the Shardfile from. + shardfile: PathBuf, + + /// The path to write public keys to. + #[arg(long)] + output_pubkeys: PathBuf, + + /// The path to discover private keys from. + key_discovery: Option, + } } impl ShardSubcommands { @@ -209,6 +256,27 @@ impl ShardSubcommands { None => panic!("{COULD_NOT_DETERMINE_FORMAT}"), } } + ShardSubcommands::Metadata { shardfile, output_pubkeys, key_discovery } => { + let shard_content = std::fs::read_to_string(shardfile)?; + if shard_content.contains("BEGIN PGP MESSAGE") { + let _ = format.insert(Format::OpenPGP(OpenPGP)); + } + + let mut output_pubkeys_file = std::fs::File::create(output_pubkeys)?; + + match format { + Some(Format::OpenPGP(o)) => o.metadata( + key_discovery.as_deref(), + shard_content.as_bytes(), + &mut output_pubkeys_file, + &mut stdout, + ), + Some(Format::P256(_p)) => { + todo!() + } + None => panic!("{COULD_NOT_DETERMINE_FORMAT}"), + } + } } } }