#![no_std] //! LIONESS implementation in Rust. //! //! # ☣️ Cryptographic hazmat ☣️ //! //! This crate is not battle tested, nor is it audited. Its usage for critical systems is strongly //! discouraged. It mainly exists as a learning exercise. //! //! # LIONESS //! //! [LIONESS](https://link.springer.com/content/pdf/10.1007/3-540-60865-6_48.pdf) is a block cipher //! based on the Luby-Rackoff construction. It combines a hash function and a stream cipher to //! produce a block permutation that allows for blocks of arbitrary(*) length to be encrypted. //! //! Internally, the cipher uses two instantiations of the keyed hash function, and two //! instantiations of the stream cipher, leading to four separate round keys. The stream cipher and //! the hash must be compatible: The output size of the hash must be the same as the key size of //! the cipher. //! //! (*): The construction does not specify how short inputs (smaller than the hash output) should //! be encrypted. As such, [Lioness] will error on such inputs, and you have to pad the input //! manually. //! //! # Implementation //! //! The cipher is implemented as the [Lioness] struct, and is kept generic over the stream cipher //! (using the [`cipher`](https://crates.io/crates/cipher) crate), as well as the hash function //! (using the [`digest`](https://crates.io/crates/digest) crate). Rust's type system is used to //! ensure that the cipher is compatible with the hash (the hash's output size must match the //! cipher's key size). //! //! The construction is implemented in-place and works without allocations. //! //! This crate implements several convenience methods: //! //! To prevent users from having to manually create four round keys, we provide //! [Lioness::new_dynamic]. This method internally uses `blake2` to create distinct round keys from //! a given (potentially short) input key. //! //! To use ciphers that require initialization vectors (IVs), we provide [ZeroIv]. This struct //! wraps a cipher that requires an IV, and supplies a zero-IV. //! //! To use unkeyed hash functions (like SHA), we provide [KeyedHash]. This struct wraps a hash //! function and turns it into a keyed hash by prepending the key to the input data. //! //! # Examples //! //! A simple working combination for [Lioness] is to use //! [ChaCha20](https://docs.rs/chacha20/latest/chacha20/type.ChaCha20.html) and //! [Blake2s256](https://docs.rs/blake2/latest/blake2/type.Blake2sMac256.html) (32 byte key/hash //! output). Note that we use the MAC variant of Blake2 to get a keyed version of the hash //! directly: //! //! ``` //! use leona::{Lioness, ZeroIv}; //! use blake2::Blake2sMac256; //! use chacha20::ChaCha20; //! //! type Cipher = Lioness, Blake2sMac256>; //! //! let mut data = [0u8; 64]; //! data[..11].copy_from_slice(b"hello world"); //! //! let cipher = Cipher::new_dynamic(b"secret"); //! cipher.encrypt(&mut data); //! assert_ne!(&data[..11], b"hello world"); //! cipher.decrypt(&mut data); //! assert_eq!(&data[..11], b"hello world"); //! ``` //! //! You can also use AES in counter mode as a stream cipher, and SHA256 as a hash. Note how we have //! to use the `Ctr32BE` wrapper to turn AES into a stream cipher, and [KeyedHash] to turn SHA into //! a keyed hash. We choose Aes256, since its key size matches SHA256's output size (32 bytes): //! //! ``` //! use leona::{KeyedHash, Lioness, ZeroIv}; //! use aes::Aes256; //! use ctr::Ctr32BE; //! use sha2::Sha256; //! use typenum::U32; //! //! type Cipher = Lioness>, KeyedHash>; //! //! let mut data = [0u8; 64]; //! data[..11].copy_from_slice(b"hello world"); //! //! let cipher = Cipher::new_dynamic(b"secret"); //! cipher.encrypt(&mut data); //! assert_ne!(&data[..11], b"hello world"); //! cipher.decrypt(&mut data); //! assert_eq!(&data[..11], b"hello world"); //! ``` //! //! Alternatively, you can use [Hmac](https://docs.rs/hmac/latest/hmac/index.html) to turn unkeyed //! hash functions into keyed hashes: //! //! ``` //! use leona::{KeyedHash, Lioness, ZeroIv}; //! use aes::Aes256; //! use ctr::Ctr32BE; //! use hmac::Hmac; //! use sha2::Sha256; //! //! type Cipher = Lioness>, Hmac>; //! //! let mut data = [0u8; 64]; //! data[..11].copy_from_slice(b"hello world"); //! //! let cipher = Cipher::new_dynamic(b"secret"); //! cipher.encrypt(&mut data); //! assert_ne!(&data[..11], b"hello world"); //! cipher.decrypt(&mut data); //! assert_eq!(&data[..11], b"hello world"); //! ``` //! //! If the input data is shorter than the hash output, encryption will fail: //! //! ``` //! # use leona::{Lioness, ZeroIv}; //! # use blake2::Blake2sMac256; //! # use chacha20::ChaCha20; //! # type Cipher = Lioness, Blake2sMac256>; //! let mut data = [0u8; 16]; //! data[..11].copy_from_slice(b"hello world"); //! //! let cipher = Cipher::new_dynamic(b"secret"); //! assert!(cipher.encrypt(&mut data).is_err()); //! ``` //! //! If you need more control over the keys, use the [Lioness::new] constructor. In this case, you //! can provide all round keys separately. //! //! # Alternatives //! //! LIONESS is slow, and it cannot encrypt short data. If you need a wide-block cipher, consider a //! different construction such as `aez` (via [aez](https://crates.io/crates/aez) or //! [zears](https://crates.io/crates/zears)): //! //! | | `aes256` | `chacha20` | //! |--------------|--------------|--------------| //! | **`sha256`** | 645.58 MiB/s | 495.01 MiB/s | //! | **`blake2`** | 285.56 MiB/s | 251.19 MiB/s | //! //! For comparison, `zears` achieves 5.8170 GiB/s (+simd, target-cpu=native). use core::marker::PhantomData; use blake2::Blake2b; use cipher::{ InOutBuf, stream::{StreamCipher, StreamCipherError}, }; use crypto_common::{KeyInit, KeyIvInit, KeySizeUser, OutputSizeUser}; use digest::{CustomizedInit, FixedOutput, Update}; use hybrid_array::{Array, ArraySize}; use thiserror::Error; use typenum::{IsLessOrEqual, True, U64, Unsigned}; use zerocopy::FromZeros; const SALT_1: &[u8] = b"k1"; const SALT_2: &[u8] = b"k2"; const SALT_3: &[u8] = b"k3"; const SALT_4: &[u8] = b"k4"; /// Errors that can occur during encryption and decryption. #[derive(Error, Debug, Clone)] pub enum Error { /// The given input data is shorter than the hash output. #[error("given input data is too short")] InputTooShort, } /// Main struct implementing the LIONESS construction. /// /// `S` represents the stream cipher and `H` represents the hash algorithm. /// /// See the crate level documentation for more details. pub struct Lioness where S: KeySizeUser, H: KeySizeUser, { k_1: Array, k_2: Array, k_3: Array, k_4: Array, } fn hash_key(salt: &[u8], input: &[u8]) -> Array where L: ArraySize + IsLessOrEqual, { let mut hasher = Blake2b::new_customized(salt); hasher.update(input); hasher.finalize_fixed() } fn xor_assign(a: &mut L::ArrayType, b: &L::ArrayType) { a.as_mut() .iter_mut() .zip(b.as_ref().iter()) .for_each(|(x, y)| *x ^= *y); } type Key = <::KeySize as ArraySize>::ArrayType; impl Lioness where S: KeySizeUser + KeyInit + StreamCipher, H: KeySizeUser + KeyInit + FixedOutput + Update, S::KeySize: IsLessOrEqual, H::KeySize: IsLessOrEqual, Array: FromZeros, { /// Create a new [Lioness] instance from the given key. /// /// Unlike [Lioness::new], you are not required to pass the correct key sizes manually. /// Instead, `blake2` is used internally to derive keys of the correct length. /// /// Note that this only works for keys up to 64 bytes. If you have longer keys, you need to use /// [Lioness::new]. pub fn new_dynamic(key: &[u8]) -> Self { Self::new( hash_key::(SALT_1, key).0, hash_key::(SALT_2, key).0, hash_key::(SALT_3, key).0, hash_key::(SALT_4, key).0, ) } } impl Lioness where S: KeySizeUser + KeyInit + StreamCipher, H: KeySizeUser + KeyInit + FixedOutput + Update, Array: FromZeros, { /// Create a new [Lioness] instance from the given round keys. /// /// `k_1` and `k_3` are used for the stream cipher and must match its key length, while `k_2` /// and `k_4` are used for the hash function. /// /// Don't be scared of the long type signatures. They are just normal arrays of `u8`: /// /// ``` /// # use leona::{Lioness, ZeroIv}; /// # use blake2::Blake2sMac256; /// # use chacha20::ChaCha20; /// type Cipher = Lioness, Blake2sMac256>; /// Cipher::new([1; 32], [2; 32], [3; 32], [4; 32]); /// ``` pub fn new(k_1: Key, k_2: Key, k_3: Key, k_4: Key) -> Self { Self { k_1: k_1.into(), k_2: k_2.into(), k_3: k_3.into(), k_4: k_4.into(), } } /// Encrypt the given buffer in-place. /// /// If the buffer is too short (shorter than the hash output), an error is returned. pub fn encrypt(&self, data: &mut [u8]) -> Result<(), Error> { let l_len = S::KeySize::USIZE; if data.len() < l_len { return Err(Error::InputTooShort); } assert!(data.len() >= l_len); // R = R + S(L + K_1) let mut key = as FromZeros>::new_zeroed(); key.copy_from_slice(&data[..l_len]); xor_assign::(&mut key.0, &self.k_1.0); Self::apply_keystream(key.into(), &mut data[l_len..]); // L = L + H(K_2, R) let hash = Self::hash(&self.k_2, &data[l_len..]); data[..l_len] .iter_mut() .zip(hash) .for_each(|(x, y)| *x ^= y); // R = R + S(L + K_3) let mut key = as FromZeros>::new_zeroed(); key.copy_from_slice(&data[..l_len]); xor_assign::(&mut key.0, &self.k_3.0); Self::apply_keystream(key.into(), &mut data[l_len..]); // L = L + H(K_4, R) let hash = Self::hash(&self.k_4, &data[l_len..]); data[..l_len] .iter_mut() .zip(hash) .for_each(|(x, y)| *x ^= y); Ok(()) } /// Decrypt the given buffer in-place. /// /// If the buffer is too short (shorter than the hash output), an error is returned. pub fn decrypt(&self, data: &mut [u8]) -> Result<(), Error> { let l_len = S::KeySize::USIZE; if data.len() < l_len { return Err(Error::InputTooShort); } assert!(data.len() >= l_len); // L = L + H(K_4, R) let hash = Self::hash(&self.k_4, &data[l_len..]); data[..l_len] .iter_mut() .zip(hash) .for_each(|(x, y)| *x ^= y); // R = R + S(L + K_3) let mut key = as FromZeros>::new_zeroed(); key.copy_from_slice(&data[..l_len]); xor_assign::(&mut key.0, &self.k_3.0); Self::apply_keystream(key.into(), &mut data[l_len..]); // L = L + H(K_2, R) let hash = Self::hash(&self.k_2, &data[l_len..]); data[..l_len] .iter_mut() .zip(hash) .for_each(|(x, y)| *x ^= y); // R = R + S(L + K_1) let mut key = as FromZeros>::new_zeroed(); key.copy_from_slice(&data[..l_len]); xor_assign::(&mut key.0, &self.k_1.0); Self::apply_keystream(key.into(), &mut data[l_len..]); Ok(()) } fn apply_keystream(key: Key, data: &mut [u8]) { let mut cipher = S::new(&Array(key)); cipher.apply_keystream(data); } fn hash(key: &Array, data: &[u8]) -> Array { let mut hasher = H::new(key); hasher.update(data); hasher.finalize_fixed() } } /// A helper struct that turns ciphers with IV into ciphers without IV. /// /// This turns ciphers that require an IV (like `ChaCha20`), represented by /// [KeyIvInit](https://docs.rs/cipher/latest/cipher/trait.KeyIvInit.html), into ciphers that only /// require a key, represented by /// [KeyInit](https://docs.rs/cipher/latest/cipher/trait.KeyInit.html). To do so, an all-zero IV is /// supplied. #[derive(Debug, Clone)] pub struct ZeroIv(C); impl KeySizeUser for ZeroIv { type KeySize = C::KeySize; } impl KeyInit for ZeroIv where Array: FromZeros, { fn new(key: &Array) -> Self { let iv = as FromZeros>::new_zeroed(); Self(C::new(key, &iv)) } } impl StreamCipher for ZeroIv { fn check_remaining(&self, data_len: usize) -> Result<(), StreamCipherError> { self.0.check_remaining(data_len) } fn unchecked_apply_keystream_inout(&mut self, buf: InOutBuf<'_, '_, u8>) { self.0.unchecked_apply_keystream_inout(buf) } fn unchecked_write_keystream(&mut self, buf: &mut [u8]) { self.0.unchecked_write_keystream(buf) } } /// A helper struct that turns unkeyed hashes into keyed hashes. /// /// This turns hashes that have no key (like SHA256) into hashes that have a key. Note that the /// construction is rather simple: the key is prepended to the input data. This method is suggested /// in the LIONESS paper /// /// > The keyed hash function H_K(M): /// > (a) is based on an unkeyed hash function H'(M), in which we append and/or prepend the key to /// > the message [...] #[derive(Debug, Clone)] pub struct KeyedHash(H, PhantomData); impl KeySizeUser for KeyedHash { type KeySize = L; } impl KeyInit for KeyedHash { fn new(key: &Array) -> Self { let mut hasher = H::default(); hasher.update(key); KeyedHash(hasher, PhantomData) } } impl OutputSizeUser for KeyedHash { type OutputSize = H::OutputSize; } impl Update for KeyedHash { fn update(&mut self, data: &[u8]) { self.0.update(data) } } impl FixedOutput for KeyedHash { fn finalize_into(self, buffer: &mut Array) { self.0.finalize_into(buffer) } }