1use std::fmt;
14
15use aes_gcm::{
16 Aes256Gcm,
17 aead::{Aead, KeyInit},
18};
19use hkdf::Hkdf;
20use k256::{
21 PublicKey, SecretKey,
22 ecdh::{SharedSecret, diffie_hellman},
23 elliptic_curve::sec1::ToEncodedPoint,
24};
25use rand_core::{OsRng, RngCore};
26use serde::{Deserialize, Serialize};
27use sha2::Sha256;
28use thiserror::Error;
29use zeroize::{ZeroizeOnDrop, Zeroizing};
30
31use crate::config::E2eeConfig;
32
33pub const EPHEMERAL_PUBLIC_KEY_LEN: usize = 65;
34pub const NONCE_LEN: usize = 12;
35pub const AES_256_KEY_LEN: usize = 32;
36pub const AES_GCM_TAG_LEN: usize = 16;
37pub const PACKED_PREFIX_LEN: usize = EPHEMERAL_PUBLIC_KEY_LEN + NONCE_LEN;
38pub const MIN_PACKED_PAYLOAD_LEN: usize = PACKED_PREFIX_LEN + AES_GCM_TAG_LEN;
39
40#[derive(Clone, Debug, PartialEq, Eq)]
42pub struct E2eeCodec {
43 hkdf_info: Vec<u8>,
44 require_encrypted_response_content: bool,
45}
46
47impl E2eeCodec {
48 pub fn from_config(config: &E2eeConfig) -> Result<Self, E2eeCodecError> {
50 Self::new(
51 config.hkdf_info.as_bytes(),
52 config.require_encrypted_response_content,
53 )
54 }
55
56 pub fn new(
58 hkdf_info: impl AsRef<[u8]>,
59 require_encrypted_response_content: bool,
60 ) -> Result<Self, E2eeCodecError> {
61 let hkdf_info = hkdf_info.as_ref();
62 if hkdf_info.is_empty() {
63 return Err(E2eeCodecError::EmptyHkdfInfo);
64 }
65
66 Ok(Self {
67 hkdf_info: hkdf_info.to_vec(),
68 require_encrypted_response_content,
69 })
70 }
71
72 pub fn require_encrypted_response_content(&self) -> bool {
74 self.require_encrypted_response_content
75 }
76
77 pub fn derive_content_key(
80 &self,
81 local_private_key: &SecretKey,
82 peer_public_key_hex: &str,
83 ) -> Result<ContentEncryptionKey, E2eeCodecError> {
84 let peer_public_key = decode_uncompressed_public_key_hex(peer_public_key_hex)?;
85 Ok(self.derive_content_key_from_public_key(local_private_key, &peer_public_key))
86 }
87
88 pub fn encrypt_content(
90 &self,
91 plaintext: &str,
92 peer_public_key_hex: &str,
93 ) -> Result<EncryptedPayload, E2eeCodecError> {
94 let peer_public_key = decode_uncompressed_public_key_hex(peer_public_key_hex)?;
95 let ephemeral_private_key = SecretKey::random(&mut OsRng);
96 let nonce = Nonce::generate();
97
98 self.encrypt_content_with_parts(plaintext, &peer_public_key, ephemeral_private_key, nonce)
99 }
100
101 pub fn decrypt_content(
103 &self,
104 payload: &EncryptedPayload,
105 recipient_private_key: &SecretKey,
106 ) -> Result<String, E2eeCodecError> {
107 let packed = PackedEncryptedPayload::unpack(payload)?;
108 let key = self.derive_content_key_from_public_key(
109 recipient_private_key,
110 &packed.ephemeral_public_key,
111 );
112 let cipher = aes256_gcm_from_key(&key);
113 let plaintext = Zeroizing::new(
114 cipher
115 .decrypt((&packed.nonce).into(), packed.ciphertext_and_tag.as_slice())
116 .map_err(|_| E2eeCodecError::AuthenticationFailed)?,
117 );
118
119 String::from_utf8(plaintext.to_vec()).map_err(|_| E2eeCodecError::InvalidPlaintextUtf8)
120 }
121
122 pub fn decrypt_response_content(
126 &self,
127 content: Option<&str>,
128 recipient_private_key: &SecretKey,
129 ) -> Result<Option<String>, E2eeCodecError> {
130 let Some(content) = content else {
131 return if self.require_encrypted_response_content {
132 Err(E2eeCodecError::MissingEncryptedContent)
133 } else {
134 Ok(None)
135 };
136 };
137
138 let payload = EncryptedPayload::from_hex(content)?;
139 self.decrypt_content(&payload, recipient_private_key)
140 .map(Some)
141 }
142
143 fn encrypt_content_with_parts(
145 &self,
146 plaintext: &str,
147 peer_public_key: &PublicKey,
148 ephemeral_private_key: SecretKey,
149 nonce: Nonce,
150 ) -> Result<EncryptedPayload, E2eeCodecError> {
151 let ephemeral_public_key = ephemeral_private_key.public_key();
152 let ephemeral_public_key = ephemeral_public_key.to_encoded_point(false);
153 let ephemeral_public_key_bytes = ephemeral_public_key.as_bytes();
154 debug_assert_eq!(ephemeral_public_key_bytes.len(), EPHEMERAL_PUBLIC_KEY_LEN);
155
156 let key = self.derive_content_key_from_public_key(&ephemeral_private_key, peer_public_key);
157 let cipher = aes256_gcm_from_key(&key);
158 let ciphertext_and_tag = cipher
159 .encrypt(nonce.as_bytes().into(), plaintext.as_bytes())
160 .map_err(|_| E2eeCodecError::EncryptionFailed)?;
161
162 let mut packed = Vec::with_capacity(PACKED_PREFIX_LEN + ciphertext_and_tag.len());
163 packed.extend_from_slice(ephemeral_public_key_bytes);
164 packed.extend_from_slice(nonce.as_bytes());
165 packed.extend_from_slice(&ciphertext_and_tag);
166
167 Ok(EncryptedPayload::from_packed_bytes_unchecked(&packed))
168 }
169
170 fn derive_content_key_from_public_key(
172 &self,
173 local_private_key: &SecretKey,
174 peer_public_key: &PublicKey,
175 ) -> ContentEncryptionKey {
176 let shared_secret = diffie_hellman(
177 local_private_key.to_nonzero_scalar(),
178 peer_public_key.as_affine(),
179 );
180 derive_aes_key(&shared_secret, &self.hkdf_info)
181 }
182}
183
184impl Default for E2eeCodec {
185 fn default() -> Self {
187 Self::from_config(&E2eeConfig::default()).expect("default E2EE config is valid")
188 }
189}
190
191pub struct ContentEncryptionKey(Zeroizing<[u8; AES_256_KEY_LEN]>);
195
196impl ContentEncryptionKey {
197 fn new(bytes: Zeroizing<[u8; AES_256_KEY_LEN]>) -> Self {
199 Self(bytes)
200 }
201
202 fn as_slice(&self) -> &[u8] {
204 &self.0[..]
205 }
206}
207
208impl ZeroizeOnDrop for ContentEncryptionKey {}
209
210impl fmt::Debug for ContentEncryptionKey {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 f.write_str("ContentEncryptionKey([redacted])")
214 }
215}
216
217impl PartialEq for ContentEncryptionKey {
218 fn eq(&self, other: &Self) -> bool {
220 self.as_slice() == other.as_slice()
221 }
222}
223
224impl Eq for ContentEncryptionKey {}
225
226fn aes256_gcm_from_key(key: &ContentEncryptionKey) -> Aes256Gcm {
228 Aes256Gcm::new((&*key.0).into())
245}
246
247#[derive(Clone, Copy, PartialEq, Eq)]
249pub struct Nonce([u8; NONCE_LEN]);
250
251impl Nonce {
252 pub fn generate() -> Self {
254 let mut bytes = [0_u8; NONCE_LEN];
255 OsRng.fill_bytes(&mut bytes);
256 Self(bytes)
257 }
258
259 pub fn from_bytes(bytes: [u8; NONCE_LEN]) -> Self {
261 Self(bytes)
262 }
263
264 pub fn as_bytes(&self) -> &[u8; NONCE_LEN] {
266 &self.0
267 }
268}
269
270impl fmt::Debug for Nonce {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
273 f.debug_tuple("Nonce")
274 .field(&hex::encode(self.as_bytes()))
275 .finish()
276 }
277}
278
279#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
281#[serde(transparent)]
282pub struct EncryptedPayload(String);
283
284impl EncryptedPayload {
285 pub fn from_hex(value: impl Into<String>) -> Result<Self, E2eeCodecError> {
287 let value = value.into();
288 validate_packed_payload_hex(&value)?;
289 Ok(Self(value.to_ascii_lowercase()))
290 }
291
292 pub fn as_hex(&self) -> &str {
294 &self.0
295 }
296
297 pub fn into_hex(self) -> String {
299 self.0
300 }
301
302 fn from_packed_bytes_unchecked(bytes: &[u8]) -> Self {
304 Self(hex::encode(bytes))
305 }
306}
307
308impl fmt::Debug for EncryptedPayload {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
311 f.debug_struct("EncryptedPayload")
312 .field("hex_len", &self.0.len())
313 .finish()
314 }
315}
316
317struct PackedEncryptedPayload {
319 ephemeral_public_key: PublicKey,
320 nonce: [u8; NONCE_LEN],
321 ciphertext_and_tag: Vec<u8>,
322}
323
324impl PackedEncryptedPayload {
325 fn unpack(payload: &EncryptedPayload) -> Result<Self, E2eeCodecError> {
327 let bytes = hex::decode(payload.as_hex()).map_err(|error| {
328 E2eeCodecError::MalformedEncryptedPayload {
329 message: error.to_string(),
330 }
331 })?;
332 if bytes.len() < MIN_PACKED_PAYLOAD_LEN {
333 return Err(E2eeCodecError::MalformedEncryptedPayload {
334 message: format!(
335 "packed encrypted payload is too short: got {} bytes, need at least {MIN_PACKED_PAYLOAD_LEN}",
336 bytes.len()
337 ),
338 });
339 }
340
341 if bytes[0] != 0x04 {
342 return Err(E2eeCodecError::MalformedEncryptedPayload {
343 message: "ephemeral public key must be uncompressed SEC1 format".to_owned(),
344 });
345 }
346 let ephemeral_public_key = PublicKey::from_sec1_bytes(&bytes[..EPHEMERAL_PUBLIC_KEY_LEN])
347 .map_err(|_| E2eeCodecError::MalformedEncryptedPayload {
348 message: "ephemeral public key is not a valid secp256k1 key".to_owned(),
349 })?;
350
351 let mut nonce = [0_u8; NONCE_LEN];
352 nonce.copy_from_slice(&bytes[EPHEMERAL_PUBLIC_KEY_LEN..PACKED_PREFIX_LEN]);
353
354 Ok(Self {
355 ephemeral_public_key,
356 nonce,
357 ciphertext_and_tag: bytes[PACKED_PREFIX_LEN..].to_vec(),
358 })
359 }
360}
361
362#[derive(Debug, Error, PartialEq, Eq)]
364pub enum E2eeCodecError {
365 #[error("configured E2EE HKDF info must not be empty")]
366 EmptyHkdfInfo,
367 #[error("encrypted response content is required but missing")]
368 MissingEncryptedContent,
369 #[error("encrypted payload is malformed: {message}")]
370 MalformedEncryptedPayload { message: String },
371 #[error("encrypted payload authentication failed")]
372 AuthenticationFailed,
373 #[error("invalid E2EE public key: {message}")]
374 InvalidPublicKey { message: String },
375 #[error("decrypted E2EE payload is not valid UTF-8")]
376 InvalidPlaintextUtf8,
377 #[error("E2EE encryption failed")]
378 EncryptionFailed,
379}
380
381fn derive_aes_key(shared_secret: &SharedSecret, hkdf_info: &[u8]) -> ContentEncryptionKey {
383 let hkdf = Hkdf::<Sha256>::new(None, shared_secret.raw_secret_bytes());
384 let mut output_key = Zeroizing::new([0_u8; AES_256_KEY_LEN]);
385 hkdf.expand(hkdf_info, output_key.as_mut_slice())
386 .expect("32-byte HKDF-SHA256 output length is always valid");
387 ContentEncryptionKey::new(output_key)
388}
389
390fn decode_uncompressed_public_key_hex(value: &str) -> Result<PublicKey, E2eeCodecError> {
392 let bytes = hex::decode(value).map_err(|error| E2eeCodecError::InvalidPublicKey {
393 message: error.to_string(),
394 })?;
395
396 if bytes.len() != EPHEMERAL_PUBLIC_KEY_LEN {
397 return Err(E2eeCodecError::InvalidPublicKey {
398 message: format!(
399 "expected {EPHEMERAL_PUBLIC_KEY_LEN} uncompressed SEC1 bytes, got {}",
400 bytes.len()
401 ),
402 });
403 }
404 if bytes.first() != Some(&0x04) {
405 return Err(E2eeCodecError::InvalidPublicKey {
406 message: "public key must be uncompressed SEC1 format".to_owned(),
407 });
408 }
409
410 PublicKey::from_sec1_bytes(&bytes).map_err(|_| E2eeCodecError::InvalidPublicKey {
411 message: "public key is not a valid secp256k1 key".to_owned(),
412 })
413}
414
415fn validate_packed_payload_hex(value: &str) -> Result<(), E2eeCodecError> {
417 if value.is_empty() {
418 return Err(E2eeCodecError::MalformedEncryptedPayload {
419 message: "encrypted payload hex string is empty".to_owned(),
420 });
421 }
422 if !value.len().is_multiple_of(2) {
423 return Err(E2eeCodecError::MalformedEncryptedPayload {
424 message: "encrypted payload hex string has odd length".to_owned(),
425 });
426 }
427 if value.len() < MIN_PACKED_PAYLOAD_LEN * 2 {
428 return Err(E2eeCodecError::MalformedEncryptedPayload {
429 message: format!(
430 "encrypted payload hex string is too short: got {} chars, need at least {}",
431 value.len(),
432 MIN_PACKED_PAYLOAD_LEN * 2
433 ),
434 });
435 }
436 if let Some((index, ch)) = value.char_indices().find(|(_, ch)| !ch.is_ascii_hexdigit()) {
437 return Err(E2eeCodecError::MalformedEncryptedPayload {
438 message: format!(
439 "encrypted payload hex string contains non-hex character {ch:?} at index {index}"
440 ),
441 });
442 }
443 Ok(())
444}
445
446#[cfg(test)]
447mod tests {
448 use super::*;
449
450 const FIXED_NONCE: [u8; NONCE_LEN] = [
451 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab,
452 ];
453 const FIXED_RECIPIENT_PRIVATE_KEY_HEX: &str =
454 "1111111111111111111111111111111111111111111111111111111111111111";
455 const FIXED_EPHEMERAL_PRIVATE_KEY_HEX: &str =
456 "2222222222222222222222222222222222222222222222222222222222222222";
457 const DETERMINISTIC_PLAINTEXT: &str = "deterministic Venice E2EE fixture";
458 const DETERMINISTIC_CIPHERTEXT_HEX: &str = "04466d7fcae563e5cb09a0d1870bb580344804617879a14949cf22285f1bae3f276728176c3c6431f8eeda4538dc37c865e2784f3a9e77d044f33e407797e1278aa0a1a2a3a4a5a6a7a8a9aaab3b364a6560dc6246955e1379bac6c7a0f453c5b2d9be6eabb00cad9955278b4c401f6793813d7f98ba8f163a5c51b87686";
459
460 fn secret_key_from_hex(value: &str) -> SecretKey {
461 let bytes = hex::decode(value).expect("test key hex should decode");
462 SecretKey::from_slice(&bytes).expect("test key should be valid")
463 }
464
465 fn public_key_hex(secret_key: &SecretKey) -> String {
466 hex::encode(secret_key.public_key().to_encoded_point(false).as_bytes())
467 }
468
469 #[test]
470 fn encrypt_decrypt_round_trip() {
471 let codec = E2eeCodec::default();
472 let recipient_private_key = SecretKey::random(&mut OsRng);
473 let recipient_public_key_hex = public_key_hex(&recipient_private_key);
474
475 let encrypted = codec
476 .encrypt_content("hello from local proxy", &recipient_public_key_hex)
477 .expect("encryption should succeed");
478 let decrypted = codec
479 .decrypt_content(&encrypted, &recipient_private_key)
480 .expect("decryption should succeed");
481
482 assert_eq!(decrypted, "hello from local proxy");
483 assert!(
484 encrypted
485 .as_hex()
486 .chars()
487 .all(|ch| !ch.is_ascii_uppercase())
488 );
489 }
490
491 #[test]
492 fn decryption_with_wrong_key_fails_authentication() {
493 let codec = E2eeCodec::default();
494 let recipient_private_key = SecretKey::random(&mut OsRng);
495 let wrong_private_key = SecretKey::random(&mut OsRng);
496 let recipient_public_key_hex = public_key_hex(&recipient_private_key);
497 let encrypted = codec
498 .encrypt_content("secret", &recipient_public_key_hex)
499 .expect("encryption should succeed");
500
501 let err = codec
502 .decrypt_content(&encrypted, &wrong_private_key)
503 .expect_err("wrong key must fail closed");
504
505 assert_eq!(err, E2eeCodecError::AuthenticationFailed);
506 }
507
508 #[test]
509 fn tampered_ciphertext_fails_authentication() {
510 let codec = E2eeCodec::default();
511 let recipient_private_key = SecretKey::random(&mut OsRng);
512 let recipient_public_key_hex = public_key_hex(&recipient_private_key);
513 let encrypted = codec
514 .encrypt_content("secret", &recipient_public_key_hex)
515 .expect("encryption should succeed");
516 let mut packed = hex::decode(encrypted.as_hex()).expect("ciphertext should decode");
517 let last = packed.last_mut().expect("ciphertext has tag byte");
518 *last ^= 0x01;
519 let tampered = EncryptedPayload::from_packed_bytes_unchecked(&packed);
520
521 let err = codec
522 .decrypt_content(&tampered, &recipient_private_key)
523 .expect_err("tampered ciphertext must fail closed");
524
525 assert_eq!(err, E2eeCodecError::AuthenticationFailed);
526 }
527
528 #[test]
529 fn malformed_payload_fails_closed() {
530 let codec = E2eeCodec::default();
531 let recipient_private_key = SecretKey::random(&mut OsRng);
532
533 let err = codec
534 .decrypt_response_content(Some("not encrypted"), &recipient_private_key)
535 .expect_err("non-hex payload should fail closed");
536 assert!(matches!(
537 err,
538 E2eeCodecError::MalformedEncryptedPayload { .. }
539 ));
540
541 let too_short = "04".repeat(EPHEMERAL_PUBLIC_KEY_LEN + NONCE_LEN);
542 let err =
543 EncryptedPayload::from_hex(too_short).expect_err("short payload should be rejected");
544 assert!(matches!(
545 err,
546 E2eeCodecError::MalformedEncryptedPayload { .. }
547 ));
548 }
549
550 #[test]
551 fn missing_encrypted_response_content_respects_config() {
552 let recipient_private_key = SecretKey::random(&mut OsRng);
553
554 let required = E2eeCodec::new("ecdsa_encryption", true).expect("config should be valid");
555 let err = required
556 .decrypt_response_content(None, &recipient_private_key)
557 .expect_err("missing required encrypted content should fail");
558 assert_eq!(err, E2eeCodecError::MissingEncryptedContent);
559
560 let optional = E2eeCodec::new("ecdsa_encryption", false).expect("config should be valid");
561 let decrypted = optional
562 .decrypt_response_content(None, &recipient_private_key)
563 .expect("missing optional content should be allowed");
564 assert_eq!(decrypted, None);
565 }
566
567 #[test]
568 fn deterministic_test_vector_with_fixed_nonce_and_ephemeral_key() {
569 let codec = E2eeCodec::default();
570 let recipient_private_key = secret_key_from_hex(FIXED_RECIPIENT_PRIVATE_KEY_HEX);
571 let recipient_public_key = recipient_private_key.public_key();
572 let ephemeral_private_key = secret_key_from_hex(FIXED_EPHEMERAL_PRIVATE_KEY_HEX);
573
574 let encrypted = codec
575 .encrypt_content_with_parts(
576 DETERMINISTIC_PLAINTEXT,
577 &recipient_public_key,
578 ephemeral_private_key,
579 Nonce::from_bytes(FIXED_NONCE),
580 )
581 .expect("deterministic encryption should succeed");
582
583 assert_eq!(encrypted.as_hex(), DETERMINISTIC_CIPHERTEXT_HEX);
584 let decrypted = codec
585 .decrypt_content(&encrypted, &recipient_private_key)
586 .expect("deterministic fixture should decrypt");
587 assert_eq!(decrypted, DETERMINISTIC_PLAINTEXT);
588 }
589
590 #[test]
591 fn derived_keys_match_from_both_sides_and_debug_is_redacted() {
592 fn assert_zeroize_on_drop<T: ZeroizeOnDrop>() {}
593
594 assert_zeroize_on_drop::<SecretKey>();
595 assert_zeroize_on_drop::<SharedSecret>();
596 assert_zeroize_on_drop::<aes::Aes256>();
597 assert_zeroize_on_drop::<ContentEncryptionKey>();
598
599 let codec = E2eeCodec::default();
600 let local_private_key = SecretKey::random(&mut OsRng);
601 let peer_private_key = SecretKey::random(&mut OsRng);
602 let local_public_key_hex = public_key_hex(&local_private_key);
603 let peer_public_key_hex = public_key_hex(&peer_private_key);
604
605 let local_key = codec
606 .derive_content_key(&local_private_key, &peer_public_key_hex)
607 .expect("local derivation should succeed");
608 let peer_key = codec
609 .derive_content_key(&peer_private_key, &local_public_key_hex)
610 .expect("peer derivation should succeed");
611
612 assert_eq!(local_key, peer_key);
613 assert_eq!(format!("{local_key:?}"), "ContentEncryptionKey([redacted])");
614 }
615}