1use openmls::ciphersuite::hpke::Error as OpenmlsHpkeError;
2use openmls::prelude::tls_codec::Error as TlsCodecError;
3use openmls_traits::crypto::OpenMlsCrypto;
4use openmls_traits::types::HpkeCiphertext;
5use thiserror::Error;
6use tls_codec::{Deserialize, Serialize};
7use xmtp_common::RetryableError;
8use xmtp_id::key_package::WrapperAlgorithm;
9
10static LIBCRUX_CRYPTO_PROVIDER: std::sync::LazyLock<openmls_libcrux_crypto::CryptoProvider> =
11 std::sync::LazyLock::new(|| {
12 openmls_libcrux_crypto::CryptoProvider::new().expect("Failed to create CryptoProvider")
13 });
14
15#[derive(Debug, Error)]
16pub enum WrapPayloadError {
17 #[error("OpenMLS HPKE error: {0}")]
18 Hpke(#[from] OpenmlsHpkeError),
19 #[error("TLS Codec error: {0}")]
20 TlsError(#[from] TlsCodecError),
21 #[error(transparent)]
22 Crypto(#[from] openmls_traits::types::CryptoError),
23}
24
25#[derive(Debug, Error)]
26pub enum UnwrapPayloadError {
27 #[error("OpenMLS HPKE error: {0}")]
28 Hpke(#[from] OpenmlsHpkeError),
29 #[error("TLS Codec error: {0}")]
30 TlsError(#[from] TlsCodecError),
31 #[error(transparent)]
32 Crypto(#[from] openmls_traits::types::CryptoError),
33}
34
35impl RetryableError for WrapPayloadError {
36 fn is_retryable(&self) -> bool {
37 false
38 }
39}
40
41impl RetryableError for UnwrapPayloadError {
42 fn is_retryable(&self) -> bool {
43 false
44 }
45}
46
47pub fn wrap_payload_hpke(
58 payload: &[u8],
59 secondary_payload: &[u8],
60 hpke_public_key: &[u8],
61 wrapper_algorithm: WrapperAlgorithm,
62 label: &str,
63) -> Result<(Vec<u8>, Vec<u8>), WrapPayloadError> {
64 let context = openmls::prelude::hpke::EncryptContext::from((label, [].as_slice()));
69 let info = context.tls_serialize_detached()?;
70 let aad = &[];
71
72 let map_hpke_error = |e| match e {
73 hpke_rs::HpkeError::InvalidConfig => openmls::prelude::CryptoError::SenderSetupError,
74 _ => openmls::prelude::CryptoError::HpkeEncryptionError,
75 };
76
77 let pk_r = hpke_rs::HpkePublicKey::new(hpke_public_key.to_vec());
78 let mut config = wrapper_algorithm.to_hpke_config();
79
80 let (enc, mut ctxt) = config
81 .setup_sender(&pk_r, &info, None, None, None)
82 .map_err(map_hpke_error)?;
83
84 let encrypted_payload = ctxt
85 .seal(aad, payload)
86 .map(|ct| HpkeCiphertext {
87 kem_output: enc.into(),
88 ciphertext: ct.into(),
89 })
90 .map_err(map_hpke_error)?;
91 let encrypted_secondary_payload = ctxt.seal(aad, secondary_payload).map_err(map_hpke_error)?;
92
93 Ok((
94 encrypted_payload.tls_serialize_detached()?,
95 encrypted_secondary_payload,
96 ))
97}
98
99pub fn unwrap_payload_hpke(
103 wrapped_payload: &[u8],
104 wrapped_secondary_payload: &[u8],
105 private_key: &[u8],
106 wrapper_algorithm: WrapperAlgorithm,
107 label: &str,
108) -> Result<(Vec<u8>, Vec<u8>), UnwrapPayloadError> {
109 let ciphertext = HpkeCiphertext::tls_deserialize_exact(wrapped_payload)?;
110
111 let context = openmls::prelude::hpke::EncryptContext::from((label, [].as_slice()));
116 let info = context.tls_serialize_detached()?;
117 let aad = &[];
118
119 let config = wrapper_algorithm.to_hpke_config();
120
121 let sk_r = hpke_rs::HpkePrivateKey::new(private_key.to_vec());
122
123 let map_hpke_error = |_| openmls::ciphersuite::hpke::Error::DecryptionFailed;
124
125 let mut ctxt = config
126 .setup_receiver(
127 ciphertext.kem_output.as_ref(),
128 &sk_r,
129 &info,
130 None,
131 None,
132 None,
133 )
134 .map_err(map_hpke_error)?;
135
136 let payload = ctxt
137 .open(aad, ciphertext.ciphertext.as_ref())
138 .map_err(map_hpke_error)?;
139 let secondary_payload = if wrapped_secondary_payload.is_empty() {
140 vec![]
141 } else {
142 ctxt.open(aad, wrapped_secondary_payload)
143 .map_err(map_hpke_error)?
144 };
145
146 Ok((payload, secondary_payload))
147}
148
149pub fn wrap_payload_symmetric(
154 data: &[u8],
155 aead_type: openmls::prelude::AeadType,
156 symmetric_key: &[u8],
157 nonce: &[u8],
158) -> Result<Vec<u8>, WrapPayloadError> {
159 (*LIBCRUX_CRYPTO_PROVIDER)
160 .aead_encrypt(aead_type, symmetric_key, data, nonce, &[])
161 .map_err(Into::into)
162}
163
164pub fn unwrap_payload_symmetric(
166 data: &[u8],
167 aead_type: openmls::prelude::AeadType,
168 symmetric_key: &[u8],
169 nonce: &[u8],
170) -> Result<Vec<u8>, UnwrapPayloadError> {
171 (*LIBCRUX_CRYPTO_PROVIDER)
172 .aead_decrypt(aead_type, symmetric_key, data, nonce, &[])
173 .map_err(Into::into)
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use openmls_traits::{crypto::OpenMlsCrypto, random::OpenMlsRand};
180 use xmtp_configuration::{CIPHERSUITE, POST_QUANTUM_CIPHERSUITE, WELCOME_HPKE_LABEL};
181
182 const LEGACY_XWING_PUBLIC_KEY: &str =
185 include_str!("payload_encryption/fixtures/legacy_xwing/public_key.hex");
186 const LEGACY_XWING_PRIVATE_KEY: &str =
187 include_str!("payload_encryption/fixtures/legacy_xwing/private_key.hex");
188 const LEGACY_XWING_WRAPPED_PAYLOAD: &str =
189 include_str!("payload_encryption/fixtures/legacy_xwing/wrapped_payload.hex");
190 const LEGACY_XWING_SECONDARY_CIPHERTEXT: &str =
191 include_str!("payload_encryption/fixtures/legacy_xwing/secondary_ciphertext.hex");
192 const LEGACY_XWING_PAYLOAD: &[u8] = b"persisted Welcome compatibility fixture";
193 const LEGACY_XWING_SECONDARY_PAYLOAD: &[u8] = b"persisted Welcome metadata";
194 const LEGACY_XWING_IKM: [u8; 32] = [7; 32];
195
196 #[xmtp_common::test(unwrap_try = true)]
197 fn legacy_xwing_opens_persisted_welcome() {
198 let (payload, secondary) = unwrap_payload_hpke(
199 &hex::decode(LEGACY_XWING_WRAPPED_PAYLOAD.trim())?,
200 &hex::decode(LEGACY_XWING_SECONDARY_CIPHERTEXT.trim())?,
201 &hex::decode(LEGACY_XWING_PRIVATE_KEY.trim())?,
202 WrapperAlgorithm::XWingMLKEM768Draft6,
203 WELCOME_HPKE_LABEL,
204 )?;
205
206 assert_eq!(payload, LEGACY_XWING_PAYLOAD);
207 assert_eq!(secondary, LEGACY_XWING_SECONDARY_PAYLOAD);
208 }
209
210 #[xmtp_common::test(unwrap_try = true)]
211 fn legacy_xwing_key_encoding_is_stable() {
212 let hpke = WrapperAlgorithm::XWingMLKEM768Draft6.to_hpke_config();
213 let pair = hpke.derive_key_pair(&LEGACY_XWING_IKM)?;
214
215 assert_eq!(
216 pair.public_key().as_slice(),
217 hex::decode(LEGACY_XWING_PUBLIC_KEY.trim())?
218 );
219 assert_eq!(
220 pair.private_key().as_slice(),
221 hex::decode(LEGACY_XWING_PRIVATE_KEY.trim())?
222 );
223 }
224
225 #[xmtp_common::test(unwrap_try = true)]
226 fn legacy_xwing_stored_keys_accept_new_welcome() {
227 let (wrapped, metadata) = wrap_payload_hpke(
228 LEGACY_XWING_PAYLOAD,
229 LEGACY_XWING_SECONDARY_PAYLOAD,
230 &hex::decode(LEGACY_XWING_PUBLIC_KEY.trim())?,
231 WrapperAlgorithm::XWingMLKEM768Draft6,
232 WELCOME_HPKE_LABEL,
233 )?;
234 let (payload, secondary) = unwrap_payload_hpke(
235 &wrapped,
236 &metadata,
237 &hex::decode(LEGACY_XWING_PRIVATE_KEY.trim())?,
238 WrapperAlgorithm::XWingMLKEM768Draft6,
239 WELCOME_HPKE_LABEL,
240 )?;
241
242 assert_eq!(payload, LEGACY_XWING_PAYLOAD);
243 assert_eq!(secondary, LEGACY_XWING_SECONDARY_PAYLOAD);
244 }
245
246 const TEST_LABEL: &str = "test xmtp payload";
247
248 fn fresh_curve25519_keypair() -> (Vec<u8>, Vec<u8>) {
249 let crypto = openmls_rust_crypto::RustCrypto::default();
250 let ikm = crypto.random_vec(CIPHERSUITE.hash_length()).unwrap();
251 let kp = crypto
252 .derive_hpke_keypair(CIPHERSUITE.hpke_config(), &ikm)
253 .unwrap();
254 (kp.public, kp.private.to_vec())
255 }
256
257 fn fresh_xwing_keypair() -> (Vec<u8>, Vec<u8>) {
258 let crypto = openmls_libcrux_crypto::CryptoProvider::new().unwrap();
259 let ikm = crypto
260 .random_vec(POST_QUANTUM_CIPHERSUITE.hash_length())
261 .unwrap();
262 let kp = crypto
263 .derive_hpke_keypair(POST_QUANTUM_CIPHERSUITE.hpke_config(), &ikm)
264 .unwrap();
265 (kp.public, kp.private.to_vec())
266 }
267
268 #[xmtp_common::test]
269 fn round_trip_curve25519_hpke() {
270 let (pk, sk) = fresh_curve25519_keypair();
271
272 let payload = xmtp_common::rand_vec::<1000>();
273 let secondary = xmtp_common::rand_vec::<32>();
274
275 let wrapped = wrap_payload_hpke(
276 &payload,
277 &secondary,
278 &pk,
279 WrapperAlgorithm::Curve25519,
280 TEST_LABEL,
281 )
282 .unwrap();
283
284 assert_ne!(payload, wrapped.0);
285 assert_ne!(secondary, wrapped.1);
286
287 let unwrapped = unwrap_payload_hpke(
288 &wrapped.0,
289 &wrapped.1,
290 &sk,
291 WrapperAlgorithm::Curve25519,
292 TEST_LABEL,
293 )
294 .unwrap();
295
296 assert_eq!(unwrapped, (payload, secondary));
297 }
298
299 #[xmtp_common::test]
300 fn round_trip_xwing_hpke() {
301 let (pk, sk) = fresh_xwing_keypair();
302
303 let payload = xmtp_common::rand_vec::<1000>();
304 let secondary = xmtp_common::rand_vec::<32>();
305
306 let wrapped = wrap_payload_hpke(
307 &payload,
308 &secondary,
309 &pk,
310 WrapperAlgorithm::XWingMLKEM768Draft6,
311 TEST_LABEL,
312 )
313 .unwrap();
314
315 assert_ne!(payload, wrapped.0);
316 assert_ne!(secondary, wrapped.1);
317
318 let unwrapped = unwrap_payload_hpke(
319 &wrapped.0,
320 &wrapped.1,
321 &sk,
322 WrapperAlgorithm::XWingMLKEM768Draft6,
323 TEST_LABEL,
324 )
325 .unwrap();
326
327 assert_eq!(unwrapped, (payload.clone(), secondary));
328
329 let unwrapped = unwrap_payload_hpke(
331 &wrapped.0,
332 &[],
333 &sk,
334 WrapperAlgorithm::XWingMLKEM768Draft6,
335 TEST_LABEL,
336 )
337 .unwrap();
338
339 assert_eq!(unwrapped, (payload, vec![]));
340 }
341
342 #[xmtp_common::test]
343 fn wrong_key_fails_curve25519() {
344 let (pk, _sk) = fresh_curve25519_keypair();
345 let (_pk2, sk2) = fresh_curve25519_keypair();
346
347 let payload = xmtp_common::rand_vec::<128>();
348 let secondary = xmtp_common::rand_vec::<32>();
349
350 let wrapped = wrap_payload_hpke(
351 &payload,
352 &secondary,
353 &pk,
354 WrapperAlgorithm::Curve25519,
355 TEST_LABEL,
356 )
357 .unwrap();
358
359 unwrap_payload_hpke(
360 &wrapped.0,
361 &wrapped.1,
362 &sk2,
363 WrapperAlgorithm::Curve25519,
364 TEST_LABEL,
365 )
366 .unwrap_err();
367 }
368
369 #[xmtp_common::test]
370 fn wrong_label_fails_curve25519() {
371 let (pk, sk) = fresh_curve25519_keypair();
372
373 let payload = xmtp_common::rand_vec::<128>();
374 let secondary = xmtp_common::rand_vec::<32>();
375
376 let wrapped = wrap_payload_hpke(
377 &payload,
378 &secondary,
379 &pk,
380 WrapperAlgorithm::Curve25519,
381 TEST_LABEL,
382 )
383 .unwrap();
384
385 unwrap_payload_hpke(
386 &wrapped.0,
387 &wrapped.1,
388 &sk,
389 WrapperAlgorithm::Curve25519,
390 "different label",
391 )
392 .unwrap_err();
393 }
394
395 #[xmtp_common::test]
396 fn welcome_label_round_trip_matches_xmtp_configuration() {
397 let (pk, sk) = fresh_curve25519_keypair();
400
401 let payload = xmtp_common::rand_vec::<256>();
402 let secondary = xmtp_common::rand_vec::<32>();
403
404 let wrapped = wrap_payload_hpke(
405 &payload,
406 &secondary,
407 &pk,
408 WrapperAlgorithm::Curve25519,
409 WELCOME_HPKE_LABEL,
410 )
411 .unwrap();
412
413 let unwrapped = unwrap_payload_hpke(
414 &wrapped.0,
415 &wrapped.1,
416 &sk,
417 WrapperAlgorithm::Curve25519,
418 WELCOME_HPKE_LABEL,
419 )
420 .unwrap();
421
422 assert_eq!(unwrapped, (payload, secondary));
423 }
424
425 #[xmtp_common::test]
426 fn round_trip_symmetric() {
427 let symmetric_key = xmtp_common::rand_array::<32>();
428 let nonce = xmtp_common::rand_array::<12>();
429 let data = xmtp_common::rand_array::<1000>();
430
431 let wrapped = wrap_payload_symmetric(
432 &data,
433 openmls::prelude::AeadType::ChaCha20Poly1305,
434 &symmetric_key,
435 &nonce,
436 )
437 .unwrap();
438 let unwrapped = unwrap_payload_symmetric(
439 &wrapped,
440 openmls::prelude::AeadType::ChaCha20Poly1305,
441 &symmetric_key,
442 &nonce,
443 )
444 .unwrap();
445 assert_eq!(data.as_slice(), unwrapped.as_slice());
446 }
447
448 #[xmtp_common::test]
449 fn symmetric_wrong_key_fails() {
450 let symmetric_key = xmtp_common::rand_array::<32>();
451 let wrong_key = xmtp_common::rand_array::<32>();
452 let nonce = xmtp_common::rand_array::<12>();
453 let data = xmtp_common::rand_array::<1000>();
454
455 let wrapped = wrap_payload_symmetric(
456 &data,
457 openmls::prelude::AeadType::ChaCha20Poly1305,
458 &symmetric_key,
459 &nonce,
460 )
461 .unwrap();
462 unwrap_payload_symmetric(
463 &wrapped,
464 openmls::prelude::AeadType::ChaCha20Poly1305,
465 &wrong_key,
466 &nonce,
467 )
468 .unwrap_err();
469 }
470}