diff --git a/src/encryption/symmetric/aes/mod.rs b/src/encryption/symmetric/aes/mod.rs index ba0c1ec..c16347c 100644 --- a/src/encryption/symmetric/aes/mod.rs +++ b/src/encryption/symmetric/aes/mod.rs @@ -220,7 +220,6 @@ where [(); N / 8]: .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()); diff --git a/src/encryption/symmetric/modes/README.md b/src/encryption/symmetric/modes/README.md index f77b9b4..56cb671 100644 --- a/src/encryption/symmetric/modes/README.md +++ b/src/encryption/symmetric/modes/README.md @@ -35,6 +35,68 @@ IV4["IV||2"]-->Fk2[F_k]-->xor2["⨁"]-->c2 m2-->xor2 ``` +## GCM: Galois/Counter Mode + +GCM is a block cipher mode of operation that provides both confidentiality and authentication. +To provide confidentiality, it uses Counter(CTR) mode for encryption and decryption. +To provide authentication, it uses a universal hash function, GHASH. +Authentication is provided not only for confidential data but also other associated data. + +In this section, we will give an informal overview of GCM mode for 128-bit block cipher. +*To see the formal definition of the operations of GCM, I recommend reading the original paper. [The Galois/Counter Mode of Operation (GCM)](https://csrc.nist.rip/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf)* + +The two operations of GCM are authenticated encryption and authenticated decryption. + +Here is a figure that gives a complete overview of the authenticated encryption in GCM. + +In the figure, we have taken +- the size of plaintext is `3 * 128-bit = 384 bits or 48 bytes`, +- Additionally Authenticated Data(AAD) is of `2 * 128-bit = 248 bits or 32 bytes`. + +![GCM](./figure_full_gcm.svg) +*Note: The yellow diamonds represent functions/algorithms, the small rectangle with a blue outline represents 128-bit blocks.* +Also, +- *Enc(K)*: The encryption operation of the cipher used, for example AES, under the key K. +- *incr*: The increment function, which treats the rightmost 32-bit of the block as an unsigned integer and increments it by 1. + +If you look at the figure carefully, you will notice that the GCM mode is composed of two main parts: +- Encryption: This part is the same as the CTR mode of operation, with minor changes to the counter block. +- Authentication: In this part, we generate an authentication tag for the ciphertext and some additional data, which we refer to as Additionally Authenticated Data(AAD). + +The counter block is the same as in CTR mode. In general, it can be thought of as a 96-bit nonce value followed by a 32-bit counter value. + +The tag is generated by XOR of: +1. Hash of ciphertext and AAD, using GHASH algorithm +2. Encryption of Counter block 0. + +### GHASH + +The GHASH algorithm can be viewed as a series of `ADD and MULTIPLY` in $GF(2^{128})$. Mathematically put the basic operation of GHASH is, + +$$ +X_{i} = +\begin{cases} +0 & \quad i = 0 \\ +( X_{i-1} \oplus B_{i} ) * H & \quad \text{otherwise} +\end{cases} +$$ + +$B_{i}$ represents blocks of AAD followed by blocks of ciphertext followed by a special length block. +The length block consists of 64-bit lengths(in bits) of AAD and ciphertext. +$H$ called the hash key, is the encryption of 128-bit of zeros using the chosen cipher and key. + +The interesting thing to note here is that the multiplication($*$) and addition($\oplus$) are operations of the Galois(finite) field of order $2^{128}$. +A brief summary of finite field arithmetic, +- The elements of the field are represented as polynomials. Each bit of the 128-bit block represents coefficients of a polynomial of degree strictly less than 128. +- Addition in a finite field is equivalent to bitwise XOR. +- Multiplication in a finite field is the multiplication of corresponding polynomials modulo an irreducible reducing polynomial. + +In GCM the reducing polynomial is $f = 1 + x + x^2 + x^7 + x^{128}$ + +If you want to read about Finite Field, the Wikipedia article on [Finite Field Arithemtic](https://en.wikipedia.org/wiki/Galois/Counter_Mode) is pretty good! + +The authenticated decryption operation is identical to authenticated encryption, except the tag is generated before the decryption. + ## Next Steps Implement more modes, and subsequent attacks/vulnerabilities: - [ ] CFB diff --git a/src/encryption/symmetric/modes/ctr.rs b/src/encryption/symmetric/modes/ctr.rs index 4864c1b..132f142 100644 --- a/src/encryption/symmetric/modes/ctr.rs +++ b/src/encryption/symmetric/modes/ctr.rs @@ -2,20 +2,22 @@ use crate::encryption::symmetric::{counter::Counter, BlockCipher}; -/// [`BlockCipher`] counter mode of operation -pub struct CTR -where [(); C::BLOCK_SIZE / 2]: { - nonce: [u8; C::BLOCK_SIZE / 2], +/// [`BlockCipher`] counter mode of operation with two parameters: +/// - `C`, a cipher that implements the `BlockCipher` trait. +/// - `M`, a usize const that indicates the size of counter in bytes. +pub struct CTR +where [(); C::BLOCK_SIZE - M]: { + nonce: [u8; C::BLOCK_SIZE - M], } -impl CTR -where [(); C::BLOCK_SIZE / 2]: +impl CTR +where [(); C::BLOCK_SIZE - M]: { /// Create a CTR mode of operation object /// # Arguments - /// - `nonce`: *Non-repeating* IV of [`BlockCipher::BLOCK_SIZE`]/2 bytes. Nonce is concatenated - /// with [`Counter`] to generate a keystream that does not repeat for a long time. - pub fn new(nonce: [u8; C::BLOCK_SIZE / 2]) -> Self { Self { nonce } } + /// - `nonce`: *Non-repeating* IV of [`BlockCipher::BLOCK_SIZE`] - M bytes. Nonce is concatenated + /// with [`Counter`], of M bytes, to generate a keystream that does not repeat for a long time. + pub fn new(nonce: [u8; C::BLOCK_SIZE - M]) -> Self { Self { nonce } } /// Encrypt a plaintext with [`BlockCipher::Key`] and [`Counter`] /// ## Arguments @@ -37,10 +39,10 @@ where [(); C::BLOCK_SIZE / 2]: /// let mut rng = thread_rng(); /// let rand_key: [u8; 16] = rng.gen(); /// let key = Key::<128>::new(rand_key); - /// let nonce: [u8; AES::<128>::BLOCK_SIZE / 2] = rng.gen(); - /// let counter: Counter<{ AES::<128>::BLOCK_SIZE / 2 }> = Counter::from(0); + /// let nonce: [u8; 12] = rng.gen(); + /// let counter: Counter<4> = Counter::from(0); /// - /// let ctr = CTR::>::new(nonce); + /// let ctr = CTR::, 4>::new(nonce); /// let plaintext = b"Hello World!"; /// /// let ciphertext = ctr.encrypt(&key, &counter, plaintext).unwrap(); @@ -48,7 +50,7 @@ where [(); C::BLOCK_SIZE / 2]: pub fn encrypt( &self, key: &C::Key, - counter: &Counter<{ C::BLOCK_SIZE / 2 }>, + counter: &Counter, plaintext: &[u8], ) -> Result, String> { let mut ciphertext = Vec::new(); @@ -56,8 +58,8 @@ where [(); C::BLOCK_SIZE / 2]: for chunk in plaintext.chunks(C::BLOCK_SIZE) { let mut block = [0u8; C::BLOCK_SIZE]; - block[..C::BLOCK_SIZE / 2].copy_from_slice(&self.nonce); - block[C::BLOCK_SIZE / 2..].copy_from_slice(&cipher_counter.0); + block[..{ C::BLOCK_SIZE - M }].copy_from_slice(&self.nonce); + block[{ C::BLOCK_SIZE - M }..].copy_from_slice(&cipher_counter.0); cipher_counter.increment()?; let encrypted = C::encrypt_block(key, &C::Block::from(block.to_vec())); @@ -70,7 +72,7 @@ where [(); C::BLOCK_SIZE / 2]: Ok(ciphertext) } - /// Decrypt a ciphertext with counter of size [`BlockCipher::BLOCK_SIZE`]/2 bytes + /// Decrypt a ciphertext with counter of size [`BlockCipher::BLOCK_SIZE`] - M bytes /// ## Usage /// ``` /// #![allow(incomplete_features)] @@ -86,10 +88,10 @@ where [(); C::BLOCK_SIZE / 2]: /// let mut rng = thread_rng(); /// let rand_key: [u8; 16] = rng.gen(); /// let key = Key::<128>::new(rand_key); - /// let nonce: [u8; AES::<128>::BLOCK_SIZE / 2] = rng.gen(); - /// let counter: Counter<{ AES::<128>::BLOCK_SIZE / 2 }> = Counter::from(0); + /// let nonce: [u8; 12] = rng.gen(); + /// let counter: Counter<4> = Counter::from(0); /// - /// let ctr = CTR::>::new(nonce); + /// let ctr = CTR::, 4>::new(nonce); /// let plaintext = b"Hello World!"; /// /// let ciphertext = ctr.encrypt(&key, &counter, plaintext).unwrap(); @@ -98,7 +100,7 @@ where [(); C::BLOCK_SIZE / 2]: pub fn decrypt( &self, key: &C::Key, - counter: &Counter<{ C::BLOCK_SIZE / 2 }>, + counter: &Counter, ciphertext: &[u8], ) -> Result, String> { self.encrypt(key, counter, ciphertext) @@ -107,6 +109,8 @@ where [(); C::BLOCK_SIZE / 2]: #[cfg(test)] mod tests { + use std::{fmt::Write, num::ParseIntError}; + use rand::{thread_rng, Rng}; use rstest::{fixture, rstest}; @@ -127,13 +131,13 @@ mod tests { } #[rstest] - fn ctr(rand_key: Key<128>) { + fn test_ctr_rand_key(rand_key: Key<128>) { for _ in 0..10 { let mut rng = thread_rng(); - let nonce: [u8; AES::<128>::BLOCK_SIZE / 2] = rng.gen(); - let counter: Counter<{ AES::<128>::BLOCK_SIZE / 2 }> = Counter::from(0); + let nonce: [u8; AES::<128>::BLOCK_SIZE - 4] = rng.gen(); + let counter: Counter<4> = Counter::from(0); - let ctr = CTR::>::new(nonce); + let ctr = CTR::, 4>::new(nonce); let plaintext = rand_message(rng.gen_range(1000..10000)); let ciphertext = ctr.encrypt(&rand_key, &counter, &plaintext).unwrap(); @@ -143,4 +147,127 @@ mod tests { assert_eq!(plaintext, decrypted); } } + + /// Encode bytes to hex + pub fn encode_hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for &b in bytes { + write!(&mut s, "{:02x}", b).unwrap(); + } + s + } + + /// Decode hex to bytes + pub fn decode_hex(s: &str) -> Result, ParseIntError> { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16)).collect() + } + + // `test_ctr_128`, `test_ctr_192`, and 'test_ctr_256' are based on test vectors given in: + // "Recommendation for Block Cipher Modes of Operation(NIST Special Publication 800-38A)" + // Link: (https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38a.pdf) + + // Appendix F.5.1 and F.5.2 + #[rstest] + #[case( + "2b7e151628aed2a6abf7158809cf4f3c", + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710", + "874d6191b620e3261bef6864990db6ce9806f66b7970fdff8617187bb9fffdff5ae4df3edbd5d35e5b4f09020db03eab1e031dda2fbe03d1792170a0f3009cee" + )] + fn test_ctr_128( + #[case] kx: &str, + #[case] ivx: &str, + #[case] ptx: &str, + #[case] expected_ctx: &str, + ) { + let k = decode_hex(kx).unwrap(); + let iv = decode_hex(ivx).unwrap(); + let pt = decode_hex(ptx).unwrap(); + + let key = Key::<128>::new(k.try_into().unwrap()); + let nonce = &iv[..8]; + let counter = Counter(iv[8..].try_into().unwrap()); + + let ctr = CTR::, 8>::new(nonce.try_into().unwrap()); + + let ct = ctr.encrypt(&key, &counter, &pt).unwrap(); + + let ctx = encode_hex(&ct); + assert_eq!(ctx, expected_ctx); + + let _pt = ctr.decrypt(&key, &counter, &ct).unwrap(); + + let _ptx = encode_hex(&_pt); + assert_eq!(_ptx, ptx); + } + + // Appendix F.5.3 and F.5.4 + #[rstest] + #[case( + "8e73b0f7da0e6452c810f32b809079e562f8ead2522c6b7b", + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710", + "1abc932417521ca24f2b0459fe7e6e0b090339ec0aa6faefd5ccc2c6f4ce8e941e36b26bd1ebc670d1bd1d665620abf74f78a7f6d29809585a97daec58c6b050" + )] + fn test_ctr_192( + #[case] kx: &str, + #[case] ivx: &str, + #[case] ptx: &str, + #[case] expected_ctx: &str, + ) { + let k = decode_hex(kx).unwrap(); + let iv = decode_hex(ivx).unwrap(); + let pt = decode_hex(ptx).unwrap(); + + let key = Key::<192>::new(k.try_into().unwrap()); + let nonce = &iv[..8]; + let counter = Counter(iv[8..].try_into().unwrap()); + + let ctr = CTR::, 8>::new(nonce.try_into().unwrap()); + + let ct = ctr.encrypt(&key, &counter, &pt).unwrap(); + + let ctx = encode_hex(&ct); + assert_eq!(ctx, expected_ctx); + + let _pt = ctr.decrypt(&key, &counter, &ct).unwrap(); + + let _ptx = encode_hex(&_pt); + assert_eq!(_ptx, ptx); + } + + // Appendix F.5.3 and F.5.4 + #[rstest] + #[case( + "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4", + "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff", + "6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710", + "601ec313775789a5b7a7f504bbf3d228f443e3ca4d62b59aca84e990cacaf5c52b0930daa23de94ce87017ba2d84988ddfc9c58db67aada613c2dd08457941a6" + )] + fn test_ctr_256( + #[case] kx: &str, + #[case] ivx: &str, + #[case] ptx: &str, + #[case] expected_ctx: &str, + ) { + let k = decode_hex(kx).unwrap(); + let iv = decode_hex(ivx).unwrap(); + let pt = decode_hex(ptx).unwrap(); + + let key = Key::<256>::new(k.try_into().unwrap()); + let nonce = &iv[..8]; + let counter = Counter(iv[8..].try_into().unwrap()); + + let ctr = CTR::, 8>::new(nonce.try_into().unwrap()); + + let ct = ctr.encrypt(&key, &counter, &pt).unwrap(); + + let ctx = encode_hex(&ct); + assert_eq!(ctx, expected_ctx); + + let _pt = ctr.decrypt(&key, &counter, &ct).unwrap(); + + let _ptx = encode_hex(&_pt); + assert_eq!(_ptx, ptx); + } } diff --git a/src/encryption/symmetric/modes/figure_full_gcm.svg b/src/encryption/symmetric/modes/figure_full_gcm.svg new file mode 100644 index 0000000..17a378c --- /dev/null +++ b/src/encryption/symmetric/modes/figure_full_gcm.svg @@ -0,0 +1,13 @@ + + + + + + + + Counter Block 1incrincrEnc(K)Enc(K)Enc(K)XORXORXORPlaintext 1Plaintext 2Plaintext 3Counter Block 2Counter Block 3Ciphertext 1Ciphertext 2Ciphertext 3Counter Block 0incrEnc(K)XORAAD 2MUL(H)XORMUL(H)XORAAD 1MUL(H)XORMUL(H)len(AAD) || len (CT)MUL(H)XORMUL(H)XORTAGPart 1: Encryption (CTR)Part 2: Authentication (GHASH) \ No newline at end of file diff --git a/src/encryption/symmetric/modes/gcm.rs b/src/encryption/symmetric/modes/gcm.rs new file mode 100644 index 0000000..2a6dc9f --- /dev/null +++ b/src/encryption/symmetric/modes/gcm.rs @@ -0,0 +1,372 @@ +//! Implementation of GCM cipher mode of operation based on NIST GCM specification. +//! [The Galois/Counter Mode of Operation (GCM)](http://www.csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf) + +use super::ctr::CTR; +use crate::{ + encryption::symmetric::{counter::Counter, BlockCipher}, + hashes::ghash::GHASH, +}; + +/// GCM (Galois/Counter Mode) struct holding a GHASH instance and a block cipher key. +/// +/// This structure implements encryption and decryption operations using the GCM +/// authenticated encryption mode, combining the CTR (counter mode) for encryption +/// and GHASH for authentication. +/// +/// # Generics +/// - `C`: A block cipher that implements the `BlockCipher` trait. +pub struct GCM { + ghash: GHASH, + key: C::Key, +} + +impl GCM +where [(); C::BLOCK_SIZE - 4]: +{ + /// Constructs a new `GCM` instance with the given key. + /// + /// # Parameters + /// - `key`: A key compatible with the block cipher `C`. + /// + /// # Returns + /// A `GCM` instance ready for encryption or decryption. + pub fn new(key: C::Key) -> Self { + assert_eq!(C::BLOCK_SIZE, 16, "GCM only supports 128-bit block size."); + // The GHASH algorithms requires the encryption of 128-bits of zeros. + let zero_string = C::Block::from(vec![0u8; 16]); + let hash_key = C::encrypt_block(&key, &zero_string); + let ghash = GHASH::new(hash_key.as_ref()); + GCM { ghash, key } + } + + /// Encrypts the given plaintext using the specified nonce and additional authenticated data + /// (AAD). + /// + /// # Parameters + /// - `nonce`: A unique nonce for the encryption process (should be '12' bytes long). + /// - `plaintext`: The data to be encrypted. + /// - `aad`: Additional authenticated data (AAD) that will be included in the authentication tag + /// but not encrypted. + /// + /// # Returns + /// A tuple `(ciphertext, tag)`: + /// - `ciphertext`: The encrypted data. + /// - `tag`: The authentication tag that verifies the integrity and authenticity of the ciphertext + /// and AAD. + /// + /// A `String` error if encryption fails. + pub fn encrypt( + &self, + nonce: &[u8], + plaintext: &[u8], + aad: &[u8], + ) -> Result<(Vec, Vec), String> { + // The `initial_block` is the first block that is encrypted. + // This is used in the tag generation step. + // It consists of two parts, a 96-bit nonce value and 32-bit counter value, which is + // incremented for each block to be encrypted. + let mut initial_block; + // `counter_start` is the start value for the 32-bit counter. + let counter_start: [u8; 4]; + // `new_nonce` is the 96-bit nonce value. + // That is C::BLOCK_SIZE(=16 bytes) - 4 bytes = 12 bytes = 96-bits! + // Also NOTE: the compiler is not happy if I write 12 bytes instead of `C::BLOCK_SIZE - 4`. :( + let new_nonce: [u8; C::BLOCK_SIZE - 4]; + + // The GCM specification recommends a 96-bit(or 12-byte) nonce, but it may not be 96-bit. + if nonce.len() != 12 { + // If the nonce is not 96-bit, we GHASH the nonce, which outputs a 128-bit value. + // The first 96-bit is used as new nonce(hence named `new_nonce`). + // The rest of the 32-bits are used as the start of counter value! + initial_block = self.ghash.digest(&[], nonce); + new_nonce = initial_block[..12].try_into().unwrap(); + counter_start = initial_block[12..].try_into().unwrap(); + } else { + new_nonce = nonce.try_into().unwrap(); + // The counter starts with `1`. + counter_start = [0, 0, 0, 1]; + initial_block = [0u8; 16]; + initial_block[..12].copy_from_slice(&new_nonce); + initial_block[12..].copy_from_slice(&counter_start); + } + + // Step 1: Increment the counter + let mut counter = Counter(counter_start); + counter.increment()?; + + // Step 2: Encrypt the plaintext using the `CTR` object. + let ctr = CTR::::new(new_nonce.try_into().unwrap()); + let ciphertext = ctr.encrypt(&self.key, &counter, plaintext)?; + + // Step3: Generate Tag + // The tag is the XOR of the `initial_block` and ghash of ciphertext. + let y0_block = C::Block::from(initial_block.to_vec()); + let y0_enc = C::encrypt_block(&self.key, &y0_block); + let hash = self.ghash.digest(aad, ciphertext.as_ref()); + let mut tag = Vec::new(); + + // XOR the ghash of ciphertext and `initial_block` + for (x, y) in hash.iter().zip(y0_enc.as_ref()) { + tag.push(x ^ y); + } + + Ok((ciphertext.to_vec(), tag)) + } + + /// Decrypt the given ciphertext using the specified nonce and additional authenticated data + /// (AAD). + /// + /// # Parameters + /// - `nonce`: A unique nonce used during encryption (should be 12 bytes long). + /// - `ciphertext`: The encrypted data to be decrypted. + /// - `aad`: Additional authenticated data (AAD) used during encryption. + /// + /// # Returns + /// A tuple `(plaintext, tag)`: + /// - `plaintext`: The decrypted data. + /// - `tag`: The authentication tag to verify the integrity and authenticity of the decrypted + /// data. + /// + /// A `String` error if decryption fails. + pub fn decrypt( + &self, + nonce: &[u8], + ciphertext: &[u8], + aad: &[u8], + ) -> Result<(Vec, Vec), String> { + // The decryption algorithm is the same as the encryption algorithm but the tag is generated + // first and then is move on to the encryption. + + let counter_start: [u8; 4]; + let new_nonce: [u8; C::BLOCK_SIZE - 4]; + let mut counter_block; + + if nonce.len() != 12 { + counter_block = self.ghash.digest(&[], nonce); + new_nonce = counter_block[..12].try_into().unwrap(); + counter_start = counter_block[12..].try_into().unwrap(); + } else { + new_nonce = nonce.try_into().unwrap(); + counter_start = [0, 0, 0, 1]; + counter_block = [0u8; 16]; + counter_block[..12].copy_from_slice(&new_nonce); + counter_block[12..].copy_from_slice(&counter_start); + } + + // Step 1: Generate Tag (same as the encryption) + let y0 = C::Block::from(counter_block.to_vec()); + let y0_enc = C::encrypt_block(&self.key, &y0); + let hash = self.ghash.digest(aad, ciphertext.as_ref()); + let mut tag = Vec::new(); + + for (x, y) in hash.iter().zip(y0_enc.as_ref()) { + tag.push(x ^ y); + } + + // Step 2: Increment Counter + let mut counter = Counter(counter_start); + counter.increment()?; + + // Step 3: Decrypt ciphertext. + let ctr = CTR::::new(new_nonce.try_into().unwrap()); + let plaintext = ctr.decrypt(&self.key, &counter, ciphertext)?; + + Ok((plaintext.to_vec(), tag)) + } +} + +#[cfg(test)] +mod tests { + + use std::{fmt::Write, num::ParseIntError}; + + use rstest::rstest; + + use super::*; + use crate::encryption::symmetric::aes::{Key, AES}; + + /// Encode bytes to hex + pub fn encode_hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for &b in bytes { + write!(&mut s, "{:02x}", b).unwrap(); + } + s + } + + pub fn decode_hex(s: &str) -> Result, ParseIntError> { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16)).collect() + } + + #[rstest] + // NIST Test Case 1 + #[case( + "00000000000000000000000000000000", + "000000000000000000000000", + "", + "", + "", + "58e2fccefa7e3061367f1d57a4e7455a" + )] + // NIST Test Case 2 + #[case( + "00000000000000000000000000000000", + "000000000000000000000000", + "00000000000000000000000000000000", + "", + "0388dace60b6a392f328c2b971b2fe78", + "ab6e47d42cec13bdf53a67b21257bddf" + )] + // NIST Test Case 3 + #[case( + "feffe9928665731c6d6a8f9467308308", + "cafebabefacedbaddecaf888", + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", + "", + "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f5985", + "4d5c2af327cd64a62cf35abd2ba6fab4" + )] + // NIST Test Case 4 + #[case( + "feffe9928665731c6d6a8f9467308308", + "cafebabefacedbaddecaf888", + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + "feedfacedeadbeeffeedfacedeadbeefabaddad2", + "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091", + "5bc94fbc3221a5db94fae95ae7121a47" + )] + // NIST Test Case 5 + #[case( + "feffe9928665731c6d6a8f9467308308", + "cafebabefacedbad", + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + "feedfacedeadbeeffeedfacedeadbeefabaddad2", + "61353b4c2806934a777ff51fa22a4755699b2a714fcdc6f83766e5f97b6c742373806900e49f24b22b097544d4896b424989b5e1ebac0f07c23f4598", + "3612d2e79e3b0785561be14aaca2fccb" + )] + // NIST Test Case 6 + #[case( + "feffe9928665731c6d6a8f9467308308", + "9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b", + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + "feedfacedeadbeeffeedfacedeadbeefabaddad2", + "8ce24998625615b603a033aca13fb894be9112a5c3a211a8ba262a3cca7e2ca701e4a9a4fba43c90ccdcb281d48c7c6fd62875d2aca417034c34aee5", + "619cc5aefffe0bfa462af43c1699d050" + )] + fn test_gcm_128( + #[case] kx: &str, + #[case] ivx: &str, + #[case] ptx: &str, + #[case] aadx: &str, + #[case] expected_ctx: &str, + #[case] expected_tagx: &str, + ) { + let k = decode_hex(kx).unwrap(); + let iv = decode_hex(ivx).unwrap(); + let pt = decode_hex(ptx).unwrap(); + let aad = decode_hex(aadx).unwrap(); + + let key = Key::<128>::new(k.try_into().unwrap()); + let gcm = GCM::>::new(key); + + let (ct, tag) = gcm.encrypt(&iv, &pt, &aad).unwrap(); + + let ctx = encode_hex(&ct); + assert_eq!(ctx, expected_ctx); + + let tagx = encode_hex(&tag); + assert_eq!(tagx, expected_tagx); + + let (_pt, _tag) = gcm.decrypt(&iv, &ct, &aad).unwrap(); + + let _ptx = encode_hex(&_pt); + assert_eq!(ptx, _ptx); + + let _tagx = encode_hex(&_tag); + assert_eq!(_tagx, expected_tagx); + } + + #[rstest] + // NIST Test Case 7 + #[case( + "000000000000000000000000000000000000000000000000", + "000000000000000000000000", + "", + "", + "", + "cd33b28ac773f74ba00ed1f312572435" + )] + // NIST Test Case 8 + #[case( + "000000000000000000000000000000000000000000000000", + "000000000000000000000000", + "00000000000000000000000000000000", + "", + "98e7247c07f0fe411c267e4384b0f600", + "2ff58d80033927ab8ef4d4587514f0fb" + )] + // NIST Test Case 9 + #[case( + "feffe9928665731c6d6a8f9467308308feffe9928665731c", + "cafebabefacedbaddecaf888", + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b391aafd255", + "", + "3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710acade256", + "9924a7c8587336bfb118024db8674a14")] + // NIST Test Case 10 + #[case( + "feffe9928665731c6d6a8f9467308308feffe9928665731c", + "cafebabefacedbaddecaf888", + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + "feedfacedeadbeeffeedfacedeadbeefabaddad2", + "3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710", + "2519498e80f1478f37ba55bd6d27618c")] + // NIST Test Case 11 + #[case( + "feffe9928665731c6d6a8f9467308308feffe9928665731c", + "cafebabefacedbad", + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + "feedfacedeadbeeffeedfacedeadbeefabaddad2", + "0f10f599ae14a154ed24b36e25324db8c566632ef2bbb34f8347280fc4507057fddc29df9a471f75c66541d4d4dad1c9e93a19a58e8b473fa0f062f7", + "65dcc57fcf623a24094fcca40d3533f8")] + // NIST Test Case 12 + #[case( + "feffe9928665731c6d6a8f9467308308feffe9928665731c", + "9313225df88406e555909c5aff5269aa6a7a9538534f7da1e4c303d2a318a728c3c0c95156809539fcf0e2429a6b525416aedbf5a0de6a57a637b39b", + "d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39", + "feedfacedeadbeeffeedfacedeadbeefabaddad2", + "d27e88681ce3243c4830165a8fdcf9ff1de9a1d8e6b447ef6ef7b79828666e4581e79012af34ddd9e2f037589b292db3e67c036745fa22e7e9b7373b", + "dcf566ff291c25bbb8568fc3d376a6d9")] + fn test_gcm_192( + #[case] kx: &str, + #[case] ivx: &str, + #[case] ptx: &str, + #[case] aadx: &str, + #[case] expected_ctx: &str, + #[case] expected_tagx: &str, + ) { + let k = decode_hex(kx).unwrap(); + let iv = decode_hex(ivx).unwrap(); + let pt = decode_hex(ptx).unwrap(); + let aad = decode_hex(aadx).unwrap(); + + let key = Key::new(k.try_into().unwrap()); + let gcm = GCM::>::new(key); + + let (ct, tag) = gcm.encrypt(&iv, &pt, &aad).unwrap(); + + let ctx = encode_hex(ct.as_ref()); + assert_eq!(ctx, expected_ctx); + + let tagx = encode_hex(&tag); + assert_eq!(tagx, expected_tagx); + + let (_pt, _tag) = gcm.decrypt(&iv, &ct, &aad).unwrap(); + + let _ptx = encode_hex(&_pt); + assert_eq!(ptx, _ptx); + + let _tagx = encode_hex(&_tag); + assert_eq!(_tagx, expected_tagx); + } +} diff --git a/src/encryption/symmetric/modes/mod.rs b/src/encryption/symmetric/modes/mod.rs index b991fe1..17ee1d1 100644 --- a/src/encryption/symmetric/modes/mod.rs +++ b/src/encryption/symmetric/modes/mod.rs @@ -6,3 +6,4 @@ #![doc = include_str!("./README.md")] pub mod cbc; pub mod ctr; +pub mod gcm; diff --git a/src/hashes/constants.rs b/src/hashes/constants.rs new file mode 100644 index 0000000..3326ed5 --- /dev/null +++ b/src/hashes/constants.rs @@ -0,0 +1,136 @@ +//! Constants used in hashes module, such as coefficients of polynomial of degree 129! +use crate::{algebra::field::prime::AESField, Field}; + +/// Represents the coefficients of field polynomial used in GCM +/// f = 1 + α + α^2 + α^7 + α^128 +pub const GCMF_IRREDUCIBLE_POLYNOMIAL_COEFFICIENTS: [AESField; 129] = [ + AESField::ONE, + AESField::ONE, + AESField::ONE, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ONE, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ZERO, + AESField::ONE, +]; diff --git a/src/hashes/ghash.rs b/src/hashes/ghash.rs new file mode 100644 index 0000000..1bc4f73 --- /dev/null +++ b/src/hashes/ghash.rs @@ -0,0 +1,263 @@ +//! Implementation of [`GHASH`] algorithm which is used in AES-GCM. +//! Based on GCM specification given by NIST: +//! [The Galois/Counter Mode of Operation (GCM)](http://www.csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf) + +use core::array; + +use super::constants::GCMF_IRREDUCIBLE_POLYNOMIAL_COEFFICIENTS; +use crate::{ + algebra::field::{extension::GaloisField, prime::AESField}, + polynomial::{Monomial, Polynomial}, + Field, +}; + +type GCMField = GaloisField<128, 2>; +/// Note: ['AESField'] is an alias for [`PrimeField<2>`] + +/// Helper function which turns [`num`], u64, value to vector of [`AESField`] elements +/// The result is aligned to [`length`] by padding with AESField::ZERO. +/// For example: +/// If num = 5 and length = 8, the result is: +/// {ZERO, ZERO, ZERO, ZERO, ZERO, ONE, ZERO, ONE} +/// where the rightmost value is the least significant bit(=2^0) +/// and `ZERO` and `ONE` are `AESField` elements. +fn to_bool_vec(mut num: u64, length: usize) -> Vec { + let mut result = vec![AESField::ZERO; length]; + let mut idx = length; + while num > 0 { + result[idx - 1] = match num & 1 { + 0 => AESField::ZERO, + _ => AESField::ONE, + }; + num >>= 1; + idx -= 1; + } + result +} + +/// Convert bytes(a slice of u8 value) to GCMField element. +impl From<&[u8]> for GCMField { + fn from(value: &[u8]) -> Self { + let mut result = Vec::::new(); + for &b in value.iter() { + let b_f: Vec = to_bool_vec(b as u64, 8); + result.extend(b_f); + } + result.extend(std::iter::repeat(AESField::ZERO).take(128 - value.len() * 8)); + Self { coeffs: result.try_into().unwrap() } + } +} + +/// Convert a GCMField element to a vector of u8 values. +impl From for Vec { + fn from(value: GCMField) -> Self { + let mut bytes = Vec::new(); + for block in value.coeffs.chunks(8) { + let mut byte: u8 = 0; + for i in 0..8 { + if block[i] == AESField::ONE { + byte += (1 << (7 - i)) as u8; + } + } + bytes.push(byte); + } + bytes + } +} + +/// Represents the GHASH object which holds `hash_key`, a GCMField element, which in +/// context of AES-GCM is equal to E(K, 0^128), where E is AES encryption and K, the key. +pub struct GHASH { + hash_key: GCMField, +} + +impl GHASH { + /// Construct GHASH object using 'h', the hash key(in bytes). + /// In context of AES-GCM, h = AES(K, 0^128), that is enc 128-bit string of zeros + /// encrypted using AES. + pub fn new(h: &[u8]) -> Self { + if h.len() != 16 { + panic!("The hash key should be 128-bits, or 16 u8 values! Got {} u8 vals", h.len()); + } + Self { hash_key: GCMField::from(h) } + } + + /// Get the hash digest using using two u8 slices `a` and `c`. + /// In context of AES-GCM they correspond to, + /// 'aad' - additional authenticated data(AAD) + /// 'ct' - ciphertext encrypted using AES. + /// + /// The result is a 128-bit digest, returned as an `[u8; 16]` array. + pub fn digest(&self, aad: &[u8], ct: &[u8]) -> [u8; 16] { + // The variable where the final digest is accumulated. + let mut j = GCMField::default(); + + // Process each 16-byte block from the aad + for block in aad.chunks(16) { + let a = GCMField::from(block); + // ADD a block of `aad` into the result and then MULTIPLY with hash_key. + // ADD interestingly is same as XOR in our field. + // MULTIPLY is a polynomial multiplication modulo a fixed field polynomial. See + // `poly_multiply` + j = Self::poly_multiply(a + j, self.hash_key); + } + + // Do the same stuff as the previous loop but with ciphertext. + for block in ct.chunks(16) { + let a = GCMField::from(block); + j = Self::poly_multiply(a + j, self.hash_key); + } + + // The final block consists of length of `aad` in bits and length of `ct` in bits, each in + // 64-bit format. + let mut aad_len = to_bool_vec((aad.len() * 8) as u64, 64); + let mut ct_len = to_bool_vec((ct.len() * 8) as u64, 64); + aad_len.append(&mut ct_len); + let len = GCMField { coeffs: aad_len.try_into().unwrap() }; + j = Self::poly_multiply(j + len, self.hash_key); + + let j_bytes: Vec = j.into(); + j_bytes.try_into().unwrap() + } + + /// Returns the result of multiplication of two GCMField elements, + /// modulo the field polynomial, f = 1 + α + α^2 + α^7 + α^128 + fn poly_multiply(x: GCMField, y: GCMField) -> GCMField { + let x_coeffs: [AESField; 128] = x.coeffs.try_into().unwrap(); + let y_coeffs: [AESField; 128] = y.coeffs.try_into().unwrap(); + let poly_x = Polynomial::::from(x_coeffs); + let poly_y = Polynomial::::from(y_coeffs); + let poly_f = + Polynomial::::from(GCMF_IRREDUCIBLE_POLYNOMIAL_COEFFICIENTS); + + let poly_z = (poly_x * poly_y) % poly_f; + let res: [AESField; 128] = + array::from_fn(|i| poly_z.coefficients.get(i).cloned().unwrap_or(AESField::ZERO)); + + GCMField::new(res) + } + + /// Multiply two elements in GCMField modulo the field polynomial f. + /// The field polynomial is fixed and is given by, f = 1 + α + α^2 + α^7 + α^128 + /// The function uses the algorithm given in the GCM spec. + /// NOTE: We do not use this function in our GHASH algorithm, it is for reference only. + #[allow(dead_code)] + fn poly_multiply_spec(x: GCMField, y: GCMField) -> GCMField { + let mut r_coeffs = to_bool_vec(0xe1, 128); + r_coeffs.rotate_left(120); + let r = GCMField { coeffs: r_coeffs.try_into().unwrap() }; + + let mut z = GCMField::from(0 as usize); + let mut v = y; + + for bit in x.coeffs { + if bit == AESField::ONE { + z = z + v; + } + + let mut v1 = v.coeffs.to_vec(); + let v1_bit = v1.pop().unwrap(); + v1.push(AESField::ZERO); + v1.rotate_right(1); + + v = GCMField { coeffs: v1.try_into().unwrap() }; + + if v1_bit == AESField::ONE { + v = v + r; + } + } + + z + } +} + +#[cfg(test)] +mod tests { + use core::str; + use std::{fmt::Write, num::ParseIntError}; + + use rstest::rstest; + + use super::*; + pub fn decode_hex(s: &str) -> Result, ParseIntError> { + (0..s.len()).step_by(2).map(|i| u8::from_str_radix(&s[i..i + 2], 16)).collect() + } + + pub fn encode_hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for &b in bytes { + write!(&mut s, "{:02x}", b).unwrap(); + } + s + } + + // Tests against NIST test vectors in [GCM spec](https://csrc.nist.rip/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf) + #[rstest] + // NIST Test Case #1 + #[case("66e94bd4ef8a2c3b884cfa59ca342b2e", "", "", "00000000000000000000000000000000")] + // NIST Test Case #2 + #[case( + "66e94bd4ef8a2c3b884cfa59ca342b2e", + "", + "0388dace60b6a392f328c2b971b2fe78", + "f38cbb1ad69223dcc3457ae5b6b0f885" + )] + // NIST Test Case #3 + #[case( + "b83b533708bf535d0aa6e52980d53b78", + "", + "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f5985", + "7f1b32b81b820d02614f8895ac1d4eac" + )] + // NIST Test Case #4 + #[case( + "b83b533708bf535d0aa6e52980d53b78", + "feedfacedeadbeeffeedfacedeadbeefabaddad2", + "42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091", + "698e57f70e6ecc7fd9463b7260a9ae5f", + )] + // NIST Test Case #6 + #[case( + "b83b533708bf535d0aa6e52980d53b78", + "feedfacedeadbeeffeedfacedeadbeefabaddad2", + "8ce24998625615b603a033aca13fb894be9112a5c3a211a8ba262a3cca7e2ca701e4a9a4fba43c90ccdcb281d48c7c6fd62875d2aca417034c34aee5", + "1c5afe9760d3932f3c9a878aac3dc3de", + )] + fn test_ghash(#[case] hx: &str, #[case] ax: &str, #[case] cx: &str, #[case] expected: &str) { + let h = decode_hex(hx).unwrap(); + let a = decode_hex(ax).unwrap(); + let c = decode_hex(cx).unwrap(); + + let gh = GHASH::new(&h); + let digest = gh.digest(&a, &c); + + let digest_hex = encode_hex(&digest); + + println!("GHASH:{}\nExpected:{}", digest_hex, expected); + assert!(digest_hex == expected); + } + + #[rstest] + #[case(1, 1)] + #[case(2, 3)] + #[case(113, 117)] + #[case(0xca, 0xfe)] + fn test_poly_multiply(#[case] x: u64, #[case] y: u64) { + let x_coeffs: Vec = to_bool_vec(x, 128).into_iter().rev().collect(); + let xf = GCMField { coeffs: x_coeffs.try_into().unwrap() }; + let y_coeffs = to_bool_vec(y, 128); + let yf = GCMField { coeffs: y_coeffs.try_into().unwrap() }; + + let zf = GHASH::poly_multiply(xf, yf); + + let z_coeffs: Vec = zf.try_into().unwrap(); + let z_hex = encode_hex(&z_coeffs); + + let expected_zf = GHASH::poly_multiply_spec(xf, yf); + let expected_z_coeffs: Vec = expected_zf.try_into().unwrap(); + let expected_z_hex = encode_hex(&expected_z_coeffs); + + println!("Got: {z_hex}\nExp: {expected_z_hex}"); + assert!(expected_z_hex == z_hex); + } +} diff --git a/src/hashes/mod.rs b/src/hashes/mod.rs index 2c50760..b94c7ab 100644 --- a/src/hashes/mod.rs +++ b/src/hashes/mod.rs @@ -5,6 +5,8 @@ #![doc = include_str!("./README.md")] pub mod sha256; use crate::Field; +pub mod constants; +pub mod ghash; pub mod poseidon; /// Sponge trait defining absorb and squeeze behavior of sponge based hash function.