diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs index 6554ca5..5e5338f 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-combine-openpgp.rs @@ -7,50 +7,33 @@ use std::{ process::ExitCode, }; -use keyfork_shard::openpgp::{combine, discover_certs, openpgp::Cert, parse_messages}; +use keyfork_shard::{Format, openpgp::OpenPGP}; type Result> = std::result::Result; fn validate( shard: impl AsRef, key_discovery: Option<&str>, -) -> Result<(File, Vec)> { +) -> Result<(File, Option)> { let key_discovery = key_discovery.map(PathBuf::from); key_discovery.as_ref().map(std::fs::metadata).transpose()?; - // Load certs from path - let certs = key_discovery - .map(discover_certs) - .transpose()? - .unwrap_or(vec![]); - - Ok((File::open(shard)?, certs)) + Ok((File::open(shard)?, key_discovery)) } fn run() -> Result<()> { let mut args = env::args(); let program_name = args.next().expect("program name"); let args = args.collect::>(); - let (messages_file, cert_list) = match args.as_slice() { + let (messages_file, key_discovery) = match args.as_slice() { [shard, key_discovery] => validate(shard, Some(key_discovery))?, [shard] => validate(shard, None)?, _ => panic!("Usage: {program_name} [key_discovery]"), }; - let mut encrypted_messages = parse_messages(messages_file)?; + let openpgp = OpenPGP; - let encrypted_metadata = encrypted_messages - .pop_front() - .expect("any pgp encrypted message"); - - let mut bytes = vec![]; - - combine( - cert_list, - &encrypted_metadata, - encrypted_messages.into(), - &mut bytes, - )?; + let bytes = openpgp.decrypt_all_shards_to_secret(key_discovery, messages_file)?; print!("{}", smex::encode(&bytes)); diff --git a/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs b/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs index 748b353..2393a1b 100644 --- a/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs +++ b/crates/keyfork-shard/src/bin/keyfork-shard-decrypt-openpgp.rs @@ -7,47 +7,33 @@ use std::{ process::ExitCode, }; -use keyfork_shard::openpgp::{decrypt, discover_certs, openpgp::Cert, parse_messages}; +use keyfork_shard::{Format, openpgp::OpenPGP}; type Result> = std::result::Result; -fn validate<'a>( - messages_file: impl AsRef, - key_discovery: impl Into>, -) -> Result<(File, Vec)> { - let key_discovery = key_discovery.into().map(PathBuf::from); +fn validate( + shard: impl AsRef, + key_discovery: Option<&str>, +) -> Result<(File, Option)> { + let key_discovery = key_discovery.map(PathBuf::from); key_discovery.as_ref().map(std::fs::metadata).transpose()?; - // Load certs from path - let certs = key_discovery - .map(discover_certs) - .transpose()? - .unwrap_or(vec![]); - - Ok((File::open(messages_file)?, certs)) + Ok((File::open(shard)?, key_discovery)) } fn run() -> Result<()> { let mut args = env::args(); let program_name = args.next().expect("program name"); let args = args.collect::>(); - let (messages_file, cert_list) = match args.as_slice() { - [messages_file, key_discovery] => validate(messages_file, key_discovery.as_str())?, - [messages_file] => validate(messages_file, None)?, - _ => panic!("Usage: {program_name} messages_file [key_discovery]"), + let (messages_file, key_discovery) = match args.as_slice() { + [shard, key_discovery] => validate(shard, Some(key_discovery))?, + [shard] => validate(shard, None)?, + _ => panic!("Usage: {program_name} [key_discovery]"), }; - let mut encrypted_messages = parse_messages(messages_file)?; + let openpgp = OpenPGP; - let encrypted_metadata = encrypted_messages - .pop_front() - .expect("any pgp encrypted message"); - - decrypt( - &cert_list, - &encrypted_metadata, - encrypted_messages.make_contiguous(), - )?; + openpgp.decrypt_one_shard_for_transport(key_discovery, messages_file)?; Ok(()) } diff --git a/crates/keyfork-shard/src/lib.rs b/crates/keyfork-shard/src/lib.rs index 67f309c..ef2e23a 100644 --- a/crates/keyfork-shard/src/lib.rs +++ b/crates/keyfork-shard/src/lib.rs @@ -1,10 +1,13 @@ #![doc = include_str!("../README.md")] -use std::io::{stdin, stdout, Write}; +use std::{ + io::{stdin, stdout, Read, Write}, + path::Path, +}; use aes_gcm::{ - aead::{Aead, AeadCore, OsRng}, - Aes256Gcm, KeyInit, + aead::{consts::U12, Aead, AeadCore, OsRng}, + Aes256Gcm, KeyInit, Nonce, }; use hkdf::Hkdf; use keyfork_mnemonic_util::{Mnemonic, Wordlist}; @@ -16,9 +19,279 @@ use sha2::Sha256; use sharks::{Share, Sharks}; use x25519_dalek::{EphemeralSecret, PublicKey}; +// 256 bit share encrypted is 49 bytes, couple more bytes before we reach max size +const ENC_LEN: u8 = 4 * 16; + #[cfg(feature = "openpgp")] pub mod openpgp; +/// A format to use for splitting and combining secrets. +pub trait Format { + /// The error type returned from any failed operations. + type Error: std::error::Error + 'static; + + /// A type encapsulating the public key recipients of shards. + type PublicKeyData; + + /// A type encapsulating the private key recipients of shards. + type PrivateKeyData; + + /// A type representing the parsed, but encrypted, Shard data. + type ShardData; + + /// A type representing a Signer derived from the secret. + type Signer; + + /// Parse the public key data from a readable type. + /// + /// # Errors + /// The method may return an error if private key data could not be properly parsed from the + /// path. + /// occurred while parsing the public key data. + fn parse_public_key_data( + &self, + key_data_path: impl AsRef, + ) -> Result; + + /// Parse the private key data from a readable type. The private key may not be accessible (it + /// may be hardware only, such as a smartcard), for which this method may return None. + /// + /// # Errors + /// The method may return an error if private key data could not be properly parsed from the + /// path. + fn parse_private_key_data( + &self, + key_data_path: impl AsRef, + ) -> Result; + + /// Parse the Shard file into a processable type. + /// + /// # Errors + /// The method may return an error if the Shard file could not be read from or if the Shard + /// file could not be properly parsed. + fn parse_shard_file( + &self, + shard_file: impl Read + Send + Sync, + ) -> Result; + + /// Write the Shard data to a Shard file. + /// + /// # Errors + /// The method may return an error if the Shard data could not be properly serialized or if the + /// Shard file could not be written to. + fn format_shard_file( + &self, + shard_data: Self::ShardData, + shard_file: impl Write, + ) -> Result<(), Self::Error>; + + /// Derive a Signer from the secret. + /// + /// # Errors + /// This function may return an error if a Signer could not be properly created. + fn derive_signer(&self, secret: &[u8]) -> Result; + + /// Encrypt multiple shares to public keys. + /// + /// # Errors + /// The method may return an error if the share could not be encrypted to a public key or if + /// the ShardData could not be created. + fn generate_shard_data( + &self, + shares: &[Share], + signer: &Self::Signer, + public_keys: Self::PublicKeyData, + ) -> Result; + + /// Decrypt shares and associated metadata from a readable input. For the current version of + /// Keyfork, the only associated metadata is a u8 representing the threshold to combine + /// secrets. + /// + /// # Errors + /// The method may return an error if the shardfile couldn't be read from, if all shards + /// could not be decrypted, or if a shard could not be parsed from the decrypted data. + fn decrypt_all_shards( + &self, + private_keys: Option, + shard_data: Self::ShardData, + ) -> Result<(Vec, u8), Self::Error>; + + /// Decrypt a single share and associated metadata from a reaable input. For the current + /// version of Keyfork, the only associated metadata is a u8 representing the threshold to + /// combine secrets. + /// + /// # Errors + /// The method may return an error if the shardfile couldn't be read from, if a shard could not + /// be decrypted, or if a shard could not be parsed from the decrypted data. + fn decrypt_one_shard( + &self, + private_keys: Option, + shard_data: Self::ShardData, + ) -> Result<(Share, u8), Self::Error>; + + /// Decrypt multiple shares and combine them to recreate a secret. + /// + /// # Errors + /// The method may return an error if the shares can't be decrypted or if the shares can't + /// be combined into a secret. + fn decrypt_all_shards_to_secret( + &self, + private_key_data_path: Option>, + reader: impl Read + Send + Sync, + ) -> Result, Box> { + let private_keys = private_key_data_path + .map(|p| self.parse_private_key_data(p)) + .transpose()?; + let shard_data = self.parse_shard_file(reader)?; + let (shares, threshold) = self.decrypt_all_shards(private_keys, shard_data)?; + + let secret = Sharks(threshold) + .recover(&shares) + .map_err(|e| SharksError::CombineShare(e.to_string()))?; + + Ok(secret) + } + + /// Establish an AES-256-GCM transport key using ECDH, decrypt a single shard, and encrypt the + /// shard to the AES key. + /// + /// # Errors + /// The method may return an error if a share can't be decrypted. The method will not return an + /// error if the camera is inaccessible or if a hardware error is encountered while scanning a + /// QR code; instead, a mnemonic prompt will be used. + fn decrypt_one_shard_for_transport( + &self, + private_key_data_path: Option>, + reader: impl Read + Send + Sync, + ) -> Result<(), Box> { + let mut pm = Terminal::new(stdin(), stdout())?; + let wordlist = Wordlist::default(); + + // parse input + let private_keys = private_key_data_path + .map(|p| self.parse_private_key_data(p)) + .transpose()?; + let shard_data = self.parse_shard_file(reader)?; + + // establish AES-256-GCM key via ECDH + let mut nonce_data: Option<[u8; 12]> = None; + let mut pubkey_data: Option<[u8; 32]> = None; + + // receive remote data via scanning QR code from camera + #[cfg(feature = "qrcode")] + { + pm.prompt_message(PromptMessage::Text( + "Press enter, then present QR code to camera".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( + "Unable to detect QR code, falling back to text".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("Their words: ", &wordlist, 3, validator.to_fn())?; + + let nonce = nonce_mnemonic + .as_bytes() + .try_into() + .map_err(|_| InvalidData)?; + let pubkey = pubkey_mnemonic + .as_bytes() + .try_into() + .map_err(|_| InvalidData)?; + (nonce, pubkey) + } + }; + + // create our shared key + let our_key = EphemeralSecret::random(); + let our_pubkey_mnemonic = + Mnemonic::from_entropy(PublicKey::from(&our_key).as_bytes(), Default::default())?; + let shared_secret = our_key + .diffie_hellman(&PublicKey::from(their_pubkey)) + .to_bytes(); + let hkdf = Hkdf::::new(None, &shared_secret); + let mut hkdf_output = [0u8; 256 / 8]; + hkdf.expand(&[], &mut hkdf_output)?; + let shared_key = Aes256Gcm::new_from_slice(&hkdf_output)?; + + // decrypt a single shard and create the payload + let (share, threshold) = self.decrypt_one_shard(private_keys, shard_data)?; + let mut payload = Vec::from(&share); + payload.insert(0, HUNK_VERSION); + payload.insert(1, threshold); + assert!( + payload.len() <= ENC_LEN as usize, + "invalid share length (too long, max {ENC_LEN} bytes)" + ); + + // encrypt data + let nonce = Nonce::::from_slice(&nonce); + let payload_bytes = shared_key.encrypt(nonce, payload.as_slice())?; + + // convert data to a static-size payload + // NOTE: Padding length is less than u8::MAX because ENC_LEN < u8::MAX + #[allow(clippy::assertions_on_constants)] + { + assert!(ENC_LEN < u8::MAX, "padding byte can be u8"); + } + #[allow(clippy::cast_possible_truncation)] + let mut out_bytes = [payload_bytes.len() as u8; ENC_LEN as usize]; + assert!( + payload_bytes.len() < out_bytes.len(), + "encrypted payload larger than acceptable limit" + ); + out_bytes[..payload_bytes.len()].clone_from_slice(&payload_bytes); + + // NOTE: This previously used a single repeated value as the padding byte, but resulted in + // difficulty when entering in prompts manually, as one's place could be lost due to + // repeated keywords. This is resolved below by having sequentially increasing numbers up to + // but not including the last byte. + #[allow(clippy::cast_possible_truncation)] + for (i, byte) in (out_bytes[payload_bytes.len()..(ENC_LEN as usize - 1)]) + .iter_mut() + .enumerate() + { + *byte = (i % u8::MAX as usize) as u8; + } + + // safety: size of out_bytes is constant and always % 4 == 0 + let payload_mnemonic = + unsafe { Mnemonic::from_raw_entropy(&out_bytes, Default::default()) }; + + #[cfg(feature = "qrcode")] + { + use keyfork_qrcode::{qrencode, ErrorCorrection}; + let mut qrcode_data = our_pubkey_mnemonic.to_bytes(); + qrcode_data.extend(payload_mnemonic.as_bytes()); + if let Ok(qrcode) = qrencode(&smex::encode(&qrcode_data), ErrorCorrection::Lowest) { + pm.prompt_message(PromptMessage::Data(qrcode))?; + } + } + + pm.prompt_message(PromptMessage::Text(format!( + "Our words: {our_pubkey_mnemonic} {payload_mnemonic}" + )))?; + + Ok(()) + } +} + /// Errors encountered while creating or combining shares using Shamir's Secret Sharing. #[derive(thiserror::Error, Debug)] pub enum SharksError { diff --git a/crates/keyfork-shard/src/openpgp.rs b/crates/keyfork-shard/src/openpgp.rs index bea64f4..b2c2bf9 100644 --- a/crates/keyfork-shard/src/openpgp.rs +++ b/crates/keyfork-shard/src/openpgp.rs @@ -56,7 +56,7 @@ use smartcard::SmartcardManager; const SHARD_METADATA_VERSION: u8 = 1; const SHARD_METADATA_OFFSET: usize = 2; -use super::{InvalidData, SharksError, HUNK_VERSION}; +use super::{Format, InvalidData, SharksError, HUNK_VERSION}; // 256 bit share is 49 bytes + some amount of hunk bytes, gives us reasonable padding const ENC_LEN: u8 = 4 * 16; @@ -156,6 +156,18 @@ impl EncryptedMessage { } } + /// Serialize all contents of the message to a writer. + /// + /// # Errors + /// The function may error for any condition in Sequoia's Serialize trait. + pub fn serialize(&self, o: &mut dyn std::io::Write) -> openpgp::Result<()> { + for pkesk in &self.pkesks { + pkesk.serialize(o)?; + } + self.message.serialize(o)?; + Ok(()) + } + /// Decrypt the message with a Sequoia policy and decryptor. /// /// This method creates a container containing the packets and passes the serialized container @@ -200,12 +212,275 @@ impl EncryptedMessage { } } +/// +pub struct OpenPGP; + +impl OpenPGP { + /// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read + /// from a file, or from files one level deep in a directory. + /// + /// # Errors + /// The function may return an error if it is unable to read the directory or if Sequoia is unable + /// to load certificates from the file. + pub fn discover_certs(path: impl AsRef) -> Result> { + let path = path.as_ref(); + + if path.is_file() { + let mut vec = vec![]; + for cert in CertParser::from_file(path).map_err(Error::Sequoia)? { + vec.push(cert.map_err(Error::Sequoia)?); + } + Ok(vec) + } else { + let mut vec = vec![]; + for entry in path + .read_dir() + .map_err(Error::Io)? + .filter_map(Result::ok) + .filter(|p| p.path().is_file()) + { + vec.push(Cert::from_file(entry.path()).map_err(Error::Sequoia)?); + } + Ok(vec) + } + } +} + +impl Format for OpenPGP { + type Error = Error; + type PublicKeyData = Vec; + type PrivateKeyData = Vec; + type ShardData = Vec; + type Signer = openpgp::crypto::KeyPair; + + fn parse_public_key_data( + &self, + key_data_path: impl AsRef, + ) -> std::result::Result { + Self::discover_certs(key_data_path) + } + + fn parse_private_key_data( + &self, + key_data_path: impl AsRef, + ) -> std::result::Result { + Self::discover_certs(key_data_path) + } + + fn parse_shard_file( + &self, + shard_file: impl Read + Send + Sync, + ) -> Result { + let mut pkesks = Vec::new(); + let mut encrypted_messages = vec![]; + + for packet in PacketPile::from_reader(shard_file) + .map_err(Error::Sequoia)? + .into_children() + { + match packet { + Packet::PKESK(p) => pkesks.push(p), + Packet::SEIP(s) => { + encrypted_messages.push(EncryptedMessage::new(&mut pkesks, s)); + } + s => { + panic!("Invalid variant found: {}", s.tag()); + } + } + } + + Ok(encrypted_messages) + } + + fn derive_signer(&self, secret: &[u8]) -> Result { + let userid = UserID::from("keyfork-sss"); + let kdr = DerivationRequest::new( + DerivationAlgorithm::Ed25519, + &DerivationPath::from_str("m/7366512'/0'")?, + ) + .derive_with_master_seed(secret.to_vec())?; + let derived_cert = keyfork_derive_openpgp::derive( + kdr, + &[KeyFlags::empty().set_certification().set_signing()], + &userid, + )?; + + let signing_key = derived_cert + .primary_key() + .parts_into_secret() + .map_err(Error::Sequoia)? + .key() + .clone() + .into_keypair() + .map_err(Error::Sequoia)?; + + Ok(signing_key) + } + + fn format_shard_file( + &self, + shard_data: Self::ShardData, + shard_file: impl Write, + ) -> Result<(), Self::Error> { + let mut writer = Writer::new(shard_file, Kind::Message).map_err(Error::SequoiaIo)?; + for message in shard_data { + message.serialize(&mut writer).map_err(Error::Sequoia)?; + } + writer.finalize().map_err(Error::SequoiaIo)?; + Ok(()) + } + + fn generate_shard_data( + &self, + shares: &[Share], + signer: &Self::Signer, + public_keys: Self::PublicKeyData, + ) -> std::result::Result { + let policy = StandardPolicy::new(); + let mut total_recipients = vec![]; + let mut messages = vec![]; + + for (share, cert) in shares.iter().zip(public_keys) { + total_recipients.push(cert.clone()); + let valid_cert = cert.with_policy(&policy, None).map_err(Error::Sequoia)?; + let encryption_keys = get_encryption_keys(&valid_cert).collect::>(); + + let mut message_output = vec![]; + let message = Message::new(&mut message_output); + let message = Encryptor2::for_recipients( + message, + encryption_keys + .iter() + .map(|k| Recipient::new(KeyID::wildcard(), k.key())), + ) + .build() + .map_err(Error::Sequoia)?; + let message = Signer::new(message, signer.clone()) + .build() + .map_err(Error::Sequoia)?; + let mut message = LiteralWriter::new(message) + .build() + .map_err(Error::Sequoia)?; + // NOTE: This shouldn't be an alloc, but it's a minor alloc, so it's fine. + message + .write_all(&Vec::from(share)) + .map_err(Error::SequoiaIo)?; + message.finalize().map_err(Error::Sequoia)?; + + messages.push(message_output); + } + + // A little bit of back and forth, we're going to parse the messages just to serialize them + // later. + let message = messages.into_iter().flatten().collect::>(); + let data = self.parse_shard_file(message.as_slice())?; + Ok(data) + } + + fn decrypt_all_shards( + &self, + private_keys: Option, + mut shard_data: Self::ShardData, + ) -> std::result::Result<(Vec, u8), Self::Error> { + // Be as liberal as possible when decrypting. + // We don't want to invalidate someone's keys just because the old sig expired. + let policy = NullPolicy::new(); + let mut keyring = Keyring::new(private_keys.unwrap_or_default())?; + let mut manager = SmartcardManager::new()?; + + let metadata = shard_data.remove(0); + let metadata_content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?; + + let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?; + + keyring.set_root_cert(root_cert.clone()); + manager.set_root_cert(root_cert.clone()); + + // Generate a controlled binding from certificates to encrypted messages. This is stable + // because we control the order packets are encrypted and certificates are stored. + + // TODO: remove alloc, convert EncryptedMessage to &EncryptedMessage + let mut messages: HashMap = certs + .iter() + .map(Cert::keyid) + .zip(shard_data) + .collect(); + let mut decrypted_messages = + decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; + + // clean decrypted messages from encrypted messages + messages.retain(|k, _v| !decrypted_messages.contains_key(k)); + + let left_from_threshold = threshold as usize - decrypted_messages.len(); + if left_from_threshold > 0 { + #[allow(clippy::cast_possible_truncation)] + let new_messages = decrypt_with_manager( + left_from_threshold as u8, + &mut messages, + &certs, + &policy, + &mut manager, + )?; + decrypted_messages.extend(new_messages); + } + + let shares = decrypted_messages + .values() + .map(|message| Share::try_from(message.as_slice())) + .collect::, &str>>() + .map_err(|e| SharksError::Share(e.to_string()))?; + Ok((shares, threshold)) + } + + fn decrypt_one_shard( + &self, + private_keys: Option, + mut shard_data: Self::ShardData, + ) -> std::result::Result<(Share, u8), Self::Error> { + let policy = NullPolicy::new(); + let mut keyring = Keyring::new(private_keys.unwrap_or_default())?; + let mut manager = SmartcardManager::new()?; + + let metadata = shard_data.remove(0); + let metadata_content = decrypt_metadata(&metadata, &policy, &mut keyring, &mut manager)?; + + let (threshold, root_cert, certs) = decode_metadata_v1(&metadata_content)?; + + keyring.set_root_cert(root_cert.clone()); + manager.set_root_cert(root_cert.clone()); + let mut messages: HashMap = certs + .iter() + .map(Cert::keyid) + .zip(shard_data) + .collect(); + + let decrypted_messages = + decrypt_with_keyring(&mut messages, &certs, &policy, &mut keyring)?; + + if let Some(message) = decrypted_messages.into_values().next() { + let share = Share::try_from(message.as_slice()).map_err(|e| SharksError::Share(e.to_string()))?; + return Ok((share, threshold)); + } + + let decrypted_messages = + decrypt_with_manager(1, &mut messages, &certs, &policy, &mut manager)?; + + if let Some(message) = decrypted_messages.into_values().next() { + let share = Share::try_from(message.as_slice()).map_err(|e| SharksError::Share(e.to_string()))?; + return Ok((share, threshold)); + } + + panic!("unable to decrypt shard"); + } +} + /// Read all OpenPGP certificates in a path and return a [`Vec`] of them. Certificates are read /// from a file, or from files one level deep in a directory. /// /// # Errors /// The function may return an error if it is unable to read the directory or if Sequoia is unable /// to load certificates from the file. +#[deprecated] pub fn discover_certs(path: impl AsRef) -> Result> { let path = path.as_ref(); @@ -238,6 +513,7 @@ pub fn discover_certs(path: impl AsRef) -> Result> { /// # Panics /// When given packets that are not a list of PKESK packets and SEIP packets, the function panics. /// The `split` utility should never give packets that are not in this format. +#[deprecated] pub fn parse_messages(reader: impl Read + Send + Sync) -> Result> { let mut pkesks = Vec::new(); let mut encrypted_messages = VecDeque::new(); @@ -409,6 +685,7 @@ fn decrypt_metadata( }) } +#[deprecated] fn decrypt_one( messages: Vec, certs: &[Cert], @@ -458,6 +735,8 @@ fn decrypt_one( /// The function may panic if a share is decrypted but has a length larger than 256 bits. This is /// atypical usage and should not be encountered in normal usage, unless something that is not a /// Keyfork seed has been fed into [`split`]. +#[deprecated] +#[allow(deprecated)] pub fn decrypt( certs: &[Cert], metadata: &EncryptedMessage,