From 6e6a230da92c7199542bc4a4778ba718f65e9150 Mon Sep 17 00:00:00 2001 From: Sambhav Date: Mon, 8 Jul 2024 21:31:01 +0530 Subject: [PATCH] feat: cipher modes of operation (#127) * feat: AES decryption * cleanup * docs: update README for aes decryption * remove accidental duplication * docs: fix typo in aes decryption example * docs: replace hex representation of a^-1(x) with decimal representation * feat: better name/code/docs for galois multiplication * more hex representation replacement * simplify galois_multiplication params * docs: fix typo * init cbc * add `BlockCipher` trait * add cbc * tests and docs * aes cbc example * add ctr * fix docs * change padding for CBC mode * fix doctests --------- Co-authored-by: bing --- Cargo.toml | 9 +- examples/aes_chained_cbc.rs | 72 +++++++++ src/encryption/symmetric/aes/mod.rs | 58 +++++-- src/encryption/symmetric/aes/tests.rs | 24 +-- src/encryption/symmetric/chacha/tests.rs | 4 +- src/encryption/symmetric/counter.rs | 59 +++++++ src/encryption/symmetric/mod.rs | 22 +++ src/encryption/symmetric/modes/README.md | 46 ++++++ src/encryption/symmetric/modes/cbc.rs | 193 +++++++++++++++++++++++ src/encryption/symmetric/modes/ctr.rs | 146 +++++++++++++++++ src/encryption/symmetric/modes/mod.rs | 8 + src/field/extension/gf_101_2.rs | 4 +- 12 files changed, 616 insertions(+), 29 deletions(-) create mode 100644 examples/aes_chained_cbc.rs create mode 100644 src/encryption/symmetric/counter.rs create mode 100644 src/encryption/symmetric/modes/README.md create mode 100644 src/encryption/symmetric/modes/cbc.rs create mode 100644 src/encryption/symmetric/modes/ctr.rs create mode 100644 src/encryption/symmetric/modes/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 27bb71e..d443d09 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ edition ="2021" license ="Apache2.0 OR MIT" name ="ronkathon" repository ="https://github.com/thor314/ronkathon" -version = "0.1.0" +version ="0.1.0" [dependencies] rand ="0.8.5" @@ -18,8 +18,8 @@ pretty_assertions ="1.4.0" sha2 ="0.10.8" ark-ff ={ version="^0.4.0", features=["std"] } ark-crypto-primitives={ version="0.4.0", features=["sponge"] } -des = "0.8.1" -chacha20 = "0.9.1" +des ="0.8.1" +chacha20 ="0.9.1" [patch.crates-io] ark-ff ={ git="https://github.com/arkworks-rs/algebra/" } @@ -27,3 +27,6 @@ ark-ec ={ git="https://github.com/arkworks-rs/algebra/" } ark-poly ={ git="https://github.com/arkworks-rs/algebra/" } ark-serialize={ git="https://github.com/arkworks-rs/algebra/" } ark-std ={ git="https://github.com/arkworks-rs/std/" } + +[[example]] +name="aes_chained_cbc" diff --git a/examples/aes_chained_cbc.rs b/examples/aes_chained_cbc.rs new file mode 100644 index 0000000..81dfe36 --- /dev/null +++ b/examples/aes_chained_cbc.rs @@ -0,0 +1,72 @@ +//! Demonstrating AES chained CBC mode of operation where last ciphertext of previous operation is +//! used as IV for next operation. This has advantage as it reduces the bandwidth to share a new IV +//! each time between the parties. But in CBC mode, IV should be unpredictable, this was formalised in [CWE-329](https://cwe.mitre.org/data/definitions/329.html). +//! +//! But this scheme is not Chosen-Plaintext Attack secure and any +//! attacker can detect which original message was used in the ciphertext which is shown here. +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] +use rand::{thread_rng, Rng}; +use ronkathon::encryption::symmetric::{ + aes::{Block, Key, AES}, + modes::cbc::CBC, +}; + +fn attacker_chosen_message() -> [&'static [u8]; 2] { + [b"You're gonna be pwned!", b"HAHA, You're gonna be dbl pwned!!"] +} + +fn xor_blocks(a: &mut [u8], b: &[u8]) { + for (x, y) in a.iter_mut().zip(b) { + *x ^= *y; + } +} + +fn attacker<'a>(key: &Key<128>, iv: &Block, ciphertext: Vec) -> &'a [u8] { + // Chose 2 random messages, {m_0, m_1} + let messages = attacker_chosen_message(); + + // first blocks' ciphertext + let c1 = &ciphertext[..16]; + + // select new IV as last blocks' ciphertext and intiate CBC with AES again with new IV + let new_iv: [u8; 16] = ciphertext[ciphertext.len() - 16..].try_into().unwrap(); + let cbc2 = CBC::>::new(Block(new_iv)); + + // Now, attacker selects the new message m_4 = IV ⨁ m_0 ⨁ NEW_IV + let mut pwned_message = iv.0; + xor_blocks(&mut pwned_message, messages[0]); + xor_blocks(&mut pwned_message, &new_iv); + + // attacker receives ciphertext from encryption oracle + let encrypted = cbc2.encrypt(key, &pwned_message); + + // attacker has gained knowledge about initial message + if c1 == encrypted { + messages[0] + } else { + messages[1] + } +} + +/// We simulate Chained CBC and show that attacker can know whether initial plaintext was message 1 +/// or 2. +fn main() { + let mut rng = thread_rng(); + + // generate a random key and publicly known IV, and initiate CBC with AES cipher + let key = Key::<128>::new(rng.gen()); + let iv = Block(rng.gen()); + let cbc = CBC::>::new(iv); + + // Chose 2 random messages, {m_0, m_1} + let messages = attacker_chosen_message(); + + // select a uniform bit b, and chose message m_b for encryption + let bit = rng.gen_range(0..=1); + let encrypted = cbc.encrypt(&key, messages[bit]); + + let predicted_message = attacker(&key, &iv, encrypted); + + assert_eq!(messages[bit], predicted_message); +} diff --git a/src/encryption/symmetric/aes/mod.rs b/src/encryption/symmetric/aes/mod.rs index f27efbc..27e3247 100644 --- a/src/encryption/symmetric/aes/mod.rs +++ b/src/encryption/symmetric/aes/mod.rs @@ -11,14 +11,33 @@ use crate::field::{extension::AESFieldExtension, prime::AESField}; pub mod sbox; #[cfg(test)] pub mod tests; -use super::SymmetricEncryption; +use super::{BlockCipher, SymmetricEncryption}; use crate::{ encryption::symmetric::aes::sbox::{INVERSE_SBOX, SBOX}, field::FiniteField, }; /// A block in AES represents a 128-bit sized message data. -pub type Block = [u8; 16]; +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct Block(pub [u8; 16]); + +impl From> for Block { + fn from(value: Vec) -> Self { + assert!(value.len() == 16); + let val: [u8; 16] = value + .try_into() + .unwrap_or_else(|v: Vec| panic!("expected a vec of len: {} but got: {}", 16, v.len())); + Self(val) + } +} + +impl AsRef<[u8]> for Block { + fn as_ref(&self) -> &[u8] { &self.0 } +} + +impl AsMut<[u8]> for Block { + fn as_mut(&mut self) -> &mut [u8] { self.0.as_mut() } +} /// A word in AES represents a 32-bit array of data. pub type Word = [u8; 4]; @@ -55,18 +74,19 @@ where [(); N / 8]: /// /// ## Example /// ```rust + /// #![allow(incomplete_features)] /// #![feature(generic_const_exprs)] /// /// use rand::{thread_rng, Rng}; /// use ronkathon::encryption::symmetric::{ - /// aes::{Key, AES}, + /// aes::{Block, 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); + /// let encrypted = AES::encrypt(&key, &Block(plaintext)); /// ``` fn encrypt(key: &Self::Key, plaintext: &Self::Block) -> Self::Block { let num_rounds = match N { @@ -76,25 +96,26 @@ where [(); N / 8]: _ => panic!("AES only supports key sizes 128, 192 and 256 bits. You provided: {N}"), }; - Self::aes_encrypt(plaintext, key, num_rounds) + Self::aes_encrypt(&plaintext.0, key, num_rounds) } /// Decrypt a ciphertext of size [`Block`] with a [`Key`] of size `N`-bits. /// /// ## Example /// ```rust + /// #![allow(incomplete_features)] /// #![feature(generic_const_exprs)] /// /// use rand::{thread_rng, Rng}; /// use ronkathon::encryption::symmetric::{ - /// aes::{Key, AES}, + /// aes::{Block, 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); + /// let encrypted = AES::encrypt(&key, &Block(plaintext)); /// let decrypted = AES::decrypt(&key, &encrypted); /// ``` fn decrypt(key: &Self::Key, ciphertext: &Self::Block) -> Self::Block { @@ -105,7 +126,7 @@ where [(); N / 8]: _ => panic!("AES only supports key sizes 128, 192 and 256 bits. You provided: {N}"), }; - Self::aes_decrypt(ciphertext, key, num_rounds) + Self::aes_decrypt(&ciphertext.0, key, num_rounds) } } @@ -223,7 +244,7 @@ where [(); N / 8]: "Round keys not fully consumed - perhaps check key expansion?" ); - state.0.into_iter().flatten().collect::>().try_into().unwrap() + Block(state.0.into_iter().flatten().collect::>().try_into().unwrap()) } /// Deciphers a given `ciphertext`, with key size of `N` (in bits), as seen in Figure 5 of the @@ -268,7 +289,7 @@ where [(); N / 8]: "Round keys not fully consumed - perhaps check key expansion?" ); - state.0.into_iter().flatten().collect::>().try_into().unwrap() + state.0.into_iter().flatten().collect::>().into() } /// XOR a round key to its internal state. @@ -477,3 +498,20 @@ where [(); N / 8]: ); } } + +impl BlockCipher for AES +where [(); N / 8]: +{ + type Block = Block; + type Key = Key; + + const BLOCK_SIZE: usize = 16; + + fn encrypt_block(key: &Self::Key, plaintext: &Self::Block) -> Self::Block { + Self::encrypt(key, plaintext) + } + + fn decrypt_block(key: &Self::Key, ciphertext: &Self::Block) -> Self::Block { + Self::decrypt(key, ciphertext) + } +} diff --git a/src/encryption/symmetric/aes/tests.rs b/src/encryption/symmetric/aes/tests.rs index b3e02d7..612f30c 100644 --- a/src/encryption/symmetric/aes/tests.rs +++ b/src/encryption/symmetric/aes/tests.rs @@ -12,16 +12,16 @@ fn test_aes_128() { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, ]; - let state = AES::encrypt(&key, &plaintext); + let state = AES::encrypt_block(&key, &Block(plaintext)); - let expected_ciphertext = Block::from([ + let expected_ciphertext = Block::from(vec![ 0x69, 0xc4, 0xe0, 0xd8, 0x6a, 0x7b, 0x04, 0x30, 0xd8, 0xcd, 0xb7, 0x80, 0x70, 0xb4, 0xc5, 0x5a, ]); assert_eq!(state, expected_ciphertext); - let decrypted = AES::decrypt(&key, &state); - assert_eq!(decrypted, plaintext); + let decrypted = AES::decrypt_block(&key, &state); + assert_eq!(decrypted.0, plaintext); } #[test] @@ -36,16 +36,16 @@ fn test_aes_192() { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, ]; - let state = AES::encrypt(&key, &plaintext); + let state = AES::encrypt_block(&key, &Block(plaintext)); - let expected_ciphertext = Block::from([ + let expected_ciphertext = Block::from(vec![ 0xdd, 0xa9, 0x7c, 0xa4, 0x86, 0x4c, 0xdf, 0xe0, 0x6e, 0xaf, 0x70, 0xa0, 0xec, 0x0d, 0x71, 0x91, ]); assert_eq!(state, expected_ciphertext); - let decrypted = AES::decrypt(&key, &state); - assert_eq!(decrypted, plaintext); + let decrypted = AES::decrypt_block(&key, &state); + assert_eq!(decrypted.0, plaintext); } #[test] @@ -60,14 +60,14 @@ fn test_aes_256() { 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, ]; - let state = AES::encrypt(&key, &plaintext); + let state = AES::encrypt_block(&key, &Block(plaintext)); - let expected_ciphertext = Block::from([ + let expected_ciphertext = Block::from(vec![ 0x8e, 0xa2, 0xb7, 0xca, 0x51, 0x67, 0x45, 0xbf, 0xea, 0xfc, 0x49, 0x90, 0x4b, 0x49, 0x60, 0x89, ]); assert_eq!(state, expected_ciphertext); - let decrypted = AES::decrypt(&key, &state); - assert_eq!(decrypted, plaintext); + let decrypted = AES::decrypt_block(&key, &state); + assert_eq!(decrypted.0, plaintext); } diff --git a/src/encryption/symmetric/chacha/tests.rs b/src/encryption/symmetric/chacha/tests.rs index 3d91e19..80a2efe 100644 --- a/src/encryption/symmetric/chacha/tests.rs +++ b/src/encryption/symmetric/chacha/tests.rs @@ -123,10 +123,10 @@ fn chacha_fuzz() { nonce.iter().flat_map(|val| val.to_le_bytes()).collect::>().try_into().expect("err"); let mut cipher = ChaCha20::new(&flat_key.into(), &flat_nonce.into()); - let mut buffer = plaintext.clone(); + let mut buffer = plaintext; cipher.apply_keystream(&mut buffer); - let ciphertext = buffer.clone(); + let ciphertext = buffer; assert_eq!(ronk_ciphertext, ciphertext.to_vec()); diff --git a/src/encryption/symmetric/counter.rs b/src/encryption/symmetric/counter.rs new file mode 100644 index 0000000..c738d4a --- /dev/null +++ b/src/encryption/symmetric/counter.rs @@ -0,0 +1,59 @@ +//! Counter used during various encryption primitives for randomising IV in reduced bandwidth +//! scenarios. Implements a simple increment by one counter. + +/// Counter consisting of big-endian integer using byte(8-bit) limbs +#[derive(Debug, Clone, Copy)] +pub struct Counter(pub [u8; C]); + +impl Counter { + /// returns a new Counter + /// ## Arguments + /// - `value`: big-endian integer represented using 8-bit limbs + pub fn new(value: [u8; C]) -> Self { Self(value) } + + /// increases counter value by 1 for each new round of `C` byte input. + /// + /// ## Note + /// Returns `max counter reached` error when counter value reaches maximum allowed by different + /// counter length. + pub fn increment(&mut self) -> Result<(), String> { + match C { + 0 => Err("counter value is 0".to_string()), + _ => { + // check for max value + let mut flag = true; + for value in self.0.iter() { + if *value != u8::MAX { + flag = false; + } + } + + if flag { + return Err("max counter reached".to_string()); + } + + let mut add_carry = true; + for i in (0..C).rev() { + let (incremented_val, carry) = self.0[i].overflowing_add(add_carry as u8); + self.0[i] = incremented_val; + add_carry = carry; + } + + Ok(()) + }, + } + } +} + +impl From for Counter { + fn from(value: usize) -> Self { + let mut limbs = [0u8; C]; + + let value_bytes = value.to_be_bytes(); + for i in (0..std::cmp::min(C, 8)).rev() { + limbs[i] = value_bytes[i]; + } + + Self(limbs) + } +} diff --git a/src/encryption/symmetric/mod.rs b/src/encryption/symmetric/mod.rs index 8b835b9..781fcee 100644 --- a/src/encryption/symmetric/mod.rs +++ b/src/encryption/symmetric/mod.rs @@ -1,7 +1,10 @@ //! Contains implementation of symmetric encryption primitives. +#![doc = include_str!("./README.md")] pub mod aes; pub mod chacha; +pub mod counter; pub mod des; +pub mod modes; /// Trait for symmetric encryption primitive pub trait SymmetricEncryption { @@ -54,3 +57,22 @@ pub trait StreamCipher { ciphertext: &[u8], ) -> Result, Self::Error>; } + +/// Trait for block ciphers that works on bytes of specific sizes +pub trait BlockCipher { + /// Block size in bytes for cipher oprations + const BLOCK_SIZE: usize; + /// Block acted upon by the cipher + type Block: AsRef<[u8]> + AsMut<[u8]> + From> + Copy + PartialEq; + /// Secret key for encryption/decryption + type Key; + + /// Encrypt a plaintext of arbitrary length and returns ciphertext of same length as plaintext. + /// + /// Note: correct padding scheme should be used as per the mode of operation of cipher. + fn encrypt_block(key: &Self::Key, plaintext: &Self::Block) -> Self::Block; + /// Decrypt a ciphertext of arbitrary length and returns plaintext. + /// + /// Note: any padded bytes should be removed + fn decrypt_block(key: &Self::Key, ciphertext: &Self::Block) -> Self::Block; +} diff --git a/src/encryption/symmetric/modes/README.md b/src/encryption/symmetric/modes/README.md new file mode 100644 index 0000000..f77b9b4 --- /dev/null +++ b/src/encryption/symmetric/modes/README.md @@ -0,0 +1,46 @@ +# Modes of Operation + +Block ciphers can only do fixed length encryption of bits in a well-defined Block. Modes of operation allows to extend cipher's encryption/decryption algorithms to allow arbitrary length data. + +Some operations require *Initialisation vector* (IV) that must not repeat for subsequent encryption operations, and must be random for some operations. IV is used so that different ciphertext is generated for same plaintext even for a single key. We demonstrate this in our [tests](./cbc.rs). + +Appropriate padding has to be performed for some modes, as block ciphers only work of fixed size blocks. Since, most of the ciphers are used with [MAC](https://en.wikipedia.org/wiki/Message_authentication_code) that provides integrity guarantees and prevent Chosen-Ciphertext attacks on the protocol, so, [any](https://crypto.stackexchange.com/questions/62379/choice-of-padding-scheme-to-prevent-cbc-padding-oracle-attacks) padding scheme works, most common is PKCS#7 or even null byte padding. Note that, without use of MACs, almost all block ciphers with padding are susceptible to [Padding Oracle Attacks](https://en.wikipedia.org/wiki/Padding_oracle_attack) and should be handled with utmost care. + +Let's go into detail about Block cipher's [mode of operation](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation): + +## ECB: Electronic codebook +- deterministic, so not CPA-secure. +- can be parallelised easily. +## CBC: cipher block chaining +- IV chosen uniformly and $c_{0}=IV$, then $c_{i}=F_{k}(c_{i-1} \oplus m_{i})$ +- sequential in nature, although decryption can be parallelised as inputs to block cipher's encryption is just the ciphertext +- chained CBC, where ciphertext is chained for subsequent encryptions. + - But it's not CPA secure, as attacker can distinguish between PRF and uniform random function by choosing appropriate text in second encryption. +## OFB: output feedback +- IV is chosen uniformly and $y_{0}:=IV$, then $y_{i}=F_{k}(y_{i-1})$ and $c_{i}=y_{i} \oplus m_{i}$. +- this allows $F_{k}$ to not be invertible, and can be simply a PRF. +- Due to this, OFB can be used to encrypt plaintext of arbitrary lengths and not have to be multiple of block length. +- pseudorandom stream can be preprocessed and then encryption can be really fast. +- it's stateful variant can be used to instantiate stream cipher's synchronised mode of operation and is secure. +## CTR: counter mode +- can be viewed as unsynchronised stream cipher mode, where $y_{i}=F_{k}(\langle IV \parallel i\rangle)$ for binary string $i = 1,2,\dots,$ and $c_{i}=y_{i}\oplus m_{i}$. +- this again allows $F_{k}$ to not be invertible and can be instantiated with a Pseudorandom function. +- can be fully parallelised. +```mermaid +flowchart TB +IV1[IV]---->IV2[IV] +IV3["IV||1"]-->Fk1[F_k]-->xor1["⨁"]-->c1 +m1-->xor1 +IV4["IV||2"]-->Fk2[F_k]-->xor2["⨁"]-->c2 +m2-->xor2 +``` + +## Next Steps +Implement more modes, and subsequent attacks/vulnerabilities: +- [ ] CFB +- [ ] OFB + +## References + +- [Understanding Cryptography by Cristof Paar & Jan Pelzl & Tim Güneysu: Chapter 3, 4](https://www.cryptography-textbook.com/) +- [Introduction to Modern Cryptography: Section 1](https://www.cs.umd.edu/~jkatz/imc.html) diff --git a/src/encryption/symmetric/modes/cbc.rs b/src/encryption/symmetric/modes/cbc.rs new file mode 100644 index 0000000..1c2ce71 --- /dev/null +++ b/src/encryption/symmetric/modes/cbc.rs @@ -0,0 +1,193 @@ +//! Cipher block chaining mode of operation for [`BlockCipher`] +use crate::encryption::symmetric::BlockCipher; + +/// Cipher block chaining mode of operation that works on any [`BlockCipher`]. Initialisation +/// Vector (IV) should not be reused for different plaintext. +pub struct CBC { + iv: C::Block, +} + +fn xor_blocks(a: &mut [u8], b: &[u8]) { + for (x, y) in a.iter_mut().zip(b) { + *x ^= *y; + } +} + +impl CBC { + /// creates a new [`CBC`] mode of operation for [`BlockCipher`] + /// ## Arguments + /// - `iv`: initialisation vector for randomising the state. + /// + /// Note: IV shouldn't be reused for different encryptions + pub fn new(iv: C::Block) -> Self { Self { iv } } + + /// Encrypt arbitrary length of bytes. + /// + /// ## Arguments + /// - `key`: cipher's secret key + /// - `plaintext`: data to encrypt + /// + /// ## Usage + /// ``` + /// #![allow(incomplete_features)] + /// #![feature(generic_const_exprs)] + /// use rand::{thread_rng, Rng}; + /// use ronkathon::encryption::symmetric::{ + /// aes::{Block, Key, AES}, + /// modes::cbc::CBC, + /// }; + /// + /// let mut rng = thread_rng(); + /// let rand_key: [u8; 16] = rng.gen(); + /// let key = Key::<128>::new(rand_key); + /// let iv = Block(rng.gen()); + /// let plaintext = b"Hello World!"; + /// + /// let cbc = CBC::>::new(iv); + /// + /// let ciphertext = cbc.encrypt(&key, plaintext); + /// ``` + /// + /// **Note**: plaintext is padded using PKCS#7, if not a multiple of [`BlockCipher::BLOCK_SIZE`]. + pub fn encrypt(&self, key: &C::Key, plaintext: &[u8]) -> Vec { + let mut ciphertext = Vec::new(); + let mut prev_ciphertext = self.iv; + + // pad plaintext using PKCS#7 padding scheme + let mut plaintext = plaintext.to_vec(); + if plaintext.len() % C::BLOCK_SIZE != 0 { + let length = C::BLOCK_SIZE - (plaintext.len() % C::BLOCK_SIZE); + plaintext.extend(std::iter::repeat(length as u8).take(length)); + } + + for chunk in plaintext.chunks(C::BLOCK_SIZE) { + xor_blocks(prev_ciphertext.as_mut(), chunk); + prev_ciphertext = C::encrypt_block(key, &prev_ciphertext); + ciphertext.extend_from_slice(prev_ciphertext.as_ref()); + } + ciphertext + } + + /// Decrypt ciphertext using CBC mode. + /// + /// ## Arguments + /// - `key`: secret key used during encryption + /// - `ciphertext`: plaintext encrypted using key and iv + /// + /// ## Usage + /// ``` + /// #![allow(incomplete_features)] + /// #![feature(generic_const_exprs)] + /// use rand::{thread_rng, Rng}; + /// use ronkathon::encryption::symmetric::{ + /// aes::{Block, Key, AES}, + /// modes::cbc::CBC, + /// }; + /// + /// let mut rng = thread_rng(); + /// let rand_key: [u8; 16] = rng.gen(); + /// let key = Key::<128>::new(rand_key); + /// let iv = Block(rng.gen()); + /// let plaintext = b"Hello World!"; + /// + /// let cbc = CBC::>::new(iv); + /// + /// let ciphertext = cbc.encrypt(&key, plaintext); + /// let decrypted = cbc.decrypt(&key, &ciphertext); + /// + /// assert_eq!(*plaintext, decrypted[..plaintext.len()]); + /// ``` + /// + /// **Note**: decrypted plaintext will be multiple of [`BlockCipher::Block`]. It's user's + /// responsibility to truncate to original plaintext's length + pub fn decrypt(&self, key: &C::Key, ciphertext: &[u8]) -> Vec { + assert!(ciphertext.len() % C::BLOCK_SIZE == 0, "ciphertext is not a multiple of block size"); + + let mut prev_ciphertext: Vec = self.iv.as_ref().to_vec(); + let mut plaintext = Vec::new(); + + for chunk in ciphertext.chunks(C::BLOCK_SIZE) { + let mut decrypted = C::decrypt_block(key, &C::Block::from(chunk.to_vec())); + xor_blocks(decrypted.as_mut(), &prev_ciphertext); + prev_ciphertext = chunk.to_vec(); + + plaintext.extend_from_slice(decrypted.as_ref()); + } + + // remove PKCS#7 padding by checking the last byte and removing all intermediate bytes + let last_byte = plaintext[plaintext.len() - 1]; + if plaintext[plaintext.len() - last_byte as usize] == last_byte { + plaintext.truncate(plaintext.len() - last_byte as usize); + } + + plaintext + } +} + +#[cfg(test)] +mod tests { + use rand::{thread_rng, Rng}; + use rstest::{fixture, rstest}; + + use super::*; + use crate::encryption::symmetric::aes::{Block, Key, AES}; + + #[fixture] + fn rand_key() -> Key<128> { + let mut rng = thread_rng(); + let rand_key: [u8; 16] = rng.gen(); + Key::new(rand_key) + } + + #[fixture] + fn rand_iv() -> Block { + let mut rng = thread_rng(); + Block(rng.gen()) + } + + fn rand_message(length: usize) -> Vec { + let mut rng = thread_rng(); + + (0..length).map(|_| rng.gen::()).collect() + } + + #[rstest] + fn cbc(rand_key: Key<128>, rand_iv: Block) { + let cbc = CBC::>::new(rand_iv); + + for _ in 0..10 { + let mut rng = thread_rng(); + let plaintext = rand_message(rng.gen_range(1000..10000)); + let ciphertext = cbc.encrypt(&rand_key, &plaintext); + + let decrypted = cbc.decrypt(&rand_key, &ciphertext); + + assert_eq!(plaintext.len(), decrypted.len()); + assert_eq!(plaintext, decrypted); + } + } + + #[rstest] + fn different_iv(rand_key: Key<128>, rand_iv: Block) { + let cbc = CBC::>::new(rand_iv); + + let mut rand_iv = rand_iv; + rand_iv.0[0] += 1; + + let cbc2 = CBC::>::new(rand_iv); + + let mut rng = thread_rng(); + let plaintext = rand_message(rng.gen_range(1000..100000)); + + let ciphertext = cbc.encrypt(&rand_key, &plaintext); + let ciphertext2 = cbc2.encrypt(&rand_key, &plaintext); + + assert_ne!(ciphertext, ciphertext2); + + let decrypted = cbc.decrypt(&rand_key, &ciphertext); + let decrypted2 = cbc2.decrypt(&rand_key, &ciphertext2); + + assert_eq!(plaintext, decrypted); + assert_eq!(decrypted, decrypted2); + } +} diff --git a/src/encryption/symmetric/modes/ctr.rs b/src/encryption/symmetric/modes/ctr.rs new file mode 100644 index 0000000..4864c1b --- /dev/null +++ b/src/encryption/symmetric/modes/ctr.rs @@ -0,0 +1,146 @@ +//! Contains implementation for Counter (CTR) mode of operation in block ciphers + +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], +} + +impl CTR +where [(); C::BLOCK_SIZE / 2]: +{ + /// 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 } } + + /// Encrypt a plaintext with [`BlockCipher::Key`] and [`Counter`] + /// ## Arguments + /// - `key`: secret key used for [`BlockCipher`] + /// - `counter`: Increment-by-one counter concatenated with nonce as IV for each block. + /// - `plaintext`: data to be encrypted + /// ## Usage + /// ``` + /// #![allow(incomplete_features)] + /// #![feature(generic_const_exprs)] + /// use rand::{thread_rng, Rng}; + /// use ronkathon::encryption::symmetric::{ + /// aes::{Key, AES}, + /// counter::Counter, + /// modes::ctr::CTR, + /// BlockCipher, + /// }; + /// + /// 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 ctr = CTR::>::new(nonce); + /// let plaintext = b"Hello World!"; + /// + /// let ciphertext = ctr.encrypt(&key, &counter, plaintext).unwrap(); + /// ``` + pub fn encrypt( + &self, + key: &C::Key, + counter: &Counter<{ C::BLOCK_SIZE / 2 }>, + plaintext: &[u8], + ) -> Result, String> { + let mut ciphertext = Vec::new(); + let mut cipher_counter = *counter; + + 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); + cipher_counter.increment()?; + + let encrypted = C::encrypt_block(key, &C::Block::from(block.to_vec())); + + for (x, y) in chunk.iter().zip(encrypted.as_ref()) { + ciphertext.push(x ^ y); + } + } + + Ok(ciphertext) + } + + /// Decrypt a ciphertext with counter of size [`BlockCipher::BLOCK_SIZE`]/2 bytes + /// ## Usage + /// ``` + /// #![allow(incomplete_features)] + /// #![feature(generic_const_exprs)] + /// use rand::{thread_rng, Rng}; + /// use ronkathon::encryption::symmetric::{ + /// aes::{Key, AES}, + /// counter::Counter, + /// modes::ctr::CTR, + /// BlockCipher, + /// }; + /// + /// 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 ctr = CTR::>::new(nonce); + /// let plaintext = b"Hello World!"; + /// + /// let ciphertext = ctr.encrypt(&key, &counter, plaintext).unwrap(); + /// let decrypted = ctr.decrypt(&key, &counter, &ciphertext).unwrap(); + /// ``` + pub fn decrypt( + &self, + key: &C::Key, + counter: &Counter<{ C::BLOCK_SIZE / 2 }>, + ciphertext: &[u8], + ) -> Result, String> { + self.encrypt(key, counter, ciphertext) + } +} + +#[cfg(test)] +mod tests { + use rand::{thread_rng, Rng}; + use rstest::{fixture, rstest}; + + use super::*; + use crate::encryption::symmetric::aes::{Key, AES}; + + #[fixture] + fn rand_key() -> Key<128> { + let mut rng = thread_rng(); + let rand_key: [u8; 16] = rng.gen(); + Key::new(rand_key) + } + + fn rand_message(length: usize) -> Vec { + let mut rng = thread_rng(); + + (0..length).map(|_| rng.gen::()).collect() + } + + #[rstest] + fn ctr(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 ctr = CTR::>::new(nonce); + + let plaintext = rand_message(rng.gen_range(1000..10000)); + let ciphertext = ctr.encrypt(&rand_key, &counter, &plaintext).unwrap(); + + let decrypted = ctr.decrypt(&rand_key, &counter, &ciphertext).unwrap(); + + assert_eq!(plaintext, decrypted); + } + } +} diff --git a/src/encryption/symmetric/modes/mod.rs b/src/encryption/symmetric/modes/mod.rs new file mode 100644 index 0000000..b991fe1 --- /dev/null +++ b/src/encryption/symmetric/modes/mod.rs @@ -0,0 +1,8 @@ +//! Contains implementation of modes of operation for different ciphers. Modes of operation allows +//! ciphers to work on arbitrary length of data. +//! - [`cbc::CBC`]: Cipher Block Chaining +//! - [`ctr::CTR`]: Counter mode +//! - [`gcm::GCM`]: Galois Counter mode +#![doc = include_str!("./README.md")] +pub mod cbc; +pub mod ctr; diff --git a/src/field/extension/gf_101_2.rs b/src/field/extension/gf_101_2.rs index 981c7da..329d71c 100644 --- a/src/field/extension/gf_101_2.rs +++ b/src/field/extension/gf_101_2.rs @@ -1,7 +1,7 @@ //! This module contains an implementation of the quadratic extension field GF(101^2). //! Elements represented as coefficients of a [`Polynomial`] in the [`Monomial`] basis of degree 1 -//! in form: `a_0 + a_1*t`` where {a_0, a_1} \in \mathhbb{F}. Uses irreducible poly of the form: -//! (X^2-K). +//! in form: `a_0 + a_1*t` where ${a_0, a_1} \in \mathhbb{F}$. Uses irreducible poly of the form: +//! $(X^2-K)$. //! //! The curve used in [`curve::pluto_curve::PlutoBaseCurve`] supports degree two extension field //! [`curve::pluto_curve::PlutoExtendedCurve`] from GF(101) to have points in GF(101^2). This can be