Repository: wolfSSL/wolfssl
Pull request #8594: Implement AES-CTS in wolfCrypt
Author: login: julek-wolfssl
Labels: name: For This Release
description: Release version 5.9.1
Created: 2025-03-26T17:36:17Z | Merged: 2025-04-15T23:35:53Z



Diff:
diff --git a/configure.ac b/configure.ac
index 3cf5971adc..6d27e84082 100644
--- a/configure.ac
+++ b/configure.ac
@@ -5438,6 +5438,22 @@ AC_ARG_ENABLE([xts],
     [ ENABLED_AESXTS=$enableval ]
     )
 
+# AES-CTS
+AC_ARG_ENABLE([aescts],
+    [AS_HELP_STRING([--enable-aescts],[Enable AES CTS (default: disabled)])],
+    [ ENABLED_AESCTS=$enableval ],
+    [ ENABLED_AESCTS=no ]
+    )
+
+if test "$ENABLED_AESCTS" = "yes"
+then
+    if test "$ENABLED_AESCBC" = "no"
+    then
+        AC_MSG_ERROR([AES CTS requires AES CBC.])
+    fi
+    AM_CFLAGS="$AM_CFLAGS -DWOLFSSL_AES_CTS"
+fi
+
 # Web Server Build
 AC_ARG_ENABLE([webserver],
     [AS_HELP_STRING([--enable-webserver],[Enable Web Server (default: disabled)])],
diff --git a/doc/dox_comments/header_files/aes.h b/doc/dox_comments/header_files/aes.h
index 4d6561eec3..94a34cb85c 100644
--- a/doc/dox_comments/header_files/aes.h
+++ b/doc/dox_comments/header_files/aes.h
@@ -1733,5 +1733,244 @@ WOLFSSL_API int wc_AesEaxDecryptFinal(AesEax* eax,
 */
 WOLFSSL_API int wc_AesEaxFree(AesEax* eax);
 
+/*!
+    \ingroup AES
+    \brief This function performs AES encryption using Ciphertext Stealing (CTS)
+    mode. It is a one-shot API that handles all operations in a single call.
+
+    \return 0 on successful encryption.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \return other negative error codes for encryption failures.
+
+    \param key pointer to the AES key used for encryption.
+    \param keySz size of the AES key in bytes (16, 24, or 32 bytes).
+    \param[out] out buffer to hold the encrypted ciphertext. Must be at least
+    the size of the input.
+    \param in pointer to the plaintext input data to encrypt.
+    \param inSz size of the plaintext input data in bytes.
+    \param iv pointer to the initialization vector (IV) used for encryption.
+    Must be 16 bytes.
+
+    _Example_
+    \code
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte plaintext[] = { 0x01, 0x02, 0x03, 0x04, 0x05 };
+        byte ciphertext[sizeof(plaintext)];
+
+        int ret = wc_AesCtsEncrypt(key, sizeof(key), ciphertext, plaintext,
+            sizeof(plaintext), iv);
+        if (ret != 0) {
+        // handle encryption error
+    }
+    \endcode
 
+    \sa wc_AesCtsDecrypt
+*/
+int wc_AesCtsEncrypt(const byte* key, word32 keySz, byte* out,
+                     const byte* in, word32 inSz,
+                     const byte* iv);
 
+/*!
+    \ingroup AES
+    \brief This function performs AES encryption using Ciphertext Stealing (CTS)
+     mode. It is a one-shot API that handles all operations in a single call.
+
+    \return 0 on successful encryption.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \return other negative error codes for encryption failures.
+
+    \param key pointer to the AES key used for encryption.
+    \param keySz size of the AES key in bytes (16, 24, or 32 bytes).
+    \param[out] out buffer to hold the encrypted ciphertext. Must be at least
+                 the same size as the input plaintext.
+    \param in pointer to the plaintext input data to encrypt.
+    \param inSz size of the plaintext input data in bytes.
+    \param iv pointer to the initialization vector (IV) used for encryption.
+             Must be 16 bytes.
+    _Example_
+    \code
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte plaintext[] = { 0x01, 0x02, 0x03, 0x04, 0x05 };
+        byte ciphertext[sizeof(plaintext)];
+        int ret = wc_AesCtsEncrypt(key, sizeof(key), ciphertext, plaintext,
+                                   sizeof(plaintext), iv);
+        if (ret != 0) {
+            // handle encryption error
+        }
+    \endcode
+    \sa wc_AesCtsDecrypt
+*/
+int wc_AesCtsEncrypt(const byte* key, word32 keySz, byte* out,
+                     const byte* in, word32 inSz,
+                     const byte* iv);
+
+/*!
+    \ingroup AES
+    \brief This function performs AES decryption using Ciphertext Stealing (CTS) mode.
+           It is a one-shot API that handles all operations in a single call.
+    \return 0 on successful decryption.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \return other negative error codes for decryption failures.
+    \param key pointer to the AES key used for decryption.
+    \param keySz size of the AES key in bytes (16, 24, or 32 bytes).
+    \param[out] out buffer to hold the decrypted plaintext. Must be at least
+                 the same size as the input ciphertext.
+    \param in pointer to the ciphertext input data to decrypt.
+    \param inSz size of the ciphertext input data in bytes.
+    \param iv pointer to the initialization vector (IV) used for decryption.
+             Must be 16 bytes.
+    _Example_
+    \code
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte ciphertext[] = { 0x01, 0x02, 0x03, 0x04, 0x05 };
+        byte plaintext[sizeof(ciphertext)];
+        int ret = wc_AesCtsDecrypt(key, sizeof(key), plaintext, ciphertext,
+                                   sizeof(ciphertext), iv);
+        if (ret != 0) {
+            // handle decryption error
+        }
+    \endcode
+    \sa wc_AesCtsEncrypt
+*/
+int wc_AesCtsDecrypt(const byte* key, word32 keySz, byte* out,
+                     const byte* in, word32 inSz,
+                     const byte* iv);
+
+/*!
+    \ingroup AES
+    \brief This function performs an update step of the AES CTS encryption.
+           It processes a chunk of plaintext and stores intermediate data.
+    \return 0 on successful processing.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \param aes pointer to the Aes structure holding the context of the operation.
+    \param[out] out buffer to hold the encrypted ciphertext. Must be large enough
+                 to store the output from this update step.
+    \param[out] outSz size in bytes of the output data written to the \c out buffer.
+                     On input, it should contain the maximum number of bytes that can
+                     be written to the \c out buffer.
+    \param in pointer to the plaintext input data to encrypt.
+    \param inSz size of the plaintext input data in bytes.
+    _Example_
+    \code
+        Aes aes;
+        wc_AesInit(&aes, NULL, INVALID_DEVID);
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte plaintext[] = { ... };
+        byte ciphertext[sizeof(plaintext)];
+        word32 outSz = sizeof(ciphertext);
+        wc_AesSetKey(&aes, key, sizeof(key), iv, AES_ENCRYPTION);
+        int ret = wc_AesCtsEncryptUpdate(&aes, ciphertext, &outSz, plaintext, sizeof(plaintext));
+        if (ret != 0) {
+            // handle error
+        }
+        wc_AesFree(&aes);
+    \endcode
+    \sa wc_AesCtsDecryptUpdate
+*/
+int wc_AesCtsEncryptUpdate(Aes* aes, byte* out, word32* outSz,
+                           const byte* in, word32 inSz);
+
+/*!
+    \ingroup AES
+    \brief This function finalizes the AES CTS encryption operation.
+           It processes any remaining plaintext and completes the encryption.
+    \return 0 on successful encryption completion.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \param aes pointer to the Aes structure holding the context of the operation.
+    \param[out] out buffer to hold the final encrypted ciphertext. Must be large
+                 enough to store any remaining ciphertext from this final step.
+    \param[out] outSz size in bytes of the output data written to the \c out buffer.
+                     On input, it should contain the maximum number of bytes that can
+                     be written to the \c out buffer.
+    _Example_
+    \code
+        Aes aes;
+        wc_AesInit(&aes, NULL, INVALID_DEVID);
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte plaintext[] = { ... };
+        byte ciphertext[sizeof(plaintext)];
+        word32 outSz = sizeof(ciphertext);
+        wc_AesSetKey(&aes, key, sizeof(key), iv, AES_ENCRYPTION);
+        // Perform any required update steps using wc_AesCtsEncryptUpdate
+        int ret = wc_AesCtsEncryptFinal(&aes, ciphertext, &outSz);
+        if (ret != 0) {
+            // handle error
+        }
+        wc_AesFree(&aes);
+    \endcode
+    \sa wc_AesCtsDecryptFinal
+*/
+int wc_AesCtsEncryptFinal(Aes* aes, byte* out, word32* outSz);
+
+/*!
+    \ingroup AES
+    \brief This function performs an update step of the AES CTS decryption.
+           It processes a chunk of ciphertext and stores intermediate data.
+    \return 0 on successful processing.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \param aes pointer to the Aes structure holding the context of the operation.
+    \param[out] out buffer to hold the decrypted plaintext. Must be large enough
+                 to store the output from this update step.
+    \param[out] outSz size in bytes of the output data written to the \c out buffer.
+                     On input, it should contain the maximum number of bytes that can
+                     be written to the \c out buffer.
+    \param in pointer to the ciphertext input data to decrypt.
+    \param inSz size of the ciphertext input data in bytes.
+    _Example_
+    \code
+        Aes aes;
+        wc_AesInit(&aes, NULL, INVALID_DEVID);
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte ciphertext[] = { ... };
+        byte plaintext[sizeof(ciphertext)];
+        word32 outSz = sizeof(plaintext);
+        wc_AesSetKey(&aes, key, sizeof(key), iv, AES_DECRYPTION);
+        int ret = wc_AesCtsDecryptUpdate(&aes, plaintext, &outSz, ciphertext, sizeof(ciphertext));
+        if (ret != 0) {
+            // handle error
+        }
+        wc_AesFree(&aes);
+    \endcode
+    \sa wc_AesCtsEncryptUpdate
+*/
+int wc_AesCtsDecryptUpdate(Aes* aes, byte* out, word32* outSz,
+                           const byte* in, word32 inSz);
+
+/*!
+    \ingroup AES
+    \brief This function finalizes the AES CTS decryption operation.
+           It processes any remaining ciphertext and completes the decryption.
+    \return 0 on successful decryption completion.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \param aes pointer to the Aes structure holding the context of the operation.
+    \param[out] out buffer to hold the final decrypted plaintext. Must be large
+                 enough to store any remaining plaintext from this final step.
+    \param[out] outSz size in bytes of the output data written to the \c out buffer.
+                     On input, it should contain the maximum number of bytes that can
+                     be written to the \c out buffer.
+    _Example_
+    \code
+        Aes aes;
+        wc_AesInit(&aes, NULL, INVALID_DEVID);
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte ciphertext[] = { ... };
+        byte plaintext[sizeof(ciphertext)];
+        word32 outSz = sizeof(plaintext);
+        wc_AesSetKey(&aes, key, sizeof(key), iv, AES_DECRYPTION);
+        // Perform any required update steps using wc_AesCtsDecryptUpdate
+        int ret = wc_AesCtsDecryptFinal(&aes, plaintext, &outSz);
+        if (ret != 0) {
+            // handle error
+        }
+        wc_AesFree(&aes);
+    \endcode
+    \sa wc_AesCtsEncryptFinal
+*/
+int wc_AesCtsDecryptFinal(Aes* aes, byte* out, word32* outSz);
diff --git a/tests/api/test_aes.c b/tests/api/test_aes.c
index 181a4cb9de..173b591671 100644
--- a/tests/api/test_aes.c
+++ b/tests/api/test_aes.c
@@ -263,6 +263,230 @@ int test_wc_AesCbcEncryptDecrypt(void)
     return EXPECT_RESULT();
 } /* END test_wc_AesCbcEncryptDecrypt */
 
+/*******************************************************************************
+ * AES-CTS
+ ******************************************************************************/
+
+int test_wc_AesCtsEncryptDecrypt(void)
+{
+    EXPECT_DECLS;
+#if !defined(NO_AES) && defined(WOLFSSL_AES_CTS) && \
+    defined(HAVE_AES_DECRYPT) && defined(WOLFSSL_AES_128)
+    /* Test vectors taken form RFC3962 Appendix B */
+    const struct {
+        const char* input;
+        const char* output;
+        size_t inLen;
+        size_t outLen;
+    } vects[] = {
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20",
+            "\xc6\x35\x35\x68\xf2\xbf\x8c\xb4\xd8\xa5\x80\x36\x2d\xa7\xff\x7f"
+            "\x97",
+            17, 17
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20",
+            "\xfc\x00\x78\x3e\x0e\xfd\xb2\xc1\xd4\x45\xd4\xc8\xef\xf7\xed\x22"
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5",
+            31, 31
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43",
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5\xa8"
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84",
+            32, 32
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43"
+            "\x68\x69\x63\x6b\x65\x6e\x2c\x20\x70\x6c\x65\x61\x73\x65\x2c",
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84"
+            "\xb3\xff\xfd\x94\x0c\x16\xa1\x8c\x1b\x55\x49\xd2\xf8\x38\x02\x9e"
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5",
+            47, 47
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43"
+            "\x68\x69\x63\x6b\x65\x6e\x2c\x20\x70\x6c\x65\x61\x73\x65\x2c\x20",
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84"
+            "\x9d\xad\x8b\xbb\x96\xc4\xcd\xc0\x3b\xc1\x03\xe1\xa1\x94\xbb\xd8"
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5\xa8",
+            48, 48
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43"
+            "\x68\x69\x63\x6b\x65\x6e\x2c\x20\x70\x6c\x65\x61\x73\x65\x2c\x20"
+            "\x61\x6e\x64\x20\x77\x6f\x6e\x74\x6f\x6e\x20\x73\x6f\x75\x70\x2e",
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84"
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5\xa8"
+            "\x48\x07\xef\xe8\x36\xee\x89\xa5\x26\x73\x0d\xbc\x2f\x7b\xc8\x40"
+            "\x9d\xad\x8b\xbb\x96\xc4\xcd\xc0\x3b\xc1\x03\xe1\xa1\x94\xbb\xd8",
+            64, 64
+        }
+    };
+    const byte keyBytes[AES_128_KEY_SIZE] = {
+        0x63, 0x68, 0x69, 0x63, 0x6b, 0x65, 0x6e, 0x20,
+        0x74, 0x65, 0x72, 0x69, 0x79, 0x61, 0x6b, 0x69
+    };
+    byte tmp[64]; /* Largest vector size */
+    size_t i;
+    byte iv[AES_IV_SIZE]; /* All-zero IV for all cases */
+
+    XMEMSET(iv, 0, sizeof(iv));
+    for (i = 0; i < XELEM_CNT(vects) && EXPECT_SUCCESS(); i++) {
+        /* One-shot encrypt */
+        XMEMSET(tmp, 0, sizeof(tmp));
+        ExpectIntEQ(wc_AesCtsEncrypt(keyBytes, sizeof(keyBytes), tmp,
+                 (const byte*)vects[i].input, (word32)vects[i].inLen, iv), 0);
+        ExpectBufEQ(tmp, vects[i].output, vects[i].outLen);
+        XMEMSET(tmp, 0, sizeof(tmp));
+        ExpectIntEQ(wc_AesCtsDecrypt(keyBytes, sizeof(keyBytes), tmp,
+                 (const byte*)vects[i].output, (word32)vects[i].outLen, iv), 0);
+        ExpectBufEQ(tmp, vects[i].input, vects[i].inLen);
+    }
+    /* Execute all branches */
+    {
+        Aes* aes = NULL;
+        int result_code = 0;
+        const byte* in = (const byte*)vects[5].input;
+        byte* out = tmp;
+        word32 outSz = (word32)vects[5].outLen;
+        word32 remSz = (word32)vects[5].outLen;
+
+        XMEMSET(tmp, 0, sizeof(tmp));
+        ExpectNotNull(aes = wc_AesNew(NULL, INVALID_DEVID, &result_code));
+        ExpectIntEQ(wc_AesSetKey(aes, keyBytes, sizeof(keyBytes), iv,
+                                 AES_ENCRYPTION), 0);
+        ExpectIntEQ(wc_AesCtsEncryptUpdate(aes, out, &outSz, in, 1), 0);
+        in += 1; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsEncryptUpdate(aes, out, &outSz, in, 31), 0);
+        in += 31; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsEncryptUpdate(aes, out, &outSz, in, 32), 0);
+        in += 32; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsEncryptFinal(aes, out, &outSz), 0);
+        remSz -= outSz;
+        ExpectIntEQ(remSz, 0);
+        ExpectBufEQ(tmp, vects[5].output, vects[5].outLen);
+        ExpectIntEQ(wc_AesDelete(aes, &aes), 0);
+    }
+    {
+        Aes* aes = NULL;
+        int result_code = 0;
+        const byte* in = (const byte*)vects[5].input;
+        byte* out = tmp;
+        word32 outSz = (word32)vects[5].outLen;
+        word32 remSz = (word32)vects[5].outLen;
+
+        ExpectNotNull(aes = wc_AesNew(NULL, INVALID_DEVID, &result_code));
+        ExpectIntEQ(wc_AesSetKey(aes, keyBytes, sizeof(keyBytes), iv,
+                                 AES_ENCRYPTION), 0);
+        ExpectIntEQ(wc_AesCtsEncryptUpdate(aes, out, &outSz, in, 1), 0);
+        in += 1; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsEncryptUpdate(aes, out, &outSz, in, 63), 0);
+        in += 63; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsEncryptFinal(aes, out, &outSz), 0);
+        remSz -= outSz;
+        ExpectIntEQ(remSz, 0);
+        ExpectBufEQ(tmp, vects[5].output, vects[5].outLen);
+        ExpectIntEQ(wc_AesDelete(aes, &aes), 0);
+    }
+    {
+        Aes* aes = NULL;
+        int result_code = 0;
+        const byte* in = (const byte*)vects[2].input;
+        byte* out = tmp;
+        word32 outSz = (word32)vects[2].outLen;
+        word32 remSz = (word32)vects[2].outLen;
+
+        ExpectNotNull(aes = wc_AesNew(NULL, INVALID_DEVID, &result_code));
+        ExpectIntEQ(wc_AesSetKey(aes, keyBytes, sizeof(keyBytes), iv,
+                                 AES_ENCRYPTION), 0);
+        ExpectIntEQ(wc_AesCtsEncryptUpdate(aes, out, &outSz, in, 16), 0);
+        in += 16; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsEncryptUpdate(aes, out, &outSz, in, 16), 0);
+        in += 16; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsEncryptFinal(aes, out, &outSz), 0);
+        remSz -= outSz;
+        ExpectIntEQ(remSz, 0);
+        ExpectBufEQ(tmp, vects[2].output, vects[2].outLen);
+        ExpectIntEQ(wc_AesDelete(aes, &aes), 0);
+    }
+    {
+        Aes* aes = NULL;
+        int result_code = 0;
+        const byte* in = (const byte*)vects[5].output;
+        byte* out = tmp;
+        word32 outSz = (word32)vects[5].inLen;
+        word32 remSz = (word32)vects[5].inLen;
+
+        XMEMSET(tmp, 0, sizeof(tmp));
+        ExpectNotNull(aes = wc_AesNew(NULL, INVALID_DEVID, &result_code));
+        ExpectIntEQ(wc_AesSetKey(aes, keyBytes, sizeof(keyBytes), iv,
+                                 AES_DECRYPTION), 0);
+        ExpectIntEQ(wc_AesCtsDecryptUpdate(aes, out, &outSz, in, 1), 0);
+        in += 1; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsDecryptUpdate(aes, out, &outSz, in, 31), 0);
+        in += 31; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsDecryptUpdate(aes, out, &outSz, in, 32), 0);
+        in += 32; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsDecryptFinal(aes, out, &outSz), 0);
+        remSz -= outSz;
+        ExpectIntEQ(remSz, 0);
+        ExpectBufEQ(tmp, vects[5].input, vects[5].inLen);
+        ExpectIntEQ(wc_AesDelete(aes, &aes), 0);
+    }
+    {
+        Aes* aes = NULL;
+        int result_code = 0;
+        const byte* in = (const byte*)vects[5].output;
+        byte* out = tmp;
+        word32 outSz = (word32)vects[5].inLen;
+        word32 remSz = (word32)vects[5].inLen;
+
+        ExpectNotNull(aes = wc_AesNew(NULL, INVALID_DEVID, &result_code));
+        ExpectIntEQ(wc_AesSetKey(aes, keyBytes, sizeof(keyBytes), iv,
+                                 AES_DECRYPTION), 0);
+        ExpectIntEQ(wc_AesCtsDecryptUpdate(aes, out, &outSz, in, 1), 0);
+        in += 1; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsDecryptUpdate(aes, out, &outSz, in, 63), 0);
+        in += 63; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsDecryptFinal(aes, out, &outSz), 0);
+        remSz -= outSz;
+        ExpectIntEQ(remSz, 0);
+        ExpectBufEQ(tmp, vects[5].input, vects[5].inLen);
+        ExpectIntEQ(wc_AesDelete(aes, &aes), 0);
+    }
+    {
+        Aes* aes = NULL;
+        int result_code = 0;
+        const byte* in = (const byte*)vects[2].output;
+        byte* out = tmp;
+        word32 outSz = (word32)vects[2].inLen;
+        word32 remSz = (word32)vects[2].inLen;
+
+        ExpectNotNull(aes = wc_AesNew(NULL, INVALID_DEVID, &result_code));
+        ExpectIntEQ(wc_AesSetKey(aes, keyBytes, sizeof(keyBytes), iv,
+                                 AES_DECRYPTION), 0);
+        ExpectIntEQ(wc_AesCtsDecryptUpdate(aes, out, &outSz, in, 16), 0);
+        in += 16; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsDecryptUpdate(aes, out, &outSz, in, 16), 0);
+        in += 16; out += outSz; remSz -= outSz; outSz = remSz;
+        ExpectIntEQ(wc_AesCtsDecryptFinal(aes, out, &outSz), 0);
+        remSz -= outSz;
+        ExpectIntEQ(remSz, 0);
+        ExpectBufEQ(tmp, vects[2].input, vects[2].inLen);
+        ExpectIntEQ(wc_AesDelete(aes, &aes), 0);
+    }
+#endif
+    return EXPECT_RESULT();
+}
+
 /*******************************************************************************
  * AES-CTR
  ******************************************************************************/
