Skip to main content

shadowforge_lib/adapters/
crypto.rs

1//! Cryptographic adapters — ML-KEM-1024, ML-DSA-87, and AES-256-GCM.
2//!
3//! Each struct implements the corresponding port trait from
4//! [`crate::domain::ports`] and wires in a `ChaCha20Rng` seeded from the OS
5//! entropy source at each call, providing forward secrecy between calls.
6
7use bytes::Bytes;
8use rand_chacha::ChaCha20Rng;
9use rand_core::SeedableRng;
10
11use crate::domain::crypto::{
12    decapsulate_kem, decrypt_aes_gcm, encapsulate_kem, encrypt_aes_gcm, generate_dsa_keypair,
13    generate_kem_keypair, sign_dsa, verify_dsa,
14};
15use crate::domain::errors::CryptoError;
16use crate::domain::ports::{Encryptor, Signer, SymmetricCipher};
17use crate::domain::types::{KeyPair, Signature};
18
19// ─── Helpers ──────────────────────────────────────────────────────────────────────────────────────
20
21/// Construct a `ChaCha20Rng` freshly seeded from the OS entropy source.
22fn fresh_rng() -> ChaCha20Rng {
23    ChaCha20Rng::from_rng(&mut rand::rng())
24}
25
26// ─── MlKemEncryptor ───────────────────────────────────────────────────────────
27
28/// ML-KEM-1024 key-encapsulation adapter.
29///
30/// Implements the [`Encryptor`] port using the `ml-kem` crate (NIST FIPS 203).
31/// Each call seeds a fresh `ChaCha20Rng` from the OS, ensuring forward
32/// secrecy between calls.
33#[derive(Debug, Default)]
34pub struct MlKemEncryptor;
35
36impl Encryptor for MlKemEncryptor {
37    fn generate_keypair(&self) -> Result<KeyPair, CryptoError> {
38        generate_kem_keypair(&mut fresh_rng())
39    }
40
41    fn encapsulate(&self, public_key: &[u8]) -> Result<(Bytes, Bytes), CryptoError> {
42        encapsulate_kem(public_key, &mut fresh_rng())
43    }
44
45    fn decapsulate(&self, secret_key: &[u8], ciphertext: &[u8]) -> Result<Bytes, CryptoError> {
46        decapsulate_kem(secret_key, ciphertext)
47    }
48}
49
50// ─── MlDsaSigner ─────────────────────────────────────────────────────────────
51
52/// ML-DSA-87 digital signature adapter.
53///
54/// Implements the [`Signer`] port using the `ml-dsa` crate (NIST FIPS 204).
55/// Signing is deterministic (no per-call randomness) for auditability.
56#[derive(Debug, Default)]
57pub struct MlDsaSigner;
58
59impl Signer for MlDsaSigner {
60    fn generate_keypair(&self) -> Result<KeyPair, CryptoError> {
61        generate_dsa_keypair(&mut fresh_rng())
62    }
63
64    fn sign(&self, secret_key: &[u8], message: &[u8]) -> Result<Signature, CryptoError> {
65        sign_dsa(secret_key, message)
66    }
67
68    fn verify(
69        &self,
70        public_key: &[u8],
71        message: &[u8],
72        signature: &Signature,
73    ) -> Result<bool, CryptoError> {
74        verify_dsa(public_key, message, signature)
75    }
76}
77
78// ─── Aes256GcmCipher ──────────────────────────────────────────────────────────
79
80/// AES-256-GCM symmetric cipher adapter.
81///
82/// Implements the [`SymmetricCipher`] port using the `aes-gcm` crate.
83#[derive(Debug, Default)]
84pub struct Aes256GcmCipher;
85
86impl SymmetricCipher for Aes256GcmCipher {
87    fn encrypt(&self, key: &[u8], nonce: &[u8], plaintext: &[u8]) -> Result<Bytes, CryptoError> {
88        encrypt_aes_gcm(key, nonce, plaintext)
89    }
90
91    fn decrypt(&self, key: &[u8], nonce: &[u8], ciphertext: &[u8]) -> Result<Bytes, CryptoError> {
92        decrypt_aes_gcm(key, nonce, ciphertext)
93    }
94}
95
96// ─── Tests ────────────────────────────────────────────────────────────────────
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    type TestResult = Result<(), Box<dyn std::error::Error>>;
103
104    #[test]
105    fn test_encryptor_adapter_roundtrip() -> TestResult {
106        let enc = MlKemEncryptor;
107        let kp = enc.generate_keypair()?;
108        let (ct, ss1) = enc.encapsulate(&kp.public_key)?;
109        let ss2 = enc.decapsulate(&kp.secret_key, &ct)?;
110        assert_eq!(ss1.as_ref(), ss2.as_ref());
111        Ok(())
112    }
113
114    #[test]
115    fn test_signer_adapter_roundtrip() -> TestResult {
116        let signer = MlDsaSigner;
117        let kp = signer.generate_keypair()?;
118        let msg = b"test message for adapter";
119        let sig = signer.sign(&kp.secret_key, msg)?;
120        let ok = signer.verify(&kp.public_key, msg, &sig)?;
121        assert!(ok, "valid sig must verify via adapter");
122        Ok(())
123    }
124
125    #[test]
126    fn test_signer_adapter_wrong_message() -> TestResult {
127        let signer = MlDsaSigner;
128        let kp = signer.generate_keypair()?;
129        let sig = signer.sign(&kp.secret_key, b"original")?;
130        let ok = signer.verify(&kp.public_key, b"tampered", &sig)?;
131        assert!(
132            !ok,
133            "sig over original must not verify against tampered msg"
134        );
135        Ok(())
136    }
137
138    #[test]
139    fn test_symmetric_adapter_roundtrip() -> TestResult {
140        let cipher = Aes256GcmCipher;
141        let key = vec![0u8; 32];
142        let nonce = vec![1u8; 12];
143        let plaintext = b"test message";
144        let ciphertext = cipher.encrypt(&key, &nonce, plaintext)?;
145        let recovered = cipher.decrypt(&key, &nonce, &ciphertext)?;
146        assert_eq!(recovered.as_ref(), plaintext);
147        Ok(())
148    }
149
150    #[test]
151    fn test_symmetric_adapter_tamper() -> TestResult {
152        let cipher = Aes256GcmCipher;
153        let key = vec![0u8; 32];
154        let nonce = vec![1u8; 12];
155        let plaintext = b"test message";
156        let mut ciphertext = cipher.encrypt(&key, &nonce, plaintext)?.to_vec();
157        *ciphertext.get_mut(0).ok_or("out of bounds")? ^= 0xFF;
158        let result = cipher.decrypt(&key, &nonce, &ciphertext);
159        assert!(result.is_err(), "tampered ciphertext must fail to decrypt");
160        Ok(())
161    }
162}