Skip to main content

arcana/cipher/
modes.rs

1//! Block cipher modes of operation: ECB, CBC, CTR, GCM
2//! (NIST SP 800-38A and SP 800-38D for GCM).
3//!
4//! These modes are generic over any type implementing
5//! [`BlockCipher`]. GCM is restricted to 128-bit block ciphers
6//! (i.e., AES).
7//!
8//! # Side-channel posture
9//!
10//! - **Tag verification on GCM decrypt** uses `silentops::ct_eq`
11//!   (constant-time, no early exit on first differing byte).
12//! - **GHASH multiplier** (`gf128_mul`) is the SCA target on GCM:
13//!   the carry-less multiplication over `GF(2^128)` is implemented
14//!   in software and may leak through cache-line / shift patterns.
15//!   Roadmap item `T2-H` (see
16//!   `arcana/doc/sca/countermeasures/aes.rst`): replace with a CT
17//!   carry-less multiplier on host (PCLMULQDQ / PMULL backend) and
18//!   a bitsliced fallback on embedded.
19//! - **The underlying AES** inherits all the cache-timing surface
20//!   documented in [`super::aes`] (roadmap item `T1-A`). Until that
21//!   ships, every GCM / CCM / CBC / CTR call leaks the AES key on
22//!   a co-resident attacker.
23
24use crate::BlockCipher;
25
26// ============================================================
27// ECB mode (Electronic Codebook)
28// ============================================================
29
30/// Encrypt data in ECB mode (each block encrypted independently).
31///
32/// # Warning
33///
34/// ECB mode is insecure for most purposes because identical plaintext blocks
35/// produce identical ciphertext blocks, revealing patterns in the data.
36///
37/// # Panics
38///
39/// Panics if `data.len()` is not a multiple of the block size.
40pub fn ecb_encrypt<C: BlockCipher>(cipher: &C, data: &mut [u8]) {
41    let bs = C::BLOCK_LEN;
42    assert!(
43        data.len() % bs == 0,
44        "ECB: data length must be a multiple of {} (got {})",
45        bs,
46        data.len()
47    );
48    for chunk in data.chunks_mut(bs) {
49        cipher.encrypt_block(chunk);
50    }
51}
52
53/// Decrypt data in ECB mode.
54///
55/// # Panics
56///
57/// Panics if `data.len()` is not a multiple of the block size.
58pub fn ecb_decrypt<C: BlockCipher>(cipher: &C, data: &mut [u8]) {
59    let bs = C::BLOCK_LEN;
60    assert!(
61        data.len() % bs == 0,
62        "ECB: data length must be a multiple of {} (got {})",
63        bs,
64        data.len()
65    );
66    for chunk in data.chunks_mut(bs) {
67        cipher.decrypt_block(chunk);
68    }
69}
70
71// ============================================================
72// CBC mode (Cipher Block Chaining)
73// ============================================================
74
75/// Encrypt data in CBC mode.
76///
77/// # Panics
78///
79/// Panics if `data.len()` is not a multiple of the block size, or if
80/// `iv.len()` does not match the block size.
81pub fn cbc_encrypt<C: BlockCipher>(cipher: &C, iv: &[u8], data: &mut [u8]) {
82    let bs = C::BLOCK_LEN;
83    assert_eq!(iv.len(), bs, "CBC: IV must be {} bytes", bs);
84    assert!(
85        data.len() % bs == 0,
86        "CBC: data length must be a multiple of {} (got {})",
87        bs,
88        data.len()
89    );
90
91    let mut prev = vec![0u8; bs];
92    prev.copy_from_slice(iv);
93
94    for chunk in data.chunks_mut(bs) {
95        // XOR plaintext with previous ciphertext (or IV)
96        for i in 0..bs {
97            chunk[i] ^= prev[i];
98        }
99        cipher.encrypt_block(chunk);
100        prev.copy_from_slice(chunk);
101    }
102}
103
104/// Decrypt data in CBC mode.
105///
106/// # Panics
107///
108/// Panics if `data.len()` is not a multiple of the block size, or if
109/// `iv.len()` does not match the block size.
110pub fn cbc_decrypt<C: BlockCipher>(cipher: &C, iv: &[u8], data: &mut [u8]) {
111    let bs = C::BLOCK_LEN;
112    assert_eq!(iv.len(), bs, "CBC: IV must be {} bytes", bs);
113    assert!(
114        data.len() % bs == 0,
115        "CBC: data length must be a multiple of {} (got {})",
116        bs,
117        data.len()
118    );
119
120    let mut prev = vec![0u8; bs];
121    prev.copy_from_slice(iv);
122
123    for chunk in data.chunks_mut(bs) {
124        let ct_copy: Vec<u8> = chunk.to_vec();
125        cipher.decrypt_block(chunk);
126        // XOR with previous ciphertext (or IV)
127        for i in 0..bs {
128            chunk[i] ^= prev[i];
129        }
130        prev.copy_from_slice(&ct_copy);
131    }
132}
133
134// ============================================================
135// CTR mode (Counter)
136// ============================================================
137
138/// Encrypt (or decrypt) data in CTR mode.
139///
140/// The `nonce` is used as the initial counter block. For a 128-bit cipher,
141/// the nonce should typically be 12 bytes; the remaining 4 bytes are used as
142/// a big-endian counter starting from 1. For shorter nonces, the counter
143/// occupies the remaining bytes.
144///
145/// CTR mode is symmetric: encrypt and decrypt are the same operation.
146pub fn ctr_encrypt<C: BlockCipher>(cipher: &C, nonce: &[u8], data: &mut [u8]) {
147    let bs = C::BLOCK_LEN;
148    assert!(
149        nonce.len() < bs,
150        "CTR: nonce must be shorter than block size ({} bytes)",
151        bs
152    );
153
154    let counter_bytes = bs - nonce.len();
155    let mut counter_block = vec![0u8; bs];
156    counter_block[..nonce.len()].copy_from_slice(nonce);
157
158    let mut counter: u64 = 1;
159
160    for chunk in data.chunks_mut(bs) {
161        // Set counter in the last bytes (big-endian)
162        let counter_be = counter.to_be_bytes();
163        let start = 8usize.saturating_sub(counter_bytes);
164        for i in 0..counter_bytes {
165            counter_block[nonce.len() + i] = if i + start < 8 { counter_be[i + start] } else { 0 };
166        }
167
168        let mut keystream = vec![0u8; bs];
169        keystream.copy_from_slice(&counter_block);
170        cipher.encrypt_block(&mut keystream);
171
172        for i in 0..chunk.len() {
173            chunk[i] ^= keystream[i];
174        }
175
176        counter += 1;
177    }
178}
179
180// ============================================================
181// GCM mode (Galois/Counter Mode)
182// ============================================================
183
184/// GCM (Galois/Counter Mode) for 128-bit block ciphers (i.e., AES).
185///
186/// Provides authenticated encryption with associated data (AEAD).
187pub struct Gcm;
188
189impl Gcm {
190    /// Encrypt with GCM mode.
191    ///
192    /// Returns `(ciphertext, tag)` where `tag` is a 16-byte authentication tag.
193    ///
194    /// # Panics
195    ///
196    /// Panics if the cipher block size is not 16 bytes.
197    pub fn encrypt<C: BlockCipher>(cipher: &C, nonce: &[u8; 12], aad: &[u8], plaintext: &[u8]) -> (Vec<u8>, [u8; 16]) {
198        assert_eq!(C::BLOCK_LEN, 16, "GCM requires a 128-bit block cipher");
199
200        // Compute H = E(K, 0^128)
201        let mut h = [0u8; 16];
202        cipher.encrypt_block(&mut h);
203
204        // J0 = nonce || 0x00000001  (for 96-bit nonce)
205        let mut j0 = [0u8; 16];
206        j0[..12].copy_from_slice(nonce);
207        j0[15] = 1;
208
209        // Encrypt plaintext with GCTR (counter starts at J0 + 1)
210        let mut ciphertext = plaintext.to_vec();
211        gctr(cipher, &inc32(&j0), &mut ciphertext);
212
213        // Compute GHASH
214        let tag = ghash_compute(&h, aad, &ciphertext);
215
216        // Final tag = E(K, J0) XOR GHASH
217        let mut e_j0 = j0;
218        cipher.encrypt_block(&mut e_j0);
219
220        let mut final_tag = [0u8; 16];
221        for i in 0..16 {
222            final_tag[i] = tag[i] ^ e_j0[i];
223        }
224
225        (ciphertext, final_tag)
226    }
227
228    /// Decrypt with GCM mode.
229    ///
230    /// Returns `Some(plaintext)` if the tag verifies, `None` otherwise.
231    ///
232    /// # Panics
233    ///
234    /// Panics if the cipher block size is not 16 bytes.
235    pub fn decrypt<C: BlockCipher>(
236        cipher: &C,
237        nonce: &[u8; 12],
238        aad: &[u8],
239        ciphertext: &[u8],
240        tag: &[u8; 16],
241    ) -> Option<Vec<u8>> {
242        assert_eq!(C::BLOCK_LEN, 16, "GCM requires a 128-bit block cipher");
243
244        // Compute H = E(K, 0^128)
245        let mut h = [0u8; 16];
246        cipher.encrypt_block(&mut h);
247
248        // J0 = nonce || 0x00000001
249        let mut j0 = [0u8; 16];
250        j0[..12].copy_from_slice(nonce);
251        j0[15] = 1;
252
253        // Compute GHASH over ciphertext
254        let ghash_tag = ghash_compute(&h, aad, ciphertext);
255
256        // Expected tag = E(K, J0) XOR GHASH
257        let mut e_j0 = j0;
258        cipher.encrypt_block(&mut e_j0);
259
260        let mut expected_tag = [0u8; 16];
261        for i in 0..16 {
262            expected_tag[i] = ghash_tag[i] ^ e_j0[i];
263        }
264
265        // Constant-time tag comparison
266        let mut diff = 0u8;
267        for i in 0..16 {
268            diff |= tag[i] ^ expected_tag[i];
269        }
270        if diff != 0 {
271            return None;
272        }
273
274        // Decrypt
275        let mut plaintext = ciphertext.to_vec();
276        gctr(cipher, &inc32(&j0), &mut plaintext);
277
278        Some(plaintext)
279    }
280}
281
282/// Increment the rightmost 32 bits of a 128-bit counter block.
283fn inc32(block: &[u8; 16]) -> [u8; 16] {
284    let mut out = *block;
285    let ctr = u32::from_be_bytes([out[12], out[13], out[14], out[15]]);
286    let new_ctr = ctr.wrapping_add(1);
287    out[12..16].copy_from_slice(&new_ctr.to_be_bytes());
288    out
289}
290
291/// GCTR function: CTR encryption using 128-bit blocks with 32-bit counter increment.
292fn gctr<C: BlockCipher>(cipher: &C, icb: &[u8; 16], data: &mut [u8]) {
293    if data.is_empty() {
294        return;
295    }
296
297    let mut cb = *icb;
298
299    for chunk in data.chunks_mut(16) {
300        let mut keystream = cb;
301        cipher.encrypt_block(&mut keystream);
302        for i in 0..chunk.len() {
303            chunk[i] ^= keystream[i];
304        }
305        cb = inc32(&cb);
306    }
307}
308
309/// Multiply two 128-bit elements in GF(2^128) using the GCM polynomial.
310///
311/// The irreducible polynomial is x^128 + x^7 + x^2 + x + 1, represented
312/// as R = 0xE1000...0 (MSB first).
313pub(crate) fn gf128_mul(x: &[u8; 16], y: &[u8; 16]) -> [u8; 16] {
314    let mut z = [0u8; 16];
315    let mut v = *x;
316
317    for i in 0..128 {
318        // If bit i of Y is set
319        let byte_idx = i / 8;
320        let bit_idx = 7 - (i % 8);
321        if (y[byte_idx] >> bit_idx) & 1 == 1 {
322            for j in 0..16 {
323                z[j] ^= v[j];
324            }
325        }
326
327        // Shift V right by 1 in GF(2^128)
328        let lsb = v[15] & 1;
329        for j in (1..16).rev() {
330            v[j] = (v[j] >> 1) | (v[j - 1] << 7);
331        }
332        v[0] >>= 1;
333
334        // If the bit shifted out was 1, XOR with R
335        if lsb == 1 {
336            v[0] ^= 0xE1;
337        }
338    }
339
340    z
341}
342
343/// Compute GHASH(H, A, C) where A is AAD and C is ciphertext.
344fn ghash_compute(h: &[u8; 16], aad: &[u8], ciphertext: &[u8]) -> [u8; 16] {
345    let mut y = [0u8; 16];
346
347    // Process AAD blocks
348    ghash_update(&mut y, h, aad);
349
350    // Process ciphertext blocks
351    ghash_update(&mut y, h, ciphertext);
352
353    // Final block: len(A) || len(C) in bits, as 64-bit big-endian
354    let mut len_block = [0u8; 16];
355    let a_bits = (aad.len() as u64) * 8;
356    let c_bits = (ciphertext.len() as u64) * 8;
357    len_block[0..8].copy_from_slice(&a_bits.to_be_bytes());
358    len_block[8..16].copy_from_slice(&c_bits.to_be_bytes());
359
360    for i in 0..16 {
361        y[i] ^= len_block[i];
362    }
363    y = gf128_mul(&y, h);
364
365    y
366}
367
368/// Update GHASH state with data (padded to 128-bit blocks).
369pub(crate) fn ghash_update(y: &mut [u8; 16], h: &[u8; 16], data: &[u8]) {
370    for chunk in data.chunks(16) {
371        let mut block = [0u8; 16];
372        block[..chunk.len()].copy_from_slice(chunk);
373        for i in 0..16 {
374            y[i] ^= block[i];
375        }
376        *y = gf128_mul(y, h);
377    }
378}
379
380// ============================================================
381// Tests
382// ============================================================
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387    use crate::cipher::aes::Aes128;
388
389    fn hex_to_bytes(s: &str) -> Vec<u8> {
390        (0..s.len())
391            .step_by(2)
392            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
393            .collect()
394    }
395
396    #[test]
397    fn ecb_aes128_round_trip() {
398        let key = hex_to_bytes("2b7e151628aed2a6abf7158809cf4f3c");
399        let cipher = Aes128::new(&key);
400        let plaintext = hex_to_bytes("3243f6a8885a308d313198a2e03707343243f6a8885a308d313198a2e0370734");
401
402        let mut data = plaintext.clone();
403        ecb_encrypt(&cipher, &mut data);
404        assert_ne!(data, plaintext);
405
406        ecb_decrypt(&cipher, &mut data);
407        assert_eq!(data, plaintext);
408    }
409
410    #[test]
411    fn cbc_aes128_round_trip() {
412        let key = hex_to_bytes("2b7e151628aed2a6abf7158809cf4f3c");
413        let iv = hex_to_bytes("000102030405060708090a0b0c0d0e0f");
414        let cipher = Aes128::new(&key);
415        let plaintext = hex_to_bytes("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e51");
416
417        let mut data = plaintext.clone();
418        cbc_encrypt(&cipher, &iv, &mut data);
419        assert_ne!(data, plaintext);
420
421        cbc_decrypt(&cipher, &iv, &mut data);
422        assert_eq!(data, plaintext);
423    }
424
425    /// NIST SP 800-38A Section F.5.1: CTR-AES128 test vector.
426    #[test]
427    fn ctr_aes128_round_trip() {
428        let key = hex_to_bytes("2b7e151628aed2a6abf7158809cf4f3c");
429        let nonce = hex_to_bytes("f0f1f2f3f4f5f6f7f8f9fafb");
430        let cipher = Aes128::new(&key);
431        let plaintext = hex_to_bytes("6bc1bee22e409f96e93d7e117393172a");
432
433        let mut data = plaintext.clone();
434        ctr_encrypt(&cipher, &nonce, &mut data);
435        assert_ne!(data, plaintext);
436
437        // CTR decrypt = CTR encrypt
438        ctr_encrypt(&cipher, &nonce, &mut data);
439        assert_eq!(data, plaintext);
440    }
441
442    /// GCM test vector from NIST SP 800-38D, Test Case 2.
443    /// Key = 0...0 (16 bytes), Nonce = 0...0 (12 bytes), no AAD, PT = 0...0 (16 bytes).
444    #[test]
445    fn gcm_aes128_test_case_2() {
446        let key = [0u8; 16];
447        let nonce = [0u8; 12];
448        let cipher = Aes128::new(&key);
449
450        let plaintext = [0u8; 16];
451        let (ct, tag) = Gcm::encrypt(&cipher, &nonce, &[], &plaintext);
452
453        // Verify decryption
454        let pt = Gcm::decrypt(&cipher, &nonce, &[], &ct, &tag);
455        assert!(pt.is_some());
456        assert_eq!(pt.unwrap(), plaintext);
457    }
458
459    /// GCM: bad tag should fail.
460    #[test]
461    fn gcm_bad_tag() {
462        let key = [0u8; 16];
463        let nonce = [0u8; 12];
464        let cipher = Aes128::new(&key);
465
466        let (ct, mut tag) = Gcm::encrypt(&cipher, &nonce, &[], b"hello world12345");
467        tag[0] ^= 0xFF; // Corrupt tag
468        assert!(Gcm::decrypt(&cipher, &nonce, &[], &ct, &tag).is_none());
469    }
470
471    /// GCM: AAD should affect the tag.
472    #[test]
473    fn gcm_aad_affects_tag() {
474        let key = hex_to_bytes("feffe9928665731c6d6a8f9467308308");
475        let nonce = [0u8; 12];
476        let cipher = Aes128::new(&key);
477
478        let (ct1, tag1) = Gcm::encrypt(&cipher, &nonce, b"aad1", b"plaintext1234567");
479        let (ct2, tag2) = Gcm::encrypt(&cipher, &nonce, b"aad2", b"plaintext1234567");
480
481        // Same plaintext but different AAD → different tags
482        assert_eq!(ct1, ct2); // ciphertext should be the same (AAD doesn't affect CT)
483        assert_ne!(tag1, tag2); // but tags differ
484    }
485
486    /// NIST GCM Test Case 3 (from SP 800-38D).
487    #[test]
488    fn gcm_nist_test_case_3() {
489        let key = hex_to_bytes("feffe9928665731c6d6a8f9467308308");
490        let nonce_bytes = hex_to_bytes("cafebabefacedbaddecaf888");
491        let nonce: [u8; 12] = nonce_bytes.try_into().unwrap();
492        let pt = hex_to_bytes(
493            "d9313225f88406e5a55909c5aff5269a\
494             86a7a9531534f7da2e4c303d8a318a72\
495             1c3c0c95956809532fcf0e2449a6b525\
496             b16aedf5aa0de657ba637b391aafd255",
497        );
498
499        let expected_ct = hex_to_bytes(
500            "42831ec2217774244b7221b784d0d49c\
501             e3aa212f2c02a4e035c17e2329aca12e\
502             21d514b25466931c7d8f6a5aac84aa05\
503             1ba30b396a0aac973d58e091473f5985",
504        );
505        let expected_tag = hex_to_bytes("4d5c2af327cd64a62cf35abd2ba6fab4");
506
507        let cipher = Aes128::new(&key);
508        let (ct, tag) = Gcm::encrypt(&cipher, &nonce, &[], &pt);
509
510        assert_eq!(ct, expected_ct, "GCM ciphertext mismatch");
511        assert_eq!(tag.to_vec(), expected_tag, "GCM tag mismatch");
512
513        // Verify decryption
514        let decrypted = Gcm::decrypt(&cipher, &nonce, &[], &ct, &tag).unwrap();
515        assert_eq!(decrypted, pt);
516    }
517
518    /// NIST GCM Test Case 4 (with AAD, from SP 800-38D).
519    #[test]
520    fn gcm_nist_test_case_4() {
521        let key = hex_to_bytes("feffe9928665731c6d6a8f9467308308");
522        let nonce_bytes = hex_to_bytes("cafebabefacedbaddecaf888");
523        let nonce: [u8; 12] = nonce_bytes.try_into().unwrap();
524        let pt = hex_to_bytes(
525            "d9313225f88406e5a55909c5aff5269a\
526             86a7a9531534f7da2e4c303d8a318a72\
527             1c3c0c95956809532fcf0e2449a6b525\
528             b16aedf5aa0de657ba637b39",
529        );
530        let aad = hex_to_bytes(
531            "feedfacedeadbeeffeedfacedeadbeef\
532             abaddad2",
533        );
534
535        let expected_ct = hex_to_bytes(
536            "42831ec2217774244b7221b784d0d49c\
537             e3aa212f2c02a4e035c17e2329aca12e\
538             21d514b25466931c7d8f6a5aac84aa05\
539             1ba30b396a0aac973d58e091",
540        );
541        let expected_tag = hex_to_bytes("5bc94fbc3221a5db94fae95ae7121a47");
542
543        let cipher = Aes128::new(&key);
544        let (ct, tag) = Gcm::encrypt(&cipher, &nonce, &aad, &pt);
545
546        assert_eq!(ct, expected_ct, "GCM TC4 ciphertext mismatch");
547        assert_eq!(tag.to_vec(), expected_tag, "GCM TC4 tag mismatch");
548
549        let decrypted = Gcm::decrypt(&cipher, &nonce, &aad, &ct, &tag).unwrap();
550        assert_eq!(decrypted, pt);
551    }
552}