Skip to main content

pgp/crypto/
x448.rs

1use cx448::x448;
2use hkdf::HkdfExtract;
3use log::debug;
4use rand::{CryptoRng, Rng};
5use sha2::Sha512;
6use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
7
8use crate::{
9    crypto::{aes_kw, Decryptor},
10    errors::{bail, ensure, Result},
11    ser::Serialize,
12    types::X448PublicParams,
13};
14
15pub const KEY_LEN: usize = 56;
16
17/// Secret key for X448
18#[derive(Clone, derive_more::Debug, Zeroize, ZeroizeOnDrop)]
19pub struct SecretKey {
20    #[debug("..")]
21    secret: x448::Secret,
22}
23
24impl PartialEq for SecretKey {
25    fn eq(&self, other: &Self) -> bool {
26        self.secret.as_bytes().eq(other.secret.as_bytes())
27    }
28}
29
30impl Eq for SecretKey {}
31
32impl From<&SecretKey> for X448PublicParams {
33    fn from(value: &SecretKey) -> Self {
34        let secret = value.secret;
35        let public = x448::PublicKey::from(&secret);
36        X448PublicParams { key: public }
37    }
38}
39
40impl SecretKey {
41    /// Generate an X448 `SecretKey`.
42    pub fn generate<R: Rng + CryptoRng>(mut rng: R) -> Self {
43        let secret = x448::Secret::new(&mut rng);
44
45        SecretKey { secret }
46    }
47
48    pub fn try_from_bytes(secret: [u8; KEY_LEN]) -> Result<Self> {
49        let secret = x448::Secret::from(secret);
50
51        Ok(Self { secret })
52    }
53
54    pub fn as_bytes(&self) -> &[u8; KEY_LEN] {
55        self.secret.as_bytes()
56    }
57}
58
59impl Serialize for SecretKey {
60    fn to_writer<W: std::io::Write>(&self, writer: &mut W) -> Result<()> {
61        let x = self.as_bytes();
62        writer.write_all(x)?;
63        Ok(())
64    }
65
66    fn write_len(&self) -> usize {
67        KEY_LEN
68    }
69}
70
71pub struct EncryptionFields<'a> {
72    /// Ephemeral X448 public key (56 bytes)
73    pub ephemeral_public_point: [u8; 56],
74
75    /// Recipient public key (56 bytes)
76    pub recipient_public: &'a x448::PublicKey,
77
78    /// Encrypted and wrapped session key
79    pub encrypted_session_key: &'a [u8],
80}
81
82impl Decryptor for SecretKey {
83    type EncryptionFields<'a> = EncryptionFields<'a>;
84
85    fn decrypt(&self, data: Self::EncryptionFields<'_>) -> Result<Zeroizing<Vec<u8>>> {
86        debug!("X448 decrypt");
87
88        let shared_secret = {
89            // create montgomery point
90            let Some(their_public) = x448::PublicKey::from_bytes(&data.ephemeral_public_point)
91            else {
92                bail!("x448: invalid public key");
93            };
94
95            // private key of the recipient.
96            let our_secret = self.secret;
97
98            // derive shared secret (None for low order points)
99            let Some(shared_secret) = our_secret.as_diffie_hellman(&their_public) else {
100                bail!("x448 Secret::as_diffie_hellman returned None");
101            };
102
103            *shared_secret.as_bytes()
104        };
105
106        // obtain the session key from the shared secret
107        derive_session_key(
108            data.ephemeral_public_point,
109            data.recipient_public.as_bytes(),
110            shared_secret,
111            data.encrypted_session_key,
112        )
113    }
114}
115
116/// Obtain the decrypted OpenPGP session key
117///
118/// This helper function performs the steps described in
119/// <https://www.rfc-editor.org/rfc/rfc9580.html#name-algorithm-specific-fields-for-x>
120pub fn derive_session_key(
121    ephemeral: [u8; 56],
122    recipient_public: &[u8; 56],
123    shared_secret: [u8; 56],
124    encrypted_session_key: &[u8],
125) -> Result<Zeroizing<Vec<u8>>> {
126    let okm = hkdf(&ephemeral, recipient_public, &shared_secret)?;
127
128    let decrypted_key = aes_kw::unwrap(&*okm, encrypted_session_key)?;
129    ensure!(!decrypted_key.is_empty(), "empty key is not valid");
130
131    Ok(decrypted_key)
132}
133
134/// HKDF for X448
135/// <https://www.rfc-editor.org/rfc/rfc9580.html#name-algorithm-specific-fields-for-x>
136pub fn hkdf(
137    ephemeral: &[u8; 56],
138    recipient_public: &[u8; 56],
139    shared_secret: &[u8; 56],
140) -> Result<Zeroizing<[u8; 32]>> {
141    // TODO: maybe share/DRY this code with the analogous x25519 implementation?
142
143    const INFO: &[u8] = b"OpenPGP X448";
144
145    // The input of HKDF is the concatenation of the following three values:
146    // 56 octets of the ephemeral X448 public key from this packet.
147    // 56 octets of the recipient public key material.
148    // 56 octets of the shared secret.
149
150    let mut hkdf_extract = HkdfExtract::<Sha512>::new(None);
151    hkdf_extract.input_ikm(ephemeral);
152    hkdf_extract.input_ikm(recipient_public);
153    hkdf_extract.input_ikm(shared_secret);
154
155    let (_, hkdf) = hkdf_extract.finalize();
156
157    // HKDF with SHA512, an info parameter of "OpenPGP X448" and no salt.
158    let mut okm = Zeroizing::new([0u8; 32]);
159    hkdf.expand(INFO, &mut (*okm))
160        .expect("32 is a valid length for Sha512 to output");
161
162    Ok(okm)
163}
164
165/// X448 encryption.
166///
167/// Returns (ephemeral, encrypted session key)
168pub fn encrypt<R: CryptoRng + Rng>(
169    mut rng: R,
170    recipient_public: &X448PublicParams,
171    plain: &[u8],
172) -> Result<([u8; 56], Vec<u8>)> {
173    debug!("X448 encrypt");
174
175    // Maximum length for `plain` - FIXME: what should the maximum be, here?
176    const MAX_SIZE: usize = 255;
177    ensure!(
178        plain.len() <= MAX_SIZE,
179        "unable to encrypt larger than {} bytes",
180        MAX_SIZE
181    );
182
183    let (ephemeral_public, shared_secret) = {
184        // create montgomery point
185        let their_public = &recipient_public.key;
186
187        let mut ephemeral_secret_key_bytes = Zeroizing::new([0u8; 56]);
188        rng.fill_bytes(&mut *ephemeral_secret_key_bytes);
189        let our_secret = x448::Secret::from(*ephemeral_secret_key_bytes);
190
191        // derive shared secret (None for low order points)
192        let Some(shared_secret) = our_secret.as_diffie_hellman(their_public) else {
193            bail!("x448 Secret::as_diffie_hellman returned None");
194        };
195
196        // Encode public point
197        let ephemeral_public = x448::PublicKey::from(&our_secret);
198
199        (ephemeral_public, shared_secret)
200    };
201
202    // hkdf key derivation
203    let okm = hkdf(
204        ephemeral_public.as_bytes(),
205        recipient_public.key.as_bytes(),
206        shared_secret.as_bytes(),
207    )?;
208
209    // Perform AES Key Wrap
210    let wrapped = aes_kw::wrap(&*okm, plain)?;
211
212    Ok((*ephemeral_public.as_bytes(), wrapped))
213}
214
215#[cfg(test)]
216mod tests {
217    #![allow(clippy::unwrap_used)]
218
219    use std::ops::Deref;
220
221    use proptest::prelude::*;
222    use rand::{RngCore, SeedableRng};
223    use rand_chacha::{ChaCha8Rng, ChaChaRng};
224
225    use super::*;
226
227    #[test]
228    fn test_encrypt_decrypt() {
229        let mut rng = ChaChaRng::from_seed([0u8; 32]);
230
231        let skey = SecretKey::generate(&mut rng);
232        let pub_params: X448PublicParams = (&skey).into();
233
234        for text_size in (8..=248).step_by(8) {
235            for _i in 0..10 {
236                let mut fingerprint = vec![0u8; 20];
237                rng.fill_bytes(&mut fingerprint);
238
239                let mut plain = vec![0u8; text_size];
240                rng.fill_bytes(&mut plain);
241
242                let (ephemeral, enc_sk) = encrypt(&mut rng, &pub_params, &plain[..]).unwrap();
243
244                let data = EncryptionFields {
245                    ephemeral_public_point: ephemeral,
246                    recipient_public: &pub_params.key,
247                    encrypted_session_key: enc_sk.deref(),
248                };
249
250                let decrypted = skey.decrypt(data).unwrap();
251
252                assert_eq!(&plain[..], &decrypted[..]);
253            }
254        }
255    }
256
257    impl Arbitrary for SecretKey {
258        type Parameters = ();
259        type Strategy = BoxedStrategy<Self>;
260
261        fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
262            any::<u64>()
263                .prop_map(|seed| {
264                    let mut rng = ChaCha8Rng::seed_from_u64(seed);
265                    SecretKey::generate(&mut rng)
266                })
267                .boxed()
268        }
269    }
270}