use subtle::ConstantTimeEq;
use zeroize::Zeroize;
const MAX_BLOCK: usize = 64;
pub const MAX_PLAINTEXT_LEN: usize = 255;
pub const MAX_AAD_LEN: usize = 255;
const H_BUF_LEN: usize = 2 * MAX_BLOCK + MAX_AAD_LEN;
const P_BUF_LEN: usize = MAX_PLAINTEXT_LEN + MAX_BLOCK;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CcmError {
PlaintextTooLong,
AadTooLong,
TagMismatch,
}
fn increment_counter(counter: &mut [u8], block_len: usize) {
for byte in counter.iter_mut().take(block_len) {
*byte = byte.wrapping_add(1);
if *byte != 0 {
return;
}
}
}
#[allow(clippy::cast_possible_truncation)] fn tag_length_code(q: usize) -> u8 {
match q {
8 => 2,
16 => 3,
32 => 4,
48 => 5,
64 => 6,
_ => 0,
}
}
#[allow(clippy::too_many_arguments)]
fn compute_tag(
encrypt_block: &dyn Fn(&[u8; MAX_BLOCK]) -> [u8; MAX_BLOCK],
block_len: usize,
ccm_nb: usize,
q: usize,
nonce: &[u8],
aad: &[u8],
plaintext: &[u8],
) -> [u8; MAX_BLOCK] {
let tmp = block_len - ccm_nb - 1;
let mut g1 = [0u8; MAX_BLOCK];
g1[..tmp].copy_from_slice(&nonce[..tmp]);
#[allow(clippy::cast_possible_truncation)] {
g1[tmp] = plaintext.len() as u8;
}
let mut flags = if plaintext.is_empty() { 0u8 } else { 0x80 };
flags |= tag_length_code(q) << 4;
#[allow(clippy::cast_possible_truncation)] {
flags |= (ccm_nb - 1) as u8;
}
g1[block_len - 1] = flags;
let mut g2 = [0u8; MAX_BLOCK];
#[allow(clippy::cast_possible_truncation)] {
g2[0] = aad.len() as u8;
}
let aad_rem = aad.len() % block_len;
let g2_len = block_len - aad_rem;
let mut h_buf = [0u8; H_BUF_LEN];
let mut h_len = 0usize;
h_buf[h_len..h_len + block_len].copy_from_slice(&g1[..block_len]);
h_len += block_len;
h_buf[h_len..h_len + g2_len].copy_from_slice(&g2[..g2_len]);
h_len += g2_len;
h_buf[h_len..h_len + aad.len()].copy_from_slice(aad);
h_len += aad.len();
let mut b = [0u8; MAX_BLOCK];
let mut offset = 0usize;
while offset < h_len {
for i in 0..block_len {
b[i] ^= h_buf[offset + i];
}
b = encrypt_block(&b);
offset += block_len;
}
let mut p_buf = [0u8; P_BUF_LEN];
p_buf[..plaintext.len()].copy_from_slice(plaintext);
let mut p_len = plaintext.len();
if !p_len.is_multiple_of(block_len) {
p_buf[p_len] = 0x80;
p_len += block_len - (p_len % block_len);
}
offset = 0;
while offset < p_len {
for i in 0..block_len {
b[i] ^= p_buf[offset + i];
}
b = encrypt_block(&b);
offset += block_len;
}
b
}
struct Gamma {
counter: [u8; MAX_BLOCK],
keystream: [u8; MAX_BLOCK],
used: usize,
block_len: usize,
}
impl Gamma {
fn new(
block_len: usize,
nonce_block: &[u8; MAX_BLOCK],
encrypt_block: &dyn Fn(&[u8; MAX_BLOCK]) -> [u8; MAX_BLOCK],
) -> Self {
let seed = encrypt_block(nonce_block);
Self {
counter: seed,
keystream: seed,
used: block_len,
block_len,
}
}
fn apply(
&mut self,
buf: &mut [u8],
encrypt_block: &dyn Fn(&[u8; MAX_BLOCK]) -> [u8; MAX_BLOCK],
) {
let block_len = self.block_len;
let mut offset = self.used;
let mut data_off = 0usize;
if offset != 0 {
while offset < block_len && data_off < buf.len() {
buf[data_off] ^= self.keystream[offset];
data_off += 1;
offset += 1;
}
if offset == block_len {
increment_counter(&mut self.counter, block_len);
self.keystream = encrypt_block(&self.counter);
offset = 0;
}
}
while data_off + block_len <= buf.len() {
for i in 0..block_len {
buf[data_off + i] ^= self.keystream[i];
}
data_off += block_len;
increment_counter(&mut self.counter, block_len);
self.keystream = encrypt_block(&self.counter);
}
while data_off < buf.len() {
buf[data_off] ^= self.keystream[offset];
data_off += 1;
offset += 1;
}
self.used = offset;
}
}
#[allow(clippy::too_many_arguments)]
fn seal_core(
encrypt_block: &dyn Fn(&[u8; MAX_BLOCK]) -> [u8; MAX_BLOCK],
block_len: usize,
ccm_nb: usize,
q: usize,
nonce: &[u8],
aad: &[u8],
buf: &mut [u8],
) -> Result<[u8; MAX_BLOCK], CcmError> {
if buf.len() > MAX_PLAINTEXT_LEN {
return Err(CcmError::PlaintextTooLong);
}
if aad.len() > MAX_AAD_LEN {
return Err(CcmError::AadTooLong);
}
let raw_tag = compute_tag(encrypt_block, block_len, ccm_nb, q, nonce, aad, buf);
let mut nonce_block = [0u8; MAX_BLOCK];
nonce_block[..block_len].copy_from_slice(&nonce[..block_len]);
let mut gamma = Gamma::new(block_len, &nonce_block, encrypt_block);
gamma.apply(buf, encrypt_block);
let mut tag_buf = [0u8; MAX_BLOCK];
tag_buf[..q].copy_from_slice(&raw_tag[..q]);
gamma.apply(&mut tag_buf[..q], encrypt_block);
Ok(tag_buf)
}
#[allow(clippy::too_many_arguments)]
fn open_core(
encrypt_block: &dyn Fn(&[u8; MAX_BLOCK]) -> [u8; MAX_BLOCK],
block_len: usize,
ccm_nb: usize,
q: usize,
nonce: &[u8],
aad: &[u8],
buf: &mut [u8],
tag: &[u8],
) -> Result<(), CcmError> {
if buf.len() > MAX_PLAINTEXT_LEN {
return Err(CcmError::PlaintextTooLong);
}
if aad.len() > MAX_AAD_LEN {
return Err(CcmError::AadTooLong);
}
let mut nonce_block = [0u8; MAX_BLOCK];
nonce_block[..block_len].copy_from_slice(&nonce[..block_len]);
let mut gamma = Gamma::new(block_len, &nonce_block, encrypt_block);
gamma.apply(buf, encrypt_block);
let mut recovered_tag = [0u8; MAX_BLOCK];
recovered_tag[..q].copy_from_slice(&tag[..q]);
gamma.apply(&mut recovered_tag[..q], encrypt_block);
let expected_tag = compute_tag(encrypt_block, block_len, ccm_nb, q, nonce, aad, buf);
let ok: bool = recovered_tag[..q].ct_eq(&expected_tag[..q]).into();
if ok {
Ok(())
} else {
buf.zeroize();
Err(CcmError::TagMismatch)
}
}
macro_rules! kalyna_ccm_variant {
($name:ident, $expanded:ident, $key_bytes:literal, $block_bytes:literal, $ccm_nb:literal, $q:literal) => {
#[doc = concat!(
"CCM mode over [`super::kalyna::", stringify!($expanded), "`] - see the module doc ",
"comment for the construction citation and its provisional status."
)]
pub struct $name {
key: super::kalyna::$expanded,
}
impl $name {
#[must_use]
pub fn new(key: &[u8; $key_bytes]) -> Self {
Self {
key: super::kalyna::$expanded::new(key),
}
}
fn encrypt_block_padded(&self, block: &[u8; MAX_BLOCK]) -> [u8; MAX_BLOCK] {
let mut input = [0u8; $block_bytes];
input.copy_from_slice(&block[..$block_bytes]);
let out = self.key.encrypt_block(&input);
let mut padded = [0u8; MAX_BLOCK];
padded[..$block_bytes].copy_from_slice(&out);
padded
}
pub fn seal_in_place(
&self,
nonce: &[u8; $block_bytes],
aad: &[u8],
buf: &mut [u8],
) -> Result<[u8; $q], CcmError> {
let encrypt_block = |b: &[u8; MAX_BLOCK]| self.encrypt_block_padded(b);
let tag = seal_core(
&encrypt_block,
$block_bytes,
$ccm_nb,
$q,
nonce,
aad,
buf,
)?;
let mut out = [0u8; $q];
out.copy_from_slice(&tag[..$q]);
Ok(out)
}
pub fn open_in_place(
&self,
nonce: &[u8; $block_bytes],
aad: &[u8],
buf: &mut [u8],
tag: &[u8; $q],
) -> Result<(), CcmError> {
let encrypt_block = |b: &[u8; MAX_BLOCK]| self.encrypt_block_padded(b);
open_core(
&encrypt_block,
$block_bytes,
$ccm_nb,
$q,
nonce,
aad,
buf,
tag,
)
}
}
};
}
kalyna_ccm_variant!(Kalyna128_128Ccm, Kalyna128_128ExpandedKey, 16, 16, 4, 16);
kalyna_ccm_variant!(Kalyna128_256Ccm, Kalyna128_256ExpandedKey, 32, 16, 4, 16);
kalyna_ccm_variant!(Kalyna256_256Ccm, Kalyna256_256ExpandedKey, 32, 32, 4, 16);
kalyna_ccm_variant!(Kalyna256_512Ccm, Kalyna256_512ExpandedKey, 64, 32, 6, 32);
kalyna_ccm_variant!(Kalyna512_512Ccm, Kalyna512_512ExpandedKey, 64, 64, 8, 64);