Skip to main content

void_crypto/
ecies.rs

1//! Shared ECIES (Elliptic Curve Integrated Encryption Scheme) operations.
2//!
3//! This module provides common ECIES functions for key derivation and
4//! shared secret validation. All inputs and outputs use typed key newtypes
5//! from void-crypto — raw x25519-dalek types stay internal.
6
7use hkdf::Hkdf;
8use sha2::Sha256;
9use x25519_dalek::{PublicKey, StaticSecret};
10
11use crate::kdf::{RecipientSecretKey, SecretKey};
12use crate::keys::RecipientPubKey;
13
14/// HKDF info parameter for ECIES key derivation.
15pub const ECIES_INFO: &[u8] = b"void-ecies-v1";
16
17/// Errors that can occur in ECIES operations.
18#[derive(Debug, thiserror::Error)]
19pub enum EciesError {
20    #[error("invalid shared secret: possible low-order point attack")]
21    InvalidSharedSecret,
22    #[error("key derivation failed")]
23    KeyDerivationFailed,
24}
25
26/// Perform X25519 DH key exchange and derive a symmetric encryption key.
27///
28/// This function:
29/// 1. Performs X25519 Diffie-Hellman to get a shared secret
30/// 2. Validates the shared secret (rejects low-order points)
31/// 3. Derives a 32-byte key using HKDF-SHA256
32///
33/// # Arguments
34/// * `our_secret` - Our X25519 private key (identity recipient key or ephemeral)
35/// * `their_public` - The other party's X25519 public key
36///
37/// # Returns
38/// A `SecretKey` suitable for AES-256-GCM encryption.
39pub fn perform_dh_and_derive(
40    our_secret: &RecipientSecretKey,
41    their_public: &RecipientPubKey,
42) -> Result<SecretKey, EciesError> {
43    let our_x25519 = StaticSecret::from(*our_secret.as_bytes());
44    let their_x25519 = PublicKey::from(*their_public.as_bytes());
45
46    let shared_secret = our_x25519.diffie_hellman(&their_x25519);
47
48    // Reject low-order point attacks (all-zero shared secret)
49    if !is_valid_shared_secret(shared_secret.as_bytes()) {
50        return Err(EciesError::InvalidSharedSecret);
51    }
52
53    // Derive key with HKDF-SHA256
54    let hk = Hkdf::<Sha256>::new(None, shared_secret.as_bytes());
55    let mut key = [0u8; 32];
56    hk.expand(ECIES_INFO, &mut key)
57        .map_err(|_| EciesError::KeyDerivationFailed)?;
58
59    Ok(SecretKey::new(key))
60}
61
62/// Check if a shared secret is valid (non-zero).
63///
64/// All-zero shared secrets can occur with low-order points and should be rejected.
65fn is_valid_shared_secret(secret: &[u8; 32]) -> bool {
66    secret.iter().any(|&b| b != 0)
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    fn random_recipient_keypair() -> (RecipientSecretKey, RecipientPubKey) {
74        let mut bytes = [0u8; 32];
75        rand::RngCore::fill_bytes(&mut rand::thread_rng(), &mut bytes);
76        let secret = StaticSecret::from(bytes);
77        let public = PublicKey::from(&secret);
78        (
79            RecipientSecretKey::from_bytes(bytes),
80            RecipientPubKey::from_bytes(public.to_bytes()),
81        )
82    }
83
84    #[test]
85    fn is_valid_shared_secret_rejects_zeros() {
86        let zero_secret = [0u8; 32];
87        assert!(!is_valid_shared_secret(&zero_secret));
88    }
89
90    #[test]
91    fn is_valid_shared_secret_accepts_nonzero() {
92        let mut secret = [0u8; 32];
93        secret[0] = 1;
94        assert!(is_valid_shared_secret(&secret));
95
96        secret[0] = 0;
97        secret[31] = 1;
98        assert!(is_valid_shared_secret(&secret));
99    }
100
101    #[test]
102    fn perform_dh_and_derive_roundtrip() {
103        let (alice_secret, alice_public) = random_recipient_keypair();
104        let (bob_secret, bob_public) = random_recipient_keypair();
105
106        // Both should derive the same key
107        let alice_key = perform_dh_and_derive(&alice_secret, &bob_public).unwrap();
108        let bob_key = perform_dh_and_derive(&bob_secret, &alice_public).unwrap();
109
110        assert_eq!(alice_key.as_bytes(), bob_key.as_bytes());
111    }
112
113    #[test]
114    fn perform_dh_and_derive_rejects_zero_public_key() {
115        let (our_secret, _) = random_recipient_keypair();
116        let zero_public = RecipientPubKey::from_bytes([0u8; 32]);
117
118        let result = perform_dh_and_derive(&our_secret, &zero_public);
119        assert!(matches!(result, Err(EciesError::InvalidSharedSecret)));
120    }
121}