diff --git a/tests/api/test_aes.h b/tests/api/test_aes.h
index 46d13c5413..22b24732aa 100644
--- a/tests/api/test_aes.h
+++ b/tests/api/test_aes.h
@@ -27,6 +27,7 @@
 int test_wc_AesSetKey(void);
 int test_wc_AesSetIV(void);
 int test_wc_AesCbcEncryptDecrypt(void);
+int test_wc_AesCtsEncryptDecrypt(void);
 int test_wc_AesCtrEncryptDecrypt(void);
 int test_wc_AesGcmSetKey(void);
 int test_wc_AesGcmEncryptDecrypt(void);
@@ -48,6 +49,7 @@ int test_wc_GmacUpdate(void);
     TEST_DECL_GROUP("aes", test_wc_AesSetKey),                  \
     TEST_DECL_GROUP("aes", test_wc_AesSetIV),                   \
     TEST_DECL_GROUP("aes", test_wc_AesCbcEncryptDecrypt),       \
+    TEST_DECL_GROUP("aes", test_wc_AesCtsEncryptDecrypt),       \
     TEST_DECL_GROUP("aes", test_wc_AesCtrEncryptDecrypt),       \
     TEST_DECL_GROUP("aes", test_wc_AesGcmSetKey),               \
     TEST_DECL_GROUP("aes", test_wc_AesGcmEncryptDecrypt),       \
diff --git a/wolfcrypt/src/aes.c b/wolfcrypt/src/aes.c
index 926e800020..095cba94cd 100644
--- a/wolfcrypt/src/aes.c
+++ b/wolfcrypt/src/aes.c
@@ -3759,7 +3759,8 @@ static WARN_UNUSED_RESULT int wc_AesDecrypt(
         ByteReverseWords(rk, rk, keylen);
     #endif
     #if defined(WOLFSSL_AES_COUNTER) || defined(WOLFSSL_AES_CFB) || \
-        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS)
+        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS) || \
+        defined(WOLFSSL_AES_CTS)
         aes->left = 0;
     #endif
         return wc_AesSetIV(aes, iv);
@@ -3840,7 +3841,8 @@ static WARN_UNUSED_RESULT int wc_AesDecrypt(
             XMEMCPY(aes->reg, iv, WC_AES_BLOCK_SIZE);
 
     #if defined(WOLFSSL_AES_COUNTER) || defined(WOLFSSL_AES_CFB) || \
-        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS)
+        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS) || \
+        defined(WOLFSSL_AES_CTS)
         aes->left = 0;
     #endif
 
@@ -3871,7 +3873,8 @@ static WARN_UNUSED_RESULT int wc_AesDecrypt(
         XMEMCPY(aes->key, userKey, keylen);
 
     #if defined(WOLFSSL_AES_COUNTER) || defined(WOLFSSL_AES_CFB) || \
-        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS)
+        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS) || \
+        defined(WOLFSSL_AES_CTS)
         aes->left = 0;
     #endif
 
