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