Skip to main content

rainier_crypt/
asymmetric.rs

1//! Public-key cryptography: Ed25519 signatures and X25519 sealed boxes.
2//!
3//! The difference from the symmetric side is who needs which secret.
4//!
5//! | | Writer needs | Reader needs |
6//! |---|---|---|
7//! | [`HmacSigner`](crate::HmacSigner) | the shared key | **the shared key** |
8//! | [`Ed25519Signer`] | the signing key | the **public** key |
9//! | [`AeadEncrypter`](crate::AeadEncrypter) | the shared key | **the shared key** |
10//! | [`SealedBox`] | the **public** key | the secret key |
11//!
12//! That asymmetry is the whole reason to reach for these. A shared key means
13//! everyone who can verify can also forge, and everyone who can decrypt can
14//! also encrypt. Sometimes that is fine — a cookie you issue and you read —
15//! and sometimes it is exactly wrong.
16
17use 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
29/// The sealed-box format tag.
30const SEAL: &str = "seal1";
31
32/// A short, stable identifier for a public key.
33///
34/// Derived from the key rather than configured, for the same reason a
35/// [symmetric key id](crate::Key::id) is: it cannot be set inconsistently and
36/// cannot be forgotten.
37fn 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// --- Ed25519 ---------------------------------------------------------------
45
46/// An Ed25519 keypair: signs, and verifies its own signatures.
47#[derive(Clone)]
48pub struct SigningKeyPair {
49    signing: SigningKey,
50    id: String,
51}
52
53impl SigningKeyPair {
54    /// A fresh keypair from the OS CSPRNG.
55    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    /// A keypair from a 32-byte seed.
64    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    /// A keypair from a base64 seed, with or without a `base64:` prefix.
71    pub fn from_base64(encoded: &str) -> Result<Self> {
72        Ok(Self::from_seed(decode_32(encoded, "signing key")?))
73    }
74
75    /// The seed, as `base64:…`. **This is the secret half.**
76    pub fn to_base64(&self) -> String {
77        format!("base64:{}", BASE64.encode(self.signing.to_bytes()))
78    }
79
80    /// The public half, which is safe to publish.
81    pub fn public(&self) -> VerifyingPublicKey {
82        VerifyingPublicKey { verifying: self.signing.verifying_key(), id: self.id.clone() }
83    }
84
85    /// The key's short identifier.
86    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/// An Ed25519 public key: verifies, and cannot sign.
98#[derive(Clone)]
99pub struct VerifyingPublicKey {
100    verifying: VerifyingKey,
101    id: String,
102}
103
104impl VerifyingPublicKey {
105    /// A public key from its 32 bytes.
106    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    /// A public key from base64.
114    pub fn from_base64(encoded: &str) -> Result<Self> {
115        Self::from_bytes(decode_32(encoded, "public key")?)
116    }
117
118    /// The key as `base64:…`. Safe to publish.
119    pub fn to_base64(&self) -> String {
120        format!("base64:{}", BASE64.encode(self.verifying.as_bytes()))
121    }
122
123    /// The key's short identifier.
124    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
135/// Ed25519 signing, as a [`Signer`].
136///
137/// Same wire shape as the HMAC signer — `<value>.<key id>.<signature>` — so the
138/// two are interchangeable at a call site. The difference is who can verify: an
139/// HMAC needs the secret, an Ed25519 signature needs only the public key.
140///
141/// ```
142/// # use rainier_crypt::{Ed25519Signer, SigningKeyPair, Signer};
143/// # fn main() -> rainier_support::Result<()> {
144/// let keys = SigningKeyPair::generate();
145/// let signer = Ed25519Signer::new(keys.clone());
146///
147/// let signed = signer.sign("licence-42")?;
148/// assert_eq!(signer.verify(&signed)?, "licence-42");
149///
150/// // A verifier holding only the public key can check it, and cannot sign.
151/// let checker = Ed25519Signer::verify_only(keys.public());
152/// assert_eq!(checker.verify(&signed)?, "licence-42");
153/// assert!(checker.sign("forged").is_err());
154/// # Ok(()) }
155/// ```
156pub struct Ed25519Signer {
157    signing: Option<SigningKeyPair>,
158    trusted: Vec<VerifyingPublicKey>,
159}
160
161impl Ed25519Signer {
162    /// A signer that can both sign and verify.
163    pub fn new(keys: SigningKeyPair) -> Self {
164        let public = keys.public();
165        Self { signing: Some(keys), trusted: vec![public] }
166    }
167
168    /// A verifier that holds no secret and so cannot sign.
169    pub fn verify_only(public: VerifyingPublicKey) -> Self {
170        Self { signing: None, trusted: vec![public] }
171    }
172
173    /// Also trust signatures from this key.
174    ///
175    /// How a rotation works on the verifying side, and how one service accepts
176    /// signatures from several others.
177    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    /// Whether this instance holds a signing key.
185    pub fn can_sign(&self) -> bool {
186        self.signing.is_some()
187    }
188
189    /// Every public key this instance will accept.
190    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        // The key id is signed alongside the value, so a signature cannot be
211        // replayed under a different key's label.
212        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// --- X25519 ----------------------------------------------------------------
252
253/// An X25519 keypair, for key agreement and [sealed boxes](SealedBox).
254#[derive(Clone)]
255pub struct BoxKeyPair {
256    secret: StaticSecret,
257    id: String,
258}
259
260impl BoxKeyPair {
261    /// A fresh keypair from the OS CSPRNG.
262    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    /// A keypair from 32 bytes of secret.
271    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    /// A keypair from base64.
278    pub fn from_base64(encoded: &str) -> Result<Self> {
279        Ok(Self::from_bytes(decode_32(encoded, "box key")?))
280    }
281
282    /// The secret, as `base64:…`. **Keep this out of anything published.**
283    pub fn to_base64(&self) -> String {
284        format!("base64:{}", BASE64.encode(self.secret.to_bytes()))
285    }
286
287    /// The public half.
288    pub fn public(&self) -> BoxPublicKey {
289        BoxPublicKey { public: X25519Public::from(&self.secret), id: self.id.clone() }
290    }
291
292    /// The key's short identifier.
293    pub fn id(&self) -> &str {
294        &self.id
295    }
296
297    /// The shared secret this keypair and `theirs` agree on.
298    ///
299    /// Both sides compute the same value from their own secret and the other's
300    /// public key, without either transmitting a secret. Returned as a
301    /// [`Key`], already run through the KDF, so it is usable with any
302    /// [`Cipher`] rather than being raw Diffie-Hellman output.
303    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/// An X25519 public key. Safe to publish; enough to encrypt *to* the holder.
315#[derive(Clone, PartialEq, Eq)]
316pub struct BoxPublicKey {
317    public: X25519Public,
318    id: String,
319}
320
321impl BoxPublicKey {
322    /// A public key from its 32 bytes.
323    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    /// A public key from base64.
330    pub fn from_base64(encoded: &str) -> Result<Self> {
331        Ok(Self::from_bytes(decode_32(encoded, "public box key")?))
332    }
333
334    /// The key as `base64:…`.
335    pub fn to_base64(&self) -> String {
336        format!("base64:{}", BASE64.encode(self.public.as_bytes()))
337    }
338
339    /// The key's short identifier.
340    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
351/// Turn a Diffie-Hellman result into a usable symmetric key.
352///
353/// Raw X25519 output is not uniformly random and must not be used as a key
354/// directly; HKDF is what makes it one.
355fn 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    // Cannot fail for a 32-byte output from SHA-256.
363    hkdf.expand(b"key", &mut bytes).expect("32 bytes is within HKDF-SHA256's limit");
364    Key::from_bytes(bytes)
365}
366
367/// Anonymous public-key encryption — libsodium's `crypto_box_seal`.
368///
369/// **Anyone** holding a recipient's public key can seal a message to them, and
370/// **only** the recipient can open it. The sender is not authenticated and is
371/// not recoverable: a sealed box says nothing about who wrote it.
372///
373/// That is the right shape for one-directional secrets — a client reporting
374/// something to a server it cannot be given a shared key for, an offline
375/// machine encrypting to a key held elsewhere, a bug report containing a token.
376/// It is the wrong shape if you need to know who sent it; sign the plaintext as
377/// well, or use [`agree`](BoxKeyPair::agree) with both parties' keys.
378///
379/// ```
380/// # use rainier_crypt::{BoxKeyPair, SealedBox};
381/// # fn main() -> rainier_support::Result<()> {
382/// let recipient = BoxKeyPair::generate();
383///
384/// // The sender needs only the public key.
385/// let sealed = SealedBox::new().seal(&recipient.public(), b"a secret report")?;
386///
387/// assert_eq!(SealedBox::new().unseal(&recipient, &sealed)?, b"a secret report");
388/// # Ok(()) }
389/// ```
390#[derive(Debug, Clone, Default)]
391pub struct SealedBox {
392    cipher: Cipher,
393}
394
395impl SealedBox {
396    /// A sealed box using the default cipher.
397    pub fn new() -> Self {
398        Self { cipher: Cipher::default() }
399    }
400
401    /// Use a specific cipher for the payload.
402    pub fn with_cipher(mut self, cipher: Cipher) -> Self {
403        self.cipher = cipher;
404        self
405    }
406
407    /// Encrypt `plain` so only the holder of `recipient`'s secret can read it.
408    pub fn seal(&self, recipient: &BoxPublicKey, plain: &[u8]) -> Result<String> {
409        // A throwaway keypair per message. Its public half travels in the
410        // payload; its secret is dropped immediately, which is what makes the
411        // message unopenable even by the sender afterwards.
412        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        // Both public keys are in the authenticated header, so a payload cannot
419        // be re-addressed to a different recipient or have its ephemeral key
420        // swapped for one the attacker knows.
421        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    /// Encrypt a string.
433    pub fn seal_string(&self, recipient: &BoxPublicKey, plain: &str) -> Result<String> {
434        self.seal(recipient, plain.as_bytes())
435    }
436
437    /// Open a sealed box addressed to `recipient`.
438    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    /// Open a sealed box holding a string.
485    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
491/// Decode 32 bytes of base64, with or without a `base64:` prefix.
492fn 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    // --- Ed25519 -----------------------------------------------------------
511
512    #[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        // The property that distinguishes this from HMAC.
524        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        // Rotation on the verifying side.
555        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    // --- sealed boxes ------------------------------------------------------
644
645    #[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        // The ephemeral secret is dropped, which is the whole point of a
666        // *sealed* box as opposed to an authenticated one.
667        let recipient = BoxKeyPair::generate();
668        let sealed = SealedBox::new().seal(&recipient.public(), b"secret").unwrap();
669
670        // Nothing in the payload lets anyone but the recipient derive the key.
671        // The best a sender can do is keep the plaintext.
672        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            // And a reader with a different default still reads it, because the
749            // payload names its algorithm.
750            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    // --- key agreement -----------------------------------------------------
771
772    #[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        // Raw X25519 output is not uniformly random; using it as a key directly
811        // is the classic mistake. This pins that HKDF ran.
812        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        // The same 32 bytes used as a signing seed and as a box secret must not
846        // produce the same id, or a diagnostic would conflate them.
847        let bytes = [5u8; 32];
848        assert_ne!(SigningKeyPair::from_seed(bytes).id(), BoxKeyPair::from_bytes(bytes).id());
849    }
850}