Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cipher modes of operation #127

Merged
merged 20 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading