diff --git a/Cargo.lock b/Cargo.lock index 7179290..8913e63 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -212,6 +212,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" @@ -671,6 +682,7 @@ version = "0.1.0" dependencies = [ "ark-crypto-primitives", "ark-ff", + "chacha20", "des", "hex", "itertools", diff --git a/Cargo.toml b/Cargo.toml index e8cd536..27bb71e 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 8dfc20c..c9007af 100644 --- a/README.md +++ b/README.md @@ -21,14 +21,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/mod.rs b/src/encryption/mod.rs index 11d899b..264cf68 100644 --- a/src/encryption/mod.rs +++ b/src/encryption/mod.rs @@ -1,3 +1,3 @@ -//! Contains cryptographic primitives like symmetric and assymetric encryption. +//! Contains cryptographic primitives like symmetric and asymmetric encryption. pub mod asymmetric; pub mod symmetric; diff --git a/src/encryption/symmetric/README.md b/src/encryption/symmetric/README.md new file mode 100644 index 0000000..33c7e18 --- /dev/null +++ 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*} +\operatorname{Enc}(s,m)=c=G(s)\oplus m \\ +\operatorname{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 new file mode 100644 index 0000000..53abac2 --- /dev/null +++ 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(\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: + +| | | | | +| ------- | ------- | ------ | ------ | +| "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 new file mode 100644 index 0000000..661ed21 --- /dev/null +++ b/src/encryption/symmetric/chacha/mod.rs @@ -0,0 +1,316 @@ +//! 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; + +#[cfg(test)] mod tests; + +/// length of encryption state +pub const STATE_WORDS: usize = 16; + +/// 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, +} + +/// 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>; + +/// [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>; + +/// 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]; + +/// ChaCha cipher 256 byte key +pub type Key = [u32; 8]; + +/// 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], +} + +/// ChaCha cipher cipher nonce, usually 2 for original variant and 3 for IETF variant +pub type Nonce = [u32; N]; + +impl Counter { + /// returns a new Counter + /// ## Arguments + /// - `value`: big-endian integer of 32-bit limb + pub fn new(value: [u32; C]) -> Self { Self { value } } + + /// 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; + } + } + + 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(()) + }, + } + } +} + +/// 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() + .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 +} + +/// 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); + quarter_round(2, 6, 10, 14, state); + 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); + quarter_round(2, 7, 8, 13, state); + 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]; + 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); +} + +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, 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. + /// + /// ## 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; + + // 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 new file mode 100644 index 0000000..3d91e19 --- /dev/null +++ b/src/encryption/symmetric/chacha/tests.rs @@ -0,0 +1,140 @@ +//! Test vectors from: https://datatracker.ietf.org/doc/html/rfc8439 + +use chacha20::{ + cipher::{KeyIvInit, StreamCipher, StreamCipherSeek}, + ChaCha20, +}; +use hex::FromHex; +use rand::{thread_rng, Rng}; +use rstest::rstest; + +use super::{block, quarter_round, ChaCha, Counter}; +use crate::encryption::symmetric::chacha::IETFChaCha20; + +#[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, 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, + 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_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 = [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, 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, + 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(&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 8bc405e..3b9d763 100644 --- a/src/encryption/symmetric/mod.rs +++ b/src/encryption/symmetric/mod.rs @@ -1,4 +1,5 @@ //! Contains implementation of symmetric encryption primitives. +pub mod chacha; pub mod des; /// Trait for symmetric encryption primitive @@ -14,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>; +}