@@ -3923,7 +3926,8 @@ static WARN_UNUSED_RESULT int wc_AesDecrypt(
             return BAD_FUNC_ARG;
 
     #if defined(WOLFSSL_AES_COUNTER) || defined(WOLFSSL_AES_CFB) || \
-        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS)
+        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS) || \
+        defined(WOLFSSL_AES_CTS)
         aes->left = 0;
     #endif
 
@@ -4004,7 +4008,8 @@ static WARN_UNUSED_RESULT int wc_AesDecrypt(
         ret = nrf51_aes_set_key(userKey);
 
     #if defined(WOLFSSL_AES_COUNTER) || defined(WOLFSSL_AES_CFB) || \
-        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS)
+        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS) || \
+        defined(WOLFSSL_AES_CTS)
         aes->left = 0;
     #endif
 
@@ -4061,7 +4066,8 @@ static WARN_UNUSED_RESULT int wc_AesDecrypt(
 
         XMEMCPY(aes->key, userKey, keylen);
         #if defined(WOLFSSL_AES_COUNTER) || defined(WOLFSSL_AES_CFB) || \
-            defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS)
+            defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS) || \
+            defined(WOLFSSL_AES_CTS)
             aes->left = 0;
         #endif
         return wc_AesSetIV(aes, iv);
@@ -4554,7 +4560,8 @@ static void AesSetKey_C(Aes* aes, const byte* key, word32 keySz, int dir)
         }
 
     #if defined(WOLFSSL_AES_COUNTER) || defined(WOLFSSL_AES_CFB) || \
-        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS)
+        defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS) || \
+        defined(WOLFSSL_AES_CTS)
         aes->left = 0;
     #endif
 
@@ -4836,7 +4843,8 @@ int wc_AesSetIV(Aes* aes, const byte* iv)
         XMEMSET(aes->reg,  0, WC_AES_BLOCK_SIZE);
 
 #if defined(WOLFSSL_AES_COUNTER) || defined(WOLFSSL_AES_CFB) || \
-    defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS)
+    defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS) || \
+    defined(WOLFSSL_AES_CTS)
     /* Clear any unused bytes from last cipher op. */
     aes->left = 0;
 #endif
