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