1use 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#[derive(Debug)]
33pub struct DecapKey {
34 ml: mlkem::DecapsulationKey,
35
36 ml_ek: mlkem::EncapsulationKey,
45 ec: elgamal::PrivateKey,
46}
47
48#[derive(Debug)]
50pub struct EncapKey {
51 ml: mlkem::EncapsulationKey,
52 ec: elgamal::PublicKey,
53}
54
55#[derive(Clone, Debug, serde::Serialize, serde::Deserialize, zeroize::ZeroizeOnDrop)]
57pub struct DecapKeyBytes {
58 ml: B64,
59
60 ml_ek: B64,
62
63 ec: B64,
65}
66
67#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
69pub struct EncapKeyBytes {
70 pub ml: B64,
71
72 pub ec: B64,
74}
75
76#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
79pub struct CiphertextBytes {
80 pub ml: B64,
81
82 pub ec: B64,
84}
85
86#[derive(Clone, Debug, zeroize::ZeroizeOnDrop)]
88pub struct SharedSecret {
89 inner: [u8; 32],
90}
91
92impl DecapKey {
93 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 pub fn encap_key(&self) -> EncapKey {
105 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 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 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 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 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 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 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 let dk2 = dk.encode().unwrap().decode().unwrap();
237 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 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 #[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 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 let dk = dk_bytes.decode().unwrap();
295 assert_eq!(dk.encap_key().encode().unwrap(), ek_bytes);
296
297 assert_eq!(dk.decap(&ct).unwrap().as_bytes(), ss_expected.as_slice());
299 }
300
301 #[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 assert_ne!(ss_sender.as_bytes(), other.decap(&ct).unwrap().as_bytes());
329 }
330}