summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJustus Winter <justus@sequoia-pgp.org>2021-04-28 10:33:59 +0200
committerJustus Winter <justus@sequoia-pgp.org>2021-04-28 11:39:47 +0200
commitd1202ff498b234dd4da0c1ff76769e68d53bf8b4 (patch)
tree565a8523d3cdd82a7e0b4290eb2d24619cf5de35
parent4bc90bb02f64b59201c13567af9ce464c8243679 (diff)
openpgp: Implement Encryptor::with_session_key.
- Fixes #390.
-rw-r--r--openpgp/NEWS1
-rw-r--r--openpgp/examples/reply-encrypted.rs232
-rw-r--r--openpgp/src/serialize/stream.rs191
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() {