diff --git a/src/encryption/symmetric/aes/README.md b/src/encryption/symmetric/aes/README.md new file mode 100644 index 0000000..a5f4e39 --- /dev/null +++ b/src/encryption/symmetric/aes/README.md @@ -0,0 +1,123 @@ +# AES encryption + +[Advanced Encryption Standard][aes] (AES) is a symmetric-key algorithm and is a variant of the Rijndael (pronounced 'rain-dull') block cipher. AES supersedes the [Data Encryption Standard][des] (DES). + +## Overview + +AES supports a block size of 128 bits, and three different key lengths: 128, 192, and 256 bits. It manipulates _bytes_ instead of _individual bits_ or _64-bit words_, and views a 16-byte plaintext as a 2D column-major grid of bytes. + +AES uses [Substitution-Permutation Network (SPN)][spn] which makes use of two main properties: _confusion_ and _diffusion_. Confusion means that the input undergoes complex transformations, and diffusion means that these transformations depend equally on all bits of the input. + +Unlike DES, it does not use a Feistel network, and most AES calculations are done in a particular finite field. + + +## Algorithm + +The core encryption algorithm consists of the following routines: +- [KeyExpansion](#KeyExpansion) +- [AddRoundKey](#AddRoundKey) +- [SubBytes](#SubBytes) +- [ShiftRows](#ShiftRows) +- [MixColumns](#MixColumns) + +For decryption, we take the inverses of these routines: + +TODO + +### Encryption + +Encryption consists of rounds of the above routines, with the number of rounds being determined by the size of the key. Keys of length 128/192/256 bits require 10/12/14 rounds respectively. + +Round *1* is always just *AddRoundKey*. For rounds *2* to *N-1*, the algorithm uses a mix of *SubBytes*, *ShiftRows*, *MixColumns*, and *AddRoundKey*, and the last round is the same except without *MixColumns*. + +#### KeyExpansion + +The **KeyExpansion** algorithm takes a 128/192/156-bit key and turns it into 11/13/15 round keys respectively of 16 bytes each. The main trick to key expansion is the fact that if 1 bit of the encryption key is changed, it should affect the round keys for several rounds. + +Using different keys for each round protects against _[slide attacks]_. + +To generate more round keys out of the original key, we do a series of word rotation and/or substitution XOR'd with round constants, depending on the round number that we are in. + +For round **i**, if i is a multiple of the length of the key (in words): + +```rust + Self::rotate_word(&mut last); + word = (u32::from_le_bytes(Self::sub_word(last)) + ^ u32::from_le_bytes(ROUND_CONSTANTS[(i / key_len) - 1])) + .to_le_bytes(); +``` + +if i + 4 is a multiple of 8: + +```rust + word = Self::sub_word(last) +``` + +The final step is always to XOR previous round's round key with the *(i - key_len)*-th round key: + +```rust + let round_key = expanded_key[i - key_len] + .iter() + .zip(last.iter()) + .map(|(w, l)| w ^ l) + .collect_vec() + .try_into() + .unwrap(); +``` + +#### AddRoundKey + +XORs a round key to the internal state. + +#### SubBytes + +Substitutes each byte in the `State` with another byte according to a [substitution box](#substitution-box). + +#### ShiftRow + +Shift i-th row of i positions, for i ranging from 0 to 3, eg. Row 0: no shift occurs, row 1: a left shift of 1 position occurs. + +#### MixColumns + +Each column of bytes is treated as a 4-term polynomial, multiplied modulo x^4 + 1 with a fixed polynomial +a(x) = 3x^3 + x^2 + x + 2. This is done using matrix multiplication. + +More details can be found [here][mixcolumns]. + + +### Decryption + +TODO + +## Substitution Box + +A substitution box is a basic component of symmetric key algorithms +performs substitution. It is used to obscure the relationship +the key and the ciphertext as part of the *confusion* property. + +During substitution, a byte is interpreted as a polynomial and +mapped to its multiplicative inverse in [Rijndael's finite field][Rijndael ff]: GF(2^8) = GF(2)[x]/(x^8 + x^4 + x^3 + x + 1). + +The inverse is then transformed using an affine transformation which is the sum of multiple rotations of the byte as a vector, where addition is the XOR operation. The result is an 8-bit output array which is used to substitute the original byte. + +## Security + +Fundamentally, AES is secure because all output bits depend on all input bits in some complex, pseudorandom way. The biggest threat to block ciphers is in their modes of operation, not their core algorithms. + +## Practical implementations + +In production-level AES code, fast AES software uses special techniques called table-based implementations which replaces the *SubBytes-ShiftRows-MixColumns* sequence with a combination of XORs and lookups in hardcoded tables loaded in memory during execution time. + +## References + +- [FIPS197](fips197) +- [Serious Cryptography - A Practical Introduction to Modern Cryptography](seriouscrypto) + +[aes]: https://en.wikipedia.org/wiki/Advanced_Encryption_Standard +[des]: ../des/README.md +[spn]: https://en.wikipedia.org/wiki/Substitution%E2%80%93permutation_network +[slide attacks]: https://en.wikipedia.org/wiki/Slide_attack +[mixcolumns]: https://en.wikipedia.org/wiki/Rijndael_MixColumns +[Rijndael ff]: https://en.wikipedia.org/wiki/Finite_field_arithmetic#Rijndael's_(AES)_finite_field +[fips197]: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197-upd1.pdf +[seriouscrypto]:https://nostarch.com/seriouscrypto diff --git a/src/encryption/symmetric/aes/mod.rs b/src/encryption/symmetric/aes/mod.rs new file mode 100644 index 0000000..ee14c0e --- /dev/null +++ b/src/encryption/symmetric/aes/mod.rs @@ -0,0 +1,295 @@ +//! This module contains the implementation for the Advanced Encryption Standard (AES) encryption +//! and decryption. +#![cfg_attr(not(doctest), doc = include_str!("./README.md"))] + +use itertools::Itertools; + +pub mod sbox; +#[cfg(test)] pub mod tests; + +use super::SymmetricEncryption; +use crate::encryption::symmetric::aes::sbox::SBOX; + +/// A block in AES represents a 128-bit sized message data. +pub type Block = [u8; 16]; + +/// A word in AES represents a 32-bit array of data. +pub type Word = [u8; 4]; + +/// A generic N-bit key. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct Key +where [(); N / 8]: { + inner: [u8; N / 8], +} + +impl Key +where [(); N / 8]: +{ + /// Creates a new `Key` of size `N` bits. + pub fn new(key_bytes: [u8; N / 8]) -> Self { Self { inner: key_bytes } } +} + +impl std::ops::Deref for Key +where [(); N / 8]: +{ + type Target = [u8; N / 8]; + + fn deref(&self) -> &Self::Target { &self.inner } +} + +impl SymmetricEncryption for AES +where [(); N / 8]: +{ + type Block = Block; + type Key = Key; + + /// Encrypt a message of size [`Block`] with a [`Key`] of size `N`-bits. + /// + /// ## Example + /// ```rust + /// #![feature(generic_const_exprs)] + /// + /// use rand::{thread_rng, Rng}; + /// use ronkathon::encryption::symmetric::{ + /// aes::{Key, AES}, + /// SymmetricEncryption, + /// }; + /// + /// let mut rng = thread_rng(); + /// let key = Key::<128>::new(rng.gen()); + /// let plaintext = rng.gen(); + /// let encrypted = AES::encrypt(&key, &plaintext); + /// ``` + fn encrypt(key: &Self::Key, plaintext: &Self::Block) -> Self::Block { + let num_rounds = match N { + 128 => 10, + 192 => 12, + 256 => 14, + _ => panic!("AES only supports key sizes 128, 192 and 256 bits. You provided: {N}"), + }; + + Self::aes_encrypt(plaintext, key, num_rounds) + } + + fn decrypt(_key: &Self::Key, _ciphertext: &Self::Block) -> Self::Block { unimplemented!() } +} + +/// Contains the values given by [x^(i-1), {00}, {00}, {00}], with x^(i-1) +/// being powers of x in the field GF(2^8). +/// +/// NOTE: i starts at 1, not 0. +const ROUND_CONSTANTS: [[u8; 4]; 10] = [ + [0x01, 0x00, 0x00, 0x00], + [0x02, 0x00, 0x00, 0x00], + [0x04, 0x00, 0x00, 0x00], + [0x08, 0x00, 0x00, 0x00], + [0x10, 0x00, 0x00, 0x00], + [0x20, 0x00, 0x00, 0x00], + [0x40, 0x00, 0x00, 0x00], + [0x80, 0x00, 0x00, 0x00], + [0x1B, 0x00, 0x00, 0x00], + [0x36, 0x00, 0x00, 0x00], +]; + +/// A struct containing an instance of an AES encryption/decryption. +#[derive(Clone)] +pub struct AES {} + +/// Instead of arranging its bytes in a line (array), +/// AES operates on a grid, specifically a 4x4 column-major array: +/// +/// [[b_0, b_4, b_8, b_12], +/// [b_1, b_5, b_9, b_13], +/// [b_2, b_6, b_10, b_14], +/// [b_3, b_7, b_11, b_15]] +/// +/// where b_i is the i-th byte. This is also how we will layout +/// bytes in our `State`. +#[derive(Debug, Default, Clone, Copy, PartialEq)] +struct State([[u8; 4]; 4]); + +impl AES +where [(); N / 8]: +{ + /// Performs the cipher, with key size of `N` (in bits), as seen in Figure 5 of the document + /// linked in the front-page. + fn aes_encrypt(plaintext: &[u8; 16], key: &Key, num_rounds: usize) -> Block { + assert!(!key.is_empty(), "Key is not instantiated"); + + let key_len_words = N / 32; + let mut round_keys_words = Vec::with_capacity(key_len_words * (num_rounds + 1)); + Self::key_expansion(*key, &mut round_keys_words, key_len_words, num_rounds); + let mut round_keys = round_keys_words.chunks_exact(4); + + let mut state = State( + plaintext + .chunks(4) + .map(|c| c.try_into().unwrap()) + .collect::>() + .try_into() + .unwrap(), + ); + assert!(state != State::default(), "State is not instantiated"); + + // Round 0 - add round key + Self::add_round_key(&mut state, round_keys.next().unwrap()); + + // Rounds 1 to N - 1 + for _ in 1..num_rounds { + Self::sub_bytes(&mut state); + Self::shift_rows(&mut state); + Self::mix_columns(&mut state); + Self::add_round_key(&mut state, round_keys.next().unwrap()); + } + + // Last round - we do not mix columns here. + Self::sub_bytes(&mut state); + Self::shift_rows(&mut state); + Self::add_round_key(&mut state, round_keys.next().unwrap()); + + assert!( + round_keys.remainder().is_empty(), + "Round keys not fully consumed - perhaps check key expansion?" + ); + + state.0.into_iter().flatten().collect::>().try_into().unwrap() + } + + /// XOR a round key to its internal state. + fn add_round_key(state: &mut State, round_key: &[[u8; 4]]) { + for (col, word) in state.0.iter_mut().zip(round_key.iter()) { + for (c, w) in col.iter_mut().zip(word.iter()) { + *c ^= w; + } + } + } + + /// Substitutes each byte [s_0, s_1, ..., s_15] with another byte according to a substitution box + /// (usually referred to as an S-box). + fn sub_bytes(state: &mut State) { + for i in 0..4 { + for j in 0..4 { + state.0[i][j] = SBOX[state.0[i][j] as usize]; + } + } + } + + /// Shift i-th row of i positions, for i ranging from 0 to 3. + /// + /// For row 0, no shifting occurs, for row 1, a left shift of 1 index occurs, .. + /// + /// Note that since our state is in column-major form, we transpose the state to a + /// row-major form to make this step simpler. + fn shift_rows(state: &mut State) { + let len = state.0.len(); + let mut iters: Vec<_> = state.0.into_iter().map(|n| n.into_iter()).collect(); + + // Transpose to row-major form + let mut transposed: Vec<_> = + (0..len).map(|_| iters.iter_mut().map(|n| n.next().unwrap()).collect::>()).collect(); + + for (r, i) in transposed.iter_mut().zip(0..4) { + let (left, right) = r.split_at(i); + *r = [right.to_vec(), left.to_vec()].concat(); + } + let mut iters: Vec<_> = transposed.into_iter().map(|n| n.into_iter()).collect(); + + state.0 = (0..len) + .map(|_| iters.iter_mut().map(|n| n.next().unwrap()).collect::>().try_into().unwrap()) + .collect::>() + .try_into() + .unwrap(); + } + + /// Applies the same linear transformation to each of the four columns of the state. + /// + /// Mix columns is done as such: + /// + /// Each column of bytes is treated as a 4-term polynomial, multiplied modulo x^4 + 1 with a fixed + /// polynomial a(x) = 3x^3 + x^2 + x + 2. This is done using matrix multiplication. + fn mix_columns(state: &mut State) { + for col in state.0.iter_mut() { + let tmp = *col; + let mut col_doubled = *col; + + // Perform the matrix multiplication in GF(2^8). + // We process the multiplications first, so we can just do additions later. + for (i, c) in col_doubled.iter_mut().enumerate() { + let hi_bit = col[i] >> 7; + *c = col[i] << 1; + *c ^= hi_bit * 0x1B; // This XOR brings the column back into the field if an + // overflow occurs (ie. hi_bit == 1) + } + + // Do all additions (XORs) here. + // 2a0 + 3a1 + a2 + a3 + col[0] = col_doubled[0] ^ tmp[3] ^ tmp[2] ^ col_doubled[1] ^ tmp[1]; + // a0 + 2a1 + 3a2 + a3 + col[1] = col_doubled[1] ^ tmp[0] ^ tmp[3] ^ col_doubled[2] ^ tmp[2]; + // a0 + a1 + 2a2 + 3a3 + col[2] = col_doubled[2] ^ tmp[1] ^ tmp[0] ^ col_doubled[3] ^ tmp[3]; + // 3a0 + a1 + a2 + 2a3 + col[3] = col_doubled[3] ^ tmp[2] ^ tmp[1] ^ col_doubled[0] ^ tmp[0]; + } + } + + /// In AES, rotword() is just a one-byte left circular shift. + fn rotate_word(word: &mut [u8; 4]) { word.rotate_left(1) } + + /// In AES, subword() is just an application of the S-box to each of the + /// four bytes of a word. + fn sub_word(mut word: [u8; 4]) -> [u8; 4] { + word.iter_mut().for_each(|b| *b = SBOX[*b as usize]); + + word + } + + /// Generates a key schedule based on a given cipher key `Key`, generating a total of + /// `Nb * (Nr + 1)` words, where Nb = size of block (in words), and Nr = number of rounds. + /// Nr is determined by the size `N` of the key. Every 4-word chunk from this output + /// is used as a round key. + /// + /// Key expansion ensures that each key used per round is different, introducing additional + /// complexity and diffusion. + fn key_expansion( + key: Key, + round_keys_words: &mut Vec, + key_len: usize, + num_rounds: usize, + ) { + let block_num_words = 128 / 32; + + let out_len = block_num_words * (num_rounds + 1); + let key_words: Vec = key.chunks(4).map(|c| c.try_into().unwrap()).collect(); + round_keys_words.extend(key_words); + + for i in key_len..(block_num_words * (num_rounds + 1)) { + let mut last = *round_keys_words.last().unwrap(); + + if i % key_len == 0 { + Self::rotate_word(&mut last); + last = (u32::from_le_bytes(Self::sub_word(last)) + ^ u32::from_le_bytes(ROUND_CONSTANTS[(i / key_len) - 1])) + .to_le_bytes(); + } else if key_len > 6 && i % key_len == 4 { + last = Self::sub_word(last) + } + + let round_key = round_keys_words[i - key_len] + .iter() + .zip(last.iter()) + .map(|(w, l)| w ^ l) + .collect_vec() + .try_into() + .unwrap(); + round_keys_words.push(round_key); + } + + assert_eq!( + round_keys_words.len(), + out_len, + "Wrong number of words output during key expansion" + ); + } +} diff --git a/src/encryption/symmetric/aes/sbox.rs b/src/encryption/symmetric/aes/sbox.rs new file mode 100644 index 0000000..3fb8d62 --- /dev/null +++ b/src/encryption/symmetric/aes/sbox.rs @@ -0,0 +1,39 @@ +//! The [`Rijndael S-box`][sbox] is a substitution box (lookup table) used in +//! the Rijndael cipher, on which AES is based. +//! +//! A substitution box is a basic component of symmetric key algorithms +//! which performs substitution. It is used to obscure the relationship +//! between the key and the ciphertext as part of the [`confusion`] +//! property. +//! +//! # Usage +//! +//! An S-box takes `m` input bits and maps them into `n` bits, where `n` is not +//! necessarily equal to `m`. An `m` x `n` S-box can be implemented as a lookup table with `2^m` +//! words of `n` bits each. +//! +//! [sbox]: https://en.wikipedia.org/wiki/Rijndael_S-box +//! [confusion]: https://en.wikipedia.org/wiki/Confusion_and_diffusion + +/// A substitution box for an instance of [`AES`](super::AES). +/// +/// Since substitution involves mapping a single byte (m = 8) into another (n = 8), we have a +/// lookup table of size 2^8 = 256 of 8 bits per index, implemented as a linear array. +pub(crate) const SBOX: [u8; 256] = [ + 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, + 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, + 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, + 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, + 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, + 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, + 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, + 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, + 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, + 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, + 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, + 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, + 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, + 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, + 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, + 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16, +]; diff --git a/src/encryption/symmetric/aes/tests.rs b/src/encryption/symmetric/aes/tests.rs new file mode 100644 index 0000000..44203e0 --- /dev/null +++ b/src/encryption/symmetric/aes/tests.rs @@ -0,0 +1,64 @@ +/// Test vectors from: https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.197.pdf +use super::*; + +#[test] +fn test_aes_128() { + const KEY_LEN: usize = 128; + let key = Key::::new([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + ]); + + let plaintext = [ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + ]; + + let state = AES::encrypt(&key, &plaintext); + + let expected_state = Block::from([ + 0x69, 0xc4, 0xe0, 0xd8, 0x6a, 0x7b, 0x04, 0x30, 0xd8, 0xcd, 0xb7, 0x80, 0x70, 0xb4, 0xc5, 0x5a, + ]); + + assert_eq!(state, expected_state); +} + +#[test] +fn test_aes_192() { + const KEY_LEN: usize = 192; + let key = Key::::new([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + ]); + + let plaintext = [ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + ]; + + let state = AES::encrypt(&key, &plaintext); + + let expected_state = Block::from([ + 0xdd, 0xa9, 0x7c, 0xa4, 0x86, 0x4c, 0xdf, 0xe0, 0x6e, 0xaf, 0x70, 0xa0, 0xec, 0x0d, 0x71, 0x91, + ]); + + assert_eq!(state, expected_state); +} + +#[test] +fn test_aes_256() { + const KEY_LEN: usize = 256; + let key = Key::::new([ + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, + ]); + + let plaintext = [ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + ]; + + let state = AES::encrypt(&key, &plaintext); + + let expected_state = Block::from([ + 0x8e, 0xa2, 0xb7, 0xca, 0x51, 0x67, 0x45, 0xbf, 0xea, 0xfc, 0x49, 0x90, 0x4b, 0x49, 0x60, 0x89, + ]); + + assert_eq!(state, expected_state); +} diff --git a/src/encryption/symmetric/mod.rs b/src/encryption/symmetric/mod.rs index 3b9d763..8b835b9 100644 --- a/src/encryption/symmetric/mod.rs +++ b/src/encryption/symmetric/mod.rs @@ -1,4 +1,5 @@ //! Contains implementation of symmetric encryption primitives. +pub mod aes; pub mod chacha; pub mod des;