Skip to content

Commit

Permalink
feat: cipher modes of operation (#127)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
lonerapier and eightfilms authored Jul 8, 2024
1 parent c218d47 commit 6e6a230
Show file tree
Hide file tree
Showing 12 changed files with 616 additions and 29 deletions.
9 changes: 6 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -18,12 +18,15 @@ 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/" }
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"
72 changes: 72 additions & 0 deletions examples/aes_chained_cbc.rs
Original file line number Diff line number Diff line change
@@ -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<u8>) -> &'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::<AES<128>>::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::<AES<128>>::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);
}
58 changes: 48 additions & 10 deletions src/encryption/symmetric/aes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Vec<u8>> for Block {
fn from(value: Vec<u8>) -> Self {
assert!(value.len() == 16);
let val: [u8; 16] = value
.try_into()
.unwrap_or_else(|v: Vec<u8>| 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];
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -223,7 +244,7 @@ where [(); N / 8]:
"Round keys not fully consumed - perhaps check key expansion?"
);

state.0.into_iter().flatten().collect::<Vec<_>>().try_into().unwrap()
Block(state.0.into_iter().flatten().collect::<Vec<_>>().try_into().unwrap())
}

/// Deciphers a given `ciphertext`, with key size of `N` (in bits), as seen in Figure 5 of the
Expand Down Expand Up @@ -268,7 +289,7 @@ where [(); N / 8]:
"Round keys not fully consumed - perhaps check key expansion?"
);

state.0.into_iter().flatten().collect::<Vec<_>>().try_into().unwrap()
state.0.into_iter().flatten().collect::<Vec<_>>().into()
}

/// XOR a round key to its internal state.
Expand Down Expand Up @@ -477,3 +498,20 @@ where [(); N / 8]:
);
}
}

impl<const N: usize> BlockCipher for AES<N>
where [(); N / 8]:
{
type Block = Block;
type Key = Key<N>;

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)
}
}
24 changes: 12 additions & 12 deletions src/encryption/symmetric/aes/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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);
}
4 changes: 2 additions & 2 deletions src/encryption/symmetric/chacha/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -123,10 +123,10 @@ fn chacha_fuzz() {
nonce.iter().flat_map(|val| val.to_le_bytes()).collect::<Vec<u8>>().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());

Expand Down
59 changes: 59 additions & 0 deletions src/encryption/symmetric/counter.rs
Original file line number Diff line number Diff line change
@@ -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<const C: usize>(pub [u8; C]);

impl<const C: usize> Counter<C> {
/// 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<const C: usize> From<usize> for Counter<C> {
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)
}
}
Loading

0 comments on commit 6e6a230

Please sign in to comment.