Improve impl and make it generic

This commit is contained in:
M Alghazwi 2026-05-01 16:08:57 +03:00
parent 3ef311c3cb
commit ebfe646bc9
No known key found for this signature in database
GPG Key ID: 646E567CAD7DB607
11 changed files with 515 additions and 289 deletions

View File

@ -7,7 +7,14 @@ license = "MIT OR Apache-2.0"
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
blake2 = "0.10" blake2 = "0.11.0-pre.4"
chacha20 = "0.10" chacha20 = "0.10.0-pre.3"
turboshake = "0.6.0" 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"] } zeroize = { version = "1", features = ["zeroize_derive"] }
rand_core = { version = "0.6.4", features = ["getrandom"] }

View File

@ -8,14 +8,18 @@ This code has not been formally audited, Use at your own risk or ask a cryptogra
### Overview ### Overview
[Lioness](https://www.cl.cam.ac.uk/~rja14/Papers/bear-lion.pdf) is a large block cipher built from [Lioness](https://www.cl.cam.ac.uk/~rja14/Papers/bear-lion.pdf) is a large block cipher built from
- Stream cipher, - `S`: Stream cipher,
- Hash function, - `H`: Keyed-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. - `K`: Key derivation function (KDF) to derive the 4 internal round keys
In here we use: Any secure compatible options and their combinations can work, in this crate we have the following options:
- Chacha20 from [rustcrypto streamciphers](https://github.com/RustCrypto/stream-ciphers) - `S`: AES-CTR-128, Chacha20
- Blake2b from [rustcrypto hashes](https://github.com/RustCrypto/hashes/tree/master) - `H`: keyed-Blake2b, HMAC-SHA-256, SHA-256 (with key prepend)
- turboshake KDF from [rustcrypto SHA3](https://github.com/RustCrypto/hashes/tree/master/sha3) - `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 security of lioness reduce to the security of the underlying stream cipher or
the hash function. 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: Use a 32-byte master key and encrypt or decrypt a block in place:
```rust ```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<()> { 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 // Blocks must be at >64 bytes long
let mut block = b"this is a long plaintext block and must stay a secret".to_vec(); let mut block = vec![0x84u8; 65];
let original = block.clone(); let original = block.clone();
cipher.encrypt_in_place(&mut block)?; cipher.encrypt_in_place(&mut block)?;
cipher.decrypt_in_place(&mut block)?; cipher.decrypt_in_place(&mut block)?;
assert_eq!(block, original); assert_eq!(block, original);
println!("success!");
Ok(()) Ok(())
} }
@ -51,13 +63,13 @@ fn main() -> anyhow::Result<()> {
Some notes: Some notes:
- Encryption and decryption are both in-place for now. - 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. - 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 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) - 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 ### TODO
- [ ] Add more tests, examples, and benchmarks ... - [x] Add more tests, examples, and benchmarks ...
- [ ] Make it generic for any compatible cipher, keyed_hash, and KDF. - [x] Make it generic for any compatible cipher, keyed_hash, and KDF.
- [ ] Compare with existing implementation + maybe with Haskel when available. - [ ] 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. - [ ] impl enc and dec to the API to work beside encrypt_in_place and decrypt_in_place.
- ... - ...

34
examples/auth.rs Normal file
View File

@ -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(())
}

View File

@ -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(())
}

View File

@ -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<()> { 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 // Blocks must be at >64 bytes long
let mut block = b"this is a long plaintext block and must stay a secret".to_vec(); let mut block = vec![0x84u8; 65];
let original = block.clone(); let original = block.clone();
cipher.encrypt_in_place(&mut block)?; cipher.encrypt_in_place(&mut block)?;

28
src/cipher.rs Normal file
View File

@ -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<aes::Aes128>;
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(())
}
}

83
src/kdf.rs Normal file
View File

@ -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<RoundKeys> {
// NOTE: this uses the default domain separation 0x1f
let mut kdf = <TurboShake128>::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<RoundKeys> {
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<RoundKeys> {
let kdf = Hkdf::<Sha256>::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<RoundKeys> {
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)
}
}

48
src/keyed_hash.rs Normal file
View File

@ -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<Digest256> {
let mut h = <Blake2bMac<U32> 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<Digest256> {
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<Sha256>;
impl LionessKeyedHash for HmacSha256KeyedHash {
fn hash(round_key: &Key256, input: &[u8]) -> anyhow::Result<Digest256> {
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)
}
}

View File

@ -1,238 +1,21 @@
use anyhow::{Result, anyhow}; pub mod cipher;
use blake2::Blake2bMac; pub mod keyed_hash;
use blake2::digest::{KeyInit as BlakeKeyInit, Mac, consts::U32}; pub mod kdf;
use chacha20::ChaCha20; pub mod lioness;
use chacha20::cipher::{KeyIvInit, StreamCipher};
use turboshake::TurboShake128;
use turboshake::digest::{Update, ExtendableOutput, XofReader};
use zeroize::{Zeroize, ZeroizeOnDrop};
// We expect the input key to be of size 32 bytes (256-bits) pub mod prelude{
// because in sphinx this is the size of the shared key `s` between the sender and each hop. pub use crate::lioness::{
// This shared key is then used to derive all the needed keys to encrypt the payload Key256, K_256,
pub const MASTER_KEY_LEN: usize = 32; LionessKdf, LionessCipher, LionessKeyedHash,
// For LIONESS, the length of the left part (after splitting block into left `L` and right `R`) Lioness
// must be the same size as: };
// - the stream cipher key pub use crate::cipher::{
// - the output (digest) of the keyed-hash function ChaCha20StreamCipher,
// this is because: };
// - in the stream cipher round you xor `L` with stream cipher key pub use crate::kdf::{
// - in the hash round, you xor `L` with hash digest TurboShake128Kdf,
pub const LEFT_LEN: usize = 32; };
// ChaCha20 expects a key size of 32 bytes. pub use crate::keyed_hash::{
pub const STREAM_KEY_LEN: usize = 32; KeyedBlake2b,
// 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 = <TurboShake128>::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 = <Blake2bMac<U32> 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);
}
}

199
src/lioness.rs Normal file
View File

@ -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<RoundKeys>;
}
/// 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<Digest256>;
}
/// 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<S, H, K> {
round_keys: RoundKeys,
#[zeroize(skip)]
phantom_data: PhantomData<(S,H,K)>
}
impl<
S: LionessCipher,
H: LionessKeyedHash,
K: LionessKdf
> Lioness<S, H, K> {
/// 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<Self> {
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;
}
}

52
tests/lioness.rs Normal file
View File

@ -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);
}
}