Skip to main content

darkpool_crypto/
aes.rs

1use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, BlockEncryptMut, KeyIvInit};
2use aes::Aes128;
3use ethers_core::types::U256;
4
5use crate::error::CryptoError;
6use crate::field::{poseidon_hash, string_to_fr};
7
8type Aes128CbcEnc = cbc::Encryptor<Aes128>;
9type Aes128CbcDec = cbc::Decryptor<Aes128>;
10
11/// Cached KDF domain separation strings -- avoids repeated Poseidon2 permutations.
12#[allow(clippy::expect_used)]
13static KDF_KEY_PURPOSE: std::sync::LazyLock<U256> = std::sync::LazyLock::new(|| {
14    string_to_fr("hisoka.enc_key")
15        .expect("domain string 'hisoka.enc_key' is 14 bytes, always valid")
16});
17
18#[allow(clippy::expect_used)]
19static KDF_IV_PURPOSE: std::sync::LazyLock<U256> = std::sync::LazyLock::new(|| {
20    string_to_fr("hisoka.enc_iv").expect("domain string 'hisoka.enc_iv' is 13 bytes, always valid")
21});
22
23/// Derive AES key (last 16 bytes of Poseidon) and IV from shared secret.
24#[must_use]
25pub fn kdf_to_aes_key_iv(shared_secret: U256) -> ([u8; 16], [u8; 16]) {
26    let key_purpose = *KDF_KEY_PURPOSE;
27    let iv_purpose = *KDF_IV_PURPOSE;
28
29    let key_fr = poseidon_hash(&[shared_secret, key_purpose]);
30    let iv_fr = poseidon_hash(&[shared_secret, iv_purpose]);
31
32    let mut key_bytes = [0u8; 32];
33    let mut iv_bytes = [0u8; 32];
34    key_fr.to_big_endian(&mut key_bytes);
35    iv_fr.to_big_endian(&mut iv_bytes);
36
37    let mut key = [0u8; 16];
38    let mut iv = [0u8; 16];
39    key.copy_from_slice(&key_bytes[16..]);
40    iv.copy_from_slice(&iv_bytes[16..]);
41
42    (key, iv)
43}
44
45/// Encrypt 192-byte plaintext using AES-128-CBC with PKCS#7 padding. Returns 208 bytes.
46#[must_use]
47#[allow(clippy::expect_used)]
48pub fn aes128_encrypt(plaintext: &[u8; 192], key: &[u8; 16], iv: &[u8; 16]) -> [u8; 208] {
49    let mut buf = [0u8; 208];
50    buf[..192].copy_from_slice(plaintext);
51
52    let cipher = Aes128CbcEnc::new(key.into(), iv.into());
53    // SAFETY: 208-byte buffer always fits 192 bytes + PKCS#7 padding block
54    let ciphertext = cipher
55        .encrypt_padded_mut::<Pkcs7>(&mut buf, 192)
56        .expect("buffer is correct size");
57
58    let mut result = [0u8; 208];
59    result.copy_from_slice(ciphertext);
60    result
61}
62
63/// Decrypt 208-byte AES-128-CBC ciphertext, returning 192 bytes on success.
64pub fn aes128_decrypt(
65    ciphertext: &[u8; 208],
66    key: &[u8; 16],
67    iv: &[u8; 16],
68) -> Result<[u8; 192], CryptoError> {
69    let mut buf = [0u8; 208];
70    buf.copy_from_slice(ciphertext);
71
72    let cipher = Aes128CbcDec::new(key.into(), iv.into());
73    let plaintext = cipher
74        .decrypt_padded_mut::<Pkcs7>(&mut buf)
75        .map_err(|_| CryptoError::DecryptionFailed("invalid PKCS#7 padding".to_string()))?;
76
77    if plaintext.len() != 192 {
78        return Err(CryptoError::DecryptionFailed(format!(
79            "expected 192 bytes plaintext, got {}",
80            plaintext.len()
81        )));
82    }
83
84    let mut result = [0u8; 192];
85    result.copy_from_slice(plaintext);
86    Ok(result)
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_kdf_produces_valid_lengths() {
95        let shared_secret = U256::from(12345u64);
96        let (key, iv) = kdf_to_aes_key_iv(shared_secret);
97
98        assert_eq!(key.len(), 16);
99        assert_eq!(iv.len(), 16);
100    }
101
102    #[test]
103    fn test_kdf_deterministic() {
104        let shared_secret = U256::from(987654321u64);
105        let (key1, iv1) = kdf_to_aes_key_iv(shared_secret);
106        let (key2, iv2) = kdf_to_aes_key_iv(shared_secret);
107
108        assert_eq!(key1, key2);
109        assert_eq!(iv1, iv2);
110    }
111
112    #[test]
113    fn test_kdf_key_iv_different() {
114        let shared_secret = U256::from(123456789u64);
115        let (key, iv) = kdf_to_aes_key_iv(shared_secret);
116
117        assert_ne!(key, iv);
118    }
119
120    #[test]
121    fn test_aes_encrypt_decrypt_roundtrip() {
122        let key = [0x12u8; 16];
123        let iv = [0x34u8; 16];
124        let plaintext = [0x42u8; 192];
125
126        let ciphertext = aes128_encrypt(&plaintext, &key, &iv);
127        assert_eq!(ciphertext.len(), 208);
128        assert_ne!(&ciphertext[..192], &plaintext[..]);
129
130        let decrypted = aes128_decrypt(&ciphertext, &key, &iv).expect("decryption should succeed");
131        assert_eq!(decrypted, plaintext);
132    }
133
134    #[test]
135    fn test_aes_different_keys_different_ciphertext() {
136        let key1 = [0x11u8; 16];
137        let key2 = [0x22u8; 16];
138        let iv = [0x00u8; 16];
139        let plaintext = [0x55u8; 192];
140
141        let ct1 = aes128_encrypt(&plaintext, &key1, &iv);
142        let ct2 = aes128_encrypt(&plaintext, &key2, &iv);
143
144        assert_ne!(ct1, ct2);
145    }
146
147    #[test]
148    fn test_aes_wrong_key_fails() {
149        let key = [0x12u8; 16];
150        let wrong_key = [0x99u8; 16];
151        let iv = [0x34u8; 16];
152        let plaintext = [0x42u8; 192];
153
154        let ciphertext = aes128_encrypt(&plaintext, &key, &iv);
155        let result = aes128_decrypt(&ciphertext, &wrong_key, &iv);
156
157        assert!(result.is_err());
158    }
159
160    #[test]
161    fn test_aes_wrong_iv_fails() {
162        let key = [0x12u8; 16];
163        let iv = [0x34u8; 16];
164        let wrong_iv = [0x99u8; 16];
165        let plaintext = [0x42u8; 192];
166
167        let ciphertext = aes128_encrypt(&plaintext, &key, &iv);
168        let result = aes128_decrypt(&ciphertext, &key, &wrong_iv);
169
170        if let Ok(decrypted) = result {
171            assert_ne!(decrypted, plaintext);
172        }
173    }
174
175    #[test]
176    fn test_aes_tampered_ciphertext() {
177        let key = [0x12u8; 16];
178        let iv = [0x34u8; 16];
179        let plaintext = [0x42u8; 192];
180
181        let mut ciphertext = aes128_encrypt(&plaintext, &key, &iv);
182
183        ciphertext[207] ^= 0x01;
184
185        let result = aes128_decrypt(&ciphertext, &key, &iv);
186        assert!(result.is_err());
187    }
188
189    #[test]
190    fn test_kdf_different_secrets() {
191        let (key1, iv1) = kdf_to_aes_key_iv(U256::from(1u64));
192        let (key2, iv2) = kdf_to_aes_key_iv(U256::from(2u64));
193
194        assert_ne!(key1, key2);
195        assert_ne!(iv1, iv2);
196    }
197
198    #[test]
199    fn test_full_kdf_encrypt_decrypt_roundtrip() {
200        let shared_secret = U256::from(0xDEADBEEFu64);
201        let (key, iv) = kdf_to_aes_key_iv(shared_secret);
202
203        let mut plaintext = [0u8; 192];
204        for (i, byte) in plaintext.iter_mut().enumerate() {
205            *byte = (i % 256) as u8;
206        }
207
208        let ciphertext = aes128_encrypt(&plaintext, &key, &iv);
209        let decrypted = aes128_decrypt(&ciphertext, &key, &iv).expect("should decrypt");
210        assert_eq!(decrypted, plaintext);
211    }
212
213    #[test]
214    fn test_aes_ciphertext_length() {
215        let key = [0xAAu8; 16];
216        let iv = [0xBBu8; 16];
217        let plaintext = [0u8; 192];
218
219        let ct = aes128_encrypt(&plaintext, &key, &iv);
220        assert_eq!(ct.len(), 208);
221    }
222
223    #[test]
224    fn test_decrypt_error_variant() {
225        let key = [0x12u8; 16];
226        let wrong_key = [0x99u8; 16];
227        let iv = [0x34u8; 16];
228        let plaintext = [0x42u8; 192];
229
230        let ciphertext = aes128_encrypt(&plaintext, &key, &iv);
231        let result = aes128_decrypt(&ciphertext, &wrong_key, &iv);
232
233        if let Err(e) = result {
234            match e {
235                CryptoError::DecryptionFailed(_) => {} // correct
236                other => panic!("Expected DecryptionFailed, got: {other:?}"),
237            }
238        }
239    }
240
241    #[test]
242    fn test_aes128_parity_with_typescript() {
243        let key = [0u8; 16];
244        let iv = [0u8; 16];
245        let plaintext = [1u8; 192];
246
247        let ct = aes128_encrypt(&plaintext, &key, &iv);
248        let ct_hex = hex::encode(ct);
249
250        let expected_hex = "e14d5d0ee27715df08b4152ba23da8e066224f25d2578c169989600e70029eac0f990cdc49f2d1fbeca95ad327def624092611708a75d10f1476b8ed6499538208f47f32921f2f184cd4323eb9d24935ea8316bc08ad5e26b76f839e93cf1e26217f8c3755e8345a5d3fc257235b7c5728814627e7c922096920484cefc7dc83a9d39bfc7abb06c8576c247a650edd007ed65ff16cd8ea38a80d8b055f3ded1748499828bcc7c0baf772b5c08f4b7dc0deead18cab10f6904fea5e4ffb8b9fdd1e83330fd492c872204b49b8105231a4";
251
252        assert_eq!(
253            ct_hex, expected_hex,
254            "AES-128-CBC ciphertext mismatch!\n  Rust: {ct_hex}\n  TS:   {expected_hex}"
255        );
256    }
257}