Skip to main content

arcana/cipher/
modes.rs

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