diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/lib.rs | 428 |
1 files changed, 428 insertions, 0 deletions
diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4c5af26 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,428 @@ +#![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<ZeroIv<ChaCha20>, 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<ZeroIv<Ctr32BE<Aes256>>, KeyedHash<U32, Sha256>>; +//! +//! 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<ZeroIv<Ctr32BE<Aes256>>, Hmac<Sha256>>; +//! +//! 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<ZeroIv<ChaCha20>, 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<S, H> +where + S: KeySizeUser, + H: KeySizeUser, +{ + k_1: Array<u8, S::KeySize>, + k_2: Array<u8, H::KeySize>, + k_3: Array<u8, S::KeySize>, + k_4: Array<u8, H::KeySize>, +} + +fn hash_key<L>(salt: &[u8], input: &[u8]) -> Array<u8, L> +where + L: ArraySize + IsLessOrEqual<U64, Output = True>, +{ + let mut hasher = Blake2b::new_customized(salt); + hasher.update(input); + hasher.finalize_fixed() +} + +fn xor_assign<L: ArraySize>(a: &mut L::ArrayType<u8>, b: &L::ArrayType<u8>) { + a.as_mut() + .iter_mut() + .zip(b.as_ref().iter()) + .for_each(|(x, y)| *x ^= *y); +} + +type Key<K> = <<K as KeySizeUser>::KeySize as ArraySize>::ArrayType<u8>; + +impl<S, H> Lioness<S, H> +where + S: KeySizeUser<KeySize = H::OutputSize> + KeyInit + StreamCipher, + H: KeySizeUser + KeyInit + FixedOutput + Update, + S::KeySize: IsLessOrEqual<U64, Output = True>, + H::KeySize: IsLessOrEqual<U64, Output = True>, + Array<u8, S::KeySize>: 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::<S::KeySize>(SALT_1, key).0, + hash_key::<H::KeySize>(SALT_2, key).0, + hash_key::<S::KeySize>(SALT_3, key).0, + hash_key::<H::KeySize>(SALT_4, key).0, + ) + } +} + +impl<S, H> Lioness<S, H> +where + S: KeySizeUser<KeySize = H::OutputSize> + KeyInit + StreamCipher, + H: KeySizeUser + KeyInit + FixedOutput + Update, + Array<u8, S::KeySize>: 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<ZeroIv<ChaCha20>, Blake2sMac256>; + /// Cipher::new([1; 32], [2; 32], [3; 32], [4; 32]); + /// ``` + pub fn new(k_1: Key<S>, k_2: Key<H>, k_3: Key<S>, k_4: Key<H>) -> 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 = <Array<u8, S::KeySize> as FromZeros>::new_zeroed(); + key.copy_from_slice(&data[..l_len]); + xor_assign::<S::KeySize>(&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 = <Array<u8, S::KeySize> as FromZeros>::new_zeroed(); + key.copy_from_slice(&data[..l_len]); + xor_assign::<S::KeySize>(&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 = <Array<u8, S::KeySize> as FromZeros>::new_zeroed(); + key.copy_from_slice(&data[..l_len]); + xor_assign::<S::KeySize>(&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 = <Array<u8, S::KeySize> as FromZeros>::new_zeroed(); + key.copy_from_slice(&data[..l_len]); + xor_assign::<S::KeySize>(&mut key.0, &self.k_1.0); + Self::apply_keystream(key.into(), &mut data[l_len..]); + + Ok(()) + } + + fn apply_keystream(key: Key<S>, data: &mut [u8]) { + let mut cipher = S::new(&Array(key)); + cipher.apply_keystream(data); + } + + fn hash(key: &Array<u8, H::KeySize>, data: &[u8]) -> Array<u8, H::OutputSize> { + 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>(C); + +impl<C: KeySizeUser> KeySizeUser for ZeroIv<C> { + type KeySize = C::KeySize; +} + +impl<C: KeyIvInit> KeyInit for ZeroIv<C> +where + Array<u8, C::IvSize>: FromZeros, +{ + fn new(key: &Array<u8, Self::KeySize>) -> Self { + let iv = <Array<u8, C::IvSize> as FromZeros>::new_zeroed(); + Self(C::new(key, &iv)) + } +} + +impl<C: StreamCipher> StreamCipher for ZeroIv<C> { + 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<L, H>(H, PhantomData<L>); + +impl<L: ArraySize, H> KeySizeUser for KeyedHash<L, H> { + type KeySize = L; +} + +impl<L: ArraySize, H: Default + Update> KeyInit for KeyedHash<L, H> { + fn new(key: &Array<u8, Self::KeySize>) -> Self { + let mut hasher = H::default(); + hasher.update(key); + KeyedHash(hasher, PhantomData) + } +} + +impl<L: ArraySize, H: OutputSizeUser> OutputSizeUser for KeyedHash<L, H> { + type OutputSize = H::OutputSize; +} + +impl<L: ArraySize, H: Update> Update for KeyedHash<L, H> { + fn update(&mut self, data: &[u8]) { + self.0.update(data) + } +} + +impl<L: ArraySize, H: FixedOutput> FixedOutput for KeyedHash<L, H> { + fn finalize_into(self, buffer: &mut Array<u8, Self::OutputSize>) { + self.0.finalize_into(buffer) + } +} |
