dcrypt_kem/ecdh/p521/
mod.rs

1// File: crates/kem/src/ecdh/p521/mod.rs
2//! ECDH-KEM with NIST P-521
3//!
4//! This module provides a Key Encapsulation Mechanism (KEM) based on the
5//! Elliptic Curve Diffie-Hellman (ECDH) protocol using the NIST P-521 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 exposure)
14//! - Constant-time operations
15//! - Proper validation of curve points
16//! - Secure key derivation using HKDF-SHA512
17
18use crate::error::Error as KemError;
19use dcrypt_algorithms::ec::p521 as ec_p521;
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 P-521 curve
30pub struct EcdhP521;
31
32/// Public key for ECDH-P521 KEM (compressed EC point)
33#[derive(Clone, Zeroize)]
34pub struct EcdhP521PublicKey([u8; ec_p521::P521_POINT_COMPRESSED_SIZE]);
35
36impl AsRef<[u8]> for EcdhP521PublicKey {
37    fn as_ref(&self) -> &[u8] {
38        &self.0
39    }
40}
41
42impl AsMut<[u8]> for EcdhP521PublicKey {
43    fn as_mut(&mut self) -> &mut [u8] {
44        &mut self.0
45    }
46}
47
48/// Secret key for ECDH-P521 KEM (scalar value)
49#[derive(Clone, Zeroize, ZeroizeOnDrop)]
50pub struct EcdhP521SecretKey(SecretBuffer<{ ec_p521::P521_SCALAR_SIZE }>);
51
52impl AsRef<[u8]> for EcdhP521SecretKey {
53    fn as_ref(&self) -> &[u8] {
54        self.0.as_ref()
55    }
56}
57
58/// Shared secret from ECDH-P521 KEM
59#[derive(Clone, Zeroize, ZeroizeOnDrop)]
60pub struct EcdhP521SharedSecret(ApiKey);
61
62impl AsRef<[u8]> for EcdhP521SharedSecret {
63    fn as_ref(&self) -> &[u8] {
64        self.0.as_ref()
65    }
66}
67
68/// Ciphertext for ECDH-P521 KEM (compressed ephemeral public key)
69#[derive(Clone)]
70pub struct EcdhP521Ciphertext([u8; ec_p521::P521_POINT_COMPRESSED_SIZE]);
71
72impl AsRef<[u8]> for EcdhP521Ciphertext {
73    fn as_ref(&self) -> &[u8] {
74        &self.0
75    }
76}
77
78impl AsMut<[u8]> for EcdhP521Ciphertext {
79    fn as_mut(&mut self) -> &mut [u8] {
80        &mut self.0
81    }
82}
83
84// --- Public key methods ---
85impl EcdhP521PublicKey {
86    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
87        if bytes.len() != ec_p521::P521_POINT_COMPRESSED_SIZE {
88            return Err(ApiError::InvalidLength {
89                context: "EcdhP521PublicKey::from_bytes",
90                expected: ec_p521::P521_POINT_COMPRESSED_SIZE,
91                actual: bytes.len(),
92            });
93        }
94        let point = ec_p521::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: "EcdhP521PublicKey::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_p521::P521_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 EcdhP521PublicKey {
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 EcdhP521SecretKey {
123    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
124        if bytes.len() != ec_p521::P521_SCALAR_SIZE {
125            return Err(ApiError::InvalidLength {
126                context: "EcdhP521SecretKey::from_bytes",
127                expected: ec_p521::P521_SCALAR_SIZE,
128                actual: bytes.len(),
129            });
130        }
131        let mut buffer_bytes = [0u8; ec_p521::P521_SCALAR_SIZE];
132        buffer_bytes.copy_from_slice(bytes);
133        let buffer = SecretBuffer::new(buffer_bytes);
134        let scalar = ec_p521::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 EcdhP521SecretKey {
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 EcdhP521SharedSecret {
155    pub fn to_bytes(&self) -> Zeroizing<Vec<u8>> {
156        Zeroizing::new(self.0.as_ref().to_vec())
157    }
158}
159
160impl SerializeSecret for EcdhP521SharedSecret {
161    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
162        Ok(Self(ApiKey::new(bytes)))
163    }
164    fn to_bytes_zeroizing(&self) -> Zeroizing<Vec<u8>> {
165        self.to_bytes()
166    }
167}
168
169// --- Ciphertext methods ---
170impl EcdhP521Ciphertext {
171    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
172        if bytes.len() != ec_p521::P521_POINT_COMPRESSED_SIZE {
173            return Err(ApiError::InvalidLength {
174                context: "EcdhP521Ciphertext::from_bytes",
175                expected: ec_p521::P521_POINT_COMPRESSED_SIZE,
176                actual: bytes.len(),
177            });
178        }
179        let point = ec_p521::Point::deserialize_compressed(bytes)
180            .map_err(|e| ApiError::from(KemError::from(e)))?;
181        if point.is_identity() {
182            return Err(ApiError::InvalidCiphertext {
183                context: "EcdhP521Ciphertext::from_bytes",
184                #[cfg(feature = "std")]
185                message: "Ephemeral public key cannot be the identity point".to_string(),
186            });
187        }
188        let mut ct_bytes = [0u8; ec_p521::P521_POINT_COMPRESSED_SIZE];
189        ct_bytes.copy_from_slice(bytes);
190        Ok(Self(ct_bytes))
191    }
192    pub fn to_bytes(&self) -> Vec<u8> {
193        self.0.to_vec()
194    }
195}
196
197impl Serialize for EcdhP521Ciphertext {
198    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
199        Self::from_bytes(bytes)
200    }
201    fn to_bytes(&self) -> Vec<u8> {
202        self.to_bytes()
203    }
204}
205
206impl Kem for EcdhP521 {
207    type PublicKey = EcdhP521PublicKey;
208    type SecretKey = EcdhP521SecretKey;
209    type SharedSecret = EcdhP521SharedSecret;
210    type Ciphertext = EcdhP521Ciphertext;
211    type KeyPair = (Self::PublicKey, Self::SecretKey);
212
213    fn name() -> &'static str {
214        "ECDH-P521"
215    }
216
217    fn keypair<R: CryptoRng + RngCore>(rng: &mut R) -> ApiResult<Self::KeyPair> {
218        let (sk_scalar, pk_point) =
219            ec_p521::generate_keypair(rng).map_err(|e| ApiError::from(KemError::from(e)))?;
220        let public_key = EcdhP521PublicKey(pk_point.serialize_compressed());
221        let secret_key = EcdhP521SecretKey(sk_scalar.as_secret_buffer().clone());
222        Ok((public_key, secret_key))
223    }
224
225    fn public_key(keypair: &Self::KeyPair) -> Self::PublicKey {
226        keypair.0.clone()
227    }
228
229    fn secret_key(keypair: &Self::KeyPair) -> Self::SecretKey {
230        keypair.1.clone()
231    }
232
233    fn encapsulate<R: CryptoRng + RngCore>(
234        rng: &mut R,
235        public_key_recipient: &Self::PublicKey,
236    ) -> ApiResult<(Self::Ciphertext, Self::SharedSecret)> {
237        let pk_r_point = ec_p521::Point::deserialize_compressed(&public_key_recipient.0)
238            .map_err(|e| ApiError::from(KemError::from(e)))?;
239        if pk_r_point.is_identity() {
240            return Err(ApiError::InvalidKey {
241                context: "ECDH-P521 encapsulate",
242                #[cfg(feature = "std")]
243                message: "Recipient public key cannot be the identity point".to_string(),
244            });
245        }
246        let (ephemeral_scalar, ephemeral_point) =
247            ec_p521::generate_keypair(rng).map_err(|e| ApiError::from(KemError::from(e)))?;
248        let ciphertext = EcdhP521Ciphertext(ephemeral_point.serialize_compressed());
249        let shared_point = ec_p521::scalar_mult(&ephemeral_scalar, &pk_r_point)
250            .map_err(|e| ApiError::from(KemError::from(e)))?;
251        if shared_point.is_identity() {
252            return Err(ApiError::DecryptionFailed {
253                context: "ECDH-P521 encapsulate",
254                #[cfg(feature = "std")]
255                message: "Shared point is the identity".to_string(),
256            });
257        }
258        let x_coord_bytes = shared_point.x_coordinate_bytes();
259        let mut kdf_ikm = Vec::with_capacity(
260            ec_p521::P521_FIELD_ELEMENT_SIZE + 2 * ec_p521::P521_POINT_COMPRESSED_SIZE,
261        );
262        kdf_ikm.extend_from_slice(&x_coord_bytes);
263        kdf_ikm.extend_from_slice(&ephemeral_point.serialize_compressed());
264        kdf_ikm.extend_from_slice(&public_key_recipient.0);
265        let info: Option<&[u8]> = Some(b"ECDH-P521-KEM");
266        let ss_bytes = ec_p521::kdf_hkdf_sha512_for_ecdh_kem(&kdf_ikm, info)
267            .map_err(|e| ApiError::from(KemError::from(e)))?;
268        let shared_secret = EcdhP521SharedSecret(ApiKey::new(&ss_bytes));
269        drop(ephemeral_scalar);
270        Ok((ciphertext, shared_secret))
271    }
272
273    fn decapsulate(
274        secret_key_recipient: &Self::SecretKey,
275        ciphertext_ephemeral_pk: &Self::Ciphertext,
276    ) -> ApiResult<Self::SharedSecret> {
277        let scalar_result = ec_p521::Scalar::from_secret_buffer(secret_key_recipient.0.clone());
278        let sk_r_scalar = match scalar_result {
279            Ok(scalar) => scalar,
280            Err(e) => return Err(ApiError::from(KemError::from(e))),
281        };
282        let q_e_point = ec_p521::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-P521 decapsulate",
287                #[cfg(feature = "std")]
288                message: "Ephemeral public key cannot be the identity point".to_string(),
289            });
290        }
291        let shared_point = ec_p521::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-P521 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_p521::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_p521::P521_FIELD_ELEMENT_SIZE + 2 * ec_p521::P521_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-P521-KEM");
310        let ss_bytes = ec_p521::kdf_hkdf_sha512_for_ecdh_kem(&kdf_ikm, info)
311            .map_err(|e| ApiError::from(KemError::from(e)))?;
312        let shared_secret = EcdhP521SharedSecret(ApiKey::new(&ss_bytes));
313        Ok(shared_secret)
314    }
315}
316
317#[cfg(test)]
318mod tests;