1use std::collections::{BTreeMap, HashSet};
4use std::io::{BufRead, Cursor};
5
6use anyhow::{Context as _, Result, bail};
7use chrono::SubsecRound;
8use deltachat_contact_tools::EmailAddress;
9use pgp::armor::BlockType;
10use pgp::composed::{
11 ArmorOptions, DecryptionOptions, Deserializable, DetachedSignature, KeyType as PgpKeyType,
12 Message, MessageBuilder, SecretKeyParamsBuilder, SignedPublicKey, SignedPublicSubKey,
13 SignedSecretKey, SubkeyParamsBuilder, TheRing,
14};
15use pgp::crypto::aead::{AeadAlgorithm, ChunkSize};
16use pgp::crypto::ecc_curve::ECCCurve;
17use pgp::crypto::hash::HashAlgorithm;
18use pgp::crypto::sym::SymmetricKeyAlgorithm;
19use pgp::packet::{SignatureConfig, SignatureType, Subpacket, SubpacketData};
20use pgp::types::{
21 CompressionAlgorithm, KeyDetails, Password, PublicKeyTrait, SecretKeyTrait as _, StringToKey,
22};
23use rand_old::{Rng as _, thread_rng};
24use tokio::runtime::Handle;
25
26use crate::key::{DcKey, Fingerprint};
27
28#[cfg(test)]
29pub(crate) const HEADER_AUTOCRYPT: &str = "autocrypt-prefer-encrypt";
30
31pub(crate) const HEADER_SETUPCODE: &str = "passphrase-begin";
32
33const SYMMETRIC_KEY_ALGORITHM: SymmetricKeyAlgorithm = SymmetricKeyAlgorithm::AES128;
35
36pub fn split_armored_data(buf: &[u8]) -> Result<(BlockType, BTreeMap<String, String>, Vec<u8>)> {
40 use std::io::Read;
41
42 let cursor = Cursor::new(buf);
43 let mut dearmor = pgp::armor::Dearmor::new(cursor);
44
45 let mut bytes = Vec::with_capacity(buf.len());
46
47 dearmor.read_to_end(&mut bytes)?;
48 let typ = dearmor.typ.context("failed to parse type")?;
49
50 let headers = dearmor
52 .headers
53 .into_iter()
54 .map(|(key, values)| {
55 (
56 key.trim().to_lowercase(),
57 values
58 .last()
59 .map_or_else(String::new, |s| s.trim().to_string()),
60 )
61 })
62 .collect();
63
64 Ok((typ, headers, bytes))
65}
66
67#[derive(Debug, Clone, Eq, PartialEq)]
72pub struct KeyPair {
73 pub public: SignedPublicKey,
75
76 pub secret: SignedSecretKey,
78}
79
80impl KeyPair {
81 pub fn new(secret: SignedSecretKey) -> Result<Self> {
85 use crate::key::DcSecretKey;
86
87 let public = secret.split_public_key()?;
88 Ok(Self { public, secret })
89 }
90}
91
92pub(crate) fn create_keypair(addr: EmailAddress) -> Result<KeyPair> {
97 let signing_key_type = PgpKeyType::Ed25519Legacy;
98 let encryption_key_type = PgpKeyType::ECDH(ECCCurve::Curve25519);
99
100 let user_id = format!("<{addr}>");
101 let key_params = SecretKeyParamsBuilder::default()
102 .key_type(signing_key_type)
103 .can_certify(true)
104 .can_sign(true)
105 .primary_user_id(user_id)
106 .passphrase(None)
107 .preferred_symmetric_algorithms(smallvec![
108 SymmetricKeyAlgorithm::AES256,
109 SymmetricKeyAlgorithm::AES192,
110 SymmetricKeyAlgorithm::AES128,
111 ])
112 .preferred_hash_algorithms(smallvec![
113 HashAlgorithm::Sha256,
114 HashAlgorithm::Sha384,
115 HashAlgorithm::Sha512,
116 HashAlgorithm::Sha224,
117 ])
118 .preferred_compression_algorithms(smallvec![
119 CompressionAlgorithm::ZLIB,
120 CompressionAlgorithm::ZIP,
121 ])
122 .subkey(
123 SubkeyParamsBuilder::default()
124 .key_type(encryption_key_type)
125 .can_encrypt(true)
126 .passphrase(None)
127 .build()
128 .context("failed to build subkey parameters")?,
129 )
130 .build()
131 .context("failed to build key parameters")?;
132
133 let mut rng = thread_rng();
134 let secret_key = key_params
135 .generate(&mut rng)
136 .context("failed to generate the key")?
137 .sign(&mut rng, &Password::empty())
138 .context("failed to sign secret key")?;
139 secret_key
140 .verify()
141 .context("invalid secret key generated")?;
142
143 let key_pair = KeyPair::new(secret_key)?;
144 key_pair
145 .public
146 .verify()
147 .context("invalid public key generated")?;
148 Ok(key_pair)
149}
150
151fn select_pk_for_encryption(key: &SignedPublicKey) -> Option<&SignedPublicSubKey> {
157 key.public_subkeys
158 .iter()
159 .find(|subkey| subkey.is_encryption_key())
160}
161
162#[derive(Debug)]
168pub enum SeipdVersion {
169 V1,
171
172 V2,
174}
175
176pub async fn pk_encrypt(
179 plain: Vec<u8>,
180 public_keys_for_encryption: Vec<SignedPublicKey>,
181 private_key_for_signing: SignedSecretKey,
182 compress: bool,
183 anonymous_recipients: bool,
184 seipd_version: SeipdVersion,
185) -> Result<String> {
186 Handle::current()
187 .spawn_blocking(move || {
188 let mut rng = thread_rng();
189
190 let pkeys = public_keys_for_encryption
191 .iter()
192 .filter_map(select_pk_for_encryption);
193
194 let msg = MessageBuilder::from_bytes("", plain);
195 let encoded_msg = match seipd_version {
196 SeipdVersion::V1 => {
197 let mut msg = msg.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
198
199 for pkey in pkeys {
200 if anonymous_recipients {
201 msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
202 } else {
203 msg.encrypt_to_key(&mut rng, &pkey)?;
204 }
205 }
206
207 let hash_algorithm = private_key_for_signing.hash_alg();
208 msg.sign(&*private_key_for_signing, Password::empty(), hash_algorithm);
209 if compress {
210 msg.compression(CompressionAlgorithm::ZLIB);
211 }
212
213 msg.to_armored_string(&mut rng, Default::default())?
214 }
215 SeipdVersion::V2 => {
216 let mut msg = msg.seipd_v2(
217 &mut rng,
218 SYMMETRIC_KEY_ALGORITHM,
219 AeadAlgorithm::Ocb,
220 ChunkSize::C8KiB,
221 );
222
223 for pkey in pkeys {
224 if anonymous_recipients {
225 msg.encrypt_to_key_anonymous(&mut rng, &pkey)?;
226 } else {
227 msg.encrypt_to_key(&mut rng, &pkey)?;
228 }
229 }
230
231 let hash_algorithm = private_key_for_signing.hash_alg();
232 msg.sign(&*private_key_for_signing, Password::empty(), hash_algorithm);
233 if compress {
234 msg.compression(CompressionAlgorithm::ZLIB);
235 }
236
237 msg.to_armored_string(&mut rng, Default::default())?
238 }
239 };
240
241 Ok(encoded_msg)
242 })
243 .await?
244}
245
246pub fn pk_calc_signature(
248 plain: Vec<u8>,
249 private_key_for_signing: &SignedSecretKey,
250) -> Result<String> {
251 let rng = thread_rng();
252
253 let mut config = SignatureConfig::from_key(
254 rng,
255 &private_key_for_signing.primary_key,
256 SignatureType::Binary,
257 )?;
258
259 config.hashed_subpackets = vec![
260 Subpacket::regular(SubpacketData::IssuerFingerprint(
261 private_key_for_signing.fingerprint(),
262 ))?,
263 Subpacket::critical(SubpacketData::SignatureCreationTime(
264 chrono::Utc::now().trunc_subsecs(0),
265 ))?,
266 ];
267 config.unhashed_subpackets = vec![Subpacket::regular(SubpacketData::Issuer(
268 private_key_for_signing.key_id(),
269 ))?];
270
271 let signature = config.sign(
272 &private_key_for_signing.primary_key,
273 &Password::empty(),
274 plain.as_slice(),
275 )?;
276
277 let sig = DetachedSignature::new(signature);
278
279 Ok(sig.to_armored_string(ArmorOptions::default())?)
280}
281
282pub fn decrypt(
290 ctext: Vec<u8>,
291 private_keys_for_decryption: &[SignedSecretKey],
292 mut shared_secrets: &[String],
293) -> Result<pgp::composed::Message<'static>> {
294 let cursor = Cursor::new(ctext);
295 let (msg, _headers) = Message::from_armor(cursor)?;
296
297 let skeys: Vec<&SignedSecretKey> = private_keys_for_decryption.iter().collect();
298 let empty_pw = Password::empty();
299
300 let decrypt_options = DecryptionOptions::new();
301 let symmetric_encryption_res = check_symmetric_encryption(&msg);
302 if symmetric_encryption_res.is_err() {
303 shared_secrets = &[];
304 }
305
306 let message_password: Vec<Password> = shared_secrets
311 .iter()
312 .map(|p| Password::from(p.as_str()))
313 .collect();
314 let message_password: Vec<&Password> = message_password.iter().collect();
315
316 let ring = TheRing {
317 secret_keys: skeys,
318 key_passwords: vec![&empty_pw],
319 message_password,
320 session_keys: vec![],
321 decrypt_options,
322 };
323
324 let res = msg.decrypt_the_ring(ring, true);
325
326 let (msg, _ring_result) = match res {
327 Ok(it) => it,
328 Err(err) => {
329 if let Err(reason) = symmetric_encryption_res {
330 bail!("{err:#} (Note: symmetric decryption was not tried: {reason})")
331 } else {
332 bail!("{err:#}");
333 }
334 }
335 };
336
337 let msg = msg.decompress()?;
339
340 Ok(msg)
341}
342
343fn check_symmetric_encryption(msg: &Message<'_>) -> std::result::Result<(), &'static str> {
353 let Message::Encrypted { esk, .. } = msg else {
354 return Err("not encrypted");
355 };
356
357 if esk.len() > 1 {
358 return Err("too many esks");
359 }
360
361 let [pgp::composed::Esk::SymKeyEncryptedSessionKey(esk)] = &esk[..] else {
362 return Err("not symmetrically encrypted");
363 };
364
365 match esk.s2k() {
366 Some(StringToKey::Salted { .. }) => Ok(()),
367 _ => Err("unsupported string2key algorithm"),
368 }
369}
370
371pub fn valid_signature_fingerprints(
377 msg: &pgp::composed::Message,
378 public_keys_for_validation: &[SignedPublicKey],
379) -> HashSet<Fingerprint> {
380 let mut ret_signature_fingerprints: HashSet<Fingerprint> = Default::default();
381 if msg.is_signed() {
382 for pkey in public_keys_for_validation {
383 if msg.verify(&pkey.primary_key).is_ok() {
384 let fp = pkey.dc_fingerprint();
385 ret_signature_fingerprints.insert(fp);
386 }
387 }
388 }
389 ret_signature_fingerprints
390}
391
392pub fn pk_validate(
394 content: &[u8],
395 signature: &[u8],
396 public_keys_for_validation: &[SignedPublicKey],
397) -> Result<HashSet<Fingerprint>> {
398 let mut ret: HashSet<Fingerprint> = Default::default();
399
400 let detached_signature = DetachedSignature::from_armor_single(Cursor::new(signature))?.0;
401
402 for pkey in public_keys_for_validation {
403 if detached_signature.verify(pkey, content).is_ok() {
404 let fp = pkey.dc_fingerprint();
405 ret.insert(fp);
406 }
407 }
408 Ok(ret)
409}
410
411pub async fn symm_encrypt_autocrypt_setup(passphrase: &str, plain: Vec<u8>) -> Result<String> {
413 let passphrase = Password::from(passphrase.to_string());
414
415 tokio::task::spawn_blocking(move || {
416 let mut rng = thread_rng();
417 let s2k = StringToKey::new_default(&mut rng);
418 let builder = MessageBuilder::from_bytes("", plain);
419 let mut builder = builder.seipd_v1(&mut rng, SYMMETRIC_KEY_ALGORITHM);
420 builder.encrypt_with_password(s2k, &passphrase)?;
421
422 let encoded_msg = builder.to_armored_string(&mut rng, Default::default())?;
423
424 Ok(encoded_msg)
425 })
426 .await?
427}
428
429pub async fn symm_encrypt_message(
433 plain: Vec<u8>,
434 private_key_for_signing: SignedSecretKey,
435 shared_secret: &str,
436 compress: bool,
437) -> Result<String> {
438 let shared_secret = Password::from(shared_secret.to_string());
439
440 tokio::task::spawn_blocking(move || {
441 let msg = MessageBuilder::from_bytes("", plain);
442 let mut rng = thread_rng();
443 let mut salt = [0u8; 8];
444 rng.fill(&mut salt[..]);
445 let s2k = StringToKey::Salted {
446 hash_alg: HashAlgorithm::default(),
447 salt,
448 };
449 let mut msg = msg.seipd_v2(
450 &mut rng,
451 SYMMETRIC_KEY_ALGORITHM,
452 AeadAlgorithm::Ocb,
453 ChunkSize::C8KiB,
454 );
455 msg.encrypt_with_password(&mut rng, s2k, &shared_secret)?;
456
457 let hash_algorithm = private_key_for_signing.hash_alg();
458 msg.sign(&*private_key_for_signing, Password::empty(), hash_algorithm);
459 if compress {
460 msg.compression(CompressionAlgorithm::ZLIB);
461 }
462
463 let encoded_msg = msg.to_armored_string(&mut rng, Default::default())?;
464
465 Ok(encoded_msg)
466 })
467 .await?
468}
469
470pub async fn symm_decrypt<T: BufRead + std::fmt::Debug + 'static + Send>(
472 passphrase: &str,
473 ctext: T,
474) -> Result<Vec<u8>> {
475 let passphrase = passphrase.to_string();
476 tokio::task::spawn_blocking(move || {
477 let (enc_msg, _) = Message::from_armor(ctext)?;
478 let password = Password::from(passphrase);
479
480 let msg = enc_msg.decrypt_with_password(&password)?;
481 let res = msg.decompress()?.as_data_vec()?;
482 Ok(res)
483 })
484 .await?
485}
486
487#[cfg(test)]
488mod tests {
489 use std::sync::LazyLock;
490 use tokio::sync::OnceCell;
491
492 use super::*;
493 use crate::{
494 key::{load_self_public_key, load_self_secret_key},
495 test_utils::{TestContextManager, alice_keypair, bob_keypair},
496 };
497 use pgp::composed::Esk;
498 use pgp::packet::PublicKeyEncryptedSessionKey;
499
500 fn pk_decrypt_and_validate<'a>(
501 ctext: &'a [u8],
502 private_keys_for_decryption: &'a [SignedSecretKey],
503 public_keys_for_validation: &[SignedPublicKey],
504 ) -> Result<(
505 pgp::composed::Message<'static>,
506 HashSet<Fingerprint>,
507 Vec<u8>,
508 )> {
509 let mut msg = decrypt(ctext.to_vec(), private_keys_for_decryption, &[])?;
510 let content = msg.as_data_vec()?;
511 let ret_signature_fingerprints =
512 valid_signature_fingerprints(&msg, public_keys_for_validation);
513
514 Ok((msg, ret_signature_fingerprints, content))
515 }
516
517 #[test]
518 fn test_split_armored_data_1() {
519 let (typ, _headers, base64) = split_armored_data(
520 b"-----BEGIN PGP MESSAGE-----\nNoVal:\n\naGVsbG8gd29ybGQ=\n-----END PGP MESSAGE-----",
521 )
522 .unwrap();
523
524 assert_eq!(typ, BlockType::Message);
525 assert!(!base64.is_empty());
526 assert_eq!(
527 std::string::String::from_utf8(base64).unwrap(),
528 "hello world"
529 );
530 }
531
532 #[test]
533 fn test_split_armored_data_2() {
534 let (typ, headers, base64) = split_armored_data(
535 b"-----BEGIN PGP PRIVATE KEY BLOCK-----\nAutocrypt-Prefer-Encrypt: mutual \n\naGVsbG8gd29ybGQ=\n-----END PGP PRIVATE KEY BLOCK-----"
536 )
537 .unwrap();
538
539 assert_eq!(typ, BlockType::PrivateKey);
540 assert!(!base64.is_empty());
541 assert_eq!(headers.get(HEADER_AUTOCRYPT), Some(&"mutual".to_string()));
542 }
543
544 #[test]
545 fn test_create_keypair() {
546 let keypair0 = create_keypair(EmailAddress::new("foo@bar.de").unwrap()).unwrap();
547 let keypair1 = create_keypair(EmailAddress::new("two@zwo.de").unwrap()).unwrap();
548 assert_ne!(keypair0.public, keypair1.public);
549 }
550
551 struct TestKeys {
554 alice_secret: SignedSecretKey,
555 alice_public: SignedPublicKey,
556 bob_secret: SignedSecretKey,
557 bob_public: SignedPublicKey,
558 }
559
560 impl TestKeys {
561 fn new() -> TestKeys {
562 let alice = alice_keypair();
563 let bob = bob_keypair();
564 TestKeys {
565 alice_secret: alice.secret.clone(),
566 alice_public: alice.public,
567 bob_secret: bob.secret.clone(),
568 bob_public: bob.public,
569 }
570 }
571 }
572
573 static CLEARTEXT: &[u8] = b"This is a test";
575
576 static KEYS: LazyLock<TestKeys> = LazyLock::new(TestKeys::new);
578
579 static CTEXT_SIGNED: OnceCell<String> = OnceCell::const_new();
580
581 async fn ctext_signed() -> &'static String {
583 let anonymous_recipients = true;
584 CTEXT_SIGNED
585 .get_or_init(|| async {
586 let keyring = vec![KEYS.alice_public.clone(), KEYS.bob_public.clone()];
587 let compress = true;
588
589 pk_encrypt(
590 CLEARTEXT.to_vec(),
591 keyring,
592 KEYS.alice_secret.clone(),
593 compress,
594 anonymous_recipients,
595 SeipdVersion::V2,
596 )
597 .await
598 .unwrap()
599 })
600 .await
601 }
602
603 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
604 async fn test_encrypt_signed() {
605 assert!(!ctext_signed().await.is_empty());
606 assert!(
607 ctext_signed()
608 .await
609 .starts_with("-----BEGIN PGP MESSAGE-----")
610 );
611 }
612
613 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
614 async fn test_decrypt_singed() {
615 let decrypt_keyring = vec![KEYS.alice_secret.clone()];
617 let sig_check_keyring = vec![KEYS.alice_public.clone()];
618 let (_msg, valid_signatures, content) = pk_decrypt_and_validate(
619 ctext_signed().await.as_bytes(),
620 &decrypt_keyring,
621 &sig_check_keyring,
622 )
623 .unwrap();
624 assert_eq!(content, CLEARTEXT);
625 assert_eq!(valid_signatures.len(), 1);
626
627 let decrypt_keyring = vec![KEYS.bob_secret.clone()];
629 let sig_check_keyring = vec![KEYS.alice_public.clone()];
630 let (_msg, valid_signatures, content) = pk_decrypt_and_validate(
631 ctext_signed().await.as_bytes(),
632 &decrypt_keyring,
633 &sig_check_keyring,
634 )
635 .unwrap();
636 assert_eq!(content, CLEARTEXT);
637 assert_eq!(valid_signatures.len(), 1);
638 }
639
640 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
641 async fn test_decrypt_no_sig_check() {
642 let keyring = vec![KEYS.alice_secret.clone()];
643 let (_msg, valid_signatures, content) =
644 pk_decrypt_and_validate(ctext_signed().await.as_bytes(), &keyring, &[]).unwrap();
645 assert_eq!(content, CLEARTEXT);
646 assert_eq!(valid_signatures.len(), 0);
647 }
648
649 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
650 async fn test_decrypt_signed_no_key() {
651 let decrypt_keyring = vec![KEYS.bob_secret.clone()];
653 let sig_check_keyring = vec![KEYS.bob_public.clone()];
654 let (_msg, valid_signatures, content) = pk_decrypt_and_validate(
655 ctext_signed().await.as_bytes(),
656 &decrypt_keyring,
657 &sig_check_keyring,
658 )
659 .unwrap();
660 assert_eq!(content, CLEARTEXT);
661 assert_eq!(valid_signatures.len(), 0);
662 }
663
664 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
665 async fn test_decrypt_unsigned() {
666 let decrypt_keyring = vec![KEYS.bob_secret.clone()];
667 let ctext_unsigned = include_bytes!("../test-data/message/ctext_unsigned.asc");
668 let (_msg, valid_signatures, content) =
669 pk_decrypt_and_validate(ctext_unsigned, &decrypt_keyring, &[]).unwrap();
670 assert_eq!(content, CLEARTEXT);
671 assert_eq!(valid_signatures.len(), 0);
672 }
673
674 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
675 async fn test_encrypt_decrypt_broadcast() -> Result<()> {
676 let mut tcm = TestContextManager::new();
677 let alice = &tcm.alice().await;
678 let bob = &tcm.bob().await;
679
680 let plain = Vec::from(b"this is the secret message");
681 let shared_secret = "shared secret";
682 let ctext = symm_encrypt_message(
683 plain.clone(),
684 load_self_secret_key(alice).await?,
685 shared_secret,
686 true,
687 )
688 .await?;
689
690 let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?;
691 let mut decrypted = decrypt(
692 ctext.into(),
693 &bob_private_keyring,
694 &[shared_secret.to_string()],
695 )?;
696
697 assert_eq!(decrypted.as_data_vec()?, plain);
698
699 Ok(())
700 }
701
702 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
706 async fn test_dont_decrypt_expensive_message() -> Result<()> {
707 let mut tcm = TestContextManager::new();
708 let bob = &tcm.bob().await;
709
710 let plain = Vec::from(b"this is the secret message");
711 let shared_secret = "shared secret";
712
713 let shared_secret_pw = Password::from(shared_secret.to_string());
717 let msg = MessageBuilder::from_bytes("", plain);
718 let mut rng = thread_rng();
719 let s2k = StringToKey::new_default(&mut rng); let mut msg = msg.seipd_v2(
722 &mut rng,
723 SymmetricKeyAlgorithm::AES128,
724 AeadAlgorithm::Ocb,
725 ChunkSize::C8KiB,
726 );
727 msg.encrypt_with_password(&mut rng, s2k, &shared_secret_pw)?;
728
729 let ctext = msg.to_armored_string(&mut rng, Default::default())?;
730
731 let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?;
734 let error = decrypt(
735 ctext.into(),
736 &bob_private_keyring,
737 &[shared_secret.to_string()],
738 )
739 .unwrap_err();
740
741 assert_eq!(
742 error.to_string(),
743 "missing key (Note: symmetric decryption was not tried: unsupported string2key algorithm)"
744 );
745
746 Ok(())
747 }
748
749 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
750 async fn test_decryption_error_msg() -> Result<()> {
751 let mut tcm = TestContextManager::new();
752 let alice = &tcm.alice().await;
753 let bob = &tcm.bob().await;
754
755 let plain = Vec::from(b"this is the secret message");
756 let pk_for_encryption = load_self_public_key(alice).await?;
757
758 let ctext = pk_encrypt(
760 plain,
761 vec![pk_for_encryption],
762 KEYS.alice_secret.clone(),
763 true,
764 true,
765 SeipdVersion::V2,
766 )
767 .await?;
768
769 let bob_private_keyring = crate::key::load_self_secret_keyring(bob).await?;
771 let error = decrypt(ctext.into(), &bob_private_keyring, &[]).unwrap_err();
772
773 assert_eq!(
774 error.to_string(),
775 "missing key (Note: symmetric decryption was not tried: not symmetrically encrypted)"
776 );
777
778 Ok(())
779 }
780
781 #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
784 async fn test_anonymous_recipients() -> Result<()> {
785 let ctext = ctext_signed().await.as_bytes();
786 let cursor = Cursor::new(ctext);
787 let (msg, _headers) = Message::from_armor(cursor)?;
788
789 let Message::Encrypted { esk, .. } = msg else {
790 unreachable!();
791 };
792
793 for encrypted_session_key in esk {
794 let Esk::PublicKeyEncryptedSessionKey(pkesk) = encrypted_session_key else {
795 unreachable!()
796 };
797
798 match pkesk {
799 PublicKeyEncryptedSessionKey::V3 { id, .. } => {
800 assert!(id.is_wildcard());
801 }
802 PublicKeyEncryptedSessionKey::V6 { fingerprint, .. } => {
803 assert!(fingerprint.is_none());
804 }
805 PublicKeyEncryptedSessionKey::Other { .. } => unreachable!(),
806 }
807 }
808 Ok(())
809 }
810}