1use alloc::vec::Vec;
2use rand_core::{CryptoRng, RngCore};
3
4use crate::VerifyingKey;
5use crate::api::Client;
6use crate::ciphertext::Plaintext;
7use crate::keys::*;
8use crate::message::{
9 MessageKeyPair, MessagePrivateKey, MessagePublicKey, keygen as message_keygen,
10};
11use crate::metadata::{MetadataKeyPair, MetadataPublicKey, keygen as metadata_keygen};
12use crate::primitives::dh_akem::{DhAkemPrivateKey, DhAkemPublicKey};
13use crate::primitives::mlkem::{MLKEM768PrivateKey, MLKEM768PublicKey};
14use crate::primitives::provider;
15use crate::primitives::x25519::DHPrivateKey;
16use crate::primitives::x25519::DHPublicKey;
17use crate::primitives::x25519::generate_dh_keypair;
18use crate::sign::{JournalistEphemeralKey, JournalistLongTermKey, Signature, SigningKey};
19use crate::traits::{Enrollable, JournalistPublic, RestrictedApi, UserPublic, UserSecret};
20
21use crate::sealed;
23
24#[cfg(not(hax))]
25impl sealed::Sealed for Journalist {}
26
27#[cfg(not(hax))]
28impl RestrictedApi for Journalist {}
29
30pub struct Journalist {
34 signing_key: SigningKeyPair,
35 fetch_key: DhFetchKeyPair,
36 message_keys: Vec<SignedMessageKeyBundle>,
37 reply_apke: MessageKeyPair,
39 self_signature: Signature<JournalistLongTermKey>,
40 signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
41 session_storage: SessionStorage,
42}
43
44#[cfg_attr(not(hax), derive(serde::Serialize, serde::Deserialize))]
47pub struct JournalistPublicView {
48 vk: VerifyingKey,
49 fetch_pk: DHPublicKey,
50 reply_apke_pk: MessagePublicKey,
51 signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
52 selfsig: Signature<JournalistLongTermKey>,
53 kb: SignedKeyBundlePublic,
54}
55
56impl JournalistPublicView {
57 pub fn new(
58 vk: VerifyingKey,
59 fetch: DHPublicKey,
60 reply_apke: MessagePublicKey,
61 selfsig: Signature<JournalistLongTermKey>,
62 signed_longterm_key_bytes: SignedLongtermPubKeyBytes,
63 kb: SignedKeyBundlePublic,
64 ) -> Self {
65 Self {
66 vk,
67 fetch_pk: fetch,
68 reply_apke_pk: reply_apke,
69 selfsig,
70 signed_longterm_key_bytes,
71 kb,
72 }
73 }
74}
75
76impl UserPublic for JournalistPublicView {
77 fn fetch_pk(&self) -> &DHPublicKey {
78 &self.fetch_pk
79 }
80
81 fn message_auth_pk(&self) -> &MessagePublicKey {
82 &self.reply_apke_pk
83 }
84
85 fn message_metadata_pk(&self) -> &MetadataPublicKey {
86 &self.kb.0.metadata_pk
87 }
88
89 fn message_enc_pk(&self) -> &MessagePublicKey {
90 &self.kb.0.apke_pk
91 }
92}
93
94impl JournalistPublic for JournalistPublicView {
95 fn verifying_key(&self) -> &VerifyingKey {
96 &self.vk
97 }
98
99 fn self_signature(&self) -> &Signature<JournalistLongTermKey> {
100 &self.selfsig
101 }
102
103 fn signed_keybytes(&self) -> &SignedLongtermPubKeyBytes {
104 &self.signed_longterm_key_bytes
105 }
106
107 fn ephemeral_bundle(&self) -> &KeyBundlePublic {
108 &self.kb.0
109 }
110
111 fn ephemeral_signature(&self) -> &Signature<JournalistEphemeralKey> {
112 &self.kb.1
113 }
114}
115
116impl Client for Journalist {
117 fn newsroom_verifying_key(&self) -> Option<&VerifyingKey> {
118 self.session_storage.nr_key.as_ref()
119 }
120
121 fn set_newsroom_verifying_key(&mut self, key: VerifyingKey) {
122 self.session_storage.nr_key = Some(key);
123 }
124}
125
126#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
127fn keybundle_refs(message_keys: &[SignedMessageKeyBundle]) -> Vec<&MessageKeyBundle> {
128 let mut out = Vec::new();
129 for signed in message_keys.iter() {
130 out.push(&signed.bundle);
131 }
132 out
133}
134
135#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
136fn signed_keybundle_publics(message_keys: &[SignedMessageKeyBundle]) -> Vec<SignedKeyBundlePublic> {
137 let mut out = Vec::new();
138 for signed in message_keys.iter() {
139 out.push((signed.bundle.public(), signed.selfsig));
140 }
141 out
142}
143
144impl UserSecret for Journalist {
146 fn num_bundles(&self) -> usize {
147 self.message_keys.len()
148 }
149
150 fn fetch_keypair(&self) -> (&DHPrivateKey, &DHPublicKey) {
151 (&self.fetch_key.sk, &self.fetch_key.pk)
152 }
153
154 fn message_auth_key(&self) -> &crate::message::MessagePrivateKey {
155 self.reply_apke.private_key()
156 }
157
158 fn own_message_auth_pk(&self) -> &MessagePublicKey {
159 self.reply_apke.public_key()
160 }
161
162 fn build_message(&self, message: Vec<u8>) -> Plaintext {
163 Plaintext {
168 sender_fetch_key: [0u8; crate::primitives::x25519::DH_PUBLIC_KEY_LEN],
169 sender_reply_pubkey_hybrid: [0u8; crate::primitives::xwing::XWING_PUBLIC_KEY_LEN],
170 msg: message,
171 }
172 }
173
174 fn keybundles(&self) -> Vec<&MessageKeyBundle> {
175 keybundle_refs(&self.message_keys)
176 }
177}
178
179impl Enrollable for Journalist {
180 fn enroll(&self) -> Enrollment {
181 Enrollment {
182 bundle: self.signed_longterm_key_bytes.clone(),
183 selfsig: self.self_signature,
184 keys: (
185 self.signing_key.pk,
186 self.fetch_key.pk.clone(),
187 self.reply_apke.public_key().clone(),
188 ),
189 }
190 }
191
192 fn signed_keybundles(&self) -> Vec<SignedKeyBundlePublic> {
193 signed_keybundle_publics(&self.message_keys)
194 }
195
196 fn signing_key(&self) -> &VerifyingKey {
197 &self.signing_key.pk
198 }
199}
200
201#[cfg_attr(hax, hax_lib::fstar::verification_status(lax))]
203fn make_signed_bundle<R: RngCore + CryptoRng>(
204 rng: &mut R,
205 signing_key: &SigningKey,
206) -> SignedMessageKeyBundle {
207 let apke_kp = message_keygen(rng).expect("SD-APKE ephemeral keygen failed");
208 let metadata_kp = metadata_keygen(rng).expect("Failed to generate metadata keys");
209
210 let bundle = MessageKeyBundle::new(apke_kp, metadata_kp);
211
212 let pubkey_bytes = bundle.public().as_bytes();
213 let selfsig: Signature<JournalistEphemeralKey> = signing_key.sign(&pubkey_bytes);
214
215 SignedMessageKeyBundle { bundle, selfsig }
216}
217
218impl Journalist {
219 #[cfg_attr(hax, hax_lib::opaque)]
220 pub fn new<R: RngCore + CryptoRng>(rng: &mut R, num_keybundles: usize) -> Self {
221 let mut key_bundles: Vec<SignedMessageKeyBundle> = Vec::with_capacity(num_keybundles);
222
223 let signing_key = SigningKey::new(rng).expect("Signing keygen failed");
224 let verifying_key = signing_key.vk;
225
226 let (sk_fetch, pk_fetch) = generate_dh_keypair(rng).expect("DH Keygen (Fetch) failed");
227
228 let reply_apke = message_keygen(rng).expect("SD-APKE Keygen (Reply) failed");
229
230 let selfsigned_pubkeys =
233 SignedLongtermPubKeyBytes::from_keys(reply_apke.public_key(), &pk_fetch);
234 let self_signature: Signature<JournalistLongTermKey> =
235 signing_key.sign(selfsigned_pubkeys.as_bytes());
236
237 for _ in 0..num_keybundles {
239 key_bundles.push(make_signed_bundle(rng, &signing_key));
240 }
241 assert_eq!(key_bundles.len(), num_keybundles);
242
243 let session_storage = SessionStorage {
244 fpf_key: None,
245 nr_key: None,
246 fpf_signature: None,
247 };
248
249 Self {
250 signing_key: KeyPair {
251 sk: signing_key,
252 pk: verifying_key,
253 },
254 fetch_key: KeyPair {
255 sk: sk_fetch,
256 pk: pk_fetch,
257 },
258 reply_apke,
259 message_keys: key_bundles,
260 self_signature,
261 signed_longterm_key_bytes: selfsigned_pubkeys,
262 session_storage,
263 }
264 }
265
266 #[cfg_attr(hax, hax_lib::opaque)]
267 pub fn public(&self, idx: usize) -> JournalistPublicView {
268 let kb = self.message_keys.get(idx).expect("Bad index");
269 JournalistPublicView::new(
270 self.signing_key.pk,
271 self.fetch_key.pk.clone(),
272 self.reply_apke.public_key().clone(),
273 self.self_signature,
274 self.signed_longterm_key_bytes.clone(),
275 (kb.bundle.public(), kb.selfsig),
276 )
277 }
278
279 pub fn long_term_bytes(&self) -> JournalistLongTermBytes {
283 JournalistLongTermBytes {
284 sig_seed: self.signing_key.sk.as_bytes(),
285 fetch_sk: *self.fetch_key.sk.as_bytes(),
286 apke_dhakem_sk: *self.reply_apke.private_key().dhakem.as_bytes(),
287 apke_mlkem_sk: *self.reply_apke.private_key().mlkem.as_bytes(),
288 apke_mlkem_pk: *self.reply_apke.public_key().mlkem.as_bytes(),
289 }
290 }
291
292 #[cfg_attr(hax, hax_lib::opaque)]
294 pub fn from_long_term_bytes(parts: JournalistLongTermBytes) -> Self {
295 use crate::message::{MessagePrivateKey, MessagePublicKey};
296 use crate::primitives::dh_akem::{DhAkemPrivateKey, DhAkemPublicKey};
297 use crate::primitives::mlkem::{MLKEM768PrivateKey, MLKEM768PublicKey};
298 use crate::primitives::provider;
299 use crate::primitives::x25519::{DHPrivateKey, dh_public_key_from_scalar};
300
301 let signing_key = SigningKey::from_seed(parts.sig_seed);
302 let verifying_key = signing_key.vk;
303
304 let sk_fetch = DHPrivateKey::from_bytes(parts.fetch_sk);
305 let pk_fetch = dh_public_key_from_scalar(parts.fetch_sk);
306
307 let mut apke_dhakem_pk_bytes = [0u8; 32];
308 provider::curve25519::secret_to_public(&mut apke_dhakem_pk_bytes, &parts.apke_dhakem_sk);
309 let apke_dhakem_sk = DhAkemPrivateKey::from_bytes(parts.apke_dhakem_sk);
310 let apke_dhakem_pk = DhAkemPublicKey::from_bytes(apke_dhakem_pk_bytes);
311 let apke_mlkem_sk = MLKEM768PrivateKey::from_bytes(parts.apke_mlkem_sk);
312 let apke_mlkem_pk = MLKEM768PublicKey::from_bytes(parts.apke_mlkem_pk);
313
314 let reply_apke = MessageKeyPair::new(
315 MessagePrivateKey {
316 dhakem: apke_dhakem_sk,
317 mlkem: apke_mlkem_sk,
318 },
319 MessagePublicKey {
320 dhakem: apke_dhakem_pk,
321 mlkem: apke_mlkem_pk,
322 },
323 );
324
325 let signed_longterm_key_bytes =
326 SignedLongtermPubKeyBytes::from_keys(reply_apke.public_key(), &pk_fetch);
327 let self_signature: Signature<JournalistLongTermKey> =
328 signing_key.sign(signed_longterm_key_bytes.as_bytes());
329
330 Self {
331 signing_key: KeyPair {
332 sk: signing_key,
333 pk: verifying_key,
334 },
335 fetch_key: KeyPair {
336 sk: sk_fetch,
337 pk: pk_fetch,
338 },
339 reply_apke,
340 message_keys: Vec::new(),
341 self_signature,
342 signed_longterm_key_bytes,
343 session_storage: SessionStorage {
344 fpf_key: None,
345 nr_key: None,
346 fpf_signature: None,
347 },
348 }
349 }
350
351 #[cfg_attr(hax, hax_lib::opaque)]
358 pub fn generate_ephemeral_bundles<R: RngCore + CryptoRng>(&mut self, rng: &mut R, n: usize) {
359 for _ in 0..n {
360 let signed = make_signed_bundle(rng, &self.signing_key.sk);
361 self.message_keys.push(signed);
362 }
363 }
364
365 #[cfg_attr(hax, hax_lib::opaque)]
370 pub fn ephemeral_bundle_bytes(&self) -> Vec<EphemeralBundleBytes> {
371 self.message_keys
372 .iter()
373 .map(|signed| EphemeralBundleBytes::from_bundle(&signed.bundle))
374 .collect()
375 }
376
377 #[cfg_attr(hax, hax_lib::opaque)]
381 pub fn load_ephemeral_bundles(&mut self, bundles: Vec<EphemeralBundleBytes>) {
382 for bytes in bundles {
383 let bundle = bytes.into_bundle();
384 let pubkey_bytes = bundle.public().as_bytes();
385 let selfsig: Signature<JournalistEphemeralKey> =
388 self.signing_key.sk.sign(&pubkey_bytes);
389 self.message_keys
390 .push(SignedMessageKeyBundle { bundle, selfsig });
391 }
392 }
393}
394
395pub struct JournalistLongTermBytes {
399 pub sig_seed: [u8; 32],
400 pub fetch_sk: [u8; 32],
401 pub apke_dhakem_sk: [u8; 32],
402 pub apke_mlkem_sk: [u8; crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN],
403 pub apke_mlkem_pk: [u8; crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN],
404}
405
406impl JournalistLongTermBytes {
407 pub const LEN: usize = 32
409 + 32
410 + 32
411 + crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN
412 + crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN;
413
414 pub fn as_bytes(&self) -> Vec<u8> {
416 let mut out = Vec::with_capacity(Self::LEN);
417 out.extend_from_slice(&self.sig_seed);
418 out.extend_from_slice(&self.fetch_sk);
419 out.extend_from_slice(&self.apke_dhakem_sk);
420 out.extend_from_slice(&self.apke_mlkem_sk);
421 out.extend_from_slice(&self.apke_mlkem_pk);
422 out
423 }
424
425 pub fn from_bytes(bytes: &[u8]) -> Result<Self, anyhow::Error> {
431 if bytes.len() != Self::LEN {
432 return Err(anyhow::anyhow!(
433 "Invalid JournalistLongTermBytes length: expected {}, got {}",
434 Self::LEN,
435 bytes.len()
436 ));
437 }
438
439 let (sig_seed, rest) = bytes.split_at(32);
440 let (fetch_sk, rest) = rest.split_at(32);
441 let (apke_dhakem_sk, rest) = rest.split_at(32);
442 let (apke_mlkem_sk, apke_mlkem_pk) =
443 rest.split_at(crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN);
444
445 Ok(Self {
447 sig_seed: sig_seed.try_into().expect("wrong checked length"),
448 fetch_sk: fetch_sk.try_into().expect("wrong checked length"),
449 apke_dhakem_sk: apke_dhakem_sk.try_into().expect("wrong checked length"),
450 apke_mlkem_sk: apke_mlkem_sk.try_into().expect("wrong checked length"),
451 apke_mlkem_pk: apke_mlkem_pk.try_into().expect("wrong checked length"),
452 })
453 }
454}
455
456pub struct EphemeralBundleBytes {
458 pub apke_dhakem_sk: [u8; 32],
459 pub apke_mlkem_sk: [u8; crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN],
460 pub apke_mlkem_pk: [u8; crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN],
461 pub metadata_sk: [u8; crate::primitives::xwing::XWING_PRIVATE_KEY_LEN],
462 pub metadata_pk: [u8; crate::primitives::xwing::XWING_PUBLIC_KEY_LEN],
463}
464
465impl EphemeralBundleBytes {
466 pub const LEN: usize = 32
469 + crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN
470 + crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN
471 + crate::primitives::xwing::XWING_PRIVATE_KEY_LEN
472 + crate::primitives::xwing::XWING_PUBLIC_KEY_LEN;
473
474 fn from_bundle(bundle: &MessageKeyBundle) -> Self {
475 Self {
476 apke_dhakem_sk: *bundle.apke.private_key().dhakem.as_bytes(),
477 apke_mlkem_sk: *bundle.apke.private_key().mlkem.as_bytes(),
478 apke_mlkem_pk: *bundle.apke.public_key().mlkem.as_bytes(),
479 metadata_sk: *bundle.metadata_kp.secret_bytes(),
480 metadata_pk: *bundle.metadata_kp.public_bytes(),
481 }
482 }
483
484 #[cfg_attr(hax, hax_lib::opaque)]
485 fn into_bundle(self) -> MessageKeyBundle {
486 let mut apke_dhakem_pk_bytes = [0u8; 32];
487 provider::curve25519::secret_to_public(&mut apke_dhakem_pk_bytes, &self.apke_dhakem_sk);
488
489 let apke = MessageKeyPair::new(
490 MessagePrivateKey {
491 dhakem: DhAkemPrivateKey::from_bytes(self.apke_dhakem_sk),
492 mlkem: MLKEM768PrivateKey::from_bytes(self.apke_mlkem_sk),
493 },
494 MessagePublicKey {
495 dhakem: DhAkemPublicKey::from_bytes(apke_dhakem_pk_bytes),
496 mlkem: MLKEM768PublicKey::from_bytes(self.apke_mlkem_pk),
497 },
498 );
499 let metadata_kp = MetadataKeyPair::from_key_bytes(self.metadata_sk, self.metadata_pk);
500
501 MessageKeyBundle::new(apke, metadata_kp)
502 }
503
504 pub fn as_bytes(&self) -> Vec<u8> {
507 let mut out = Vec::with_capacity(Self::LEN);
508 out.extend_from_slice(&self.apke_dhakem_sk);
509 out.extend_from_slice(&self.apke_mlkem_sk);
510 out.extend_from_slice(&self.apke_mlkem_pk);
511 out.extend_from_slice(&self.metadata_sk);
512 out.extend_from_slice(&self.metadata_pk);
513 out
514 }
515
516 pub fn from_bytes(bytes: &[u8]) -> Result<Self, anyhow::Error> {
523 if bytes.len() != Self::LEN {
524 return Err(anyhow::anyhow!(
525 "Invalid EphemeralBundleBytes length: expected {}, got {}",
526 Self::LEN,
527 bytes.len()
528 ));
529 }
530
531 let (apke_dhakem_sk, rest) = bytes.split_at(32);
532 let (apke_mlkem_sk, rest) =
533 rest.split_at(crate::primitives::mlkem::MLKEM768_PRIVATE_KEY_LEN);
534 let (apke_mlkem_pk, rest) =
535 rest.split_at(crate::primitives::mlkem::MLKEM768_PUBLIC_KEY_LEN);
536 let (metadata_sk, metadata_pk) =
537 rest.split_at(crate::primitives::xwing::XWING_PRIVATE_KEY_LEN);
538
539 Ok(Self {
541 apke_dhakem_sk: apke_dhakem_sk.try_into().expect("wrong checked length"),
542 apke_mlkem_sk: apke_mlkem_sk.try_into().expect("wrong checked length"),
543 apke_mlkem_pk: apke_mlkem_pk.try_into().expect("wrong checked length"),
544 metadata_sk: metadata_sk.try_into().expect("wrong checked length"),
545 metadata_pk: metadata_pk.try_into().expect("wrong checked length"),
546 })
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553 use crate::Enrollable;
554 use crate::api::JournalistApi;
555 use crate::wire::setup::{JournalistEphemeralKeyRequest, JournalistSetupRequest};
556 use rand_chacha::ChaCha20Rng;
557 use rand_core::SeedableRng;
558
559 #[test]
560 fn test_journalist_setup_request_serde_roundtrip() {
561 let mut rng = ChaCha20Rng::seed_from_u64(7);
562 let journalist = Journalist::new(&mut rng, 0);
563 let req = JournalistSetupRequest {
564 enrollment: journalist.enroll(),
565 };
566 let json = serde_json::to_string(&req).expect("serialize");
567 let restored: JournalistSetupRequest = serde_json::from_str(&json).expect("deserialize");
568 assert_eq!(
569 req.enrollment.bundle.as_bytes(),
570 restored.enrollment.bundle.as_bytes()
571 );
572 assert_eq!(
573 req.enrollment.selfsig.as_bytes(),
574 restored.enrollment.selfsig.as_bytes()
575 );
576 assert_eq!(
577 req.enrollment.keys.0.into_bytes(),
578 restored.enrollment.keys.0.into_bytes()
579 );
580 assert_eq!(
581 req.enrollment.keys.1.into_bytes(),
582 restored.enrollment.keys.1.into_bytes()
583 );
584 assert_eq!(
585 req.enrollment.keys.2.as_bytes(),
586 restored.enrollment.keys.2.as_bytes()
587 );
588 }
589
590 #[test]
591 fn test_journalist_setup() {
592 let mut rng = ChaCha20Rng::seed_from_u64(666);
593
594 let journalist = Journalist::new(&mut rng, 5);
595 assert_eq!(journalist.message_keys.len(), 5);
596 let skb: Vec<SignedKeyBundlePublic> = journalist.signed_keybundles();
597 assert_eq!(journalist.message_keys.len(), skb.len());
598
599 let kbs: Vec<&MessageKeyBundle> = journalist.keybundles();
600 assert_eq!(kbs.len(), journalist.message_keys.len());
601
602 for i in 0..kbs.len() {
603 assert_eq!(
604 journalist.message_keys[i]
605 .bundle
606 .apke
607 .public_key()
608 .as_bytes(),
609 kbs[i].apke.public_key().as_bytes()
610 );
611 assert_eq!(
612 journalist.message_keys[i]
613 .bundle
614 .metadata_kp
615 .private_key()
616 .as_bytes(),
617 kbs[i].metadata_kp.private_key().as_bytes()
618 );
619 assert_eq!(
620 journalist.message_keys[i]
621 .bundle
622 .metadata_kp
623 .public_key()
624 .as_bytes(),
625 kbs[i].metadata_kp.public_key().as_bytes()
626 );
627 }
628 }
629
630 #[test]
631 fn test_journalist_enroll_selfsig() {
632 let mut rng = ChaCha20Rng::seed_from_u64(666);
633
634 let journalist = Journalist::new(&mut rng, 5);
635 let e = journalist.enroll();
636
637 journalist
638 .signing_key()
639 .verify(e.bundle.as_bytes(), &e.selfsig)
640 .expect("Need correct enrollment sig");
641 }
642
643 use proptest::prelude::*;
644
645 proptest! {
646 #[test]
647 fn test_journalist_long_term_bytes_roundtrip(rng_seed: u64) {
648 let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
649 let original = Journalist::new(&mut rng, 0);
650 let parts = original.long_term_bytes();
651 let restored = Journalist::from_long_term_bytes(parts);
652
653 prop_assert_eq!(
655 original.signing_key.pk.into_bytes(),
656 restored.signing_key.pk.into_bytes()
657 );
658 prop_assert_eq!(
659 original.signed_longterm_key_bytes.as_bytes(),
660 restored.signed_longterm_key_bytes.as_bytes()
661 );
662 prop_assert_eq!(
663 original.self_signature.as_bytes(),
664 restored.self_signature.as_bytes()
665 );
666 prop_assert!(restored.message_keys.is_empty());
667 }
668
669 #[test]
670 fn test_journalist_long_term_bytes_serde_roundtrip(rng_seed: u64) {
671 let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
672 let parts = Journalist::new(&mut rng, 0).long_term_bytes();
673
674 let bytes = parts.as_bytes();
675 prop_assert_eq!(bytes.len(), JournalistLongTermBytes::LEN);
676
677 let restored = JournalistLongTermBytes::from_bytes(&bytes).expect("valid length");
678 prop_assert_eq!(restored.sig_seed, parts.sig_seed);
679 prop_assert_eq!(restored.fetch_sk, parts.fetch_sk);
680 prop_assert_eq!(restored.apke_dhakem_sk, parts.apke_dhakem_sk);
681 prop_assert_eq!(restored.apke_mlkem_sk, parts.apke_mlkem_sk);
682 prop_assert_eq!(restored.apke_mlkem_pk, parts.apke_mlkem_pk);
683 }
684
685 #[test]
686 fn test_ephemeral_bundle_bytes_roundtrip(rng_seed: u64, n in 0usize..4) {
687 let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
688 let mut original = Journalist::new(&mut rng, 0);
689 original.generate_ephemeral_bundles(&mut rng, n);
690
691 let persisted: Vec<EphemeralBundleBytes> = original
692 .ephemeral_bundle_bytes()
693 .into_iter()
694 .map(|b| {
695 let bytes = b.as_bytes();
696 prop_assert_eq!(bytes.len(), EphemeralBundleBytes::LEN);
697 Ok(EphemeralBundleBytes::from_bytes(&bytes).expect("valid length"))
698 })
699 .collect::<Result<_, TestCaseError>>()?;
700
701 let mut restored = Journalist::from_long_term_bytes(original.long_term_bytes());
702 restored.load_ephemeral_bundles(persisted);
703
704 let orig_pub = original.signed_keybundles();
705 let restored_pub = restored.signed_keybundles();
706 prop_assert_eq!(orig_pub.len(), n);
707 prop_assert_eq!(restored_pub.len(), n);
708 for (a, b) in orig_pub.iter().zip(restored_pub.iter()) {
709 prop_assert_eq!(a.0.as_bytes(), b.0.as_bytes());
710 prop_assert_eq!(a.1.as_bytes(), b.1.as_bytes());
711 }
712 }
713
714 #[test]
715 fn test_journalist_ephemeral_key_request_serde_roundtrip(rng_seed: u64, n in 1usize..4) {
716 let mut rng = ChaCha20Rng::seed_from_u64(rng_seed);
717 let mut journalist = Journalist::new(&mut rng, 0);
718 journalist.generate_ephemeral_bundles(&mut rng, n);
719
720 let req = journalist.create_ephemeral_key_request();
721 let json = serde_json::to_string(&req).expect("serialize");
722 let restored: JournalistEphemeralKeyRequest =
723 serde_json::from_str(&json).expect("deserialize");
724
725 prop_assert_eq!(
726 req.verifying_key.into_bytes(),
727 restored.verifying_key.into_bytes()
728 );
729 prop_assert_eq!(req.bundles.len(), n);
730 prop_assert_eq!(restored.bundles.len(), n);
731 for (a, b) in req.bundles.iter().zip(restored.bundles.iter()) {
732 prop_assert_eq!(a.0.as_bytes(), b.0.as_bytes());
733 prop_assert_eq!(a.1.as_bytes(), b.1.as_bytes());
734 }
735 }
736 }
737
738 #[test]
739 fn test_journalist_long_term_bytes_from_bytes_rejects_wrong_length() {
740 assert!(JournalistLongTermBytes::from_bytes(&[]).is_err());
741 assert!(
742 JournalistLongTermBytes::from_bytes(&[0u8; JournalistLongTermBytes::LEN - 1]).is_err()
743 );
744 assert!(
745 JournalistLongTermBytes::from_bytes(&[0u8; JournalistLongTermBytes::LEN + 1]).is_err()
746 );
747 }
748}