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//! The implementation is secure against timing attacks and follows best practices
7//! for key derivation according to RFC 9180 (HPKE).
8//!
9//! This implementation uses compressed point format for optimal bandwidth efficiency.
10//!
11//! # Security Features
12//!
13//! - No direct byte access to keys (prevents tampering and accidental exposure)
14//! - Constant-time scalar operations
15//! - Point validation to prevent invalid curve attacks
16//! - Secure key derivation using HKDF-SHA256
17//! - Implicit rejection for IND-CCA2 security
18
19use crate::error::Error as KemError;
20use dcrypt_algorithms::ec::p256 as ec_p256;
21use dcrypt_api::{
22    error::Error as ApiError,
23    traits::serialize::{Serialize, SerializeSecret},
24    Kem, Key as ApiKey, Result as ApiResult,
25};
26use dcrypt_common::security::SecretBuffer;
27use rand::{CryptoRng, RngCore};
28use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
29
30/// ECDH KEM with P-256 curve
31pub struct EcdhP256;
32
33/// Public key for ECDH-P-256 KEM (compressed EC point)
34#[derive(Clone, Zeroize)]
35pub struct EcdhP256PublicKey([u8; ec_p256::P256_POINT_COMPRESSED_SIZE]);
36
37impl AsRef<[u8]> for EcdhP256PublicKey {
38    fn as_ref(&self) -> &[u8] {
39        &self.0
40    }
41}
42
43impl AsMut<[u8]> for EcdhP256PublicKey {
44    fn as_mut(&mut self) -> &mut [u8] {
45        &mut self.0
46    }
47}
48
49/// Secret key for ECDH-P-256 KEM (scalar value)
50#[derive(Clone, Zeroize, ZeroizeOnDrop)]
51pub struct EcdhP256SecretKey(SecretBuffer<{ ec_p256::P256_SCALAR_SIZE }>);
52
53impl AsRef<[u8]> for EcdhP256SecretKey {
54    fn as_ref(&self) -> &[u8] {
55        self.0.as_ref()
56    }
57}
58
59/// Shared secret from ECDH-P-256 KEM
60#[derive(Clone, Zeroize, ZeroizeOnDrop)]
61pub struct EcdhP256SharedSecret(ApiKey);
62
63impl AsRef<[u8]> for EcdhP256SharedSecret {
64    fn as_ref(&self) -> &[u8] {
65        self.0.as_ref()
66    }
67}
68
69/// Ciphertext for ECDH-P-256 KEM (compressed ephemeral public key)
70#[derive(Clone)]
71pub struct EcdhP256Ciphertext([u8; ec_p256::P256_POINT_COMPRESSED_SIZE]);
72
73impl AsRef<[u8]> for EcdhP256Ciphertext {
74    fn as_ref(&self) -> &[u8] {
75        &self.0
76    }
77}
78
79impl AsMut<[u8]> for EcdhP256Ciphertext {
80    fn as_mut(&mut self) -> &mut [u8] {
81        &mut self.0
82    }
83}
84
85// --- Public key methods ---
86impl EcdhP256PublicKey {
87    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
88        if bytes.len() != ec_p256::P256_POINT_COMPRESSED_SIZE {
89            return Err(ApiError::InvalidLength {
90                context: "EcdhP256PublicKey::from_bytes",
91                expected: ec_p256::P256_POINT_COMPRESSED_SIZE,
92                actual: bytes.len(),
93            });
94        }
95        let point = ec_p256::Point::deserialize_compressed(bytes)
96            .map_err(|e| ApiError::from(KemError::from(e)))?;
97        if point.is_identity() {
98            return Err(ApiError::InvalidKey {
99                context: "EcdhP256PublicKey::from_bytes",
100                #[cfg(feature = "std")]
101                message: "Public key cannot be the identity point".to_string(),
102            });
103        }
104        let mut key_bytes = [0u8; ec_p256::P256_POINT_COMPRESSED_SIZE];
105        key_bytes.copy_from_slice(bytes);
106        Ok(Self(key_bytes))
107    }
108    pub fn to_bytes(&self) -> Vec<u8> {
109        self.0.to_vec()
110    }
111}
112
113impl Serialize for EcdhP256PublicKey {
114    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
115        Self::from_bytes(bytes)
116    }
117    fn to_bytes(&self) -> Vec<u8> {
118        self.to_bytes()
119    }
120}
121
122// --- Secret key methods ---
123impl EcdhP256SecretKey {
124    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
125        if bytes.len() != ec_p256::P256_SCALAR_SIZE {
126            return Err(ApiError::InvalidLength {
127                context: "EcdhP256SecretKey::from_bytes",
128                expected: ec_p256::P256_SCALAR_SIZE,
129                actual: bytes.len(),
130            });
131        }
132        let mut buffer_bytes = [0u8; ec_p256::P256_SCALAR_SIZE];
133        buffer_bytes.copy_from_slice(bytes);
134        let buffer = SecretBuffer::new(buffer_bytes);
135        let scalar = ec_p256::Scalar::from_secret_buffer(buffer.clone())
136            .map_err(|e| ApiError::from(KemError::from(e)))?;
137        drop(scalar);
138        Ok(Self(buffer))
139    }
140    pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
141        Zeroizing::new(self.0.as_ref().to_vec())
142    }
143}
144
145impl SerializeSecret for EcdhP256SecretKey {
146    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
147        Self::from_bytes(bytes)
148    }
149    fn to_bytes_zeroizing(&self) -> Zeroizing<Vec<u8>> {
150        self.to_bytes()
151    }
152}
153
154// --- Shared secret methods ---
155impl EcdhP256SharedSecret {
156    pub fn to_bytes(&self) -> Vec<u8> {
157        self.0.as_ref().to_vec()
158    }
159    pub fn to_zeroizing_bytes(&self) -> Zeroizing<Vec<u8>> {
160        Zeroizing::new(self.to_bytes())
161    }
162}
163
164impl SerializeSecret for EcdhP256SharedSecret {
165    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
166        Ok(Self(ApiKey::new(bytes)))
167    }
168    fn to_bytes_zeroizing(&self) -> Zeroizing<Vec<u8>> {
169        self.to_zeroizing_bytes()
170    }
171}
172
173// --- Ciphertext methods ---
174impl EcdhP256Ciphertext {
175    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
176        if bytes.len() != ec_p256::P256_POINT_COMPRESSED_SIZE {
177            return Err(ApiError::InvalidLength {
178                context: "EcdhP256Ciphertext::from_bytes",
179                expected: ec_p256::P256_POINT_COMPRESSED_SIZE,
180                actual: bytes.len(),
181            });
182        }
183        let point = ec_p256::Point::deserialize_compressed(bytes)
184            .map_err(|e| ApiError::from(KemError::from(e)))?;
185        if point.is_identity() {
186            return Err(ApiError::InvalidCiphertext {
187                context: "EcdhP256Ciphertext::from_bytes",
188                #[cfg(feature = "std")]
189                message: "Ephemeral public key cannot be the identity point".to_string(),
190            });
191        }
192        let mut ct_bytes = [0u8; ec_p256::P256_POINT_COMPRESSED_SIZE];
193        ct_bytes.copy_from_slice(bytes);
194        Ok(Self(ct_bytes))
195    }
196    pub fn to_bytes(&self) -> Vec<u8> {
197        self.0.to_vec()
198    }
199}
200
201impl Serialize for EcdhP256Ciphertext {
202    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
203        Self::from_bytes(bytes)
204    }
205    fn to_bytes(&self) -> Vec<u8> {
206        self.to_bytes()
207    }
208}
209
210impl Kem for EcdhP256 {
211    type PublicKey = EcdhP256PublicKey;
212    type SecretKey = EcdhP256SecretKey;
213    type SharedSecret = EcdhP256SharedSecret;
214    type Ciphertext = EcdhP256Ciphertext;
215    type KeyPair = (Self::PublicKey, Self::SecretKey);
216
217    fn name() -> &'static str {
218        "ECDH-P256"
219    }
220
221    fn keypair<R: CryptoRng + RngCore>(rng: &mut R) -> ApiResult<Self::KeyPair> {
222        let (sk_scalar, pk_point) =
223            ec_p256::generate_keypair(rng).map_err(|e| ApiError::from(KemError::from(e)))?;
224        let public_key = EcdhP256PublicKey(pk_point.serialize_compressed());
225        let secret_key = EcdhP256SecretKey(sk_scalar.as_secret_buffer().clone());
226        Ok((public_key, secret_key))
227    }
228
229    fn public_key(keypair: &Self::KeyPair) -> Self::PublicKey {
230        keypair.0.clone()
231    }
232
233    fn secret_key(keypair: &Self::KeyPair) -> Self::SecretKey {
234        keypair.1.clone()
235    }
236
237    fn encapsulate<R: CryptoRng + RngCore>(
238        rng: &mut R,
239        public_key_recipient: &Self::PublicKey,
240    ) -> ApiResult<(Self::Ciphertext, Self::SharedSecret)> {
241        let pk_r_point = ec_p256::Point::deserialize_compressed(&public_key_recipient.0)
242            .map_err(|e| ApiError::from(KemError::from(e)))?;
243        if pk_r_point.is_identity() {
244            return Err(ApiError::InvalidKey {
245                context: "ECDH-P256 encapsulate",
246                #[cfg(feature = "std")]
247                message: "Recipient public key cannot be the identity point".to_string(),
248            });
249        }
250        let mut ephemeral_bytes = [0u8; ec_p256::P256_SCALAR_SIZE];
251        rng.fill_bytes(&mut ephemeral_bytes);
252        let ephemeral_buffer = SecretBuffer::new(ephemeral_bytes);
253        let ephemeral_scalar = ec_p256::Scalar::from_secret_buffer(ephemeral_buffer)
254            .map_err(|e| ApiError::from(KemError::from(e)))?;
255        let ephemeral_point = ec_p256::scalar_mult_base_g(&ephemeral_scalar)
256            .map_err(|e| ApiError::from(KemError::from(e)))?;
257        let ciphertext = EcdhP256Ciphertext(ephemeral_point.serialize_compressed());
258        let shared_point = ec_p256::scalar_mult(&ephemeral_scalar, &pk_r_point)
259            .map_err(|e| ApiError::from(KemError::from(e)))?;
260        if shared_point.is_identity() {
261            return Err(ApiError::DecryptionFailed {
262                context: "ECDH-P256 encapsulate",
263                #[cfg(feature = "std")]
264                message: "Shared point is the identity".to_string(),
265            });
266        }
267        let x_coord_bytes = shared_point.x_coordinate_bytes();
268        let mut kdf_ikm = Vec::with_capacity(
269            ec_p256::P256_FIELD_ELEMENT_SIZE + 2 * ec_p256::P256_POINT_COMPRESSED_SIZE,
270        );
271        kdf_ikm.extend_from_slice(&x_coord_bytes);
272        kdf_ikm.extend_from_slice(&ephemeral_point.serialize_compressed());
273        kdf_ikm.extend_from_slice(&public_key_recipient.0);
274        let info: Option<&[u8]> = Some(b"ECDH-P256-KEM");
275        let ss_bytes = ec_p256::kdf_hkdf_sha256_for_ecdh_kem(&kdf_ikm, info)
276            .map_err(|e| ApiError::from(KemError::from(e)))?;
277        let shared_secret = EcdhP256SharedSecret(ApiKey::new(&ss_bytes));
278        drop(ephemeral_scalar);
279        Ok((ciphertext, shared_secret))
280    }
281
282    fn decapsulate(
283        secret_key_recipient: &Self::SecretKey,
284        ciphertext_ephemeral_pk: &Self::Ciphertext,
285    ) -> ApiResult<Self::SharedSecret> {
286        let scalar_result = ec_p256::Scalar::from_secret_buffer(secret_key_recipient.0.clone());
287        let sk_r_scalar = match scalar_result {
288            Ok(scalar) => scalar,
289            Err(e) => return Err(ApiError::from(KemError::from(e))),
290        };
291        let q_e_point = ec_p256::Point::deserialize_compressed(&ciphertext_ephemeral_pk.0)
292            .map_err(|e| ApiError::from(KemError::from(e)))?;
293        if q_e_point.is_identity() {
294            return Err(ApiError::InvalidCiphertext {
295                context: "ECDH-P256 decapsulate",
296                #[cfg(feature = "std")]
297                message: "Ephemeral public key cannot be the identity point".to_string(),
298            });
299        }
300        let shared_point = ec_p256::scalar_mult(&sk_r_scalar, &q_e_point)
301            .map_err(|e| ApiError::from(KemError::from(e)))?;
302        if shared_point.is_identity() {
303            return Err(ApiError::DecryptionFailed {
304                context: "ECDH-P256 decapsulate",
305                #[cfg(feature = "std")]
306                message: "Shared point is the identity".to_string(),
307            });
308        }
309        let x_coord_bytes = shared_point.x_coordinate_bytes();
310        let q_r_point = ec_p256::scalar_mult_base_g(&sk_r_scalar)
311            .map_err(|e| ApiError::from(KemError::from(e)))?;
312        let mut kdf_ikm = Vec::with_capacity(
313            ec_p256::P256_FIELD_ELEMENT_SIZE + 2 * ec_p256::P256_POINT_COMPRESSED_SIZE,
314        );
315        kdf_ikm.extend_from_slice(&x_coord_bytes);
316        kdf_ikm.extend_from_slice(&ciphertext_ephemeral_pk.0);
317        kdf_ikm.extend_from_slice(&q_r_point.serialize_compressed());
318        let info: Option<&[u8]> = Some(b"ECDH-P256-KEM");
319        let ss_bytes = ec_p256::kdf_hkdf_sha256_for_ecdh_kem(&kdf_ikm, info)
320            .map_err(|e| ApiError::from(KemError::from(e)))?;
321        let shared_secret = EcdhP256SharedSecret(ApiKey::new(&ss_bytes));
322        Ok(shared_secret)
323    }
324}
325
326#[cfg(test)]
327mod tests;