blob: 3e729ab34a60ef09938be895226b608e68286596 [file]
// Licensed under the Apache-2.0 license
// SPDX-License-Identifier: Apache-2.0
//! HACE AES (128/256, ECB/CBC, raw-key path).
//!
//! Reference behavior follows pinned Zephyr `aspeed_hace` (`cfe94dc`).
//! Correctness is checked with NIST AESAVS/CAVP KATs.
//!
//! Deltas:
//! - A1: borrow-arbitrated exclusivity via `&mut HaceDevice`
//! - A2: ciphertext is plain `CT` (IV is separate)
//! - A3: key/IV context bytes are zeroized after each op and on drop
//! - A4: invalid block sizing returns `InvalidInput` before programming HW
//! - A5: AES-192 and OTP/secret-vault keys are out of scope
use super::constants::{
AES_CMD_BASE, HACE_CMD_AES128, HACE_CMD_AES256, HACE_CMD_CBC, HACE_CMD_ECB, HACE_CMD_ENCRYPT,
HACE_SG_LAST, POLL_YIELD_NS,
};
use super::context::CryptoContext;
use super::device::HaceDevice;
use super::error::HaceError;
use super::helpers::ptr_to_u32;
use super::registers::HaceRegisters;
use core::marker::PhantomData;
use openprot_hal_blocking::cipher::{
BlockCipherMode, CipherInit, CipherMode, CipherOp, ErrorType, SymmetricCipher,
};
/// AES block size in bytes. ECB/CBC input must be block-aligned.
pub const AES_BLOCK: usize = 16;
/// Borrow-arbitrated AES operation over HACE.
///
/// Created via [`AesCipher::from_device`]. The retained `&mut` borrow prevents
/// overlapping AES/digest/HMAC operations.
pub struct AesCipher<'a> {
regs: HaceRegisters,
ctx: &'a mut CryptoContext,
poll_budget: u32,
/// Cooperative yield hook called once per completion poll.
yield_fn: &'a mut dyn FnMut(u32),
/// Cache flush hook: invalidates stale CPU cache lines after HACE DMA.
/// Injected from [`HaceDevice`] so this module has no direct SCU dependency.
cache_flush: fn(),
}
impl<'a> AesCipher<'a> {
pub(crate) fn new(
regs: HaceRegisters,
ctx: &'a mut CryptoContext,
poll_budget: u32,
yield_fn: &'a mut dyn FnMut(u32),
cache_flush: fn(),
) -> Self {
Self {
regs,
ctx,
poll_budget,
yield_fn,
cache_flush,
}
}
/// Build an AES adapter from a [`HaceDevice`](super::device::HaceDevice).
///
/// # Safety
/// No concurrent or reentrant HACE access for the returned lifetime.
pub unsafe fn from_device<Y: FnMut(u32)>(device: &'a mut super::device::HaceDevice<Y>) -> Self {
// Borrow split; retained `yield_fn` keeps the device exclusively borrowed.
let regs = device.regs;
let poll_budget = device.poll_budget;
let cache_flush = device.cache_flush;
// SAFETY: single-instance device + exclusive live borrow gate access.
let ctx: &'a mut CryptoContext = unsafe { &mut *device.crypto_ctx };
let yield_fn: &'a mut dyn FnMut(u32) = &mut device.yield_fn;
Self::new(regs, ctx, poll_budget, yield_fn, cache_flush)
}
/// Map AES key length to command bits. Reject AES-192.
fn keylen_bits(key: &[u8]) -> Result<u32, HaceError> {
match key.len() {
16 => Ok(HACE_CMD_AES128),
32 => Ok(HACE_CMD_AES256),
_ => Err(HaceError::InvalidInput),
}
}
/// One-shot AES transform.
///
/// `mode_bits` is ECB or CBC. `iv` is set only for CBC. Buffers are plain
/// data (no in-band IV). Context key/IV bytes are always zeroized.
fn crypt(
&mut self,
mode_bits: u32,
encrypt: bool,
key: &[u8],
iv: Option<&[u8; AES_BLOCK]>,
input: &[u8],
output: &mut [u8],
) -> Result<(), HaceError> {
// Enforce block-aligned sizing before programming.
if input.is_empty() || input.len() % AES_BLOCK != 0 || output.len() < input.len() {
return Err(HaceError::InvalidInput);
}
let kbits = Self::keylen_bits(key)?;
let len = u32::try_from(input.len()).map_err(|_| HaceError::InvalidInput)?;
// Engine context: IV at [0..16) for IV modes, key at [16..16+keylen).
self.ctx.ctx = [0u8; 64];
if let Some(iv) = iv {
if let Some(dst) = self.ctx.ctx.get_mut(..AES_BLOCK) {
if let Ok(dst) = <&mut [u8; AES_BLOCK]>::try_from(dst) {
*dst = *iv;
}
}
}
if let Some(dst) = self.ctx.ctx.get_mut(AES_BLOCK..AES_BLOCK + key.len()) {
dst.copy_from_slice(key);
}
// Point SG descriptors directly at the caller's SRAM buffers.
//
// The AST10x0 crypto MBUS reads and writes payload data from ordinary
// cacheable SRAM (below 0x000A0000). Staging through the `.ram_nc`
// window (0x000A0000+) works for the engine context and SG descriptors
// but not for source/destination payload data: putting payloads in
// `.ram_nc` causes the engine to fire the completion intflag without
// writing any output (observed on hardware). Callers must therefore
// supply buffers in ordinary SRAM (e.g. `static mut` or heap); the
// KAT uses `static mut AES_IN / AES_OUT` which are in `.bss` / regular
// SRAM and work correctly.
//
// After the engine completes, `dcache_invd_all()` is called to
// invalidate any stale cache lines over the output buffer so the CPU
// reads the engine-written data rather than pre-op cached zeros
// (authority: `hace_aspeed.c` calls `cache_data_invd_all()` after
// every crypto op; `aspeed-rust` does the same).
let in_ptr = ptr_to_u32(input.as_ptr())?;
let out_ptr = ptr_to_u32(output.as_mut_ptr())?;
self.ctx.src.addr = in_ptr;
self.ctx.src.len = len | HACE_SG_LAST;
self.ctx.dst.addr = out_ptr;
self.ctx.dst.len = len | HACE_SG_LAST;
let cmd = AES_CMD_BASE | kbits | mode_bits | if encrypt { HACE_CMD_ENCRYPT } else { 0 };
let src_desc = ptr_to_u32(core::ptr::addr_of!(self.ctx.src))?;
let dst_desc = ptr_to_u32(core::ptr::addr_of!(self.ctx.dst))?;
let ctx_base = ptr_to_u32(self.ctx.ctx.as_ptr())?;
let data_len = len;
self.regs.clear_crypto_intflag();
self.regs
.program_crypto_operation(src_desc, dst_desc, ctx_base, data_len, cmd);
let mut done = false;
for _ in 0..self.poll_budget {
if self.regs.crypto_intflag_is_set() {
done = true;
break;
}
(self.yield_fn)(POLL_YIELD_NS);
}
// Invalidate the data cache so the CPU reads what the engine wrote.
(self.cache_flush)();
// Always clear key/IV material from the DMA context buffer.
self.ctx.ctx = [0u8; 64];
if done {
Ok(())
} else {
pw_log::error!(
"hace: AES timeout: HACE1C={:#010x}, cmd={:#010x}, len={}",
self.regs.read_hace1c() as u32,
cmd as u32,
len as u32,
);
Err(HaceError::Timeout)
}
}
/// AES-ECB encrypt.
///
/// `key` is 16 or 32 bytes. `pt` must be non-empty and block-aligned.
/// `ct` must be at least `pt.len()`. Output is ciphertext only.
pub fn ecb_encrypt(&mut self, key: &[u8], pt: &[u8], ct: &mut [u8]) -> Result<(), HaceError> {
self.crypt(HACE_CMD_ECB, true, key, None, pt, ct)
}
/// AES-ECB decrypt (inverse of [`ecb_encrypt`](Self::ecb_encrypt)).
pub fn ecb_decrypt(&mut self, key: &[u8], ct: &[u8], pt: &mut [u8]) -> Result<(), HaceError> {
self.crypt(HACE_CMD_ECB, false, key, None, ct, pt)
}
/// AES-CBC encrypt with a 16-byte IV. Output excludes IV prefix.
pub fn cbc_encrypt(
&mut self,
key: &[u8],
iv: &[u8; AES_BLOCK],
pt: &[u8],
ct: &mut [u8],
) -> Result<(), HaceError> {
self.crypt(HACE_CMD_CBC, true, key, Some(iv), pt, ct)
}
/// AES-CBC decrypt with a 16-byte IV; input excludes IV prefix.
pub fn cbc_decrypt(
&mut self,
key: &[u8],
iv: &[u8; AES_BLOCK],
ct: &[u8],
pt: &mut [u8],
) -> Result<(), HaceError> {
self.crypt(HACE_CMD_CBC, false, key, Some(iv), ct, pt)
}
}
impl Drop for AesCipher<'_> {
fn drop(&mut self) {
// Defensive scrub on drop.
self.ctx.ctx = [0u8; 64];
}
}
// ===== Optional openprot cipher-trait skin (ADR-A1) =====================
//
// Thin fixed-`N` wrapper over `AesCipher`. Kept separate because
// `SymmetricCipher` uses fixed associated buffer types and cannot express
// large streaming DMA paths.
/// AES-ECB mode marker (port-defined; the hal declares no concrete modes).
#[derive(Debug, Clone, Copy)]
pub struct Ecb;
/// AES-CBC mode marker.
#[derive(Debug, Clone, Copy)]
pub struct Cbc;
impl CipherMode for Ecb {}
impl BlockCipherMode for Ecb {}
impl CipherMode for Cbc {}
impl BlockCipherMode for Cbc {}
/// Owned AES key for the trait skin (raw-key path only).
///
/// Size selects variant: 16 => AES-128, 32 => AES-256.
///
/// The key bytes are zeroized when this value is dropped.
#[derive(Clone)]
pub enum AesKey {
Aes128([u8; 16]),
Aes256([u8; 32]),
}
impl AesKey {
#[inline]
fn as_slice(&self) -> &[u8] {
match self {
AesKey::Aes128(k) => k,
AesKey::Aes256(k) => k,
}
}
fn zeroize(&mut self) {
match self {
AesKey::Aes128(k) => k.fill(0),
AesKey::Aes256(k) => k.fill(0),
}
}
}
impl Drop for AesKey {
fn drop(&mut self) {
self.zeroize();
}
}
/// Fixed-`N` openprot cipher-trait skin bound to one [`HaceDevice`].
///
/// Each context wraps borrow-arbitrated [`AesCipher`].
pub struct AesSkin<'d, Y: FnMut(u32), const N: usize> {
dev: &'d mut HaceDevice<Y>,
}
impl<'d, Y: FnMut(u32), const N: usize> AesSkin<'d, Y, N> {
/// Bind the cipher skin to the device.
///
/// # Safety
/// Same contract as [`AesCipher::from_device`]: no concurrent/reentrant
/// HACE access for this skin or derived contexts.
pub unsafe fn new(dev: &'d mut HaceDevice<Y>) -> Self {
Self { dev }
}
}
/// In-flight trait-skin op: core plus session key/IV.
pub struct AesOp<'a, const N: usize, M> {
core: AesCipher<'a>,
key: AesKey,
iv: [u8; AES_BLOCK],
_m: PhantomData<M>,
}
impl<'d, Y: FnMut(u32), const N: usize> ErrorType for AesSkin<'d, Y, N> {
type Error = HaceError;
}
impl<'d, Y: FnMut(u32), const N: usize> SymmetricCipher for AesSkin<'d, Y, N> {
type Key = AesKey;
type Nonce = [u8; AES_BLOCK];
type PlainText = [u8; N];
type CipherText = [u8; N];
}
impl<'a, const N: usize, M> ErrorType for AesOp<'a, N, M> {
type Error = HaceError;
}
impl<'a, const N: usize, M> SymmetricCipher for AesOp<'a, N, M> {
type Key = AesKey;
type Nonce = [u8; AES_BLOCK];
type PlainText = [u8; N];
type CipherText = [u8; N];
}
macro_rules! cipher_init {
($mode:ty) => {
impl<'d, Y: FnMut(u32), const N: usize> CipherInit<$mode> for AesSkin<'d, Y, N> {
type CipherContext<'a>
= AesOp<'a, N, $mode>
where
Self: 'a;
fn init<'a>(
&'a mut self,
key: &Self::Key,
nonce: &Self::Nonce,
) -> Result<Self::CipherContext<'a>, Self::Error> {
// SAFETY: `AesSkin::new` guarantees non-reentrancy; reborrow is exclusive.
let core = unsafe { AesCipher::from_device(&mut *self.dev) };
Ok(AesOp {
core,
key: key.clone(),
iv: *nonce,
_m: PhantomData,
})
}
}
};
}
cipher_init!(Ecb);
cipher_init!(Cbc);
impl<'a, const N: usize> CipherOp<Ecb> for AesOp<'a, N, Ecb> {
fn encrypt(&mut self, plaintext: [u8; N]) -> Result<[u8; N], HaceError> {
const { assert!(N % AES_BLOCK == 0, "AesSkin<N>: N must be a multiple of 16") };
let mut ct = [0u8; N];
self.core
.ecb_encrypt(self.key.as_slice(), &plaintext, &mut ct)?;
Ok(ct)
}
fn decrypt(&mut self, ciphertext: [u8; N]) -> Result<[u8; N], HaceError> {
const { assert!(N % AES_BLOCK == 0, "AesSkin<N>: N must be a multiple of 16") };
let mut pt = [0u8; N];
self.core
.ecb_decrypt(self.key.as_slice(), &ciphertext, &mut pt)?;
Ok(pt)
}
}
impl<'a, const N: usize> CipherOp<Cbc> for AesOp<'a, N, Cbc> {
fn encrypt(&mut self, plaintext: [u8; N]) -> Result<[u8; N], HaceError> {
const { assert!(N % AES_BLOCK == 0, "AesSkin<N>: N must be a multiple of 16") };
let mut ct = [0u8; N];
self.core
.cbc_encrypt(self.key.as_slice(), &self.iv, &plaintext, &mut ct)?;
// Advance IV to last ciphertext block so sequential encrypt() calls
// form a correct CBC chain instead of reusing the original IV.
if let Some(last) = ct.get(N - AES_BLOCK..) {
if let Ok(last) = <[u8; AES_BLOCK]>::try_from(last) {
self.iv = last;
}
}
Ok(ct)
}
fn decrypt(&mut self, ciphertext: [u8; N]) -> Result<[u8; N], HaceError> {
const { assert!(N % AES_BLOCK == 0, "AesSkin<N>: N must be a multiple of 16") };
let mut pt = [0u8; N];
self.core
.cbc_decrypt(self.key.as_slice(), &self.iv, &ciphertext, &mut pt)?;
// Advance IV to last ciphertext block (CBC decrypt chaining).
if let Some(last) = ciphertext.get(N - AES_BLOCK..) {
if let Ok(last) = <[u8; AES_BLOCK]>::try_from(last) {
self.iv = last;
}
}
Ok(pt)
}
}