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(()) -}