Skip to main content

ic_cipher/aes/
x86.rs

1//! The x86-64 AES-NI backend.
2//!
3//! # What is and is not reimplemented here
4//!
5//! Only the *round function* is accelerated. Key expansion stays in
6//! [`portable::Schedule`][super::portable::Schedule], and this backend loads
7//! its output into SIMD registers. Expansion happens once per key and is not on
8//! the hot path, so a second implementation of it would buy nothing and risk a
9//! divergence — particularly for AES-192, whose SIMD key schedule is the
10//! fiddliest part of a typical AES-NI implementation.
11//!
12//! Decryption uses the equivalent inverse cipher: the encryption round keys are
13//! passed through `AESIMC` and reversed at construction, which is what lets
14//! `AESDEC` run the inverse rounds in the same shape as the forward ones.
15//!
16//! # Safety
17//!
18//! These functions are `unsafe` because `#[target_feature]` requires it: a
19//! caller must not invoke them on a CPU without AES-NI. The intrinsics
20//! themselves are safe once that feature is enabled in scope, so the only
21//! genuinely unsafe operations are the raw-pointer loads and stores — and those
22//! are the only things wrapped in an `unsafe` block.
23//!
24//! # Constant-time properties
25//!
26//! `AESENC` and friends are single instructions with data-independent latency,
27//! so this backend is constant-time for the same reason the portable one is —
28//! and it never touches a lookup table at all.
29
30use super::portable::{Schedule, BLOCK_LEN};
31use ic_core::{ensure, Result, Zeroize};
32
33#[cfg(target_arch = "x86")]
34use core::arch::x86::*;
35#[cfg(target_arch = "x86_64")]
36use core::arch::x86_64::*;
37
38/// How many blocks the batch path processes at once.
39///
40/// `AESENC` has a latency of around four cycles but is fully pipelined, so
41/// interleaving eight independent blocks keeps the unit busy instead of
42/// stalling on each result. This is where most of the speedup over the portable
43/// backend comes from in CTR and GCM.
44pub const PARALLEL_BLOCKS: usize = 8;
45
46/// AES round keys held in SIMD registers.
47#[derive(Clone, Copy)]
48pub struct Keys {
49    enc: [__m128i; 15],
50    dec: [__m128i; 15],
51    rounds: usize,
52}
53
54impl Keys {
55    /// Load an expanded schedule into SIMD registers.
56    ///
57    /// # Safety
58    ///
59    /// The caller must ensure the `aes` and `sse2` target features are
60    /// available on this CPU.
61    #[target_feature(enable = "aes")]
62    pub unsafe fn load(sched: &Schedule) -> Keys {
63        let rounds = sched.rounds;
64        let mut enc = [_mm_setzero_si128(); 15];
65        let mut dec = [_mm_setzero_si128(); 15];
66
67        for (r, slot) in enc.iter_mut().enumerate().take(rounds + 1) {
68            let rk = sched.round_key(r);
69            // SAFETY: `round_key` always returns exactly BLOCK_LEN bytes, and
70            // the unaligned load has no alignment requirement.
71            *slot = unsafe { _mm_loadu_si128(rk.as_ptr() as *const __m128i) };
72        }
73
74        // Equivalent inverse cipher: reverse the round keys and apply AESIMC to
75        // everything except the first and last.
76        dec[0] = enc[rounds];
77        for i in 1..rounds {
78            dec[i] = _mm_aesimc_si128(enc[rounds - i]);
79        }
80        dec[rounds] = enc[0];
81
82        Keys { enc, dec, rounds }
83    }
84
85    /// The encryption round keys and the round count, for the fused AES-GCM
86    /// kernels in `gcm_x86`, which run their own rounds interleaved with GHASH.
87    #[cfg(target_arch = "x86_64")]
88    #[inline]
89    pub(crate) fn enc_round_keys(&self) -> (&[__m128i; 15], usize) {
90        (&self.enc, self.rounds)
91    }
92
93    /// Encrypt one block in registers.
94    ///
95    /// # Safety
96    ///
97    /// Requires the `aes` target feature.
98    #[target_feature(enable = "aes")]
99    #[inline]
100    unsafe fn encrypt(&self, block: __m128i) -> __m128i {
101        let mut b = _mm_xor_si128(block, self.enc[0]);
102        for r in 1..self.rounds {
103            b = _mm_aesenc_si128(b, self.enc[r]);
104        }
105        _mm_aesenclast_si128(b, self.enc[self.rounds])
106    }
107
108    /// Decrypt one block in registers.
109    ///
110    /// # Safety
111    ///
112    /// Requires the `aes` target feature.
113    #[target_feature(enable = "aes")]
114    #[inline]
115    unsafe fn decrypt(&self, block: __m128i) -> __m128i {
116        let mut b = _mm_xor_si128(block, self.dec[0]);
117        for r in 1..self.rounds {
118            b = _mm_aesdec_si128(b, self.dec[r]);
119        }
120        _mm_aesdeclast_si128(b, self.dec[self.rounds])
121    }
122}
123
124/// Encrypt one block in place.
125///
126/// # Safety
127///
128/// Requires the `aes` target feature.
129#[target_feature(enable = "aes")]
130pub unsafe fn encrypt_block(keys: &Keys, block: &mut [u8]) -> Result<()> {
131    ensure!(block.len() == BLOCK_LEN, InvalidLength, "aes block");
132    // SAFETY: `block` is exactly BLOCK_LEN bytes, checked above; the loads and
133    // stores are unaligned and so have no alignment requirement.
134    unsafe {
135        let b = _mm_loadu_si128(block.as_ptr() as *const __m128i);
136        let out = keys.encrypt(b);
137        _mm_storeu_si128(block.as_mut_ptr() as *mut __m128i, out);
138    }
139    Ok(())
140}
141
142/// Decrypt one block in place.
143///
144/// # Safety
145///
146/// Requires the `aes` target feature.
147#[target_feature(enable = "aes")]
148pub unsafe fn decrypt_block(keys: &Keys, block: &mut [u8]) -> Result<()> {
149    ensure!(block.len() == BLOCK_LEN, InvalidLength, "aes block");
150    // SAFETY: as above.
151    unsafe {
152        let b = _mm_loadu_si128(block.as_ptr() as *const __m128i);
153        let out = keys.decrypt(b);
154        _mm_storeu_si128(block.as_mut_ptr() as *mut __m128i, out);
155    }
156    Ok(())
157}
158
159/// Encrypt a whole number of blocks in place, eight at a time.
160///
161/// # Safety
162///
163/// Requires the `aes` target feature.
164#[target_feature(enable = "aes")]
165pub unsafe fn encrypt_blocks(keys: &Keys, data: &mut [u8]) -> Result<()> {
166    ensure!(
167        data.len().is_multiple_of(BLOCK_LEN),
168        InvalidLength,
169        "aes batch must be block-aligned"
170    );
171
172    let mut chunks = data.chunks_exact_mut(BLOCK_LEN * PARALLEL_BLOCKS);
173    for chunk in &mut chunks {
174        let p = chunk.as_mut_ptr();
175        let mut b = [_mm_setzero_si128(); PARALLEL_BLOCKS];
176
177        // SAFETY: the chunk is exactly PARALLEL_BLOCKS * BLOCK_LEN bytes, so
178        // every offset below is in bounds, and the loads are unaligned.
179        unsafe {
180            for (i, slot) in b.iter_mut().enumerate() {
181                *slot = _mm_loadu_si128(p.add(i * BLOCK_LEN) as *const __m128i);
182            }
183        }
184
185        // Interleave the rounds across all eight blocks so the pipeline stays
186        // full rather than waiting on each AESENC.
187        for slot in b.iter_mut() {
188            *slot = _mm_xor_si128(*slot, keys.enc[0]);
189        }
190        for r in 1..keys.rounds {
191            let rk = keys.enc[r];
192            for slot in b.iter_mut() {
193                *slot = _mm_aesenc_si128(*slot, rk);
194            }
195        }
196        let last = keys.enc[keys.rounds];
197        for slot in b.iter_mut() {
198            *slot = _mm_aesenclast_si128(*slot, last);
199        }
200
201        // SAFETY: same bounds as the loads above.
202        unsafe {
203            for (i, slot) in b.iter().enumerate() {
204                _mm_storeu_si128(p.add(i * BLOCK_LEN) as *mut __m128i, *slot);
205            }
206        }
207    }
208
209    // Whatever is left over is fewer than PARALLEL_BLOCKS blocks.
210    for block in chunks.into_remainder().chunks_exact_mut(BLOCK_LEN) {
211        // SAFETY: the caller established the `aes` feature for this call.
212        unsafe { encrypt_block(keys, block)? };
213    }
214    Ok(())
215}
216
217/// XOR `data` with the CTR keystream starting at `counter`, advancing the
218/// counter's last 32 bits big-endian and wrapping within them -- GCM's
219/// `inc32` -- eight blocks at a time.
220///
221/// GCM used to reach AES through `encrypt_blocks` eight blocks per call, with
222/// the counters built and the keystream XORed byte by byte around each call:
223/// 8.5 GiB/s where one call over the whole buffer runs at 15. Here the
224/// counters are written straight into the blocks, the keystream never leaves
225/// registers on the whole-block path, and there is one call per message.
226///
227/// The counter blocks are assembled with plain stores rather than a byte
228/// shuffle, so this needs nothing beyond the `aes` feature the caller already
229/// checked; building eight 16-byte blocks is noise beside eighty `AESENC`s.
230///
231/// On return `counter` is the block after the last one used.
232///
233/// # Safety
234///
235/// Requires the `aes` target feature.
236#[target_feature(enable = "aes")]
237pub unsafe fn ctr32_xor(keys: &Keys, counter: &mut [u8; BLOCK_LEN], data: &mut [u8]) {
238    let mut n = u32::from_be_bytes([counter[12], counter[13], counter[14], counter[15]]);
239    let mut blocks = [[0u8; BLOCK_LEN]; PARALLEL_BLOCKS];
240    for block in blocks.iter_mut() {
241        block[..12].copy_from_slice(&counter[..12]);
242    }
243
244    for chunk in data.chunks_mut(BLOCK_LEN * PARALLEL_BLOCKS) {
245        for (i, block) in blocks.iter_mut().enumerate() {
246            block[12..].copy_from_slice(&n.wrapping_add(i as u32).to_be_bytes());
247        }
248        let mut b = [_mm_setzero_si128(); PARALLEL_BLOCKS];
249        for (slot, block) in b.iter_mut().zip(blocks.iter()) {
250            // SAFETY: `block` is exactly 16 bytes and the load is unaligned.
251            *slot = _mm_xor_si128(
252                unsafe { _mm_loadu_si128(block.as_ptr() as *const __m128i) },
253                keys.enc[0],
254            );
255        }
256        for r in 1..keys.rounds {
257            let rk = keys.enc[r];
258            for slot in b.iter_mut() {
259                *slot = _mm_aesenc_si128(*slot, rk);
260            }
261        }
262        let last = keys.enc[keys.rounds];
263        for slot in b.iter_mut() {
264            *slot = _mm_aesenclast_si128(*slot, last);
265        }
266
267        let p = chunk.as_mut_ptr();
268        if chunk.len() == BLOCK_LEN * PARALLEL_BLOCKS {
269            for (i, ks) in b.iter().enumerate() {
270                // SAFETY: the chunk is exactly PARALLEL_BLOCKS blocks, so every
271                // offset is in bounds; loads and stores are unaligned.
272                unsafe {
273                    let d = _mm_loadu_si128(p.add(i * BLOCK_LEN) as *const __m128i);
274                    _mm_storeu_si128(p.add(i * BLOCK_LEN) as *mut __m128i, _mm_xor_si128(d, *ks));
275                }
276            }
277        } else {
278            // The final, shorter chunk: through a buffer, and only as far as
279            // the data goes. The keystream past the end is discarded.
280            let mut stream = [0u8; BLOCK_LEN * PARALLEL_BLOCKS];
281            for (i, ks) in b.iter().enumerate() {
282                // SAFETY: `stream` holds PARALLEL_BLOCKS blocks.
283                unsafe {
284                    _mm_storeu_si128(stream.as_mut_ptr().add(i * BLOCK_LEN) as *mut __m128i, *ks)
285                };
286            }
287            for (d, k) in chunk.iter_mut().zip(stream.iter()) {
288                *d ^= k;
289            }
290            stream.zeroize();
291        }
292        n = n.wrapping_add(chunk.len().div_ceil(BLOCK_LEN) as u32);
293    }
294    counter[12..].copy_from_slice(&n.to_be_bytes());
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300    use crate::aes::portable;
301
302    fn available() -> bool {
303        std::arch::is_x86_feature_detected!("aes")
304    }
305
306    /// The whole point of this backend is that it agrees with the portable one
307    /// exactly. This is the differential test that makes the acceleration safe
308    /// to trust: the portable path is validated against FIPS 197 vectors, and
309    /// this asserts bit-for-bit equality with it.
310    #[test]
311    fn matches_the_portable_backend_on_every_key_length() {
312        if !available() {
313            return;
314        }
315        for key_len in [16usize, 24, 32] {
316            let key: Vec<u8> = (0..key_len).map(|i| (i * 7 + 1) as u8).collect();
317            let sched = portable::Schedule::expand(&key).unwrap();
318            // SAFETY: guarded by the runtime feature check above.
319            let keys = unsafe { Keys::load(&sched) };
320
321            for seed in 0..64u8 {
322                let original: [u8; 16] =
323                    core::array::from_fn(|i| seed ^ (i as u8).wrapping_mul(31));
324
325                let mut a = original;
326                let mut b = original;
327                portable::encrypt_block(&sched, &mut a).unwrap();
328                unsafe { encrypt_block(&keys, &mut b).unwrap() };
329                assert_eq!(a, b, "encrypt mismatch, key_len {key_len}, seed {seed}");
330
331                let mut a = original;
332                let mut b = original;
333                portable::decrypt_block(&sched, &mut a).unwrap();
334                unsafe { decrypt_block(&keys, &mut b).unwrap() };
335                assert_eq!(a, b, "decrypt mismatch, key_len {key_len}, seed {seed}");
336            }
337        }
338    }
339
340    /// The batch path must agree with the single-block path, including at the
341    /// boundary where the eight-way loop hands off to the remainder.
342    #[test]
343    fn batch_matches_single_block_at_every_length() {
344        if !available() {
345            return;
346        }
347        let sched = portable::Schedule::expand(&[0x2bu8; 32]).unwrap();
348        // SAFETY: guarded by the runtime feature check above.
349        let keys = unsafe { Keys::load(&sched) };
350
351        for blocks in 0..=(PARALLEL_BLOCKS * 2 + 3) {
352            let data: Vec<u8> = (0..blocks * BLOCK_LEN).map(|i| (i * 13) as u8).collect();
353
354            let mut batched = data.clone();
355            unsafe { encrypt_blocks(&keys, &mut batched).unwrap() };
356
357            let mut one_at_a_time = data.clone();
358            for block in one_at_a_time.chunks_exact_mut(BLOCK_LEN) {
359                unsafe { encrypt_block(&keys, block).unwrap() };
360            }
361
362            assert_eq!(batched, one_at_a_time, "{blocks} blocks");
363        }
364    }
365
366    #[test]
367    fn batch_rejects_unaligned_input() {
368        if !available() {
369            return;
370        }
371        let sched = portable::Schedule::expand(&[0u8; 16]).unwrap();
372        // SAFETY: guarded by the runtime feature check above.
373        let keys = unsafe { Keys::load(&sched) };
374        let mut data = [0u8; BLOCK_LEN + 1];
375        assert!(unsafe { encrypt_blocks(&keys, &mut data) }.is_err());
376    }
377
378    /// Decryption inverts encryption within the accelerated backend itself.
379    #[test]
380    fn decryption_inverts_encryption() {
381        if !available() {
382            return;
383        }
384        let sched = portable::Schedule::expand(&[0x42u8; 24]).unwrap();
385        // SAFETY: guarded by the runtime feature check above.
386        let keys = unsafe { Keys::load(&sched) };
387        let original: [u8; 16] = core::array::from_fn(|i| i as u8);
388        let mut block = original;
389        unsafe {
390            encrypt_block(&keys, &mut block).unwrap();
391            assert_ne!(block, original);
392            decrypt_block(&keys, &mut block).unwrap();
393        }
394        assert_eq!(block, original);
395    }
396}