diff options
-rw-r--r-- | openpgp/NEWS | 1 | ||||
-rw-r--r-- | openpgp/examples/reply-encrypted.rs | 232 | ||||
-rw-r--r-- | openpgp/src/serialize/stream.rs | 191 |
3 files changed, 419 insertions, 5 deletions
diff --git a/openpgp/NEWS b/openpgp/NEWS index 9a0da59d..fd5e70bc 100644 --- a/openpgp/NEWS +++ b/openpgp/NEWS @@ -5,6 +5,7 @@ * Changes in 1.2.0 ** New functionality - ComponentBundle::attestations + - Encryptor::with_session_key - Signature::verify_user_attribute_attestation - Signature::verify_userid_attestation - SignatureBuilder::pre_sign diff --git a/openpgp/examples/reply-encrypted.rs b/openpgp/examples/reply-encrypted.rs new file mode 100644 index 00000000..f0ed293c --- /dev/null +++ b/openpgp/examples/reply-encrypted.rs @@ -0,0 +1,232 @@ +//! Demonstates how to reply to an encrypted message without having +//! everyone's certs. +//! +//! This example demonstrates how to fall back to the original +//! message's session key in order to encrypt a reply. +//! +//! Replying to an encrypted message usually requires the encryption +//! (sub)keys for every recipient. If even one key is not available, +//! it is not possible to encrypt the new session key. Rather than +//! falling back to replying unencrypted, one can reuse the original +//! message's session key that was encrypted for every recipient and +//! reuse the original PKESKs. +//! +//! Decrypts an asymmetrically-encrypted OpenPGP message using the +//! openpgp crate, Sequoia's low-level API, remembering the session +//! key and PKESK packets. It then encrypts a new message reusing +//! both the session key and PKESK packets. +//! +//! # Examples +//! +//! First, we generate two keys. Second, we encrypt a message for +//! both certs. We then decrypt the original message using Alice's +//! key and this example program, composing an encrypted reply reusing +//! the session key and PKESK packets. Finally, we decrypt the reply +//! using Bob's key. +//! +//! ```sh +//! $ sqop generate-key alice@example.org > alice.pgp +//! $ sqop generate-key bob@example.org > bob.pgp +//! $ echo Original message | sqop encrypt alice.pgp bob.pgp > original.pgp +//! $ echo Reply | cargo run -p sequoia-openpgp --example reply-encrypted -- \ +//! original.pgp alice.pgp > reply.pgp +//! $ sqop decrypt --session-key-out original.sk bob.pgp < reply.pgp +//! Encrypted using AES with 256-bit key +//! - Original message: +//! Original message +//! - Reusing (AES with 256-bit key, 62F3EADC...) with 2 PKESK packets +//! Reply +//! $ cat original.sk +//! 9:62F3EADC98E1D3D34495E79264B5959391B4FABB2B2A2B7E03861F92D0B03161 +//! ``` + +use std::collections::HashMap; +use std::env; +use std::io; + +use anyhow::Context; + +use sequoia_openpgp as openpgp; + +use openpgp::{KeyID, Fingerprint}; +use openpgp::cert::prelude::*; +use openpgp::packet::prelude::*; +use openpgp::crypto::{KeyPair, SessionKey}; +use openpgp::types::SymmetricAlgorithm; +use openpgp::parse::{Parse, stream::*}; +use openpgp::serialize::{Serialize, stream::*}; +use openpgp::policy::Policy; +use openpgp::policy::StandardPolicy as P; + +pub fn main() -> openpgp::Result<()> { + let p = &P::new(); + + let args: Vec<String> = env::args().collect(); + if args.len() < 3 { + return Err(anyhow::anyhow!("Reply-to-all without having all certs.\n\n\ + Usage: {} <encrypted-msg> <keyfile> [<keyfile>...] \ + <plaintext >ciphertext\n", args[0])); + } + + let encrypted_message = &args[1]; + + // Read the transferable secret keys from the given files. + let certs = + args[2..].iter().map(|f| { + openpgp::Cert::from_file(f) + }).collect::<openpgp::Result<Vec<_>>>() + .context("Failed to read key")?; + + // Now, create a decryptor with a helper using the given Certs. + let mut decryptor = + DecryptorBuilder::from_file(encrypted_message)? + .with_policy(p, None, Helper::new(p, certs))?; + + // Finally, stream the decrypted data to stderr. + eprintln!("- Original message:"); + io::copy(&mut decryptor, &mut io::stderr()) + .context("Decryption failed")?; + + let (algo, sk, pkesks) = decryptor.into_helper().recycling_bin.unwrap(); + eprintln!("- Reusing ({}, {}) with {} PKESK packets", + algo, openpgp::fmt::hex::encode(&sk), pkesks.len()); + + // Compose a writer stack corresponding to the output format and + // packet structure we want. + let mut sink = io::stdout(); + + // Stream an OpenPGP message. + let message = Message::new(&mut sink); + let mut message = Armorer::new(message).build()?; + + // Emit the stashed PKESK packets. + for p in pkesks { + openpgp::Packet::from(p).serialize(&mut message)?; + } + + // We want to encrypt a literal data packet. + let message = Encryptor::with_session_key(message, algo, sk)? + .build().context("Failed to create encryptor")?; + + let mut message = LiteralWriter::new(message).build() + .context("Failed to create literal writer")?; + + // Copy stdin to our writer stack to encrypt the data. + io::copy(&mut io::stdin(), &mut message) + .context("Failed to encrypt")?; + + // Finally, finalize the OpenPGP message by tearing down the + // writer stack. + message.finalize()?; + + + Ok(()) +} + +/// This helper provides secrets for the decryption, fetches public +/// keys for the signature verification and implements the +/// verification policy. +struct Helper { + keys: HashMap<KeyID, (Fingerprint, KeyPair)>, + recycling_bin: Option<(SymmetricAlgorithm, SessionKey, Vec<PKESK>)>, +} + +impl Helper { + /// Creates a Helper for the given Certs with appropriate secrets. + fn new(p: &dyn Policy, certs: Vec<openpgp::Cert>) -> Self { + // Map (sub)KeyIDs to primary fingerprints and secrets. + let mut keys = HashMap::new(); + for cert in certs { + for ka in cert.keys().unencrypted_secret().with_policy(p, None) + .supported() + .for_storage_encryption().for_transport_encryption() + { + keys.insert(ka.key().keyid(), + (cert.fingerprint(), + ka.key().clone().into_keypair().unwrap())); + } + } + + Helper { + keys, + recycling_bin: None, + } + } +} + +impl DecryptionHelper for Helper { + fn decrypt<D>(&mut self, + pkesks: &[openpgp::packet::PKESK], + _skesks: &[openpgp::packet::SKESK], + sym_algo: Option<SymmetricAlgorithm>, + mut decrypt: D) + -> openpgp::Result<Option<openpgp::Fingerprint>> + where D: FnMut(SymmetricAlgorithm, &SessionKey) -> bool + { + // Try each PKESK until we succeed. + let mut recipient = None; + let mut encryption_context = None; + for pkesk in pkesks { + if let Some((fp, pair)) = self.keys.get_mut(pkesk.recipient()) { + if pkesk.decrypt(pair, sym_algo) + .map(|(algo, session_key)| { + let success = decrypt(algo, &session_key); + if success { + // Keep a copy the algorithm, session key, + // and all PKESK packets for the reply. + encryption_context = + Some(( + algo, + session_key.clone(), + pkesks.iter().cloned().collect(), + )); + } + success + }) + .unwrap_or(false) + { + recipient = Some(fp.clone()); + break; + } + } + } + + // Store for later use. + self.recycling_bin = encryption_context; + Ok(recipient) + } +} + +impl VerificationHelper for Helper { + fn get_certs(&mut self, _ids: &[openpgp::KeyHandle]) + -> openpgp::Result<Vec<openpgp::Cert>> { + Ok(Vec::new()) // Feed the Certs to the verifier here. + } + fn check(&mut self, structure: MessageStructure) + -> openpgp::Result<()> { + for layer in structure.iter() { + match layer { + MessageLayer::Compression { algo } => + eprintln!("Compressed using {}", algo), + MessageLayer::Encryption { sym_algo, aead_algo } => + if let Some(aead_algo) = aead_algo { + eprintln!("Encrypted and protected using {}/{}", + sym_algo, aead_algo); + } else { + eprintln!("Encrypted using {}", sym_algo); + }, + MessageLayer::SignatureGroup { ref results } => + for result in results { + match result { + Ok(GoodChecksum { ka, .. }) => { + eprintln!("Good signature from {}", ka.cert()); + }, + Err(e) => + eprintln!("Error: {:?}", e), + } + } + } + } + Ok(()) // Implement your verification policy here. + } +} diff --git a/openpgp/src/serialize/stream.rs b/openpgp/src/serialize/stream.rs index 7b713107..6ebf1925 100644 --- a/openpgp/src/serialize/stream.rs +++ b/openpgp/src/serialize/stream.rs @@ -2297,6 +2297,7 @@ impl<'a> Recipient<'a> { /// ``` pub struct Encryptor<'a> { inner: writer::BoxStack<'a, Cookie>, + session_key: Option<SessionKey>, recipients: Vec<Recipient<'a>>, passwords: Vec<Password>, sym_algo: SymmetricAlgorithm, @@ -2366,8 +2367,9 @@ impl<'a> Encryptor<'a> { where R: IntoIterator, R::Item: Into<Recipient<'a>>, { - Self { + Self { inner: inner.into(), + session_key: None, recipients: recipients.into_iter().map(|r| r.into()).collect(), passwords: Vec::new(), sym_algo: Default::default(), @@ -2409,6 +2411,7 @@ impl<'a> Encryptor<'a> { { Self { inner: inner.into(), + session_key: None, recipients: Vec::new(), passwords: passwords.into_iter().map(|p| p.into()).collect(), sym_algo: Default::default(), @@ -2418,6 +2421,174 @@ impl<'a> Encryptor<'a> { } } + /// Creates a new encryptor for the given algorithm and session + /// key. + /// + /// Usually, the encryptor creates a session key and decrypts it + /// for the given recipients and passwords. Using this function, + /// the session key can be supplied instead. There are two main + /// use cases for this: + /// + /// - Replying to an encrypted message usually requires the + /// encryption (sub)keys for every recipient. If even one key + /// is not available, it is not possible to encrypt the new + /// session key. Rather than falling back to replying + /// unencrypted, one can reuse the original message's session + /// key that was encrypted for every recipient and reuse the + /// original [`PKESK`](crate::packet::PKESK)s. + /// + /// - Using the encryptor if the session key is transmitted or + /// derived using a scheme not supported by Sequoia. + /// + /// To add more passwords, use [`Encryptor::add_passwords`]. To + /// add recipients, use [`Encryptor::add_recipients`]. + /// + /// # Examples + /// + /// This example demonstrates how to fall back to the original + /// message's session key in order to encrypt a reply. + /// + /// ``` + /// # fn main() -> sequoia_openpgp::Result<()> { + /// # use std::io::{self, Write}; + /// # use sequoia_openpgp as openpgp; + /// # use openpgp::{KeyHandle, KeyID, Fingerprint, Result}; + /// # use openpgp::cert::prelude::*; + /// # use openpgp::packet::prelude::*; + /// # use openpgp::crypto::{KeyPair, SessionKey}; + /// # use openpgp::types::SymmetricAlgorithm; + /// # use openpgp::parse::{Parse, stream::*}; + /// # use openpgp::serialize::{Serialize, stream::*}; + /// # use openpgp::policy::{Policy, StandardPolicy}; + /// # let p = &StandardPolicy::new(); + /// # + /// // Generate two keys. + /// let (alice, _) = CertBuilder::general_purpose( + /// None, Some("Alice Lovelace <alice@example.org>")).generate()?; + /// let (bob, _) = CertBuilder::general_purpose( + /// None, Some("Bob Babbage <bob@example.org>")).generate()?; + /// + /// // Encrypt a message for both keys. + /// let recipients = vec![&alice, &bob].into_iter().flat_map(|cert| { + /// cert.keys().with_policy(p, None).supported().alive().revoked(false) + /// .for_transport_encryption() + /// }); + /// + /// let mut original = vec![]; + /// let message = Message::new(&mut original); + /// let message = Encryptor::for_recipients(message, recipients).build()?; + /// let mut w = LiteralWriter::new(message).build()?; + /// w.write_all(b"Original message")?; + /// w.finalize()?; + /// + /// // Decrypt original message using Alice's key. + /// let mut decryptor = DecryptorBuilder::from_bytes(&original)? + /// .with_policy(p, None, Helper::new(alice))?; + /// io::copy(&mut decryptor, &mut io::sink())?; + /// let (algo, sk, pkesks) = decryptor.into_helper().recycling_bin.unwrap(); + /// + /// // Compose the reply using the same session key. + /// let mut reply = vec![]; + /// let mut message = Message::new(&mut reply); + /// for p in pkesks { // Emit the stashed PKESK packets. + /// Packet::from(p).serialize(&mut message)?; + /// } + /// let message = Encryptor::with_session_key(message, algo, sk)?.build()?; + /// let mut w = LiteralWriter::new(message).build()?; + /// w.write_all(b"Encrypted reply")?; + /// w.finalize()?; + /// + /// // Check that Bob can decrypt it. + /// let mut decryptor = DecryptorBuilder::from_bytes(&reply)? + /// .with_policy(p, None, Helper::new(bob))?; + /// io::copy(&mut decryptor, &mut io::sink())?; + /// + /// /// Decrypts the message preserving algo, session key, and PKESKs. + /// struct Helper { + /// key: Cert, + /// recycling_bin: Option<(SymmetricAlgorithm, SessionKey, Vec<PKESK>)>, + /// } + /// + /// # impl Helper { + /// # fn new(key: Cert) -> Self { + /// # Helper { key, recycling_bin: None, } + /// # } + /// # } + /// # + /// impl DecryptionHelper for Helper { + /// fn decrypt<D>(&mut self, pkesks: &[PKESK], _skesks: &[SKESK], + /// sym_algo: Option<SymmetricAlgorithm>, mut decrypt: D) + /// -> Result<Option<Fingerprint>> + /// where D: FnMut(SymmetricAlgorithm, &SessionKey) -> bool + /// { + /// let p = &StandardPolicy::new(); + /// let mut encryption_context = None; + /// + /// for pkesk in pkesks { // Try each PKESK until we succeed. + /// for ka in self.key.keys().with_policy(p, None) + /// .supported().unencrypted_secret() + /// .key_handle(pkesk.recipient()) + /// .for_storage_encryption().for_transport_encryption() + /// { + /// let mut pair = ka.key().clone().into_keypair().unwrap(); + /// if pkesk.decrypt(&mut pair, sym_algo) + /// .map(|(algo, session_key)| { + /// let success = decrypt(algo, &session_key); + /// if success { + /// // Copy algor, session key, and PKESKs. + /// encryption_context = + /// Some((algo, session_key.clone(), + /// pkesks.iter().cloned().collect())); + /// } + /// success + /// }) + /// .unwrap_or(false) + /// { + /// break; // Decryption successful. + /// } + /// } + /// } + /// + /// self.recycling_bin = encryption_context; // Store for the reply. + /// Ok(Some(self.key.fingerprint())) + /// } + /// } + /// + /// impl VerificationHelper for Helper { + /// // ... + /// # fn get_certs(&mut self, _ids: &[KeyHandle]) -> Result<Vec<Cert>> { + /// # Ok(Vec::new()) // Lookup certificates here. + /// # } + /// # fn check(&mut self, structure: MessageStructure) -> Result<()> { + /// # Ok(()) // Implement your verification policy here. + /// # } + /// } + /// # Ok(()) } + /// ``` + pub fn with_session_key(inner: Message<'a>, + sym_algo: SymmetricAlgorithm, + session_key: SessionKey) + -> Result<Self> + { + let sym_key_size = sym_algo.key_size()?; + if session_key.len() != sym_key_size { + return Err(Error::InvalidArgument( + format!("{} requires a {} bit key, but session key has {}", + sym_algo, sym_key_size, session_key.len())).into()); + } + + Ok(Self { + inner: inner.into(), + session_key: Some(session_key), + recipients: Vec::new(), + passwords: Vec::with_capacity(0), + sym_algo, + aead_algo: Default::default(), + hash: HashAlgorithm::SHA1.context().unwrap(), + cookie: Default::default(), // Will be fixed in build. + }) + } + /// Adds recipients. /// /// The resulting message can be encrypted by any recipient and @@ -2691,9 +2862,12 @@ impl<'a> Encryptor<'a> { /// # Ok(()) } /// ``` pub fn build(mut self) -> Result<Message<'a>> { - if self.recipients.len() + self.passwords.len() == 0 { + if self.recipients.len() + self.passwords.len() == 0 + && self.session_key.is_none() + { return Err(Error::InvalidOperation( - "Neither recipients nor passwords given".into()).into()); + "Neither recipients, passwords, nor session key given".into() + ).into()); } struct AEADParameters { @@ -2717,8 +2891,15 @@ impl<'a> Encryptor<'a> { let mut inner = self.inner; let level = inner.as_ref().cookie_ref().level + 1; - // Generate a session key. - let sk = SessionKey::new(self.sym_algo.key_size()?); + // Reuse existing session key or generate a new one. + let sym_key_size = self.sym_algo.key_size()?; + let sk = self.session_key.take() + .unwrap_or_else(|| SessionKey::new(sym_key_size)); + if sk.len() != sym_key_size { + return Err(Error::InvalidOperation( + format!("{} requires a {} bit key, but session key has {}", + self.sym_algo, sym_key_size, sk.len())).into()); + } // Write the PKESK packet(s). for recipient in self.recipients.iter() { |