Skip to main content

kernoid_e2e/
keys.rs

1// SPDX-License-Identifier: MIT
2//! An account's key (RFC 0006, 4): made by the client, of one of two kinds; its secret key leaves the client only as
3//! a protected copy – the OpenPGP secret key protected with Argon2 and AEAD (RFC 9580, 3.7) under a passphrase: a
4//! passkey's PRF output, a recovery code, a device's own secret.
5
6use pgp::{
7    composed::{
8        ArmorOptions, Deserializable, EncryptionCaps, KeyType, SecretKeyParamsBuilder, SignedPublicKey,
9        SignedPublicSubKey, SignedSecretKey, SubkeyParamsBuilder,
10    },
11    crypto::{aead::AeadAlgorithm, ecc_curve::ECCCurve, sym::SymmetricKeyAlgorithm},
12    ser::Serialize,
13    types::{KeyDetails, KeyVersion, Password, PublicParams, S2kParams, StringToKey},
14};
15use rand::{Rng, rngs::OsRng};
16
17use crate::{Error, Result};
18
19/// The kind of an account's key (RFC 0006, 4.1).
20#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Kind {
23    /// v6, ML-DSA-65+Ed25519 with an ML-KEM-768+X25519 subkey (RFC 9980): secure against recorded mail being
24    /// decrypted by a quantum computer later.
25    Pqc,
26    /// v4, Ed25519 with an X25519 subkey (Curve25519 as GnuPG knows it): for correspondents whose programs know only v4.
27    Compatible,
28}
29
30impl Kind {
31    pub fn of(key: &SignedPublicKey) -> Option<Kind> {
32        match key.version() {
33            KeyVersion::V6 => Some(Kind::Pqc),
34            KeyVersion::V4 => Some(Kind::Compatible),
35            _ => None,
36        }
37    }
38}
39
40/// An account's secret key, unlocked.
41#[derive(Debug, Clone)]
42pub struct SecretKey(pub(crate) SignedSecretKey);
43
44/// A public key – one's own or someone else's.
45#[derive(Debug, Clone)]
46pub struct PublicKey(pub(crate) SignedPublicKey);
47
48impl SecretKey {
49    /// Makes a key for the given addresses (the first becomes the primary user id).
50    pub fn generate(kind: Kind, addresses: &[&str]) -> Result<SecretKey> {
51        let (first, others) = addresses.split_first().ok_or_else(|| Error::Key("no address".to_owned()))?;
52        let (version, primary, encryption) = match kind {
53            Kind::Pqc => (KeyVersion::V6, KeyType::MlDsa65Ed25519, KeyType::MlKem768X25519),
54            Kind::Compatible => (KeyVersion::V4, KeyType::Ed25519Legacy, KeyType::ECDH(ECCCurve::Curve25519Legacy)),
55        };
56        let subkey = SubkeyParamsBuilder::default()
57            .version(version)
58            .key_type(encryption)
59            .can_sign(false)
60            .can_encrypt(EncryptionCaps::All)
61            .can_authenticate(false)
62            .build()
63            .map_err(|e| Error::Key(e.to_string()))?;
64        let mut params = SecretKeyParamsBuilder::default();
65        params
66            .version(version)
67            .key_type(primary)
68            .can_certify(true)
69            .can_sign(true)
70            .can_encrypt(EncryptionCaps::None)
71            .primary_user_id(format!("<{first}>"))
72            .user_ids(others.iter().map(|a| format!("<{a}>")).collect())
73            .feature_seipd_v2(kind == Kind::Pqc)
74            .preferred_symmetric_algorithms(
75                [SymmetricKeyAlgorithm::AES256, SymmetricKeyAlgorithm::AES128].into_iter().collect(),
76            )
77            .subkeys(vec![subkey]);
78        if kind == Kind::Pqc {
79            params
80                .preferred_aead_algorithms([(SymmetricKeyAlgorithm::AES256, AeadAlgorithm::Ocb)].into_iter().collect());
81        }
82        let params = params.build().map_err(|e| Error::Key(e.to_string()))?;
83        Ok(SecretKey(params.generate(OsRng)?))
84    }
85
86    pub fn public(&self) -> PublicKey {
87        PublicKey(SignedPublicKey::from(self.0.clone()))
88    }
89
90    pub fn fingerprint(&self) -> String {
91        hex(self.0.fingerprint().as_bytes())
92    }
93
94    /// A protected copy of the secret key, binary: every secret part protected with Argon2 (RFC 9106, choice 2) and
95    /// AES-256-OCB under `passphrase`.
96    pub fn protect(&self, passphrase: &str) -> Result<Vec<u8>> {
97        let mut key = self.0.clone();
98        let password = Password::from(passphrase);
99        key.primary_key.set_password_with_s2k(&password, s2k())?;
100        for subkey in &mut key.secret_subkeys {
101            subkey.key.set_password_with_s2k(&password, s2k())?;
102        }
103        Ok(key.to_bytes()?)
104    }
105
106    /// Unlocks a protected copy. A wrong passphrase and a damaged copy are the same `Unlock`.
107    pub fn unlock(protected: &[u8], passphrase: &str) -> Result<SecretKey> {
108        let mut key = SignedSecretKey::from_bytes(protected).map_err(|e| Error::Key(e.to_string()))?;
109        key.verify_bindings().map_err(|e| Error::Key(e.to_string()))?;
110        let password = Password::from(passphrase);
111        key.primary_key.remove_password(&password).map_err(|_| Error::Unlock)?;
112        for subkey in &mut key.secret_subkeys {
113            subkey.key.remove_password(&password).map_err(|_| Error::Unlock)?;
114        }
115        Ok(SecretKey(key))
116    }
117
118    /// The secret key unprotected, armored – only for the user to keep or move to another program.
119    pub fn export(&self) -> Result<String> {
120        Ok(self.0.to_armored_string(ArmorOptions::default())?)
121    }
122
123    pub fn import(armored_or_binary: &[u8]) -> Result<SecretKey> {
124        let (key, _) = SignedSecretKey::from_reader_single(armored_or_binary).map_err(|e| Error::Key(e.to_string()))?;
125        key.verify_bindings().map_err(|e| Error::Key(e.to_string()))?;
126        if key.primary_key.secret_params().is_encrypted() {
127            return Err(Error::Key("the key is protected; remove its passphrase first".to_owned()));
128        }
129        let algorithms = std::iter::once(key.primary_key.public_params())
130            .chain(key.secret_subkeys.iter().map(|s| s.key.public_params()));
131        if !algorithms.into_iter().all(supported) {
132            return Err(Error::Key("only Curve25519 and post-quantum keys can be used here".to_owned()));
133        }
134        Ok(SecretKey(key))
135    }
136}
137
138impl PublicKey {
139    /// Reads a public key, armored or binary, and checks its self-signatures.
140    pub fn parse(armored_or_binary: &[u8]) -> Result<PublicKey> {
141        let (key, _) = SignedPublicKey::from_reader_single(armored_or_binary).map_err(|e| Error::Key(e.to_string()))?;
142        key.verify_bindings().map_err(|e| Error::Key(e.to_string()))?;
143        Ok(PublicKey(key))
144    }
145
146    /// Reads concatenated binary keys (a Web Key Directory's answer); keys that do not parse or verify are left out.
147    pub fn parse_many(binary: &[u8]) -> Vec<PublicKey> {
148        match SignedPublicKey::from_bytes_many(binary) {
149            Ok(keys) => {
150                keys.filter_map(|key| key.ok()).filter(|key| key.verify_bindings().is_ok()).map(PublicKey).collect()
151            }
152            Err(_) => Vec::new(),
153        }
154    }
155
156    pub fn fingerprint(&self) -> String {
157        hex(self.0.fingerprint().as_bytes())
158    }
159
160    pub fn kind(&self) -> Option<Kind> {
161        Kind::of(&self.0)
162    }
163
164    pub fn to_bytes(&self) -> Result<Vec<u8>> {
165        Ok(self.0.to_bytes()?)
166    }
167
168    pub fn armored(&self) -> Result<String> {
169        Ok(self.0.to_armored_string(ArmorOptions::default())?)
170    }
171
172    /// The addresses of the key's user ids (the part in angle brackets, or the whole id).
173    pub fn addresses(&self) -> Vec<String> {
174        self.0
175            .details
176            .users
177            .iter()
178            .filter_map(|user| user.id.as_str())
179            .map(|id| match (id.rfind('<'), id.rfind('>')) {
180                (Some(start), Some(end)) if start < end => id[start + 1..end].to_owned(),
181                _ => id.to_owned(),
182            })
183            .collect()
184    }
185
186    /// Whether something can be encrypted to the key.
187    pub fn can_encrypt(&self) -> bool {
188        !self.encryption_subkeys().is_empty()
189    }
190
191    /// The subkeys that may encrypt: an algorithm that can, and a binding that says so.
192    pub(crate) fn encryption_subkeys(&self) -> Vec<&SignedPublicSubKey> {
193        self.0
194            .public_subkeys
195            .iter()
196            .filter(|subkey| subkey.key.algorithm().can_encrypt())
197            .filter(|subkey| {
198                subkey.signatures.iter().any(|s| s.key_flags().encrypt_comms() || s.key_flags().encrypt_storage())
199            })
200            .collect()
201    }
202}
203
204/// A recovery code (RFC 0006, 4.2): 32 characters of Crockford's Base32 – 160 bits – in groups of four.
205pub fn recovery_code() -> String {
206    const ALPHABET: &[u8] = b"0123456789ABCDEFGHJKMNPQRSTVWXYZ";
207    let mut rng = OsRng;
208    (0..32)
209        .map(|i| {
210            let c = ALPHABET[rng.gen_range(0..ALPHABET.len())] as char;
211            if i > 0 && i % 4 == 0 { format!("-{c}") } else { c.to_string() }
212        })
213        .collect()
214}
215
216/// A recovery code as the user may type it: upper case, without separators and spaces, with Crockford's look-alikes.
217pub fn normalize_recovery_code(typed: &str) -> String {
218    let code: String = typed
219        .chars()
220        .filter(|c| c.is_ascii_alphanumeric())
221        .map(|c| match c.to_ascii_uppercase() {
222            'O' => '0',
223            'I' | 'L' => '1',
224            c => c,
225        })
226        .collect();
227    code.as_bytes().chunks(4).map(|c| String::from_utf8_lossy(c).into_owned()).collect::<Vec<_>>().join("-")
228}
229
230/// The algorithms of the two kinds (RFC 0006, 4.1). Secret keys of others – RSA, NIST curves, X448 – are not taken:
231/// their arithmetic (crypto-bigint 0.5 for the curves) is not constant-time (RUSTSEC-2026-0387, `deny.toml`).
232fn supported(params: &PublicParams) -> bool {
233    match params {
234        PublicParams::EdDSALegacy(_) | PublicParams::Ed25519(_) | PublicParams::X25519(_) => true,
235        PublicParams::MlDsa65Ed25519(_) | PublicParams::MlKem768X25519(_) => true,
236        PublicParams::ECDH(ecdh) => ecdh.curve() == ECCCurve::Curve25519Legacy,
237        _ => false,
238    }
239}
240
241fn s2k() -> S2kParams {
242    let mut rng = OsRng;
243    let aead_mode = AeadAlgorithm::Ocb;
244    let mut nonce = vec![0u8; aead_mode.nonce_size()];
245    rng.fill(&mut nonce[..]);
246    let mut salt = [0u8; 16];
247    rng.fill(&mut salt[..]);
248    S2kParams::Aead {
249        sym_alg: SymmetricKeyAlgorithm::AES256,
250        aead_mode,
251        s2k: StringToKey::Argon2 { salt, t: 3, p: 4, m_enc: 16 },
252        nonce: nonce.into(),
253    }
254}
255
256pub(crate) fn hex(bytes: &[u8]) -> String {
257    bytes.iter().map(|b| format!("{b:02x}")).collect()
258}