use alloc::vec::Vec;
use lib_q_core::{
Aead,
AeadKey,
Error,
Nonce,
Result,
};
use lib_q_hash::Kmac256;
use lib_q_saturnin::SaturninStream;
use subtle::{
Choice,
ConstantTimeEq,
};
use zeroize::{
Zeroize,
Zeroizing,
};
type SubKeys = (Zeroizing<[u8; 32]>, Zeroizing<[u8; 32]>);
pub const KEY_BYTES: usize = 32;
pub const TAG_BYTES: usize = 32;
pub const MAX_NONCE_BYTES: usize = 64;
const MAX_PLAINTEXT_BYTES: u64 = 1 << 36;
const CUSTOM_KDF_MAC: &[u8] = b"libq.saturnin-siv.v1.kdf.mac";
const CUSTOM_KDF_ENC: &[u8] = b"libq.saturnin-siv.v1.kdf.enc";
const CUSTOM_TAG: &[u8] = b"libq.saturnin-siv.v1.tag";
const CUSTOM_MSGKEY: &[u8] = b"libq.saturnin-siv.v1.msgkey";
#[must_use]
pub fn ct_tag_eq(a: &[u8; TAG_BYTES], b: &[u8; TAG_BYTES]) -> Choice {
a.ct_eq(b)
}
#[derive(Debug, Default, Clone, Copy)]
pub struct SaturninSiv;
impl SaturninSiv {
#[must_use]
pub const fn new() -> Self {
Self
}
#[must_use]
pub const fn key_size() -> usize {
KEY_BYTES
}
#[must_use]
pub const fn tag_size() -> usize {
TAG_BYTES
}
fn check_key(key: &[u8]) -> Result<()> {
if key.len() != KEY_BYTES {
return Err(Error::InvalidKeySize {
expected: KEY_BYTES,
actual: key.len(),
});
}
Ok(())
}
fn check_nonce(nonce: &[u8]) -> Result<()> {
if nonce.len() > MAX_NONCE_BYTES {
return Err(Error::InvalidNonceSize {
expected: MAX_NONCE_BYTES,
actual: nonce.len(),
});
}
Ok(())
}
fn check_len(len: usize) -> Result<()> {
if len as u64 > MAX_PLAINTEXT_BYTES {
return Err(Error::InvalidMessageSize {
max: MAX_PLAINTEXT_BYTES as usize,
actual: len,
});
}
Ok(())
}
fn kmac32(key: &[u8], custom: &[u8], msg: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
let mut out = Zeroizing::new([0u8; 32]);
let mut k = Kmac256::new(key, custom);
k.update(msg);
k.finalize(out.as_mut_slice())
.ok_or_else(|| Error::EncryptionFailed {
operation: "Saturnin-SIV: KMAC256 finalize rejected a 32-byte output length".into(),
})?;
Ok(out)
}
fn derive_subkeys(master: &[u8]) -> Result<SubKeys> {
let k_mac = Self::kmac32(master, CUSTOM_KDF_MAC, b"")?;
let k_enc = Self::kmac32(master, CUSTOM_KDF_ENC, b"")?;
Ok((k_mac, k_enc))
}
fn compute_tag(
k_mac: &[u8; 32],
nonce: &[u8],
ad: &[u8],
plaintext: &[u8],
) -> Result<[u8; TAG_BYTES]> {
let mut m = Kmac256::new(k_mac, CUSTOM_TAG);
m.update(&(ad.len() as u64).to_be_bytes());
m.update(ad);
m.update(&(nonce.len() as u64).to_be_bytes());
m.update(nonce);
m.update(&(plaintext.len() as u64).to_be_bytes());
m.update(plaintext);
let mut tag = [0u8; TAG_BYTES];
m.finalize(&mut tag)
.ok_or_else(|| Error::EncryptionFailed {
operation: "Saturnin-SIV: KMAC256 finalize rejected a 32-byte tag length".into(),
})?;
Ok(tag)
}
fn ctr_xor(k_enc: &[u8; 32], tag: &[u8; TAG_BYTES], data: &[u8]) -> Result<Vec<u8>> {
let k_msg = Self::kmac32(k_enc, CUSTOM_MSGKEY, tag)?;
SaturninStream::new().encrypt(&k_msg[..], &tag[..16], data)
}
pub fn seal(&self, key: &[u8], nonce: &[u8], plaintext: &[u8], ad: &[u8]) -> Result<Vec<u8>> {
Self::check_key(key)?;
Self::check_nonce(nonce)?;
Self::check_len(plaintext.len())?;
let (k_mac, k_enc) = Self::derive_subkeys(key)?;
let tag = Self::compute_tag(&k_mac, nonce, ad, plaintext)?;
let body = Self::ctr_xor(&k_enc, &tag, plaintext)?;
let mut out = Vec::with_capacity(TAG_BYTES + body.len());
out.extend_from_slice(&tag);
out.extend_from_slice(&body);
Ok(out)
}
pub fn open(&self, key: &[u8], nonce: &[u8], ciphertext: &[u8], ad: &[u8]) -> Result<Vec<u8>> {
Self::check_key(key)?;
Self::check_nonce(nonce)?;
if ciphertext.len() < TAG_BYTES {
return Err(Error::aead_ciphertext_shorter_than_tag(
TAG_BYTES,
ciphertext.len(),
));
}
Self::check_len(ciphertext.len() - TAG_BYTES)?;
let mut tag = [0u8; TAG_BYTES];
tag.copy_from_slice(&ciphertext[..TAG_BYTES]);
let body = &ciphertext[TAG_BYTES..];
let (k_mac, k_enc) = Self::derive_subkeys(key)?;
let mut plaintext = Self::ctr_xor(&k_enc, &tag, body)?;
let expected = Self::compute_tag(&k_mac, nonce, ad, &plaintext)?;
if bool::from(ct_tag_eq(&expected, &tag)) {
Ok(plaintext)
} else {
plaintext.zeroize();
Err(Error::VerificationFailed {
operation: "Saturnin-SIV AEAD tag verification".into(),
})
}
}
}
impl Aead for SaturninSiv {
fn encrypt(
&self,
key: &AeadKey,
nonce: &Nonce,
plaintext: &[u8],
associated_data: Option<&[u8]>,
) -> Result<Vec<u8>> {
self.seal(
&key.data,
&nonce.data,
plaintext,
associated_data.unwrap_or(&[]),
)
}
fn decrypt(
&self,
key: &AeadKey,
nonce: &Nonce,
ciphertext: &[u8],
associated_data: Option<&[u8]>,
) -> Result<Vec<u8>> {
self.open(
&key.data,
&nonce.data,
ciphertext,
associated_data.unwrap_or(&[]),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subkeys_are_distinct_from_each_other_and_from_the_master_key() {
let master = [0x5Au8; KEY_BYTES];
let (k_mac, k_enc) = SaturninSiv::derive_subkeys(&master).expect("derivation");
assert_ne!(
k_mac[..],
k_enc[..],
"K_mac and K_enc are equal: the MAC and the cipher share a key"
);
assert_ne!(
k_mac[..],
master[..],
"K_mac is the raw master key: no derivation happened"
);
assert_ne!(
k_enc[..],
master[..],
"K_enc is the raw master key: no derivation happened"
);
}
#[test]
fn subkeys_depend_on_the_master_key() {
let a = [0u8; KEY_BYTES];
let mut b = a;
b[31] ^= 1;
let (a_mac, a_enc) = SaturninSiv::derive_subkeys(&a).expect("derivation");
let (b_mac, b_enc) = SaturninSiv::derive_subkeys(&b).expect("derivation");
assert_ne!(a_mac[..], b_mac[..], "K_mac ignored a master-key bit flip");
assert_ne!(a_enc[..], b_enc[..], "K_enc ignored a master-key bit flip");
}
#[test]
fn framing_shift_between_ad_and_plaintext_changes_the_tag() {
let k_mac = [0x11u8; 32];
let nonce: [u8; 0] = [];
let t1 = SaturninSiv::compute_tag(&k_mac, &nonce, b"ab", b"c").expect("tag");
let t2 = SaturninSiv::compute_tag(&k_mac, &nonce, b"a", b"bc").expect("tag");
assert_ne!(
t1, t2,
"(AD=\"ab\", P=\"c\") and (AD=\"a\", P=\"bc\") produced the same tag: the MAC \
encoding is not injective (this is the lib-q-ring-sig 275bf59 defect)"
);
}
#[test]
fn framing_shift_between_nonce_and_plaintext_changes_the_tag() {
let k_mac = [0x11u8; 32];
let t1 = SaturninSiv::compute_tag(&k_mac, b"xy", b"", b"z").expect("tag");
let t2 = SaturninSiv::compute_tag(&k_mac, b"x", b"", b"yz").expect("tag");
assert_ne!(t1, t2, "nonce/plaintext boundary is not authenticated");
}
}