on 2026-05-08 2:48 AM
When STM32 AES encryption or decryption output doesn't match the expected test vector, the instinct is to immediately question the key, mode, or algorithm. In many cases, the real issue is more simple: the bytes are correct in memory, but they are not packed into the AES input register in the order expected by the peripheral.
This article explains why that happens and how to fix it when using the STM32 hardware AES/CRYP peripheral. The provided example is tested on STM32U585. The same principle applies to other STM32 devices that include a hardware CRYP/AES peripheral.
For demonstration purposes we use a AES ECB (Electronic Codebook) example vector from the National Institute of Standards and Technology: NIST special publication 800-38a.
Figure 1: AES ECB NIST Example Vector
STM32 devices include a hardware accelerator for the AES algorithm. Depending on the device family, the peripheral supports modes such as: ECB, CBC, CTR, GCM, GMAC and CCM chaining for key sizes of 128 and 256 bits.
The image below is a representation of the AES ECB peripheral overview:
Figure 2: AES ECB peripheral representation.
AES processes 128-bit (16 bytes) blocks of plaintext, but on STM32 the data is transferred through 32-bit register accesses. That means the way data is stored in memory, and the way it is read by the CPU and written to the peripheral, matters.
AES itself is not “big endian” or “little endian”. The issue is not the algorithm. The issue is purely about how the peripheral interface packs bytes into its 32-bit registers.
The confusion comes from the interaction of three facts:
The choice of buffer representation (uint32_t[], uint16_t[], or uint8_t[]) affects how the data is fed to the peripheral, how the data is represented in memory, and the operation result (encryption/decryption).
This is the sample test vector:
6b c1 be e2 2e 40 9f 96 e9 3d 7e 11 73 93 17 2a
For demonstration purposes we show 2 test cases with the AES peripheral.
The NIST AES-128 ECB plaintext stored as a uint8_t :
uint8_t input_data[16] = {
0x6b, 0xc1, 0xbe, 0xe2,
0x2e, 0x40, 0x9f, 0x96,
0xe9, 0x3d, 0x7e, 0x11,
0x73, 0x93, 0x17, 0x2a
};
Take the first four bytes:
6b c1 be e2
In memory, the first four bytes are laid out exactly in that order:
Address: 0x20000000 0x20000001 0x20000002 0x20000003
Data: 0x6b 0xc1 0xbe 0xe2
On a little endian Cortex®-M, when these four bytes are read as a 32-bit word, the value becomes:
0xE2BEC16B
If this word is written directly to the AES input register, the bytes enter the peripheral in the wrong order.
Register: 0x40000000 0x40000001 0x40000002 0x40000003
Data: 0xe2 0xbe 0xc1 0xe6b
Consider the same logical data stored as 32-bit words:
__ALIGN_BEGIN uint32_t input_data[4] __ALIGN_END= {
0x6BC1BEE2,
0x2E409F96,
0xE93D7E11,
0x7393172A
};
In this case, the first word is written into memory as follows:
Register: 0x20000000 0x20000001 0x20000002 0x20000003
Data: 0xe2 0xbe 0xc1 0xe6b
The value written to the AES input register already matches the expected order for the peripheral.
Register: 0x40000000 0x40000001 0x40000002 0x40000003
Data: 0x6b 0xc1 0xbe 0xee2
That is why two buffers can represent the same plaintext at the application level but behave differently in hardware:
This difference matters when the AES peripheral receives data through 32-bit accesses.
Note: The AES input data register is write-only. Therefore, the content shown here is only a conceptual representation of the written data.
As Figure 2 AES ECB peripheral representation illustrates, the AES peripheral incorporates a mechanism for swapping, controlled via special control bits located in the control register.
The swap mechanism can perform:
In HAL, this is controlled by the DataType field in CRYP_HandleTypeDef.
If your data is stored as uint8_t[] use:
hcryp.Init.DataType = CRYP_DATATYPE_8B;
This tells the peripheral to reverse the byte order of each 32-bit word as it is loaded into the AES core.
The value that would otherwise be interpreted as:
0xE2BEC16B
Is swapped to:
0x6BC1BEE2
This is the correct and sufficient fix when your input buffer is a byte-for-byte copy of the NIST test vector.
If your data is stored as uint32_t[]: If your data is already arranged as 32-bit words in the correct order, use:
hcryp.Init.DataType = CRYP_DATATYPE_32B;
No, this is applicable to all AES modes.
AES ECB is used here only because it is the simplest mode to demonstrate: one independent 16-byte block, with no IV or counter.
The same byte-order issue applies to all AES modes supported by the peripheral, because they all feed data through the same 32-bit input path.
Be careful with:
Below is a minimal AES-128 ECB example using the NIST test vector.
#include "main.h"
__ALIGN_BEGIN static const uint32_t pKeyAES[4] __ALIGN_END = {
0x2B7E1516,0x28AED2A6,0xABF71588,0x09CF4F3C};
uint8_t input_data[16] = { 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9,
0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a };
uint8_t encrypted_data[16];
uint8_t decrypted_data[16];
CRYP_HandleTypeDef hcryp;
static void MX_AES_Init(void)
{
hcryp.Instance = AES;
hcryp.Init.DataType = CRYP_DATATYPE_8B; /* byte swap for uint8_t[] */
hcryp.Init.KeySize = CRYP_KEYSIZE_128B;
hcryp.Init.pKey = (uint32_t*) pKeyAES;
hcryp.Init.Algorithm = CRYP_AES_ECB;
hcryp.Init.DataWidthUnit = CRYP_DATAWIDTHUNIT_BYTE;
hcryp.Init.KeyIVConfigSkip = CRYP_KEYIVCONFIG_ALWAYS;
if (HAL_CRYP_Init(&hcryp) != HAL_OK) {
Error_Handler();
}
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_AES_Init();
/* Encrypt */
if (HAL_CRYP_Encrypt(&hcryp, (uint32_t*) input_data, sizeof(input_data),
(uint32_t*) encrypted_data, 1000) != HAL_OK) {
Error_Handler();
}
/* Decrypt */
if (HAL_CRYP_Decrypt(&hcryp, (uint32_t*) encrypted_data, sizeof(input_data),
(uint32_t*) decrypted_data, 1000) != HAL_OK) {
Error_Handler();
}
while (1) {
}
}
For this NIST AES-128 ECB test vector, the expected ciphertext is:
3a d7 7b b4 0d 7a 36 60 a8 9e ca f3 24 66 ef 97
hcryp.Init.DataWidthUnit = CRYP_DATAWIDTHUNIT_X;
hcryp.Init.DataType = CRYP_DATATYPE_X ;
In this section, we provide examples of how to use the OpenSSL CLI tool to encrypt and decrypt data, along with a sample Python program using PyCryptodome.
openssl enc -aes-128-ecb -K 2b7e151628aed2a6abf7158809cf4f3c -nopad -nosalt -in plain.bin -out enc.bin
openssl enc -d -aes-128-ecb -K 2b7e151628aed2a6abf7158809cf4f3c -nopad -nosalt -in enc.bin -out dec.bin
hexdump -C enc.bin
Note: The plain.bin contains the NIST test vector in binary format. You can find it attached below.
from Crypto.Cipher import AES
key = bytes([
0x2b, 0x7e, 0x15, 0x16,
0x28, 0xae, 0xd2, 0xa6,
0xab, 0xf7, 0x15, 0x88,
0x09, 0xcf, 0x4f, 0x3c
])
plaintext = bytes([
0x6b, 0xc1, 0xbe, 0xe2,
0x2e, 0x40, 0x9f, 0x96,
0xe9, 0x3d, 0x7e, 0x11,
0x73, 0x93, 0x17, 0x2a
])
cipher = AES.new(key, AES.MODE_ECB)
ciphertext = cipher.encrypt(plaintext)
decrypted = cipher.decrypt(ciphertext)
print("Key :", key.hex())
print("Plaintext :", plaintext.hex())
print("Ciphertext :", ciphertext.hex())
print("Decrypted :", decrypted.hex())