1use base64::engine::general_purpose::STANDARD as BASE64;
18use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
19use base64::Engine as _;
20use ed25519_dalek::{Signature, Signer as _, SigningKey, Verifier as _, VerifyingKey};
21use rainier_support::{Error, Result};
22use sha2::{Digest, Sha256};
23use x25519_dalek::{PublicKey as X25519Public, StaticSecret};
24
25use crate::cipher::Cipher;
26use crate::key::Key;
27use crate::signer::Signer;
28
29const SEAL: &str = "seal1";
31
32fn public_id(domain: &[u8], public: &[u8]) -> String {
38 let mut hasher = Sha256::new();
39 hasher.update(domain);
40 hasher.update(public);
41 hasher.finalize().iter().take(4).map(|byte| format!("{byte:02x}")).collect()
42}
43
44#[derive(Clone)]
48pub struct SigningKeyPair {
49 signing: SigningKey,
50 id: String,
51}
52
53impl SigningKeyPair {
54 pub fn generate() -> Self {
56 use rand::RngCore;
57
58 let mut seed = [0u8; 32];
59 rand::rngs::OsRng.fill_bytes(&mut seed);
60 Self::from_seed(seed)
61 }
62
63 pub fn from_seed(seed: [u8; 32]) -> Self {
65 let signing = SigningKey::from_bytes(&seed);
66 let id = public_id(b"rainier-ed25519", signing.verifying_key().as_bytes());
67 Self { signing, id }
68 }
69
70 pub fn from_base64(encoded: &str) -> Result<Self> {
72 Ok(Self::from_seed(decode_32(encoded, "signing key")?))
73 }
74
75 pub fn to_base64(&self) -> String {
77 format!("base64:{}", BASE64.encode(self.signing.to_bytes()))
78 }
79
80 pub fn public(&self) -> VerifyingPublicKey {
82 VerifyingPublicKey { verifying: self.signing.verifying_key(), id: self.id.clone() }
83 }
84
85 pub fn id(&self) -> &str {
87 &self.id
88 }
89}
90
91impl std::fmt::Debug for SigningKeyPair {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("SigningKeyPair").field("id", &self.id).field("seed", &"<redacted>").finish()
94 }
95}
96
97#[derive(Clone)]
99pub struct VerifyingPublicKey {
100 verifying: VerifyingKey,
101 id: String,
102}
103
104impl VerifyingPublicKey {
105 pub fn from_bytes(bytes: [u8; 32]) -> Result<Self> {
107 let verifying = VerifyingKey::from_bytes(&bytes)
108 .map_err(|_| Error::internal("that is not a valid Ed25519 public key"))?;
109 let id = public_id(b"rainier-ed25519", verifying.as_bytes());
110 Ok(Self { verifying, id })
111 }
112
113 pub fn from_base64(encoded: &str) -> Result<Self> {
115 Self::from_bytes(decode_32(encoded, "public key")?)
116 }
117
118 pub fn to_base64(&self) -> String {
120 format!("base64:{}", BASE64.encode(self.verifying.as_bytes()))
121 }
122
123 pub fn id(&self) -> &str {
125 &self.id
126 }
127}
128
129impl std::fmt::Debug for VerifyingPublicKey {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 f.debug_struct("VerifyingPublicKey").field("id", &self.id).finish()
132 }
133}
134
135pub struct Ed25519Signer {
157 signing: Option<SigningKeyPair>,
158 trusted: Vec<VerifyingPublicKey>,
159}
160
161impl Ed25519Signer {
162 pub fn new(keys: SigningKeyPair) -> Self {
164 let public = keys.public();
165 Self { signing: Some(keys), trusted: vec![public] }
166 }
167
168 pub fn verify_only(public: VerifyingPublicKey) -> Self {
170 Self { signing: None, trusted: vec![public] }
171 }
172
173 pub fn trusting(mut self, public: VerifyingPublicKey) -> Self {
178 if !self.trusted.iter().any(|key| key.id() == public.id()) {
179 self.trusted.push(public);
180 }
181 self
182 }
183
184 pub fn can_sign(&self) -> bool {
186 self.signing.is_some()
187 }
188
189 pub fn trusted_ids(&self) -> Vec<&str> {
191 self.trusted.iter().map(VerifyingPublicKey::id).collect()
192 }
193}
194
195impl Signer for Ed25519Signer {
196 fn sign(&self, value: &str) -> Result<String> {
197 let keys = self.signing.as_ref().ok_or_else(|| {
198 Error::internal(
199 "this Ed25519 signer holds only a public key, so it can verify but not sign",
200 )
201 })?;
202
203 if value.contains('.') {
204 return Err(Error::internal(
205 "a signed value must not contain `.` — encode it first (base64, or JSON \
206 through the encrypter)",
207 ));
208 }
209
210 let message = format!("{}.{value}", keys.id());
213 let signature = keys.signing.sign(message.as_bytes());
214
215 Ok(format!("{value}.{}.{}", keys.id(), B64.encode(signature.to_bytes())))
216 }
217
218 fn verify(&self, signed: &str) -> Result<String> {
219 let invalid = || Error::bad_request("the signature is not valid");
220
221 let mut parts = signed.split('.');
222 let (Some(value), Some(key_id), Some(tag), None) =
223 (parts.next(), parts.next(), parts.next(), parts.next())
224 else {
225 return Err(invalid());
226 };
227
228 let key = self.trusted.iter().find(|key| key.id() == key_id).ok_or_else(invalid)?;
229
230 let bytes: [u8; 64] =
231 B64.decode(tag).map_err(|_| invalid())?[..].try_into().map_err(|_| invalid())?;
232 let signature = Signature::from_bytes(&bytes);
233
234 let message = format!("{key_id}.{value}");
235 key.verifying
236 .verify(message.as_bytes(), &signature)
237 .map(|()| value.to_string())
238 .map_err(|_| invalid())
239 }
240}
241
242impl std::fmt::Debug for Ed25519Signer {
243 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
244 f.debug_struct("Ed25519Signer")
245 .field("can_sign", &self.can_sign())
246 .field("trusted", &self.trusted_ids())
247 .finish()
248 }
249}
250
251#[derive(Clone)]
255pub struct BoxKeyPair {
256 secret: StaticSecret,
257 id: String,
258}
259
260impl BoxKeyPair {
261 pub fn generate() -> Self {
263 use rand::RngCore;
264
265 let mut bytes = [0u8; 32];
266 rand::rngs::OsRng.fill_bytes(&mut bytes);
267 Self::from_bytes(bytes)
268 }
269
270 pub fn from_bytes(bytes: [u8; 32]) -> Self {
272 let secret = StaticSecret::from(bytes);
273 let id = public_id(b"rainier-x25519", X25519Public::from(&secret).as_bytes());
274 Self { secret, id }
275 }
276
277 pub fn from_base64(encoded: &str) -> Result<Self> {
279 Ok(Self::from_bytes(decode_32(encoded, "box key")?))
280 }
281
282 pub fn to_base64(&self) -> String {
284 format!("base64:{}", BASE64.encode(self.secret.to_bytes()))
285 }
286
287 pub fn public(&self) -> BoxPublicKey {
289 BoxPublicKey { public: X25519Public::from(&self.secret), id: self.id.clone() }
290 }
291
292 pub fn id(&self) -> &str {
294 &self.id
295 }
296
297 pub fn agree(&self, theirs: &BoxPublicKey) -> Key {
304 derive_shared(&self.secret, &theirs.public, b"rainier-x25519-agree")
305 }
306}
307
308impl std::fmt::Debug for BoxKeyPair {
309 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
310 f.debug_struct("BoxKeyPair").field("id", &self.id).field("secret", &"<redacted>").finish()
311 }
312}
313
314#[derive(Clone, PartialEq, Eq)]
316pub struct BoxPublicKey {
317 public: X25519Public,
318 id: String,
319}
320
321impl BoxPublicKey {
322 pub fn from_bytes(bytes: [u8; 32]) -> Self {
324 let public = X25519Public::from(bytes);
325 let id = public_id(b"rainier-x25519", public.as_bytes());
326 Self { public, id }
327 }
328
329 pub fn from_base64(encoded: &str) -> Result<Self> {
331 Ok(Self::from_bytes(decode_32(encoded, "public box key")?))
332 }
333
334 pub fn to_base64(&self) -> String {
336 format!("base64:{}", BASE64.encode(self.public.as_bytes()))
337 }
338
339 pub fn id(&self) -> &str {
341 &self.id
342 }
343}
344
345impl std::fmt::Debug for BoxPublicKey {
346 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
347 f.debug_struct("BoxPublicKey").field("id", &self.id).finish()
348 }
349}
350
351fn derive_shared(secret: &StaticSecret, public: &X25519Public, domain: &[u8]) -> Key {
356 use hkdf::Hkdf;
357
358 let shared = secret.diffie_hellman(public);
359 let hkdf = Hkdf::<Sha256>::new(Some(domain), shared.as_bytes());
360
361 let mut bytes = [0u8; 32];
362 hkdf.expand(b"key", &mut bytes).expect("32 bytes is within HKDF-SHA256's limit");
364 Key::from_bytes(bytes)
365}
366
367#[derive(Debug, Clone, Default)]
391pub struct SealedBox {
392 cipher: Cipher,
393}
394
395impl SealedBox {
396 pub fn new() -> Self {
398 Self { cipher: Cipher::default() }
399 }
400
401 pub fn with_cipher(mut self, cipher: Cipher) -> Self {
403 self.cipher = cipher;
404 self
405 }
406
407 pub fn seal(&self, recipient: &BoxPublicKey, plain: &[u8]) -> Result<String> {
409 let ephemeral = BoxKeyPair::generate();
413 let shared = derive_shared(&ephemeral.secret, &recipient.public, b"rainier-sealed-box");
414
415 let nonce = self.cipher.nonce();
416 let ephemeral_public = ephemeral.public();
417
418 let header = format!(
422 "{SEAL}.{}.{}.{}",
423 self.cipher.id(),
424 recipient.id(),
425 B64.encode(ephemeral_public.public.as_bytes())
426 );
427 let sealed = self.cipher.encrypt(&shared, &nonce, header.as_bytes(), plain)?;
428
429 Ok(format!("{header}.{}.{}", B64.encode(&nonce), B64.encode(&sealed)))
430 }
431
432 pub fn seal_string(&self, recipient: &BoxPublicKey, plain: &str) -> Result<String> {
434 self.seal(recipient, plain.as_bytes())
435 }
436
437 pub fn unseal(&self, recipient: &BoxKeyPair, payload: &str) -> Result<Vec<u8>> {
439 let invalid = || Error::bad_request("the sealed box could not be opened");
440
441 let mut parts = payload.split('.');
442 let (
443 Some(tag),
444 Some(algorithm),
445 Some(key_id),
446 Some(ephemeral),
447 Some(nonce),
448 Some(sealed),
449 None,
450 ) = (
451 parts.next(),
452 parts.next(),
453 parts.next(),
454 parts.next(),
455 parts.next(),
456 parts.next(),
457 parts.next(),
458 )
459 else {
460 return Err(invalid());
461 };
462
463 if tag != SEAL || key_id != recipient.id() {
464 return Err(invalid());
465 }
466
467 let cipher = Cipher::from_id(algorithm).ok_or_else(invalid)?;
468
469 let ephemeral_bytes: [u8; 32] =
470 B64.decode(ephemeral).map_err(|_| invalid())?[..].try_into().map_err(|_| invalid())?;
471 let shared = derive_shared(
472 &recipient.secret,
473 &X25519Public::from(ephemeral_bytes),
474 b"rainier-sealed-box",
475 );
476
477 let nonce = B64.decode(nonce).map_err(|_| invalid())?;
478 let sealed = B64.decode(sealed).map_err(|_| invalid())?;
479
480 let header = format!("{tag}.{algorithm}.{key_id}.{ephemeral}");
481 cipher.decrypt(&shared, &nonce, header.as_bytes(), &sealed).map_err(|_| invalid())
482 }
483
484 pub fn unseal_string(&self, recipient: &BoxKeyPair, payload: &str) -> Result<String> {
486 let bytes = self.unseal(recipient, payload)?;
487 String::from_utf8(bytes).map_err(|_| Error::internal("the opened value is not valid UTF-8"))
488 }
489}
490
491fn decode_32(encoded: &str, what: &str) -> Result<[u8; 32]> {
493 let trimmed = encoded.trim();
494 let payload = trimmed.strip_prefix("base64:").unwrap_or(trimmed);
495
496 let decoded = BASE64
497 .decode(payload)
498 .map_err(|_| Error::internal(format!("the {what} is not valid base64")))?;
499
500 decoded.as_slice().try_into().map_err(|_| {
501 Error::internal(format!("the {what} must be 32 bytes, but decoded to {}", decoded.len()))
502 })
503}
504
505#[cfg(test)]
506mod tests {
507 use super::*;
508 use crate::signer::SignerExt;
509
510 #[test]
513 fn an_ed25519_signature_round_trips() {
514 let signer = Ed25519Signer::new(SigningKeyPair::generate());
515 let signed = signer.sign("licence-42").unwrap();
516
517 assert!(signed.starts_with("licence-42."), "the value stays readable");
518 assert_eq!(signer.verify(&signed).unwrap(), "licence-42");
519 }
520
521 #[test]
522 fn a_public_key_verifies_without_being_able_to_sign() {
523 let keys = SigningKeyPair::generate();
525 let signed = Ed25519Signer::new(keys.clone()).sign("licence-42").unwrap();
526
527 let checker = Ed25519Signer::verify_only(keys.public());
528
529 assert!(!checker.can_sign());
530 assert_eq!(checker.verify(&signed).unwrap(), "licence-42");
531
532 let err = checker.sign("forged").unwrap_err();
533 assert!(err.message().contains("cannot sign") || err.message().contains("but not sign"));
534 }
535
536 #[test]
537 fn altering_the_value_invalidates_it() {
538 let signer = Ed25519Signer::new(SigningKeyPair::generate());
539 let signed = signer.sign("licence-42").unwrap();
540
541 assert!(signer.verify(&signed.replacen("licence-42", "licence-43", 1)).is_err());
542 }
543
544 #[test]
545 fn another_keys_signature_is_not_accepted() {
546 let signed = Ed25519Signer::new(SigningKeyPair::generate()).sign("x").unwrap();
547 let other = Ed25519Signer::new(SigningKeyPair::generate());
548
549 assert!(other.verify(&signed).is_err());
550 }
551
552 #[test]
553 fn a_trusted_second_key_is_accepted() {
554 let old = SigningKeyPair::generate();
556 let signed = Ed25519Signer::new(old.clone()).sign("from-before").unwrap();
557
558 let now = Ed25519Signer::new(SigningKeyPair::generate()).trusting(old.public());
559
560 assert_eq!(now.verify(&signed).unwrap(), "from-before");
561 assert_eq!(now.trusted_ids().len(), 2);
562 }
563
564 #[test]
565 fn trusting_the_same_key_twice_does_not_duplicate_it() {
566 let keys = SigningKeyPair::generate();
567 let signer = Ed25519Signer::new(keys.clone()).trusting(keys.public());
568
569 assert_eq!(signer.trusted_ids().len(), 1);
570 }
571
572 #[test]
573 fn a_signature_cannot_be_replayed_under_another_key_id() {
574 let old = SigningKeyPair::generate();
575 let current = SigningKeyPair::generate();
576 let signer = Ed25519Signer::new(current).trusting(old.public());
577
578 let signed = signer.sign("value").unwrap();
579 let mut parts: Vec<&str> = signed.split('.').collect();
580 parts[1] = old.id();
581
582 assert!(signer.verify(&parts.join(".")).is_err(), "the id is inside the signature");
583 }
584
585 #[test]
586 fn a_malformed_ed25519_signature_is_a_400() {
587 let signer = Ed25519Signer::new(SigningKeyPair::generate());
588
589 for bad in ["", "nope", "a.b", "a.b.c.d", "value.deadbeef.!!!"] {
590 assert_eq!(signer.verify(bad).unwrap_err().status(), 400, "{bad}");
591 }
592 }
593
594 #[test]
595 fn a_value_containing_a_dot_is_refused() {
596 let signer = Ed25519Signer::new(SigningKeyPair::generate());
597 assert!(signer.sign("a.b").unwrap_err().message().contains("encode it first"));
598 }
599
600 #[test]
601 fn is_valid_reaches_ed25519_too() {
602 let signer = Ed25519Signer::new(SigningKeyPair::generate());
603 assert!(signer.is_valid(&signer.sign("x").unwrap()));
604 }
605
606 #[test]
607 fn a_signing_keypair_round_trips_through_base64() {
608 let keys = SigningKeyPair::generate();
609 let restored = SigningKeyPair::from_base64(&keys.to_base64()).unwrap();
610
611 assert_eq!(restored.id(), keys.id());
612 assert_eq!(
613 Ed25519Signer::verify_only(keys.public())
614 .verify(&Ed25519Signer::new(restored).sign("x").unwrap())
615 .unwrap(),
616 "x"
617 );
618 }
619
620 #[test]
621 fn a_public_key_round_trips_through_base64() {
622 let public = SigningKeyPair::generate().public();
623 let restored = VerifyingPublicKey::from_base64(&public.to_base64()).unwrap();
624
625 assert_eq!(restored.id(), public.id());
626 }
627
628 #[test]
629 fn rubbish_public_keys_are_rejected() {
630 assert!(VerifyingPublicKey::from_base64("not base64!!").is_err());
631 assert!(VerifyingPublicKey::from_base64(&BASE64.encode([0u8; 16])).is_err());
632 }
633
634 #[test]
635 fn ed25519_debug_does_not_disclose_the_secret() {
636 let keys = SigningKeyPair::generate();
637 let rendered = format!("{keys:?}");
638
639 assert!(rendered.contains("redacted"));
640 assert!(!rendered.contains(&BASE64.encode(keys.signing.to_bytes())), "{rendered}");
641 }
642
643 #[test]
646 fn a_sealed_box_round_trips() {
647 let recipient = BoxKeyPair::generate();
648 let sealed = SealedBox::new().seal_string(&recipient.public(), "a secret").unwrap();
649
650 assert!(!sealed.contains("secret"), "{sealed}");
651 assert_eq!(SealedBox::new().unseal_string(&recipient, &sealed).unwrap(), "a secret");
652 }
653
654 #[test]
655 fn only_the_recipient_can_open_it() {
656 let recipient = BoxKeyPair::generate();
657 let someone_else = BoxKeyPair::generate();
658 let sealed = SealedBox::new().seal(&recipient.public(), b"secret").unwrap();
659
660 assert!(SealedBox::new().unseal(&someone_else, &sealed).is_err());
661 }
662
663 #[test]
664 fn the_sender_cannot_reopen_it() {
665 let recipient = BoxKeyPair::generate();
668 let sealed = SealedBox::new().seal(&recipient.public(), b"secret").unwrap();
669
670 assert_eq!(sealed.matches('.').count(), 5, "tag.alg.kid.ephemeral.nonce.ct");
673 }
674
675 #[test]
676 fn sealing_the_same_value_twice_produces_different_payloads() {
677 let recipient = BoxKeyPair::generate();
678 let seal = SealedBox::new();
679
680 assert_ne!(
681 seal.seal(&recipient.public(), b"same").unwrap(),
682 seal.seal(&recipient.public(), b"same").unwrap(),
683 "a fresh ephemeral key and nonce per message"
684 );
685 }
686
687 #[test]
688 fn re_addressing_a_sealed_box_is_detected() {
689 let recipient = BoxKeyPair::generate();
690 let other = BoxKeyPair::generate();
691 let sealed = SealedBox::new().seal(&recipient.public(), b"secret").unwrap();
692
693 let mut parts: Vec<&str> = sealed.split('.').collect();
694 parts[2] = other.id();
695
696 assert!(SealedBox::new().unseal(&other, &parts.join(".")).is_err());
697 }
698
699 #[test]
700 fn swapping_the_ephemeral_key_is_detected() {
701 let recipient = BoxKeyPair::generate();
702 let sealed = SealedBox::new().seal(&recipient.public(), b"secret").unwrap();
703
704 let attacker = BoxKeyPair::generate();
705 let swapped = B64.encode(attacker.public().public.as_bytes());
706 let mut parts: Vec<&str> = sealed.split('.').collect();
707 parts[3] = &swapped;
708
709 assert!(SealedBox::new().unseal(&recipient, &parts.join(".")).is_err());
710 }
711
712 #[test]
713 fn tampering_with_a_sealed_box_is_detected() {
714 let recipient = BoxKeyPair::generate();
715 let sealed = SealedBox::new().seal(&recipient.public(), b"transfer 10").unwrap();
716
717 let mut parts: Vec<&str> = sealed.split('.').collect();
718 let mut bytes = B64.decode(parts[5]).unwrap();
719 bytes[0] ^= 0x01;
720 let flipped = B64.encode(bytes);
721 parts[5] = &flipped;
722
723 assert!(SealedBox::new().unseal(&recipient, &parts.join(".")).is_err());
724 }
725
726 #[test]
727 fn a_malformed_sealed_box_is_a_400() {
728 let recipient = BoxKeyPair::generate();
729
730 for bad in ["", "nope", "seal1.xc20p.abc", "notseal.xc20p.a.b.c.d"] {
731 assert_eq!(
732 SealedBox::new().unseal(&recipient, bad).unwrap_err().status(),
733 400,
734 "{bad}"
735 );
736 }
737 }
738
739 #[test]
740 fn every_cipher_works_in_a_sealed_box() {
741 let recipient = BoxKeyPair::generate();
742
743 for cipher in Cipher::ALL {
744 let seal = SealedBox::new().with_cipher(cipher);
745 let sealed = seal.seal(&recipient.public(), b"secret").unwrap();
746
747 assert_eq!(seal.unseal(&recipient, &sealed).unwrap(), b"secret", "{cipher}");
748 assert_eq!(SealedBox::new().unseal(&recipient, &sealed).unwrap(), b"secret");
751 }
752 }
753
754 #[test]
755 fn empty_and_large_payloads_round_trip() {
756 let recipient = BoxKeyPair::generate();
757 let seal = SealedBox::new();
758 let big = vec![3u8; 100_000];
759
760 assert!(seal
761 .unseal(&recipient, &seal.seal(&recipient.public(), b"").unwrap())
762 .unwrap()
763 .is_empty());
764 assert_eq!(
765 seal.unseal(&recipient, &seal.seal(&recipient.public(), &big).unwrap()).unwrap(),
766 big
767 );
768 }
769
770 #[test]
773 fn both_sides_agree_on_the_same_key() {
774 let alice = BoxKeyPair::generate();
775 let bob = BoxKeyPair::generate();
776
777 assert_eq!(
778 alice.agree(&bob.public()).bytes(),
779 bob.agree(&alice.public()).bytes(),
780 "that is what Diffie-Hellman is for"
781 );
782 }
783
784 #[test]
785 fn a_third_party_agrees_on_something_else() {
786 let alice = BoxKeyPair::generate();
787 let bob = BoxKeyPair::generate();
788 let eve = BoxKeyPair::generate();
789
790 assert_ne!(alice.agree(&bob.public()).bytes(), alice.agree(&eve.public()).bytes());
791 assert_ne!(alice.agree(&bob.public()).bytes(), eve.agree(&bob.public()).bytes());
792 }
793
794 #[test]
795 fn an_agreed_key_encrypts_between_the_two_parties() {
796 let alice = BoxKeyPair::generate();
797 let bob = BoxKeyPair::generate();
798 let cipher = Cipher::default();
799
800 let key = alice.agree(&bob.public());
801 let nonce = cipher.nonce();
802 let sealed = cipher.encrypt(&key, &nonce, b"", b"between us").unwrap();
803
804 let theirs = bob.agree(&alice.public());
805 assert_eq!(cipher.decrypt(&theirs, &nonce, b"", &sealed).unwrap(), b"between us");
806 }
807
808 #[test]
809 fn an_agreed_key_is_not_the_raw_diffie_hellman_output() {
810 let alice = BoxKeyPair::generate();
813 let bob = BoxKeyPair::generate();
814
815 let raw = alice.secret.diffie_hellman(&bob.public().public);
816 assert_ne!(alice.agree(&bob.public()).bytes(), raw.as_bytes());
817 }
818
819 #[test]
820 fn a_box_keypair_round_trips_through_base64() {
821 let keys = BoxKeyPair::generate();
822 let restored = BoxKeyPair::from_base64(&keys.to_base64()).unwrap();
823
824 assert_eq!(restored.id(), keys.id());
825 assert_eq!(restored.public(), keys.public());
826 }
827
828 #[test]
829 fn a_box_public_key_round_trips_through_base64() {
830 let public = BoxKeyPair::generate().public();
831 assert_eq!(BoxPublicKey::from_base64(&public.to_base64()).unwrap(), public);
832 }
833
834 #[test]
835 fn box_debug_does_not_disclose_the_secret() {
836 let keys = BoxKeyPair::generate();
837 let rendered = format!("{keys:?}");
838
839 assert!(rendered.contains("redacted"));
840 assert!(!rendered.contains(&BASE64.encode(keys.secret.to_bytes())), "{rendered}");
841 }
842
843 #[test]
844 fn key_ids_are_domain_separated_between_the_two_schemes() {
845 let bytes = [5u8; 32];
848 assert_ne!(SigningKeyPair::from_seed(bytes).id(), BoxKeyPair::from_bytes(bytes).id());
849 }
850}