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..7a78cd5 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs @@ -7,52 +7,33 @@ use std::{ process::ExitCode, }; -use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages}; +use keyfork_shard::{openpgp::OpenPGP, Format}; 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 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, - )?; - - print!("{}", smex::encode(&bytes)); + let openpgp = OpenPGP; + 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 748b353..3b06219 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.as_deref(), messages_file)?; 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-shard/src/bin/keyfork-shard-split-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-split-openpgp.rs index 29d6f0a..3423ef6 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.as_path(), std::io::stdout())?; Ok(()) } diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 01e01d1..d930bef 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -1,10 +1,10 @@ #![doc = include_str!("../README.md")] -use std::io::{stdin, stdout, Write}; +use std::io::{stdin, stdout, Read, Write}; 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 +16,338 @@ 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 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. + type Error: std::error::Error + 'static; + + /// 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 EncryptedData; + + /// 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::PublicKey], + threshold: u8, + ) -> Result; + + /// 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; + + /// 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, Self::Error>; + + /// 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, + 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 + /// 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, + encrypted_messages: &[Self::EncryptedData], + ) -> 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, + encrypted_data: &[Self::EncryptedData], + ) -> 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_discovery: Option>, + reader: impl Read + Send + Sync, + ) -> Result, Box> { + 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)?; + + 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_discovery: Option>, + reader: impl Read + Send + Sync, + ) -> Result<(), Box> { + let mut pm = Terminal::new(stdin(), stdout())?; + + // parse input + let private_keys = private_key_discovery + .map(|p| p.discover_private_keys()) + .transpose()?; + let encrypted_messages = 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, + 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_bytes(PublicKey::from(&our_key).as_bytes())?; + 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, &encrypted_messages)?; + 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_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(()) + } + + /// 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_discovery: impl KeyDiscovery, + 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 = public_key_discovery.discover_public_keys()?; + 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. #[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..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::{ - 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 -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)] @@ -163,6 +92,61 @@ 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![]; + + 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. + fn serialize(&self, mut o: impl std::io::Write + Send + Sync) -> openpgp::Result<()> { + for pkesk in &self.pkesks { + 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)?; + } + 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(()) + } + /// Decrypt the message with a Sequoia policy and decryptor. /// /// This method creates a container containing the packets and passes the serialized container @@ -176,23 +160,7 @@ 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) - .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)?; + self.serialize(&mut packets).map_err(Error::Sequoia)?; let mut decryptor = DecryptorBuilder::from_bytes(&packets) .map_err(Error::Sequoia)? @@ -207,64 +175,348 @@ impl EncryptedMessage { } } -/// 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(); +pub struct OpenPGP; - 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)?); +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) } - Ok(vec) - } else { - let mut vec = vec![]; - for entry in path - .read_dir() - .map_err(Error::Io)? - .filter_map(Result::ok) - .filter(|p| p.path().is_file()) - { - vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?); - } - Ok(vec) } } -/// Parse messages from a type implementing [`Read`] and store them as [`EncryptedMessage`]. -/// -/// # Errors -/// The function may return an error if the reader has run out of data or if the data is not -/// properly formatted OpenPGP messages. -/// -/// # Panics -/// When given packets that are not a list of PKESK packets and SEIP packets, the function panics. -/// The `split` utility should never give packets that are not in this format. -pub fn parse_messages(reader: impl Read + Send + Sync) -> Result> { - let mut pkesks = Vec::new(); - let mut encrypted_messages = VecDeque::new(); +impl Format for OpenPGP { + type Error = Error; + type PublicKey = Cert; + type PrivateKeyData = Vec; + type SigningKey = Cert; + type EncryptedData = EncryptedMessage; - 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()); - } - } + /// 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") } - Ok(encrypted_messages) + fn format_encrypted_header( + &self, + signing_key: &Self::SigningKey, + key_data: &[Self::PublicKey], + threshold: u8, + ) -> 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 + 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)?; + + // 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( + &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)?; + + 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_shard_file( + &self, + shard_file: impl Read + Send + Sync, + ) -> Result, Self::Error> { + EncryptedMessage::from_reader(shard_file).map_err(Error::Sequoia) + } + + fn format_shard_file( + &self, + 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 encrypted_data { + message.serialize(&mut writer).map_err(Error::Sequoia)?; + } + writer.finalize().map_err(Error::SequoiaIo)?; + Ok(()) + } + + fn decrypt_all_shards( + &self, + private_keys: Option, + 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. + let policy = NullPolicy::new(); + let mut keyring = Keyring::new(private_keys.unwrap_or_default())?; + let mut manager = SmartcardManager::new()?; + + 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)?; + + 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(encrypted_messages.cloned()) + .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, + 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 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(encrypted_messages.cloned()) + .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"); + } +} + +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()) + } } fn get_encryption_keys<'a>( @@ -415,378 +667,3 @@ fn decrypt_metadata( message.decrypt_with(policy, keyring)? }) } - -fn decrypt_one( - messages: Vec, - certs: &[Cert], - metadata: &EncryptedMessage, -) -> Result<(Vec, u8, Cert)> { - let policy = NullPolicy::new(); - - let mut keyring = Keyring::new(certs)?; - let mut manager = SmartcardManager::new()?; - - let content = decrypt_metadata(metadata, &policy, &mut keyring, &mut manager)?; - - let (threshold, root_cert, certs) = decode_metadata_v1(&content)?; - - keyring.set_root_cert(root_cert.clone()); - manager.set_root_cert(root_cert.clone()); - - let mut messages: HashMap = - certs.iter().map(Cert::keyid).zip(messages).collect(); - - let decrypted_messages = decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; - - if let Some(message) = decrypted_messages.into_values().next() { - return Ok((message, threshold, root_cert)); - } - - let decrypted_messages = decrypt_with_manager(1, &mut messages, &certs, &policy, &mut manager)?; - - if let Some(message) = decrypted_messages.into_values().next() { - return Ok((message, threshold, root_cert)); - } - - unreachable!("smartcard manager should always decrypt") -} - -/// Decrypt a single shard, encrypt to a remote operator, and present the transport shard as a QR -/// code and mnemonic to be sent to the remote operator. -/// -/// # Errors -/// -/// The function may error if an error occurs while displaying a prompt or while decrypting the -/// shard. An error will not be returned if the camera has a hardware error while scanning a QR -/// code; instead, a mnemonic prompt will be used. -/// -/// # Panics -/// -/// The function may panic if a share is decrypted but has a length larger than 256 bits. This is -/// atypical usage and should not be encountered in normal usage, unless something that is not a -/// Keyfork seed has been fed into [`split`]. -pub fn decrypt( - certs: &[Cert], - metadata: &EncryptedMessage, - encrypted_messages: &[EncryptedMessage], -) -> Result<()> { - let mut pm = Terminal::new(stdin(), stdout())?; - - let 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. -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. -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(()) -} 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 d68a193..2a0339a 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.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 6439b5f..0fcc04e 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}, @@ -31,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( + &self, + key_discovery: Option<&Path>, + input: impl Read + Send + Sync, output: &mut impl Write, ) -> Result<(), Box>; - fn combine( + fn decrypt( &self, - key_discovery: Option, + key_discovery: Option<&Path>, input: impl Read + Send + Sync, - output: &mut impl Write, - ) -> Result<(), Box> - where - T: AsRef; - - fn decrypt( - &self, - key_discovery: Option, - input: impl Read + Send + Sync, - ) -> Result<(), Box> - where - T: AsRef; + ) -> Result<(), Box>; } #[derive(Clone, Debug)] @@ -62,77 +59,36 @@ impl ShardExec for OpenPGP { &self, threshold: u8, max: u8, - key_discovery: impl AsRef, + key_discovery: &Path, 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( + 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 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, - )?; - - write!(output, "{}", smex::encode(&bytes))?; + let openpgp = keyfork_shard::openpgp::OpenPGP; + let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, input)?; + write!(output, "{}", smex::encode(bytes))?; Ok(()) } - fn decrypt( + fn decrypt( &self, - key_discovery: Option, + key_discovery: Option<&Path>, input: impl Read + Send + Sync, ) -> Result<(), Box> - 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(()) } } @@ -225,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}"), @@ -242,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 3c6e1db..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,11 +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)?; - keyfork_shard::openpgp::split(threshold, certs, &seed, output)?; + opgp.shard_and_encrypt(threshold, certs.len() as u8, &seed, &certs[..], output)?; } else { - 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(()) }