pgp/packet/
sym_encrypted_protected_data.rs

1use std::io::{self, BufRead, Read};
2
3use byteorder::WriteBytesExt;
4use bytes::Bytes;
5use rand::{CryptoRng, Rng};
6
7use crate::{
8    crypto::{
9        aead::{AeadAlgorithm, ChunkSize, StreamEncryptor},
10        sym::SymmetricKeyAlgorithm,
11    },
12    errors::{ensure_eq, format_err, InvalidInputSnafu, Result},
13    packet::{GnupgAeadDataConfig, PacketHeader, PacketTrait, SymEncryptedProtectedDataConfig},
14    parsing_reader::BufReadParsing,
15    ser::Serialize,
16    types::Tag,
17};
18
19/// Either a standard OpenPGP SEIPD config or a Gnupg-specific AEAD config
20#[derive(Clone, PartialEq, Eq, derive_more::Debug)]
21pub enum ProtectedDataConfig {
22    Seipd(SymEncryptedProtectedDataConfig),
23    GnupgAead(GnupgAeadDataConfig),
24}
25
26/// Symmetrically Encrypted Integrity Protected Data Packet
27/// <https://www.rfc-editor.org/rfc/rfc9580.html#name-symmetrically-encrypted-and>
28#[derive(Clone, PartialEq, Eq, derive_more::Debug)]
29pub struct SymEncryptedProtectedData {
30    packet_header: PacketHeader,
31    config: Config,
32    #[debug("{}", hex::encode(data))]
33    data: Bytes,
34}
35
36#[derive(Clone, PartialEq, Eq, derive_more::Debug)]
37pub enum Config {
38    V1,
39    V2 {
40        sym_alg: SymmetricKeyAlgorithm,
41        aead: AeadAlgorithm,
42        chunk_size: ChunkSize,
43        #[debug("{}", hex::encode(salt))]
44        salt: [u8; 32],
45    },
46}
47
48impl Config {
49    pub fn try_from_reader<R: BufRead>(mut data: R) -> Result<Self> {
50        let version = data.read_u8()?;
51        match version {
52            0x01 => Ok(Self::V1),
53            0x02 => {
54                let sym_alg = data.read_u8().map(SymmetricKeyAlgorithm::from)?;
55                let aead = data.read_u8().map(AeadAlgorithm::from)?;
56                let chunk_size = data
57                    .read_u8()?
58                    .try_into()
59                    .map_err(|_| InvalidInputSnafu.build())?;
60                let salt = data.read_array::<32>()?;
61
62                Ok(Self::V2 {
63                    sym_alg,
64                    aead,
65                    chunk_size,
66                    salt,
67                })
68            }
69            _ => Err(format_err!(
70                "unknown SymEncryptedProtectedData version {}",
71                version
72            )),
73        }
74    }
75}
76
77impl SymEncryptedProtectedData {
78    /// Parses a `SymEncryptedProtectedData` packet from the given buf.
79    pub fn try_from_reader<B: BufRead>(packet_header: PacketHeader, mut data: B) -> Result<Self> {
80        ensure_eq!(
81            packet_header.tag(),
82            Tag::SymEncryptedProtectedData,
83            "invalid tag"
84        );
85
86        let config = Config::try_from_reader(&mut data)?;
87        let data = data.rest()?;
88
89        Ok(SymEncryptedProtectedData {
90            packet_header,
91            config,
92            data: data.freeze(),
93        })
94    }
95
96    /// Encrypts the data using the given symmetric key.
97    pub fn encrypt_seipdv1<R: CryptoRng + Rng>(
98        rng: R,
99        alg: SymmetricKeyAlgorithm,
100        key: &[u8],
101        plaintext: &[u8],
102    ) -> Result<Self> {
103        let data: Bytes = alg.encrypt_protected(rng, key, plaintext)?.into();
104        let config = Config::V1;
105        let len = config.write_len() + data.len();
106        let packet_header =
107            PacketHeader::new_fixed(Tag::SymEncryptedProtectedData, len.try_into()?);
108
109        Ok(SymEncryptedProtectedData {
110            packet_header,
111            config,
112            data,
113        })
114    }
115
116    /// Encrypts the data using the given symmetric key.
117    pub fn encrypt_seipdv2<R: CryptoRng + Rng>(
118        mut rng: R,
119        sym_alg: SymmetricKeyAlgorithm,
120        aead: AeadAlgorithm,
121        chunk_size: ChunkSize,
122        session_key: &[u8],
123        mut plaintext: &[u8],
124    ) -> Result<Self> {
125        // Generate new salt for this seipd packet.
126        let mut salt = [0u8; 32];
127        rng.fill(&mut salt[..]);
128
129        let mut encryptor = crate::crypto::aead::StreamEncryptor::new(
130            sym_alg,
131            aead,
132            chunk_size,
133            session_key,
134            &salt,
135            &mut plaintext,
136        )?;
137
138        let mut out = Vec::new();
139        encryptor.read_to_end(&mut out)?;
140
141        let config = Config::V2 {
142            sym_alg,
143            aead,
144            chunk_size,
145            salt,
146        };
147        let data: Bytes = out.into();
148        let len = config.write_len() + data.len();
149        let packet_header =
150            PacketHeader::new_fixed(Tag::SymEncryptedProtectedData, len.try_into()?);
151
152        Ok(SymEncryptedProtectedData {
153            packet_header,
154            config,
155            data,
156        })
157    }
158
159    /// Encrypts the data using the given symmetric key.
160    pub fn encrypt_seipdv2_stream<R: io::Read>(
161        sym_alg: SymmetricKeyAlgorithm,
162        aead: AeadAlgorithm,
163        chunk_size: ChunkSize,
164        session_key: &[u8],
165        salt: [u8; 32],
166        source: R,
167    ) -> Result<StreamEncryptor<R>> {
168        let encryptor =
169            StreamEncryptor::new(sym_alg, aead, chunk_size, session_key, &salt, source)?;
170
171        Ok(encryptor)
172    }
173
174    pub fn data(&self) -> &Bytes {
175        &self.data
176    }
177
178    pub fn version(&self) -> usize {
179        match self.config {
180            Config::V1 => 1,
181            Config::V2 { .. } => 2,
182        }
183    }
184
185    /// Returns the configuration for this packet.
186    pub fn config(&self) -> &Config {
187        &self.config
188    }
189
190    /// Decrypts the inner data, returning the result.
191    pub fn decrypt(
192        &self,
193        session_key: &[u8],
194        sym_alg: Option<SymmetricKeyAlgorithm>,
195    ) -> Result<Vec<u8>> {
196        match &self.config {
197            Config::V1 => {
198                let sym_alg = sym_alg.expect("v1");
199                let mut decryptor = StreamDecryptor::v1(sym_alg, session_key, &self.data[..])?;
200                let mut out = Vec::new();
201                decryptor.read_to_end(&mut out)?;
202                Ok(out)
203            }
204            Config::V2 {
205                sym_alg,
206                aead,
207                chunk_size,
208                salt,
209            } => {
210                ensure_eq!(
211                    session_key.len(),
212                    sym_alg.key_size(),
213                    "Unexpected session key length for {:?}",
214                    sym_alg
215                );
216
217                let mut decryptor = StreamDecryptor::v2(
218                    *sym_alg,
219                    *aead,
220                    *chunk_size,
221                    salt,
222                    session_key,
223                    &self.data[..],
224                )?;
225                let mut out = Vec::new();
226                decryptor.read_to_end(&mut out)?;
227                Ok(out)
228            }
229        }
230    }
231}
232
233impl Serialize for Config {
234    fn to_writer<W: io::Write>(&self, writer: &mut W) -> Result<()> {
235        match self {
236            Config::V1 => {
237                writer.write_u8(0x01)?;
238            }
239            Config::V2 {
240                sym_alg,
241                aead,
242                chunk_size,
243                salt,
244            } => {
245                writer.write_u8(0x02)?;
246                writer.write_u8((*sym_alg).into())?;
247                writer.write_u8((*aead).into())?;
248                writer.write_u8((*chunk_size).into())?;
249                writer.write_all(salt)?;
250            }
251        }
252        Ok(())
253    }
254
255    fn write_len(&self) -> usize {
256        match self {
257            Config::V1 => 1,
258            Config::V2 { salt, .. } => {
259                let mut sum = 1 + 1 + 1 + 1;
260                sum += salt.len();
261                sum
262            }
263        }
264    }
265}
266impl Serialize for SymEncryptedProtectedData {
267    fn to_writer<W: io::Write>(&self, writer: &mut W) -> Result<()> {
268        self.config.to_writer(writer)?;
269        writer.write_all(&self.data)?;
270        Ok(())
271    }
272
273    fn write_len(&self) -> usize {
274        let mut sum = self.config.write_len();
275        sum += self.data.len();
276        sum
277    }
278}
279
280impl PacketTrait for SymEncryptedProtectedData {
281    fn packet_header(&self) -> &PacketHeader {
282        &self.packet_header
283    }
284}
285
286#[derive(Debug)]
287#[allow(clippy::large_enum_variant)]
288pub enum StreamDecryptor<R: BufRead> {
289    V1(crate::crypto::sym::StreamDecryptor<R>),
290    GnupgAead(crate::crypto::aead::StreamDecryptor<R>),
291    V2(crate::crypto::aead::StreamDecryptor<R>),
292}
293
294impl<R: BufRead> BufRead for StreamDecryptor<R> {
295    fn fill_buf(&mut self) -> io::Result<&[u8]> {
296        match self {
297            Self::V1(r) => r.fill_buf(),
298            Self::GnupgAead(r) => r.fill_buf(),
299            Self::V2(r) => r.fill_buf(),
300        }
301    }
302
303    fn consume(&mut self, amt: usize) {
304        match self {
305            Self::V1(r) => r.consume(amt),
306            Self::GnupgAead(r) => r.consume(amt),
307            Self::V2(r) => r.consume(amt),
308        }
309    }
310}
311
312impl<R: BufRead> Read for StreamDecryptor<R> {
313    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
314        match self {
315            Self::V1(r) => r.read(buf),
316            Self::GnupgAead(r) => r.read(buf),
317            Self::V2(r) => r.read(buf),
318        }
319    }
320}
321
322impl<R: BufRead> StreamDecryptor<R> {
323    pub fn v1(sym_alg: SymmetricKeyAlgorithm, key: &[u8], source: R) -> Result<Self> {
324        let decryptor = sym_alg.stream_decryptor_protected(key, source)?;
325        Ok(Self::V1(decryptor))
326    }
327
328    pub fn gnupg_aead(
329        sym_alg: SymmetricKeyAlgorithm,
330        aead: AeadAlgorithm,
331        chunk_size: ChunkSize,
332        key: &[u8],
333        iv: &[u8],
334        source: R,
335    ) -> Result<Self> {
336        let decryptor = crate::crypto::aead::StreamDecryptor::new_gnupg(
337            sym_alg, aead, chunk_size, key, iv, source,
338        )?;
339        Ok(Self::GnupgAead(decryptor))
340    }
341
342    pub fn v2(
343        sym_alg: SymmetricKeyAlgorithm,
344        aead: AeadAlgorithm,
345        chunk_size: ChunkSize,
346        salt: &[u8; 32],
347        key: &[u8],
348        source: R,
349    ) -> Result<Self> {
350        let decryptor = crate::crypto::aead::StreamDecryptor::new_rfc9580(
351            sym_alg, aead, chunk_size, salt, key, source,
352        )?;
353        Ok(Self::V2(decryptor))
354    }
355
356    pub fn into_inner(self) -> R {
357        match self {
358            Self::V1(r) => r.into_inner(),
359            Self::GnupgAead(r) => r.into_inner(),
360            Self::V2(r) => r.into_inner(),
361        }
362    }
363
364    pub fn get_ref(&self) -> &R {
365        match self {
366            Self::V1(r) => r.get_ref(),
367            Self::GnupgAead(r) => r.get_ref(),
368            Self::V2(r) => r.get_ref(),
369        }
370    }
371
372    pub fn get_mut(&mut self) -> &mut R {
373        match self {
374            Self::V1(r) => r.get_mut(),
375            Self::GnupgAead(r) => r.get_mut(),
376            Self::V2(r) => r.get_mut(),
377        }
378    }
379}
380
381#[cfg(test)]
382mod tests {
383    #![allow(clippy::unwrap_used)]
384
385    use proptest::{collection::vec, prelude::*};
386    use rand::{RngCore, SeedableRng};
387    use rand_chacha::ChaCha8Rng;
388
389    use super::*;
390    use crate::{
391        packet::sym_encrypted_protected_data::Config,
392        types::{PacketHeaderVersion, PacketLength},
393    };
394
395    #[test]
396    fn test_aead_message_sizes() {
397        // Test that AEAD encryption/decryption works for message sizes that span 0-2 chunks.
398
399        let mut rng = ChaCha8Rng::from_seed([0u8; 32]);
400
401        const SYM_ALG: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
402
403        let mut session_key = [0; 16];
404        rng.fill_bytes(&mut session_key);
405
406        // Iterate over message sizes from 0 bytes through all 1-chunk and 2-chunk lengths
407        // (ending with two chunks of a full 64 bytes)
408        for size in 0..=512 {
409            let mut message = vec![0; size];
410            rng.fill_bytes(&mut message);
411
412            for aead in [AeadAlgorithm::Ocb, AeadAlgorithm::Eax, AeadAlgorithm::Gcm] {
413                println!("{size} bytes: {aead:?}");
414                let enc = SymEncryptedProtectedData::encrypt_seipdv2(
415                    &mut rng,
416                    SYM_ALG,
417                    aead,
418                    ChunkSize::C64B,
419                    &session_key,
420                    &message,
421                )
422                .expect("encrypt");
423
424                let dec = enc.decrypt(&session_key, Some(SYM_ALG)).expect("decrypt");
425                assert_eq!(message, dec);
426
427                // write test
428                let mut buffer = Vec::new();
429                enc.to_writer(&mut buffer).unwrap();
430                assert_eq!(buffer.len(), enc.write_len());
431
432                let back =
433                    SymEncryptedProtectedData::try_from_reader(enc.packet_header, &mut &buffer[..])
434                        .unwrap();
435                assert_eq!(enc, back);
436            }
437        }
438    }
439
440    impl Arbitrary for Config {
441        type Parameters = ();
442        type Strategy = BoxedStrategy<Self>;
443
444        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
445            prop_oneof![
446                Just(Config::V1),
447                any::<(SymmetricKeyAlgorithm, AeadAlgorithm, ChunkSize)>()
448                    .prop_flat_map(move |(sym_alg, aead, chunk_size)| {
449                        (
450                            Just(sym_alg),
451                            Just(aead),
452                            Just(chunk_size),
453                            vec(0u8..=255u8, 32),
454                        )
455                    })
456                    .prop_map(move |(sym_alg, aead, chunk_size, salt)| Config::V2 {
457                        sym_alg,
458                        aead,
459                        chunk_size,
460                        salt: salt.try_into().unwrap(),
461                    })
462            ]
463            .boxed()
464        }
465    }
466
467    impl Arbitrary for SymEncryptedProtectedData {
468        type Parameters = ();
469        type Strategy = BoxedStrategy<Self>;
470
471        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
472            any::<Config>()
473                .prop_flat_map(move |config| (Just(config), vec(0u8..=255u8, 0..=2048)))
474                .prop_map(move |(config, data)| {
475                    let len = 1u32; // unused
476                    let packet_header = PacketHeader::from_parts(
477                        PacketHeaderVersion::New,
478                        Tag::SymEncryptedProtectedData,
479                        PacketLength::Fixed(len),
480                    )
481                    .unwrap();
482                    SymEncryptedProtectedData {
483                        config,
484                        packet_header,
485                        data: data.into(),
486                    }
487                })
488                .boxed()
489        }
490    }
491
492    proptest! {
493        #[test]
494        fn write_len(data: SymEncryptedProtectedData) {
495            let mut buf = Vec::new();
496            data.to_writer(&mut buf).unwrap();
497            prop_assert_eq!(buf.len(), data.write_len());
498        }
499
500
501        #[test]
502        fn packet_roundtrip(data: SymEncryptedProtectedData) {
503            let mut buf = Vec::new();
504            data.to_writer(&mut buf).unwrap();
505            let new_data = SymEncryptedProtectedData::try_from_reader(data.packet_header, &mut &buf[..]).unwrap();
506            prop_assert_eq!(data, new_data);
507        }
508    }
509}