Skip to main content

dcrypt_kem/ecdh/p256/
mod.rs

1// File: crates/kem/src/ecdh/p256/mod.rs
2//! ECDH-KEM with NIST P-256
3//!
4//! This module provides a Key Encapsulation Mechanism (KEM) based on the
5//! Elliptic Curve Diffie-Hellman (ECDH) protocol using the NIST P-256 curve.
6//! This is a dcrypt-specific ECDH-plus-HKDF construction, not RFC 9180 HPKE.
7//! Invalid inputs return errors. No blanket constant-time or IND-CCA claim is
8//! made; arithmetic behavior depends on the backend, compiler, and target.
9//!
10//! This implementation uses compressed point format for optimal bandwidth efficiency.
11//!
12//! Public points are validated and the shared point is processed with
13//! HKDF-SHA256. Protocols needing HPKE or implicit rejection must use a vetted
14//! implementation of that construction instead.
15
16use super::concat_kdf_ikm;
17use crate::error::Error as KemError;
18use alloc::vec::Vec;
19use dcrypt_algorithms::ec::p256 as ec_p256;
20use dcrypt_api::{
21    error::Error as ApiError,
22    traits::serialize::{Serialize, SerializeSecret},
23    Kem, Key as ApiKey, Result as ApiResult, ZeroizingBytes,
24};
25use dcrypt_common::security::SecretBuffer;
26use dcrypt_internal::random::{try_fill_bytes_zeroing_on_error, CryptoRng, RngCore};
27use dcrypt_internal::zeroing::Zeroizing;
28
29const KDF_INFO: &[u8] = b"dcrypt-v3/ECDH-P256-KEM/shared-secret";
30
31/// ECDH KEM with P-256 curve
32pub struct EcdhP256;
33
34/// Public key for ECDH-P-256 KEM (compressed EC point)
35#[derive(Clone)]
36pub struct EcdhP256PublicKey([u8; ec_p256::P256_POINT_COMPRESSED_SIZE]);
37
38impl_zeroize_tuple!(EcdhP256PublicKey);
39
40impl AsRef<[u8]> for EcdhP256PublicKey {
41    fn as_ref(&self) -> &[u8] {
42        &self.0
43    }
44}
45
46impl AsMut<[u8]> for EcdhP256PublicKey {
47    fn as_mut(&mut self) -> &mut [u8] {
48        &mut self.0
49    }
50}
51
52/// Secret key for ECDH-P-256 KEM (scalar value)
53#[derive(Clone)]
54pub struct EcdhP256SecretKey(SecretBuffer<{ ec_p256::P256_SCALAR_SIZE }>);
55
56impl_zeroize_on_drop_tuple!(EcdhP256SecretKey);
57
58impl AsRef<[u8]> for EcdhP256SecretKey {
59    fn as_ref(&self) -> &[u8] {
60        self.0.as_ref()
61    }
62}
63
64/// Shared secret from ECDH-P-256 KEM
65#[derive(Clone)]
66pub struct EcdhP256SharedSecret(ApiKey);
67
68impl_zeroize_on_drop_tuple!(EcdhP256SharedSecret);
69
70impl AsRef<[u8]> for EcdhP256SharedSecret {
71    fn as_ref(&self) -> &[u8] {
72        self.0.as_ref()
73    }
74}
75
76/// Ciphertext for ECDH-P-256 KEM (compressed ephemeral public key)
77#[derive(Clone)]
78pub struct EcdhP256Ciphertext([u8; ec_p256::P256_POINT_COMPRESSED_SIZE]);
79
80impl AsRef<[u8]> for EcdhP256Ciphertext {
81    fn as_ref(&self) -> &[u8] {
82        &self.0
83    }
84}
85
86impl AsMut<[u8]> for EcdhP256Ciphertext {
87    fn as_mut(&mut self) -> &mut [u8] {
88        &mut self.0
89    }
90}
91
92// --- Public key methods ---
93impl EcdhP256PublicKey {
94    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
95        if bytes.len() != ec_p256::P256_POINT_COMPRESSED_SIZE {
96            return Err(ApiError::InvalidLength {
97                context: "EcdhP256PublicKey::from_bytes",
98                expected: ec_p256::P256_POINT_COMPRESSED_SIZE,
99                actual: bytes.len(),
100            });
101        }
102        let point = ec_p256::Point::deserialize_compressed(bytes)
103            .map_err(|e| ApiError::from(KemError::from(e)))?;
104        if point.is_identity() {
105            return Err(ApiError::InvalidKey {
106                context: "EcdhP256PublicKey::from_bytes",
107                #[cfg(feature = "std")]
108                message: "Public key cannot be the identity point".to_string(),
109            });
110        }
111        let mut key_bytes = [0u8; ec_p256::P256_POINT_COMPRESSED_SIZE];
112        key_bytes.copy_from_slice(bytes);
113        Ok(Self(key_bytes))
114    }
115    pub fn to_bytes(&self) -> Vec<u8> {
116        self.0.to_vec()
117    }
118}
119
120impl Serialize for EcdhP256PublicKey {
121    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
122        Self::from_bytes(bytes)
123    }
124    fn to_bytes(&self) -> Vec<u8> {
125        self.to_bytes()
126    }
127}
128
129// --- Secret key methods ---
130impl EcdhP256SecretKey {
131    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
132        if bytes.len() != ec_p256::P256_SCALAR_SIZE {
133            return Err(ApiError::InvalidLength {
134                context: "EcdhP256SecretKey::from_bytes",
135                expected: ec_p256::P256_SCALAR_SIZE,
136                actual: bytes.len(),
137            });
138        }
139        let mut buffer = SecretBuffer::zeroed();
140        buffer.as_mut().copy_from_slice(bytes);
141        let scalar = ec_p256::Scalar::from_secret_buffer(buffer.clone())
142            .map_err(|e| ApiError::from(KemError::from(e)))?;
143        drop(scalar);
144        Ok(Self(buffer))
145    }
146    pub fn to_bytes(&self) -> ZeroizingBytes {
147        self.0.to_bytes_zeroizing_boxed()
148    }
149}
150
151impl SerializeSecret for EcdhP256SecretKey {
152    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
153        Self::from_bytes(bytes)
154    }
155    fn to_bytes_zeroizing(&self) -> ZeroizingBytes {
156        self.to_bytes()
157    }
158}
159
160// --- Shared secret methods ---
161impl EcdhP256SharedSecret {
162    pub fn to_bytes(&self) -> ZeroizingBytes {
163        self.0.to_bytes_zeroizing_boxed()
164    }
165    pub fn to_zeroizing_bytes(&self) -> ZeroizingBytes {
166        self.to_bytes()
167    }
168}
169
170impl SerializeSecret for EcdhP256SharedSecret {
171    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
172        if bytes.len() != ec_p256::P256_KEM_SHARED_SECRET_KDF_OUTPUT_SIZE {
173            return Err(ApiError::InvalidLength {
174                context: "EcdhP256SharedSecret::from_bytes",
175                expected: ec_p256::P256_KEM_SHARED_SECRET_KDF_OUTPUT_SIZE,
176                actual: bytes.len(),
177            });
178        }
179        Ok(Self(ApiKey::new(bytes)))
180    }
181    fn to_bytes_zeroizing(&self) -> ZeroizingBytes {
182        self.to_zeroizing_bytes()
183    }
184}
185
186// --- Ciphertext methods ---
187impl EcdhP256Ciphertext {
188    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
189        if bytes.len() != ec_p256::P256_POINT_COMPRESSED_SIZE {
190            return Err(ApiError::InvalidLength {
191                context: "EcdhP256Ciphertext::from_bytes",
192                expected: ec_p256::P256_POINT_COMPRESSED_SIZE,
193                actual: bytes.len(),
194            });
195        }
196        let point = ec_p256::Point::deserialize_compressed(bytes)
197            .map_err(|e| ApiError::from(KemError::from(e)))?;
198        if point.is_identity() {
199            return Err(ApiError::InvalidCiphertext {
200                context: "EcdhP256Ciphertext::from_bytes",
201                #[cfg(feature = "std")]
202                message: "Ephemeral public key cannot be the identity point".to_string(),
203            });
204        }
205        let mut ct_bytes = [0u8; ec_p256::P256_POINT_COMPRESSED_SIZE];
206        ct_bytes.copy_from_slice(bytes);
207        Ok(Self(ct_bytes))
208    }
209    pub fn to_bytes(&self) -> Vec<u8> {
210        self.0.to_vec()
211    }
212}
213
214impl Serialize for EcdhP256Ciphertext {
215    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
216        Self::from_bytes(bytes)
217    }
218    fn to_bytes(&self) -> Vec<u8> {
219        self.to_bytes()
220    }
221}
222
223impl Kem for EcdhP256 {
224    type PublicKey = EcdhP256PublicKey;
225    type SecretKey = EcdhP256SecretKey;
226    type SharedSecret = EcdhP256SharedSecret;
227    type Ciphertext = EcdhP256Ciphertext;
228    type KeyPair = (Self::PublicKey, Self::SecretKey);
229
230    fn name() -> &'static str {
231        "ECDH-P256"
232    }
233
234    fn keypair<R: CryptoRng + RngCore>(rng: &mut R) -> ApiResult<Self::KeyPair> {
235        let (sk_scalar, pk_point) =
236            ec_p256::generate_keypair(rng).map_err(|e| ApiError::from(KemError::from(e)))?;
237        let public_key = EcdhP256PublicKey(pk_point.serialize_compressed());
238        let secret_key = EcdhP256SecretKey(sk_scalar.as_secret_buffer().clone());
239        Ok((public_key, secret_key))
240    }
241
242    fn public_key(keypair: &Self::KeyPair) -> Self::PublicKey {
243        keypair.0.clone()
244    }
245
246    fn secret_key(keypair: &Self::KeyPair) -> Self::SecretKey {
247        keypair.1.clone()
248    }
249
250    fn encapsulate<R: CryptoRng + RngCore>(
251        rng: &mut R,
252        public_key_recipient: &Self::PublicKey,
253    ) -> ApiResult<(Self::Ciphertext, Self::SharedSecret)> {
254        let pk_r_point = ec_p256::Point::deserialize_compressed(&public_key_recipient.0)
255            .map_err(|e| ApiError::from(KemError::from(e)))?;
256        if pk_r_point.is_identity() {
257            return Err(ApiError::InvalidKey {
258                context: "ECDH-P256 encapsulate",
259                #[cfg(feature = "std")]
260                message: "Recipient public key cannot be the identity point".to_string(),
261            });
262        }
263        // Rejection-sample until the random value maps to a valid non-zero
264        // private scalar. A CSPRNG output outside [1, n-1] is expected input,
265        // not an operation failure.
266        let ephemeral_scalar = loop {
267            let mut ephemeral_bytes = Zeroizing::new([0u8; ec_p256::P256_SCALAR_SIZE]);
268            try_fill_bytes_zeroing_on_error(rng, ephemeral_bytes.as_mut()).map_err(|_| {
269                ApiError::RandomGenerationError {
270                    context: "ECDH-P256 encapsulate",
271                    #[cfg(feature = "std")]
272                    message: "caller-provided randomness source failed".to_string(),
273                }
274            })?;
275            let ephemeral_buffer = SecretBuffer::new(*ephemeral_bytes);
276            if let Ok(scalar) = ec_p256::Scalar::from_secret_buffer(ephemeral_buffer) {
277                break scalar;
278            }
279        };
280        let ephemeral_point = ec_p256::scalar_mult_base_g(&ephemeral_scalar)
281            .map_err(|e| ApiError::from(KemError::from(e)))?;
282        let ciphertext = EcdhP256Ciphertext(ephemeral_point.serialize_compressed());
283        let shared_point = ec_p256::scalar_mult(&ephemeral_scalar, &pk_r_point)
284            .map_err(|e| ApiError::from(KemError::from(e)))?;
285        if shared_point.is_identity() {
286            return Err(ApiError::DecryptionFailed {
287                context: "ECDH-P256 encapsulate",
288                #[cfg(feature = "std")]
289                message: "Shared point is the identity".to_string(),
290            });
291        }
292        let x_coord_bytes = Zeroizing::new(shared_point.x_coordinate_bytes());
293        let kdf_ikm = concat_kdf_ikm(
294            x_coord_bytes.as_ref(),
295            &ephemeral_point.serialize_compressed(),
296            &public_key_recipient.0,
297        );
298        let ss_bytes = ec_p256::kdf_hkdf_sha256_for_ecdh_kem(&kdf_ikm, Some(KDF_INFO))
299            .map_err(|e| ApiError::from(KemError::from(e)))?;
300        let shared_secret = EcdhP256SharedSecret(ApiKey::new(&ss_bytes[..]));
301        drop(ephemeral_scalar);
302        Ok((ciphertext, shared_secret))
303    }
304
305    fn decapsulate(
306        secret_key_recipient: &Self::SecretKey,
307        ciphertext_ephemeral_pk: &Self::Ciphertext,
308    ) -> ApiResult<Self::SharedSecret> {
309        let scalar_result = ec_p256::Scalar::from_secret_buffer(secret_key_recipient.0.clone());
310        let sk_r_scalar = match scalar_result {
311            Ok(scalar) => scalar,
312            Err(e) => return Err(ApiError::from(KemError::from(e))),
313        };
314        let q_e_point = ec_p256::Point::deserialize_compressed(&ciphertext_ephemeral_pk.0)
315            .map_err(|e| ApiError::from(KemError::from(e)))?;
316        if q_e_point.is_identity() {
317            return Err(ApiError::InvalidCiphertext {
318                context: "ECDH-P256 decapsulate",
319                #[cfg(feature = "std")]
320                message: "Ephemeral public key cannot be the identity point".to_string(),
321            });
322        }
323        let shared_point = ec_p256::scalar_mult(&sk_r_scalar, &q_e_point)
324            .map_err(|e| ApiError::from(KemError::from(e)))?;
325        if shared_point.is_identity() {
326            return Err(ApiError::DecryptionFailed {
327                context: "ECDH-P256 decapsulate",
328                #[cfg(feature = "std")]
329                message: "Shared point is the identity".to_string(),
330            });
331        }
332        let x_coord_bytes = Zeroizing::new(shared_point.x_coordinate_bytes());
333        let q_r_point = ec_p256::scalar_mult_base_g(&sk_r_scalar)
334            .map_err(|e| ApiError::from(KemError::from(e)))?;
335        let kdf_ikm = concat_kdf_ikm(
336            x_coord_bytes.as_ref(),
337            &ciphertext_ephemeral_pk.0,
338            &q_r_point.serialize_compressed(),
339        );
340        let ss_bytes = ec_p256::kdf_hkdf_sha256_for_ecdh_kem(&kdf_ikm, Some(KDF_INFO))
341            .map_err(|e| ApiError::from(KemError::from(e)))?;
342        let shared_secret = EcdhP256SharedSecret(ApiKey::new(&ss_bytes[..]));
343        Ok(shared_secret)
344    }
345}
346
347#[cfg(test)]
348mod tests;