1use cx448::x448;
2use hkdf::Hkdf;
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#[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 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 pub ephemeral_public_point: [u8; 56],
74
75 pub recipient_public: &'a x448::PublicKey,
77
78 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<Vec<u8>> {
86 debug!("X448 decrypt");
87
88 let shared_secret = {
89 let Some(their_public) = x448::PublicKey::from_bytes(&data.ephemeral_public_point)
91 else {
92 bail!("x448: invalid public key");
93 };
94
95 let our_secret = self.secret;
97
98 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 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
116pub 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<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
134pub fn hkdf(
137 ephemeral: &[u8; 56],
138 recipient_public: &[u8; 56],
139 shared_secret: &[u8; 56],
140) -> Result<[u8; 32]> {
141 const INFO: &[u8] = b"OpenPGP X448";
144
145 let mut input = vec![];
151 input.extend_from_slice(ephemeral);
152 input.extend_from_slice(recipient_public);
153 input.extend_from_slice(shared_secret);
154
155 let hk = Hkdf::<Sha512>::new(None, &input);
157 let mut okm = [0u8; 32];
158 hk.expand(INFO, &mut okm)
159 .expect("32 is a valid length for Sha512 to output");
160
161 Ok(okm)
162}
163
164pub fn encrypt<R: CryptoRng + Rng>(
168 mut rng: R,
169 recipient_public: &X448PublicParams,
170 plain: &[u8],
171) -> Result<([u8; 56], Vec<u8>)> {
172 debug!("X448 encrypt");
173
174 const MAX_SIZE: usize = 255;
176 ensure!(
177 plain.len() <= MAX_SIZE,
178 "unable to encrypt larger than {} bytes",
179 MAX_SIZE
180 );
181
182 let (ephemeral_public, shared_secret) = {
183 let their_public = &recipient_public.key;
185
186 let mut ephemeral_secret_key_bytes = Zeroizing::new([0u8; 56]);
187 rng.fill_bytes(&mut *ephemeral_secret_key_bytes);
188 let our_secret = x448::Secret::from(*ephemeral_secret_key_bytes);
189
190 let Some(shared_secret) = our_secret.as_diffie_hellman(their_public) else {
192 bail!("x448 Secret::as_diffie_hellman returned None");
193 };
194
195 let ephemeral_public = x448::PublicKey::from(&our_secret);
197
198 (ephemeral_public, shared_secret)
199 };
200
201 let okm = hkdf(
203 ephemeral_public.as_bytes(),
204 recipient_public.key.as_bytes(),
205 shared_secret.as_bytes(),
206 )?;
207
208 let wrapped = aes_kw::wrap(&okm, plain)?;
210
211 Ok((*ephemeral_public.as_bytes(), wrapped))
212}
213
214#[cfg(test)]
215mod tests {
216 #![allow(clippy::unwrap_used)]
217
218 use std::ops::Deref;
219
220 use proptest::prelude::*;
221 use rand::{RngCore, SeedableRng};
222 use rand_chacha::{ChaCha8Rng, ChaChaRng};
223
224 use super::*;
225
226 #[test]
227 fn test_encrypt_decrypt() {
228 let mut rng = ChaChaRng::from_seed([0u8; 32]);
229
230 let skey = SecretKey::generate(&mut rng);
231 let pub_params: X448PublicParams = (&skey).into();
232
233 for text_size in (8..=248).step_by(8) {
234 for _i in 0..10 {
235 let mut fingerprint = vec![0u8; 20];
236 rng.fill_bytes(&mut fingerprint);
237
238 let mut plain = vec![0u8; text_size];
239 rng.fill_bytes(&mut plain);
240
241 let (ephemeral, enc_sk) = encrypt(&mut rng, &pub_params, &plain[..]).unwrap();
242
243 let data = EncryptionFields {
244 ephemeral_public_point: ephemeral,
245 recipient_public: &pub_params.key,
246 encrypted_session_key: enc_sk.deref(),
247 };
248
249 let decrypted = skey.decrypt(data).unwrap();
250
251 assert_eq!(&plain[..], &decrypted[..]);
252 }
253 }
254 }
255
256 impl Arbitrary for SecretKey {
257 type Parameters = ();
258 type Strategy = BoxedStrategy<Self>;
259
260 fn arbitrary_with(_args: Self::Parameters) -> Self::Strategy {
261 any::<u64>()
262 .prop_map(|seed| {
263 let mut rng = ChaCha8Rng::seed_from_u64(seed);
264 SecretKey::generate(&mut rng)
265 })
266 .boxed()
267 }
268 }
269}