Skip to main content

sioul_core/
pgp.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2// Copyright © 2026 Aurélien Pierre
3
4//! OpenPGP (RFC 9580) for mail, with Sequoia and its pure-Rust cryptography.
5//!
6//! - **Your keys** live in `~/.local/share/sioul/pgp/own/`. A key made here
7//!   keeps its revocation certificate beside it, `<fingerprint>.rev`, yours
8//!   alone as the key is ([`revocation_path`]); revoking is not built. A key's private
9//!   part is in one of two places ([`Place`]): here, encrypted with a
10//!   passphrase the system keyring keeps (`sioul-sync` asks it), so nothing is
11//!   asked at each message; or on a security key, which signs and opens itself
12//!   after its PIN ([`crate::securitykey`]), Sioul keeping only its public
13//!   certificate. Keys come from an import (a file, GnuPG's export), are made
14//!   here, or are a security key's.
15//! - **Others' keys** live in `…/pgp/others/`: imported, found in their
16//!   messages (Autocrypt), or looked up on request.
17//! - **Signing and opening** take Sequoia's own `Signer` and `Decryptor`
18//!   traits: a key here or a security key, the same way. A message a security
19//!   key opened keeps its session key in memory ([`SessionKeys`]), so that it
20//!   opens again without the key until Sioul closes.
21//! - **What this module does not do**: talk to the network, or to the
22//!   keyring. Passphrases and PINs come in as arguments.
23
24use crate::config::data_dir;
25use crate::securitykey::{self, CardAccess, CardDecryptor, KnownCard};
26use openpgp::cert::prelude::*;
27use openpgp::crypto::{self, Password, SessionKey, mpi};
28use openpgp::packet::{PKESK, SKESK, key};
29use openpgp::parse::Parse;
30use openpgp::parse::stream::{DecryptionHelper, DecryptorBuilder, DetachedVerifierBuilder, MessageLayer, MessageStructure, VerificationHelper};
31use openpgp::policy::StandardPolicy;
32use openpgp::serialize::stream::{Armorer, Encryptor, LiteralWriter, Message, Signer};
33use openpgp::serialize::{Serialize, SerializeInto};
34use openpgp::types::{HashAlgorithm, SymmetricAlgorithm};
35use openpgp::{Cert, KeyHandle};
36/// A key's fingerprint, as the crates that do not use Sequoia themselves name it.
37pub use openpgp::Fingerprint;
38use sequoia_openpgp as openpgp;
39use serde::Serialize as Serde;
40use std::cell::RefCell;
41use std::collections::BTreeMap;
42use std::io::Write;
43use std::path::PathBuf;
44use std::sync::Mutex;
45
46fn policy() -> StandardPolicy<'static> {
47    StandardPolicy::new()
48}
49
50fn pgp_dir() -> PathBuf {
51    data_dir().join("pgp")
52}
53
54fn own_dir() -> PathBuf {
55    pgp_dir().join("own")
56}
57
58fn others_dir() -> PathBuf {
59    pgp_dir().join("others")
60}
61
62/// The keys Sioul knows: yours (their secret parts here, or on a security
63/// key), others', and your security keys.
64#[derive(Default, Clone)]
65pub struct Keys {
66    pub own: Vec<Cert>,
67    pub others: Vec<Cert>,
68    /// Your security keys, as `pgp/cards.toml` lists them.
69    pub cards: Vec<KnownCard>,
70}
71
72/// Where the private part of one of your keys is.
73#[derive(Debug, Clone, PartialEq, Eq)]
74pub enum Place {
75    /// Here: its secret part in `pgp/own`, encrypted with a passphrase the system keyring keeps.
76    Here,
77    /// On a security key, by its identifier ("0006:12345678").
78    Card {
79        /// The security key, as [`securitykey::CardInfo::ident`].
80        ident: String,
81    },
82}
83
84/// Your key for an address: its certificate, and where its private part is.
85#[derive(Debug, Clone)]
86pub struct Own<'a> {
87    pub cert: &'a Cert,
88    pub place: Place,
89}
90
91fn read_certs(dir: &std::path::Path) -> Vec<Cert> {
92    let Ok(entries) = std::fs::read_dir(dir) else { return Vec::new() };
93    entries
94        .filter_map(Result::ok)
95        .map(|e| e.path())
96        .filter(|p| p.extension().is_some_and(|x| x == "asc" || x == "pgp"))
97        .filter_map(|p| std::fs::read(&p).ok())
98        .flat_map(|bytes| CertParser::from_bytes(&bytes).map(|parser| parser.filter_map(Result::ok).collect::<Vec<_>>()).unwrap_or_default())
99        .collect()
100}
101
102/// Whether a key holds `address` among its user IDs.
103fn names(cert: &Cert, address: &str) -> bool {
104    cert.userids().any(|u| u.userid().email().ok().flatten().is_some_and(|e| e.eq_ignore_ascii_case(address.trim())))
105}
106
107impl Keys {
108    pub fn load() -> Keys {
109        Keys { own: read_certs(&own_dir()), others: read_certs(&others_dir()), cards: securitykey::known_cards() }
110    }
111
112    /// Your key for an address, able to sign now. A security key that serves
113    /// the address comes first, and alone: a key kept here for the same
114    /// address only opens older mail. When that security key's certificate
115    /// expired, nothing signs for the address until it is renewed.
116    pub fn own_for(&self, address: &str) -> Option<Own<'_>> {
117        if let Some((card, cert)) = self.card_for(address) {
118            return can_sign_with(cert, &card.sign).then(|| Own { cert, place: Place::Card { ident: card.ident.clone() } });
119        }
120        let policy = policy();
121        self.own
122            .iter()
123            .find(|c| names(c, address) && c.keys().secret().with_policy(&policy, None).alive().revoked(false).for_signing().next().is_some())
124            .map(|cert| Own { cert, place: Place::Here })
125    }
126
127    /// The security key that signs for `address`, and its certificate, even
128    /// when it expired: to say so.
129    pub fn card_for(&self, address: &str) -> Option<(&KnownCard, &Cert)> {
130        self.cards.iter().filter(|card| card.serves(address)).find_map(|card| Some((card, self.cert(&card.cert)?)))
131    }
132
133    /// Your certificate of this fingerprint.
134    pub fn cert(&self, fingerprint: &str) -> Option<&Cert> {
135        self.own.iter().find(|c| c.fingerprint().to_hex().eq_ignore_ascii_case(fingerprint))
136    }
137
138    /// A key to encrypt to `address`: someone's, or yours.
139    pub fn for_address(&self, address: &str) -> Option<&Cert> {
140        let policy = policy();
141        self.others.iter().chain(&self.own).filter(|c| names(c, address)).find(|c| c.keys().with_policy(&policy, None).alive().revoked(false).for_transport_encryption().next().is_some())
142    }
143
144    fn all(&self) -> impl Iterator<Item = &Cert> {
145        self.own.iter().chain(&self.others)
146    }
147
148    fn by_handle(&self, handle: &KeyHandle) -> Option<&Cert> {
149        self.all().find(|c| c.keys().any(|k| k.key().key_handle().aliases(handle)))
150    }
151}
152
153/// Whether the key `fingerprint` of `cert` can sign now: valid, alive, not
154/// revoked, allowed to sign.
155fn can_sign_with(cert: &Cert, fingerprint: &str) -> bool {
156    let policy = policy();
157    cert.keys().with_policy(&policy, None).alive().revoked(false).for_signing().any(|k| k.key().fingerprint().to_hex().eq_ignore_ascii_case(fingerprint))
158}
159
160/// The key `fingerprint` of `cert`, its public part, primary or subkey.
161pub fn key_of(cert: &Cert, fingerprint: &str) -> Option<securitykey::PublicKey> {
162    cert.keys().find(|k| k.key().fingerprint().to_hex().eq_ignore_ascii_case(fingerprint)).map(|k| k.key().clone())
163}
164
165/// A key as the window lists it.
166#[derive(Debug, Clone, Serde, PartialEq, Eq)]
167pub struct KeySummary {
168    pub fingerprint: String,
169    /// "Jane Exemple <jane@example.org>", one per user ID.
170    pub names: Vec<String>,
171    pub own: bool,
172    /// "2026-10-03".
173    pub created: String,
174    /// "2029-10-03", or empty when it does not expire.
175    pub expires: String,
176    pub can_encrypt: bool,
177    /// The security key holding its private keys ("0006:12345678"); empty when they are here.
178    pub card: String,
179    /// Yours, made here: its revocation certificate is kept beside it (`revocation_path`).
180    pub revocation: bool,
181}
182
183fn summary(cert: &Cert, own: bool) -> KeySummary {
184    let policy = policy();
185    let valid = cert.with_policy(&policy, None).ok();
186    let date = |t: std::time::SystemTime| jiff::Timestamp::try_from(t).map(|t| t.strftime("%Y-%m-%d").to_string()).unwrap_or_default();
187    KeySummary {
188        fingerprint: cert.fingerprint().to_hex(),
189        names: cert.userids().map(|u| String::from_utf8_lossy(u.userid().value()).to_string()).collect(),
190        own,
191        created: date(cert.primary_key().key().creation_time()),
192        expires: valid.as_ref().and_then(|v| v.primary_key().key_expiration_time()).map(date).unwrap_or_default(),
193        can_encrypt: cert.keys().with_policy(&policy, None).alive().revoked(false).for_transport_encryption().next().is_some(),
194        card: String::new(),
195        revocation: false,
196    }
197}
198
199/// Every key, yours first; a security key's certificate names its key.
200pub fn summaries() -> Vec<KeySummary> {
201    let keys = Keys::load();
202    let mut all: Vec<KeySummary> = keys.own.iter().map(|c| summary(c, true)).chain(keys.others.iter().map(|c| summary(c, false))).collect();
203    for key in all.iter_mut().filter(|k| k.own) {
204        if let Some(card) = keys.cards.iter().find(|c| c.cert.eq_ignore_ascii_case(&key.fingerprint)) {
205            key.card = card.ident.clone();
206        }
207        key.revocation = revocation_path(&key.fingerprint).is_file();
208    }
209    all
210}
211
212/// Keeps a key, merged with what was known of it. Secret parts go with
213/// yours, and so does the certificate of a security key of yours, whatever
214/// brings it again (an Autocrypt header, a lookup).
215fn store(cert: Cert) -> Result<KeySummary, String> {
216    store_at(&pgp_dir(), cert)
217}
218
219/// [`store`] into the folder `root` (`pgp/`).
220fn store_at(root: &std::path::Path, cert: Cert) -> Result<KeySummary, String> {
221    let known_as_own = root.join("own").join(format!("{}.asc", cert.fingerprint().to_hex())).exists();
222    let own = cert.is_tsk() || known_as_own;
223    store_in(root, cert, own)
224}
225
226fn store_in(root: &std::path::Path, cert: Cert, own: bool) -> Result<KeySummary, String> {
227    let dir = root.join(if own { "own" } else { "others" });
228    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
229    let path = dir.join(format!("{}.asc", cert.fingerprint().to_hex()));
230    let merged = match std::fs::read(&path).ok().and_then(|bytes| Cert::from_bytes(&bytes).ok()) {
231        Some(known) => known.merge_public_and_secret(cert).map_err(|e| e.to_string())?,
232        None => cert,
233    };
234    let armored = if merged.is_tsk() { merged.as_tsk().armored().to_vec() } else { merged.armored().to_vec() }.map_err(|e| e.to_string())?;
235    write_key(&path, &armored)?;
236    Ok(summary(&merged, own))
237}
238
239/// Keeps the certificate of your security key with your keys: public parts only.
240pub fn keep_card_certificate(cert: &Cert) -> Result<KeySummary, String> {
241    store_in(&pgp_dir(), cert.clone().strip_secret_key_material(), true)
242}
243
244/// Writes a key file beside, then moves it in place. On Unix it is readable by
245/// you alone, whatever the umask: your secret keys, and whose keys you keep.
246fn write_key(path: &std::path::Path, armored: &[u8]) -> Result<(), String> {
247    let fail = |e: std::io::Error| format!("{}: {e}", path.display());
248    let temporary = path.with_extension("tmp");
249    // A file left by an interrupted write keeps its permissions when truncated: it goes first.
250    let _ = std::fs::remove_file(&temporary);
251    let mut options = std::fs::OpenOptions::new();
252    options.write(true).create_new(true);
253    #[cfg(unix)]
254    {
255        use std::os::unix::fs::OpenOptionsExt;
256        options.mode(0o600);
257    }
258    let mut file = options.open(&temporary).map_err(fail)?;
259    file.write_all(armored).and_then(|()| file.sync_all()).map_err(fail)?;
260    drop(file);
261    std::fs::rename(&temporary, path).map_err(fail)
262}
263
264/// The certificates of `bytes` (armored or binary), read, not kept.
265pub fn certificates_in(bytes: &[u8]) -> Vec<Cert> {
266    CertParser::from_bytes(bytes).map(|parser| parser.filter_map(Result::ok).collect()).unwrap_or_default()
267}
268
269/// Imports the keys of a file: armored or binary, public or secret.
270pub fn import(bytes: &[u8]) -> Result<Vec<KeySummary>, String> {
271    let certs: Vec<Cert> = CertParser::from_bytes(bytes).map_err(|e| e.to_string())?.filter_map(Result::ok).collect();
272    if certs.is_empty() {
273        return Err("no OpenPGP key".into());
274    }
275    certs.into_iter().map(store).collect()
276}
277
278/// Whether `passphrase` opens the secret parts of your key `fingerprint`.
279pub fn opens(fingerprint: &str, passphrase: &str) -> bool {
280    let keys = Keys::load();
281    let Some(cert) = keys.own.iter().find(|c| c.fingerprint().to_hex() == fingerprint) else { return false };
282    let policy = policy();
283    let password = Password::from(passphrase);
284    cert.keys().secret().with_policy(&policy, None).for_signing().next().is_some_and(|k| k.key().clone().decrypt_secret(&password).is_ok())
285}
286
287/// A long random passphrase for a key made here: nobody types it, the keyring keeps it.
288pub fn new_passphrase() -> Result<String, String> {
289    let mut bytes = [0u8; 24];
290    openpgp::crypto::random(&mut bytes).map_err(|e| e.to_string())?;
291    Ok(crate::lines::base64_encode(&bytes))
292}
293
294/// Makes a key for you: Curve25519, for signing and encrypting, valid three
295/// years; its secret parts encrypted with `passphrase`. Its revocation
296/// certificate, which Sequoia makes with it, is kept beside it
297/// (`revocation_path`): the only way to revoke the key once it is lost.
298pub fn generate(name: &str, address: &str, passphrase: &str) -> Result<KeySummary, String> {
299    generate_at(&pgp_dir(), name, address, passphrase)
300}
301
302/// [`generate`] into the folder `root` (`pgp/`).
303fn generate_at(root: &std::path::Path, name: &str, address: &str, passphrase: &str) -> Result<KeySummary, String> {
304    let userid = if name.trim().is_empty() { format!("<{}>", address.trim()) } else { format!("{} <{}>", name.trim(), address.trim()) };
305    let (cert, revocation) = CertBuilder::general_purpose(Some(userid))
306        .set_cipher_suite(CipherSuite::Cv25519)
307        .set_validity_period(std::time::Duration::from_secs(3 * 365 * 24 * 3600))
308        .set_password(Some(Password::from(passphrase)))
309        .generate()
310        .map_err(|e| e.to_string())?;
311    // The certificate first: a key is never kept without it.
312    let own = root.join("own");
313    std::fs::create_dir_all(&own).map_err(|e| e.to_string())?;
314    let kept = own.join(format!("{}.rev", cert.fingerprint().to_hex()));
315    write_key(&kept, revocation_text(&cert, revocation)?.as_bytes())?;
316    match store_at(root, cert) {
317        Ok(summary) => Ok(KeySummary { revocation: true, ..summary }),
318        Err(e) => {
319            let _ = std::fs::remove_file(&kept);
320            Err(e)
321        }
322    }
323}
324
325/// Where the revocation certificate of your key made here is kept: beside
326/// the key, `pgp/own/<fingerprint>.rev`, readable by you alone (`write_key`),
327/// as GnuPG keeps its own in `openpgp-revocs.d`.
328pub fn revocation_path(fingerprint: &str) -> PathBuf {
329    own_dir().join(format!("{fingerprint}.rev"))
330}
331
332/// A revocation certificate as a file to keep: what it is, in a few lines,
333/// then the signature armored, a colon before its first line so that it
334/// cannot be imported by mistake (as GnuPG writes its own).
335fn revocation_text(cert: &Cert, revocation: openpgp::packet::Signature) -> Result<String, String> {
336    let mut armored = Vec::new();
337    {
338        let mut writer = openpgp::armor::Writer::with_headers(&mut armored, openpgp::armor::Kind::PublicKey, [("Comment", "This is a revocation certificate")]).map_err(|e| e.to_string())?;
339        openpgp::Packet::from(revocation).serialize(&mut writer).map_err(|e| e.to_string())?;
340        writer.finalize().map_err(|e| e.to_string())?;
341    }
342    let names: Vec<String> = cert.userids().map(|u| String::from_utf8_lossy(u.userid().value()).to_string()).collect();
343    Ok(format!(
344        "This is the revocation certificate of the OpenPGP key\n{}\n{}\n\n\
345         Published, it tells everyone that this key must no longer be used, for\n\
346         good: it cannot be taken back. Keep a copy where only you can reach it,\n\
347         apart from the key, for the day the key is lost or stolen.\n\n\
348         A colon stands before the five dashes below, so that the certificate\n\
349         cannot be imported by mistake: take it away with a text editor before\n\
350         you import the certificate and publish it.\n\n:{}",
351        cert.fingerprint().to_hex(),
352        names.join("\n"),
353        String::from_utf8_lossy(&armored)
354    ))
355}
356
357/// Copies the revocation certificate of your key `fingerprint` into
358/// `folder`, as `<fingerprint>.rev` (a number added when that name is
359/// taken), yours alone; none kept for that key, an error.
360pub fn save_revocation(fingerprint: &str, folder: &std::path::Path) -> Result<PathBuf, String> {
361    save_revocation_at(&pgp_dir(), fingerprint, folder)
362}
363
364/// [`save_revocation`] from the folder `root` (`pgp/`).
365fn save_revocation_at(root: &std::path::Path, fingerprint: &str, folder: &std::path::Path) -> Result<PathBuf, String> {
366    let text = std::fs::read(root.join("own").join(format!("{fingerprint}.rev"))).map_err(|e| e.to_string())?;
367    std::fs::create_dir_all(folder).map_err(|e| e.to_string())?;
368    let path = (1..)
369        .map(|n| folder.join(if n == 1 { format!("{fingerprint}.rev") } else { format!("{fingerprint}-{n}.rev") }))
370        .find(|p| !p.exists())
371        .unwrap_or_else(|| folder.join(format!("{fingerprint}.rev")));
372    write_key(&path, &text)?;
373    Ok(path)
374}
375
376/// A key's public part, armored, to give to others.
377pub fn export_public(fingerprint: &str) -> Option<String> {
378    let keys = Keys::load();
379    let cert = keys.all().find(|c| c.fingerprint().to_hex() == fingerprint)?;
380    cert.armored().to_vec().ok().map(|b| String::from_utf8_lossy(&b).to_string())
381}
382
383/// Removes a key; yours are kept aside in `pgp/removed`, never deleted, with
384/// their revocation certificate.
385pub fn remove(fingerprint: &str) -> Result<(), String> {
386    for (dir, own) in [(own_dir(), true), (others_dir(), false)] {
387        let path = dir.join(format!("{fingerprint}.asc"));
388        if !path.exists() {
389            continue;
390        }
391        if own {
392            let aside = data_dir().join("pgp").join("removed");
393            std::fs::create_dir_all(&aside).map_err(|e| e.to_string())?;
394            std::fs::rename(&path, aside.join(format!("{fingerprint}.asc"))).map_err(|e| e.to_string())?;
395            let revocation = revocation_path(fingerprint);
396            if revocation.exists() {
397                std::fs::rename(&revocation, aside.join(format!("{fingerprint}.rev"))).map_err(|e| e.to_string())?;
398            }
399        } else {
400            std::fs::remove_file(&path).map_err(|e| e.to_string())?;
401        }
402    }
403    Ok(())
404}
405
406/// A signer of yours, as Sequoia takes it: a key here ([`here_signer`]) or
407/// on a security key ([`securitykey::CardSigner`]).
408pub type Signing<'a> = dyn crypto::Signer + Send + Sync + 'a;
409
410/// Your key here that signs, its secret part opened with `passphrase`: the
411/// signer of a key kept on this device.
412pub fn here_signer(cert: &Cert, passphrase: &str) -> Result<crypto::KeyPair, String> {
413    let policy = policy();
414    let key = cert.keys().secret().with_policy(&policy, None).alive().revoked(false).for_signing().next().ok_or("no signing key")?.key().clone();
415    let key = if key.has_unencrypted_secret() { key } else { key.decrypt_secret(&Password::from(passphrase)).map_err(|_| "wrong passphrase".to_string())? };
416    key.into_keypair().map_err(|e| e.to_string())
417}
418
419/// A signer lent for one message: Sequoia's stream takes its signer by value,
420/// and a security key's signer borrows what it needs to reach the key.
421struct Lent<'r, 'o>(&'r mut Signing<'o>);
422
423impl crypto::Signer for Lent<'_, '_> {
424    fn public(&self) -> &openpgp::packet::Key<key::PublicParts, key::UnspecifiedRole> {
425        self.0.public()
426    }
427
428    fn acceptable_hashes(&self) -> &[HashAlgorithm] {
429        self.0.acceptable_hashes()
430    }
431
432    fn sign(&mut self, hash_algo: HashAlgorithm, digest: &[u8]) -> openpgp::Result<mpi::Signature> {
433        self.0.sign(hash_algo, digest)
434    }
435}
436
437/// A detached signature of `data`, armored, and the hash it used ("pgp-sha512"),
438/// for multipart/signed (RFC 3156 §5). `signer` is your key here
439/// ([`here_signer`]) or on a security key ([`securitykey::CardSigner`]).
440pub fn sign_detached(data: &[u8], signer: &mut Signing<'_>) -> Result<(String, String), String> {
441    let mut out = Vec::new();
442    {
443        let message = Message::new(&mut out);
444        let message = Armorer::new(message).kind(openpgp::armor::Kind::Signature).build().map_err(|e| e.to_string())?;
445        let mut signing = Signer::new(message, Lent(signer)).map_err(|e| e.to_string())?.detached().build().map_err(|e| e.to_string())?;
446        signing.write_all(data).map_err(|e| e.to_string())?;
447        signing.finalize().map_err(|e| e.to_string())?;
448    }
449    let micalg = openpgp::Packet::from_bytes(&out)
450        .ok()
451        .and_then(|p| match p {
452            openpgp::Packet::Signature(s) => Some(format!("pgp-{}", s.hash_algo().to_string().to_ascii_lowercase())),
453            _ => None,
454        })
455        .unwrap_or_else(|| "pgp-sha512".into());
456    Ok((String::from_utf8_lossy(&out).to_string(), micalg))
457}
458
459/// `data` encrypted to the recipients' keys, signed by yours when given; armored.
460pub fn encrypt(data: &[u8], recipients: &[&Cert], signer: Option<&mut Signing<'_>>) -> Result<String, String> {
461    let policy = policy();
462    let keys: Vec<_> = recipients.iter().flat_map(|c| c.keys().with_policy(&policy, None).supported().alive().revoked(false).for_transport_encryption()).collect();
463    if keys.is_empty() {
464        return Err("no encryption key".into());
465    }
466    let mut out = Vec::new();
467    {
468        let message = Message::new(&mut out);
469        let message = Armorer::new(message).build().map_err(|e| e.to_string())?;
470        let message = Encryptor::for_recipients(message, keys).build().map_err(|e| e.to_string())?;
471        let message = match signer {
472            Some(signer) => Signer::new(message, Lent(signer)).map_err(|e| e.to_string())?.build().map_err(|e| e.to_string())?,
473            None => message,
474        };
475        let mut literal = LiteralWriter::new(message).build().map_err(|e| e.to_string())?;
476        literal.write_all(data).map_err(|e| e.to_string())?;
477        literal.finalize().map_err(|e| e.to_string())?;
478    }
479    Ok(String::from_utf8_lossy(&out).to_string())
480}
481
482/// A signature, checked.
483#[derive(Debug, Clone, Serde, PartialEq, Eq)]
484pub struct Checked {
485    pub good: bool,
486    /// The signer's first user ID, when their key is known.
487    pub signer: String,
488    pub fingerprint: String,
489    /// Why it is not good: "unknown key", "bad signature"…
490    pub problem: String,
491}
492
493/// What opening a message found.
494#[derive(Debug, Clone, Default)]
495pub struct Opened {
496    pub data: Vec<u8>,
497    pub signatures: Vec<Checked>,
498}
499
500/// The session keys of messages a security key opened, by message: such a
501/// message opens again, with its attachments and its answer, without the key,
502/// until Sioul closes. In memory only, never on disk.
503#[derive(Default)]
504pub struct SessionKeys(Mutex<BTreeMap<[u8; 32], (Option<SymmetricAlgorithm>, SessionKey)>>);
505
506impl SessionKeys {
507    /// None yet.
508    pub const fn new() -> SessionKeys {
509        SessionKeys(Mutex::new(BTreeMap::new()))
510    }
511
512    /// The session's own: one for each running Sioul.
513    pub fn global() -> &'static SessionKeys {
514        static GLOBAL: SessionKeys = SessionKeys::new();
515        &GLOBAL
516    }
517
518    fn get(&self, id: &[u8; 32]) -> Option<(Option<SymmetricAlgorithm>, SessionKey)> {
519        self.0.lock().ok()?.get(id).cloned()
520    }
521
522    fn keep(&self, id: [u8; 32], algo: Option<SymmetricAlgorithm>, key: SessionKey) {
523        if let Ok(mut kept) = self.0.lock() {
524            kept.insert(id, (algo, key));
525        }
526    }
527
528    /// Forgets them all: every message a security key opened needs it again.
529    pub fn forget(&self) {
530        if let Ok(mut kept) = self.0.lock() {
531            kept.clear();
532        }
533    }
534}
535
536/// A message's name among the session keys: SHA-256 of its encrypted bytes.
537fn message_id(message: &[u8]) -> Option<[u8; 32]> {
538    let mut context = HashAlgorithm::SHA256.context().ok()?.for_digest();
539    context.update(message);
540    context.into_digest().ok()?.try_into().ok()
541}
542
543/// How your keys open what is encrypted to you.
544pub struct Unlock<'a> {
545    /// The passphrase of a key kept here, from the system keyring.
546    pub passphrase: &'a dyn Fn(&Fingerprint) -> Option<String>,
547    /// The session keys of messages a security key opened before.
548    pub sessions: &'a SessionKeys,
549    /// Your security key, when you asked to open with it now; without it, no
550    /// security key is ever asked anything.
551    pub card: Option<&'a CardAccess<'a>>,
552    /// The security key a message waits for.
553    wanted: RefCell<Option<String>>,
554}
555
556impl<'a> Unlock<'a> {
557    /// Your keys here, with their passphrases, and the session keys kept.
558    pub fn new(passphrase: &'a dyn Fn(&Fingerprint) -> Option<String>, sessions: &'a SessionKeys) -> Unlock<'a> {
559        Unlock { passphrase, sessions, card: None, wanted: RefCell::new(None) }
560    }
561
562    /// And your security key, asked now.
563    pub fn with_card(mut self, card: &'a CardAccess<'a>) -> Unlock<'a> {
564        self.card = Some(card);
565        self
566    }
567
568    /// The security key the last message waits for: it is encrypted to it, and
569    /// it was not asked, or it did not open it.
570    pub fn wanted(&self) -> Option<String> {
571        self.wanted.borrow().clone()
572    }
573}
574
575struct Helper<'a> {
576    keys: &'a Keys,
577    unlock: &'a Unlock<'a>,
578    /// The message's name for its session key.
579    id: Option<[u8; 32]>,
580    checked: Vec<Checked>,
581}
582
583impl VerificationHelper for Helper<'_> {
584    fn get_certs(&mut self, ids: &[KeyHandle]) -> openpgp::Result<Vec<Cert>> {
585        Ok(ids.iter().filter_map(|id| self.keys.by_handle(id).cloned()).collect())
586    }
587
588    fn check(&mut self, structure: MessageStructure) -> openpgp::Result<()> {
589        for layer in structure {
590            let MessageLayer::SignatureGroup { results } = layer else { continue };
591            for result in results {
592                self.checked.push(match result {
593                    Ok(good) => Checked {
594                        good: true,
595                        signer: good.ka.cert().userids().next().map(|u| String::from_utf8_lossy(u.userid().value()).to_string()).unwrap_or_default(),
596                        fingerprint: good.ka.cert().fingerprint().to_hex(),
597                        problem: String::new(),
598                    },
599                    Err(error) => {
600                        use openpgp::parse::stream::VerificationError as E;
601                        let (fingerprint, problem) = match &error {
602                            E::MissingKey { sig } => (sig.issuer_fingerprints().next().map(|f| f.to_hex()).unwrap_or_default(), "unknown key"),
603                            E::BadSignature { .. } => (String::new(), "bad signature"),
604                            E::BadKey { .. } => (String::new(), "bad key"),
605                            E::MalformedSignature { .. } => (String::new(), "malformed signature"),
606                            E::UnboundKey { .. } => (String::new(), "unbound key"),
607                            _ => (String::new(), "unknown"),
608                        };
609                        Checked { good: false, signer: String::new(), fingerprint, problem: problem.to_string() }
610                    }
611                });
612            }
613        }
614        // Whatever the signatures say, the text is shown with what they say.
615        Ok(())
616    }
617}
618
619impl DecryptionHelper for Helper<'_> {
620    fn decrypt(&mut self, pkesks: &[PKESK], _skesks: &[SKESK], sym_algo: Option<SymmetricAlgorithm>, decrypt: &mut dyn FnMut(Option<SymmetricAlgorithm>, &SessionKey) -> bool) -> openpgp::Result<Option<Cert>> {
621        // Opened before with a security key: its session key, kept in memory.
622        if let Some(id) = &self.id
623            && let Some((algo, session)) = self.unlock.sessions.get(id)
624            && decrypt(algo, &session)
625        {
626            return Ok(None);
627        }
628        let policy = policy();
629        for pkesk in pkesks {
630            for cert in &self.keys.own {
631                let passphrase = (self.unlock.passphrase)(&cert.fingerprint()).unwrap_or_default();
632                for key in cert.keys().secret().with_policy(&policy, None).for_transport_encryption().chain(cert.keys().secret().with_policy(&policy, None).for_storage_encryption()) {
633                    let recipient_matches = pkesk.recipient().is_none_or(|r| key.key().key_handle().aliases(&r));
634                    if !recipient_matches {
635                        continue;
636                    }
637                    let secret = key.key().clone();
638                    let secret = if secret.has_unencrypted_secret() { secret } else if let Ok(s) = secret.decrypt_secret(&Password::from(passphrase.as_str())) { s } else { continue };
639                    let Ok(mut pair) = secret.into_keypair() else { continue };
640                    if let Some((algo, session)) = pkesk.decrypt(&mut pair, sym_algo)
641                        && decrypt(algo, &session)
642                    {
643                        return Ok(Some(cert.clone()));
644                    }
645                }
646            }
647        }
648        // Keys on a security key: asked only when you asked to open with it;
649        // otherwise the message says which key it waits for.
650        for pkesk in pkesks {
651            let Some(recipient) = pkesk.recipient() else { continue };
652            for card in &self.keys.cards {
653                let Ok(fingerprint) = card.decrypt.parse::<Fingerprint>() else { continue };
654                if !KeyHandle::from(fingerprint).aliases(&recipient) {
655                    continue;
656                }
657                let Some((cert, public)) = self.keys.cert(&card.cert).and_then(|cert| Some((cert, key_of(cert, &card.decrypt)?))) else { continue };
658                *self.unlock.wanted.borrow_mut() = Some(card.ident.clone());
659                let Some(access) = self.unlock.card else { continue };
660                let mut decryptor = CardDecryptor::new(access, &card.ident, public);
661                if let Some((algo, session)) = pkesk.decrypt(&mut decryptor, sym_algo)
662                    && decrypt(algo, &session)
663                {
664                    *self.unlock.wanted.borrow_mut() = None;
665                    if let Some(id) = self.id {
666                        self.unlock.sessions.keep(id, algo, session);
667                    }
668                    return Ok(Some(cert.clone()));
669                }
670            }
671        }
672        Err(openpgp::Error::InvalidOperation("no key of yours opens this message".into()).into())
673    }
674}
675
676/// The most a decrypted message may hold: anyone can encrypt to your key, and
677/// a few kilobytes of compressed data can inflate to gigabytes.
678const LARGEST_DECRYPTED: u64 = 256 * 1024 * 1024;
679
680/// Decrypts an OpenPGP message (armored or not) with your keys, checking the
681/// signatures inside. `unlock` gives the passphrase of a key of yours here,
682/// the session keys kept, and your security key when you asked to use it.
683pub fn decrypt(message: &[u8], keys: &Keys, unlock: &Unlock) -> Result<Opened, String> {
684    use std::io::Read as _;
685    let policy = policy();
686    let helper = Helper { keys, unlock, id: message_id(message), checked: Vec::new() };
687    let mut decryptor = DecryptorBuilder::from_bytes(message).map_err(|e| e.to_string())?.with_policy(&policy, None, helper).map_err(|e| e.to_string())?;
688    let mut data = Vec::new();
689    (&mut decryptor).take(LARGEST_DECRYPTED + 1).read_to_end(&mut data).map_err(|e| e.to_string())?;
690    if data.len() as u64 > LARGEST_DECRYPTED {
691        return Err(format!("larger than {LARGEST_DECRYPTED} bytes once decrypted"));
692    }
693    let helper = decryptor.into_helper();
694    Ok(Opened { data, signatures: helper.checked })
695}
696
697/// Checks a detached signature of `data` (multipart/signed).
698pub fn verify_detached(data: &[u8], signature: &[u8], keys: &Keys) -> Vec<Checked> {
699    let policy = policy();
700    let none = |_: &Fingerprint| None;
701    let sessions = SessionKeys::new();
702    let unlock = Unlock::new(&none, &sessions);
703    let helper = Helper { keys, unlock: &unlock, id: None, checked: Vec::new() };
704    let Ok(builder) = DetachedVerifierBuilder::from_bytes(signature) else { return Vec::new() };
705    let Ok(mut verifier) = builder.with_policy(&policy, None, helper) else { return Vec::new() };
706    let _ = verifier.verify_bytes(data);
707    verifier.into_helper().checked
708}
709
710/// Where a domain's Web Key Directory keeps the key of `address`: the
711/// advanced address, then the direct one (draft-koch-openpgp-webkey-service §3.1).
712pub fn wkd_urls(address: &str) -> Option<[String; 2]> {
713    let (local, domain) = address.trim().rsplit_once('@')?;
714    let domain = domain.to_ascii_lowercase();
715    // A name in the DNS, nothing else: an address answered may come from anyone,
716    // and "x@192.168.1.1", "x@[::1]" or "x@host:8080/path?" would send the
717    // request elsewhere than to a domain's Web Key Directory.
718    let labels: Vec<&str> = domain.split('.').collect();
719    let named = labels.len() >= 2
720        && labels.iter().all(|l| !l.is_empty() && !l.starts_with('-') && !l.ends_with('-') && l.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'))
721        && labels.last().is_some_and(|tld| tld.chars().any(|c| c.is_ascii_alphabetic()) && *tld != "localhost");
722    if !named {
723        return None;
724    }
725    let mut context = openpgp::types::HashAlgorithm::SHA1.context().ok()?.for_digest();
726    context.update(local.to_lowercase().as_bytes());
727    let digest = context.into_digest().ok()?;
728    let hash = zbase32(&digest);
729    let query = local.bytes().map(|b| if b.is_ascii_alphanumeric() || b"-._~".contains(&b) { (b as char).to_string() } else { format!("%{b:02X}") }).collect::<String>();
730    Some([
731        format!("https://openpgpkey.{domain}/.well-known/openpgpkey/{domain}/hu/{hash}?l={query}"),
732        format!("https://{domain}/.well-known/openpgpkey/hu/{hash}?l={query}"),
733    ])
734}
735
736/// z-base-32 (RFC 6189 §5.1.6), as WKD names keys.
737fn zbase32(bytes: &[u8]) -> String {
738    const ALPHABET: &[u8; 32] = b"ybndrfg8ejkmcpqxot1uwisza345h769";
739    let mut out = String::new();
740    let (mut buffer, mut bits) = (0u32, 0u32);
741    for &byte in bytes {
742        buffer = (buffer << 8) | u32::from(byte);
743        bits += 8;
744        while bits >= 5 {
745            bits -= 5;
746            out.push(ALPHABET[((buffer >> bits) & 31) as usize] as char);
747        }
748    }
749    if bits > 0 {
750        out.push(ALPHABET[((buffer << (5 - bits)) & 31) as usize] as char);
751    }
752    out
753}
754
755/// Keeps keys found for `address`: public parts only, and only those naming it.
756pub fn learn(bytes: &[u8], address: &str) -> Vec<KeySummary> {
757    CertParser::from_bytes(bytes)
758        .map(|parser| parser.filter_map(Result::ok).filter(|c| names(c, address)).filter_map(|c| store(c.strip_secret_key_material()).ok()).collect())
759        .unwrap_or_default()
760}
761
762/// What a protected message showed when opened, for the reader's line.
763#[derive(Debug, Clone, Default, Serde, PartialEq, Eq)]
764pub struct PgpView {
765    pub encrypted: bool,
766    /// Decrypted; false when no key of yours opens it.
767    pub opened: bool,
768    pub signatures: Vec<Checked>,
769    /// Not opened, and encrypted to your security key of this identifier: the
770    /// reader offers "Open with your security key".
771    pub security_key: Option<String>,
772}
773
774/// Lone LF become CRLF: what was signed is the canonical form (RFC 3156 §5).
775fn canonical(bytes: &[u8]) -> Vec<u8> {
776    let mut out = Vec::with_capacity(bytes.len() + bytes.len() / 32);
777    for (i, &b) in bytes.iter().enumerate() {
778        if b == b'\n' && (i == 0 || bytes[i - 1] != b'\r') {
779            out.push(b'\r');
780        }
781        out.push(b);
782    }
783    out
784}
785
786fn kind_of(part: &mail_parser::MessagePart) -> (String, String) {
787    use mail_parser::MimeHeaders;
788    part.content_type().map_or_else(Default::default, |t| {
789        (format!("{}/{}", t.ctype(), t.subtype().unwrap_or("")).to_ascii_lowercase(), t.attribute("protocol").unwrap_or("").to_ascii_lowercase())
790    })
791}
792
793/// The message's own header lines but its MIME ones, which the decrypted body brings.
794fn outer_headers(raw: &[u8], body_offset: usize) -> Vec<u8> {
795    let head = String::from_utf8_lossy(&raw[..body_offset.min(raw.len())]).to_string();
796    let mut out = String::new();
797    let mut keep = false;
798    for line in head.split_inclusive('\n') {
799        if !line.starts_with([' ', '\t']) {
800            let name = line.split(':').next().unwrap_or("").trim().to_ascii_lowercase();
801            keep = !name.is_empty() && !name.starts_with("content-") && name != "mime-version";
802        }
803        if keep {
804            out.push_str(line);
805        }
806    }
807    canonical(out.trim_end().as_bytes()).into_iter().chain(b"\r\nMIME-Version: 1.0\r\n".iter().copied()).collect()
808}
809
810/// Opens a protected message: PGP/MIME encrypted or signed (RFC 3156), or
811/// inline PGP. Gives back the message as it reads once decrypted (its own
812/// headers, then what was inside), when it was encrypted, and what was found;
813/// None when nothing is protected. A message encrypted to your security key
814/// opens only when `unlock` carries it, or from a session key kept; otherwise
815/// its view names the key it waits for.
816pub fn open(raw: &[u8], keys: &Keys, unlock: &Unlock) -> Option<(Option<Vec<u8>>, PgpView)> {
817    let message = mail_parser::MessageParser::default().parse(raw)?;
818    let root = message.parts.first()?;
819    let (kind, protocol) = kind_of(root);
820    let children: Vec<&mail_parser::MessagePart> = root.sub_parts().map(|ids| ids.iter().filter_map(|&id| message.parts.get(id as usize)).collect()).unwrap_or_default();
821    let closed = || PgpView { encrypted: true, opened: false, signatures: Vec::new(), security_key: unlock.wanted() };
822    if kind == "multipart/encrypted" && protocol == "application/pgp-encrypted" {
823        let payload = children.iter().find(|p| kind_of(p).0 == "application/octet-stream").map(|p| p.contents().to_vec())?;
824        return Some(match decrypt(&payload, keys, unlock) {
825            Ok(opened) => {
826                let mut virtual_message = outer_headers(raw, root.raw_body_offset() as usize);
827                virtual_message.extend(canonical(&opened.data));
828                let mut signatures = opened.signatures;
829                // Signed, then encrypted as a multipart/signed (RFC 3156 §6.1): checked too.
830                if let Some((_, inner)) = open(&virtual_message, keys, unlock) {
831                    signatures.extend(inner.signatures);
832                }
833                (Some(virtual_message), PgpView { encrypted: true, opened: true, signatures, security_key: None })
834            }
835            Err(_) => (None, closed()),
836        });
837    }
838    if kind == "multipart/signed" && protocol == "application/pgp-signature" && children.len() >= 2 {
839        let signed = children[0];
840        let (start, end) = (signed.raw_header_offset() as usize, signed.raw_end_offset() as usize);
841        let data = canonical(raw.get(start..end)?);
842        let signature = children.iter().find(|p| kind_of(p).0 == "application/pgp-signature")?.contents().to_vec();
843        // The line break before the boundary belongs to the boundary; parsers
844        // differ on where a part ends, so the part is checked with and without it.
845        let mut checked = verify_detached(&data, &signature, keys);
846        if !checked.iter().any(|c| c.good) && data.ends_with(b"\r\n") {
847            let trimmed = verify_detached(&data[..data.len() - 2], &signature, keys);
848            if trimmed.iter().any(|c| c.good) {
849                checked = trimmed;
850            }
851        }
852        return Some((None, PgpView { encrypted: false, opened: true, signatures: checked, security_key: None }));
853    }
854    // Inline: an armored message in the text.
855    let text = message.body_text(0)?.to_string();
856    let start = text.find("-----BEGIN PGP MESSAGE-----")?;
857    let end = text[start..].find("-----END PGP MESSAGE-----").map(|e| start + e + "-----END PGP MESSAGE-----".len())?;
858    Some(match decrypt(text[start..end].as_bytes(), keys, unlock) {
859        Ok(opened) => {
860            let mut virtual_message = outer_headers(raw, root.raw_body_offset() as usize);
861            virtual_message.extend_from_slice(b"Content-Type: text/plain; charset=utf-8\r\n\r\n");
862            virtual_message.extend(canonical(&opened.data));
863            (Some(virtual_message), PgpView { encrypted: true, opened: true, signatures: opened.signatures, security_key: None })
864        }
865        Err(_) => (None, closed()),
866    })
867}
868
869/// Autocrypt (Level 1, §2.1): the key a message's header carries for its sender.
870pub fn autocrypt_key(header: &str, from: &str) -> Option<Cert> {
871    let mut addr = None;
872    let mut keydata = None;
873    for part in header.split(';') {
874        let (name, value) = part.split_once('=')?;
875        match name.trim() {
876            "addr" => addr = Some(value.trim().to_string()),
877            "keydata" => keydata = Some(value.split_whitespace().collect::<String>()),
878            _ => {}
879        }
880    }
881    if !addr?.eq_ignore_ascii_case(from.trim()) {
882        return None;
883    }
884    let bytes = crate::lines::base64_decode(&keydata?)?;
885    let cert = Cert::from_bytes(&bytes).ok()?;
886    names(&cert, from).then_some(cert)
887}
888
889/// Keeps a key from an Autocrypt header: only public parts, only when it names its sender.
890pub fn learn_autocrypt(header: &str, from: &str) -> Option<KeySummary> {
891    let cert = autocrypt_key(header, from)?;
892    store(cert.strip_secret_key_material()).ok()
893}
894
895/// The Autocrypt header for your outgoing mail: your key, smallest form
896/// (primary, encryption subkey, the user ID with your address).
897pub fn autocrypt_header(cert: &Cert, address: &str) -> Option<String> {
898    let policy = policy();
899    let valid = cert.with_policy(&policy, None).ok()?;
900    let userid = valid.userids().find(|u| u.userid().email().ok().flatten().is_some_and(|e| e.eq_ignore_ascii_case(address)))?;
901    let encryption = valid.keys().subkeys().alive().revoked(false).for_transport_encryption().next()?;
902    let mut packets: Vec<openpgp::Packet> = vec![cert.primary_key().key().clone().into()];
903    packets.push(userid.userid().clone().into());
904    packets.extend(userid.self_signatures().take(1).cloned().map(Into::into));
905    packets.push(encryption.key().clone().into());
906    packets.extend(encryption.self_signatures().take(1).cloned().map(Into::into));
907    let minimal = Cert::from_packets(packets.into_iter()).ok()?;
908    let mut bytes = Vec::new();
909    minimal.serialize(&mut bytes).ok()?;
910    // A space every 76 characters, where the header may be folded (Autocrypt §2.1):
911    // a line holds 998 characters at most (RFC 5322 §2.1.1), and an RSA key
912    // imported from GnuPG takes several thousand.
913    let encoded: Vec<char> = crate::lines::base64_encode(&bytes).chars().collect();
914    let keydata = encoded.chunks(76).map(|chunk| chunk.iter().collect::<String>()).collect::<Vec<_>>().join(" ");
915    Some(format!("addr={address}; keydata={keydata}"))
916}
917
918#[cfg(test)]
919mod tests {
920    use super::*;
921
922    fn temporary() -> PathBuf {
923        let dir = std::env::temp_dir().join(format!("sioul-pgp-{}-{}", std::process::id(), std::thread::current().name().unwrap_or("t").replace("::", "-")));
924        std::fs::create_dir_all(&dir).unwrap();
925        dir
926    }
927
928    fn key(address: &str) -> Cert {
929        CertBuilder::general_purpose(Some(format!("Someone <{address}>")))
930            .set_cipher_suite(CipherSuite::Cv25519)
931            .set_password(Some(Password::from("secret words")))
932            .generate()
933            .unwrap()
934            .0
935    }
936
937    #[test]
938    fn signs_encrypts_decrypts_and_verifies() {
939        let _ = temporary();
940        let alice = key("alice@example.org");
941        let bob = key("bob@example.org");
942        let keys_of_bob = Keys { own: vec![bob.clone()], others: vec![alice.clone().strip_secret_key_material()], ..Keys::default() };
943        let mut signer = here_signer(&alice, "secret words").unwrap();
944        let sealed = encrypt(b"Content-Type: text/plain\r\n\r\nHello Bob.\r\n", &[&bob, &alice], Some(&mut signer)).unwrap();
945        assert!(sealed.starts_with("-----BEGIN PGP MESSAGE-----"));
946        let passphrase = |_: &Fingerprint| Some("secret words".to_string());
947        let sessions = SessionKeys::new();
948        let opened = decrypt(sealed.as_bytes(), &keys_of_bob, &Unlock::new(&passphrase, &sessions)).unwrap();
949        assert_eq!(opened.data, b"Content-Type: text/plain\r\n\r\nHello Bob.\r\n");
950        assert_eq!(opened.signatures.len(), 1);
951        assert!(opened.signatures[0].good && opened.signatures[0].signer.contains("alice@example.org"));
952        let wrong = |_: &Fingerprint| Some("other words".to_string());
953        assert!(decrypt(sealed.as_bytes(), &keys_of_bob, &Unlock::new(&wrong, &sessions)).is_err());
954        assert!(here_signer(&alice, "other words").is_err());
955        let (signature, micalg) = sign_detached(b"signed part", &mut signer).unwrap();
956        assert!(micalg.starts_with("pgp-sha"), "{micalg}");
957        let checked = verify_detached(b"signed part", signature.as_bytes(), &keys_of_bob);
958        assert!(checked[0].good);
959        let tampered = verify_detached(b"signed part!", signature.as_bytes(), &keys_of_bob);
960        assert!(!tampered[0].good);
961        let stranger = verify_detached(b"signed part", signature.as_bytes(), &Keys::default());
962        assert_eq!(stranger[0].problem, "unknown key");
963    }
964
965    #[test]
966    fn key_files_are_yours_alone() {
967        let path = temporary().join("key.asc");
968        write_key(&path, b"-----BEGIN PGP PRIVATE KEY BLOCK-----").unwrap();
969        write_key(&path, b"-----BEGIN PGP PRIVATE KEY BLOCK----- again").unwrap();
970        assert_eq!(std::fs::read(&path).unwrap(), b"-----BEGIN PGP PRIVATE KEY BLOCK----- again");
971        #[cfg(unix)]
972        {
973            use std::os::unix::fs::PermissionsExt;
974            assert_eq!(std::fs::metadata(&path).unwrap().permissions().mode() & 0o777, 0o600);
975        }
976        let _ = std::fs::remove_file(&path);
977    }
978
979    /// A key made here keeps its revocation certificate beside it, yours
980    /// alone; a copy can be saved; removed, the key takes it aside with it.
981    #[test]
982    fn a_key_made_here_keeps_its_revocation_certificate() {
983        let root = temporary().join("pgp-made");
984        let _ = std::fs::remove_dir_all(&root);
985        let made = generate_at(&root, "Jane Exemple", "jane@example.org", "secret words").unwrap();
986        assert!(made.own && made.revocation);
987        let kept = root.join("own").join(format!("{}.rev", made.fingerprint));
988        let text = std::fs::read_to_string(&kept).unwrap();
989        #[cfg(unix)]
990        {
991            use std::os::unix::fs::PermissionsExt;
992            assert_eq!(std::fs::metadata(&kept).unwrap().permissions().mode() & 0o777, 0o600, "protected as the key is");
993        }
994        assert!(text.contains(&made.fingerprint) && text.contains("Jane Exemple <jane@example.org>"), "{text}");
995        // Never imported by mistake: a colon before the armor; read as no key.
996        assert!(text.contains("\n:-----BEGIN PGP PUBLIC KEY BLOCK-----") && certificates_in(text.as_bytes()).is_empty());
997        // The colon taken away, it revokes this very key.
998        let armored = &text[text.find(":-----BEGIN").unwrap() + 1..];
999        let pile = openpgp::PacketPile::from_bytes(armored.as_bytes()).unwrap();
1000        let revocation = pile.descendants().find_map(|p| if let openpgp::Packet::Signature(s) = p { Some(s.clone()) } else { None }).unwrap();
1001        let key = Cert::from_bytes(&std::fs::read(root.join("own").join(format!("{}.asc", made.fingerprint))).unwrap()).unwrap();
1002        assert!(matches!(key.revocation_status(&policy(), None), openpgp::types::RevocationStatus::NotAsFarAsWeKnow));
1003        let (revoked, _) = key.insert_packets(revocation).unwrap();
1004        assert!(matches!(revoked.revocation_status(&policy(), None), openpgp::types::RevocationStatus::Revoked(_)));
1005        // A copy saved elsewhere, as kept, yours alone; a second one beside it.
1006        let elsewhere = root.join("saved");
1007        let copy = save_revocation_at(&root, &made.fingerprint, &elsewhere).unwrap();
1008        assert_eq!((copy.file_name().unwrap().to_string_lossy().to_string(), std::fs::read_to_string(&copy).unwrap()), (format!("{}.rev", made.fingerprint), text));
1009        assert!(save_revocation_at(&root, &made.fingerprint, &elsewhere).unwrap().ends_with(format!("{}-2.rev", made.fingerprint)));
1010        assert!(save_revocation_at(&root, "0000", &elsewhere).is_err(), "none kept for a key not made here");
1011        let _ = std::fs::remove_dir_all(&root);
1012    }
1013
1014    #[test]
1015    fn web_key_directory_names() {
1016        // The example of draft-koch-openpgp-webkey-service §3.1.
1017        let [advanced, direct] = wkd_urls("Joe.Doe@Example.ORG").unwrap();
1018        assert_eq!(direct, "https://example.org/.well-known/openpgpkey/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q?l=Joe.Doe");
1019        assert!(advanced.starts_with("https://openpgpkey.example.org/.well-known/openpgpkey/example.org/hu/iy9q119eutrkn8s1mk4r39qejnbu3n5q"));
1020        // Only a domain's own directory: no address, no local name, no port or path.
1021        for elsewhere in ["joe@192.168.1.1", "joe@[::1]", "joe@localhost", "joe@host.example:8080/x?", "joe@1.2.3"] {
1022            assert!(wkd_urls(elsewhere).is_none(), "{elsewhere}");
1023        }
1024    }
1025
1026    /// A key as a security key holds it: the primary signs, a Curve25519 subkey decrypts.
1027    fn card_key(address: &str, made: Option<std::time::SystemTime>) -> Cert {
1028        let mut builder = CertBuilder::new()
1029            .add_userid(format!("Card Holder <{address}>"))
1030            .set_cipher_suite(CipherSuite::Cv25519)
1031            .set_primary_key_flags(openpgp::types::KeyFlags::empty().set_certification().set_signing())
1032            .add_transport_encryption_subkey();
1033        if let Some(made) = made {
1034            builder = builder.set_creation_time(made).set_validity_period(std::time::Duration::from_secs(365 * 24 * 3600));
1035        }
1036        builder.generate().unwrap().0
1037    }
1038
1039    fn record(card: &securitykey::SoftCard, cert: &Cert, address: &str) -> KnownCard {
1040        KnownCard {
1041            ident: card.info.ident.clone(),
1042            sign: card.info.sign.as_ref().map(|s| s.fingerprint.clone()).unwrap_or_default(),
1043            decrypt: card.info.decrypt.as_ref().map(|s| s.fingerprint.clone()).unwrap_or_default(),
1044            cert: cert.fingerprint().to_hex(),
1045            addresses: vec![address.to_string()],
1046            ..KnownCard::default()
1047        }
1048    }
1049
1050    #[test]
1051    fn a_security_key_signs_alone_for_its_addresses() {
1052        let on_card = card_key("me@example.org", None);
1053        let card = securitykey::SoftCard::new("0006:12345678", &on_card, "123456");
1054        let here = key("me@example.org");
1055        let mut keys = Keys { own: vec![on_card.clone().strip_secret_key_material(), here.clone()], ..Keys::default() };
1056        assert_eq!(keys.own_for("me@example.org").unwrap().place, Place::Here);
1057        keys.cards.push(record(&card, &on_card, "me@example.org"));
1058        let own = keys.own_for("ME@example.org").unwrap();
1059        assert_eq!(own.place, Place::Card { ident: "0006:12345678".into() });
1060        assert_eq!(own.cert.fingerprint(), on_card.fingerprint());
1061        // Autocrypt announces the security key's certificate.
1062        assert!(autocrypt_header(own.cert, "me@example.org").is_some());
1063        assert!(keys.own_for("other@example.org").is_none());
1064        // Expired on the card: nothing signs for the address, and that is known.
1065        let late = card_key("late@example.org", Some(std::time::SystemTime::now() - std::time::Duration::from_secs(3 * 365 * 24 * 3600)));
1066        let late_card = securitykey::SoftCard::new("0006:00000002", &late, "123456");
1067        let keys = Keys { own: vec![late.clone().strip_secret_key_material(), key("late@example.org")], cards: vec![record(&late_card, &late, "late@example.org")], ..Keys::default() };
1068        assert!(keys.own_for("late@example.org").is_none());
1069        assert!(keys.card_for("late@example.org").is_some());
1070    }
1071
1072    #[test]
1073    fn a_message_to_the_security_key_waits_for_it_then_opens_from_memory() {
1074        let on_card = card_key("me@example.org", None);
1075        let card = securitykey::SoftCard::new("0006:12345678", &on_card, "123456").with_x25519(securitykey::x25519::x25519);
1076        let keys = Keys { own: vec![on_card.clone().strip_secret_key_material()], cards: vec![record(&card, &on_card, "me@example.org")], ..Keys::default() };
1077        let readers = securitykey::SoftReaders::with(vec![card]);
1078        let sealed = encrypt(b"Hello me.", &[&on_card], None).unwrap();
1079        let none = |_: &Fingerprint| None;
1080        let sessions = SessionKeys::new();
1081        // Shown: nothing is asked of the key, the message says which key it waits for.
1082        let unlock = Unlock::new(&none, &sessions);
1083        assert!(decrypt(sealed.as_bytes(), &keys, &unlock).is_err());
1084        assert_eq!(unlock.wanted().as_deref(), Some("0006:12345678"));
1085        assert_eq!(readers.card("0006:12345678", |c| c.operations), Some(0));
1086        // A wrong PIN: said, nothing opened.
1087        let wrong = Password::from("654321");
1088        let access = CardAccess::new(&readers, &wrong, &|| {});
1089        let unlock = Unlock::new(&none, &sessions).with_card(&access);
1090        assert!(decrypt(sealed.as_bytes(), &keys, &unlock).is_err());
1091        assert_eq!(access.failure(), Some(securitykey::CardError::WrongPin { left: 2 }));
1092        // "Open with your security key": opened, its session key kept.
1093        let pin = Password::from("123456");
1094        let access = CardAccess::new(&readers, &pin, &|| {});
1095        let opened = decrypt(sealed.as_bytes(), &keys, &Unlock::new(&none, &sessions).with_card(&access)).unwrap();
1096        assert_eq!(opened.data, b"Hello me.");
1097        assert_eq!(readers.card("0006:12345678", |c| c.operations), Some(1));
1098        // Again, its attachments, its answer: from memory, the key left alone.
1099        let again = decrypt(sealed.as_bytes(), &keys, &Unlock::new(&none, &sessions)).unwrap();
1100        assert_eq!(again.data, b"Hello me.");
1101        assert_eq!(readers.card("0006:12345678", |c| c.operations), Some(1));
1102        // Another message still waits for the key; forgotten, this one too.
1103        let other = encrypt(b"Another.", &[&on_card], None).unwrap();
1104        assert!(decrypt(other.as_bytes(), &keys, &Unlock::new(&none, &sessions)).is_err());
1105        sessions.forget();
1106        assert!(decrypt(sealed.as_bytes(), &keys, &Unlock::new(&none, &sessions)).is_err());
1107    }
1108
1109    #[test]
1110    fn an_expired_security_key_still_opens_older_mail() {
1111        let then = std::time::SystemTime::now() - std::time::Duration::from_secs(3 * 365 * 24 * 3600);
1112        let late = card_key("late@example.org", Some(then));
1113        // Sent while the key was valid.
1114        let policy = policy();
1115        let sent = then + std::time::Duration::from_secs(24 * 3600);
1116        let recipients: Vec<_> = late.keys().with_policy(&policy, sent).alive().for_transport_encryption().collect();
1117        assert!(!recipients.is_empty());
1118        let mut sealed = Vec::new();
1119        {
1120            let message = Message::new(&mut sealed);
1121            let message = Encryptor::for_recipients(message, recipients).build().unwrap();
1122            let mut literal = LiteralWriter::new(message).build().unwrap();
1123            literal.write_all(b"Older mail.").unwrap();
1124            literal.finalize().unwrap();
1125        }
1126        let card = securitykey::SoftCard::new("0006:00000002", &late, "123456").with_x25519(securitykey::x25519::x25519);
1127        let keys = Keys { own: vec![late.clone().strip_secret_key_material()], cards: vec![record(&card, &late, "late@example.org")], ..Keys::default() };
1128        let readers = securitykey::SoftReaders::with(vec![card]);
1129        let pin = Password::from("123456");
1130        let access = CardAccess::new(&readers, &pin, &|| {});
1131        let none = |_: &Fingerprint| None;
1132        let sessions = SessionKeys::new();
1133        let opened = decrypt(&sealed, &keys, &Unlock::new(&none, &sessions).with_card(&access)).unwrap();
1134        assert_eq!(opened.data, b"Older mail.");
1135        // But nobody can encrypt to it now.
1136        assert!(encrypt(b"New mail.", &[&late], None).is_err());
1137    }
1138
1139    #[test]
1140    fn a_security_key_certificate_comes_back_to_your_keys() {
1141        let root = temporary().join("pgp-card");
1142        let _ = std::fs::remove_dir_all(&root);
1143        let on_card = card_key("me@example.org", None);
1144        let name = format!("{}.asc", on_card.fingerprint().to_hex());
1145        let kept = store_in(&root, on_card.clone().strip_secret_key_material(), true).unwrap();
1146        assert!(kept.own);
1147        let file = std::fs::read_to_string(root.join("own").join(&name)).unwrap();
1148        assert!(file.starts_with("-----BEGIN PGP PUBLIC KEY BLOCK-----"), "public only: {file}");
1149        // Brought again, by an Autocrypt header or a lookup: with yours, never with others'.
1150        let header = autocrypt_header(&on_card, "me@example.org").unwrap();
1151        let again = autocrypt_key(&header, "me@example.org").unwrap();
1152        assert!(store_at(&root, again.strip_secret_key_material()).unwrap().own);
1153        assert!(!root.join("others").join(&name).exists());
1154        // Someone else's goes with others'.
1155        let jane = key("jane@example.org").strip_secret_key_material();
1156        assert!(!store_at(&root, jane).unwrap().own);
1157        let _ = std::fs::remove_dir_all(&root);
1158    }
1159
1160    #[test]
1161    fn autocrypt_carries_the_smallest_key() {
1162        let alice = key("alice@example.org");
1163        let header = autocrypt_header(&alice, "alice@example.org").unwrap();
1164        assert!(header.starts_with("addr=alice@example.org; keydata="));
1165        assert!(header.split_whitespace().all(|word| word.len() <= 100), "foldable: {header}");
1166        let found = autocrypt_key(&header, "alice@example.org").unwrap();
1167        assert_eq!(found.fingerprint(), alice.fingerprint());
1168        assert!(!found.is_tsk());
1169        assert!(autocrypt_key(&header, "mallory@example.org").is_none());
1170    }
1171}