mirror of
https://github.com/logos-storage/lioness_blockcipher.git
synced 2026-05-18 18:49:28 +00:00
Improve impl and make it generic
This commit is contained in:
parent
3ef311c3cb
commit
ebfe646bc9
11
Cargo.toml
11
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"] }
|
||||
|
||||
48
README.md
48
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.
|
||||
- ...
|
||||
|
||||
34
examples/auth.rs
Normal file
34
examples/auth.rs
Normal 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(())
|
||||
}
|
||||
@ -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(())
|
||||
}
|
||||
@ -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
28
src/cipher.rs
Normal 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
83
src/kdf.rs
Normal 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
48
src/keyed_hash.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
257
src/lib.rs
257
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 = <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
199
src/lioness.rs
Normal 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
52
tests/lioness.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user