1use crate::pq::{self, MlKem768Keypair};
26use rand::rngs::OsRng;
27use sha3::{Digest, Sha3_256};
28use x25519_dalek::{PublicKey, StaticSecret};
29use zeroize::Zeroize;
30
31const COMBINER_LABEL: &[u8] = b"ling-hybrid-x25519-mlkem768-v1";
32
33const X25519_LEN: usize = 32;
34pub const PUBLIC_KEY_LEN: usize = X25519_LEN + pq::ENCAPS_KEY_LEN; pub const CIPHERTEXT_LEN: usize = X25519_LEN + pq::CIPHERTEXT_LEN; pub const SHARED_SECRET_LEN: usize = 32;
40
41fn combine(ss_mlkem: &[u8], ss_x25519: &[u8], eph_pk: &[u8], recipient_pk: &[u8]) -> [u8; 32] {
43 let mut h = Sha3_256::new();
44 h.update(COMBINER_LABEL);
45 h.update(ss_mlkem);
46 h.update(ss_x25519);
47 h.update(eph_pk);
48 h.update(recipient_pk);
49 h.finalize().into()
50}
51
52pub struct HybridKeypair {
54 x25519_secret: StaticSecret,
55 x25519_public: [u8; X25519_LEN],
56 mlkem: MlKem768Keypair,
57}
58
59impl HybridKeypair {
60 pub fn generate() -> Self {
62 let x25519_secret = StaticSecret::random_from_rng(OsRng);
63 let x25519_public = PublicKey::from(&x25519_secret).to_bytes();
64 Self {
65 x25519_secret,
66 x25519_public,
67 mlkem: MlKem768Keypair::generate(),
68 }
69 }
70
71 pub fn public_key(&self) -> Vec<u8> {
73 let mut out = Vec::with_capacity(PUBLIC_KEY_LEN);
74 out.extend_from_slice(&self.x25519_public);
75 out.extend_from_slice(&self.mlkem.encapsulation_key());
76 out
77 }
78
79 pub fn decapsulate(&self, ciphertext: &[u8]) -> Result<[u8; SHARED_SECRET_LEN], &'static str> {
81 if ciphertext.len() != CIPHERTEXT_LEN {
82 return Err("hybrid ciphertext wrong length");
83 }
84 let (eph_pk_bytes, ct_pq) = ciphertext.split_at(X25519_LEN);
85 let mut eph_arr = [0u8; X25519_LEN];
86 eph_arr.copy_from_slice(eph_pk_bytes);
87
88 let eph_pk = PublicKey::from(eph_arr);
90 let mut ss_x = self.x25519_secret.diffie_hellman(&eph_pk).to_bytes();
91
92 let ss_pq = self.mlkem.decapsulate(ct_pq)?;
94
95 let out = combine(&ss_pq, &ss_x, eph_pk_bytes, &self.x25519_public);
96 ss_x.zeroize();
97 Ok(out)
98 }
99}
100
101pub fn encapsulate(
106 hybrid_public_key: &[u8],
107) -> Result<(Vec<u8>, [u8; SHARED_SECRET_LEN]), &'static str> {
108 if hybrid_public_key.len() != PUBLIC_KEY_LEN {
109 return Err("hybrid public key wrong length");
110 }
111 let (x_pk_bytes, mlkem_ek) = hybrid_public_key.split_at(X25519_LEN);
112 let mut x_pk_arr = [0u8; X25519_LEN];
113 x_pk_arr.copy_from_slice(x_pk_bytes);
114
115 let eph_secret = StaticSecret::random_from_rng(OsRng);
117 let eph_public = PublicKey::from(&eph_secret).to_bytes();
118 let mut ss_x = eph_secret
119 .diffie_hellman(&PublicKey::from(x_pk_arr))
120 .to_bytes();
121
122 let (ct_pq, ss_pq) = pq::encapsulate(mlkem_ek)?;
124
125 let shared = combine(&ss_pq, &ss_x, &eph_public, x_pk_bytes);
126 ss_x.zeroize();
127
128 let mut ciphertext = Vec::with_capacity(CIPHERTEXT_LEN);
129 ciphertext.extend_from_slice(&eph_public);
130 ciphertext.extend_from_slice(&ct_pq);
131 Ok((ciphertext, shared))
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn round_trip() {
140 let kp = HybridKeypair::generate();
141 let pk = kp.public_key();
142 assert_eq!(pk.len(), PUBLIC_KEY_LEN);
143
144 let (ct, ss_send) = encapsulate(&pk).expect("encapsulate");
145 assert_eq!(ct.len(), CIPHERTEXT_LEN);
146
147 let ss_recv = kp.decapsulate(&ct).expect("decapsulate");
148 assert_eq!(ss_send, ss_recv, "hybrid shared secrets agree");
149 }
150
151 #[test]
152 fn distinct_encapsulations_differ() {
153 let kp = HybridKeypair::generate();
154 let pk = kp.public_key();
155 let (_, a) = encapsulate(&pk).unwrap();
156 let (_, b) = encapsulate(&pk).unwrap();
157 assert_ne!(a, b, "fresh randomness yields distinct shared secrets");
158 }
159
160 #[test]
161 fn tampered_ciphertext_changes_secret() {
162 let kp = HybridKeypair::generate();
166 let pk = kp.public_key();
167 let (mut ct, ss_send) = encapsulate(&pk).unwrap();
168 let last = ct.len() - 1;
169 ct[last] ^= 0xFF;
170 let ss_recv = kp.decapsulate(&ct).unwrap();
171 assert_ne!(ss_send, ss_recv);
172 }
173
174 #[test]
175 fn wrong_lengths_rejected() {
176 let kp = HybridKeypair::generate();
177 assert!(encapsulate(&[0u8; 10]).is_err());
178 assert!(kp.decapsulate(&[0u8; 10]).is_err());
179 }
180}