2025-10-17 4:06 AM
Hi,
I am trying to send encrypted data between a C# application and STM32 in AES GCM mode.
Currently, I am only doing encryption of a known test vector.
Key and IV are both all zeros.
AAD length is 0.
My testvector is: 00 00 00 00 00 01 00 00 00 00 00 00 00 00 42 49
Using the code below, I get the same ciphertext in both applications:
03 88 DA CE 60 B7 A3 92 F3 28 C2 B9 71 B2 BC 31
But the tag never matches.
I suspect there is something wrong in the final stage, where the tag is calculated.
void AesTest()
{
uint8 iv[12];
uint8 plainText[] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x42, 0x49 };
uint8 header[0];
uint8 cipherText[sizeof(plainText)];
uint8 authTag[16];
memset(iv, 0, sizeof(iv));
uart_printf("ENCRYPT\r\n");
if (!AesEncryptGcm(iv, header, sizeof(header), plainText, sizeof(plainText), cipherText, authTag))
return;
}
bool AesEncryptGcm(const uint8 *iv, const uint8 *header, uint8 headerLen, const uint8 *plainText, uint16 len, uint8 *cipherText, uint8 *authTag)
{
if ((headerLen % 16) != 0 || (len % 16) != 0)
{
uart_printf("AES encryption error: header and src length must be a multiple of 16 bytes\r\n");
return false;
}
AesGcm(true, (const uint32*) iv, (const uint32*) header, headerLen / 4, (const uint32*) plainText, len / 4, (uint32*) cipherText, (uint32*) authTag);
return true;
}
static void AesGcm(bool encrypt, const uint32 *iv, const uint32 *header, uint16 headerLen, const uint32 *src, uint16 srcLen, uint32 *dst, uint32 *authTag)
{
// GCM INITIALIZE.
// Must disable AES peripheral before setting mode, key etc.
AES->CR &= ~BIT(0);
// Chaining mode: GCM.
AES->CR = (AES->CR & 0b11111111111111111111111110011111) | (0x3 << 5);
// Mode: encrypt or decrypt.
if (encrypt)
AES->CR = (AES->CR & 0b11111111111111111111111111100111) | (0x0 << 3); // Encryption.
else
AES->CR = (AES->CR & 0b11111111111111111111111111100111) | (0x2 << 3); // Decryption.
// Data type: ASCII stream = 8-bit data (byte swapping).
AES->CR = (AES->CR & 0b11111111111111111111111111111001) | (0x2 << 1);
// Key size: 128-bit.
AES->CR &= ~BIT(18);
// Key mode: key in AES core.
AES->CR = (AES->CR & 0b11111100111111111111111111111111) | (0x0 << 24);
// Select GCM initialization phase.
AES->CR = (AES->CR & 0b11111111111111111001111111111111) | (0x0 << 13);
// Write 96-bit IV + 32-bit counter start value.
AES->IVR3 = iv[0];
AES->IVR2 = iv[1];
AES->IVR1 = iv[2];
AES->IVR0 = 0x0002; // Counter starts from 2 for each new IV, automatically increased by the AES engine for each block.
// Write key. The key registers must always be written in ascending or descending order.
AES->KEYR3 = key[0]; // Key[127:96]
AES->KEYR2 = key[1]; // Key[95:64]
AES->KEYR1 = key[2]; // Key[63:32]
AES->KEYR0 = key[3]; // Key[31:0]
// Wait until key valid.
while ((AES->SR & BIT(7)) == 0)
;
// Start hash key calculation.
// Enable AES peripheral. Automatically cleared when the calculation is completed.
AES->CR |= BIT(0);
while ((AES->ISR & BIT(0)) == 0)
;
AES->ICR = BIT(0); // Clear CCF flag.
// GCM HEADER PHASE.
// Select GCM header phase.
AES->CR = (AES->CR & 0b11111111111111111001111111111111) | (0x1 << 13);
// Enable AES peripheral.
AES->CR |= BIT(0);
// Append header data.
// A 16-byte data block enters the AES peripheral with four successive 32-bit word writes.
// The most significant word (bits [127:96]) first, the least significant word (bits [31:0]) last.
// The four 32-bit words of a 16-byte data block must be stored in the memory consecutively
// and in big-endian order, that is, with the most significant word on the lowest address.
for (uint16 block = 0; block < headerLen; block += 4)
{
AES->ICR = BIT(0); // Clear CCF flag
for (uint8 i = 0; i < 4; i++)
AES->DINR = header[block + i];
// Wait for operation to complete.
while ((AES->ISR & BIT(0)) == 0)
;
// No data are read during header phase.
}
// GCM PAYLOAD PHASE.
// Select GCM payload phase.
AES->CR = (AES->CR & 0b11111111111111111001111111111111) | (0x2 << 13);
for (uint16 block = 0; block < srcLen; block += 4)
{
AES->ICR = BIT(0); // Clear CCF flag
for (uint8 i = 0; i < 4; i++)
AES->DINR = src[block + i];
// Wait for operation to complete.
while ((AES->ISR & BIT(0)) == 0)
;
// Save the header block.
// Data weights from the first to the fourth read operation are: [127:96], [95:64], [63:32], and [31:0].
for (uint8 i = 0; i < 4; i++)
dst[block + i] = AES->DOUTR;
}
// GCM FINALIZATION.
// Select GCM final phase.
AES->CR = (AES->CR & 0b11111111111111111001111111111111) | (0x3 << 13);
// In the final phase, data are written to AES_DINR normally (no swapping), while swapping is
// applied to tag data read from AES_DOUTR.
uart_printf("header len = %d\r\n", headerLen);
uart_printf("payload len = %d\r\n", srcLen);
// Write the concatenated AAD and payload byte lengths.
AES->ICR = BIT(0); // Clear CCF flag
AES->DINR = 0; // AAD length [63:32]
AES->DINR = headerLen * 4; // AAD length [31:0]
AES->DINR = 0; // Payload length [63:32]
AES->DINR = srcLen * 4; // Payload length [31:0]
// Wait for operation to complete.
while ((AES->ISR & BIT(0)) == 0)
;
// Get GCM authentication tag.
// Swapping is applied to tag data read from AES_DOUTR.
authTag[0] = __REV(AES->DOUTR);
authTag[1] = __REV(AES->DOUTR);
authTag[2] = __REV(AES->DOUTR);
authTag[3] = __REV(AES->DOUTR);
AES->ICR = BIT(0); // Clear CCF flag
// Disable AES peripheral.
AES->CR &= ~BIT(0);
}
Solved! Go to Solution.
2025-10-20 9:07 AM - edited 2025-10-20 9:07 AM
Hello @Dimlite ,
In the CR register, you selected the datatype as Bit-Level swapping (AES->CR[2:1] = 0b11), but headerLen and srcLen multiplied by 4 represent sizes in bytes. Since GCM expects bit lengths, you should multiply by 8 as well (or replace 4 with 32 directly).
If this doesn't resolve your issue, please check your endianness. GCM expects length fields in big-endian format. If your C# code uses big-endian but your STM32 uses little-endian, the tag will not match. Print the tag as a hex string on both sides and compare.
Finally, I suggest you review STM32 AES-GCM implementation examples, examine their process, and compare it with your implementation.
Best regards,
2025-10-20 9:07 AM - edited 2025-10-20 9:07 AM
Hello @Dimlite ,
In the CR register, you selected the datatype as Bit-Level swapping (AES->CR[2:1] = 0b11), but headerLen and srcLen multiplied by 4 represent sizes in bytes. Since GCM expects bit lengths, you should multiply by 8 as well (or replace 4 with 32 directly).
If this doesn't resolve your issue, please check your endianness. GCM expects length fields in big-endian format. If your C# code uses big-endian but your STM32 uses little-endian, the tag will not match. Print the tag as a hex string on both sides and compare.
Finally, I suggest you review STM32 AES-GCM implementation examples, examine their process, and compare it with your implementation.
Best regards,
2025-10-22 4:54 AM
Hi @STackPointer64 ,
multiplying by 8 and removing the swapping macro (__REV) did the trick, thanks a million!
But I am not sure about the bit-level swapping. In the beginning of the code, I select byte swapping, and this remains unchanged for the rest of the function:
// Data type: ASCII stream = 8-bit data (byte swapping).
AES->CR = (AES->CR & 0b11111111111111111111111111111001) | (0x2 << 1);According to chapter 33.4.15 in the datasheet "The data in AES key registers (AES_KEYRx) and initialization vector registers (AES_IVRx) are not sensitive to the swap mode selection".
Question: Is the auth tag also not sensitive to swap mode selection?
Related to this, I find the following sentence in the datasheet a bit puzzling:
"In the final phase, data are written to AES_DINR normally (no swapping), while swapping is
applied to tag data read from AES_DOUTR."
This could be interpreted as either "swapping is applied by the HW" or "you should swap the data read".
I did the latter using the __REV macro, but this was obviously not right.
2025-10-22 7:04 AM
Hello @Dimlite,
Regarding your concern about bit-level swapping: I suggest stepping through the function in debug mode and checking the value of AES->CR each time. This way, you can identify where the change occurs.
About the documentation interpretation:
“Swapping is applied” means the hardware automatically performs the swap when you read the tag from AES_DOUTR; you do not need to swap it in software. If you apply __REV (software byte swap) to the tag, you will get an incorrect result because the hardware has already swapped it for you.
The phrase “swapping is applied to tag data read from AES_DOUTR” can be ambiguous, but in STM32’s AES peripheral, it always means the hardware handles the swap.
Best regards,