From b65ecf58776269b85a6bba4bf0bf42ee324aca32 Mon Sep 17 00:00:00 2001 From: lonerapier Date: Wed, 26 Jun 2024 22:43:44 +0530 Subject: [PATCH 1/5] add chacha --- src/encryption/mod.rs | 2 + src/encryption/symmetric/chacha/README.md | 0 src/encryption/symmetric/chacha/mod.rs | 123 ++++++++++++++++++++++ src/encryption/symmetric/chacha/tests.rs | 66 ++++++++++++ src/encryption/symmetric/mod.rs | 1 + src/lib.rs | 1 + 6 files changed, 193 insertions(+) create mode 100644 src/encryption/mod.rs create mode 100644 src/encryption/symmetric/chacha/README.md create mode 100644 src/encryption/symmetric/chacha/mod.rs create mode 100644 src/encryption/symmetric/chacha/tests.rs create mode 100644 src/encryption/symmetric/mod.rs diff --git a/src/encryption/mod.rs b/src/encryption/mod.rs new file mode 100644 index 0000000..b5771d7 --- /dev/null +++ b/src/encryption/mod.rs @@ -0,0 +1,2 @@ +//! Contains implementation of symmetric encrpytion primitives. +pub mod symmetric; diff --git a/src/encryption/symmetric/chacha/README.md b/src/encryption/symmetric/chacha/README.md new file mode 100644 index 0000000..e69de29 diff --git a/src/encryption/symmetric/chacha/mod.rs b/src/encryption/symmetric/chacha/mod.rs new file mode 100644 index 0000000..3c4ced3 --- /dev/null +++ b/src/encryption/symmetric/chacha/mod.rs @@ -0,0 +1,123 @@ +//! Contains implemenation of ChaCha stream cipher algorithm. + +use itertools::Itertools; +#[cfg(test)] mod tests; + +pub const STATE_WORDS: usize = 16; + +pub type ChaCha20 = ChaCha<20>; +pub type ChaCha12 = ChaCha<12>; +pub type ChaCha8 = ChaCha<8>; + +pub struct ChaCha { + rounds: usize, +} + +/// Nothing-up-my-sleeve constant: `["expa", "nd 3", "2-by", "te-k"]` +pub const CONSTS: [u32; 4] = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]; + +impl ChaCha { + pub fn new(rounds: usize) -> Self { Self { rounds } } + + pub fn encrypt( + &self, + key: [u32; 8], + counter: [u32; 1], + nonce: [u32; 3], + plaintext: &[u8], + ) -> Vec { + debug_assert_eq!(counter.len() + nonce.len(), 4); + + let mut encrypted: Vec = Vec::new(); + + let chunks = plaintext.chunks_exact(64); + let remainder = chunks.remainder(); + + for (i, chunk) in chunks.enumerate() { + let keystream = block(key, [counter[0] + i as u32], nonce, self.rounds); + let res: Vec = keystream.iter().zip(chunk).map(|(a, b)| a ^ b).collect(); + encrypted.extend(res); + } + + if !remainder.is_empty() { + let size = plaintext.len() / 64; + let keystream = block(key, [counter[0] + size as u32], nonce, self.rounds); + let res: Vec = remainder.iter().zip(keystream).map(|(a, b)| a ^ b).collect(); + encrypted.extend(res); + } + + encrypted + } + + pub fn decrypt( + &self, + key: [u32; 8], + counter: [u32; 1], + nonce: [u32; 3], + ciphertext: &[u8], + ) -> Vec { + Self::encrypt(&self, key, counter, nonce, ciphertext) + } +} + +fn block(key: [u32; 8], counter: [u32; 1], nonce: [u32; 3], rounds: usize) -> [u8; 64] { + let mut state: Vec = CONSTS.to_vec(); + state.extend_from_slice(&key); + state.extend_from_slice(&counter); + state.extend_from_slice(&nonce); + + let mut initial_state: [u32; 16] = state + .clone() + .try_into() + .unwrap_or_else(|v: Vec| panic!("expected vec of len: {} but got: {}", 16, v.len())); + + for _ in 0..rounds / 2 { + column_rounds(&mut initial_state); + diagonal_rounds(&mut initial_state); + } + + let state: [u32; 16] = state + .into_iter() + .zip(initial_state) + .map(|(a, b)| a.wrapping_add(b)) + .collect::>() + .try_into() + .unwrap_or_else(|v: Vec| panic!("expected vec of len: {} but got: {}", 16, v.len())); + + let mut output = [0u8; 64]; + state.iter().flat_map(|v| v.to_le_bytes()).enumerate().for_each(|(i, byte)| output[i] = byte); + + output +} + +const fn column_rounds(state: &mut [u32; STATE_WORDS]) { + quarter_round(0, 4, 8, 12, state); + quarter_round(1, 5, 9, 13, state); + quarter_round(2, 6, 10, 14, state); + quarter_round(3, 7, 11, 15, state); +} + +const fn diagonal_rounds(state: &mut [u32; STATE_WORDS]) { + quarter_round(0, 5, 10, 15, state); + quarter_round(1, 6, 11, 12, state); + quarter_round(2, 7, 8, 13, state); + quarter_round(3, 4, 9, 14, state); +} + +const fn quarter_round(a: usize, b: usize, c: usize, d: usize, state: &mut [u32; STATE_WORDS]) { + state[a] = state[a].wrapping_add(state[b]); + state[d] ^= state[a]; + state[d] = state[d].rotate_left(16); + + state[c] = state[c].wrapping_add(state[d]); + state[b] ^= state[c]; + state[b] = state[b].rotate_left(12); + + state[a] = state[a].wrapping_add(state[b]); + state[d] ^= state[a]; + state[d] = state[d].rotate_left(8); + + state[c] = state[c].wrapping_add(state[d]); + state[b] ^= state[c]; + state[b] = state[b].rotate_left(7); +} diff --git a/src/encryption/symmetric/chacha/tests.rs b/src/encryption/symmetric/chacha/tests.rs new file mode 100644 index 0000000..bb95111 --- /dev/null +++ b/src/encryption/symmetric/chacha/tests.rs @@ -0,0 +1,66 @@ +//! Test vectors from: https://datatracker.ietf.org/doc/html/rfc8439 + +use itertools::Itertools; + +use super::{block, quarter_round, ChaCha}; + +#[test] +fn test_quarter_round() { + let mut state = [ + 0x879531e0, 0xc5ecf37d, 0x516461b1, 0xc9a62f8a, 0x44c20ef3, 0x3390af7f, 0xd9fc690b, 0x2a5f714c, + 0x53372767, 0xb00a5631, 0x974c541a, 0x359e9963, 0x5c971061, 0x3d631689, 0x2098d9d6, 0x91dbd320, + ]; + + quarter_round(2, 7, 8, 13, &mut state); + + assert_eq!(state, [ + 0x879531e0, 0xc5ecf37d, 0xbdb886dc, 0xc9a62f8a, 0x44c20ef3, 0x3390af7f, 0xd9fc690b, 0xcfacafd2, + 0xe46bea80, 0xb00a5631, 0x974c541a, 0x359e9963, 0x5c971061, 0xccc07c79, 0x2098d9d6, 0x91dbd320, + ]); +} + +#[test] +fn chacha_block() { + let key = [ + 0x03020100, 0x07060504, 0x0b0a0908, 0x0f0e0d0c, 0x13121110, 0x17161514, 0x1b1a1918, 0x1f1e1d1c, + ]; + let nonce = [0x09000000, 0x4a000000, 0x00000000]; + let counter = [0x00000001]; + let state = block(key, counter, nonce, 20); + + assert_eq!(state, [ + 0x10, 0xf1, 0xe7, 0xe4, 0xd1, 0x3b, 0x59, 0x15, 0x50, 0x0f, 0xdd, 0x1f, 0xa3, 0x20, 0x71, 0xc4, + 0xc7, 0xd1, 0xf4, 0xc7, 0x33, 0xc0, 0x68, 0x03, 0x04, 0x22, 0xaa, 0x9a, 0xc3, 0xd4, 0x6c, 0x4e, + 0xd2, 0x82, 0x64, 0x46, 0x07, 0x9f, 0xaa, 0x09, 0x14, 0xc2, 0xd7, 0x05, 0xd9, 0x8b, 0x02, 0xa2, + 0xb5, 0x12, 0x9c, 0xd1, 0xde, 0x16, 0x4e, 0xb9, 0xcb, 0xd0, 0x83, 0xe8, 0xa2, 0x50, 0x3c, 0x4e, + ]); +} + +#[test] +fn chacha_encrypt() { + let key = [ + 0x03020100, 0x07060504, 0x0b0a0908, 0x0f0e0d0c, 0x13121110, 0x17161514, 0x1b1a1918, 0x1f1e1d1c, + ]; + let nonce = [0x00000000, 0x4a000000, 0x00000000]; + let counter = [0x00000001]; + + let plaintext = b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."; + + let chacha = ChaCha::<20>::new(20); + let ciphertext = chacha.encrypt(key, counter, nonce, plaintext); + + assert_eq!(ciphertext, [ + 0x6e, 0x2e, 0x35, 0x9a, 0x25, 0x68, 0xf9, 0x80, 0x41, 0xba, 0x07, 0x28, 0xdd, 0x0d, 0x69, 0x81, + 0xe9, 0x7e, 0x7a, 0xec, 0x1d, 0x43, 0x60, 0xc2, 0x0a, 0x27, 0xaf, 0xcc, 0xfd, 0x9f, 0xae, 0x0b, + 0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57, + 0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8, + 0x07, 0xca, 0x0d, 0xbf, 0x50, 0x0d, 0x6a, 0x61, 0x56, 0xa3, 0x8e, 0x08, 0x8a, 0x22, 0xb6, 0x5e, + 0x52, 0xbc, 0x51, 0x4d, 0x16, 0xcc, 0xf8, 0x06, 0x81, 0x8c, 0xe9, 0x1a, 0xb7, 0x79, 0x37, 0x36, + 0x5a, 0xf9, 0x0b, 0xbf, 0x74, 0xa3, 0x5b, 0xe6, 0xb4, 0x0b, 0x8e, 0xed, 0xf2, 0x78, 0x5e, 0x42, + 0x87, 0x4d, + ]); + + let decrypt = chacha.decrypt(key, counter, nonce, &ciphertext); + + assert_eq!(decrypt, plaintext.to_vec()); +} diff --git a/src/encryption/symmetric/mod.rs b/src/encryption/symmetric/mod.rs new file mode 100644 index 0000000..3e5cc26 --- /dev/null +++ b/src/encryption/symmetric/mod.rs @@ -0,0 +1 @@ +pub mod chacha; diff --git a/src/lib.rs b/src/lib.rs index 73d306d..ac7110e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,6 +24,7 @@ pub mod codes; pub mod curve; pub mod ecdsa; +pub mod encryption; pub mod field; pub mod hashes; pub mod kzg; From f2da47330920a27c29466b09a06696540ec4debe Mon Sep 17 00:00:00 2001 From: lonerapier Date: Fri, 28 Jun 2024 16:13:18 +0530 Subject: [PATCH 2/5] add stream cipher trait --- Cargo.lock | 12 + Cargo.toml | 1 + README.md | 12 +- src/encryption/symmetric/README.md | 0 src/encryption/symmetric/chacha/mod.rs | 283 +++++++++++++++++++---- src/encryption/symmetric/chacha/tests.rs | 95 +++++++- src/encryption/symmetric/mod.rs | 38 +++ 7 files changed, 380 insertions(+), 61 deletions(-) create mode 100644 src/encryption/symmetric/README.md diff --git a/Cargo.lock b/Cargo.lock index d67a187..f2201bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,6 +207,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "cipher" version = "0.4.4" @@ -638,6 +649,7 @@ version = "0.1.0" dependencies = [ "ark-crypto-primitives", "ark-ff", + "chacha20", "des", "hex", "itertools 0.13.0", diff --git a/Cargo.toml b/Cargo.toml index c288342..17edfb8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ 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" [patch.crates-io] ark-ff ={ git="https://github.com/arkworks-rs/algebra/" } diff --git a/README.md b/README.md index f4a8883..1ccf81d 100644 --- a/README.md +++ b/README.md @@ -20,14 +20,22 @@ Ronkathon is a rust implementation of a collection of cryptographic primitives. ## Primitives - [Fields and Their Extensions](src/field/README.md) + - [Binary Fields](src/field/binary_towers/README.md) - [Curves and Their Pairings](src/curve/README.md) - [Polynomials](src/polynomial/mod.rs) - [KZG Commitments](src/kzg/README.md) - [Reed-Solomon Codes](src/codes/README.md) +- [Merkle Proofs](src/tree/README.md) + +### Signatures - [Tiny ECDSA](src/ecdsa.rs) -- [RSA](src/rsa/README.md) + +### Encryption +- [RSA](src/encryption/asymmetric/rsa/README.md) +- [DES](src/encryption/symmetric/des/README.md) + +### Hash - [Sha256 Hash](src/hashes/README.md) -- [Merkle Proofs](src/tree/README.md) - [Poseidon Hash](src/hashes/poseidon/README.md) ## In Progress diff --git a/src/encryption/symmetric/README.md b/src/encryption/symmetric/README.md new file mode 100644 index 0000000..e69de29 diff --git a/src/encryption/symmetric/chacha/mod.rs b/src/encryption/symmetric/chacha/mod.rs index 3c4ced3..761988b 100644 --- a/src/encryption/symmetric/chacha/mod.rs +++ b/src/encryption/symmetric/chacha/mod.rs @@ -1,70 +1,109 @@ -//! Contains implemenation of ChaCha stream cipher algorithm. +//! Contains implementation of ChaCha stream cipher algorithm. + +use super::StreamCipher; -use itertools::Itertools; #[cfg(test)] mod tests; +/// length of encryption state pub const STATE_WORDS: usize = 16; -pub type ChaCha20 = ChaCha<20>; -pub type ChaCha12 = ChaCha<12>; -pub type ChaCha8 = ChaCha<8>; - -pub struct ChaCha { - rounds: usize, +/// ChaCha encryption algorithm with following constants: +/// - `R`: number of rounds, usually 20, 12, or 8 +/// - `N`: nonce's length, IETF version suggests 2 and original ChaCha suggests 1 +/// - `C`: big-endian 32-bit counter length, IETF version: 32-byte counter, and original ChaCha: +/// 64-byte counter +pub struct ChaCha { + key: Key, + nonce: Nonce, } -/// Nothing-up-my-sleeve constant: `["expa", "nd 3", "2-by", "te-k"]` -pub const CONSTS: [u32; 4] = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]; +/// IETF [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) ChaCha variant with 20 rounds +pub type IETFChaCha20 = ChaCha<20, 3, 1>; +/// IETF [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) ChaCha variant with 12 rounds +pub type IETFChaCha12 = ChaCha<12, 3, 1>; +/// IETF [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) ChaCha variant with 8 rounds +pub type IETFChaCha8 = ChaCha<8, 3, 1>; -impl ChaCha { - pub fn new(rounds: usize) -> Self { Self { rounds } } +/// [Original](https://cr.yp.to/chacha/chacha-20080128.pdf) ChaCha variant with 20 rounds +pub type ChaCha20 = ChaCha<20, 2, 2>; +/// [Original](https://cr.yp.to/chacha/chacha-20080128.pdf) ChaCha variant with 12 rounds +pub type ChaCha12 = ChaCha<12, 2, 2>; +/// [Original](https://cr.yp.to/chacha/chacha-20080128.pdf) ChaCha variant with 8 rounds +pub type ChaCha8 = ChaCha<8, 2, 2>; - pub fn encrypt( - &self, - key: [u32; 8], - counter: [u32; 1], - nonce: [u32; 3], - plaintext: &[u8], - ) -> Vec { - debug_assert_eq!(counter.len() + nonce.len(), 4); +/// Nothing-up-my-sleeve constant used as first four words in encrpytion state: +/// `["expa", "nd 3", "2-by", "te-k"]` +pub const STATE_CONSTS: [u32; 4] = [0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]; - let mut encrypted: Vec = Vec::new(); +/// ChaCha cipher 256 byte key +pub type Key = [u32; 8]; - let chunks = plaintext.chunks_exact(64); - let remainder = chunks.remainder(); +/// ChaCha cipher counter consisting of big-endian integer using 32 bits limbs, usually 2 for +/// orignal variant and 1 for IETF variant +#[derive(Debug, Clone, Copy)] +pub struct Counter { + value: [u32; C], +} - for (i, chunk) in chunks.enumerate() { - let keystream = block(key, [counter[0] + i as u32], nonce, self.rounds); - let res: Vec = keystream.iter().zip(chunk).map(|(a, b)| a ^ b).collect(); - encrypted.extend(res); - } +/// ChaCha cipher cipher nonce, usually 2 for original variant and 3 for IETF variant +pub type Nonce = [u32; N]; - if !remainder.is_empty() { - let size = plaintext.len() / 64; - let keystream = block(key, [counter[0] + size as u32], nonce, self.rounds); - let res: Vec = remainder.iter().zip(keystream).map(|(a, b)| a ^ b).collect(); - encrypted.extend(res); - } +impl Counter { + /// returns a new Counter + /// ## Arguments + /// - `value`: big-endian integer of 32-bit limb + pub fn new(value: [u32; C]) -> Self { Self { value } } - encrypted - } + /// increases counter value by 1 for each new round of 64-byte input. + /// + /// ## Note + /// Returns `max counter reached` error when counter value reaches maximum allowed by different + /// counter length. Example: for IETF version, counter length is one, so it throws an error when + /// counter reaches [`u32::MAX`]. + fn increment(&mut self) -> Result<(), &str> { + match C { + 0 => Err("counter value is 0"), + _ => { + // check for max value + let mut flag = true; + for value in self.value.iter() { + if *value != u32::MAX { + flag = false; + } + } - pub fn decrypt( - &self, - key: [u32; 8], - counter: [u32; 1], - nonce: [u32; 3], - ciphertext: &[u8], - ) -> Vec { - Self::encrypt(&self, key, counter, nonce, ciphertext) + if flag { + return Err("max counter reached"); + } + + let mut add_carry = true; + for i in (0..C).rev() { + let (incremented_val, carry) = self.value[i].overflowing_add(add_carry as u32); + self.value[i] = incremented_val; + add_carry = carry; + } + + Ok(()) + }, + } } } -fn block(key: [u32; 8], counter: [u32; 1], nonce: [u32; 3], rounds: usize) -> [u8; 64] { - let mut state: Vec = CONSTS.to_vec(); - state.extend_from_slice(&key); - state.extend_from_slice(&counter); - state.extend_from_slice(&nonce); +/// computes ChaCha stream cipher block function. It performs following steps: +/// - creates cipher state by concatenating ([`STATE_CONSTS`]|[`Key`]|[`Counter`]|[`Nonce`]) and +/// visualising as 4x4 matrix +/// - scramble the state by performing rounds/2, column rounds and diagonal rounds. +/// - perform (initial state + scrambled state) to add non-linearity +fn block( + key: &Key, + counter: &Counter, + nonce: &Nonce, + rounds: usize, +) -> [u8; 64] { + let mut state: Vec = STATE_CONSTS.to_vec(); + state.extend_from_slice(key); + state.extend_from_slice(&counter.value); + state.extend_from_slice(nonce); let mut initial_state: [u32; 16] = state .clone() @@ -90,6 +129,7 @@ fn block(key: [u32; 8], counter: [u32; 1], nonce: [u32; 3], rounds: usize) -> [u output } +/// quarter round on all 4 columns const fn column_rounds(state: &mut [u32; STATE_WORDS]) { quarter_round(0, 4, 8, 12, state); quarter_round(1, 5, 9, 13, state); @@ -97,6 +137,7 @@ const fn column_rounds(state: &mut [u32; STATE_WORDS]) { quarter_round(3, 7, 11, 15, state); } +/// quarter round on 4 diagonals const fn diagonal_rounds(state: &mut [u32; STATE_WORDS]) { quarter_round(0, 5, 10, 15, state); quarter_round(1, 6, 11, 12, state); @@ -104,6 +145,8 @@ const fn diagonal_rounds(state: &mut [u32; STATE_WORDS]) { quarter_round(3, 4, 9, 14, state); } +/// ChaCha cipher quarter round that scrambles the state using `Add-Rotate-XOR` operations on four +/// of the state's inputs. const fn quarter_round(a: usize, b: usize, c: usize, d: usize, state: &mut [u32; STATE_WORDS]) { state[a] = state[a].wrapping_add(state[b]); state[d] ^= state[a]; @@ -121,3 +164,145 @@ const fn quarter_round(a: usize, b: usize, c: usize, d: usize, state: &mut [u32; state[b] ^= state[c]; state[b] = state[b].rotate_left(7); } + +impl ChaCha { + /// returns a new ChaCha encryption function + /// ## Arguments + /// - [`Key`]: 256-bit key in big-endian 32-bit limbs + /// - [`Nonce`]: initialisation vector with varying length, 64-bit in original variant and 96-bit + /// for [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) variant. + /// + /// *Note*: same nonce value shouldn't be used with a key as stream ciphers are malleable. + pub fn new(key: &Key, nonce: &Nonce) -> Self { + Self { key: key.clone(), nonce: nonce.clone() } + } + + /// Encrypts a plaintext of maximum length $2^{32*C}$ by performing $ENC_k(m) = m ⨁ B(k)$, where + /// B(k) is pseudoranom keystream calculated using ChaCha block function. + /// + /// ## Usage + /// ``` + /// use rand::{thread_rng, Rng}; + /// use ronkathon::encryption::symmetric::chacha::{ChaCha, Counter, Key, Nonce}; + /// let mut rng = thread_rng(); + /// let key: Key = rng.gen(); + /// let nonce: Nonce<3> = rng.gen(); + /// + /// let chacha = ChaCha::<20, 3, 1>::new(&key, &nonce); + /// + /// let counter: Counter<1> = Counter::new([0]); + /// // plaintext can be of length `2^{32*i}` 64-byte + /// let plaintext = b"Hello World!"; + /// + /// let ciphertext = chacha.encrypt(&counter, plaintext); + /// ``` + /// + /// ## Note: + /// - [`Counter`] can be initialised to any number and is incremented by `1` until it reaches max + /// value, i.e. for $C=2$, encryption can happen for $2^64$ 64-byte input. + /// - counter and nonce length should be equal to 128 bytes + pub fn encrypt(&self, counter: &Counter, plaintext: &[u8]) -> Result, String> { + // counter and nonce length should be equal to 128 bytes + if C + N != 4 { + return Err("invalid counter and nonce lengths".to_string()); + } + + let mut ciphertext: Vec = Vec::new(); + + let mut counter_iter = counter.clone(); + + // parse inputs in chunks of 64 bytes + let chunks = plaintext.chunks_exact(64); + let remainder = chunks.remainder(); + + for chunk in chunks { + // compute pseudorandom keystream from key, counter and nonce + let keystream = block(&self.key, &counter_iter, &self.nonce, R); + // increment the counter + counter_iter.increment()?; + + // perform: Enc_k(m) = m ^ B(k) + let res = keystream.iter().zip(chunk).map(|(a, b)| a ^ b).collect::>(); + + // serialize encrypted bytes to ciphertext + ciphertext.extend(res); + } + + // encrypt remainder plaintext bytes separately + if !remainder.is_empty() { + // compute pseudorandom keystream from key, counter and nonce + let keystream = block(&self.key, &counter_iter, &self.nonce, R); + + // perform: Enc_k(m) = m ^ B(k) + let res = remainder.iter().zip(keystream).map(|(a, b)| a ^ b).collect::>(); + + // serialize encrypted bytes to ciphertext + ciphertext.extend(res); + } + + Ok(ciphertext) + } + + /// Decrypts a ciphertext of arbitrary length using [`Self::encrypt`]. + /// + /// ## Usage + /// ``` + /// use rand::{thread_rng, Rng}; + /// use ronkathon::encryption::symmetric::chacha::{ChaCha, Counter, Key, Nonce}; + /// let mut rng = thread_rng(); + /// let key: Key = rng.gen(); + /// let nonce: Nonce<3> = rng.gen(); + /// + /// let chacha = ChaCha::<20, 3, 1>::new(&key, &nonce); + /// + /// let counter: Counter<1> = Counter::new([0]); + /// // plaintext can be of length `2^{32*i}` 64-byte + /// let plaintext = b"Hello World!"; + /// + /// let ciphertext = chacha.encrypt(&counter, plaintext).unwrap(); + /// let decrypted = chacha.decrypt(&counter, &ciphertext).unwrap(); + /// + /// assert_eq!(decrypted, plaintext); + /// ``` + pub fn decrypt(&self, counter: &Counter, ciphertext: &[u8]) -> Result, String> { + self.encrypt(counter, ciphertext) + } +} + +impl StreamCipher for ChaCha { + type Counter = Counter; + type Error = String; + type Key = Key; + type Nonce = Nonce; + + fn new(key: &Self::Key, nonce: &Self::Nonce) -> Result + where Self: Sized { + Ok(ChaCha::new(key, nonce)) + } + + fn encrypt(&self, plaintext: &[u8]) -> Result, Self::Error> { + let counter = Counter::new([0u32; C]); + self.encrypt(&counter, plaintext) + } + + fn decrypt(&self, ciphertext: &[u8]) -> Result, Self::Error> { + let counter = Counter::new([0u32; C]); + self.decrypt(&counter, ciphertext) + } + + fn encrypt_with_counter( + &self, + counter: &Self::Counter, + plaintext: &[u8], + ) -> Result, Self::Error> { + self.encrypt(counter, plaintext) + } + + fn decrypt_with_counter( + &self, + counter: &Self::Counter, + ciphertext: &[u8], + ) -> Result, Self::Error> { + self.decrypt(counter, ciphertext) + } +} diff --git a/src/encryption/symmetric/chacha/tests.rs b/src/encryption/symmetric/chacha/tests.rs index bb95111..23a7c19 100644 --- a/src/encryption/symmetric/chacha/tests.rs +++ b/src/encryption/symmetric/chacha/tests.rs @@ -1,8 +1,16 @@ //! Test vectors from: https://datatracker.ietf.org/doc/html/rfc8439 -use itertools::Itertools; +use chacha20::{ + cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}, + ChaCha20, +}; +use des::cipher::KeyInit; +use hex::FromHex; +use rand::{thread_rng, Rng}; +use rstest::rstest; -use super::{block, quarter_round, ChaCha}; +use super::{block, quarter_round, ChaCha, Counter}; +use crate::encryption::symmetric::chacha::IETFChaCha20; #[test] fn test_quarter_round() { @@ -24,9 +32,10 @@ fn chacha_block() { let key = [ 0x03020100, 0x07060504, 0x0b0a0908, 0x0f0e0d0c, 0x13121110, 0x17161514, 0x1b1a1918, 0x1f1e1d1c, ]; - let nonce = [0x09000000, 0x4a000000, 0x00000000]; - let counter = [0x00000001]; - let state = block(key, counter, nonce, 20); + + let nonce = [0x09000000, 0x4a000000, 0]; + let counter = Counter::new([1]); + let state = block(&key, &counter, &nonce, 20); assert_eq!(state, [ 0x10, 0xf1, 0xe7, 0xe4, 0xd1, 0x3b, 0x59, 0x15, 0x50, 0x0f, 0xdd, 0x1f, 0xa3, 0x20, 0x71, 0xc4, @@ -36,18 +45,33 @@ fn chacha_block() { ]); } +#[test] +fn chacha_block_2() { + let key = [0u32; 8]; + let nonce = [0u32; 3]; + let counter = Counter::new([0]); + let state = block(&key, &counter, &nonce, 20); + + assert_eq!(state, [ + 0x76, 0xb8, 0xe0, 0xad, 0xa0, 0xf1, 0x3d, 0x90, 0x40, 0x5d, 0x6a, 0xe5, 0x53, 0x86, 0xbd, 0x28, + 0xbd, 0xd2, 0x19, 0xb8, 0xa0, 0x8d, 0xed, 0x1a, 0xa8, 0x36, 0xef, 0xcc, 0x8b, 0x77, 0x0d, 0xc7, + 0xda, 0x41, 0x59, 0x7c, 0x51, 0x57, 0x48, 0x8d, 0x77, 0x24, 0xe0, 0x3f, 0xb8, 0xd8, 0x4a, 0x37, + 0x6a, 0x43, 0xb8, 0xf4, 0x15, 0x18, 0xa1, 0x1c, 0xc3, 0x87, 0xb6, 0x69, 0xb2, 0xee, 0x65, 0x86 + ]); +} + #[test] fn chacha_encrypt() { let key = [ 0x03020100, 0x07060504, 0x0b0a0908, 0x0f0e0d0c, 0x13121110, 0x17161514, 0x1b1a1918, 0x1f1e1d1c, ]; - let nonce = [0x00000000, 0x4a000000, 0x00000000]; - let counter = [0x00000001]; + let nonce = [0, 0x4a000000, 0]; + let counter = Counter::new([1]); let plaintext = b"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."; - let chacha = ChaCha::<20>::new(20); - let ciphertext = chacha.encrypt(key, counter, nonce, plaintext); + let chacha = ChaCha::<20, 3, 1>::new(&key, &nonce); + let ciphertext = chacha.encrypt(&counter, plaintext).unwrap(); assert_eq!(ciphertext, [ 0x6e, 0x2e, 0x35, 0x9a, 0x25, 0x68, 0xf9, 0x80, 0x41, 0xba, 0x07, 0x28, 0xdd, 0x0d, 0x69, 0x81, @@ -60,7 +84,58 @@ fn chacha_encrypt() { 0x87, 0x4d, ]); - let decrypt = chacha.decrypt(key, counter, nonce, &ciphertext); + let decrypt = chacha.decrypt(&counter, &ciphertext).unwrap(); assert_eq!(decrypt, plaintext.to_vec()); } + +#[rstest] +#[case([0, 10], [0, 11])] +#[case([1, u32::MAX], [2, 0])] +#[should_panic] +#[case([u32::MAX, u32::MAX, u32::MAX], [0, 0, 0])] +fn counter(#[case] a: [u32; C], #[case] b: [u32; C]) { + let mut counter = Counter::new(a); + let val = counter.increment(); + assert!(val.is_ok()); + + assert_eq!(counter.value, b); +} + +#[test] +fn chacha_fuzz() { + let mut rng = thread_rng(); + + let key: [u32; 8] = rng.gen(); + let nonce: [u32; 3] = rng.gen(); + let plaintext = <[u8; 16]>::from_hex("000102030405060708090A0B0C0D0E0F").unwrap(); + + // ronk chacha cipher + let ronk_chacha = IETFChaCha20::new(&key, &nonce); + let counter = Counter::new([0]); + let ronk_ciphertext = ronk_chacha.encrypt(&counter, &plaintext).unwrap(); + let decrypted = ronk_chacha.decrypt(&counter, &ronk_ciphertext).unwrap(); + + // Key and IV must be references to the `GenericArray` type. + // Here we use the `Into` trait to convert arrays into it. + let flat_key: [u8; 32] = + key.iter().flat_map(|val| val.to_le_bytes()).collect::>().try_into().expect("err"); + let flat_nonce: [u8; 12] = + 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(); + cipher.apply_keystream(&mut buffer); + + let ciphertext = buffer.clone(); + + assert_eq!(ronk_ciphertext, ciphertext.to_vec()); + + // ChaCha ciphers support seeking + cipher.seek(0u32); + + // decrypt ciphertext by applying keystream again + cipher.apply_keystream(&mut buffer); + assert_eq!(buffer, plaintext); + assert_eq!(buffer.to_vec(), decrypted); +} diff --git a/src/encryption/symmetric/mod.rs b/src/encryption/symmetric/mod.rs index 226dd3f..3b9d763 100644 --- a/src/encryption/symmetric/mod.rs +++ b/src/encryption/symmetric/mod.rs @@ -15,3 +15,41 @@ pub trait SymmetricEncryption { /// Decrypts ciphertext using key and returns plaintext fn decrypt(key: &Self::Key, ciphertext: &Self::Block) -> Self::Block; } + +/// Trait for stream ciphers +pub trait StreamCipher { + /// secret key used in encryption and decryption + type Key; + /// Initialisation vector (IV) + type Nonce; + /// Error originating during encryption + type Error; + /// Counter used for some encryption primitives like [`chacha::ChaCha`] + type Counter; + + /// Create a new Stream cipher object. + /// ## Arguments + /// - `key`: secret key used to encrypt/decrypt + /// - `nonce`: nonce value + fn new(key: &Self::Key, nonce: &Self::Nonce) -> Result + where Self: Sized; + + /// Encrypt a plaintext of arbitrary length bytes + fn encrypt(&self, plaintext: &[u8]) -> Result, Self::Error>; + /// Decrypt a ciphertext of arbitrary length bytes + fn decrypt(&self, ciphertext: &[u8]) -> Result, Self::Error>; + + /// encrpypt a plaintext with counter that increments with every new block + fn encrypt_with_counter( + &self, + counter: &Self::Counter, + plaintext: &[u8], + ) -> Result, Self::Error>; + + /// decrypt a ciphertext with counter + fn decrypt_with_counter( + &self, + counter: &Self::Counter, + ciphertext: &[u8], + ) -> Result, Self::Error>; +} From 3e5ecd03b5ee96bc9d23fe99411f50417f646f00 Mon Sep 17 00:00:00 2001 From: lonerapier Date: Fri, 28 Jun 2024 16:15:20 +0530 Subject: [PATCH 3/5] clippy fix --- src/encryption/symmetric/chacha/mod.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/encryption/symmetric/chacha/mod.rs b/src/encryption/symmetric/chacha/mod.rs index 761988b..b720c74 100644 --- a/src/encryption/symmetric/chacha/mod.rs +++ b/src/encryption/symmetric/chacha/mod.rs @@ -173,9 +173,7 @@ impl ChaCha { /// for [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) variant. /// /// *Note*: same nonce value shouldn't be used with a key as stream ciphers are malleable. - pub fn new(key: &Key, nonce: &Nonce) -> Self { - Self { key: key.clone(), nonce: nonce.clone() } - } + pub fn new(key: &Key, nonce: &Nonce) -> Self { Self { key: *key, nonce: *nonce } } /// Encrypts a plaintext of maximum length $2^{32*C}$ by performing $ENC_k(m) = m ⨁ B(k)$, where /// B(k) is pseudoranom keystream calculated using ChaCha block function. @@ -209,7 +207,7 @@ impl ChaCha { let mut ciphertext: Vec = Vec::new(); - let mut counter_iter = counter.clone(); + let mut counter_iter = *counter; // parse inputs in chunks of 64 bytes let chunks = plaintext.chunks_exact(64); From 5aeb16c2c0c5fe3eedc0bbd436d03ef0cc1f4173 Mon Sep 17 00:00:00 2001 From: lonerapier Date: Fri, 28 Jun 2024 16:53:09 +0530 Subject: [PATCH 4/5] add docs --- src/encryption/symmetric/README.md | 40 +++++++++++++++++++ src/encryption/symmetric/chacha/README.md | 48 +++++++++++++++++++++++ src/encryption/symmetric/chacha/mod.rs | 12 +++++- src/encryption/symmetric/chacha/tests.rs | 1 - 4 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/encryption/symmetric/README.md b/src/encryption/symmetric/README.md index e69de29..5346807 100644 --- a/src/encryption/symmetric/README.md +++ b/src/encryption/symmetric/README.md @@ -0,0 +1,40 @@ +# Symmetric Encryption algorithms + +[Symmetric encryption](https://en.wikipedia.org/wiki/Symmetric-key_algorithm) algorithms are cryptographic algorithms that uses same key for encryption and decryption. + +## Block Ciphers + +TODO + +## Stream ciphers + +[stream ciphers](https://en.wikipedia.org/wiki/Stream_cipher) are symmetric encryption cryptography primitives that works on digits, often bits, rather than fixed-size blocks as in [block-ciphers](https://en.wikipedia.org/wiki/Block_cipher). + +$$ +\begin{align*} +\text{Enc}(s,m)=c=G(s)\oplus m \\ +\text{Dec}(s,c)=m=c \oplus G(s) +\end{align*} +$$ + +- Plaintext digits can be of any size, as cipher works on bits, i.e. $c,m\in \{ 0,1 \}^{L}$ +- $G$ is a PRG that generatoes **Keystream** which is a pseudorandom digit stream that is combined with plaintext to obtain ciphertext. +- Keystream is generated using a seed value using **shift registers**. +- **Seed** is the key value required for decrypting ciphertext. +- Can be approximated as one-time pad (OTP), where keystream is used only once. +- Keystream has to be updated for every new plaintext bit encrypted. Updation of keystream can depend on plaintext or can happen independent of it. + +```mermaid +flowchart LR +k[Key]-->ksg["Key Stream Generator"] +ksg--s_i-->xor +x["Plaintext (x_i)"]:::hiddenBorder-->xor["⊕"] +xor-->y_i +y_i["Ciphertext (y_i)"]-.->ksg +``` + +Now, encryption in stream ciphers is just XOR operation, i.e. $y_{i}=x_{i} \oplus s_{i}$. Due to this, encryption and decrpytion is the same function which. Then, comes the major question: + +## Implementations + +- [ChaCha stream cipher](./chacha/README.md) \ No newline at end of file diff --git a/src/encryption/symmetric/chacha/README.md b/src/encryption/symmetric/chacha/README.md index e69de29..4ca4def 100644 --- a/src/encryption/symmetric/chacha/README.md +++ b/src/encryption/symmetric/chacha/README.md @@ -0,0 +1,48 @@ +# ChaCha Encryption + +- ChaCha's core is a permutation $P$ that operates on 512-bit strings +- operates on ARX based design: add, rotate, xor. all of these operations are done 32-bit integers +- $P$ is supposed to be secure instantiation of *random permutation* and constructions based on $P$ are analysed in *random-permutation* model. +- using permutation $P$, a pseudorandom function $F$ is constructed that takes a 256 bit key and 128-bit input to 512-bit output with a 128-bit *constant*. + +$$ +F_{k}(x)\xlongequal{def} P(\text{const} \parallel k \parallel x)\boxplus \text{const} \parallel k \parallel x +$$ + +Then, chacha stream cipher's internal state is defined using $F$ with a 256-bit seed $s$ and 64-bit initialisation vector $IV$ and 64-bit nonce that is used only once for a seed. Often defined as a 4x4 matrix with each cell containing 4 bytes: + +| | | | | +| ------- | ------- | ------ | ------ | +| "expa" | "nd 3" | "2-by" | "te k" | +| Key | Key | Key | Key | +| Key | Key | Key | Key | +| Counter | Counter | Nonce | Nonce | + + +Let's define what happens inside $F$, it runs a quarter round that takes as input 4 4-byte input and apply constant time ARX operations: + +``` +a += b; d ^= a; d <<<= 16; +c += d; b ^= c; b <<<= 12; +a += b; d ^= a; d <<<= 8; +c += d; b ^= c; b <<<= 7; +``` + +**quarter round** is run 4 times, for each column and 4 times for each diagonal. ChaCha added diagonal rounds from row rounds in Salsa for better implementation in software. Quarter round by itself, is an invertible transformation, to prevent this, ChaCha adds initial state back to the quarter-round outputs. + +This completes 1 round of block scrambling and as implied in the name, ChaCha20 does this for 20 similar rounds. [ChaCha family][chacha-family] proposes different variants with different rounds, namely ChaCha8, ChaCha12. + +**Nonce** can be increased to 96 bits, by using 3 nonce cells. [XChaCha][xchacha] takes this a step further and allows for 192-bit nonces. + +Reason for constants: +- prevents zero block during cipher scrambling +- attacker can only control 25% of the block, when given access to counter as nonce as well. + +During initial round, **counters** are initialised to 0, and for next rounds, increase the counter as 64-bit little-endian integer and scramble the block again. Thus, ChaCha can encrypt a maximum of $2^{64}$ 64-byte message. This is so huge, that we'll never ever require to transmit this many amount of data. [IETF][ietf] variant of ChaCha only has one cell of nonce, i.e. 32 bits, and thus, can encrypt a message of $2^{32}$ 64-byte length, i.e. 256 GB. + +[uct]: +[ietf]: +[xchacha]: +[salsa]: +[chacha]: +[chacha-family]: \ No newline at end of file diff --git a/src/encryption/symmetric/chacha/mod.rs b/src/encryption/symmetric/chacha/mod.rs index b720c74..661ed21 100644 --- a/src/encryption/symmetric/chacha/mod.rs +++ b/src/encryption/symmetric/chacha/mod.rs @@ -1,4 +1,14 @@ -//! Contains implementation of ChaCha stream cipher algorithm. +//! Contains implementation of [`ChaCha`] [`StreamCipher`] algorithm with [`Counter`]. +//! +//! Provides implementation of variants: +//! - [`ChaCha20`]: [Original](https://cr.yp.to/chacha/chacha-20080128.pdf) ChaCha variant with 20 +//! rounds +//! - [`ChaCha12`]: [Original](https://cr.yp.to/chacha/chacha-20080128.pdf) ChaCha variant with 12 +//! rounds +//! - [`ChaCha8`]: [Original](https://cr.yp.to/chacha/chacha-20080128.pdf) ChaCha variant with 8 +//! rounds +//! - [`IETFChaCha20`],[`IETFChaCha12`],[`IETFChaCha8`]: [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) +//! with 20, 12, 8 rounds use super::StreamCipher; diff --git a/src/encryption/symmetric/chacha/tests.rs b/src/encryption/symmetric/chacha/tests.rs index 23a7c19..3d91e19 100644 --- a/src/encryption/symmetric/chacha/tests.rs +++ b/src/encryption/symmetric/chacha/tests.rs @@ -4,7 +4,6 @@ use chacha20::{ cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}, ChaCha20, }; -use des::cipher::KeyInit; use hex::FromHex; use rand::{thread_rng, Rng}; use rstest::rstest; From c50a19cf39866905a36d015e0bc3d72b851f65cd Mon Sep 17 00:00:00 2001 From: lonerapier Date: Mon, 1 Jul 2024 15:57:32 +0530 Subject: [PATCH 5/5] tex --- src/encryption/symmetric/README.md | 4 ++-- src/encryption/symmetric/chacha/README.md | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/encryption/symmetric/README.md b/src/encryption/symmetric/README.md index 5346807..33c7e18 100644 --- a/src/encryption/symmetric/README.md +++ b/src/encryption/symmetric/README.md @@ -12,8 +12,8 @@ TODO $$ \begin{align*} -\text{Enc}(s,m)=c=G(s)\oplus m \\ -\text{Dec}(s,c)=m=c \oplus G(s) +\operatorname{Enc}(s,m)=c=G(s)\oplus m \\ +\operatorname{Dec}(s,c)=m=c \oplus G(s) \end{align*} $$ diff --git a/src/encryption/symmetric/chacha/README.md b/src/encryption/symmetric/chacha/README.md index 4ca4def..53abac2 100644 --- a/src/encryption/symmetric/chacha/README.md +++ b/src/encryption/symmetric/chacha/README.md @@ -6,7 +6,7 @@ - using permutation $P$, a pseudorandom function $F$ is constructed that takes a 256 bit key and 128-bit input to 512-bit output with a 128-bit *constant*. $$ -F_{k}(x)\xlongequal{def} P(\text{const} \parallel k \parallel x)\boxplus \text{const} \parallel k \parallel x +F_{k}(x)\xlongequal{def} P(\operatorname{const} \parallel k \parallel x)\boxplus \operatorname{const} \parallel k \parallel x $$ Then, chacha stream cipher's internal state is defined using $F$ with a 256-bit seed $s$ and 64-bit initialisation vector $IV$ and 64-bit nonce that is used only once for a seed. Often defined as a 4x4 matrix with each cell containing 4 bytes: