From a9f23deeba5d821cc9119b9247d186a4aca4b37f Mon Sep 17 00:00:00 2001 From: Karel Miko Date: Thu, 16 Apr 2026 15:51:33 +0200 Subject: [PATCH 1/2] XChaCha20 / XChaCha20-Poly1305 --- doc/crypt.tex | 68 ++++++++- .../chachapoly/chacha20poly1305_setiv.c | 57 ++++++- .../chachapoly/chacha20poly1305_test.c | 65 ++++++++ src/headers/tomcrypt_cipher.h | 16 ++ src/headers/tomcrypt_custom.h | 5 + src/misc/crypt/crypt.c | 3 + src/stream/chacha/xchacha20_memory.c | 34 +++++ src/stream/chacha/xchacha20_setup.c | 144 ++++++++++++++++++ src/stream/chacha/xchacha20_test.c | 119 +++++++++++++++ tests/cipher_hash_test.c | 3 + 10 files changed, 509 insertions(+), 5 deletions(-) create mode 100644 src/stream/chacha/xchacha20_memory.c create mode 100644 src/stream/chacha/xchacha20_setup.c create mode 100644 src/stream/chacha/xchacha20_test.c diff --git a/doc/crypt.tex b/doc/crypt.tex index fc879fa9a..be7c6dca8 100644 --- a/doc/crypt.tex +++ b/doc/crypt.tex @@ -1305,14 +1305,28 @@ \chapter{Stream Ciphers} Note: You shouldn't use the keystream interface as a PRNG, as it doesn't allow to re-seed the internal state. -\mysection{ChaCha} +\mysection{ChaCha and XChaCha20} \textit{ChaCha} is currently the most modern stream cipher included in LibTomCrypt, so use this one unless you have a reason for using some of the older algorithms. +\vspace{1mm} + +\textit{XChaCha20} is a variant of \textit{ChaCha20} designed to accept only a 256-bit key and a +longer 192-bit nonce (24 bytes), which dramatically reduces the risk of nonce collisions when +generating nonces randomly. Initialization is the only difference between \textit{XChaCha20} +and \textit{ChaCha20}. Even the \textit{chacha\_state} is the same. Thereafter, +chacha\_crypt(), chacha\_keystream(), and chacha\_done() are used unaltered. +chacha\_ivctr32() and chacha\_ivctr64() are NOT used with xchacha20\_setup(). +\vspace{1mm} For more information about ChaCha see \url{https://en.wikipedia.org/wiki/ChaCha_(cipher)}. +\vspace{1mm} -Supported key size: 16 or 32 bytes (128 or 256 bits). +For more information about XChaCha20 see +\url{https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha}. +\vspace{1mm} + +Supported key size: 16 or 32 bytes (128 or 256 bits) for ChaCha, 32 bytes only for XChaCha20. You can initialize ChaCha with 96bit \textit{nonce} + 32bit \textit{counter}: \begin{verbatim} @@ -1328,9 +1342,17 @@ \chapter{Stream Ciphers} err = chacha_ivctr64(&st, nonce, 8, initial_64bit_ctr); \end{verbatim} +To initialize \textit{XChaCha20} for the recommended 20 rounds with a 256-bit +key (32 bytes) and a 192-bit nonce (24 bytes), use: +\begin{verbatim} +chacha_state st; +err = xchacha20_setup(&st, key, key_len, nonce, nonce_len, rounds); +\end{verbatim} + The \textit{chacha\_setup} takes the number of rounds as a parameter -- choose 20 if you are not sure. As always never ever use the same key + nonce pair more than once. +Both \textit{ChaCha} and \textit{XChaCha20} use the following functions. For the actual encryption or decryption you have to call: \begin{verbatim} err = chacha_crypt(&st, in_buffer, in_len, out_buffer); @@ -1354,6 +1376,39 @@ \chapter{Stream Ciphers} err = chacha_memory(key, keylen, iv, ivlen, datain, datalen, rounds, dataout); \end{verbatim} +To encrypt plaintext (or decrypt ciphertext) using XChaCha20 for data already in +memory with a single function call, the following function may be used. +\begin{verbatim} +err = xchacha20_memory(key, keylen, rounds, nonce, nonce_len, datain, datalen, dataout); +\end{verbatim} + +For both \textit{ChaCha} and \textit{XChaCha20} rounds must be an even number +and if set to 0 the default number of rounds, 20, will be used. +\vspace{1mm} + +\subsection{HChaCha20} + +\textit{HChaCha20} is the key derivation function underlying \textit{XChaCha20}. It applies the +ChaCha20 block function (without the final addition step) to a 256-bit key and a 128-bit input, +producing a 256-bit derived key. It is also useful as a standalone KDF, for example in NaCl-style +\textit{crypto\_box} constructions. + +\index{xchacha20\_hchacha20()} +\begin{verbatim} +int xchacha20_hchacha20(unsigned char *out, unsigned long outlen, + const unsigned char *key, unsigned long keylen, + const unsigned char *in, unsigned long inlen, + int rounds); +\end{verbatim} +This derives a 32-byte subkey from a 32-byte \textit{key} and a 16-byte \textit{in} using +\textit{rounds} ChaCha20 rounds (0 = default 20). The output is stored in \textit{out} +(\textit{outlen} must be 32, \textit{keylen} must be 32, \textit{inlen} must be 16). +\vspace{1mm} + +If you define \textit{LTC\_XCHACHA20} to include \textit{XChaCha20} and \textit{HChaCha20} in a +minimal \textit{libtomcrypt} library build, you must also define \textit{LTC\_CHACHA}. +\vspace{1mm} + \mysection{Salsa20 and XSalsa20} \textit{Salsa20} was Daniel Bernstein's submission to the EU eSTREAM @@ -2462,6 +2517,9 @@ \subsection{Example Usage} This authenticated encryption is based on the ChaCha20 stream cipher and Poly1305 authenticator. It is defined by \url{https://tools.ietf.org/html/rfc8439}. +When \textit{LTC\_XCHACHA20} is enabled, this mode also supports 24-byte IVs for +\textit{XChaCha20--Poly1305} as defined by +\url{https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha}. IMPORTANT NOTICE: The Chacha20--Poly1305 implementation of LibTomCrypt is compliant to \textbf{RFC8439}, which differs slightly to what OpenSSH defined as \textbf{chacha20-poly1305@openssh.com}. The OpenSSH compatibility mode can be enabled @@ -2489,8 +2547,10 @@ \subsection{Initialization Vector} const unsigned char *iv, unsigned long ivlen); \end{verbatim} -This adds the initialization vector from \textit{iv} of length \textit{ivlen} octects (valid lengths: 8 or 12) to -the ChaCha20--Poly1305 state \textit{st}. +This adds the initialization vector from \textit{iv} of length \textit{ivlen} octects (valid lengths: 8, 12, +or 24 when \textit{LTC\_XCHACHA20} is enabled) to the ChaCha20--Poly1305 state \textit{st}. +When \textit{ivlen} is 24, this operates as \textit{XChaCha20--Poly1305}, internally deriving +a subkey via HChaCha20 before proceeding. \index{chacha20poly1305\_setiv\_rfc7905()} \begin{verbatim} diff --git a/src/encauth/chachapoly/chacha20poly1305_setiv.c b/src/encauth/chachapoly/chacha20poly1305_setiv.c index d5ec3e692..e5937b41c 100644 --- a/src/encauth/chachapoly/chacha20poly1305_setiv.c +++ b/src/encauth/chachapoly/chacha20poly1305_setiv.c @@ -9,7 +9,7 @@ Set IV + counter data to the ChaCha20Poly1305 state and reset the context @param st The ChaCha20Poly1305 state @param iv The IV data to add - @param ivlen The length of the IV (must be 12 or 8) + @param ivlen The length of the IV (must be 12 or 8, or 24 when LTC_XCHACHA20 is enabled) @return CRYPT_OK on success */ int chacha20poly1305_setiv(chacha20poly1305_state *st, const unsigned char *iv, unsigned long ivlen) @@ -20,7 +20,62 @@ int chacha20poly1305_setiv(chacha20poly1305_state *st, const unsigned char *iv, LTC_ARGCHK(st != NULL); LTC_ARGCHK(iv != NULL); +#ifdef LTC_XCHACHA20 + LTC_ARGCHK(ivlen == 12 || ivlen == 8 || ivlen == 24); +#else LTC_ARGCHK(ivlen == 12 || ivlen == 8); +#endif + +#ifdef LTC_XCHACHA20 + if (ivlen == 24) { + unsigned char orig_key[32]; + + /* extract the original key from state (stored by chacha_setup via chacha20poly1305_init) */ + STORE32L(st->chacha.input[4], orig_key + 0); + STORE32L(st->chacha.input[5], orig_key + 4); + STORE32L(st->chacha.input[6], orig_key + 8); + STORE32L(st->chacha.input[7], orig_key + 12); + STORE32L(st->chacha.input[8], orig_key + 16); + STORE32L(st->chacha.input[9], orig_key + 20); + STORE32L(st->chacha.input[10], orig_key + 24); + STORE32L(st->chacha.input[11], orig_key + 28); + + /* derive subkey via HChaCha20 and set up state with counter=0 */ + if ((err = xchacha20_setup(&st->chacha, orig_key, 32, iv, 24, 20)) != CRYPT_OK) { + zeromem(orig_key, sizeof(orig_key)); + return err; + } + + /* copy state to temporary for Poly1305 key derivation (counter=0) */ + for (i = 0; i < 16; i++) tmp_st.input[i] = st->chacha.input[i]; + tmp_st.input[12] = 0; + tmp_st.rounds = st->chacha.rounds; + tmp_st.ksleft = 0; + tmp_st.ivlen = 12; /* use IETF counter mode for keystream generation */ + + /* generate Poly1305 key from block 0 */ + if ((err = chacha_keystream(&tmp_st, polykey, 32)) != CRYPT_OK) { + zeromem(orig_key, sizeof(orig_key)); + return err; + } + + /* set main state counter to 1 for encryption */ + st->chacha.input[12] = 1; + st->chacha.ksleft = 0; + + /* initialize Poly1305 */ + if ((err = poly1305_init(&st->poly, polykey, 32)) != CRYPT_OK) { + zeromem(orig_key, sizeof(orig_key)); + return err; + } + st->ctlen = 0; + st->aadlen = 0; + st->aadflg = 1; + + zeromem(orig_key, sizeof(orig_key)); + return CRYPT_OK; + } +#endif /* set IV for chacha20 */ if (ivlen == 12) { diff --git a/src/encauth/chachapoly/chacha20poly1305_test.c b/src/encauth/chachapoly/chacha20poly1305_test.c index f6f9c130b..902985c30 100644 --- a/src/encauth/chachapoly/chacha20poly1305_test.c +++ b/src/encauth/chachapoly/chacha20poly1305_test.c @@ -152,6 +152,71 @@ int chacha20poly1305_test(void) } } +#ifdef LTC_XCHACHA20 + /* XChaCha20-Poly1305 AEAD test (draft-irtf-cfrg-xchacha, Appendix A.3.1) */ + { + unsigned char xk[] = { 0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f, + 0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f }; + unsigned char xi24[] = { 0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x4b, + 0x4c,0x4d,0x4e,0x4f,0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x57 }; + unsigned char xaad[] = { 0x50,0x51,0x52,0x53,0xc0,0xc1,0xc2,0xc3,0xc4,0xc5,0xc6,0xc7 }; + unsigned char xpt[] = { 0x4c,0x61,0x64,0x69,0x65,0x73,0x20,0x61,0x6e,0x64,0x20,0x47,0x65,0x6e,0x74,0x6c, + 0x65,0x6d,0x65,0x6e,0x20,0x6f,0x66,0x20,0x74,0x68,0x65,0x20,0x63,0x6c,0x61,0x73, + 0x73,0x20,0x6f,0x66,0x20,0x27,0x39,0x39,0x3a,0x20,0x49,0x66,0x20,0x49,0x20,0x63, + 0x6f,0x75,0x6c,0x64,0x20,0x6f,0x66,0x66,0x65,0x72,0x20,0x79,0x6f,0x75,0x20,0x6f, + 0x6e,0x6c,0x79,0x20,0x6f,0x6e,0x65,0x20,0x74,0x69,0x70,0x20,0x66,0x6f,0x72,0x20, + 0x74,0x68,0x65,0x20,0x66,0x75,0x74,0x75,0x72,0x65,0x2c,0x20,0x73,0x75,0x6e,0x73, + 0x63,0x72,0x65,0x65,0x6e,0x20,0x77,0x6f,0x75,0x6c,0x64,0x20,0x62,0x65,0x20,0x69, + 0x74,0x2e }; + unsigned char xenc[] = { 0xbd,0x6d,0x17,0x9d,0x3e,0x83,0xd4,0x3b,0x95,0x76,0x57,0x94,0x93,0xc0,0xe9,0x39, + 0x57,0x2a,0x17,0x00,0x25,0x2b,0xfa,0xcc,0xbe,0xd2,0x90,0x2c,0x21,0x39,0x6c,0xbb, + 0x73,0x1c,0x7f,0x1b,0x0b,0x4a,0xa6,0x44,0x0b,0xf3,0xa8,0x2f,0x4e,0xda,0x7e,0x39, + 0xae,0x64,0xc6,0x70,0x8c,0x54,0xc2,0x16,0xcb,0x96,0xb7,0x2e,0x12,0x13,0xb4,0x52, + 0x2f,0x8c,0x9b,0xa4,0x0d,0xb5,0xd9,0x45,0xb1,0x1b,0x69,0xb9,0x82,0xc1,0xbb,0x9e, + 0x3f,0x3f,0xac,0x2b,0xc3,0x69,0x48,0x8f,0x76,0xb2,0x38,0x35,0x65,0xd3,0xff,0xf9, + 0x21,0xf9,0x66,0x4c,0x97,0x63,0x7d,0xa9,0x76,0x88,0x12,0xf6,0x15,0xc6,0x8b,0x13, + 0xb5,0x2e }; + unsigned char xtag[] = { 0xc0,0x87,0x59,0x24,0xc1,0xc7,0x98,0x79,0x47,0xde,0xaf,0xd8,0x78,0x0a,0xcf,0x49 }; + unsigned long xptlen = sizeof(xpt); + + /* encrypt with incremental API */ + if ((err = chacha20poly1305_init(&st1, xk, sizeof(xk))) != CRYPT_OK) return err; + if ((err = chacha20poly1305_setiv(&st1, xi24, sizeof(xi24))) != CRYPT_OK) return err; + if ((err = chacha20poly1305_add_aad(&st1, xaad, sizeof(xaad))) != CRYPT_OK) return err; + if ((err = chacha20poly1305_encrypt(&st1, xpt, xptlen, ct)) != CRYPT_OK) return err; + len = sizeof(emac); + if ((err = chacha20poly1305_done(&st1, emac, &len)) != CRYPT_OK) return err; + + if (ltc_compare_testvector(ct, xptlen, xenc, sizeof(xenc), "XCHACHA20POLY1305-ENC-CT", 1) != 0) return CRYPT_FAIL_TESTVECTOR; + if (ltc_compare_testvector(emac, len, xtag, sizeof(xtag), "XCHACHA20POLY1305-ENC-TAG", 2) != 0) return CRYPT_FAIL_TESTVECTOR; + + /* decrypt with incremental API */ + if ((err = chacha20poly1305_init(&st2, xk, sizeof(xk))) != CRYPT_OK) return err; + if ((err = chacha20poly1305_setiv(&st2, xi24, sizeof(xi24))) != CRYPT_OK) return err; + if ((err = chacha20poly1305_add_aad(&st2, xaad, sizeof(xaad))) != CRYPT_OK) return err; + if ((err = chacha20poly1305_decrypt(&st2, ct, xptlen, pt)) != CRYPT_OK) return err; + len = sizeof(dmac); + if ((err = chacha20poly1305_done(&st2, dmac, &len)) != CRYPT_OK) return err; + + if (ltc_compare_testvector(pt, xptlen, xpt, xptlen, "XCHACHA20POLY1305-DEC-PT", 3) != 0) return CRYPT_FAIL_TESTVECTOR; + if (ltc_compare_testvector(dmac, len, xtag, sizeof(xtag), "XCHACHA20POLY1305-DEC-TAG", 4) != 0) return CRYPT_FAIL_TESTVECTOR; + + /* chacha20poly1305_memory - encrypt with 24-byte IV */ + len = sizeof(emac); + if ((err = chacha20poly1305_memory(xk, sizeof(xk), xi24, sizeof(xi24), xaad, sizeof(xaad), xpt, + xptlen, ct, emac, &len, CHACHA20POLY1305_ENCRYPT)) != CRYPT_OK) return err; + if (ltc_compare_testvector(ct, xptlen, xenc, sizeof(xenc), "XCHACHA20POLY1305-MEM-CT", 1) != 0) return CRYPT_FAIL_TESTVECTOR; + if (ltc_compare_testvector(emac, len, xtag, sizeof(xtag), "XCHACHA20POLY1305-MEM-TAG", 2) != 0) return CRYPT_FAIL_TESTVECTOR; + + /* chacha20poly1305_memory - decrypt with 24-byte IV */ + len = sizeof(dmac); + XMEMCPY(dmac, xtag, sizeof(xtag)); + if ((err = chacha20poly1305_memory(xk, sizeof(xk), xi24, sizeof(xi24), xaad, sizeof(xaad), + ct, xptlen, pt, dmac, &len, CHACHA20POLY1305_DECRYPT)) != CRYPT_OK) return err; + if (ltc_compare_testvector(pt, xptlen, xpt, xptlen, "XCHACHA20POLY1305-MEM-PT", 3) != 0) return CRYPT_FAIL_TESTVECTOR; + } +#endif + return CRYPT_OK; #endif } diff --git a/src/headers/tomcrypt_cipher.h b/src/headers/tomcrypt_cipher.h index b370fedb0..3712f0dd3 100644 --- a/src/headers/tomcrypt_cipher.h +++ b/src/headers/tomcrypt_cipher.h @@ -1056,6 +1056,22 @@ int chacha_memory(const unsigned char *key, unsigned long keylen, unsigned l #endif /* LTC_CHACHA */ +#ifdef LTC_XCHACHA20 + +int xchacha20_hchacha20(unsigned char *out, unsigned long outlen, + const unsigned char *key, unsigned long keylen, + const unsigned char *in, unsigned long inlen, + int rounds); +int xchacha20_setup(chacha_state *st, const unsigned char *key, unsigned long keylen, + const unsigned char *nonce, unsigned long noncelen, + int rounds); +int xchacha20_test(void); +int xchacha20_memory(const unsigned char *key, unsigned long keylen, unsigned long rounds, + const unsigned char *nonce, unsigned long noncelen, + const unsigned char *datain, unsigned long datalen, unsigned char *dataout); + +#endif /* LTC_XCHACHA20 */ + #ifdef LTC_SALSA20 typedef struct { diff --git a/src/headers/tomcrypt_custom.h b/src/headers/tomcrypt_custom.h index ed1440082..99cc54868 100644 --- a/src/headers/tomcrypt_custom.h +++ b/src/headers/tomcrypt_custom.h @@ -218,6 +218,7 @@ /* stream ciphers */ #define LTC_CHACHA +#define LTC_XCHACHA20 #define LTC_SALSA20 #define LTC_XSALSA20 #define LTC_SOSEMANUK @@ -705,6 +706,10 @@ #error LTC_CHACHA20_PRNG requires LTC_CHACHA #endif +#if defined(LTC_XCHACHA20) && !defined(LTC_CHACHA) + #error LTC_XCHACHA20 requires LTC_CHACHA +#endif + #if defined(LTC_XSALSA20) && !defined(LTC_SALSA20) #error LTC_XSALSA20 requires LTC_SALSA20 #endif diff --git a/src/misc/crypt/crypt.c b/src/misc/crypt/crypt.c index ccef4a0c7..4dd7dd083 100644 --- a/src/misc/crypt/crypt.c +++ b/src/misc/crypt/crypt.c @@ -134,6 +134,9 @@ const char *crypt_build_settings = #if defined(LTC_CHACHA) " ChaCha\n" #endif +#if defined(LTC_XCHACHA20) + " XChaCha20\n" +#endif #if defined(LTC_SALSA20) " Salsa20\n" #endif diff --git a/src/stream/chacha/xchacha20_memory.c b/src/stream/chacha/xchacha20_memory.c new file mode 100644 index 000000000..3d4ccd18b --- /dev/null +++ b/src/stream/chacha/xchacha20_memory.c @@ -0,0 +1,34 @@ +/* LibTomCrypt, modular cryptographic library -- Tom St Denis */ +/* SPDX-License-Identifier: Unlicense */ +#include "tomcrypt_private.h" + +#ifdef LTC_XCHACHA20 + +/** + Encrypt (or decrypt) bytes of ciphertext (or plaintext) with XChaCha20 + @param key The key + @param keylen The key length + @param rounds The number of rounds + @param nonce The nonce + @param noncelen The nonce length, must be 24 (octets) + @param datain The plaintext (or ciphertext) + @param datalen The length of the input and output (octets) + @param dataout [out] The ciphertext (or plaintext) + @return CRYPT_OK if successful +*/ +int xchacha20_memory(const unsigned char *key, unsigned long keylen, unsigned long rounds, + const unsigned char *nonce, unsigned long noncelen, + const unsigned char *datain, unsigned long datalen, unsigned char *dataout) +{ + chacha_state st; + int err; + + err = xchacha20_setup(&st, key, keylen, nonce, noncelen, rounds); + if (err == CRYPT_OK) { + err = chacha_crypt(&st, datain, datalen, dataout); + } + chacha_done(&st); + return err; +} + +#endif /* LTC_XCHACHA20 */ diff --git a/src/stream/chacha/xchacha20_setup.c b/src/stream/chacha/xchacha20_setup.c new file mode 100644 index 000000000..c8d84329c --- /dev/null +++ b/src/stream/chacha/xchacha20_setup.c @@ -0,0 +1,144 @@ +/* LibTomCrypt, modular cryptographic library -- Tom St Denis */ +/* SPDX-License-Identifier: Unlicense */ +#include "tomcrypt_private.h" + +/* The implementation is based on: + draft-irtf-cfrg-xchacha, "XChaCha: eXtended-nonce ChaCha and AEAD_XChaCha20_Poly1305" + RFC 8439, "ChaCha20 and Poly1305 for IETF Protocols" +*/ + +#ifdef LTC_XCHACHA20 + +#define QUARTERROUND(a,b,c,d) \ + x[a] += x[b]; x[d] = ROL(x[d] ^ x[a], 16); \ + x[c] += x[d]; x[b] = ROL(x[b] ^ x[c], 12); \ + x[a] += x[b]; x[d] = ROL(x[d] ^ x[a], 8); \ + x[c] += x[d]; x[b] = ROL(x[b] ^ x[c], 7); + +/* ChaCha20 double-round without the final addition */ +static void s_hchacha20(ulong32 *x, int rounds) +{ + int i; + + for (i = rounds; i > 0; i -= 2) { + /* columnround */ + QUARTERROUND( 0, 4, 8,12) + QUARTERROUND( 1, 5, 9,13) + QUARTERROUND( 2, 6,10,14) + QUARTERROUND( 3, 7,11,15) + /* diagonalround */ + QUARTERROUND( 0, 5,10,15) + QUARTERROUND( 1, 6,11,12) + QUARTERROUND( 2, 7, 8,13) + QUARTERROUND( 3, 4, 9,14) + } +} + +#undef QUARTERROUND + +/** + HChaCha20: derive a 256-bit subkey from a 256-bit key and 128-bit input. + This is the ChaCha20 core (double-rounds) without the final addition step, + extracting output from state positions {0,1,2,3,12,13,14,15}. + @param out [out] The derived 32-byte subkey + @param outlen The length of the output buffer, must be 32 (octets) + @param key The secret key + @param keylen The length of the secret key, must be 32 (octets) + @param in The 16-byte input (nonce or constant) + @param inlen The length of the input, must be 16 (octets) + @param rounds Number of rounds (must be evenly divisible by 2, default is 20) + @return CRYPT_OK if successful +*/ +int xchacha20_hchacha20(unsigned char *out, unsigned long outlen, + const unsigned char *key, unsigned long keylen, + const unsigned char *in, unsigned long inlen, + int rounds) +{ + const char * const constants = "expand 32-byte k"; + ulong32 x[16]; + + LTC_ARGCHK(out != NULL); + LTC_ARGCHK(outlen == 32); + LTC_ARGCHK(key != NULL); + LTC_ARGCHK(keylen == 32); + LTC_ARGCHK(in != NULL); + LTC_ARGCHK(inlen == 16); + if (rounds == 0) rounds = 20; + LTC_ARGCHK(rounds % 2 == 0); + + LOAD32L(x[ 0], constants + 0); + LOAD32L(x[ 1], constants + 4); + LOAD32L(x[ 2], constants + 8); + LOAD32L(x[ 3], constants + 12); + LOAD32L(x[ 4], key + 0); + LOAD32L(x[ 5], key + 4); + LOAD32L(x[ 6], key + 8); + LOAD32L(x[ 7], key + 12); + LOAD32L(x[ 8], key + 16); + LOAD32L(x[ 9], key + 20); + LOAD32L(x[10], key + 24); + LOAD32L(x[11], key + 28); + LOAD32L(x[12], in + 0); + LOAD32L(x[13], in + 4); + LOAD32L(x[14], in + 8); + LOAD32L(x[15], in + 12); + + s_hchacha20(x, rounds); + + STORE32L(x[ 0], out + 0); + STORE32L(x[ 1], out + 4); + STORE32L(x[ 2], out + 8); + STORE32L(x[ 3], out + 12); + STORE32L(x[12], out + 16); + STORE32L(x[13], out + 20); + STORE32L(x[14], out + 24); + STORE32L(x[15], out + 28); + + zeromem(x, sizeof(x)); + return CRYPT_OK; +} + +/** + Initialize an XChaCha20 context (HChaCha20 subkey derivation + ChaCha20 IETF setup) + @param st [out] The destination of the ChaCha20 state + @param key The secret key + @param keylen The length of the secret key, must be 32 (octets) + @param nonce The nonce + @param noncelen The length of the nonce, must be 24 (octets) + @param rounds Number of rounds (must be evenly divisible by 2, default is 20) + @return CRYPT_OK if successful +*/ +int xchacha20_setup(chacha_state *st, const unsigned char *key, unsigned long keylen, + const unsigned char *nonce, unsigned long noncelen, + int rounds) +{ + unsigned char subkey[32]; + unsigned char iv[12]; + int err; + + LTC_ARGCHK(st != NULL); + LTC_ARGCHK(nonce != NULL); + LTC_ARGCHK(noncelen == 24); + + /* HChaCha20: derive subkey from key and first 16 bytes of nonce */ + if ((err = xchacha20_hchacha20(subkey, 32, key, keylen, nonce, 16, rounds)) != CRYPT_OK) goto cleanup; + + /* set up ChaCha20 with the derived subkey */ + if ((err = chacha_setup(st, subkey, 32, rounds)) != CRYPT_OK) goto cleanup; + + /* build 12-byte IETF IV: 4 zero bytes + last 8 bytes of original nonce */ + XMEMSET(iv, 0, 4); + XMEMCPY(iv + 4, nonce + 16, 8); + if ((err = chacha_ivctr32(st, iv, 12, 0)) != CRYPT_OK) goto cleanup; + + /* mark as XChaCha20 */ + st->ivlen = 24; + +cleanup: + zeromem(subkey, sizeof(subkey)); + zeromem(iv, sizeof(iv)); + + return err; +} + +#endif /* LTC_XCHACHA20 */ diff --git a/src/stream/chacha/xchacha20_test.c b/src/stream/chacha/xchacha20_test.c new file mode 100644 index 000000000..4a280afdd --- /dev/null +++ b/src/stream/chacha/xchacha20_test.c @@ -0,0 +1,119 @@ +/* LibTomCrypt, modular cryptographic library -- Tom St Denis */ +/* SPDX-License-Identifier: Unlicense */ +#include "tomcrypt_private.h" + +#ifdef LTC_XCHACHA20 + +int xchacha20_test(void) +{ +#ifndef LTC_TEST + return CRYPT_NOP; +#else + /* TV1: HChaCha20 known-answer test (draft-irtf-cfrg-xchacha, Section 2.2.1) */ + { + const unsigned char key[] = { + 0x00,0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0a,0x0b,0x0c,0x0d,0x0e,0x0f, + 0x10,0x11,0x12,0x13,0x14,0x15,0x16,0x17,0x18,0x19,0x1a,0x1b,0x1c,0x1d,0x1e,0x1f + }; + const unsigned char in[] = { + 0x00,0x00,0x00,0x09,0x00,0x00,0x00,0x4a,0x00,0x00,0x00,0x00,0x31,0x41,0x59,0x27 + }; + const unsigned char expected_subkey[] = { + 0x82,0x41,0x3b,0x42,0x27,0xb2,0x7b,0xfe,0xd3,0x0e,0x42,0x50,0x8a,0x87,0x7d,0x73, + 0xa0,0xf9,0xe4,0xd5,0x8a,0x74,0xa8,0x53,0xc1,0x2e,0xc4,0x13,0x26,0xd3,0xec,0xdc + }; + unsigned char subkey[32]; + int err; + + if ((err = xchacha20_hchacha20(subkey, 32, key, 32, in, 16, 20)) != CRYPT_OK) return err; + + if (ltc_compare_testvector(subkey, 32, expected_subkey, 32, "XCHACHA20-TV1 (HChaCha20)", 1)) return CRYPT_FAIL_TESTVECTOR; + } + + /* TV2: XChaCha20 encryption test (draft-irtf-cfrg-xchacha, Appendix A.3.2) */ + { + const unsigned char key[] = { + 0x80,0x81,0x82,0x83,0x84,0x85,0x86,0x87,0x88,0x89,0x8a,0x8b,0x8c,0x8d,0x8e,0x8f, + 0x90,0x91,0x92,0x93,0x94,0x95,0x96,0x97,0x98,0x99,0x9a,0x9b,0x9c,0x9d,0x9e,0x9f + }; + const unsigned char nonce[] = { + 0x40,0x41,0x42,0x43,0x44,0x45,0x46,0x47,0x48,0x49,0x4a,0x4b,0x4c,0x4d,0x4e,0x4f, + 0x50,0x51,0x52,0x53,0x54,0x55,0x56,0x58 + }; + /* "The dhole (pronounced "dole") is also known as the Asiatic wild dog, ..." */ + const unsigned char plaintext[] = { + 0x54,0x68,0x65,0x20,0x64,0x68,0x6f,0x6c,0x65,0x20,0x28,0x70,0x72,0x6f,0x6e,0x6f, + 0x75,0x6e,0x63,0x65,0x64,0x20,0x22,0x64,0x6f,0x6c,0x65,0x22,0x29,0x20,0x69,0x73, + 0x20,0x61,0x6c,0x73,0x6f,0x20,0x6b,0x6e,0x6f,0x77,0x6e,0x20,0x61,0x73,0x20,0x74, + 0x68,0x65,0x20,0x41,0x73,0x69,0x61,0x74,0x69,0x63,0x20,0x77,0x69,0x6c,0x64,0x20, + 0x64,0x6f,0x67,0x2c,0x20,0x72,0x65,0x64,0x20,0x64,0x6f,0x67,0x2c,0x20,0x61,0x6e, + 0x64,0x20,0x77,0x68,0x69,0x73,0x74,0x6c,0x69,0x6e,0x67,0x20,0x64,0x6f,0x67,0x2e, + 0x20,0x49,0x74,0x20,0x69,0x73,0x20,0x61,0x62,0x6f,0x75,0x74,0x20,0x74,0x68,0x65, + 0x20,0x73,0x69,0x7a,0x65,0x20,0x6f,0x66,0x20,0x61,0x20,0x47,0x65,0x72,0x6d,0x61, + 0x6e,0x20,0x73,0x68,0x65,0x70,0x68,0x65,0x72,0x64,0x20,0x62,0x75,0x74,0x20,0x6c, + 0x6f,0x6f,0x6b,0x73,0x20,0x6d,0x6f,0x72,0x65,0x20,0x6c,0x69,0x6b,0x65,0x20,0x61, + 0x20,0x6c,0x6f,0x6e,0x67,0x2d,0x6c,0x65,0x67,0x67,0x65,0x64,0x20,0x66,0x6f,0x78, + 0x2e,0x20,0x54,0x68,0x69,0x73,0x20,0x68,0x69,0x67,0x68,0x6c,0x79,0x20,0x65,0x6c, + 0x75,0x73,0x69,0x76,0x65,0x20,0x61,0x6e,0x64,0x20,0x73,0x6b,0x69,0x6c,0x6c,0x65, + 0x64,0x20,0x6a,0x75,0x6d,0x70,0x65,0x72,0x20,0x69,0x73,0x20,0x63,0x6c,0x61,0x73, + 0x73,0x69,0x66,0x69,0x65,0x64,0x20,0x77,0x69,0x74,0x68,0x20,0x77,0x6f,0x6c,0x76, + 0x65,0x73,0x2c,0x20,0x63,0x6f,0x79,0x6f,0x74,0x65,0x73,0x2c,0x20,0x6a,0x61,0x63, + 0x6b,0x61,0x6c,0x73,0x2c,0x20,0x61,0x6e,0x64,0x20,0x66,0x6f,0x78,0x65,0x73,0x20, + 0x69,0x6e,0x20,0x74,0x68,0x65,0x20,0x74,0x61,0x78,0x6f,0x6e,0x6f,0x6d,0x69,0x63, + 0x20,0x66,0x61,0x6d,0x69,0x6c,0x79,0x20,0x43,0x61,0x6e,0x69,0x64,0x61,0x65,0x2e + }; + const unsigned char expected_ct[] = { + 0x45,0x59,0xab,0xba,0x4e,0x48,0xc1,0x61,0x02,0xe8,0xbb,0x2c,0x05,0xe6,0x94,0x7f, + 0x50,0xa7,0x86,0xde,0x16,0x2f,0x9b,0x0b,0x7e,0x59,0x2a,0x9b,0x53,0xd0,0xd4,0xe9, + 0x8d,0x8d,0x64,0x10,0xd5,0x40,0xa1,0xa6,0x37,0x5b,0x26,0xd8,0x0d,0xac,0xe4,0xfa, + 0xb5,0x23,0x84,0xc7,0x31,0xac,0xbf,0x16,0xa5,0x92,0x3c,0x0c,0x48,0xd3,0x57,0x5d, + 0x4d,0x0d,0x2c,0x67,0x3b,0x66,0x6f,0xaa,0x73,0x10,0x61,0x27,0x77,0x01,0x09,0x3a, + 0x6b,0xf7,0xa1,0x58,0xa8,0x86,0x42,0x92,0xa4,0x1c,0x48,0xe3,0xa9,0xb4,0xc0,0xda, + 0xec,0xe0,0xf8,0xd9,0x8d,0x0d,0x7e,0x05,0xb3,0x7a,0x30,0x7b,0xbb,0x66,0x33,0x31, + 0x64,0xec,0x9e,0x1b,0x24,0xea,0x0d,0x6c,0x3f,0xfd,0xdc,0xec,0x4f,0x68,0xe7,0x44, + 0x30,0x56,0x19,0x3a,0x03,0xc8,0x10,0xe1,0x13,0x44,0xca,0x06,0xd8,0xed,0x8a,0x2b, + 0xfb,0x1e,0x8d,0x48,0xcf,0xa6,0xbc,0x0e,0xb4,0xe2,0x46,0x4b,0x74,0x81,0x42,0x40, + 0x7c,0x9f,0x43,0x1a,0xee,0x76,0x99,0x60,0xe1,0x5b,0xa8,0xb9,0x68,0x90,0x46,0x6e, + 0xf2,0x45,0x75,0x99,0x85,0x23,0x85,0xc6,0x61,0xf7,0x52,0xce,0x20,0xf9,0xda,0x0c, + 0x09,0xab,0x6b,0x19,0xdf,0x74,0xe7,0x6a,0x95,0x96,0x74,0x46,0xf8,0xd0,0xfd,0x41, + 0x5e,0x7b,0xee,0x2a,0x12,0xa1,0x14,0xc2,0x0e,0xb5,0x29,0x2a,0xe7,0xa3,0x49,0xae, + 0x57,0x78,0x20,0xd5,0x52,0x0a,0x1f,0x3f,0xb6,0x2a,0x17,0xce,0x6a,0x7e,0x68,0xfa, + 0x7c,0x79,0x11,0x1d,0x88,0x60,0x92,0x0b,0xc0,0x48,0xef,0x43,0xfe,0x84,0x48,0x6c, + 0xcb,0x87,0xc2,0x5f,0x0a,0xe0,0x45,0xf0,0xcc,0xe1,0xe7,0x98,0x9a,0x9a,0xa2,0x20, + 0xa2,0x8b,0xdd,0x48,0x27,0xe7,0x51,0xa2,0x4a,0x6d,0x5c,0x62,0xd7,0x90,0xa6,0x63, + 0x93,0xb9,0x31,0x11,0xc1,0xa5,0x5d,0xd7,0x42,0x1a,0x10,0x18,0x49,0x74,0xc7,0xc5 + }; + unsigned long ptlen = sizeof(plaintext); + unsigned char ciphertext[304]; + unsigned char msg2[304]; + chacha_state st; + int err; + + /* encrypt with incremental API */ + if ((err = xchacha20_setup(&st, key, 32, nonce, 24, 20)) != CRYPT_OK) return err; + if ((err = chacha_crypt(&st, plaintext, ptlen, ciphertext)) != CRYPT_OK) return err; + if ((err = chacha_done(&st)) != CRYPT_OK) return err; + + if (ltc_compare_testvector(ciphertext, ptlen, expected_ct, ptlen, "XCHACHA20-TV2", 1)) return CRYPT_FAIL_TESTVECTOR; + + /* decrypt and verify round-trip */ + if ((err = xchacha20_setup(&st, key, 32, nonce, 24, 20)) != CRYPT_OK) return err; + if ((err = chacha_crypt(&st, ciphertext, ptlen, msg2)) != CRYPT_OK) return err; + if ((err = chacha_done(&st)) != CRYPT_OK) return err; + + if (ltc_compare_testvector(msg2, ptlen, plaintext, ptlen, "XCHACHA20-TV3 (roundtrip)", 1)) + return CRYPT_FAIL_TESTVECTOR; + + /* round-trip with xchacha20_memory */ + if ((err = xchacha20_memory(key, 32, 20, nonce, 24, plaintext, ptlen, ciphertext)) != CRYPT_OK) return err; + if ((err = xchacha20_memory(key, 32, 20, nonce, 24, ciphertext, ptlen, msg2)) != CRYPT_OK) return err; + + if (ltc_compare_testvector(msg2, ptlen, plaintext, ptlen, "XCHACHA20-TV4 (memory roundtrip)", 1)) return CRYPT_FAIL_TESTVECTOR; + } + + return CRYPT_OK; + +#endif +} + +#endif /* LTC_XCHACHA20 */ diff --git a/tests/cipher_hash_test.c b/tests/cipher_hash_test.c index 92e082307..102bc3d96 100644 --- a/tests/cipher_hash_test.c +++ b/tests/cipher_hash_test.c @@ -32,6 +32,9 @@ int cipher_hash_test(void) #ifdef LTC_CHACHA DO(chacha_test()); #endif +#ifdef LTC_XCHACHA20 + DO(xchacha20_test()); +#endif #ifdef LTC_SALSA20 DO(salsa20_test()); #endif From 918062ea73586e7761d46ba82f164e7123d6b9c3 Mon Sep 17 00:00:00 2001 From: Karel Miko Date: Thu, 16 Apr 2026 15:51:38 +0200 Subject: [PATCH 2/2] Update makefiles --- libtomcrypt_VS2008.vcproj | 12 ++++++++++++ makefile.mingw | 3 ++- makefile.msvc | 3 ++- makefile.unix | 3 ++- makefile_include.mk | 3 ++- sources.cmake | 3 +++ 6 files changed, 23 insertions(+), 4 deletions(-) diff --git a/libtomcrypt_VS2008.vcproj b/libtomcrypt_VS2008.vcproj index 987f3a33e..88056e78e 100644 --- a/libtomcrypt_VS2008.vcproj +++ b/libtomcrypt_VS2008.vcproj @@ -2910,6 +2910,18 @@ RelativePath="src\stream\chacha\chacha_test.c" > + + + + + +