Skip to main content

pubhubs/common/
kem.rs

1//! Hybrid post-quantum [key encapsulation
2//! mechanism](https://en.wikipedia.org/wiki/Key_encapsulation_mechanism)
3//! (KEM) based on ML-KEM and ristretto25519.
4//!
5//! The [`rustls`] package already uses such a hybrid kem (X25519MLKEM768) for PQ KX, but does not
6//! expose this to us.  So instead of adding an additional package to pull in X25519MLKEM768,
7//! we implement our own hybrid KEM using the implementations of ML-KEM and ristretto25519
8//! already in our dependency tree.
9//!
10//! # Usage
11//!
12//! To establish a [`SharedSecret`] between, say, Alice and Bob, Alice first generates
13//! a **decapsulation key** using [`DecapKey::generate`].  She keeps this key private, but sends
14//! the associated **encapsulation key** (obtained via [`DecapKey::encap_key`]) to Bob.
15//!
16//! Using this [`EncapKey`], Bob can generate a [`CiphertextBytes`]-[`SharedSecret`] pair using
17//! [`EncapKey::encap`] **encapsulating** the shared secret into the **ciphertext**.
18//!
19//! The shared secret can be retrieved, or **decapsulated**, from the ciphertext using Alice's
20//! decapsulation key, via [`DecapKey::decap`].
21
22use aws_lc_rs::kem as mlkem;
23use curve25519_dalek::ristretto::CompressedRistretto;
24
25use crate::common::elgamal::{self, Encoding as _};
26use crate::common::secret;
27use crate::misc::error::{OPAQUE, Opaque};
28use crate::misc::serde_ext::bytes_wrapper::B64;
29use crate::phcrypto;
30
31/// Decapsulation key, the 'private key' of the KEM.  Generate using [`DecapKey::generate`].
32#[derive(Debug)]
33pub struct DecapKey {
34    ml: mlkem::DecapsulationKey,
35
36    /// Cached ML-KEM encapsulation key.  
37    ///
38    /// The [`mlkem::DecapsulationKey`] type is a bit weird: it either contains just the
39    /// decapsulation key (when read from bytes via [`mlkem::DecapsulationKey::new`] *or*
40    /// it contains both an encapsulation and decapsulation key (when generated via
41    /// [`mlkem::DecapsulationKey::generate`].)
42    ///
43    /// So to deal with the former case, we include the encapsulation key here as well.
44    ml_ek: mlkem::EncapsulationKey,
45    ec: elgamal::PrivateKey,
46}
47
48/// Encapsulation key, the 'public key' of the KEM.  Can be obtained via [`DecapKey::encap_key`].
49#[derive(Debug)]
50pub struct EncapKey {
51    ml: mlkem::EncapsulationKey,
52    ec: elgamal::PublicKey,
53}
54
55/// Encoded form of [`DecapKey`].
56#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, zeroize::ZeroizeOnDrop)]
57pub struct DecapKeyBytes {
58    ml: B64,
59
60    /// ML-KEM encapsulation-key bytes; see [`DecapKey::ml_ek`].
61    ml_ek: B64,
62
63    /// Scalar bytes.
64    ec: B64,
65}
66
67/// Encoded form of [`EncapKey`].
68#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
69pub struct EncapKeyBytes {
70    pub ml: B64,
71
72    /// [`CompressedRistretto`] bytes.
73    pub ec: B64,
74}
75
76/// Encrypted [`SharedSecret`].  Create using [`EncapKey::encap`] and
77/// decrypt using [`DecapKey::decap`].
78#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
79pub struct CiphertextBytes {
80    pub ml: B64,
81
82    /// [`CompressedRistretto`] bytes.
83    pub ec: B64,
84}
85
86/// Holds shared secrets generated by this KEM.
87#[derive(Clone, Debug, zeroize::ZeroizeOnDrop)]
88pub struct SharedSecret {
89    inner: [u8; 32],
90}
91
92impl DecapKey {
93    /// Generates a [`DecapKey`].  Expensive.
94    pub fn generate() -> Result<Self, Opaque> {
95        let ml = mlkem::DecapsulationKey::generate(&mlkem::ML_KEM_768)?;
96        Ok(Self {
97            ml_ek: ml.encapsulation_key()?,
98            ml,
99            ec: elgamal::PrivateKey::random(),
100        })
101    }
102
103    /// Returns the associated [`EncapKey`].  Deterministic and cheap.
104    pub fn encap_key(&self) -> EncapKey {
105        // Getting the encapsulation_key via self.ml.encapsulation_key is fastest,
106        // but it might not be there.
107        let ml = self.ml.encapsulation_key().unwrap_or_else(|_| {
108            mlkem::EncapsulationKey::new(
109                &mlkem::ML_KEM_768,
110                self.ml_ek
111                    .key_bytes()
112                    .expect("failed to retrieve encapsulation key bytes")
113                    .as_ref(),
114            )
115            .expect("cached invalid encapsulation key")
116        });
117        EncapKey {
118            ml,
119            ec: self.ec.public_key().clone(),
120        }
121    }
122
123    /// Decapsulates ciphertext, recovering the [`SharedSecret`] within.  Expensive.
124    pub fn decap(&self, ct: &CiphertextBytes) -> Result<SharedSecret, Opaque> {
125        let ss_ml = self.ml.decapsulate(mlkem::Ciphertext::from(&ct.ml[..]))?;
126
127        let ec_bytes: [u8; 32] = (&ct.ec[..]).try_into()?;
128        let pk =
129            elgamal::PublicKey::try_from(CompressedRistretto(ec_bytes)).map_err(|()| OPAQUE)?;
130        let ss_ec = self.ec.shared_secret(&pk);
131
132        Ok(phcrypto::kem_shared_secret(&ss_ml, &ss_ec))
133    }
134
135    /// Encodes for storage.
136    pub fn encode(&self) -> Result<DecapKeyBytes, Opaque> {
137        Ok(DecapKeyBytes {
138            ml: B64::from_bytes(self.ml.key_bytes()?.as_ref()),
139            ml_ek: B64::from_bytes(self.ml_ek.key_bytes()?.as_ref()),
140            ec: B64::from_bytes(self.ec.as_scalar().to_bytes()),
141        })
142    }
143}
144
145impl DecapKeyBytes {
146    /// Decodes into [`DecapKey`].  Expensive.
147    pub fn decode(&self) -> Result<DecapKey, Opaque> {
148        Ok(DecapKey {
149            ml: mlkem::DecapsulationKey::new(&mlkem::ML_KEM_768, &self.ml[..])?,
150            ml_ek: mlkem::EncapsulationKey::new(&mlkem::ML_KEM_768, &self.ml_ek[..])?,
151            ec: elgamal::PrivateKey::from_slice(&self.ec[..]).ok_or(OPAQUE)?,
152        })
153    }
154}
155
156impl EncapKey {
157    /// Creates a [`SharedSecret`] and a [`CiphertextBytes`] encapsulating it.  Expensive.
158    pub fn encap(&self) -> Result<(CiphertextBytes, SharedSecret), Opaque> {
159        let (ct_ml, ss_ml) = self.ml.encapsulate()?;
160
161        let ephemeral_sk = elgamal::PrivateKey::random();
162        let ss_ec = ephemeral_sk.shared_secret(&self.ec);
163
164        let ct = CiphertextBytes {
165            ml: B64::from_bytes(ct_ml),
166            ec: B64::from_bytes(ephemeral_sk.public_key().to_bytes()),
167        };
168        Ok((ct, phcrypto::kem_shared_secret(&ss_ml, &ss_ec)))
169    }
170
171    /// Encodes this key.  Cheap.
172    pub fn encode(&self) -> Result<EncapKeyBytes, Opaque> {
173        Ok(EncapKeyBytes {
174            ml: B64::from_bytes(self.ml.key_bytes()?.as_ref()),
175            ec: B64::from_bytes(self.ec.to_bytes()),
176        })
177    }
178}
179
180impl EncapKeyBytes {
181    /// Decodes into [`EncapKey`].
182    pub fn decode(&self) -> Result<EncapKey, Opaque> {
183        Ok(EncapKey {
184            ml: mlkem::EncapsulationKey::new(&mlkem::ML_KEM_768, &self.ml[..])?,
185            ec: {
186                let bytes: [u8; 32] = (&self.ec[..]).try_into()?;
187                elgamal::PublicKey::try_from(CompressedRistretto(bytes)).map_err(|()| OPAQUE)?
188            },
189        })
190    }
191
192    pub fn id(&self) -> crate::id::Id {
193        phcrypto::encap_key_id(self)
194    }
195}
196
197impl From<[u8; 32]> for SharedSecret {
198    fn from(inner: [u8; 32]) -> Self {
199        Self { inner }
200    }
201}
202
203impl SharedSecret {
204    pub fn random() -> Self {
205        crate::misc::crypto::random_32_bytes().into()
206    }
207}
208
209impl secret::DigestibleSecret for SharedSecret {
210    fn as_bytes(&self) -> &[u8] {
211        &self.inner
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::common::secret::DigestibleSecret as _;
219
220    #[test]
221    fn hybrid_roundtrip() {
222        let dk = DecapKey::generate().unwrap();
223        let ek = dk.encap_key();
224
225        let (ct, ss_sender) = ek.encap().unwrap();
226        let ss_recipient = dk.decap(&ct).unwrap();
227
228        assert_eq!(ss_sender.as_bytes(), ss_recipient.as_bytes());
229    }
230
231    #[test]
232    fn encode_decode_roundtrip() {
233        let dk = DecapKey::generate().unwrap();
234
235        // Decapsulation key survives an encode/decode trip (config persistence).
236        let dk2 = dk.encode().unwrap().decode().unwrap();
237        // Encapsulation key likewise (wire), and both keys still agree on a fresh secret.
238        let ek = dk.encap_key().encode().unwrap().decode().unwrap();
239
240        let (ct, ss_sender) = ek.encap().unwrap();
241        assert_eq!(ss_sender.as_bytes(), dk2.decap(&ct).unwrap().as_bytes());
242    }
243
244    #[test]
245    fn bytes_serde_roundtrip() {
246        let dk = DecapKey::generate().unwrap();
247        let ek_bytes = dk.encap_key().encode().unwrap();
248
249        let json = serde_json::to_string(&ek_bytes).unwrap();
250        let ek_bytes2: EncapKeyBytes = serde_json::from_str(&json).unwrap();
251
252        assert_eq!(ek_bytes, ek_bytes2);
253    }
254
255    #[test]
256    fn decap_key_bytes_serde_roundtrip() {
257        let dk_bytes = DecapKey::generate().unwrap().encode().unwrap();
258        let json = serde_json::to_string(&dk_bytes).unwrap();
259        // DecapKeyBytes isn't PartialEq, so round-trip via re-serialize.
260        let dk_bytes2: DecapKeyBytes = serde_json::from_str(&json).unwrap();
261        assert_eq!(serde_json::to_string(&dk_bytes2).unwrap(), json);
262    }
263
264    #[test]
265    fn ciphertext_bytes_serde_roundtrip() {
266        let dk = DecapKey::generate().unwrap();
267        let (ct, _) = dk.encap_key().encap().unwrap();
268        let json = serde_json::to_string(&ct).unwrap();
269        let ct2: CiphertextBytes = serde_json::from_str(&json).unwrap();
270        assert_eq!(ct, ct2);
271    }
272
273    /// Pins encoding.  (See [`print_test_vectors`] for the source.)
274    #[test]
275    fn pinned_vectors() {
276        use base64ct::Encoding as _;
277
278        const DK_JSON: &str = r#"{"ml":"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","ml_ek":"v+TH0Iu93tK3KdRKgoPCDLVZR2Udv6kI8tEGY9c6YmmL8vWxssN2RHdOQdkRADxy0WqE5ol+k0o7x4o8dyCN7uExNOxsoXCc0rteLEQgONE0qtiF4RcVYAihYclmv7F+XgdmMpjDobhSqRgS4NgvhzFMLFUSI7OEoxKJndiS59mRnCckuYzJDncQ0ijCrSF3cMFDgshEyjhon1QTkRu4Y3w1dHO44oOFH9GFm9Fl7SgcsaIOYaGqrXGZLsjJ3tUPwiWiEQZ3quWvVmxDfRmnl6Rb+zy9ZcABJOtGTwomEMQyXBAfT6UU58xftLM37kgyQzVmBtFQ4hx1MGpcibm2hbGNzaG6gbq0EYYRDpiMdwxQ4YgJ7Psxy+yFgGhoFCRei3M/LbGA56fD34hmzPkOhOO5h/AfGCfCkvklT5YLLki2OCUjwyJzuoPKC0lNdvU8peGbzjTLS4IokipdaEHLP2ZRl7EVIqC0Rzt5g+qvQBvHIDM0qUuUO/CXpay5rDkfUMrDeYGE2Xd3fIprEHqv7oNF8xtgVJwKZXQXBcAjJhd46VTGlwgPH8qrzNZUiHRaV4AtHgluw8UdTai1vui0EVBIFSlpiBmc75F0NnUDisbGMUIryOUdVFULOjVst6xJ6VYRYhdkIHt/JVksb1LNHowvugwaT3Nah9G+gMDOmncFmkV363supwMFoMcb/zy/ZMBZYzkJtoMSbmSVaVgOA3iulSB3WpkxRYuSmZwIOJhv4nuTzCtFRpaGzxSacwbLsLFY19povwAcuhge5CpN1vVtL5kbIquCFQI9WCdHYECE2TxAwXh4UWFxy5CfbkmSroFyRRVhnjGuKZDME0ltBRJoD2LP0taEftBKCgRa/UZZr3RUkPcVZ/evG5I2unIv69CZ0GHKBRCY0omzxmF6hqCneeYFaGQpfhMpJQeFj/V+UdUv2VfMUqMFYsVfW4tP2qFGAqV4t3YFnhRLWfzPjAc6FvKT0aU1qjQp3aC72vFppiyyAcoQADcx7xmLy4XB03GQyCCyVPRjVwK1rJpzfJB0i9zOzyQVe7ks2buXeCauoZVyEcgETuFi6sFd5SLE7nXDtYUo3FCTzDy/r5HLAecQjUF7h8qkzSFauWfDmVmLjjxa04GEJHdWSAiZTWg4OUVY3KdTyxMMR6tF9ghfcYslFru3KzQTtiibkqtmlGacCoVDbzefjLG7/naGbZidggck9lJd4sTCbBqIy2y+7KM2edgu3MA6qXBt/8Q7dbWho7Z3sGBVexeTIYiWRDZAfsUEHyXKWkhLOZo/zZOIfNiLvzBWhBCSGQJ9F4MLmeB0aew7+gixHWKIn3iNoIJ/m5elaUJenbIn8/abeMpyJXCL0lofrOxoMAwpg3WzLPuaxDadR1J/v5dGATGcq3UfoIOv3NaDzpmnrqsvI4CHwJF5mbRBj+Q8m3g40iG72zinJDFs0gR9yXclnbvGe/GFNOUI+eI/VpOctsEOC3pACWgoQQxePyRRlvF/jxWdPKRJd0WcBbQZ/IBsvnaEQ6ByY5dhYGdHIFGkl/LiwDoZhXxkP5QpmcPFg758Q0uQ7Z0=","ec":"u+coWbaISlx2hr7pA5LmWPGg6FZC8pIX+kOC+v/MGgA="}"#;
279        const EK_JSON: &str = r#"{"ml":"v+TH0Iu93tK3KdRKgoPCDLVZR2Udv6kI8tEGY9c6YmmL8vWxssN2RHdOQdkRADxy0WqE5ol+k0o7x4o8dyCN7uExNOxsoXCc0rteLEQgONE0qtiF4RcVYAihYclmv7F+XgdmMpjDobhSqRgS4NgvhzFMLFUSI7OEoxKJndiS59mRnCckuYzJDncQ0ijCrSF3cMFDgshEyjhon1QTkRu4Y3w1dHO44oOFH9GFm9Fl7SgcsaIOYaGqrXGZLsjJ3tUPwiWiEQZ3quWvVmxDfRmnl6Rb+zy9ZcABJOtGTwomEMQyXBAfT6UU58xftLM37kgyQzVmBtFQ4hx1MGpcibm2hbGNzaG6gbq0EYYRDpiMdwxQ4YgJ7Psxy+yFgGhoFCRei3M/LbGA56fD34hmzPkOhOO5h/AfGCfCkvklT5YLLki2OCUjwyJzuoPKC0lNdvU8peGbzjTLS4IokipdaEHLP2ZRl7EVIqC0Rzt5g+qvQBvHIDM0qUuUO/CXpay5rDkfUMrDeYGE2Xd3fIprEHqv7oNF8xtgVJwKZXQXBcAjJhd46VTGlwgPH8qrzNZUiHRaV4AtHgluw8UdTai1vui0EVBIFSlpiBmc75F0NnUDisbGMUIryOUdVFULOjVst6xJ6VYRYhdkIHt/JVksb1LNHowvugwaT3Nah9G+gMDOmncFmkV363supwMFoMcb/zy/ZMBZYzkJtoMSbmSVaVgOA3iulSB3WpkxRYuSmZwIOJhv4nuTzCtFRpaGzxSacwbLsLFY19povwAcuhge5CpN1vVtL5kbIquCFQI9WCdHYECE2TxAwXh4UWFxy5CfbkmSroFyRRVhnjGuKZDME0ltBRJoD2LP0taEftBKCgRa/UZZr3RUkPcVZ/evG5I2unIv69CZ0GHKBRCY0omzxmF6hqCneeYFaGQpfhMpJQeFj/V+UdUv2VfMUqMFYsVfW4tP2qFGAqV4t3YFnhRLWfzPjAc6FvKT0aU1qjQp3aC72vFppiyyAcoQADcx7xmLy4XB03GQyCCyVPRjVwK1rJpzfJB0i9zOzyQVe7ks2buXeCauoZVyEcgETuFi6sFd5SLE7nXDtYUo3FCTzDy/r5HLAecQjUF7h8qkzSFauWfDmVmLjjxa04GEJHdWSAiZTWg4OUVY3KdTyxMMR6tF9ghfcYslFru3KzQTtiibkqtmlGacCoVDbzefjLG7/naGbZidggck9lJd4sTCbBqIy2y+7KM2edgu3MA6qXBt/8Q7dbWho7Z3sGBVexeTIYiWRDZAfsUEHyXKWkhLOZo/zZOIfNiLvzBWhBCSGQJ9F4MLmeB0aew7+gixHWKIn3iNoIJ/m5elaUJenbIn8/abeMpyJXCL0lofrOxoMAwpg3WzLPuaxDadR1J/v5dGATGcq3UfoIOv3NaDzpmnrqsvI4CHwJF5mbRBj+Q8m3g40iG72zinJDFs0gR9yXclnbvGe/GFNOUI+eI/VpOctsEOC3pACWgoQQxePyRRlvF/jxWdPKRJd0WcBbQZ/IBsvnaEQ6ByY5dhYGdHIFGkl/LiwDoZhXxkP5QpmcPFg758Q0uQ7Z0=","ec":"oh0u4IZUZXQvAbdr1492EdP0RWVFocTi7vPDCFcLsUc="}"#;
280        const CT_JSON: &str = r#"{"ml":"9Wh+jj9EQPYpmg/f3IIDVSt+FzoX2hjlapIFruvffN4Rg/nlQ0tNg286lhA+nvhNX+uavGoFuavTsxThFR0yM8NqALXhXEc3pDjtoFcEzjV+utV/zZ+ifDJ1kF06gtirHdBfOCQZri8m9nJM4/X5Ig1tI/4wdX8c3Jpu3trfnD1en/A/xZ8Do3XTaNK5XJjRY48OU/mk10p0nmvzu5hjL4JeMYSlzFnJWa3/tJTJGLiU9pnkoENss/VEVuDXCY3H2M0/ITt87eTTCW4D0gKrqAyAvxR1URn+5U0Vf68bN8Cz1rs4ypSFnXs2chxh6we7aGBi3e3661aPdaVfXl7mGaQ0KNZGtUEpIUAO9j4FVB5Hj4vQXXB41FeNsvRRWl5CuRQKBgiPvqdfaZPcXT7SizyFuIRZwHOOX1liLiKp/GIXBe4IaR2HHDXpunf07+FDs95QWvyspIAIiwYsLB8w6TNVkasMibC0cTeX0fQWXy0FJ2ZaZ/gBbBZeorYsn0pn6sCr4JTqHBWlyoxk89kk//7CGU1TB7m2RP+xf/+1/Sff0abb2oqDgIeyLF8AP9obeHdry7Yq7SeqUbvp8Vro/P8zkUpUIHFscOOXdNZu+3ZZstxEhDeRIRde+aUYvAwWsyl7cHp966/fGeGguUT1fdEPOCJj3yq7YkOX8Yw69+dtXLU5q4z0RtKpwDyUdvIM3B+p400bh1//BPQ1NLi9YEbRT7FSXivP1QWLGEB0PSo8koeCMpujBx3LafhSV3VQf6G27TvkxeZJ9XC5d8WkkEE6gq6bFWvp26Z0aeug/RY1Q1v9s4caSvzIE+AERJ88mVI8Uv+zbP2UGpYDquhQIglYFUu13/XshBtgkmBT5fw6Cg01dzc+oBwySz/f3FCGU/wCutFvMJFFafJsLSS9iKuC0/zbnH3tB2NF3Atlfq4heHma5mxlo2Wn2pxv/k57fYVw5FF165j4LZnOCUoBwCWz59vsvpfqHI3CNi9BhvehQIlxwyn/vYFcD+VktIJtdTHLfPEXxE5q7IiK6XzQaaa8hCJCeJMjyHbVlT1S225J/e4hy+FDrA3eJPx19e237uqW++LOJSl8SuYMHyKyxrPyQSt8BJKQ801OYRxRX5a1HztL0stF1D2g83puFKBeeWl7S1aOAVneJMZeQyfZQ6yrOjQU9qzdBn3iEwp8XUHcHcak1Bv2+5rU4cfcTUg7QJaHOW5fmEMAo04LO8iSuXRUxzMjAx4ZS9QMey9ojVjzDMHYS5Ma9+3QAtGl1cIDuQpFX9iSnaIFi6DIdM4+E9tOdMPzToGMIDKTR/h9r5wru/6lz+VrpLpeBtyvAJ2gS9e81QP7uqRc5PPDd09pJ5S2d5xO8lEPet1GPRJnUi/wcMXAIYyLHvUteOl39yNorTM4g74+XtnLAkBJGxKZ81o7YoVYAfsr0JWFZniIDGk=","ec":"3H9cUtMTfKCSXae/d2Xb7NYCSeUdUBuIarhJAKb95VM="}"#;
281        const SS_B64: &str = "4bpge1ouyjzuygcFzRK62n18Pz8Jidt+Olcpq/02d34=";
282
283        let dk_bytes: DecapKeyBytes = serde_json::from_str(DK_JSON).unwrap();
284        let ek_bytes: EncapKeyBytes = serde_json::from_str(EK_JSON).unwrap();
285        let ct: CiphertextBytes = serde_json::from_str(CT_JSON).unwrap();
286        let ss_expected = base64ct::Base64::decode_vec(SS_B64).unwrap();
287
288        // Serde format is stable: parsed values re-serialize to the captured JSON.
289        assert_eq!(serde_json::to_string(&dk_bytes).unwrap(), DK_JSON);
290        assert_eq!(serde_json::to_string(&ek_bytes).unwrap(), EK_JSON);
291        assert_eq!(serde_json::to_string(&ct).unwrap(), CT_JSON);
292
293        // Encapsulation key derived from the decoded DK matches the captured EK.
294        let dk = dk_bytes.decode().unwrap();
295        assert_eq!(dk.encap_key().encode().unwrap(), ek_bytes);
296
297        // Decap of the captured ciphertext yields the captured shared secret.
298        assert_eq!(dk.decap(&ct).unwrap().as_bytes(), ss_expected.as_slice());
299    }
300
301    /// Prints fresh values to regenerate [`pinned_vectors`].  Run with
302    /// `cargo test --lib common::kem::tests::print_test_vectors -- --ignored --nocapture`.
303    #[test]
304    #[ignore]
305    fn print_test_vectors() {
306        use base64ct::Encoding as _;
307        let dk = DecapKey::generate().unwrap();
308        let dk_bytes = dk.encode().unwrap();
309        let ek_bytes = dk.encap_key().encode().unwrap();
310        let (ct, ss) = dk.encap_key().encap().unwrap();
311
312        println!("DK_JSON: {}", serde_json::to_string(&dk_bytes).unwrap());
313        println!("EK_JSON: {}", serde_json::to_string(&ek_bytes).unwrap());
314        println!("CT_JSON: {}", serde_json::to_string(&ct).unwrap());
315        println!(
316            "SS_B64:  {}",
317            base64ct::Base64::encode_string(ss.as_bytes())
318        );
319    }
320
321    #[test]
322    fn distinct_keys_disagree() {
323        let dk = DecapKey::generate().unwrap();
324        let other = DecapKey::generate().unwrap();
325
326        let (ct, ss_sender) = dk.encap_key().encap().unwrap();
327        // Decapsulating with the wrong key yields a different secret, never an error.
328        assert_ne!(ss_sender.as_bytes(), other.decap(&ct).unwrap().as_bytes());
329    }
330}