From ebfe646bc922f01c66fa340d3ce19f42300b0574 Mon Sep 17 00:00:00 2001 From: M Alghazwi Date: Fri, 1 May 2026 16:08:57 +0300 Subject: [PATCH] Improve impl and make it generic --- Cargo.toml | 11 +- README.md | 48 ++++--- examples/auth.rs | 34 +++++ examples/integrity.rs | 27 ---- examples/simple_example.rs | 17 ++- src/cipher.rs | 28 ++++ src/kdf.rs | 83 ++++++++++++ src/keyed_hash.rs | 48 +++++++ src/lib.rs | 257 +++---------------------------------- src/lioness.rs | 199 ++++++++++++++++++++++++++++ tests/lioness.rs | 52 ++++++++ 11 files changed, 515 insertions(+), 289 deletions(-) create mode 100644 examples/auth.rs delete mode 100644 examples/integrity.rs create mode 100644 src/cipher.rs create mode 100644 src/kdf.rs create mode 100644 src/keyed_hash.rs create mode 100644 src/lioness.rs create mode 100644 tests/lioness.rs diff --git a/Cargo.toml b/Cargo.toml index 24f4129..027edcb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,14 @@ license = "MIT OR Apache-2.0" [dependencies] anyhow = "1" -blake2 = "0.10" -chacha20 = "0.10" +blake2 = "0.11.0-pre.4" +chacha20 = "0.10.0-pre.3" turboshake = "0.6.0" +aes = "0.9.0-pre.2" +ctr = "0.10.0-pre.2" +sha2 = "0.11.0-pre.4" +sha3 = "0.11.0-pre.4" +hmac = "0.13.0-pre.4" +hkdf = "0.13.0-pre.4" zeroize = { version = "1", features = ["zeroize_derive"] } +rand_core = { version = "0.6.4", features = ["getrandom"] } diff --git a/README.md b/README.md index 3ef8708..f3affcb 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,18 @@ This code has not been formally audited, Use at your own risk or ask a cryptogra ### Overview [Lioness](https://www.cl.cam.ac.uk/~rja14/Papers/bear-lion.pdf) is a large block cipher built from -- Stream cipher, -- Hash function, -- Key derivation function (KDF), although this can be removed if the input key is large enough to cover the four sub-keys used. +- `S`: Stream cipher, +- `H`: Keyed-Hash function, +- `K`: Key derivation function (KDF) to derive the 4 internal round keys -In here we use: -- Chacha20 from [rustcrypto streamciphers](https://github.com/RustCrypto/stream-ciphers) -- Blake2b from [rustcrypto hashes](https://github.com/RustCrypto/hashes/tree/master) -- turboshake KDF from [rustcrypto SHA3](https://github.com/RustCrypto/hashes/tree/master/sha3) +Any secure compatible options and their combinations can work, in this crate we have the following options: +- `S`: AES-CTR-128, Chacha20 +- `H`: keyed-Blake2b, HMAC-SHA-256, SHA-256 (with key prepend) +- `K`: TURBOSHAKE-128, SHAKE-128, HKDF-SHA-256, domain-seperated SHA-256 + +These primitives are imported from: +- [rustcrypto streamciphers](https://github.com/RustCrypto/stream-ciphers) +- [rustcrypto hashes](https://github.com/RustCrypto/hashes/tree/master) The security of lioness reduce to the security of the underlying stream cipher or the hash function. @@ -27,22 +31,30 @@ Here is an example of how to use the `Lioness_blockcipher` create. Use a 32-byte master key and encrypt or decrypt a block in place: ```rust -use lioness_blockcipher::{Lioness, MasterKey}; +use rand_core::{OsRng, RngCore}; +use lioness_blockcipher::prelude::*; +type TestLioness = Lioness::< + ChaCha20StreamCipher, + KeyedBlake2b, + TurboShake128Kdf +>; fn main() -> anyhow::Result<()> { - let mut key: MasterKey = [0x42; 32]; + let mut key: Key256 = Default::default(); + OsRng.fill_bytes(&mut key); - let cipher = Lioness::new(&key); + let cipher: TestLioness = Lioness::new(&key)?; - // Blocks must be at >32 bytes long - let mut block = b"this is a long plaintext block and must stay a secret".to_vec(); + // Blocks must be at >64 bytes long + let mut block = vec![0x84u8; 65]; let original = block.clone(); cipher.encrypt_in_place(&mut block)?; cipher.decrypt_in_place(&mut block)?; - + assert_eq!(block, original); + println!("success!"); Ok(()) } @@ -51,13 +63,13 @@ fn main() -> anyhow::Result<()> { Some notes: - Encryption and decryption are both in-place for now. -- The block length need to be bigger than `32` bytes because Lioness splits the block into two where the left part is 32-byte, and the right part can't be empty. might support small blocks in the future, but for Sphinx use-case, this should work. -- If you need authenticity, make sure to prepend the plaintext with `k = 128-bits` zeros and check the zeros after decryption. This will be supported in the future... see [integrity example](./examples/integrity.rs) +- The block length need to be bigger than `64` bytes because Lioness splits the block into two where the left part is 32-byte, and the right part needs at least 16 bytes. might support small blocks in the future, but for Sphinx use-case, this should work. +- If you need authenticity, make sure to use `encrypt_in_place_auth` which prepends the plaintext with 128-bits zeros. Also use `decrypt_in_place_auth` to check the zeros after decryption. see [authentication example](./examples/auth) ### TODO -- [ ] Add more tests, examples, and benchmarks ... -- [ ] Make it generic for any compatible cipher, keyed_hash, and KDF. +- [x] Add more tests, examples, and benchmarks ... +- [x] Make it generic for any compatible cipher, keyed_hash, and KDF. - [ ] Compare with existing implementation + maybe with Haskel when available. -- [ ] Add a version with API which prepend the plaintext with k-zeros and checks authenticity after decryption. +- [x] Add function which prepend the plaintext with k-zeros and checks authenticity after decryption. - [ ] impl enc and dec to the API to work beside encrypt_in_place and decrypt_in_place. - ... diff --git a/examples/auth.rs b/examples/auth.rs new file mode 100644 index 0000000..f66b3cc --- /dev/null +++ b/examples/auth.rs @@ -0,0 +1,34 @@ +use anyhow::Result; +use rand_core::{OsRng, RngCore}; +use lioness_blockcipher::lioness::SEC_PARAM; +use lioness_blockcipher::prelude::*; + +type TestLioness = Lioness::< + ChaCha20StreamCipher, + KeyedBlake2b, + TurboShake128Kdf +>; + +fn main() -> Result<()> { + let mut key: Key256 = Default::default(); + OsRng.fill_bytes(&mut key); + let cipher: TestLioness = Lioness::new(&key)?; + + let payload = vec![0x84u8; 4096]; + let mut plaintext = vec![0u8; SEC_PARAM]; + plaintext.extend_from_slice(&payload); + + let mut block = plaintext.clone(); + cipher.encrypt_in_place(&mut block)?; + + // tamper with the ciphertext + block[21] ^= 0x01; + + cipher.decrypt_in_place(&mut block)?; + + if block[..SEC_PARAM].iter().all(|&b| b != 0) { + println!("tampering detected i.e. zero-prefix check failed"); + } + + Ok(()) +} diff --git a/examples/integrity.rs b/examples/integrity.rs deleted file mode 100644 index f7fd7e0..0000000 --- a/examples/integrity.rs +++ /dev/null @@ -1,27 +0,0 @@ -use anyhow::Result; -use lioness_blockcipher::{Lioness, MasterKey}; - -const K: usize = 16; - -fn main() -> Result<()> { - let key: MasterKey = [0x42; 32]; - let cipher = Lioness::new(&key); - - let payload = b"this plaintext msg is prefixed with k zeros before encryption"; - let mut plaintext = vec![0u8; K]; - plaintext.extend_from_slice(payload); - - let mut block = plaintext.clone(); - cipher.encrypt_in_place(&mut block)?; - - // tamper with the ciphertext - block[K + 5] ^= 0x01; - - cipher.decrypt_in_place(&mut block)?; - - if block[..K].iter().all(|&b| b != 0) { - println!("tampering detected i.e. zero-prefix check failed"); - } - - Ok(()) -} diff --git a/examples/simple_example.rs b/examples/simple_example.rs index 4ae8313..e6b20fc 100644 --- a/examples/simple_example.rs +++ b/examples/simple_example.rs @@ -1,12 +1,19 @@ -use lioness_blockcipher::{Lioness, MasterKey}; +use rand_core::{OsRng, RngCore}; +use lioness_blockcipher::prelude::*; +type TestLioness = Lioness::< + ChaCha20StreamCipher, + KeyedBlake2b, + TurboShake128Kdf +>; fn main() -> anyhow::Result<()> { - let key: MasterKey = [0x42; 32]; + let mut key: Key256 = Default::default(); + OsRng.fill_bytes(&mut key); - let cipher = Lioness::new(&key); + let cipher: TestLioness = Lioness::new(&key)?; - // Blocks must be at >32 bytes long - let mut block = b"this is a long plaintext block and must stay a secret".to_vec(); + // Blocks must be at >64 bytes long + let mut block = vec![0x84u8; 65]; let original = block.clone(); cipher.encrypt_in_place(&mut block)?; diff --git a/src/cipher.rs b/src/cipher.rs new file mode 100644 index 0000000..d20ba12 --- /dev/null +++ b/src/cipher.rs @@ -0,0 +1,28 @@ +use chacha20::ChaCha20; +use chacha20::cipher::{KeyIvInit, StreamCipher}; +use crate::lioness::{Key256, LionessCipher}; + +// ---------------- chacha20 ------------------ // +// chacha IV +const CHACHA20_IV: [u8; 12] = *b"chacha20_iv\0"; +pub struct ChaCha20StreamCipher; +impl LionessCipher for ChaCha20StreamCipher{ + fn apply_keystream(round_key: &Key256, block: &mut [u8]) -> anyhow::Result<()> { + let mut cipher = ChaCha20::new(&(*round_key).into(), &CHACHA20_IV.into()); + cipher.apply_keystream(block); + Ok(()) + } +} + +//----------------------AES-CTR-128--------------------------// + +pub struct Aes128CtrStreamCipher; +type Aes128CtrBE = ctr::Ctr128BE; +impl LionessCipher for Aes128CtrStreamCipher{ + fn apply_keystream(round_key: &Key256, block: &mut [u8]) -> anyhow::Result<()> { + let (aes_key, aes_iv) = round_key.split_at(16); + let mut cipher = Aes128CtrBE::new_from_slices(aes_key, aes_iv)?; + cipher.apply_keystream(block); + Ok(()) + } +} \ No newline at end of file diff --git a/src/kdf.rs b/src/kdf.rs new file mode 100644 index 0000000..5e57910 --- /dev/null +++ b/src/kdf.rs @@ -0,0 +1,83 @@ +use sha2::{Sha256, Digest}; +use turboshake::TurboShake128; +use turboshake::digest::{Update, ExtendableOutput, XofReader}; +use sha3::Shake128; +use hkdf::Hkdf; +use zeroize::Zeroize; +use crate::lioness::{K_256, Key256, LionessKdf}; +use crate::lioness::RoundKeys; + +const KEY_MATERIAL_SIZE: usize = 4 * K_256; +const LIONESS_KDF_DOMAIN: &[u8] = b"lioness-payload-key"; + +//-----------------------turboshake128-------------------------// +pub struct TurboShake128Kdf; +impl LionessKdf for TurboShake128Kdf{ + fn derive_keys(master_key: &Key256) -> anyhow::Result { + // NOTE: this uses the default domain separation 0x1f + let mut kdf = ::default(); + kdf.update(LIONESS_KDF_DOMAIN); + kdf.update(master_key); + let mut reader = kdf.finalize_xof(); + let mut material = [0u8; KEY_MATERIAL_SIZE]; + reader.read(&mut material); + let round_keys = RoundKeys::from_key_material(&material); + material.zeroize(); + Ok(round_keys) + } +} + +//--------------------------SHAKE-128------------------------------// +pub struct Shake128Kdf; +impl LionessKdf for Shake128Kdf { + fn derive_keys(master_key: &Key256) -> anyhow::Result { + let mut kdf = Shake128::default(); + kdf.update(LIONESS_KDF_DOMAIN); + kdf.update(master_key); + let mut reader = kdf.finalize_xof(); + let mut material = [0u8; KEY_MATERIAL_SIZE]; + reader.read(&mut material); + let round_keys = RoundKeys::from_key_material(&material); + material.zeroize(); + Ok(round_keys) + } +} + +//-----------------------------HKDF-SHA256----------------------------------// +pub struct HkdfSha256; +impl LionessKdf for HkdfSha256{ + fn derive_keys(master_key: &Key256) -> anyhow::Result { + let kdf = Hkdf::::new(None, master_key); + let mut material = [0u8; KEY_MATERIAL_SIZE]; + kdf.expand(LIONESS_KDF_DOMAIN, &mut material)?; + let round_keys = RoundKeys::from_key_material(&material); + material.zeroize(); + Ok(round_keys) + } +} + +//---------------------------------dom-sep-sha256----------------------------------------// + +pub struct DomSepSha256Kdf; +const LIONESS_ROUND_KEY_DOMAINS: [&[u8]; 4] = [ + b"Lioness-key1", + b"Lioness-key2", + b"Lioness-key3", + b"Lionesskey4", +]; +impl LionessKdf for DomSepSha256Kdf { + fn derive_keys(master_key: &Key256) -> anyhow::Result { + let mut round_keys: RoundKeys = Default::default(); + for (i, key) in round_keys.keys.iter_mut().enumerate() { + let mut hash = Sha256::new(); + Digest::update(&mut hash, LIONESS_KDF_DOMAIN); + Digest::update(& mut hash, LIONESS_ROUND_KEY_DOMAINS[i]); + Digest::update(& mut hash, master_key); + Digest::update(& mut hash, master_key); + let output = hash.finalize(); + key.copy_from_slice(&output); + + } + Ok(round_keys) + } +} \ No newline at end of file diff --git a/src/keyed_hash.rs b/src/keyed_hash.rs new file mode 100644 index 0000000..2da7c7f --- /dev/null +++ b/src/keyed_hash.rs @@ -0,0 +1,48 @@ +use blake2::Blake2bMac; +use blake2::digest::{KeyInit as BlakeKeyInit, Mac, consts::U32}; +use hmac::Hmac; +use sha2::{Sha256, Digest}; +use crate::lioness::{Digest256, Key256, LionessKeyedHash}; + +//-----------------------keyed-blake2b-------------------------// +pub struct KeyedBlake2b; +impl LionessKeyedHash for KeyedBlake2b { + fn hash(round_key: &Key256, input: &[u8]) -> anyhow::Result { + let mut h = as BlakeKeyInit>::new_from_slice(round_key)?; + Mac::update(&mut h, input); + let mut digest: Digest256 = Default::default(); + digest.copy_from_slice(&h.finalize().into_bytes()); + Ok(digest) + } +} + +//----------------------------SHA256-PrependKey---------------------------------// +/// SHA256-PrependKey is here just SHA256 with the key prepended to the message, i.e. H(k || m) +/// Don't use this as MAC, should only be used in the context of LIONESS. +pub struct Sha256PrependKey; +impl LionessKeyedHash for Sha256PrependKey{ + fn hash(round_key: &Key256, input: &[u8]) -> anyhow::Result { + let mut hash = Sha256::new(); + hash.update(round_key); + hash.update(input); + let output = hash.finalize(); + let mut digest: Digest256 = Default::default(); + digest.copy_from_slice(&output); + Ok(digest) + } +} + +//----------------------------------HMAC-SHA-256--------------------------------------------// +pub struct HmacSha256KeyedHash; +type HmacSha256 = Hmac; +impl LionessKeyedHash for HmacSha256KeyedHash { + fn hash(round_key: &Key256, input: &[u8]) -> anyhow::Result { + let mut mac = HmacSha256::new_from_slice(round_key)?; + mac.update(input); + let result = mac.finalize().into_bytes(); + + let mut digest: Digest256 = Default::default(); + digest.copy_from_slice(&result); + Ok(digest) + } +} diff --git a/src/lib.rs b/src/lib.rs index 7f060c5..3561b76 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,238 +1,21 @@ -use anyhow::{Result, anyhow}; -use blake2::Blake2bMac; -use blake2::digest::{KeyInit as BlakeKeyInit, Mac, consts::U32}; -use chacha20::ChaCha20; -use chacha20::cipher::{KeyIvInit, StreamCipher}; -use turboshake::TurboShake128; -use turboshake::digest::{Update, ExtendableOutput, XofReader}; -use zeroize::{Zeroize, ZeroizeOnDrop}; +pub mod cipher; +pub mod keyed_hash; +pub mod kdf; +pub mod lioness; -// We expect the input key to be of size 32 bytes (256-bits) -// because in sphinx this is the size of the shared key `s` between the sender and each hop. -// This shared key is then used to derive all the needed keys to encrypt the payload -pub const MASTER_KEY_LEN: usize = 32; -// For LIONESS, the length of the left part (after splitting block into left `L` and right `R`) -// must be the same size as: -// - the stream cipher key -// - the output (digest) of the keyed-hash function -// this is because: -// - in the stream cipher round you xor `L` with stream cipher key -// - in the hash round, you xor `L` with hash digest -pub const LEFT_LEN: usize = 32; -// ChaCha20 expects a key size of 32 bytes. -pub const STREAM_KEY_LEN: usize = 32; -// we use hash key of size 64. Rustcrypto blake2b accepts any key size -// but will pad to 64 anyway, 32 would also work -pub const HASH_KEY_LEN: usize = 64; - -// we expect the block length `m` to be big -// it need to be bigger than the `left` length or stream cipher key size (these are equal) -// the paper states that |L| = k , and that |R| = m-k , so it implies that m > k -// there are probably ways to support small blocks, but for the sphinx case this would work. -pub const MIN_BLOCK_LEN: usize = LEFT_LEN + 1; -// the size of needed key material to output from the KDF, -// we need 2 steam keys and 2 hash keys -pub const KEY_MATERIAL_LEN: usize = 2 * STREAM_KEY_LEN + 2 * HASH_KEY_LEN; -// the master key supplied from user -pub type MasterKey = [u8; MASTER_KEY_LEN]; - -// chacha IV -const CHACHA20_IV: [u8; 12] = *b"chacha20_iv\0"; - -/// We need 4 keys, one for each round. Key sizes depend on if it is a cipher or hash round. -#[derive(Clone, Zeroize, ZeroizeOnDrop)] -struct RoundKeys { - k1: [u8; STREAM_KEY_LEN], - k2: [u8; HASH_KEY_LEN], - k3: [u8; STREAM_KEY_LEN], - k4: [u8; HASH_KEY_LEN], -} - -/// LIONESS large-block cipher with: -/// - ChaCha20 stream cipher -/// - BLAKE2b keyed-hash (MAC) truncated to 32 bytes -/// - TurboSHAKE256 KDF for deriving sub-keys from a 32-byte "master" input key, -/// -/// WARNING: integrity/authenticity is not guaranteed by the LIONESS large-block cipher -/// This is because LIONESS is not an AEAD but one can add an authentication check by -/// simply prepending the plaintext with `k` bytes of zeros -/// a safe value for `k` would be 16-bytes which is what the Sphinx paper suggests. -/// However, this prepending is not part of the code here. -#[derive(Clone, ZeroizeOnDrop)] -pub struct Lioness { - round_keys: RoundKeys, -} - -impl Lioness { - /// Create a new LIONESS instance from a 32-byte user supplied "master" key. - pub fn new(master_key: &MasterKey) -> Self { - Self { - round_keys: derive_round_keys(master_key), - } - } - - /// Encrypt a single wide block in place. What it does is the following: - /// - Split block `B` into left `L` and right `R` parts, `B` = `L|R` where `|L| = k` and `|R| = m-k` - /// `m` is the message/plaintext size and `k` is the size of both streamcipher key and hash output - /// - using `S` as the stream cipher, `K_i` as the round key, and `H` as the hash, apply the four rounds: - /// 1. R = R ^ S(L ^ K_1) - /// 2. L = L ^ H_{K_2}(R) - /// 3. R = R ^ S(L ^ K_3) - /// 4. L = L ^ H_{K_4}(R) - pub fn encrypt_in_place(&self, block: &mut [u8]) -> Result<()> { - if block.len() < MIN_BLOCK_LEN { - return Err(anyhow!("block must be at least {} bytes", MIN_BLOCK_LEN)); - } - // B = L|R - let (left, right) = block.split_at_mut(LEFT_LEN); - - // R = R ^ S(L ^ K_1) - stream_round(left, right, &self.round_keys.k1); - // L = L ^ H_{K_2}(R) - hash_round(left, right, &self.round_keys.k2)?; - // R = R ^ S(L ^ K_3) - stream_round(left, right, &self.round_keys.k3); - // L = L ^ H_{K_4}(R) - hash_round(left, right, &self.round_keys.k4)?; - - Ok(()) - } - - /// Decrypt a single wide block in place. - /// Same as encryption but with the four steps flipped so from 4 -> 1 - pub fn decrypt_in_place(&self, block: &mut [u8]) -> Result<()> { - if block.len() < MIN_BLOCK_LEN { - return Err(anyhow!("blocks must be at least {} bytes", MIN_BLOCK_LEN)); - } - // B = L|R - let (left, right) = block.split_at_mut(LEFT_LEN); - - // L = L ^ H_{K_4}(R) - hash_round(left, right, &self.round_keys.k4)?; - // R = R ^ S(L ^ K_3) - stream_round(left, right, &self.round_keys.k3); - // L = L ^ H_{K_2}(R) - hash_round(left, right, &self.round_keys.k2)?; - // R = R ^ S(L ^ K_1) - stream_round(left, right, &self.round_keys.k1); - - Ok(()) - } -} - -/// derive all 4 keys from the master key using the KDF i.e. turboshake in here. -fn derive_round_keys(master_key: &MasterKey) -> RoundKeys { - // WARNING: this uses the default domain separation 0x1f - let mut kdf = ::default(); - kdf.update(master_key); - - let mut reader = kdf.finalize_xof(); - let mut material = [0u8; KEY_MATERIAL_LEN]; - reader.read(&mut material); - - let mut k1 = [0u8; STREAM_KEY_LEN]; - let mut k2 = [0u8; HASH_KEY_LEN]; - let mut k3 = [0u8; STREAM_KEY_LEN]; - let mut k4 = [0u8; HASH_KEY_LEN]; - - k1.copy_from_slice(&material[..STREAM_KEY_LEN]); - k2.copy_from_slice(&material[STREAM_KEY_LEN..STREAM_KEY_LEN + HASH_KEY_LEN]); - k3.copy_from_slice(&material[STREAM_KEY_LEN + HASH_KEY_LEN..2 * STREAM_KEY_LEN + HASH_KEY_LEN]); - k4.copy_from_slice(&material[2 * STREAM_KEY_LEN + HASH_KEY_LEN..]); - material.zeroize(); - - RoundKeys { k1, k2, k3, k4 } -} - -/// apply the steam cipher round -/// R = R ^ S(L ^ K_i) -fn stream_round(left: &[u8], right: &mut [u8], subkey: &[u8; STREAM_KEY_LEN]) { - let mut round_key = [0u8; STREAM_KEY_LEN]; - // K = L ^ K_i - xor(left, subkey, &mut round_key); - // C = S(L ^ K) - let mut cipher = ChaCha20::new(&round_key.into(), &CHACHA20_IV.into()); - // R = R ^ C - cipher.apply_keystream(right); - round_key.zeroize(); -} - -/// apply the hash round -/// L = L ^ H_{K_i}(R) -fn hash_round(left: &mut [u8], right: &[u8], subkey: &[u8; HASH_KEY_LEN]) -> Result<()> { - let mut h = as BlakeKeyInit>::new_from_slice(subkey)?; - Mac::update(&mut h, right); - - let mut digest = [0u8; LEFT_LEN]; - // D = H_{K_i}(R) - digest.copy_from_slice(&h.finalize().into_bytes()); - // L = L ^ D - xor_in_place(left, &digest); - digest.zeroize(); - - Ok(()) -} - -fn xor(left: &[u8], right: &[u8], out: &mut [u8]) { - assert_eq!(left.len(), right.len()); - assert_eq!(left.len(), out.len()); - - for ((dst, lhs), rhs) in out.iter_mut().zip(left.iter()).zip(right.iter()) { - *dst = *lhs ^ *rhs; - } -} - -fn xor_in_place(bufer: &mut [u8], mask: &[u8]) { - assert_eq!(bufer.len(), mask.len()); - - for (dest, src) in bufer.iter_mut().zip(mask.iter()) { - *dest ^= *src; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn get_test_key() -> MasterKey { - let mut key = [0u8; MASTER_KEY_LEN]; - for (i, byte) in key.iter_mut().enumerate() { - *byte = i as u8; - } - key - } - - #[test] - fn rejects_short_blocks() { - let cipher = Lioness::new(&get_test_key()); - let mut block = [0u8; LEFT_LEN]; - - assert!(cipher.encrypt_in_place(&mut block).is_err()); - } - - #[test] - fn enc_dec_round_trip() { - let cipher = Lioness::new(&get_test_key()); - let mut block = vec![0x84u8; 4096]; - - let original = block.clone(); - cipher.encrypt_in_place(&mut block).unwrap(); - assert_ne!(block, original); - - cipher.decrypt_in_place(&mut block).unwrap(); - assert_eq!(block, original); - } - - #[test] - fn same_input_same_key() { - let key = get_test_key(); - let cipher_a = Lioness::new(&key); - let cipher_b = Lioness::new(&key); - let mut outa = vec![0x42u8; 512]; - let mut outb = outa.clone(); - - cipher_a.encrypt_in_place(&mut outa).unwrap(); - cipher_b.encrypt_in_place(&mut outb).unwrap(); - assert_eq!(outa, outb); - } -} +pub mod prelude{ + pub use crate::lioness::{ + Key256, K_256, + LionessKdf, LionessCipher, LionessKeyedHash, + Lioness + }; + pub use crate::cipher::{ + ChaCha20StreamCipher, + }; + pub use crate::kdf::{ + TurboShake128Kdf, + }; + pub use crate::keyed_hash::{ + KeyedBlake2b, + }; +} \ No newline at end of file diff --git a/src/lioness.rs b/src/lioness.rs new file mode 100644 index 0000000..b2fa465 --- /dev/null +++ b/src/lioness.rs @@ -0,0 +1,199 @@ +use std::marker::PhantomData; +use anyhow::{Result, anyhow}; +use zeroize::{Zeroize, ZeroizeOnDrop}; + +/// security parameter used. This is set to 16-bytes (128-bits) to match the Mix protocol. +pub const SEC_PARAM: usize = 16; + +/// k, which we set here to 32-bytes (256-bits), is a constant size that will be used for many params: +/// - the length of the left part (after splitting block into left `L` and right `R`) +/// - the stream cipher key size +/// - the keyed-hash key size +/// - the output (digest) size of the keyed-hash function +/// - the internal LIONESS round key size +pub const K_256: usize = 2*SEC_PARAM; + +/// 32 bytes (256-bit) key type +pub type Key256 = [u8; K_256]; +/// digest type of size 32-bytes (256-bit). +/// We require the keyed hash to output a digest of the same size as Key256 +pub type Digest256 = Key256; + +/// We need 4 keys, one for each round. We use 256-byte keys for both cipher and keyed hash. +#[derive(Clone, Zeroize, ZeroizeOnDrop)] +pub struct RoundKeys { + pub keys: [Key256; 4], +} + +impl Default for RoundKeys { + fn default() -> Self{ + Self{ + keys: Default::default() + } + } +} + +impl RoundKeys { + pub fn from_key_material(key_material: &[u8; 4*K_256]) -> Self{ + let mut keys: [Key256; 4] = Default::default(); + for (ki, km) in keys.iter_mut().zip(key_material.chunks_exact(K_256)){ + ki.copy_from_slice(km); + } + Self{ + keys + } + } +} + +/// LIONESS KDF trait, require a function which take a master key and generates 4 round keys each of size `K_256` +pub trait LionessKdf{ + fn derive_keys(master_key: &Key256) -> Result; +} + +/// LIONESS stream cipher trait, the cipher takes a round key of size `K_256`, a block of data of any size +/// the function `apply_keystream` will generate a key stream (using the round key) and apply that keystream to the block. +pub trait LionessCipher{ + fn apply_keystream(round_key: &Key256, block: &mut [u8]) -> Result<()>; +} + +/// LIONESS keyed hash trait, the hash takes a key (round key) and input of any size +/// outputs a hash digest with size equals to `K_256` +pub trait LionessKeyedHash{ + fn hash(round_key: &Key256, input: &[u8]) -> Result; +} + +/// LIONESS large-block cipher with: +/// - S: stream cipher +/// - H: keyed-hash (MAC) truncated to 32 bytes +/// - K: KDF for deriving sub-keys from a 32-byte "masterkey", +#[derive(Clone, ZeroizeOnDrop)] +pub struct Lioness { + round_keys: RoundKeys, + #[zeroize(skip)] + phantom_data: PhantomData<(S,H,K)> +} + +impl< + S: LionessCipher, + H: LionessKeyedHash, + K: LionessKdf +> Lioness { + /// Create a new LIONESS instance from a 32-byte user supplied "master key". + /// We expect the input "masterkey" to be of size 32 bytes. + /// because in sphinx this is the size of the shared key `s` between the sender and each hop. + /// This shared key is then used to derive all the needed keys to encrypt the payload + pub fn new(master_key: &Key256) -> Result { + Ok( + Self { + round_keys: K::derive_keys(master_key)?, + phantom_data: Default::default(), + } + ) + } + + /// Encrypt a single wide block in place. What it does is the following: + /// - Split block `B` into left `L` and right `R` parts, `B` = `L|R` where `|L| = k` and `|R| = m-k` + /// `block` is the message/plaintext size + /// - using `S` as the stream cipher, `K_i` as the round key, and `H` as the keyed hash, apply the four rounds: + /// 1. R = R ^ S(L ^ K_1) + /// 2. L = L ^ H_{K_2}(R) + /// 3. R = R ^ S(L ^ K_3) + /// 4. L = L ^ H_{K_4}(R) + /// WARNING: for encryption with integrity/authenticity use `encrypt_in_place_auth` + pub fn encrypt_in_place(&self, block: &mut [u8]) -> Result<()> { + // we expect the block length `m` to be big, so we expect at least `2*K_256` + if block.len() < 2*K_256 { + return Err(anyhow!("block must be at least {} bytes", 2*K_256)); + } + // B = L|R + let (left, right) = block.split_at_mut(K_256); + + // R = R ^ S(L ^ K_1) + self.stream_round(left, right, &self.round_keys.keys[0])?; + // L = L ^ H_{K_2}(R) + self.hash_round(left, right, &self.round_keys.keys[1])?; + // R = R ^ S(L ^ K_3) + self.stream_round(left, right, &self.round_keys.keys[2])?; + // L = L ^ H_{K_4}(R) + self.hash_round(left, right, &self.round_keys.keys[3])?; + + Ok(()) + } + + /// Same as `encrypt_in_place` but prepends the plaintext with `SEC_PARAM` bytes of zeros + pub fn encrypt_in_place_auth(&self, _block: &mut [u8]) -> Result<()> { + let mut plaintext = vec![0u8; SEC_PARAM]; + plaintext.extend_from_slice(_block); + todo!() + } + + /// Decrypt a single wide block in place. + /// Same as encryption but with the four steps flipped so from 4 -> 1 + /// WARNING: for decryption with integrity/authenticity use `decrypt_in_place_auth` + pub fn decrypt_in_place(&self, block: &mut [u8]) -> Result<()> { + if block.len() < 2*K_256 { + return Err(anyhow!("blocks must be at least {} bytes", 2*K_256)); + } + // B = L|R + let (left, right) = block.split_at_mut(K_256); + + // L = L ^ H_{K_4}(R) + self.hash_round(left, right, &self.round_keys.keys[3])?; + // R = R ^ S(L ^ K_3) + self.stream_round(left, right, &self.round_keys.keys[2])?; + // L = L ^ H_{K_2}(R) + self.hash_round(left, right, &self.round_keys.keys[1])?; + // R = R ^ S(L ^ K_1) + self.stream_round(left, right, &self.round_keys.keys[0])?; + + Ok(()) + } + + /// Same as `decrypt_in_place` with added check for `SEC_PARAM`-bytes zero prefix + pub fn decrypt_in_place_auth(&self, _block: &mut [u8]) -> Result<()> { + todo!() + } + + /// apply the steam cipher round + /// R = R ^ S(L ^ K_i) + fn stream_round(&self, left: &[u8], right: &mut [u8], subkey: &Key256) -> Result<()>{ + let mut round_key: Key256 = [0u8; K_256]; + // K = L ^ K_i + xor(left, subkey, &mut round_key); + // generate key stream: KS = S(L ^ K) + // and apply the key stream R = R ^ KS + S::apply_keystream(&round_key, right)?; + round_key.zeroize(); + Ok(()) + } + + /// apply the hash round + /// L = L ^ H_{K_i}(R) + fn hash_round(&self, left: &mut [u8], right: &[u8], round_key: &Key256) -> Result<()> { + // h = H_{K_i}(R) + let mut digest= H::hash(round_key, right)?; + // L = L ^ h + xor_in_place(left, &digest); + digest.zeroize(); + + Ok(()) + } +} + +fn xor(left: &[u8], right: &[u8], out: &mut [u8]) { + assert_eq!(left.len(), right.len()); + assert_eq!(left.len(), out.len()); + + for ((dst, lhs), rhs) in out.iter_mut().zip(left.iter()).zip(right.iter()) { + *dst = *lhs ^ *rhs; + } +} + +fn xor_in_place(bufer: &mut [u8], mask: &[u8]) { + assert_eq!(bufer.len(), mask.len()); + + for (dest, src) in bufer.iter_mut().zip(mask.iter()) { + *dest ^= *src; + } +} + diff --git a/tests/lioness.rs b/tests/lioness.rs new file mode 100644 index 0000000..cdfb9d2 --- /dev/null +++ b/tests/lioness.rs @@ -0,0 +1,52 @@ +#[cfg(test)] +mod tests { + use rand_core::{OsRng, RngCore}; + use lioness_blockcipher::cipher::Aes128CtrStreamCipher; + use lioness_blockcipher::kdf::DomSepSha256Kdf; + use lioness_blockcipher::keyed_hash::HmacSha256KeyedHash; + use lioness_blockcipher::prelude::*; + type TestLioness = Lioness::< + Aes128CtrStreamCipher, + HmacSha256KeyedHash, + DomSepSha256Kdf + >; + fn get_test_key() -> Key256 { + let mut key: Key256 = Default::default(); + OsRng.fill_bytes(&mut key); + key + } + + #[test] + fn rejects_short_blocks() { + let cipher: TestLioness = Lioness::new(&get_test_key()).expect("invalid master key"); + let mut block = [0u8; K_256]; + + assert!(cipher.encrypt_in_place(&mut block).is_err()); + } + + #[test] + fn enc_dec_round_trip() { + let cipher: TestLioness = Lioness::new(&get_test_key()).expect("invalid master key"); + let mut block = vec![0x84u8; 4096]; + + let original = block.clone(); + cipher.encrypt_in_place(&mut block).unwrap(); + assert_ne!(block, original); + + cipher.decrypt_in_place(&mut block).unwrap(); + assert_eq!(block, original); + } + + #[test] + fn same_input_same_key() { + let key = get_test_key(); + let cipher_a: TestLioness = Lioness::new(&key).expect("invalid master key"); + let cipher_b: TestLioness = Lioness::new(&key).expect("invalid master key"); + let mut outa = vec![0x42u8; 512]; + let mut outb = outa.clone(); + + cipher_a.encrypt_in_place(&mut outa).unwrap(); + cipher_b.encrypt_in_place(&mut outb).unwrap(); + assert_eq!(outa, outb); + } +} \ No newline at end of file