dcrypt_kem/ecdh/k256/
mod.rs

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