Skip to main content

dcrypt_kem/ecdh/p224/
mod.rs

1// File: crates/kem/src/ecdh/p224/mod.rs
2//! ECDH-KEM with NIST P-224
3//!
4//! This module provides a Key Encapsulation Mechanism (KEM) based on the
5//! Elliptic Curve Diffie-Hellman (ECDH) protocol using the NIST P-224 curve.
6//! Uses HKDF-SHA256 for key derivation and compressed points for ciphertexts.
7//! Includes authentication via HMAC-SHA256 tags to ensure key confirmation.
8//!
9//! The construction validates public points and adds an HMAC-SHA256
10//! key-confirmation tag. This is not RFC 9180 HPKE and makes no blanket
11//! constant-time or IND-CCA claim.
12
13use super::concat_kdf_ikm;
14use crate::error::Error as KemError;
15use alloc::vec::Vec;
16use dcrypt_algorithms::ec::p224 as ec; // Use P-224 algorithms
17use dcrypt_algorithms::hash::sha2::Sha256;
18use dcrypt_algorithms::mac::hmac::Hmac;
19use dcrypt_api::{
20    error::Error as ApiError,
21    traits::serialize::{Serialize, SerializeSecret},
22    Kem, Key as ApiKey, Result as ApiResult, ZeroizingBytes,
23};
24use dcrypt_common::security::SecretBuffer;
25use dcrypt_internal::random::{CryptoRng, RngCore};
26use dcrypt_internal::zeroing::Zeroizing;
27
28const KDF_INFO: &[u8] = b"dcrypt-v3/ECDH-P224-KEM/shared-secret";
29const CONFIRMATION_LABEL: &[u8] = b"dcrypt-v3/ECDH-P224-KEM/confirmation";
30
31/// ECDH KEM with P-224 curve
32pub struct EcdhP224;
33
34/// Public key for ECDH-P224 KEM (compressed EC point)
35#[derive(Clone)]
36pub struct EcdhP224PublicKey([u8; ec::P224_POINT_COMPRESSED_SIZE]);
37
38impl_zeroize_tuple!(EcdhP224PublicKey);
39
40/// Secret key for ECDH-P224 KEM (scalar value)
41#[derive(Clone)]
42pub struct EcdhP224SecretKey(SecretBuffer<{ ec::P224_SCALAR_SIZE }>);
43
44impl_zeroize_on_drop_tuple!(EcdhP224SecretKey);
45
46/// Shared secret from ECDH-P224 KEM
47#[derive(Clone)]
48pub struct EcdhP224SharedSecret(ApiKey);
49
50impl_zeroize_on_drop_tuple!(EcdhP224SharedSecret);
51
52/// Ciphertext for ECDH-P224 KEM (compressed ephemeral public key + authentication tag)
53#[derive(Clone)]
54pub struct EcdhP224Ciphertext([u8; ec::P224_CIPHERTEXT_SIZE]);
55
56// --- Public key methods ---
57impl EcdhP224PublicKey {
58    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
59        if bytes.len() != ec::P224_POINT_COMPRESSED_SIZE {
60            return Err(ApiError::InvalidLength {
61                context: "EcdhP224PublicKey::from_bytes",
62                expected: ec::P224_POINT_COMPRESSED_SIZE,
63                actual: bytes.len(),
64            });
65        }
66        let point = ec::Point::deserialize_compressed(bytes)
67            .map_err(|e| ApiError::from(KemError::from(e)))?;
68        if point.is_identity() {
69            return Err(ApiError::InvalidKey {
70                context: "EcdhP224PublicKey::from_bytes",
71                #[cfg(feature = "std")]
72                message: "Public key cannot be the identity point".to_string(),
73            });
74        }
75        let mut key_bytes = [0u8; ec::P224_POINT_COMPRESSED_SIZE];
76        key_bytes.copy_from_slice(bytes);
77        Ok(Self(key_bytes))
78    }
79
80    pub fn to_bytes(&self) -> Vec<u8> {
81        self.0.to_vec()
82    }
83
84    pub fn validate(&self) -> ApiResult<()> {
85        let point = ec::Point::deserialize_compressed(&self.0)
86            .map_err(|e| ApiError::from(KemError::from(e)))?;
87        if point.is_identity() {
88            return Err(ApiError::InvalidKey {
89                context: "validate_public_key",
90                #[cfg(feature = "std")]
91                message: "Public key is the identity point".to_string(),
92            });
93        }
94        Ok(())
95    }
96}
97
98impl Serialize for EcdhP224PublicKey {
99    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
100        Self::from_bytes(bytes)
101    }
102    fn to_bytes(&self) -> Vec<u8> {
103        self.to_bytes()
104    }
105}
106
107// --- Secret key methods ---
108impl EcdhP224SecretKey {
109    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
110        if bytes.len() != ec::P224_SCALAR_SIZE {
111            return Err(ApiError::InvalidLength {
112                context: "EcdhP224SecretKey::from_bytes",
113                expected: ec::P224_SCALAR_SIZE,
114                actual: bytes.len(),
115            });
116        }
117        let mut buffer = SecretBuffer::zeroed();
118        buffer.as_mut().copy_from_slice(bytes);
119        let scalar = ec::Scalar::from_secret_buffer(buffer.clone())
120            .map_err(|e| ApiError::from(KemError::from(e)))?;
121        drop(scalar);
122        Ok(Self(buffer))
123    }
124    pub fn to_bytes(&self) -> ZeroizingBytes {
125        self.0.to_bytes_zeroizing_boxed()
126    }
127    pub fn validate(&self) -> ApiResult<()> {
128        let _ = ec::Scalar::from_secret_buffer(self.0.clone())
129            .map_err(|e| ApiError::from(KemError::from(e)))?;
130        Ok(())
131    }
132}
133
134impl SerializeSecret for EcdhP224SecretKey {
135    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
136        Self::from_bytes(bytes)
137    }
138    fn to_bytes_zeroizing(&self) -> ZeroizingBytes {
139        self.to_bytes()
140    }
141}
142
143// --- Shared secret methods ---
144impl EcdhP224SharedSecret {
145    pub fn to_bytes(&self) -> ZeroizingBytes {
146        self.0.to_bytes_zeroizing_boxed()
147    }
148}
149
150impl SerializeSecret for EcdhP224SharedSecret {
151    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
152        if bytes.len() != ec::P224_KEM_SHARED_SECRET_KDF_OUTPUT_SIZE {
153            return Err(ApiError::InvalidLength {
154                context: "EcdhP224SharedSecret::from_bytes",
155                expected: ec::P224_KEM_SHARED_SECRET_KDF_OUTPUT_SIZE,
156                actual: bytes.len(),
157            });
158        }
159        Ok(Self(ApiKey::new(bytes)))
160    }
161    fn to_bytes_zeroizing(&self) -> ZeroizingBytes {
162        self.to_bytes()
163    }
164}
165
166// --- Ciphertext methods ---
167impl EcdhP224Ciphertext {
168    pub fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
169        if bytes.len() != ec::P224_CIPHERTEXT_SIZE {
170            return Err(ApiError::InvalidLength {
171                context: "EcdhP224Ciphertext::from_bytes",
172                expected: ec::P224_CIPHERTEXT_SIZE,
173                actual: bytes.len(),
174            });
175        }
176        let pk_bytes = &bytes[..ec::P224_POINT_COMPRESSED_SIZE];
177        let point = ec::Point::deserialize_compressed(pk_bytes)
178            .map_err(|e| ApiError::from(KemError::from(e)))?;
179        if point.is_identity() {
180            return Err(ApiError::InvalidCiphertext {
181                context: "EcdhP224Ciphertext::from_bytes",
182                #[cfg(feature = "std")]
183                message: "Ephemeral public key cannot be the identity point".to_string(),
184            });
185        }
186        let mut ct_bytes = [0u8; ec::P224_CIPHERTEXT_SIZE];
187        ct_bytes.copy_from_slice(bytes);
188        Ok(Self(ct_bytes))
189    }
190    pub fn to_bytes(&self) -> Vec<u8> {
191        self.0.to_vec()
192    }
193    pub fn validate(&self) -> ApiResult<()> {
194        let pk_bytes = &self.0[..ec::P224_POINT_COMPRESSED_SIZE];
195        let point = ec::Point::deserialize_compressed(pk_bytes)
196            .map_err(|e| ApiError::from(KemError::from(e)))?;
197        if point.is_identity() {
198            return Err(ApiError::InvalidCiphertext {
199                context: "validate_ciphertext",
200                #[cfg(feature = "std")]
201                message: "Ciphertext contains identity point".to_string(),
202            });
203        }
204        Ok(())
205    }
206}
207
208impl Serialize for EcdhP224Ciphertext {
209    fn from_bytes(bytes: &[u8]) -> ApiResult<Self> {
210        Self::from_bytes(bytes)
211    }
212    fn to_bytes(&self) -> Vec<u8> {
213        self.to_bytes()
214    }
215}
216
217/// Calculate authentication tag for key confirmation
218///
219/// Uses truncated HMAC-SHA256 to create a 16-byte tag that proves
220/// the sender and receiver computed the same shared secret.
221fn calc_auth_tag(shared_secret: &[u8]) -> Result<[u8; ec::P224_TAG_SIZE], KemError> {
222    let mut hmac = Hmac::<Sha256>::new(shared_secret).map_err(KemError::from)?;
223    hmac.update(CONFIRMATION_LABEL).map_err(KemError::from)?;
224
225    // Finalize and get tag (SHA256 produces 32-byte tags)
226    let tag_vec = hmac.finalize().map_err(KemError::from)?;
227
228    // Truncate to P224_TAG_SIZE bytes
229    let mut truncated = [0u8; ec::P224_TAG_SIZE];
230    truncated.copy_from_slice(&tag_vec[..ec::P224_TAG_SIZE]);
231    Ok(truncated)
232}
233
234impl Kem for EcdhP224 {
235    type PublicKey = EcdhP224PublicKey;
236    type SecretKey = EcdhP224SecretKey;
237    type SharedSecret = EcdhP224SharedSecret;
238    type Ciphertext = EcdhP224Ciphertext;
239    type KeyPair = (Self::PublicKey, Self::SecretKey);
240
241    fn name() -> &'static str {
242        "ECDH-P224"
243    }
244
245    fn keypair<R: CryptoRng + RngCore>(rng: &mut R) -> ApiResult<Self::KeyPair> {
246        let (sk_scalar, pk_point) =
247            ec::generate_keypair(rng).map_err(|e| ApiError::from(KemError::from(e)))?;
248        let public_key = EcdhP224PublicKey(pk_point.serialize_compressed());
249        let secret_key = EcdhP224SecretKey(sk_scalar.as_secret_buffer().clone());
250        Ok((public_key, secret_key))
251    }
252
253    fn public_key(keypair: &Self::KeyPair) -> Self::PublicKey {
254        keypair.0.clone()
255    }
256
257    fn secret_key(keypair: &Self::KeyPair) -> Self::SecretKey {
258        keypair.1.clone()
259    }
260
261    fn encapsulate<R: CryptoRng + RngCore>(
262        rng: &mut R,
263        public_key_recipient: &Self::PublicKey,
264    ) -> ApiResult<(Self::Ciphertext, Self::SharedSecret)> {
265        let pk_r_point = ec::Point::deserialize_compressed(&public_key_recipient.0)
266            .map_err(|e| ApiError::from(KemError::from(e)))?;
267        if pk_r_point.is_identity() {
268            return Err(ApiError::InvalidKey {
269                context: "ECDH-P224 encapsulate",
270                #[cfg(feature = "std")]
271                message: "Recipient public key is identity".to_string(),
272            });
273        }
274
275        let (ephemeral_scalar, ephemeral_point) =
276            ec::generate_keypair(rng).map_err(|e| ApiError::from(KemError::from(e)))?;
277        let ephemeral_pk_compressed = ephemeral_point.serialize_compressed();
278
279        let shared_point = ec::scalar_mult(&ephemeral_scalar, &pk_r_point)
280            .map_err(|e| ApiError::from(KemError::from(e)))?;
281        if shared_point.is_identity() {
282            return Err(ApiError::DecryptionFailed {
283                context: "ECDH-P224 encapsulate",
284                #[cfg(feature = "std")]
285                message: "Shared point is identity".to_string(),
286            });
287        }
288        let x_coord_bytes = Zeroizing::new(shared_point.x_coordinate_bytes());
289
290        let kdf_ikm = concat_kdf_ikm(
291            x_coord_bytes.as_ref(),
292            &ephemeral_pk_compressed,
293            &public_key_recipient.0,
294        );
295
296        let ss_bytes = ec::kdf_hkdf_sha256_for_ecdh_kem(&kdf_ikm, Some(KDF_INFO))
297            .map_err(|e| ApiError::from(KemError::from(e)))?;
298
299        let shared_secret = EcdhP224SharedSecret(ApiKey::new(&ss_bytes[..]));
300
301        // Create authenticated ciphertext: ephemeral_pk || tag
302        let mut ct_bytes = [0u8; ec::P224_CIPHERTEXT_SIZE];
303        ct_bytes[..ec::P224_POINT_COMPRESSED_SIZE].copy_from_slice(&ephemeral_pk_compressed);
304        let tag = calc_auth_tag(&ss_bytes[..]).map_err(ApiError::from)?;
305        ct_bytes[ec::P224_POINT_COMPRESSED_SIZE..].copy_from_slice(&tag);
306        let ciphertext = EcdhP224Ciphertext(ct_bytes);
307
308        drop(ephemeral_scalar);
309        Ok((ciphertext, shared_secret))
310    }
311
312    fn decapsulate(
313        secret_key_recipient: &Self::SecretKey,
314        ciphertext_ephemeral_pk: &Self::Ciphertext,
315    ) -> ApiResult<Self::SharedSecret> {
316        // Split ciphertext into ephemeral public key and tag
317        let (pk_bytes, tag_bytes) = ciphertext_ephemeral_pk
318            .0
319            .split_at(ec::P224_POINT_COMPRESSED_SIZE);
320
321        // Convert tag bytes to array for comparison
322        let mut received_tag = [0u8; ec::P224_TAG_SIZE];
323        received_tag.copy_from_slice(tag_bytes);
324
325        let sk_r_scalar = ec::Scalar::from_secret_buffer(secret_key_recipient.0.clone())
326            .map_err(|e| ApiError::from(KemError::from(e)))?;
327        let q_e_point = ec::Point::deserialize_compressed(pk_bytes)
328            .map_err(|e| ApiError::from(KemError::from(e)))?;
329        if q_e_point.is_identity() {
330            return Err(ApiError::InvalidCiphertext {
331                context: "ECDH-P224 decapsulate",
332                #[cfg(feature = "std")]
333                message: "Ephemeral PK is identity".to_string(),
334            });
335        }
336
337        let shared_point = ec::scalar_mult(&sk_r_scalar, &q_e_point)
338            .map_err(|e| ApiError::from(KemError::from(e)))?;
339        if shared_point.is_identity() {
340            return Err(ApiError::DecryptionFailed {
341                context: "ECDH-P224 decapsulate",
342                #[cfg(feature = "std")]
343                message: "Shared point is identity".to_string(),
344            });
345        }
346        let x_coord_bytes = Zeroizing::new(shared_point.x_coordinate_bytes());
347        let q_r_point =
348            ec::scalar_mult_base_g(&sk_r_scalar).map_err(|e| ApiError::from(KemError::from(e)))?;
349
350        let kdf_ikm = concat_kdf_ikm(
351            x_coord_bytes.as_ref(),
352            pk_bytes,
353            &q_r_point.serialize_compressed(),
354        );
355
356        let ss_bytes = ec::kdf_hkdf_sha256_for_ecdh_kem(&kdf_ikm, Some(KDF_INFO))
357            .map_err(|e| ApiError::from(KemError::from(e)))?;
358
359        // Verify authentication tag
360        let expected_tag = calc_auth_tag(&ss_bytes[..]).map_err(ApiError::from)?;
361
362        // Constant-time comparison of tags (array to array)
363        use dcrypt_common::security::SecureCompare;
364        if !received_tag.secure_eq(&expected_tag) {
365            return Err(ApiError::DecryptionFailed {
366                context: "ECDH-P224 decapsulate",
367                #[cfg(feature = "std")]
368                message: "Authentication tag mismatch".to_string(),
369            });
370        }
371
372        Ok(EcdhP224SharedSecret(ApiKey::new(&ss_bytes[..])))
373    }
374}
375
376#[cfg(test)]
377mod tests;