use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use sha2::{self};
use std::collections::HashSet;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use crate::error::{HsmError, HsmResult};
const AES_GCM_MAX_PLAINTEXT: usize = (1usize << 36) - 32;
const AES_CBC_CTR_MAX_PLAINTEXT: usize = 256 * 1024 * 1024;
static GCM_ENCRYPT_COUNTERS: std::sync::LazyLock<dashmap::DashMap<[u8; 32], AtomicU64>> =
std::sync::LazyLock::new(dashmap::DashMap::new);
static GCM_NONCE_PREFIXES: std::sync::LazyLock<dashmap::DashMap<[u8; 32], [u8; 4]>> =
std::sync::LazyLock::new(dashmap::DashMap::new);
const GCM_MAX_RANDOM_NONCE_ENCRYPTIONS: u64 = 1u64 << 31;
pub fn reset_gcm_counters() {
reset_iv_trackers();
tracing::info!(
"CBC/CTR IV trackers reset. GCM per-key counters preserved \
(tied to key lifetime, not library lifecycle)."
);
}
pub fn remove_gcm_counter(key: &[u8]) {
let kid = gcm_key_id(key);
GCM_ENCRYPT_COUNTERS.remove(&kid);
GCM_NONCE_PREFIXES.remove(&kid);
CBC_CTR_IV_TRACKER.remove(&kid);
GCM_IV_TRACKER.remove(&kid);
}
pub fn force_reset_all_counters() {
GCM_ENCRYPT_COUNTERS.clear();
GCM_NONCE_PREFIXES.clear();
GCM_IV_TRACKER.clear();
reset_iv_trackers();
tracing::info!(
"All GCM counters, nonce prefixes, and IV trackers cleared (token re-initialized)."
);
}
#[inline(always)]
fn gcm_key_id(key: &[u8]) -> [u8; 32] {
use sha2::{Digest, Sha256};
Sha256::digest(key).into()
}
fn gcm_nonce_prefix(key: &[u8]) -> HsmResult<[u8; 4]> {
let kid = gcm_key_id(key);
let entry = GCM_NONCE_PREFIXES
.entry(kid)
.or_try_insert_with(|| -> HsmResult<[u8; 4]> {
let mut prefix = [0u8; 4];
let mut drbg = crate::crypto::drbg::HmacDrbg::new()?;
drbg.generate(&mut prefix)?;
Ok(prefix)
})?;
Ok(*entry.value())
}
static CBC_CTR_IV_TRACKER: std::sync::LazyLock<
dashmap::DashMap<[u8; 32], Mutex<HashSet<[u8; 16]>>>,
> = std::sync::LazyLock::new(dashmap::DashMap::new);
const MAX_TRACKED_IVS_PER_KEY: usize = 100_000;
const MAX_TRACKED_KEYS: usize = 10_000;
static GCM_IV_TRACKER: std::sync::LazyLock<
dashmap::DashMap<[u8; 32], Mutex<HashSet<[u8; GCM_IV_LEN]>>>,
> = std::sync::LazyLock::new(dashmap::DashMap::new);
fn check_iv_reuse(key: &[u8], iv: &[u8; 16], mode: &str) -> HsmResult<()> {
check_iv_reuse_in(&CBC_CTR_IV_TRACKER, key, iv, mode)
}
fn check_iv_reuse_in<const N: usize>(
tracker: &dashmap::DashMap<[u8; 32], Mutex<HashSet<[u8; N]>>>,
key: &[u8],
iv: &[u8; N],
mode: &str,
) -> HsmResult<()> {
let key_hash = gcm_key_id(key);
if !tracker.contains_key(&key_hash) && tracker.len() >= MAX_TRACKED_KEYS {
tracing::error!(
"{}: IV tracker key limit reached ({} keys) — destroy unused keys \
before creating new ones.",
mode,
MAX_TRACKED_KEYS
);
return Err(HsmError::GeneralError);
}
let entry = tracker
.entry(key_hash)
.or_insert_with(|| Mutex::new(HashSet::new()));
let mut iv_set = entry.value().lock().map_err(|_| {
tracing::error!(
"{}: IV tracker mutex poisoned — a prior panic left the tracker \
in an inconsistent state. Refusing operation to prevent \
undetected IV reuse.",
mode
);
HsmError::GeneralError
})?;
if iv_set.contains(iv) {
tracing::error!(
"{} IV reuse detected — same IV used with the same key. \
This is a critical security violation.",
mode
);
return Err(HsmError::MechanismParamInvalid);
}
if iv_set.len() >= MAX_TRACKED_IVS_PER_KEY {
tracing::error!(
"{}: IV tracker for key is full ({} entries) — re-key required. \
Generate a new AES key via C_GenerateKey and destroy the exhausted key.",
mode,
MAX_TRACKED_IVS_PER_KEY
);
return Err(HsmError::GeneralError);
}
iv_set.insert(*iv);
Ok(())
}
pub fn reset_iv_trackers() {
CBC_CTR_IV_TRACKER.clear();
}
pub fn aes_256_gcm_encrypt(key: &[u8], plaintext: &[u8]) -> HsmResult<Vec<u8>> {
if key.len() != 32 {
return Err(HsmError::KeySizeRange);
}
if plaintext.len() > AES_GCM_MAX_PLAINTEXT {
return Err(HsmError::DataLenRange);
}
let kid = gcm_key_id(key);
let counter = GCM_ENCRYPT_COUNTERS
.entry(kid)
.or_insert_with(|| AtomicU64::new(0));
let count = loop {
let current = counter.value().load(Ordering::Acquire);
if current >= GCM_MAX_RANDOM_NONCE_ENCRYPTIONS {
tracing::error!(
"AES-GCM per-key encryption limit reached ({} operations) — \
re-key required to prevent nonce reuse. Generate a new AES key \
via C_GenerateKey and destroy the exhausted key via C_DestroyObject.",
current
);
return Err(HsmError::GeneralError);
}
match counter.value().compare_exchange(
current,
current + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(prev) => break prev,
Err(_) => continue, }
};
const GCM_WARN_THRESHOLD: u64 = GCM_MAX_RANDOM_NONCE_ENCRYPTIONS * 3 / 4;
if count == GCM_WARN_THRESHOLD {
tracing::warn!(
"AES-GCM per-key encryption count at 75% of limit ({}/{}) — \
schedule key rotation to avoid hitting the hard cap. \
Generate a new AES key via C_GenerateKey and re-wrap data.",
count,
GCM_MAX_RANDOM_NONCE_ENCRYPTIONS,
);
}
let aes_key = Key::<Aes256Gcm>::from_slice(key);
let cipher = Aes256Gcm::new(aes_key);
let prefix = gcm_nonce_prefix(key)?;
let mut nonce_bytes = [0u8; 12];
nonce_bytes[..4].copy_from_slice(&prefix);
nonce_bytes[4..].copy_from_slice(&count.to_be_bytes());
let nonce = Nonce::from_slice(&nonce_bytes);
let ciphertext = cipher
.encrypt(nonce, plaintext)
.map_err(|_| HsmError::GeneralError)?;
let mut result = Vec::with_capacity(12 + ciphertext.len());
result.extend_from_slice(&nonce_bytes);
result.extend_from_slice(&ciphertext);
Ok(result)
}
pub fn aes_256_gcm_encrypt_with_aad(
key: &[u8],
plaintext: &[u8],
aad: &[u8],
) -> HsmResult<Vec<u8>> {
use aes_gcm::aead::Payload;
if key.len() != 32 {
return Err(HsmError::KeySizeRange);
}
if plaintext.len() > AES_GCM_MAX_PLAINTEXT {
return Err(HsmError::DataLenRange);
}
let kid = gcm_key_id(key);
let counter = GCM_ENCRYPT_COUNTERS
.entry(kid)
.or_insert_with(|| AtomicU64::new(0));
let count = loop {
let current = counter.value().load(Ordering::Acquire);
if current >= GCM_MAX_RANDOM_NONCE_ENCRYPTIONS {
return Err(HsmError::GeneralError);
}
match counter.value().compare_exchange(
current,
current + 1,
Ordering::AcqRel,
Ordering::Acquire,
) {
Ok(prev) => break prev,
Err(_) => continue,
}
};
const GCM_WARN_THRESHOLD: u64 = GCM_MAX_RANDOM_NONCE_ENCRYPTIONS * 3 / 4;
if count == GCM_WARN_THRESHOLD {
tracing::warn!("AES-GCM per-key count at 75% — schedule key rotation");
}
let aes_key = Key::<Aes256Gcm>::from_slice(key);
let cipher = Aes256Gcm::new(aes_key);
let prefix = gcm_nonce_prefix(key)?;
let mut nonce_bytes = [0u8; 12];
nonce_bytes[..4].copy_from_slice(&prefix);
nonce_bytes[4..].copy_from_slice(&count.to_be_bytes());
let nonce = Nonce::from_slice(&nonce_bytes);
let payload = Payload {
msg: plaintext,
aad,
};
let ciphertext = cipher
.encrypt(nonce, payload)
.map_err(|_| HsmError::GeneralError)?;
let mut result = Vec::with_capacity(12 + ciphertext.len());
result.extend_from_slice(&nonce_bytes);
result.extend_from_slice(&ciphertext);
Ok(result)
}
pub fn aes_256_gcm_decrypt(key: &[u8], data: &[u8]) -> HsmResult<Vec<u8>> {
if key.len() != 32 {
return Err(HsmError::KeySizeRange);
}
if data.len() < 28 {
return Err(HsmError::EncryptedDataInvalid);
}
let aes_key = Key::<Aes256Gcm>::from_slice(key);
let cipher = Aes256Gcm::new(aes_key);
let nonce = Nonce::from_slice(&data[..12]);
let ciphertext = &data[12..];
cipher
.decrypt(nonce, ciphertext)
.map_err(|_| HsmError::EncryptedDataInvalid)
}
pub fn aes_256_gcm_decrypt_with_aad(key: &[u8], data: &[u8], aad: &[u8]) -> HsmResult<Vec<u8>> {
use aes_gcm::aead::Payload;
if key.len() != 32 {
return Err(HsmError::KeySizeRange);
}
if data.len() < 28 {
return Err(HsmError::EncryptedDataInvalid);
}
let aes_key = Key::<Aes256Gcm>::from_slice(key);
let cipher = Aes256Gcm::new(aes_key);
let nonce = Nonce::from_slice(&data[..12]);
let ciphertext = &data[12..];
let payload = Payload {
msg: ciphertext,
aad,
};
cipher
.decrypt(nonce, payload)
.map_err(|_| HsmError::EncryptedDataInvalid)
}
pub const GCM_IV_LEN: usize = 12;
pub const GCM_TAG_LEN: usize = 16;
pub fn check_gcm_caller_iv(key: &[u8], iv: &[u8]) -> HsmResult<()> {
let iv: &[u8; GCM_IV_LEN] = iv.try_into().map_err(|_| HsmError::MechanismParamInvalid)?;
if iv.iter().all(|&b| b == 0) {
tracing::error!("AES-GCM: all-zero IV rejected");
return Err(HsmError::MechanismParamInvalid);
}
check_iv_reuse_in(&GCM_IV_TRACKER, key, iv, "AES-GCM")?;
let counter = GCM_ENCRYPT_COUNTERS
.entry(gcm_key_id(key))
.or_insert_with(|| AtomicU64::new(0));
counter
.value()
.fetch_update(Ordering::AcqRel, Ordering::Acquire, |c| {
(c < GCM_MAX_RANDOM_NONCE_ENCRYPTIONS).then_some(c + 1)
})
.map_err(|current| {
tracing::error!(
"AES-GCM per-key encryption limit reached ({} operations) — re-key required.",
current
);
HsmError::GeneralError
})?;
Ok(())
}
pub fn aes_256_gcm_encrypt_with_iv(
key: &[u8],
iv: &[u8],
aad: &[u8],
plaintext: &[u8],
) -> HsmResult<Vec<u8>> {
use aes_gcm::aead::Payload;
if key.len() != 32 {
return Err(HsmError::KeySizeRange);
}
if iv.len() != GCM_IV_LEN {
return Err(HsmError::MechanismParamInvalid);
}
if plaintext.len() > AES_GCM_MAX_PLAINTEXT {
return Err(HsmError::DataLenRange);
}
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
cipher
.encrypt(
Nonce::from_slice(iv),
Payload {
msg: plaintext,
aad,
},
)
.map_err(|_| HsmError::GeneralError)
}
pub fn aes_256_gcm_decrypt_with_iv(
key: &[u8],
iv: &[u8],
aad: &[u8],
data: &[u8],
) -> HsmResult<Vec<u8>> {
use aes_gcm::aead::Payload;
if key.len() != 32 {
return Err(HsmError::KeySizeRange);
}
if iv.len() != GCM_IV_LEN {
return Err(HsmError::MechanismParamInvalid);
}
if data.len() < GCM_TAG_LEN {
return Err(HsmError::EncryptedDataInvalid);
}
let cipher = Aes256Gcm::new(Key::<Aes256Gcm>::from_slice(key));
cipher
.decrypt(Nonce::from_slice(iv), Payload { msg: data, aad })
.map_err(|_| HsmError::EncryptedDataInvalid)
}
pub fn aes_cbc_encrypt(key: &[u8], iv: &[u8], plaintext: &[u8]) -> HsmResult<Vec<u8>> {
use cbc::cipher::{BlockEncryptMut, KeyIvInit};
tracing::warn!("AES-CBC encrypt: unauthenticated mode — prefer AES-GCM for new applications");
if plaintext.len() > AES_CBC_CTR_MAX_PLAINTEXT {
return Err(HsmError::DataLenRange);
}
if iv.len() != 16 {
return Err(HsmError::MechanismParamInvalid);
}
if iv.iter().all(|&b| b == 0) {
tracing::error!(
"AES-CBC encrypt rejected: all-zero IV is not permitted (likely uninitialized)"
);
return Err(HsmError::MechanismParamInvalid);
}
tracing::warn!(
"AES-CBC encrypt invoked — this is non-authenticated encryption. \
Prefer AES-GCM or apply encrypt-then-MAC to prevent padding oracle attacks."
);
let iv_array: [u8; 16] = iv.try_into().unwrap(); check_iv_reuse(key, &iv_array, "AES-CBC")?;
match key.len() {
16 => {
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
let encryptor =
Aes128CbcEnc::new_from_slices(key, iv).map_err(|_| HsmError::KeySizeRange)?;
Ok(encryptor.encrypt_padded_vec_mut::<cbc::cipher::block_padding::Pkcs7>(plaintext))
}
24 => {
type Aes192CbcEnc = cbc::Encryptor<aes::Aes192>;
let encryptor =
Aes192CbcEnc::new_from_slices(key, iv).map_err(|_| HsmError::KeySizeRange)?;
Ok(encryptor.encrypt_padded_vec_mut::<cbc::cipher::block_padding::Pkcs7>(plaintext))
}
32 => {
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
let encryptor =
Aes256CbcEnc::new_from_slices(key, iv).map_err(|_| HsmError::KeySizeRange)?;
Ok(encryptor.encrypt_padded_vec_mut::<cbc::cipher::block_padding::Pkcs7>(plaintext))
}
_ => Err(HsmError::KeySizeRange),
}
}
pub fn aes_cbc_decrypt(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> HsmResult<Vec<u8>> {
use cbc::cipher::{BlockDecryptMut, KeyIvInit};
let start = std::time::Instant::now();
tracing::warn!(
"AES-CBC decrypt invoked — vulnerable to padding oracle without external MAC. \
Ensure encrypt-then-MAC is applied before decryption."
);
let result: HsmResult<Vec<u8>> = if iv.len() != 16 {
Err(HsmError::MechanismParamInvalid)
} else {
match key.len() {
16 => {
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
Aes128CbcDec::new_from_slices(key, iv)
.map_err(|_| HsmError::EncryptedDataInvalid)
.and_then(|decryptor| {
decryptor
.decrypt_padded_vec_mut::<cbc::cipher::block_padding::Pkcs7>(ciphertext)
.map_err(|_| HsmError::EncryptedDataInvalid)
})
}
24 => {
type Aes192CbcDec = cbc::Decryptor<aes::Aes192>;
Aes192CbcDec::new_from_slices(key, iv)
.map_err(|_| HsmError::EncryptedDataInvalid)
.and_then(|decryptor| {
decryptor
.decrypt_padded_vec_mut::<cbc::cipher::block_padding::Pkcs7>(ciphertext)
.map_err(|_| HsmError::EncryptedDataInvalid)
})
}
32 => {
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
Aes256CbcDec::new_from_slices(key, iv)
.map_err(|_| HsmError::EncryptedDataInvalid)
.and_then(|decryptor| {
decryptor
.decrypt_padded_vec_mut::<cbc::cipher::block_padding::Pkcs7>(ciphertext)
.map_err(|_| HsmError::EncryptedDataInvalid)
})
}
_ => Err(HsmError::EncryptedDataInvalid),
}
};
const CBC_DECRYPT_MIN_DURATION: std::time::Duration = std::time::Duration::from_millis(10);
let elapsed = start.elapsed();
if elapsed < CBC_DECRYPT_MIN_DURATION {
std::thread::sleep(CBC_DECRYPT_MIN_DURATION - elapsed);
}
result
}
pub fn aes_cbc_encrypt_unpadded(key: &[u8], iv: &[u8], plaintext: &[u8]) -> HsmResult<Vec<u8>> {
use cbc::cipher::{BlockEncryptMut, KeyIvInit};
tracing::warn!("AES-CBC encrypt: unauthenticated mode — prefer AES-GCM for new applications");
if plaintext.is_empty()
|| !plaintext.len().is_multiple_of(16)
|| plaintext.len() > AES_CBC_CTR_MAX_PLAINTEXT
{
return Err(HsmError::DataLenRange);
}
if iv.len() != 16 {
return Err(HsmError::MechanismParamInvalid);
}
if iv.iter().all(|&b| b == 0) {
tracing::error!(
"AES-CBC encrypt rejected: all-zero IV is not permitted (likely uninitialized)"
);
return Err(HsmError::MechanismParamInvalid);
}
let iv_array: [u8; 16] = iv.try_into().unwrap();
check_iv_reuse(key, &iv_array, "AES-CBC")?;
match key.len() {
16 => {
type Aes128CbcEnc = cbc::Encryptor<aes::Aes128>;
let encryptor =
Aes128CbcEnc::new_from_slices(key, iv).map_err(|_| HsmError::KeySizeRange)?;
Ok(
encryptor
.encrypt_padded_vec_mut::<cbc::cipher::block_padding::NoPadding>(plaintext),
)
}
24 => {
type Aes192CbcEnc = cbc::Encryptor<aes::Aes192>;
let encryptor =
Aes192CbcEnc::new_from_slices(key, iv).map_err(|_| HsmError::KeySizeRange)?;
Ok(
encryptor
.encrypt_padded_vec_mut::<cbc::cipher::block_padding::NoPadding>(plaintext),
)
}
32 => {
type Aes256CbcEnc = cbc::Encryptor<aes::Aes256>;
let encryptor =
Aes256CbcEnc::new_from_slices(key, iv).map_err(|_| HsmError::KeySizeRange)?;
Ok(
encryptor
.encrypt_padded_vec_mut::<cbc::cipher::block_padding::NoPadding>(plaintext),
)
}
_ => Err(HsmError::KeySizeRange),
}
}
pub fn aes_cbc_decrypt_unpadded(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> HsmResult<Vec<u8>> {
use cbc::cipher::{BlockDecryptMut, KeyIvInit};
if iv.len() != 16 {
return Err(HsmError::MechanismParamInvalid);
}
if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(16) {
return Err(HsmError::EncryptedDataLenRange);
}
match key.len() {
16 => {
type Aes128CbcDec = cbc::Decryptor<aes::Aes128>;
Aes128CbcDec::new_from_slices(key, iv)
.map_err(|_| HsmError::KeySizeRange)
.and_then(|decryptor| {
decryptor
.decrypt_padded_vec_mut::<cbc::cipher::block_padding::NoPadding>(ciphertext)
.map_err(|_| HsmError::EncryptedDataInvalid)
})
}
24 => {
type Aes192CbcDec = cbc::Decryptor<aes::Aes192>;
Aes192CbcDec::new_from_slices(key, iv)
.map_err(|_| HsmError::KeySizeRange)
.and_then(|decryptor| {
decryptor
.decrypt_padded_vec_mut::<cbc::cipher::block_padding::NoPadding>(ciphertext)
.map_err(|_| HsmError::EncryptedDataInvalid)
})
}
32 => {
type Aes256CbcDec = cbc::Decryptor<aes::Aes256>;
Aes256CbcDec::new_from_slices(key, iv)
.map_err(|_| HsmError::KeySizeRange)
.and_then(|decryptor| {
decryptor
.decrypt_padded_vec_mut::<cbc::cipher::block_padding::NoPadding>(ciphertext)
.map_err(|_| HsmError::EncryptedDataInvalid)
})
}
_ => Err(HsmError::KeySizeRange),
}
}
pub fn aes_ctr_encrypt(key: &[u8], iv: &[u8], plaintext: &[u8]) -> HsmResult<Vec<u8>> {
tracing::warn!(
"AES-CTR encrypt: unauthenticated, malleable mode — prefer AES-GCM for new applications"
);
aes_ctr_validate(key, iv, plaintext)?;
let iv_array: [u8; 16] = iv.try_into().unwrap(); check_iv_reuse(key, &iv_array, "AES-CTR")?;
aes_ctr_apply(key, iv, plaintext)
}
pub fn aes_ctr_decrypt(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> HsmResult<Vec<u8>> {
tracing::warn!(
"AES-CTR decrypt: unauthenticated, malleable mode — prefer AES-GCM for new applications"
);
aes_ctr_validate(key, iv, ciphertext)?;
aes_ctr_apply(key, iv, ciphertext)
}
#[cfg(feature = "legacy-insecure-ctr")]
#[deprecated(
since = "0.2.0",
note = "Use aes_ctr_encrypt/aes_ctr_decrypt for nonce-reuse protection. \
This function bypasses IV-reuse detection entirely."
)]
pub fn aes_ctr_crypt(key: &[u8], iv: &[u8], data: &[u8]) -> HsmResult<Vec<u8>> {
tracing::warn!(
"AES-CTR: using legacy combined crypt — migrate to aes_ctr_encrypt/aes_ctr_decrypt \
for nonce-reuse protection"
);
aes_ctr_validate(key, iv, data)?;
aes_ctr_apply(key, iv, data)
}
fn aes_ctr_validate(key: &[u8], iv: &[u8], data: &[u8]) -> HsmResult<()> {
if data.len() > AES_CBC_CTR_MAX_PLAINTEXT {
return Err(HsmError::DataLenRange);
}
if iv.len() != 16 {
return Err(HsmError::MechanismParamInvalid);
}
if iv.iter().all(|&b| b == 0) {
tracing::error!("AES-CTR rejected: all-zero IV/nonce is not permitted");
return Err(HsmError::MechanismParamInvalid);
}
if !matches!(key.len(), 16 | 24 | 32) {
return Err(HsmError::KeySizeRange);
}
Ok(())
}
fn aes_ctr_apply(key: &[u8], iv: &[u8], data: &[u8]) -> HsmResult<Vec<u8>> {
use ctr::cipher::{KeyIvInit, StreamCipher};
let mut output = data.to_vec();
match key.len() {
16 => {
type Aes128Ctr = ctr::Ctr128BE<aes::Aes128>;
let mut cipher =
Aes128Ctr::new_from_slices(key, iv).map_err(|_| HsmError::KeySizeRange)?;
cipher.apply_keystream(&mut output);
}
24 => {
type Aes192Ctr = ctr::Ctr128BE<aes::Aes192>;
let mut cipher =
Aes192Ctr::new_from_slices(key, iv).map_err(|_| HsmError::KeySizeRange)?;
cipher.apply_keystream(&mut output);
}
32 => {
type Aes256Ctr = ctr::Ctr128BE<aes::Aes256>;
let mut cipher =
Aes256Ctr::new_from_slices(key, iv).map_err(|_| HsmError::KeySizeRange)?;
cipher.apply_keystream(&mut output);
}
_ => return Err(HsmError::KeySizeRange),
}
Ok(output)
}
#[cfg(test)]
mod tests {
use super::*;
const CBC_FLOOR_MS: u128 = 10;
const CBC_FLOOR_MIN_MS: u128 = 8;
#[test]
fn aes_cbc_decrypt_bad_iv_length_pays_timing_floor() {
let key = [0u8; 32];
let short_iv = [0u8; 8]; let ciphertext = [0u8; 16];
let start = std::time::Instant::now();
let result = aes_cbc_decrypt(&key, &short_iv, &ciphertext);
let elapsed_ms = start.elapsed().as_millis();
assert!(result.is_err(), "bad IV length must return an error");
assert!(
elapsed_ms >= CBC_FLOOR_MIN_MS,
"bad-IV-length path returned in {} ms; expected >= {} ms \
(timing-equalization floor is {} ms)",
elapsed_ms,
CBC_FLOOR_MIN_MS,
CBC_FLOOR_MS,
);
}
#[test]
fn aes_cbc_decrypt_bad_padding_pays_timing_floor() {
let key = [0u8; 32];
let iv = [1u8; 16]; let ciphertext = [0u8; 16];
let start = std::time::Instant::now();
let result = aes_cbc_decrypt(&key, &iv, &ciphertext);
let elapsed_ms = start.elapsed().as_millis();
assert!(result.is_err(), "bad padding must return an error");
assert!(
elapsed_ms >= CBC_FLOOR_MIN_MS,
"bad-padding path returned in {} ms; expected >= {} ms \
(timing-equalization floor is {} ms)",
elapsed_ms,
CBC_FLOOR_MIN_MS,
CBC_FLOOR_MS,
);
}
}