@@ -10857,7 +10865,7 @@ int wc_GmacVerify(const byte* key, word32 keySz,
 #endif /* WC_NO_RNG */
 
 
-WOLFSSL_API int wc_GmacSetKey(Gmac* gmac, const byte* key, word32 len)
+int wc_GmacSetKey(Gmac* gmac, const byte* key, word32 len)
 {
     if (gmac == NULL || key == NULL) {
         return BAD_FUNC_ARG;
@@ -10866,7 +10874,7 @@ WOLFSSL_API int wc_GmacSetKey(Gmac* gmac, const byte* key, word32 len)
 }
 
 
-WOLFSSL_API int wc_GmacUpdate(Gmac* gmac, const byte* iv, word32 ivSz,
+int wc_GmacUpdate(Gmac* gmac, const byte* iv, word32 ivSz,
                               const byte* authIn, word32 authInSz,
                               byte* authTag, word32 authTagSz)
 {
@@ -14906,4 +14914,276 @@ int wc_AesEaxFree(AesEax* eax)
 
 #endif /* WOLFSSL_AES_EAX */
 
+#ifdef WOLFSSL_AES_CTS
+
+
+/* One-shot API */
+int wc_AesCtsEncrypt(const byte* key, word32 keySz, byte* out,
+                     const byte* in, word32 inSz,
+                     const byte* iv)
+{
+#ifdef WOLFSSL_SMALL_STACK
+    Aes *aes = NULL;
+#else
+    Aes aes[1];
+#endif
+    int ret = 0;
+    word32 outSz = inSz;
+
+    if (key == NULL || out == NULL || in == NULL || iv == NULL)
+        return BAD_FUNC_ARG;
+
+#ifdef WOLFSSL_SMALL_STACK
+    aes = wc_AesNew(NULL, INVALID_DEVID, &ret);
+#else
+    ret = wc_AesInit(aes, NULL, INVALID_DEVID);
+#endif
+    if (ret == 0)
+        ret = wc_AesSetKey(aes, key, keySz, iv, AES_ENCRYPTION);
+    if (ret == 0)
+        ret = wc_AesCtsEncryptUpdate(aes, out, &outSz, in, inSz);
+    if (ret == 0) {
+        out += outSz;
+        outSz = inSz - outSz;
+        ret = wc_AesCtsEncryptFinal(aes, out, &outSz);
+    }
+
+#ifdef WOLFSSL_SMALL_STACK
+    wc_AesDelete(aes, NULL);
+#else
+    wc_AesFree(aes);
+#endif
+    return ret;
+}
+
+int wc_AesCtsDecrypt(const byte* key, word32 keySz, byte* out,
+                     const byte* in, word32 inSz,
+                     const byte* iv)
+{
+#ifdef WOLFSSL_SMALL_STACK
+    Aes *aes = NULL;
+#else
+    Aes aes[1];
+#endif
+    int ret = 0;
+    word32 outSz = inSz;
+
+    if (key == NULL || out == NULL || in == NULL || iv == NULL) {
+        return BAD_FUNC_ARG;
+    }
+
+#ifdef WOLFSSL_SMALL_STACK
+    aes = wc_AesNew(NULL, INVALID_DEVID, &ret);
+#else
+    ret = wc_AesInit(aes, NULL, INVALID_DEVID);
+#endif
+    if (ret == 0)
+        ret = wc_AesSetKey(aes, key, keySz, iv, AES_DECRYPTION);
+    if (ret == 0)
+        ret = wc_AesCtsDecryptUpdate(aes, out, &outSz, in, inSz);
+    if (ret == 0) {
+        out += outSz;
+        outSz = inSz - outSz;
+        ret = wc_AesCtsDecryptFinal(aes, out, &outSz);
+    }
+
+#ifdef WOLFSSL_SMALL_STACK
+    wc_AesDelete(aes, NULL);
+#else
+    wc_AesFree(aes);
+#endif
+    return ret;
+}
+
+static int AesCtsUpdate(Aes* aes, byte* out, word32* outSz,
+                        const byte* in, word32 inSz, int enc)
+{
+    word32 blocks = 0;
+    int ret = 0;
+    word32 writtenSz = 0;
+    word32 tmpOutSz;
+
+    if (aes == NULL || out == NULL || in == NULL || outSz == NULL)
+        return BAD_FUNC_ARG;
+
+    /* Error out early for easy sanity check */
+    if (*outSz < inSz)
+        return BUFFER_E;
+    tmpOutSz = *outSz;
+
+    /* We need to store last two blocks of plaintext */
+    if (aes->left > 0) {
+        word32 copySz = min(inSz, (WC_AES_BLOCK_SIZE * 2) - aes->left);
+        XMEMCPY(aes->ctsBlock + aes->left, in, copySz);
+        aes->left += copySz;
+        in += copySz;
+        inSz -= copySz;
+
+        if (aes->left == WC_AES_BLOCK_SIZE * 2) {
+            if (inSz > WC_AES_BLOCK_SIZE) {
+                if (tmpOutSz < WC_AES_BLOCK_SIZE * 2)
+                    return BUFFER_E;
+                if (enc) {
+                    ret = wc_AesCbcEncrypt(aes, out, aes->ctsBlock,
+                                           WC_AES_BLOCK_SIZE * 2);
+                }
+                else {
+                    ret = wc_AesCbcDecrypt(aes, out, aes->ctsBlock,
+                                           WC_AES_BLOCK_SIZE * 2);
+                }
+                if (ret != 0)
+                    return ret;
+                out += WC_AES_BLOCK_SIZE * 2;
+                writtenSz += WC_AES_BLOCK_SIZE * 2;
+                tmpOutSz -= WC_AES_BLOCK_SIZE * 2;
+                aes->left = 0;
+            }
+            else if (inSz > 0) {
+                if (tmpOutSz < WC_AES_BLOCK_SIZE)
+                    return BUFFER_E;
+                if (enc) {
+                    ret = wc_AesCbcEncrypt(aes, out, aes->ctsBlock,
+                                           WC_AES_BLOCK_SIZE);
+                }
+                else {
+                    ret = wc_AesCbcDecrypt(aes, out, aes->ctsBlock,
+                                           WC_AES_BLOCK_SIZE);
+                }
+                if (ret != 0)
+                    return ret;
+                out += WC_AES_BLOCK_SIZE;
+                writtenSz += WC_AES_BLOCK_SIZE;
+                tmpOutSz -= WC_AES_BLOCK_SIZE;
+                /* Move the last block in ctsBlock to the beginning for
+                 * next operation */
+                XMEMCPY(aes->ctsBlock, aes->ctsBlock + WC_AES_BLOCK_SIZE,
+                        WC_AES_BLOCK_SIZE);
+                XMEMCPY(aes->ctsBlock + WC_AES_BLOCK_SIZE, in, inSz);
+                aes->left = WC_AES_BLOCK_SIZE + inSz;
+                *outSz = writtenSz;
+                return ret; /* Return the result of encryption */
+            }
+            else {
+                /* Can't output data as we need > 1 block for Final call */
+                *outSz = writtenSz;
+                return 0;
+            }
+        }
+        else {
+            /* All input has been absorbed into aes->ctsBlock */
+            *outSz = 0;
+            return 0;
+        }
+    }
+    if (inSz > WC_AES_BLOCK_SIZE) {
+        /* We need to store the last two full or partial blocks */
+        blocks = (inSz + (WC_AES_BLOCK_SIZE - 1)) / WC_AES_BLOCK_SIZE;
+        blocks -= 2;
+    }
+    if (tmpOutSz < blocks * WC_AES_BLOCK_SIZE)
+        return BUFFER_E;
+    if (enc)
+        ret = wc_AesCbcEncrypt(aes, out, in, blocks * WC_AES_BLOCK_SIZE);
+    else
+        ret = wc_AesCbcDecrypt(aes, out, in, blocks * WC_AES_BLOCK_SIZE);
+    in += blocks * WC_AES_BLOCK_SIZE;
+    inSz -= blocks * WC_AES_BLOCK_SIZE;
+    XMEMCPY(aes->ctsBlock, in, inSz);
+    aes->left = inSz;
+    writtenSz += blocks * WC_AES_BLOCK_SIZE;
+    *outSz = writtenSz;
+    return ret;
+}
+
+/* Incremental API */
+int wc_AesCtsEncryptUpdate(Aes* aes, byte* out, word32* outSz,
+                           const byte* in, word32 inSz)
+{
+    return AesCtsUpdate(aes, out, outSz, in, inSz, 1);
+}
+
+int wc_AesCtsEncryptFinal(Aes* aes, byte* out, word32* outSz)
+{
+    int ret = 0;
+
+    if (aes == NULL || out == NULL || outSz == NULL)
+        return BAD_FUNC_ARG;
+    if (*outSz < aes->left)
+        return BUFFER_E;
+
+    /* Input must be at least two complete or partial blocks */
+    if (aes->left <= WC_AES_BLOCK_SIZE)
+        return BAD_FUNC_ARG;
+
+    /* Zero padding */
+    XMEMSET(aes->ctsBlock + aes->left, 0, (WC_AES_BLOCK_SIZE * 2) - aes->left);
+
+    ret = wc_AesCbcEncrypt(aes, aes->ctsBlock, aes->ctsBlock,
+                           WC_AES_BLOCK_SIZE * 2);
+    if (ret != 0)
+        return ret;
+
+    XMEMCPY(out, aes->ctsBlock + WC_AES_BLOCK_SIZE, WC_AES_BLOCK_SIZE);
+    XMEMCPY(out + WC_AES_BLOCK_SIZE, aes->ctsBlock,
+            aes->left - WC_AES_BLOCK_SIZE);
+    *outSz = aes->left;
+    return ret;
+}
+
+int wc_AesCtsDecryptUpdate(Aes* aes, byte* out, word32* outSz,
+                           const byte* in, word32 inSz)
+{
+    return AesCtsUpdate(aes, out, outSz, in, inSz, 0);
+}
+
+int wc_AesCtsDecryptFinal(Aes* aes, byte* out, word32* outSz)
+{
+    int ret = 0;
+    byte iv[WC_AES_BLOCK_SIZE];
+    byte tmp[WC_AES_BLOCK_SIZE];
+    word32 partialSz;
+    word32 padSz;
+
+    if (aes == NULL || out == NULL || outSz == NULL)
+        return BAD_FUNC_ARG;
+    if (*outSz < aes->left)
+        return BUFFER_E;
+
+    /* Input must be at least two complete or partial blocks */
+    if (aes->left <= WC_AES_BLOCK_SIZE)
+        return BAD_FUNC_ARG;
+
+    partialSz = aes->left - WC_AES_BLOCK_SIZE;
+    padSz = 2 * WC_AES_BLOCK_SIZE - aes->left;
+    /* Zero pad */
+    XMEMSET(aes->ctsBlock + aes->left, 0, padSz);
+
+    /* Store IV */
+    XMEMCPY(iv, aes->reg, WC_AES_BLOCK_SIZE);
+    /* Load IV */
+    XMEMCPY(aes->reg, aes->ctsBlock + WC_AES_BLOCK_SIZE, WC_AES_BLOCK_SIZE);
+
+    ret = wc_AesCbcDecrypt(aes, tmp, aes->ctsBlock, WC_AES_BLOCK_SIZE);
+    if (ret != 0)
+        return ret;
+
+    /* Write out partial block */
+    XMEMCPY(out + WC_AES_BLOCK_SIZE, tmp, partialSz);
+    /* Retrieve the padding */
+    XMEMCPY(aes->ctsBlock + aes->left, tmp + partialSz, padSz);
+    /* Restore IV */
+    XMEMCPY(aes->reg, iv, WC_AES_BLOCK_SIZE);
+
+    ret = wc_AesCbcDecrypt(aes, out, aes->ctsBlock + WC_AES_BLOCK_SIZE,
+                           WC_AES_BLOCK_SIZE);
+    if (ret != 0)
+        return ret;
+
+    *outSz = aes->left;
+    return ret;
+}
+
+#endif /* WOLFSSL_AES_CTS */
+
+
 #endif /* !NO_AES */
diff --git a/wolfssl/wolfcrypt/aes.h b/wolfssl/wolfcrypt/aes.h
index 0ce8acc02e..128611cc54 100644
--- a/wolfssl/wolfcrypt/aes.h
+++ b/wolfssl/wolfcrypt/aes.h
@@ -334,7 +334,8 @@ struct Aes {
     WC_ASYNC_DEV asyncDev;
 #endif /* WOLFSSL_ASYNC_CRYPT */
 #if defined(WOLFSSL_AES_COUNTER) || defined(WOLFSSL_AES_CFB) || \
-    defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS)
+    defined(WOLFSSL_AES_OFB) || defined(WOLFSSL_AES_XTS) || \
+    defined(WOLFSSL_AES_CTS)
     word32  left;            /* unused bytes left from last call */
 #endif
 #ifdef WOLFSSL_XILINX_CRYPT
@@ -425,6 +426,9 @@ struct Aes {
                                * trackable by sanitizers.
                                */
 #endif
+#ifdef WOLFSSL_AES_CTS
+    byte ctsBlock[WC_AES_BLOCK_SIZE * 2];
+#endif
 };
 
 #ifndef WC_AES_TYPE_DEFINED
@@ -849,6 +853,28 @@ WOLFSSL_API int wc_AesEaxFree(AesEax* eax);
 
 #endif /* WOLFSSL_AES_EAX */
 
+#ifdef WOLFSSL_AES_CTS
+/* Ciphertext stealing encryption compatible with RFC2040 and RFC3962. */
+
+/* One-shot API */
+WOLFSSL_API int wc_AesCtsEncrypt(const byte* key, word32 keySz, byte* out,
+                                 const byte* in, word32 inSz,
+                                 const byte* iv);
+WOLFSSL_API int wc_AesCtsDecrypt(const byte* key, word32 keySz, byte* out,
+                                 const byte* in, word32 inSz,
+                                 const byte* iv);
+
+/* Incremental API */
+WOLFSSL_API int wc_AesCtsEncryptUpdate(Aes* aes, byte* out, word32* outSz,
+                                       const byte* in, word32 inSz);
+WOLFSSL_API int wc_AesCtsDecryptUpdate(Aes* aes, byte* out, word32* outSz,
+                                       const byte* in, word32 inSz);
+WOLFSSL_API int wc_AesCtsEncryptFinal(Aes* aes, byte* out, word32* outSz);
+WOLFSSL_API int wc_AesCtsDecryptFinal(Aes* aes, byte* out, word32* outSz);
+
+
+#endif
+
 #if defined(__aarch64__) && defined(WOLFSSL_ARMASM) && \
     !defined(WOLFSSL_ARMASM_NO_HW_CRYPTO)
 
diff --git a/wolfssl/wolfcrypt/settings.h b/wolfssl/wolfcrypt/settings.h
index 12397f1828..cbdb35f3d2 100644
--- a/wolfssl/wolfcrypt/settings.h
+++ b/wolfssl/wolfcrypt/settings.h
@@ -3170,6 +3170,11 @@ extern void uITRON4_free(void *p) ;
         #define WOLFSSL_AES_DIRECT
         #endif
     #endif
+    #ifdef WOLFSSL_AES_CTS
+        #if defined(NO_AES_CBC) || !defined(HAVE_AES_CBC)
+            #error "AES CTS requires AES CBC"
+        #endif
+    #endif
 #endif
 
 #if (defined(WOLFSSL_TLS13) && defined(WOLFSSL_NO_TLS12)) || \


Reviews:
user: login: LinuxJedi
body: Given a first-pass review. Looks good to me, but I don't know this area of code well.

Please add dox comments to `doc/dox_comments/header_files/aes.h` for the new API calls.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-04-10T09:12:05Z
user: login: LinuxJedi
body: Many thanks! Kicking it to another reviewer.
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-11T16:23:07Z
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 6 out of 7 changed files in this pull request and generated 1 comment.

<details>
<summary>Files not reviewed (1)</summary>

* **configure.ac**: Language not supported
</details>

<details>
<summary>Comments suppressed due to low confidence (1)</summary>

**wolfcrypt/src/aes.c:15018**
* The usage of 'min' here may rely on a macro or function not defined in this file. Please ensure that 'min' is defined or replace it with a safe alternative (e.g., MIN macro) to avoid potential compilation issues.
```
word32 copySz = min(inSz, (WC_AES_BLOCK_SIZE * 2) - aes->left);
```
</details>


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-11T16:58:58Z
user: login: dgarske
state: DISMISSED
author_association: MEMBER
submitted_at: 2025-04-11T17:01:12Z
user: login: julek-wolfssl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:40:10Z
user: login: julek-wolfssl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:44:37Z
user: login: julek-wolfssl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:44:42Z
user: login: julek-wolfssl
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-04-14T15:45:07Z
user: login: douzzer
body: tested with `wolfssl-multi-test.sh ... check-source-text clang-tidy-all-sp-all sanitizer-all-intelasm-c-fallback-fuzzer all-c89-cppcheck all-c89-clang-tidy` and `llm-analyze-changes` with `qwen2.5-72B-32k`

state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-15T23:33:01Z

Review comments:
diff_hunk: @@ -263,6 +263,230 @@ int test_wc_AesCbcEncryptDecrypt(void)
     return EXPECT_RESULT();
 } /* END test_wc_AesCbcEncryptDecrypt */
 
+/*******************************************************************************
+ * AES-CTS
+ ******************************************************************************/
+
+int test_wc_AesCtsEncryptDecrypt(void)
+{
+    EXPECT_DECLS;
+#if !defined(NO_AES) && defined(WOLFSSL_AES_CTS) && \
+    defined(HAVE_AES_DECRYPT) && defined(WOLFSSL_AES_128)
+    /* Test vectors taken form RFC3962 Appendix B */
+    struct {
path: tests/api/test_aes.c
user: login: dgarske
body: Can this be `const`?
created_at: 2025-04-11T16:58:47Z
author_association: MEMBER
diff_hunk: @@ -1733,5 +1733,244 @@ WOLFSSL_API int wc_AesEaxDecryptFinal(AesEax* eax,
 */
 WOLFSSL_API int wc_AesEaxFree(AesEax* eax);
 
+/*!
+    \ingroup AES
+    \brief This function performs AES encryption using Ciphertext Stealing (CTS)
+    mode. It is a one-shot API that handles all operations in a single call.
+
+    \return 0 on successful encryption.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \return other negative error codes for encryption failures.
+
+    \param key pointer to the AES key used for encryption.
+    \param keySz size of the AES key in bytes (16, 24, or 32 bytes).
+    \param[out] out buffer to hold the encrypted ciphertext. Must be at least
+    the size of the input.
+    \param in pointer to the plaintext input data to encrypt.
+    \param inSz size of the plaintext input data in bytes.
+    \param iv pointer to the initialization vector (IV) used for encryption.
+    Must be 16 bytes.
+
+    _Example_
+    \code
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte plaintext[] = { 0x01, 0x02, 0x03, 0x04, 0x05 };
+        byte ciphertext[sizeof(plaintext)];
+
+        int ret = wc_AesCtsEncrypt(key, sizeof(key), ciphertext, plaintext,
+            sizeof(plaintext), iv);
+        if (ret != 0) {
+        // handle encryption error
+    }
+    \endcode
 
+    \sa wc_AesCtsDecrypt
+*/
+int wc_AesCtsEncrypt(const byte* key, word32 keySz, byte* out,
+                     const byte* in, word32 inSz,
+                     const byte* iv);
 
+/*!
+    \ingroup AES
+    \brief This function performs AES encryption using Ciphertext Stealing (CTS)
+     mode. It is a one-shot API that handles all operations in a single call.
+
+    \return 0 on successful encryption.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \return other negative error codes for encryption failures.
+
+    \param key pointer to the AES key used for encryption.
+    \param keySz size of the AES key in bytes (16, 24, or 32 bytes).
+    \param[out] out buffer to hold the encrypted ciphertext. Must be at least
+                 the same size as the input plaintext.
+    \param in pointer to the plaintext input data to encrypt.
+    \param inSz size of the plaintext input data in bytes.
+    \param iv pointer to the initialization vector (IV) used for encryption.
+             Must be 16 bytes.
+    _Example_
+    \code
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte plaintext[] = { 0x01, 0x02, 0x03, 0x04, 0x05 };
+        byte ciphertext[sizeof(plaintext)];
+        int ret = wc_AesCtsEncrypt(key, sizeof(key), ciphertext, plaintext,
+                                   sizeof(plaintext), iv);
+        if (ret != 0) {
+            // handle encryption error
+        }
+    \endcode
+    \sa wc_AesCtsDecrypt
+*/
+int wc_AesCtsEncrypt(const byte* key, word32 keySz, byte* out,
path: doc/dox_comments/header_files/aes.h
user: login: Copilot
body: Duplicate declaration of wc_AesCtsEncrypt found in the header documentation. Removing the duplicate would reduce confusion and maintenance overhead.
created_at: 2025-04-11T16:58:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -263,6 +263,230 @@ int test_wc_AesCbcEncryptDecrypt(void)
     return EXPECT_RESULT();
 } /* END test_wc_AesCbcEncryptDecrypt */
 
+/*******************************************************************************
+ * AES-CTS
+ ******************************************************************************/
+
+int test_wc_AesCtsEncryptDecrypt(void)
+{
+    EXPECT_DECLS;
+#if !defined(NO_AES) && defined(WOLFSSL_AES_CTS) && \
+    defined(HAVE_AES_DECRYPT) && defined(WOLFSSL_AES_128)
+    /* Test vectors taken form RFC3962 Appendix B */
+    struct {
+        const char* input;
+        const char* output;
+        size_t inLen;
+        size_t outLen;
+    } vects[] = {
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20",
+            "\xc6\x35\x35\x68\xf2\xbf\x8c\xb4\xd8\xa5\x80\x36\x2d\xa7\xff\x7f"
+            "\x97",
+            17, 17
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20",
+            "\xfc\x00\x78\x3e\x0e\xfd\xb2\xc1\xd4\x45\xd4\xc8\xef\xf7\xed\x22"
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5",
+            31, 31
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43",
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5\xa8"
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84",
+            32, 32
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43"
+            "\x68\x69\x63\x6b\x65\x6e\x2c\x20\x70\x6c\x65\x61\x73\x65\x2c",
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84"
+            "\xb3\xff\xfd\x94\x0c\x16\xa1\x8c\x1b\x55\x49\xd2\xf8\x38\x02\x9e"
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5",
+            47, 47
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43"
+            "\x68\x69\x63\x6b\x65\x6e\x2c\x20\x70\x6c\x65\x61\x73\x65\x2c\x20",
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84"
+            "\x9d\xad\x8b\xbb\x96\xc4\xcd\xc0\x3b\xc1\x03\xe1\xa1\x94\xbb\xd8"
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5\xa8",
+            48, 48
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43"
+            "\x68\x69\x63\x6b\x65\x6e\x2c\x20\x70\x6c\x65\x61\x73\x65\x2c\x20"
+            "\x61\x6e\x64\x20\x77\x6f\x6e\x74\x6f\x6e\x20\x73\x6f\x75\x70\x2e",
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84"
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5\xa8"
+            "\x48\x07\xef\xe8\x36\xee\x89\xa5\x26\x73\x0d\xbc\x2f\x7b\xc8\x40"
+            "\x9d\xad\x8b\xbb\x96\xc4\xcd\xc0\x3b\xc1\x03\xe1\xa1\x94\xbb\xd8",
+            64, 64
+        }
+    };
+    byte keyBytes[AES_128_KEY_SIZE] = {
path: tests/api/test_aes.c
user: login: dgarske
body: Same... `const`.
created_at: 2025-04-11T16:59:03Z
author_association: MEMBER
diff_hunk: @@ -14906,4 +14914,278 @@ int wc_AesEaxFree(AesEax* eax)
 
 #endif /* WOLFSSL_AES_EAX */
 
+#ifdef WOLFSSL_AES_CTS
+
+
+/* One-shot API */
+int wc_AesCtsEncrypt(const byte* key, word32 keySz, byte* out,
+                     const byte* in, word32 inSz,
+                     const byte* iv)
+{
+#ifdef WOLFSSL_SMALL_STACK
+    Aes *aes = NULL;
+#else
+    Aes _aes;
path: wolfcrypt/src/aes.c
user: login: dgarske
body: Prefer `Aes aes[1]`. Single line that does the same.
created_at: 2025-04-11T17:00:16Z
author_association: MEMBER
diff_hunk: @@ -1733,5 +1733,244 @@ WOLFSSL_API int wc_AesEaxDecryptFinal(AesEax* eax,
 */
 WOLFSSL_API int wc_AesEaxFree(AesEax* eax);
 
+/*!
+    \ingroup AES
+    \brief This function performs AES encryption using Ciphertext Stealing (CTS)
+    mode. It is a one-shot API that handles all operations in a single call.
+
+    \return 0 on successful encryption.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \return other negative error codes for encryption failures.
+
+    \param key pointer to the AES key used for encryption.
+    \param keySz size of the AES key in bytes (16, 24, or 32 bytes).
+    \param[out] out buffer to hold the encrypted ciphertext. Must be at least
+    the size of the input.
+    \param in pointer to the plaintext input data to encrypt.
+    \param inSz size of the plaintext input data in bytes.
+    \param iv pointer to the initialization vector (IV) used for encryption.
+    Must be 16 bytes.
+
+    _Example_
+    \code
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte plaintext[] = { 0x01, 0x02, 0x03, 0x04, 0x05 };
+        byte ciphertext[sizeof(plaintext)];
+
+        int ret = wc_AesCtsEncrypt(key, sizeof(key), ciphertext, plaintext,
+            sizeof(plaintext), iv);
+        if (ret != 0) {
+        // handle encryption error
+    }
+    \endcode
 
+    \sa wc_AesCtsDecrypt
+*/
+int wc_AesCtsEncrypt(const byte* key, word32 keySz, byte* out,
+                     const byte* in, word32 inSz,
+                     const byte* iv);
 
+/*!
+    \ingroup AES
+    \brief This function performs AES encryption using Ciphertext Stealing (CTS)
+     mode. It is a one-shot API that handles all operations in a single call.
+
+    \return 0 on successful encryption.
+    \return BAD_FUNC_ARG if input arguments are invalid.
+    \return other negative error codes for encryption failures.
+
+    \param key pointer to the AES key used for encryption.
+    \param keySz size of the AES key in bytes (16, 24, or 32 bytes).
+    \param[out] out buffer to hold the encrypted ciphertext. Must be at least
+                 the same size as the input plaintext.
+    \param in pointer to the plaintext input data to encrypt.
+    \param inSz size of the plaintext input data in bytes.
+    \param iv pointer to the initialization vector (IV) used for encryption.
+             Must be 16 bytes.
+    _Example_
+    \code
+        byte key[16] = { 0 };
+        byte iv[16] = { 0 };
+        byte plaintext[] = { 0x01, 0x02, 0x03, 0x04, 0x05 };
+        byte ciphertext[sizeof(plaintext)];
+        int ret = wc_AesCtsEncrypt(key, sizeof(key), ciphertext, plaintext,
+                                   sizeof(plaintext), iv);
+        if (ret != 0) {
+            // handle encryption error
+        }
+    \endcode
+    \sa wc_AesCtsDecrypt
+*/
+int wc_AesCtsEncrypt(const byte* key, word32 keySz, byte* out,
path: doc/dox_comments/header_files/aes.h
user: login: julek-wolfssl
body: Its detecting the definition in the docs as a duplicate.
created_at: 2025-04-14T15:40:10Z
author_association: MEMBER
diff_hunk: @@ -263,6 +263,230 @@ int test_wc_AesCbcEncryptDecrypt(void)
     return EXPECT_RESULT();
 } /* END test_wc_AesCbcEncryptDecrypt */
 
+/*******************************************************************************
+ * AES-CTS
+ ******************************************************************************/
+
+int test_wc_AesCtsEncryptDecrypt(void)
+{
+    EXPECT_DECLS;
+#if !defined(NO_AES) && defined(WOLFSSL_AES_CTS) && \
+    defined(HAVE_AES_DECRYPT) && defined(WOLFSSL_AES_128)
+    /* Test vectors taken form RFC3962 Appendix B */
+    struct {
path: tests/api/test_aes.c
user: login: julek-wolfssl
body: Adding
created_at: 2025-04-14T15:44:37Z
author_association: MEMBER
diff_hunk: @@ -263,6 +263,230 @@ int test_wc_AesCbcEncryptDecrypt(void)
     return EXPECT_RESULT();
 } /* END test_wc_AesCbcEncryptDecrypt */
 
+/*******************************************************************************
+ * AES-CTS
+ ******************************************************************************/
+
+int test_wc_AesCtsEncryptDecrypt(void)
+{
+    EXPECT_DECLS;
+#if !defined(NO_AES) && defined(WOLFSSL_AES_CTS) && \
+    defined(HAVE_AES_DECRYPT) && defined(WOLFSSL_AES_128)
+    /* Test vectors taken form RFC3962 Appendix B */
+    struct {
+        const char* input;
+        const char* output;
+        size_t inLen;
+        size_t outLen;
+    } vects[] = {
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20",
+            "\xc6\x35\x35\x68\xf2\xbf\x8c\xb4\xd8\xa5\x80\x36\x2d\xa7\xff\x7f"
+            "\x97",
+            17, 17
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20",
+            "\xfc\x00\x78\x3e\x0e\xfd\xb2\xc1\xd4\x45\xd4\xc8\xef\xf7\xed\x22"
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5",
+            31, 31
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43",
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5\xa8"
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84",
+            32, 32
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43"
+            "\x68\x69\x63\x6b\x65\x6e\x2c\x20\x70\x6c\x65\x61\x73\x65\x2c",
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84"
+            "\xb3\xff\xfd\x94\x0c\x16\xa1\x8c\x1b\x55\x49\xd2\xf8\x38\x02\x9e"
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5",
+            47, 47
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43"
+            "\x68\x69\x63\x6b\x65\x6e\x2c\x20\x70\x6c\x65\x61\x73\x65\x2c\x20",
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84"
+            "\x9d\xad\x8b\xbb\x96\xc4\xcd\xc0\x3b\xc1\x03\xe1\xa1\x94\xbb\xd8"
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5\xa8",
+            48, 48
+        },
+        {
+            "\x49\x20\x77\x6f\x75\x6c\x64\x20\x6c\x69\x6b\x65\x20\x74\x68\x65"
+            "\x20\x47\x65\x6e\x65\x72\x61\x6c\x20\x47\x61\x75\x27\x73\x20\x43"
+            "\x68\x69\x63\x6b\x65\x6e\x2c\x20\x70\x6c\x65\x61\x73\x65\x2c\x20"
+            "\x61\x6e\x64\x20\x77\x6f\x6e\x74\x6f\x6e\x20\x73\x6f\x75\x70\x2e",
+            "\x97\x68\x72\x68\xd6\xec\xcc\xc0\xc0\x7b\x25\xe2\x5e\xcf\xe5\x84"
+            "\x39\x31\x25\x23\xa7\x86\x62\xd5\xbe\x7f\xcb\xcc\x98\xeb\xf5\xa8"
+            "\x48\x07\xef\xe8\x36\xee\x89\xa5\x26\x73\x0d\xbc\x2f\x7b\xc8\x40"
+            "\x9d\xad\x8b\xbb\x96\xc4\xcd\xc0\x3b\xc1\x03\xe1\xa1\x94\xbb\xd8",
+            64, 64
+        }
+    };
+    byte keyBytes[AES_128_KEY_SIZE] = {
path: tests/api/test_aes.c
user: login: julek-wolfssl
body: Adding
created_at: 2025-04-14T15:44:42Z
author_association: MEMBER
diff_hunk: @@ -14906,4 +14914,278 @@ int wc_AesEaxFree(AesEax* eax)
 
 #endif /* WOLFSSL_AES_EAX */
 
+#ifdef WOLFSSL_AES_CTS
+
+
+/* One-shot API */
+int wc_AesCtsEncrypt(const byte* key, word32 keySz, byte* out,
+                     const byte* in, word32 inSz,
+                     const byte* iv)
+{
+#ifdef WOLFSSL_SMALL_STACK
+    Aes *aes = NULL;
+#else
+    Aes _aes;
path: wolfcrypt/src/aes.c
user: login: julek-wolfssl
body: Was following other functions in `aes.c` but a single element array is cleaner.
created_at: 2025-04-14T15:45:07Z
author_association: MEMBER

PR comments:
user: login: julek-wolfssl
created_at: 2025-03-31T18:28:16Z
body: retest this please.
author_association: MEMBER
user: login: LinuxJedi
created_at: 2025-04-01T12:03:22Z
body: I think the Jenkins failure `check-source-text` is genuine, I just can't immediately see what is triggering it.
author_association: MEMBER
user: login: LinuxJedi
created_at: 2025-04-09T05:34:20Z
body: retest this please.
author_association: MEMBER
user: login: douzzer
created_at: 2025-04-15T23:35:45Z
body: would like to see a test for this added to `wolfcrypt/test/test.c`.

author_association: CONTRIBUTOR