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]
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"] }

View File

@ -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.
- ...

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<()> {
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)?;

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};
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 = <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);
}
}
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,
};
}

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