Crypto work queue [AES] (#3880)

* Implement a generic work queue

* Implement AES work queue backend with software block modes

* Move tail after enqueueing

* Use NonNull more liberally

* Tweak the queue a bit

* Merge the AES-DMA test into the AES test suite

* Drop AesFlavour

* Document things

* Stop the driver when its handle is dropped

* Fix docs

* Address some correctness concerns
This commit is contained in:
Dániel Buga 2025-08-01 10:45:36 +02:00 committed by GitHub
parent 3c12be8d24
commit 3ec26ead9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1512 additions and 163 deletions

View File

@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `TrngSource` to manage random number generator entropy (#3829)
- On RISC-V you can opt-out of nested interrupts for an interrupt handler by using `new_not_nested` (#3875)
- A new default feature `exception-handler` was added (#3887)
- `AesBackend, AesContext`: Work-queue based AES driver (#3880)
### Changed
@ -32,7 +33,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Removed
- `Trng::new` (replaced by `Trng::try_new`) (#3829)
- `AesDma::write_block` has been removed. (#3882)
- `AesDma::{write_key, write_block}` have been removed. (#3880, #3882)
- `AesFlavour` trait and `AesX` structs have been removed. (#3880)
## [v1.0.0-rc.0] - 2025-07-16

View File

@ -0,0 +1,299 @@
//! Software implementations of the supported block cipher operating modes.
//!
//! These may be used by the Typical AES operating mode, as well as by the DMA-enabled driver where
//! the DMA does not support a particular operating mode in hardware.
use core::ptr::NonNull;
use super::{BLOCK_SIZE, CryptoBuffers};
/// Electronic codebook mode.
pub(super) struct Ecb {}
impl Ecb {
pub(super) fn encrypt_decrypt(
&mut self,
buffer: CryptoBuffers,
mut process_block: impl FnMut(NonNull<[u8]>, NonNull<[u8]>),
) {
buffer.for_data_chunks(BLOCK_SIZE, |input, output, len| {
process_block(
NonNull::slice_from_raw_parts(input, len),
NonNull::slice_from_raw_parts(output, len),
)
});
}
}
/// Cipher block chaining mode.
pub(super) struct Cbc {
pub(super) iv: [u8; BLOCK_SIZE],
}
impl Cbc {
pub(super) fn encrypt(
&mut self,
buffer: CryptoBuffers,
mut process_block: impl FnMut(NonNull<[u8]>, NonNull<[u8]>),
) {
let iv = NonNull::from(self.iv.as_mut());
buffer.for_data_chunks(BLOCK_SIZE, |plaintext, ciphertext, len| {
// Block input is feedback/IV mixed with plaintext. We can let this overwrite the IV
// because we'll overwrite this again with the ciphertext as the next IV.
xor_into(iv.cast(), plaintext, len);
// Block output is ciphertext directly
process_block(iv, NonNull::slice_from_raw_parts(ciphertext, len));
// Feed back the ciphertext for the next iteration.
copy(iv.cast(), ciphertext, len);
});
}
pub(super) fn decrypt(
&mut self,
buffer: CryptoBuffers,
mut process_block: impl FnMut(NonNull<[u8]>, NonNull<[u8]>),
) {
let iv = NonNull::from(self.iv.as_mut());
buffer.for_data_chunks(BLOCK_SIZE, |ciphertext, plaintext, len| {
// Block output is plaintext, mixed with IV
process_block(iv, NonNull::slice_from_raw_parts(plaintext, len));
xor_into(plaintext, iv.cast(), len);
// Next IV is the previous ciphertext
copy(iv.cast(), ciphertext, len);
});
}
}
/// Output feedback mode.
pub(super) struct Ofb {
pub(super) iv: [u8; BLOCK_SIZE],
pub(super) offset: usize,
}
impl Ofb {
pub(super) fn encrypt_decrypt(
&mut self,
buffer: CryptoBuffers,
mut process_block: impl FnMut(NonNull<[u8]>, NonNull<[u8]>),
) {
let mut offset = self.offset;
buffer.for_data_chunks(1, |plaintext, ciphertext, _| {
if offset == 0 {
// Out of bytes, generate next key.
let iv = NonNull::from(self.iv.as_mut());
process_block(iv, iv);
}
// Calculate ciphertext by mixing the key with the plaintext.
unsafe { ciphertext.write(plaintext.read() ^ self.iv[offset]) };
offset = (offset + 1) % BLOCK_SIZE;
});
self.offset = offset;
}
}
/// Counter mode.
pub(super) struct Ctr {
/// The nonce + counter. Note that the security of this relies on the nonce being random.
pub(super) nonce: [u8; BLOCK_SIZE],
/// The key produced by the block cipher.
pub(super) buffer: [u8; BLOCK_SIZE],
pub(super) offset: usize,
}
impl Ctr {
pub(super) fn encrypt_decrypt(
&mut self,
buffer: CryptoBuffers,
mut process_block: impl FnMut(NonNull<[u8]>, NonNull<[u8]>),
) {
fn increment(nonce: &mut [u8]) {
for byte in nonce.iter_mut().rev() {
*byte = byte.wrapping_add(1);
if *byte != 0 {
break;
}
}
}
let mut offset = self.offset;
buffer.for_data_chunks(1, |plaintext, ciphertext, _| {
if offset == 0 {
let nonce = NonNull::from(self.nonce.as_mut());
let buffer = NonNull::from(self.buffer.as_mut());
// Block input is feedback/IV. Block output is feedback/IV.
process_block(nonce, buffer);
increment(&mut self.nonce);
}
// Calculate ciphertext by mixing the key with the plaintext.
unsafe { ciphertext.write(plaintext.read() ^ self.buffer[offset]) };
offset = (offset + 1) % BLOCK_SIZE;
});
self.offset = offset;
}
}
/// Cipher feedback with 8-bit shift mode.
pub(super) struct Cfb8 {
pub(super) iv: [u8; BLOCK_SIZE],
}
impl Cfb8 {
pub(super) fn encrypt(
&mut self,
buffer: CryptoBuffers,
mut process_block: impl FnMut(NonNull<[u8]>, NonNull<[u8]>),
) {
let mut ov = [0; BLOCK_SIZE];
buffer.for_data_chunks(1, |plaintext, ciphertext, _| {
ov.copy_from_slice(&self.iv);
let iv = NonNull::from(self.iv.as_mut());
process_block(iv, iv);
unsafe {
let out = self.iv[0] ^ plaintext.read();
ciphertext.write(out);
// Shift the key by a byte.
self.iv[0..BLOCK_SIZE - 1].copy_from_slice(&ov[1..BLOCK_SIZE]);
self.iv[BLOCK_SIZE - 1] = ciphertext.read();
}
});
}
pub(super) fn decrypt(
&mut self,
buffer: CryptoBuffers,
mut process_block: impl FnMut(NonNull<[u8]>, NonNull<[u8]>),
) {
let mut ov = [0; BLOCK_SIZE];
buffer.for_data_chunks(1, |ciphertext, plaintext, _| {
ov.copy_from_slice(&self.iv);
let iv = NonNull::from(self.iv.as_mut());
process_block(iv, iv);
unsafe {
let c = ciphertext.read();
let out = self.iv[0] ^ c;
plaintext.write(out);
// Shift the key by a byte.
self.iv[0..BLOCK_SIZE - 1].copy_from_slice(&ov[1..BLOCK_SIZE]);
self.iv[BLOCK_SIZE - 1] = c;
}
});
}
}
/// Cipher feedback with 128-bit shift mode.
pub(super) struct Cfb128 {
pub(super) iv: [u8; BLOCK_SIZE],
pub(super) offset: usize,
}
impl Cfb128 {
pub(super) fn encrypt(
&mut self,
buffer: CryptoBuffers,
mut process_block: impl FnMut(NonNull<[u8]>, NonNull<[u8]>),
) {
let mut offset = self.offset;
buffer.for_data_chunks(1, |plaintext, ciphertext, _| {
if offset == 0 {
let iv = NonNull::from(self.iv.as_mut());
process_block(iv, iv);
}
unsafe {
self.iv[offset] ^= plaintext.read();
ciphertext.write(self.iv[offset]);
}
offset = (offset + 1) % BLOCK_SIZE;
});
self.offset = offset;
}
pub(super) fn decrypt(
&mut self,
buffer: CryptoBuffers,
mut process_block: impl FnMut(NonNull<[u8]>, NonNull<[u8]>),
) {
let mut offset = self.offset;
buffer.for_data_chunks(1, |ciphertext, plaintext, _| {
if offset == 0 {
let iv = NonNull::from(self.iv.as_mut());
process_block(iv, iv);
}
unsafe {
let c = ciphertext.read();
plaintext.write(self.iv[offset] ^ c);
self.iv[offset] = c;
}
offset = (offset + 1) % BLOCK_SIZE;
});
self.offset = offset;
}
}
// Utilities
impl CryptoBuffers {
fn for_data_chunks(
self,
chunk_size: usize,
mut cb: impl FnMut(NonNull<u8>, NonNull<u8>, usize),
) {
let input = pointer_chunks(self.input, chunk_size);
let output = pointer_chunks(self.output, chunk_size);
for (input, output, len) in input
.zip(output)
.map(|((input, len), (output, _))| (input, output, len))
{
cb(input, output, len)
}
}
}
fn pointer_chunks<T>(
ptr: NonNull<[T]>,
chunk: usize,
) -> impl Iterator<Item = (NonNull<T>, usize)> + Clone {
let mut len = ptr.len();
let mut ptr = ptr.cast::<T>();
core::iter::from_fn(move || {
let advance = if len > chunk {
chunk
} else if len > 0 {
len
} else {
return None;
};
let retval = (ptr, advance);
unsafe { ptr = ptr.add(advance) };
len -= advance;
Some(retval)
})
}
fn xor_into(mut out: NonNull<u8>, mut a: NonNull<u8>, len: usize) {
let end = unsafe { out.add(len) };
while out < end {
unsafe {
out.write(out.read() ^ a.read());
a = a.add(1);
out = out.add(1);
}
}
}
fn copy(out: NonNull<u8>, from: NonNull<u8>, len: usize) {
unsafe {
out.copy_from(from, len);
}
}

View File

@ -43,16 +43,13 @@
//! // The decryption happens in-place, so the plaintext is in `block`
//! # {after_snippet}
//! ```
//!
//! ### AES-DMA
//!
//! Visit the [AES-DMA] test for a more advanced example of using AES-DMA
//! mode.
//!
//! [AES-DMA]: https://github.com/esp-rs/esp-hal/blob/main/hil-test/tests/aes_dma.rs
use core::ptr::NonNull;
use crate::{pac, peripherals::AES, system::GenericPeripheralGuard};
mod cipher_modes;
for_each_aes_key_length! {
($len:literal) => {
// Implementing From for easy conversion from array to Key enum.
@ -63,16 +60,6 @@ for_each_aes_key_length! {
}
}
}
paste::paste! {
#[doc = concat!("Marker type for AES-", stringify!($len))]
pub struct [<Aes $len>];
impl crate::private::Sealed for [<Aes $len>] {}
impl AesFlavour for [<Aes $len>] {
type KeyType<'b> = &'b [u8; $len / 8];
}
}
};
(bits $( ($len:literal) ),*) => {
@ -264,19 +251,70 @@ impl<'d> Aes<'d> {
let mode = key.decrypt_mode();
self.process(block, mode, key)
}
/// Encrypts/Decrypts the given buffer based on `mode` parameter
fn process_work_item(&mut self, work_item: &mut AesOperation) {
// Note that we can't just create slices out of the input and output buffers, because they
// may alias (when encrypting/decrypting data in place).
let slice = work_item.key.as_slice();
self.write_key(slice);
unsafe {
match work_item.cipher_mode.as_ref() {
CipherModeState::Ecb(_) | CipherModeState::Cbc(_) => {
self.write_mode(if work_item.mode == Operation::Encrypt {
work_item.key.encrypt_mode()
} else {
work_item.key.decrypt_mode()
})
}
CipherModeState::Ofb(_)
| CipherModeState::Ctr(_)
| CipherModeState::Cfb8(_)
| CipherModeState::Cfb128(_) => self.write_mode(work_item.key.encrypt_mode()),
}
}
let process_block = |input: NonNull<[u8]>, mut output: NonNull<[u8]>| {
unsafe { self.write_block(input.as_ref()) };
self.start();
while !self.is_idle() {}
unsafe { self.read_block(output.as_mut()) };
};
// Safety: the reference to the algorithm state is only held for the duration of the
// operation.
match unsafe { work_item.cipher_mode.as_mut() } {
CipherModeState::Ecb(algo) => algo.encrypt_decrypt(work_item.buffers, process_block),
CipherModeState::Cbc(algo) => {
if work_item.mode == Operation::Encrypt {
algo.encrypt(work_item.buffers, process_block);
} else {
algo.decrypt(work_item.buffers, process_block);
}
}
CipherModeState::Ofb(algo) => algo.encrypt_decrypt(work_item.buffers, process_block),
CipherModeState::Ctr(algo) => algo.encrypt_decrypt(work_item.buffers, process_block),
CipherModeState::Cfb8(algo) => {
if work_item.mode == Operation::Encrypt {
algo.encrypt(work_item.buffers, process_block)
} else {
algo.decrypt(work_item.buffers, process_block)
}
}
CipherModeState::Cfb128(algo) => {
if work_item.mode == Operation::Encrypt {
algo.encrypt(work_item.buffers, process_block)
} else {
algo.decrypt(work_item.buffers, process_block)
}
}
}
}
}
/// Specifications for AES flavours
pub trait AesFlavour: crate::private::Sealed {
/// Type of the AES key, a fixed-size array of bytes
///
/// The size of this type depends on various factors, such as the device
/// being targeted and the desired key size.
type KeyType<'b>;
}
/// State matrix endianness
#[cfg(any(esp32, esp32s2))]
/// Data endianness
#[cfg(aes_endianness_configurable)]
pub enum Endianness {
/// Big endian (most-significant byte at the smallest address)
BigEndian = 1,
@ -309,8 +347,6 @@ pub mod dma {
system::{Peripheral, PeripheralClockControl},
};
const ALIGN_SIZE: usize = core::mem::size_of::<u32>();
/// Specifies the block cipher modes available for AES operations.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CipherMode {
@ -343,7 +379,7 @@ pub mod dma {
channel: Channel<Blocking, PeripheralDmaChannel<AES<'d>>>,
}
impl<'d> crate::aes::Aes<'d> {
impl<'d> super::Aes<'d> {
/// Enable DMA for the current instance of the AES driver
pub fn with_dma(self, channel: impl DmaChannelFor<AES<'d>>) -> AesDma<'d> {
let channel = Channel::new(channel.degrade());
@ -359,13 +395,9 @@ pub mod dma {
}
impl<'d> AesDma<'d> {
/// Writes the encryption key to the AES hardware, checking that its
/// length matches expected constraints.
pub fn write_key(&mut self, key: impl Into<Key>) {
let key = key.into(); // Convert into Key enum
fn write_key(&mut self, key: impl Into<Key>) {
let key = key.into();
let key = key.as_slice();
debug_assert!(key.len() <= 8 * ALIGN_SIZE);
debug_assert_eq!(key.len() % ALIGN_SIZE, 0);
self.aes.write_key(key);
}
@ -562,6 +594,497 @@ pub mod dma {
}
}
use crate::work_queue::{
Handle,
Poll,
Status,
VTable,
WorkQueue,
WorkQueueDriver,
WorkQueueFrontend,
};
/// Specifies the block cipher modes available for AES operations.
///
/// In DMA-driven mode, if a particular mode is not supported by the hardware, the driver will fall
/// back to a CPU-driven method.
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CipherMode {
/// Electronic Codebook Mode
Ecb,
/// Cipher Block Chaining Mode
Cbc,
/// Output Feedback Mode
Ofb,
/// Counter Mode
Ctr,
/// Cipher Feedback Mode with 8-bit shifting.
Cfb8,
/// Cipher Feedback Mode with 128-bit shifting.
Cfb128,
/// Galois Counter Mode
Gcm,
}
#[derive(Clone, Copy)]
struct CryptoBuffers {
input: NonNull<[u8]>,
output: NonNull<[u8]>,
text_length: usize,
}
enum CipherModeState {
/// Electronic Codebook Mode
// TODO: for this, we need text_length to be a muliple of BLOCK_SIZE
Ecb(cipher_modes::Ecb),
/// Cipher Block Chaining Mode
// TODO: for this, we need text_length to be a muliple of BLOCK_SIZE
Cbc(cipher_modes::Cbc),
/// Output Feedback Mode
Ofb(cipher_modes::Ofb),
/// Counter Mode
Ctr(cipher_modes::Ctr),
/// Cipher Feedback Mode with 8-bit shifting.
Cfb8(cipher_modes::Cfb8),
/// Cipher Feedback Mode with 128-bit shifting.
Cfb128(cipher_modes::Cfb128),
// Galois Counter Mode
// Gcm(*mut Gcm), // TODO: this is more involved
}
const BLOCK_SIZE: usize = 16;
struct AesOperation {
mode: Operation,
// The block cipher operating mode.
cipher_mode: NonNull<CipherModeState>,
// The length of the key is determined by the operating mode.
buffers: CryptoBuffers,
key: Key,
}
// Safety: AesOperation is safe to share between threads, in the context of a WorkQueue. The
// WorkQueue ensures that only a single location can access the data. All the internals, except
// for the pointers, are Sync. The pointers are safe to share because they point at data that the
// AES driver ensures can be accessed safely and soundly.
unsafe impl Sync for AesOperation {}
static AES_WORK_QUEUE: WorkQueue<AesOperation> = WorkQueue::new();
const BLOCKING_AES_VTABLE: VTable<AesOperation> = VTable {
post: |driver, _item| {
// Since we run the operation in `poll`, there is nothing to do here, besides accepting the
// work item. However, DS may be using the AES peripheral. We solve that problem (later) by
// taking a lock in `on_work_available`
let driver = unsafe { AesBackend::from_raw(driver) };
driver.ensure_initialized()
},
poll: |driver, item| {
// Fully process the work item. A single CPU-driven AES operation would complete
// faster than we could poll the queue with all the locking around it.
let driver = unsafe { AesBackend::from_raw(driver) };
driver.process(item)
},
cancel: |_driver, _item| {
// To achieve a decent performance in Typical AES mode, we run the operations in a blocking
// manner and so they can't be cancelled.
},
stop: |driver| {
// Drop the AES driver to conserve power when there is nothig to do (or when the driver was
// stopped).
let driver = unsafe { AesBackend::from_raw(driver) };
driver.deinitialize()
},
};
#[procmacros::doc_replace]
/// CPU-driven AES processing backend.
///
/// Due to how this backend works, it provides no `run` method(s). Posting work to this backend will
/// immediately be executed, in a blocking way. The backend only needs to be kept alive in order to
/// function.
///
/// ## Example
///
/// ```rust, no_run
/// # {before_snippet}
/// use esp_hal::aes::{AesBackend, AesContext, Operation};
/// #
/// let mut aes = AesBackend::new(peripherals.AES);
/// // Start the backend, which allows processing AES operations.
/// let _backend = aes.start();
///
/// // Create a new context with a 128-bit key. The context will use the ECB block cipher mode.
/// // The key length must be supported by the hardware.
/// let mut ecb_encrypt = AesContext::ecb(
/// Operation::Encrypt,
/// [
/// b'S', b'U', b'p', b'4', b'S', b'e', b'C', b'p', b'@', b's', b'S', b'w', b'0', b'r',
/// b'd', 0,
/// ],
/// );
///
/// // Process a block of data in this context. The ECB operating mode requires that
/// // the length of the data is a multiple of the block (16 bytes) size.
/// let input_buffer: [u8; 16] = [
/// b'm', b'e', b's', b's', b'a', b'g', b'e', 0, 0, 0, 0, 0, 0, 0, 0, 0,
/// ];
/// let mut output_buffer: [u8; 16] = [0; 16];
///
/// let operation_handle = ecb_encrypt
/// .process(&input_buffer, &mut output_buffer)
/// .unwrap();
/// operation_handle.wait_blocking();
///
/// // output_buffer now contains the ciphertext
/// assert_eq!(
/// output_buffer,
/// [
/// 0xb3, 0xc8, 0xd2, 0x3b, 0xa7, 0x36, 0x5f, 0x18, 0x61, 0x70, 0x0, 0x3e, 0xd9, 0x3a,
/// 0x31, 0x96,
/// ]
/// );
/// #
/// # {after_snippet}
/// ```
pub struct AesBackend<'d> {
peri: AES<'d>,
driver: Option<Aes<'d>>,
}
impl<'d> AesBackend<'d> {
#[procmacros::doc_replace]
/// Creates a new AES backend.
///
/// The backend needs to be [`start`][Self::start]ed before it can execute AES operations.
///
/// ## Example
///
/// ```rust, no_run
/// # {before_snippet}
/// use esp_hal::aes::AesBackend;
/// #
/// let mut aes = AesBackend::new(peripherals.AES);
/// # {after_snippet}
/// ```
pub fn new(aes: AES<'d>) -> Self {
Self {
peri: aes,
driver: None,
}
}
#[procmacros::doc_replace]
/// Registers the CPU-driven AES driver to process AES operations.
///
/// The driver stops operating when the returned object is dropped.
///
/// ## Example
///
/// ```rust, no_run
/// # {before_snippet}
/// use esp_hal::aes::AesBackend;
/// #
/// let mut aes = AesBackend::new(peripherals.AES);
/// // Start the backend, which allows processing AES operations.
/// let _backend = aes.start();
/// # {after_snippet}
/// ```
pub fn start(&mut self) -> AesWorkQueueDriver<'_, 'd> {
AesWorkQueueDriver {
_inner: WorkQueueDriver::new(self, BLOCKING_AES_VTABLE, &AES_WORK_QUEUE),
}
}
// WorkQueue callbacks. They may run in any context.
unsafe fn from_raw<'any>(ptr: *const ()) -> &'any mut Self {
unsafe { unwrap!(ptr.cast_mut().cast::<AesBackend<'_>>().as_mut()) }
}
fn process(&mut self, item: &mut AesOperation) -> Option<Poll> {
let driver = self.driver.as_mut()?;
driver.process_work_item(item);
Some(Poll::Ready(Status::Completed))
}
fn ensure_initialized(&mut self) -> bool {
if self.driver.is_none() {
let peri = unsafe { self.peri.clone_unchecked() };
self.driver = Some(Aes::new(peri));
}
// TODO: when DS is implemented, it will need to be able to lock out AES. In that case,
// this function should return `false` if the AES peripheral is locked.
true
}
fn deinitialize(&mut self) {
self.driver = None;
}
}
/// An error related to an AES operation.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
#[non_exhaustive]
pub enum Error {
/// The input and output buffers have different lengths.
BuffersNotEqual,
/// The buffer length is not appropriate for the current cipher mode.
IncorrectBufferLength,
}
/// An active work queue driver.
///
/// This object must be kept around, otherwise AES operations will never complete.
pub struct AesWorkQueueDriver<'t, 'd> {
_inner: WorkQueueDriver<'t, AesBackend<'d>, AesOperation>,
}
/// An AES work queue user.
pub struct AesContext {
cipher_mode: CipherModeState,
frontend: WorkQueueFrontend<AesOperation>,
}
impl AesContext {
fn new(cipher_mode: CipherModeState, operation: Operation, key: Key) -> Self {
Self {
cipher_mode,
frontend: WorkQueueFrontend::new(AesOperation {
mode: operation,
cipher_mode: NonNull::dangling(),
buffers: CryptoBuffers {
input: NonNull::from(&[]),
output: NonNull::from(&[]),
text_length: 0,
},
key,
}),
}
}
/// Creates a new context to encrypt or decrypt data with the Electronic Codebook operating
/// mode.
pub fn ecb(operation: Operation, key: impl Into<Key>) -> Self {
Self::new(
CipherModeState::Ecb(cipher_modes::Ecb {}),
operation,
key.into(),
)
}
/// Creates a new context to encrypt or decrypt data with the Cipher Block Chaining operating
/// mode.
pub fn cbc(operation: Operation, iv: [u8; BLOCK_SIZE], key: impl Into<Key>) -> Self {
Self::new(
CipherModeState::Cbc(cipher_modes::Cbc { iv }),
operation,
key.into(),
)
}
/// Creates a new context to encrypt or decrypt data with the Counter operating
/// mode.
pub fn ctr(operation: Operation, key: impl Into<Key>, nonce: [u8; BLOCK_SIZE]) -> Self {
Self::new(
CipherModeState::Ctr(cipher_modes::Ctr {
nonce,
buffer: [0; BLOCK_SIZE],
offset: 0,
}),
operation,
key.into(),
)
}
/// Creates a new context to encrypt or decrypt data with the Cipher feedback (with 8-bit
/// shifting) operating mode.
pub fn cfb8(operation: Operation, key: impl Into<Key>, iv: [u8; BLOCK_SIZE]) -> Self {
Self::new(
CipherModeState::Cfb8(cipher_modes::Cfb8 { iv }),
operation,
key.into(),
)
}
/// Creates a new context to encrypt or decrypt data with the Cipher feedback (with 8-bit
/// shifting) operating mode.
pub fn cfb128(operation: Operation, key: impl Into<Key>, iv: [u8; BLOCK_SIZE]) -> Self {
Self::new(
CipherModeState::Cfb128(cipher_modes::Cfb128 { iv, offset: 0 }),
operation,
key.into(),
)
}
/// Creates a new context to encrypt or decrypt data with the Output feedback operating mode.
pub fn ofb(operation: Operation, key: impl Into<Key>, iv: [u8; BLOCK_SIZE]) -> Self {
Self::new(
CipherModeState::Ofb(cipher_modes::Ofb { iv, offset: 0 }),
operation,
key.into(),
)
}
fn post(&mut self) -> AesHandle<'_> {
AesHandle(self.frontend.post(&AES_WORK_QUEUE))
}
fn validate(&self, buffer: &[u8]) -> Result<(), Error> {
// TODO: check that data is appropriate for the cipher mode
let requires_block_aligned = matches!(
self.cipher_mode,
CipherModeState::Ecb(_) | CipherModeState::Ofb(_)
);
if requires_block_aligned && !buffer.len().is_multiple_of(BLOCK_SIZE) {
return Err(Error::IncorrectBufferLength);
}
Ok(())
}
/// Starts transforming the input buffer, and writes the result into the output buffer.
///
/// - For encryption the input is the plaintext, the output is the ciphertext.
/// - For decryption the input is the ciphertext, the output is the plaintext.
///
/// The returned Handle must be polled until it returns [`Poll::Ready`]. Dropping the handle
/// before the operation finishes will cancel the operation.
///
/// For an example, see the documentation of [`AesBackend`].
///
/// ## Errors
///
/// - If the lengths of the input and output buffers don't match, an error is returned.
/// - The ECB and OFB cipher modes require the data length to be a multiple of the block size
/// (16), otherwise an error is returned.
pub fn process<'t>(
&'t mut self,
input: &'t [u8],
output: &'t mut [u8],
) -> Result<AesHandle<'t>, Error> {
if input.len() != output.len() {
return Err(Error::BuffersNotEqual);
}
self.validate(input)?;
self.validate(output)?;
let data = self.frontend.data_mut();
data.cipher_mode = NonNull::from(&mut self.cipher_mode);
data.buffers.text_length = input.len();
data.buffers.input = NonNull::from(input);
data.buffers.output = NonNull::from(output);
Ok(self.post())
}
#[procmacros::doc_replace]
/// Starts transforming the buffer.
///
/// The processed data will be written back to the `buffer`.
///
/// The returned Handle must be polled until it returns [`Poll::Ready`]. Dropping the handle
/// before the operation finishes will cancel the operation.
///
/// This function operates similar to [`AesContext::process`], but it overwrites the data buffer
/// with the result of the transformation.
///
/// ```rust, no_run
/// # {before_snippet}
/// use esp_hal::aes::{AesBackend, AesContext, Operation};
/// #
/// let mut aes = AesBackend::new(peripherals.AES);
/// // Start the backend, which pins it in place and allows processing AES operations.
/// let _backend = aes.start();
///
/// // Create a new context with a 128-bit key. The context will use the ECB block cipher mode.
/// // The key length must be supported by the hardware.
/// let mut ecb_encrypt = AesContext::ecb(
/// Operation::Encrypt,
/// [
/// b'S', b'U', b'p', b'4', b'S', b'e', b'C', b'p', b'@', b's', b'S', b'w', b'0', b'r',
/// b'd', 0,
/// ],
/// );
///
/// // Process a block of data in this context, in place. The ECB operating mode requires that
/// // the length of the data is a multiple of the block (16 bytes) size.
/// let mut buffer: [u8; 16] = [
/// b'm', b'e', b's', b's', b'a', b'g', b'e', 0, 0, 0, 0, 0, 0, 0, 0, 0,
/// ];
///
/// let operation_handle = ecb_encrypt.process_in_place(&mut buffer).unwrap();
/// operation_handle.wait_blocking();
///
/// // Instead of the plaintext message, buffer now contains the ciphertext.
/// assert_eq!(
/// buffer,
/// [
/// 0xb3, 0xc8, 0xd2, 0x3b, 0xa7, 0x36, 0x5f, 0x18, 0x61, 0x70, 0x0, 0x3e, 0xd9, 0x3a,
/// 0x31, 0x96,
/// ]
/// );
/// #
/// # {after_snippet}
/// ```
///
/// ## Errors
///
/// The ECB and OFB cipher modes require the data length to be a multiple of the block size
/// (16), otherwise an error is returned.
pub fn process_in_place<'t>(
&'t mut self,
buffer: &'t mut [u8],
) -> Result<AesHandle<'t>, Error> {
self.validate(buffer)?;
let data = self.frontend.data_mut();
data.cipher_mode = NonNull::from(&self.cipher_mode);
data.buffers.text_length = buffer.len();
let ptr = NonNull::from(buffer);
data.buffers.input = ptr;
data.buffers.output = ptr;
Ok(self.post())
}
}
/// The handle to the pending AES operation.
///
/// This object is returned by [`AesContext::process`] and [`AesContext::process_in_place`].
///
/// Dropping this handle before the operation finishes will cancel the operation.
///
/// For an example, see the documentation of [`AesBackend`].
pub struct AesHandle<'t>(Handle<'t, AesOperation>);
impl AesHandle<'_> {
/// Polls the status of the work item.
#[inline]
pub fn poll(&mut self) -> Poll {
self.0.poll()
}
/// Blocks until the work item to be processed.
#[inline]
pub fn wait_blocking(mut self) {
while self.poll() == Poll::Pending {}
}
/// Waits for the work item to be processed.
#[inline]
pub fn wait(&mut self) -> impl Future<Output = Status> {
self.0.wait()
}
}
// Utilities
fn read_words(slice: &[u8]) -> impl Iterator<Item = u32> {

View File

@ -333,6 +333,7 @@ unstable_module! {
#[cfg(psram)] // DMA needs some things from here
pub mod psram;
pub mod efuse;
pub mod work_queue;
}
unstable_driver! {

490
esp-hal/src/work_queue.rs Normal file
View File

@ -0,0 +1,490 @@
//! A generic work queue.
//!
//! Work queues are the backbone of cryptographic drivers. They enable asynchronous and
//! blocking operations using shared peripherals, like crypto accelerators. Clients
//! post work items into the work queue, and poll for completion. The act of polling
//! processes the queue, which allows any number of clients to post work without deadlocking.
//!
//! Work queues are configured by backends. Backends register a `process` callback which is
//! called when a client posts a work item or polls for completion.
//!
//! Posting a work item into the queue returns a handle. The handle can be used to poll whether
//! the work item has been processed. Dropping the handle will cancel the work item.
#![cfg_attr(esp32c2, allow(unused))]
use core::{future::poll_fn, marker::PhantomData, ptr::NonNull};
use crate::{asynch::AtomicWaker, sync::Locked};
/// Queue driver operations.
///
/// Functions in this VTable are provided by drivers that consume work items.
/// These functions may be called both in the context of the queue frontends and drivers.
pub(crate) struct VTable<T: Sync> {
/// Starts processing a new work item.
///
/// The function returns whether the work item was accepted. If there is no driver currently
/// processing the queue, this function will return false to prevent removing the work item
/// from the queue.
///
/// This function should be as short as possible.
pub(crate) post: fn(*const (), &mut T) -> bool,
/// Polls the status of a particular work item.
///
/// Return `None` if the work item is not currently being processed.
///
/// This function should be as short as possible.
pub(crate) poll: fn(*const (), &mut T) -> Option<Poll>,
/// Attempts to abort processing a work item.
///
/// This function should be as short as possible.
pub(crate) cancel: fn(*const (), &mut T),
/// Called when the driver may be stopped.
///
/// This function should be as short as possible.
pub(crate) stop: fn(*const ()),
}
impl<T: Sync> VTable<T> {
pub(crate) const fn noop() -> Self {
Self {
post: |_, _| false,
poll: |_, _| None,
cancel: |_, _| (),
stop: |_| (),
}
}
}
struct Inner<T: Sync> {
head: Option<NonNull<WorkItem<T>>>,
tail: Option<NonNull<WorkItem<T>>>,
current: Option<NonNull<WorkItem<T>>>,
// The data pointer will be passed to VTable functions, which may be called in any context.
data: *const (),
vtable: VTable<T>,
}
impl<T: Sync> Inner<T> {
/// Places a work item at the end of the queue.
fn enqueue(&mut self, ptr: NonNull<WorkItem<T>>) {
if let Some(tail) = self.tail.as_mut() {
// Queue contains something, append to `tail`.
unsafe { tail.as_mut().next = Some(ptr) };
} else {
// Queue was empty, set `head` to the first element.
self.head = Some(ptr);
}
// Move `tail` to the newly inserted item.
self.tail = Some(ptr);
}
/// Places a work item at the front of the queue.
fn enqueue_front(&mut self, mut ptr: NonNull<WorkItem<T>>) {
// Chain the node into the list.
unsafe { ptr.as_mut().next = self.head };
// Adjust list `head` to point at the new-first element.
self.head = Some(ptr);
if self.tail.is_none() {
// The queue was empty, we need to set `tail` to the last element.
self.tail = Some(ptr);
}
}
/// Runs one processing iteration.
///
/// This function enqueues a new work item or polls the status of the currently processed one.
fn process(&mut self) {
if let Some(mut current) = self.current {
let result = unsafe { (self.vtable.poll)(self.data, &mut current.as_mut().data) };
if let Some(Poll::Ready(status)) = result {
unsafe { current.as_mut().complete(status) };
self.current = None;
self.dequeue_and_post(true);
}
} else {
// If the queue is empty, the driver should already have been notified when the queue
// became empty, so we don't notify it here.
self.dequeue_and_post(false);
}
}
// Note: even if the queue itself may be implemented lock-free, dequeuing and posting to the
// driver must be done atomically to ensure that the queue can be processed fully by any of
// the frontends polling it.
fn dequeue_and_post(&mut self, notify_on_empty: bool) {
if let Some(mut ptr) = self.dequeue() {
// Start processing a new work item.
if unsafe { (self.vtable.post)(self.data, &mut ptr.as_mut().data) } {
self.current = Some(ptr);
} else {
// If the driver didn't accept the work item, place it back to the front of the
// queue.
self.enqueue_front(ptr);
}
} else if notify_on_empty {
// There are no more work items. Notify the driver that it can stop.
(self.vtable.stop)(self.data);
}
}
/// Pops and returns a work item from the start of the queue.
fn dequeue(&mut self) -> Option<NonNull<WorkItem<T>>> {
// If the `head` is None, the queue is empty. Return None and do nothing.
let ptr = self.head?;
unsafe { self.head = ptr.as_ref().next };
// If the new `head` is null, the queue is empty. Clear the `tail` pointer.
if self.head.is_none() {
self.tail = None;
}
Some(ptr)
}
/// Cancels a particular work item.
///
/// If the work item is currently being processed, this function notifies the driver. Otherwise,
/// it tries to remove the pointer from the work queue.
///
/// The function returns true when the item was immediately cancelled.
///
/// This function is not `unsafe` because it only dereferences `work_item` if the function has
/// determined that the item belongs to this queue.
fn cancel(&mut self, mut work_item: NonNull<WorkItem<T>>) -> bool {
if self.current == Some(work_item) {
// Cancelling an in-progress item is more complicated than plucking it from the
// queue. Forward the request to the driver to (maybe) cancel the
// operation.
(self.vtable.cancel)(self.data, unsafe {
// This is safe to do, because the work item is currently owned by this queue.
&mut work_item.as_mut().data
});
// Queue will need to be polled to query item status.
return false;
}
// The work item is not the current one, remove it from the queue. This immediately
// cancels the work item. `remove` only uses the address of the work item without
// dereferencing it.
if self.remove(work_item) {
unsafe { work_item.as_mut().complete(Status::Cancelled) };
// Cancelled immediately, no further polling necessary for this item.
return true;
}
// In this case the item doesn't belong to this queue, it can be in any state. The item will
// need to be polled, but this also means something may have gone wrong.
false
}
/// Removes the item from the queue.
///
/// Returns `true` if the work item was successfully removed, `false` if the work item was not
/// found in the queue.
///
/// This function is not `unsafe` because it does not dereference `ptr`, so it does not matter
/// that `ptr` may belong to a different work queue.
fn remove(&mut self, ptr: NonNull<WorkItem<T>>) -> bool {
// Walk the queue to find `ptr`.
let mut prev = None;
let mut current = self.head;
while let Some(current_item) = current {
let next = unsafe { current_item.as_ref().next };
if current_item != ptr {
// Not what we're looking for. Move to the next element.
prev = current;
current = next;
continue;
}
// We've found `ptr`. Remove it from the list.
if Some(ptr) == self.head {
self.head = next;
} else {
// Unwrapping is fine, because if the current pointer is not the `head`, the
// previous pointer must be Some.
unsafe { unwrap!(prev).as_mut().next = next };
}
if Some(ptr) == self.tail {
self.tail = prev;
}
return true;
}
// Did not find `ptr`.
false
}
}
/// A generic work queue.
pub(crate) struct WorkQueue<T: Sync> {
inner: Locked<Inner<T>>,
}
impl<T: Sync> WorkQueue<T> {
/// Creates a new `WorkQueue`.
pub const fn new() -> Self {
Self {
inner: Locked::new(Inner {
head: None,
tail: None,
current: None,
data: core::ptr::null(),
vtable: VTable::noop(),
}),
}
}
/// Configures the queue.
///
/// The provided data pointer will be passed to the VTable functions.
///
/// # Safety
///
/// The `data` pointer must be valid as long as the `WorkQueue` is configured with it. The
/// driver must access the data pointer appropriately (i.e. it must not move !Send data out of
/// it).
pub unsafe fn configure<D: Sync>(&self, data: *const D, vtable: VTable<T>) {
self.inner.with(|inner| {
(inner.vtable.stop)(inner.data);
inner.data = data.cast();
inner.vtable = vtable;
})
}
/// Enqueues a work item.
pub fn post_work<'t>(&'t self, work_item: &'t mut WorkItem<T>) -> Handle<'t, T> {
let ptr = unsafe {
// Safety: `Handle` and `work_item` have lifetime 't, which ensures this call
// can't be called on an in-flight work item. As `Handle` (and the underlying driver
// that processed the work queue) does not use the reference, and using the
// reference is not possible while `Handle` exists, this should be safe.
work_item.prepare()
};
self.inner.with(|inner| inner.enqueue(ptr));
Handle {
queue: self,
work_item: ptr,
_marker: PhantomData,
}
}
/// Polls the queue once.
pub fn process(&self) {
self.inner.with(|inner| inner.process());
}
/// Polls the queue once and returns the status of the given work item.
///
/// ## Safety
///
/// The caller must ensure that `item` belongs to the polled queue. An item belongs to the
/// **last queue it was enqueued in**, even if the item is no longer in the queue's linked
/// list. This relationship is broken when the Handle that owns the WorkItem is dropped.
pub unsafe fn poll(&self, item: NonNull<WorkItem<T>>) -> Poll {
self.inner.with(|inner| {
let status = unsafe { (*item.as_ptr()).status };
if status == Poll::Pending {
inner.process();
unsafe { (*item.as_ptr()).status }
} else {
status
}
})
}
/// Schedules the work item to be cancelled.
///
/// The function returns true when the item was immediately cancelled. If the function returns
/// `false`, the item will need to be polled until its status becomes [`Poll::Ready`].
///
/// The work item should not be assumed to be immediately cancelled. Polling its handle
/// is necessary to ensure it is no longer being processed by the underlying driver.
pub fn cancel(&self, work_item: NonNull<WorkItem<T>>) -> bool {
self.inner.with(|inner| inner.cancel(work_item))
}
}
/// The status of a work item.
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Status {
/// The processing has completed.
Completed,
/// The work item has been cancelled.
Cancelled,
}
/// A unit of work in the work queue.
pub(crate) struct WorkItem<T: Sync> {
next: Option<NonNull<WorkItem<T>>>,
status: Poll,
data: T,
waker: AtomicWaker,
}
impl<T: Sync> WorkItem<T> {
/// Completes the work item.
///
/// This function is intended to be called from the underlying drivers.
pub fn complete(&mut self, status: Status) {
self.status = Poll::Ready(status);
self.waker.wake();
}
/// Prepares a work item to be enqueued.
///
/// # Safety:
///
/// The caller must ensure the reference is not used again while the pointer returned by this
/// function is in use.
unsafe fn prepare(&mut self) -> NonNull<Self> {
self.next = None;
self.status = Poll::Pending;
NonNull::from(self)
}
}
/// The status of a work item posted to a work queue.
#[derive(Clone, Copy, PartialEq, Debug)]
#[cfg_attr(feature = "defmt", derive(defmt::Format))]
pub enum Poll {
/// The work item has not yet been fully processed.
Pending,
/// The work item has been processed.
Ready(Status),
}
/// A reference to a posted [`WorkItem`].
///
/// This struct ensures that the work item is valid until the item is processed or is removed from
/// the work queue.
///
/// Dropping the handle cancels the work item, but may block for some time if the work item is
/// already being processed.
pub(crate) struct Handle<'t, T: Sync> {
queue: &'t WorkQueue<T>,
work_item: NonNull<WorkItem<T>>,
// Make sure lifetime is invariant to prevent UB.
_marker: PhantomData<&'t mut WorkItem<T>>,
}
impl<'t, T: Sync> Handle<'t, T> {
/// Returns the status of the work item.
pub fn poll(&mut self) -> Poll {
unsafe { self.queue.poll(self.work_item) }
}
/// Waits for the work item to be processed.
pub fn wait(&mut self) -> impl Future<Output = Status> {
poll_fn(|ctx| {
unsafe { (*self.work_item.as_ptr()).waker.register(ctx.waker()) };
match self.poll() {
Poll::Pending => core::task::Poll::Pending,
Poll::Ready(status) => core::task::Poll::Ready(status),
}
})
}
}
impl<'t, T: Sync> Drop for Handle<'t, T> {
fn drop(&mut self) {
if self.queue.cancel(self.work_item) {
// We must wait for the driver to release our WorkItem.
while self.poll() == Poll::Pending {}
}
}
}
pub(crate) struct WorkQueueDriver<'t, D, T>
where
D: Sync,
T: Sync,
{
queue: &'t WorkQueue<T>,
_marker: PhantomData<&'t mut D>,
}
impl<'t, D, T> WorkQueueDriver<'t, D, T>
where
D: Sync,
T: Sync,
{
pub fn new(driver: &'t mut D, vtable: VTable<T>, queue: &'t WorkQueue<T>) -> Self {
unsafe {
// Safety: the lifetime 't ensures the pointer remains valid for the lifetime of the
// WorkQueueDriver. The Drop implementation (and the general "Don't forget" clause)
// ensure the pointer is not used after the WQD has been dropped.
queue.configure((driver as *mut D).cast_const(), vtable);
}
Self {
queue,
_marker: PhantomData,
}
}
#[expect(unused)] // TODO this will be used with AesDmaBackend, for example
pub fn poll(&mut self) {
self.queue.process();
}
}
impl<D, T> Drop for WorkQueueDriver<'_, D, T>
where
D: Sync,
T: Sync,
{
fn drop(&mut self) {
unsafe {
// Safety: the noop VTable functions don't use the pointer at all.
self.queue.configure(core::ptr::null::<D>(), VTable::noop())
};
}
}
/// Used by work queue clients, allows hiding WorkItem.
pub(crate) struct WorkQueueFrontend<T: Sync> {
work_item: WorkItem<T>,
}
impl<T: Sync> WorkQueueFrontend<T> {
pub fn new(initial: T) -> Self {
Self {
work_item: WorkItem {
next: None,
status: Poll::Pending,
data: initial,
waker: AtomicWaker::new(),
},
}
}
pub fn data_mut(&mut self) -> &mut T {
&mut self.work_item.data
}
pub fn post<'t>(&'t mut self, queue: &'t WorkQueue<T>) -> Handle<'t, T> {
queue.post_work(&mut self.work_item)
}
}

View File

@ -11,10 +11,6 @@ name = "hil_test"
name = "aes"
harness = false
[[test]]
name = "aes_dma"
harness = false
[[test]]
name = "alloc_psram"
harness = false

View File

@ -28,7 +28,7 @@ const fn pad_to<const K: usize>(input: &[u8]) -> [u8; K] {
mod tests {
use esp_hal::{
Config,
aes::{Aes, Key},
aes::{Aes, AesBackend, AesContext, Key, Operation},
clock::CpuClock,
};
use hil_test as _;
@ -36,6 +36,8 @@ mod tests {
use super::*;
const KEY: &[u8] = b"SUp4SeCp@sSw0rd";
const KEY_128: [u8; 16] = pad_to::<16>(KEY);
const KEY_256: [u8; 32] = pad_to::<32>(KEY);
const PLAINTEXT: &[u8] = b"message";
const PLAINTEXT_BUF: [u8; 16] = pad_to::<16>(PLAINTEXT);
@ -82,4 +84,160 @@ mod tests {
test_aes_block::<32>(&mut aes, PLAINTEXT_BUF, CIPHERTEXT_ECB_256);
}
#[test]
#[cfg(not(esp32))]
fn test_aes_dma_ecb() {
use esp_hal::{
aes::dma::{AesDma, CipherMode},
dma::{DmaRxBuf, DmaTxBuf},
dma_buffers,
};
fn test_aes_ecb<const K: usize>(
mut aes: AesDma<'_>,
plaintext: [u8; 16],
ciphertext: [u8; 16],
) -> AesDma<'_>
where
Key: From<[u8; K]>,
{
const DMA_BUFFER_SIZE: usize = 16;
let (output, rx_descriptors, input, tx_descriptors) = dma_buffers!(DMA_BUFFER_SIZE);
let mut output = DmaRxBuf::new(rx_descriptors, output).unwrap();
let mut input = DmaTxBuf::new(tx_descriptors, input).unwrap();
// Encrypt
input.as_mut_slice().copy_from_slice(&plaintext);
let transfer = aes
.process(
1,
output,
input,
Operation::Encrypt,
CipherMode::Ecb,
pad_to::<K>(KEY),
)
.map_err(|e| e.0)
.unwrap();
(aes, output, input) = transfer.wait();
assert_eq!(output.as_slice(), ciphertext);
// Decrypt
input.as_mut_slice().copy_from_slice(&ciphertext);
let transfer = aes
.process(
1,
output,
input,
Operation::Decrypt,
CipherMode::Ecb,
pad_to::<K>(KEY),
)
.map_err(|e| e.0)
.unwrap();
(aes, output, _) = transfer.wait();
assert_eq!(output.as_slice(), plaintext);
aes
}
let peripherals = esp_hal::init(Config::default().with_cpu_clock(CpuClock::max()));
cfg_if::cfg_if! {
if #[cfg(esp32s2)] {
let dma_channel = peripherals.DMA_CRYPTO;
} else {
let dma_channel = peripherals.DMA_CH0;
}
}
let aes = Aes::new(peripherals.AES).with_dma(dma_channel);
let aes = test_aes_ecb::<16>(aes, PLAINTEXT_BUF, CIPHERTEXT_ECB_128);
let _ = test_aes_ecb::<32>(aes, PLAINTEXT_BUF, CIPHERTEXT_ECB_256);
}
#[test]
fn test_aes_work_queue() {
let p = esp_hal::init(Config::default().with_cpu_clock(CpuClock::max()));
let mut aes = AesBackend::new(p.AES);
let _backend = aes.start();
let mut output = [0; 16];
let mut ecb_encrypt = AesContext::ecb(Operation::Encrypt, KEY_128);
let handle = ecb_encrypt.process(&PLAINTEXT_BUF, &mut output).unwrap();
handle.wait_blocking();
assert_eq!(output, CIPHERTEXT_ECB_128);
}
#[test]
fn test_aes_work_queue_work_posted_before_queue_started() {
let p = esp_hal::init(Config::default().with_cpu_clock(CpuClock::max()));
let mut output = [0; 16];
let mut ecb_encrypt = AesContext::ecb(Operation::Encrypt, KEY_128);
let handle = ecb_encrypt.process(&PLAINTEXT_BUF, &mut output).unwrap();
let mut aes = AesBackend::new(p.AES);
let _backend = aes.start();
handle.wait_blocking();
assert_eq!(output, CIPHERTEXT_ECB_128);
}
#[test]
fn test_aes_work_queue_in_place() {
let p = esp_hal::init(Config::default().with_cpu_clock(CpuClock::max()));
let mut buffer = PLAINTEXT_BUF;
let mut ecb_encrypt = AesContext::ecb(Operation::Encrypt, KEY_128);
let handle = ecb_encrypt.process_in_place(&mut buffer).unwrap();
let mut aes = AesBackend::new(p.AES);
let _backend = aes.start();
handle.wait_blocking();
assert_eq!(buffer, CIPHERTEXT_ECB_128);
}
#[test]
fn test_aes_cancelling_work() {
// In this test, we post two work items, and cancel the first one before starting the
// backend. We will assert that, when the second item has finished correctly, the first did
// not modify its output buffer.
// Note that this result is not guaranteed. The cancellation can come later than the work
// item has finished processing. We can only reliably test it because we start the backend
// after cancelling the operation.
let p = esp_hal::init(Config::default().with_cpu_clock(CpuClock::max()));
let mut buffer1 = PLAINTEXT_BUF;
let mut ecb_encrypt = AesContext::ecb(Operation::Encrypt, KEY_128);
let handle1 = ecb_encrypt.process_in_place(&mut buffer1).unwrap();
let mut buffer2 = PLAINTEXT_BUF;
let mut ecb_encrypt = AesContext::ecb(Operation::Encrypt, KEY_256);
let handle2 = ecb_encrypt.process_in_place(&mut buffer2).unwrap();
let mut buffer3 = PLAINTEXT_BUF;
let mut ecb_encrypt = AesContext::ecb(Operation::Encrypt, KEY_128);
let handle3 = ecb_encrypt.process_in_place(&mut buffer3).unwrap();
core::mem::drop(handle1);
let mut aes = AesBackend::new(p.AES);
let _backend = aes.start();
handle3.wait_blocking();
handle2.wait_blocking();
assert_eq!(buffer1, PLAINTEXT_BUF);
assert_eq!(buffer2, CIPHERTEXT_ECB_256);
assert_eq!(buffer3, CIPHERTEXT_ECB_128);
}
}

View File

@ -1,120 +0,0 @@
//! AES DMA Test
//% CHIPS: esp32c3 esp32c6 esp32h2 esp32s2 esp32s3
//% FEATURES: unstable
#![no_std]
#![no_main]
use esp_hal::{
aes::{Aes, Key, Operation, dma::CipherMode},
clock::CpuClock,
dma::{DmaRxBuf, DmaTxBuf},
dma_buffers,
};
use hil_test as _;
esp_bootloader_esp_idf::esp_app_desc!();
const fn pad_to<const K: usize>(input: &[u8]) -> [u8; K] {
let mut out = [0; K];
let in_bytes = input.len();
assert!(in_bytes <= K);
let mut i = 0;
while i < in_bytes {
out[i] = input[i];
i += 1;
}
out
}
const DMA_BUFFER_SIZE: usize = 16;
#[cfg(test)]
#[embedded_test::tests(default_timeout = 3)]
mod tests {
use esp_hal::aes::dma::AesDma;
use super::*;
const KEY: &[u8] = b"SUp4SeCp@sSw0rd";
const PLAINTEXT: &[u8] = b"message";
const PLAINTEXT_BUF: [u8; 16] = pad_to::<16>(PLAINTEXT);
const CIPHERTEXT_ECB_128: [u8; 16] = [
0xb3, 0xc8, 0xd2, 0x3b, 0xa7, 0x36, 0x5f, 0x18, 0x61, 0x70, 0x0, 0x3e, 0xd9, 0x3a, 0x31,
0x96,
];
const CIPHERTEXT_ECB_256: [u8; 16] = [
0x0, 0x63, 0x3f, 0x02, 0xa4, 0x53, 0x09, 0x72, 0x20, 0x6d, 0xc9, 0x08, 0x7c, 0xe5, 0xfd,
0xc,
];
#[test]
fn test_aes_dma_ecb() {
fn test_aes_ecb<const K: usize>(
mut aes: AesDma<'_>,
plaintext: [u8; 16],
ciphertext: [u8; 16],
) -> AesDma<'_>
where
Key: From<[u8; K]>,
{
let (output, rx_descriptors, input, tx_descriptors) = dma_buffers!(DMA_BUFFER_SIZE);
let mut output = DmaRxBuf::new(rx_descriptors, output).unwrap();
let mut input = DmaTxBuf::new(tx_descriptors, input).unwrap();
// Encrypt
input.as_mut_slice().copy_from_slice(&plaintext);
let transfer = aes
.process(
1,
output,
input,
Operation::Encrypt,
CipherMode::Ecb,
pad_to::<K>(KEY),
)
.map_err(|e| e.0)
.unwrap();
(aes, output, input) = transfer.wait();
assert_eq!(output.as_slice(), ciphertext);
// Decrypt
input.as_mut_slice().copy_from_slice(&ciphertext);
let transfer = aes
.process(
1,
output,
input,
Operation::Decrypt,
CipherMode::Ecb,
pad_to::<K>(KEY),
)
.map_err(|e| e.0)
.unwrap();
(aes, output, _) = transfer.wait();
assert_eq!(output.as_slice(), plaintext);
aes
}
let peripherals = esp_hal::init(esp_hal::Config::default().with_cpu_clock(CpuClock::max()));
cfg_if::cfg_if! {
if #[cfg(esp32s2)] {
let dma_channel = peripherals.DMA_CRYPTO;
} else {
let dma_channel = peripherals.DMA_CH0;
}
}
let aes = Aes::new(peripherals.AES).with_dma(dma_channel);
let aes = test_aes_ecb::<16>(aes, PLAINTEXT_BUF, CIPHERTEXT_ECB_128);
let _ = test_aes_ecb::<32>(aes, PLAINTEXT_BUF, CIPHERTEXT_ECB_256);
}
}