p2panda_encryption/two_party/
x3dh.rs1use serde::{Deserialize, Serialize};
14use thiserror::Error;
15
16use crate::crypto::aead::{AeadError, AeadNonce, aead_decrypt, aead_encrypt};
17use crate::crypto::hkdf::{HkdfError, hkdf};
18use crate::crypto::x25519::{PublicKey, SecretKey, X25519Error};
19use crate::crypto::{Rng, RngError};
20use crate::key_bundle::{KeyBundleError, OneTimePreKeyId, PreKeyId};
21use crate::traits::KeyBundle;
22
23const KDF_INFO: &[u8; 7] = b"p2panda";
25
26#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
29pub struct X3dhCiphertext {
30 pub identity_key: PublicKey,
32
33 pub onetime_prekey_id: Option<OneTimePreKeyId>,
36
37 pub prekey_id: PreKeyId,
39
40 pub ciphertext: Vec<u8>,
42
43 pub ephemeral_key: PublicKey,
45}
46
47pub fn x3dh_encrypt<KB: KeyBundle>(
49 plaintext: &[u8],
50 our_identity_secret: &SecretKey,
51 their_prekey_bundle: &KB,
52 rng: &Rng,
53) -> Result<X3dhCiphertext, X3dhError> {
54 their_prekey_bundle.verify()?;
56
57 let our_identity_key = our_identity_secret.public_key()?;
58
59 let our_ephemeral_secret = SecretKey::from_bytes(rng.random_array()?);
60 let our_ephemeral_key = our_ephemeral_secret.public_key()?;
61
62 let mut ikm = Vec::with_capacity({
63 if their_prekey_bundle.onetime_prekey().is_none() {
64 32 * 4
65 } else {
66 32 * 5
67 }
68 });
69
70 ikm.extend_from_slice(&[0xFFu8; 32]); ikm.extend_from_slice(
74 &our_identity_secret.calculate_agreement(their_prekey_bundle.signed_prekey())?,
75 );
76
77 ikm.extend_from_slice(
79 &our_ephemeral_secret.calculate_agreement(their_prekey_bundle.identity_key())?,
80 );
81
82 ikm.extend_from_slice(
84 &our_ephemeral_secret.calculate_agreement(their_prekey_bundle.signed_prekey())?,
85 );
86
87 if let Some(onetime_prekey) = their_prekey_bundle.onetime_prekey() {
89 ikm.extend_from_slice(&our_ephemeral_secret.calculate_agreement(onetime_prekey)?);
90 }
91
92 let sk: [u8; 32] = {
94 let salt = vec![0_u8; 32];
95 hkdf(&salt, &ikm, Some(KDF_INFO))?
96 };
97
98 drop(our_ephemeral_secret);
99 drop(ikm);
100
101 let ad = {
103 let mut buf = Vec::new();
104 buf.extend_from_slice(our_identity_key.as_bytes());
105 buf.extend_from_slice(their_prekey_bundle.identity_key().as_bytes());
106 buf
107 };
108
109 let nonce: AeadNonce = hkdf(b"", &sk, None)?;
110 let ciphertext = aead_encrypt(&sk, plaintext, nonce, Some(&ad))?;
111
112 Ok(X3dhCiphertext {
113 ciphertext,
114 ephemeral_key: our_ephemeral_key,
115 identity_key: our_identity_key,
116 prekey_id: *their_prekey_bundle.signed_prekey(),
117 onetime_prekey_id: their_prekey_bundle.onetime_prekey_id(),
118 })
119}
120
121pub fn x3dh_decrypt(
127 their_ciphertext: &X3dhCiphertext,
128 our_identity_secret: &SecretKey,
129 our_prekey_secret: &SecretKey,
130 our_onetime_secret: Option<&SecretKey>,
131) -> Result<Vec<u8>, X3dhError> {
132 let our_identity_key = our_identity_secret.public_key()?;
133
134 let mut ikm = Vec::with_capacity(if our_onetime_secret.is_none() {
135 32 * 4
136 } else {
137 32 * 5
138 });
139
140 ikm.extend_from_slice(&[0xFFu8; 32]); ikm.extend_from_slice(&our_prekey_secret.calculate_agreement(&their_ciphertext.identity_key)?);
144
145 ikm.extend_from_slice(
147 &our_identity_secret.calculate_agreement(&their_ciphertext.ephemeral_key)?,
148 );
149
150 ikm.extend_from_slice(&our_prekey_secret.calculate_agreement(&their_ciphertext.ephemeral_key)?);
152
153 if let Some(our_onetime_secret) = our_onetime_secret {
155 ikm.extend_from_slice(
156 &our_onetime_secret.calculate_agreement(&their_ciphertext.ephemeral_key)?,
157 );
158 }
159
160 let sk: [u8; 32] = {
162 let salt = vec![0_u8; 32];
163 hkdf(&salt, &ikm, Some(KDF_INFO))?
164 };
165
166 drop(ikm);
167
168 let ad = {
170 let mut buf = Vec::new();
171 buf.extend_from_slice(their_ciphertext.identity_key.as_bytes());
172 buf.extend_from_slice(our_identity_key.as_bytes());
173 buf
174 };
175
176 let nonce: AeadNonce = hkdf(b"", &sk, None)?;
177 let plaintext = aead_decrypt(&sk, &their_ciphertext.ciphertext, nonce, Some(&ad))?;
178
179 Ok(plaintext)
180}
181
182#[derive(Debug, Error)]
183pub enum X3dhError {
184 #[error(transparent)]
185 Rng(#[from] RngError),
186
187 #[error(transparent)]
188 Aead(#[from] AeadError),
189
190 #[error(transparent)]
191 Hkdf(#[from] HkdfError),
192
193 #[error(transparent)]
194 X25519(#[from] X25519Error),
195
196 #[error(transparent)]
197 KeyBundle(#[from] KeyBundleError),
198}
199
200#[cfg(test)]
201mod tests {
202 use crate::crypto::Rng;
203 use crate::crypto::x25519::SecretKey;
204 use crate::key_bundle::{Lifetime, LongTermKeyBundle, OneTimeKeyBundle, OneTimePreKey, PreKey};
205
206 use super::{x3dh_decrypt, x3dh_encrypt};
207
208 #[test]
209 fn encrypt_decrypt() {
210 let rng = Rng::from_seed([1; 32]);
211
212 let bob_identity_secret = SecretKey::from_bytes(rng.random_array().unwrap());
213
214 let bob_prekey_secret = SecretKey::from_bytes(rng.random_array().unwrap());
215 let bob_signed_prekey =
216 PreKey::new(bob_prekey_secret.public_key().unwrap(), Lifetime::default());
217
218 let bob_onetime_secret = SecretKey::from_bytes(rng.random_array().unwrap());
219 let bob_onetime_prekey = OneTimePreKey::new(bob_onetime_secret.public_key().unwrap(), 2);
220
221 let bob_prekey_signature = bob_signed_prekey.sign(&bob_identity_secret, &rng).unwrap();
222
223 let bob_prekey_bundle = OneTimeKeyBundle::new(
224 bob_identity_secret.public_key().unwrap(),
225 bob_signed_prekey,
226 bob_prekey_signature,
227 Some(bob_onetime_prekey),
228 );
229
230 let alice_identity_secret = SecretKey::from_bytes(rng.random_array().unwrap());
231
232 let ciphertext = x3dh_encrypt(
233 b"Hello, Panda!",
234 &alice_identity_secret,
235 &bob_prekey_bundle,
236 &rng,
237 )
238 .unwrap();
239
240 let plaintext = x3dh_decrypt(
241 &ciphertext,
242 &bob_identity_secret,
243 &bob_prekey_secret,
244 Some(&bob_onetime_secret),
245 )
246 .unwrap();
247
248 assert_eq!(b"Hello, Panda!", plaintext.as_slice());
249 }
250
251 #[test]
252 fn longterm_key_bundle() {
253 let rng = Rng::from_seed([1; 32]);
254
255 let bob_identity_secret = SecretKey::from_bytes(rng.random_array().unwrap());
256
257 let bob_prekey_secret = SecretKey::from_bytes(rng.random_array().unwrap());
258 let bob_signed_prekey =
259 PreKey::new(bob_prekey_secret.public_key().unwrap(), Lifetime::default());
260
261 let bob_prekey_signature = bob_signed_prekey.sign(&bob_identity_secret, &rng).unwrap();
262
263 let bob_prekey_bundle = LongTermKeyBundle::new(
264 bob_identity_secret.public_key().unwrap(),
265 bob_signed_prekey,
266 bob_prekey_signature,
267 );
268
269 let alice_identity_secret = SecretKey::from_bytes(rng.random_array().unwrap());
270
271 let ciphertext = x3dh_encrypt(
272 b"Hello, Panda!",
273 &alice_identity_secret,
274 &bob_prekey_bundle,
275 &rng,
276 )
277 .unwrap();
278
279 let plaintext =
280 x3dh_decrypt(&ciphertext, &bob_identity_secret, &bob_prekey_secret, None).unwrap();
281
282 assert_eq!(b"Hello, Panda!", plaintext.as_slice());
283 }
284}