use crate::kdf::{slot_commitment, verify_commitment};
use rand_core::CryptoRng;
use crate::secret::{Cek, Kek, SECRET_LEN};
use crate::CryptoError;
use chacha20poly1305::{
aead::{Aead, Payload},
KeyInit, XChaCha20Poly1305,
};
use zeroize::{Zeroize, Zeroizing};
pub const WRAP_NONCE_LEN: usize = 24;
pub const WRAPPED_CEK_LEN: usize = WRAP_NONCE_LEN + SECRET_LEN + 16;
pub fn wrap_cek<R: CryptoRng + ?Sized>(
kek: &Kek,
cek: &Cek,
core_hash: &[u8; 32],
rng: &mut R,
) -> Result<([u8; WRAPPED_CEK_LEN], [u8; 32]), CryptoError> {
let mut nonce_seed = Zeroizing::new([0u8; WRAP_NONCE_LEN]);
rng.fill_bytes(nonce_seed.as_mut_slice());
let nonce = crate::kdf::hedged_nonce::<WRAP_NONCE_LEN>(
crate::label::WRAP_NONCE,
nonce_seed.as_slice(),
cek.expose().as_slice(),
core_hash,
)?;
let cipher = XChaCha20Poly1305::new(kek.expose().into());
let sealed = cipher
.encrypt(
(&nonce).into(),
Payload {
msg: cek.expose().as_slice(),
aad: core_hash.as_slice(),
},
)
.map_err(|_| CryptoError::Authentication)?;
let mut wrapped = [0u8; WRAPPED_CEK_LEN];
let (head, tail) = wrapped.split_at_mut(WRAP_NONCE_LEN);
head.copy_from_slice(&nonce);
if tail.len() != sealed.len() {
return Err(CryptoError::BadLength);
}
tail.copy_from_slice(&sealed);
Ok((wrapped, slot_commitment(kek, core_hash)))
}
pub fn unwrap_cek(
kek: &Kek,
wrapped: &[u8; WRAPPED_CEK_LEN],
commitment: &[u8; 32],
core_hash: &[u8; 32],
) -> Result<Cek, CryptoError> {
verify_commitment(&slot_commitment(kek, core_hash), commitment)?;
let (nonce, sealed) = wrapped.split_at(WRAP_NONCE_LEN);
let nonce: [u8; WRAP_NONCE_LEN] =
nonce.try_into().map_err(|_| CryptoError::BadLength)?;
let cipher = XChaCha20Poly1305::new(kek.expose().into());
let mut opened = cipher
.decrypt(
(&nonce).into(),
Payload { msg: sealed, aad: core_hash.as_slice() },
)
.map_err(|_| CryptoError::Authentication)?;
let converted: Result<[u8; SECRET_LEN], _> = opened.as_slice().try_into();
opened.zeroize();
let mut bytes = converted.map_err(|_| CryptoError::BadLength)?;
let cek = Cek::from_bytes(bytes);
bytes.zeroize();
Ok(cek)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::panic)]
mod tests {
use super::*;
const CORE_HASH: [u8; 32] = [0x0c; 32];
const OTHER_CORE_HASH: [u8; 32] = [0x0d; 32];
struct TestRng(u64);
impl TestRng {
fn step(&mut self) -> u64 {
self.0 = self.0.wrapping_add(0x9e37_79b9_7f4a_7c15);
let mut z = self.0;
z = (z ^ (z >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
z = (z ^ (z >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
z ^ (z >> 31)
}
}
impl rand_core::TryRng for TestRng {
type Error = core::convert::Infallible;
fn try_next_u32(&mut self) -> Result<u32, Self::Error> {
Ok(self.step() as u32)
}
fn try_next_u64(&mut self) -> Result<u64, Self::Error> {
Ok(self.step())
}
fn try_fill_bytes(&mut self, dst: &mut [u8]) -> Result<(), Self::Error> {
for chunk in dst.chunks_mut(8) {
let word = self.step().to_le_bytes();
for (out, src) in chunk.iter_mut().zip(word.iter()) {
*out = *src;
}
}
Ok(())
}
}
impl rand_core::TryCryptoRng for TestRng {}
#[test]
fn two_different_ceks_under_one_kek_never_share_a_keystream() {
let other_cek = Cek::from_bytes([0x77; 32]);
let (first, _) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(1)).unwrap();
let (second, _) = wrap_cek(&kek(), &other_cek, &CORE_HASH, &mut TestRng(2)).unwrap();
let expected: Vec<u8> = cek()
.expose()
.iter()
.zip(other_cek.expose().iter())
.map(|(a, b)| a ^ b)
.collect();
let actual: Vec<u8> = first
.iter()
.skip(WRAP_NONCE_LEN)
.take(SECRET_LEN)
.zip(second.iter().skip(WRAP_NONCE_LEN).take(SECRET_LEN))
.map(|(a, b)| a ^ b)
.collect();
assert_ne!(actual, expected, "XOR обёрток раскрывает XOR ключей: nonce повторился");
}
fn kek() -> Kek {
Kek::from_bytes([0x5e; 32])
}
fn cek() -> Cek {
Cek::from_bytes([0xc3; 32])
}
#[test]
fn a_wrapped_cek_round_trips_under_the_same_kek() {
let (wrapped, commitment) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
let opened = unwrap_cek(&kek(), &wrapped, &commitment, &CORE_HASH).unwrap();
assert_eq!(opened.expose(), cek().expose());
}
#[test]
fn the_wrapped_cek_is_the_key_plus_an_authentication_tag() {
let (wrapped, _) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
assert_eq!(wrapped.len(), WRAPPED_CEK_LEN);
assert_eq!(WRAPPED_CEK_LEN, WRAP_NONCE_LEN + SECRET_LEN + 16, "nonce, ключ и тег");
assert!(wrapped.iter().skip(WRAP_NONCE_LEN).take(SECRET_LEN).ne(cek().expose().iter()));
}
#[test]
fn a_tampered_commitment_is_refused_even_though_the_aead_would_open() {
let (wrapped, commitment) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
let mut tampered = commitment;
if let Some(byte) = tampered.get_mut(0) {
*byte ^= 0x01;
}
assert_eq!(
unwrap_cek(&kek(), &wrapped, &tampered, &CORE_HASH).err(),
Some(CryptoError::Authentication)
);
assert!(unwrap_cek(&kek(), &wrapped, &commitment, &CORE_HASH).is_ok());
}
#[test]
fn a_slot_moved_to_another_header_is_refused_by_the_commitment() {
let (wrapped, commitment) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
assert_eq!(
unwrap_cek(&kek(), &wrapped, &commitment, &OTHER_CORE_HASH).err(),
Some(CryptoError::Authentication)
);
}
#[test]
fn the_core_hash_is_bound_by_the_aead_itself_and_not_only_by_the_commitment() {
let (wrapped, _) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
let recomputed = slot_commitment(&kek(), &OTHER_CORE_HASH);
assert!(
verify_commitment(&slot_commitment(&kek(), &OTHER_CORE_HASH), &recomputed).is_ok(),
"предпосылка неверна: обязательство не сходится, до AEAD дело не дойдёт"
);
assert_eq!(
unwrap_cek(&kek(), &wrapped, &recomputed, &OTHER_CORE_HASH).err(),
Some(CryptoError::Authentication),
"обёртка открылась под чужим core_hash: связанных данных нет"
);
}
#[test]
fn a_foreign_kek_never_unwraps() {
let (wrapped, commitment) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
let foreign = Kek::from_bytes([0x5f; 32]);
assert_eq!(
unwrap_cek(&foreign, &wrapped, &commitment, &CORE_HASH).err(),
Some(CryptoError::Authentication)
);
}
#[test]
fn a_flipped_bit_in_the_wrapped_key_is_refused() {
let (wrapped, commitment) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
for position in [0usize, SECRET_LEN, 47] {
let mut broken = wrapped;
if let Some(byte) = broken.get_mut(position) {
*byte ^= 0x80;
}
assert_eq!(
unwrap_cek(&kek(), &broken, &commitment, &CORE_HASH).err(),
Some(CryptoError::Authentication),
"правка байта {position} осталась незамеченной"
);
}
}
#[test]
fn a_wrong_commitment_and_a_wrong_tag_are_indistinguishable() {
let (wrapped, commitment) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
let mut tampered_commitment = commitment;
if let Some(byte) = tampered_commitment.get_mut(0) {
*byte ^= 0x01;
}
let mut tampered_tag = wrapped;
if let Some(byte) = tampered_tag.get_mut(47) {
*byte ^= 0x01;
}
let by_commitment = unwrap_cek(&kek(), &wrapped, &tampered_commitment, &CORE_HASH).err();
let by_tag = unwrap_cek(&kek(), &tampered_tag, &commitment, &CORE_HASH).err();
assert_eq!(by_commitment, Some(CryptoError::Authentication));
assert_eq!(by_commitment, by_tag);
}
#[test]
fn wrapping_is_deterministic_in_the_injected_generator_and_only_in_it() {
let first = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
let second = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
assert_eq!(first, second, "при одном сиде обёртка обязана совпадать побайтно");
let third = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(9)).unwrap();
assert_ne!(first, third, "nonce не зависит от генератора — значит, выводится");
}
#[test]
fn two_containers_never_share_a_slot_commitment() {
let (_, first) = wrap_cek(&kek(), &cek(), &CORE_HASH, &mut TestRng(7)).unwrap();
let (_, second) = wrap_cek(&kek(), &cek(), &OTHER_CORE_HASH, &mut TestRng(9)).unwrap();
assert_ne!(first, second);
}
}