From ab32bf431a4e67425f368416d6ada30690f2a41a Mon Sep 17 00:00:00 2001 From: Karel Miko Date: Thu, 16 Apr 2026 17:04:51 +0200 Subject: [PATCH 1/2] make hsalsa20 a public API function --- doc/crypt.tex | 21 ++++++- src/headers/tomcrypt_cipher.h | 4 ++ src/stream/salsa20/xsalsa20_setup.c | 85 ++++++++++++++++++++--------- src/stream/salsa20/xsalsa20_test.c | 23 ++++++++ 4 files changed, 105 insertions(+), 28 deletions(-) diff --git a/doc/crypt.tex b/doc/crypt.tex index fc879fa9a..99aa371a0 100644 --- a/doc/crypt.tex +++ b/doc/crypt.tex @@ -1437,11 +1437,30 @@ \chapter{Stream Ciphers} As always, never ever use the same key + nonce/IV pair more than once. \vspace{1mm} +\subsection{HSalsa20} + +\textit{HSalsa20} is the key derivation function underlying \textit{XSalsa20}. It applies the +Salsa20 core (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 where it derives a symmetric key from an X25519 shared secret. + +\index{xsalsa20\_hsalsa20()} +\begin{verbatim} +int xsalsa20_hsalsa20(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} Salsa20 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} + For more information about Salsa20 see \url{https://en.wikipedia.org/wiki/Salsa20}. \vspace{1mm} -For more information about XSalsa20 see +For more information about XSalsa20 and HSalsa20 see \url{https://cr.yp.to/snuffle/xsalsa-20081128.pdf}. \vspace{1mm} diff --git a/src/headers/tomcrypt_cipher.h b/src/headers/tomcrypt_cipher.h index b370fedb0..802c294ba 100644 --- a/src/headers/tomcrypt_cipher.h +++ b/src/headers/tomcrypt_cipher.h @@ -1080,6 +1080,10 @@ int salsa20_memory(const unsigned char *key, unsigned long keylen, unsigned #ifdef LTC_XSALSA20 +int xsalsa20_hsalsa20(unsigned char *out, unsigned long outlen, + const unsigned char *key, unsigned long keylen, + const unsigned char *in, unsigned long inlen, + int rounds); int xsalsa20_setup(salsa20_state *st, const unsigned char *key, unsigned long keylen, const unsigned char *nonce, unsigned long noncelen, int rounds); diff --git a/src/stream/salsa20/xsalsa20_setup.c b/src/stream/salsa20/xsalsa20_setup.c index c764a896c..eb2cc502a 100644 --- a/src/stream/salsa20/xsalsa20_setup.c +++ b/src/stream/salsa20/xsalsa20_setup.c @@ -40,34 +40,37 @@ static void s_xsalsa20_doubleround(ulong32 *x, int rounds) #undef QUARTERROUND /** - Initialize an XSalsa20 context - @param st [out] The destination of the XSalsa20 state + HSalsa20: derive a 256-bit subkey from a 256-bit key and 128-bit input. + This is the Salsa20 core (double-rounds) without the final addition step, + extracting output from state positions {0,5,10,15,6,7,8,9}. + @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 nonce The nonce - @param noncelen The length of the nonce, must be 24 (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 xsalsa20_setup(salsa20_state *st, const unsigned char *key, unsigned long keylen, - const unsigned char *nonce, unsigned long noncelen, - int rounds) +int xsalsa20_hsalsa20(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"; - const int sti[] = {0, 5, 10, 15, 6, 7, 8, 9}; /* indices used to build subkey fm x */ - ulong32 x[64]; /* input to & output fm doubleround */ - unsigned char subkey[32]; + const int sti[] = {0, 5, 10, 15, 6, 7, 8, 9}; + ulong32 x[16]; int i; - LTC_ARGCHK(st != NULL); - LTC_ARGCHK(key != NULL); - LTC_ARGCHK(keylen == 32); - LTC_ARGCHK(nonce != NULL); - LTC_ARGCHK(noncelen == 24); + 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); /* number of rounds must be evenly divisible by 2 */ + LTC_ARGCHK(rounds % 2 == 0); - /* load the state to "hash" the key */ LOAD32L(x[ 0], constants + 0); LOAD32L(x[ 5], constants + 4); LOAD32L(x[10], constants + 8); @@ -80,20 +83,48 @@ int xsalsa20_setup(salsa20_state *st, const unsigned char *key, unsigned long ke LOAD32L(x[12], key + 20); LOAD32L(x[13], key + 24); LOAD32L(x[14], key + 28); - LOAD32L(x[ 6], nonce + 0); - LOAD32L(x[ 7], nonce + 4); - LOAD32L(x[ 8], nonce + 8); - LOAD32L(x[ 9], nonce + 12); + LOAD32L(x[ 6], in + 0); + LOAD32L(x[ 7], in + 4); + LOAD32L(x[ 8], in + 8); + LOAD32L(x[ 9], in + 12); - /* use modified salsa20 doubleround (no final addition) */ s_xsalsa20_doubleround(x, rounds); - /* extract the subkey */ for (i = 0; i < 8; ++i) { - STORE32L(x[sti[i]], subkey + 4 * i); + STORE32L(x[sti[i]], out + 4 * i); } - /* load the final initial state */ + zeromem(x, sizeof(x)); + return CRYPT_OK; +} + +/** + Initialize an XSalsa20 context + @param st [out] The destination of the XSalsa20 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 xsalsa20_setup(salsa20_state *st, const unsigned char *key, unsigned long keylen, + const unsigned char *nonce, unsigned long noncelen, + int rounds) +{ + const char * const constants = "expand 32-byte k"; + unsigned char subkey[32]; + int err; + + LTC_ARGCHK(st != NULL); + LTC_ARGCHK(nonce != NULL); + LTC_ARGCHK(noncelen == 24); + if (rounds == 0) rounds = 20; + + /* HSalsa20: derive subkey from key and first 16 bytes of nonce */ + if ((err = xsalsa20_hsalsa20(subkey, 32, key, keylen, nonce, 16, rounds)) != CRYPT_OK) goto cleanup; + + /* load the final initial state with the derived subkey */ LOAD32L(st->input[ 0], constants + 0); LOAD32L(st->input[ 5], constants + 4); LOAD32L(st->input[10], constants + 8); @@ -114,12 +145,12 @@ int xsalsa20_setup(salsa20_state *st, const unsigned char *key, unsigned long ke st->ksleft = 0; st->ivlen = 24; /* set switch to say nonce/IV has been loaded */ +cleanup: #ifdef LTC_CLEAN_STACK - zeromem(x, sizeof(x)); zeromem(subkey, sizeof(subkey)); #endif - return CRYPT_OK; + return err; } diff --git a/src/stream/salsa20/xsalsa20_test.c b/src/stream/salsa20/xsalsa20_test.c index a267cdaad..f2a5004f5 100644 --- a/src/stream/salsa20/xsalsa20_test.c +++ b/src/stream/salsa20/xsalsa20_test.c @@ -28,6 +28,29 @@ int xsalsa20_test(void) return CRYPT_NOP; #else + /*************************************************************************** + * TV0: HSalsa20 known-answer test + * From the NaCl test suite / https://cr.yp.to/snuffle/xsalsa-20081128.pdf + */ + { + const unsigned char key[] = { + 0x1b,0x27,0x55,0x64,0x73,0xe9,0x85,0xd4,0x62,0xcd,0x51,0x19,0x7a,0x9a,0x46,0xc7, + 0x60,0x09,0x54,0x9e,0xac,0x64,0x74,0xf2,0x06,0xc4,0xee,0x08,0x44,0xf6,0x83,0x89 + }; + const unsigned char in[] = { + 0x69,0x69,0x6e,0xe9,0x55,0xb6,0x2b,0x73,0xcd,0x62,0xbd,0xa8,0x75,0xfc,0x73,0xd6 + }; + const unsigned char expected[] = { + 0xdc,0x90,0x8d,0xda,0x0b,0x93,0x44,0xa9,0x53,0x62,0x9b,0x73,0x38,0x20,0x77,0x88, + 0x80,0xf3,0xce,0xb4,0x21,0xbb,0x61,0xb9,0x1c,0xbd,0x4c,0x3e,0x66,0x25,0x6c,0xe4 + }; + unsigned char out[32]; + int err; + + if ((err = xsalsa20_hsalsa20(out, 32, key, 32, in, 16, 20)) != CRYPT_OK) return err; + if (ltc_compare_testvector(out, 32, expected, 32, "XSALSA20-TV0 (HSalsa20)", 0)) return CRYPT_FAIL_TESTVECTOR; + } + /*************************************************************************** * verify a round trip: */ From 93fa641b1dfcd5931b54f21fecb1cf17a88b3aa5 Mon Sep 17 00:00:00 2001 From: Karel Miko Date: Thu, 16 Apr 2026 17:07:27 +0200 Subject: [PATCH 2/2] blake2bmac_init/blake2smac_init - allows NULL key when keylen is 0 (in line with the BLAKE2 spec) --- src/mac/blake2/blake2bmac.c | 2 +- src/mac/blake2/blake2smac.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/mac/blake2/blake2bmac.c b/src/mac/blake2/blake2bmac.c index ac26e106a..c8387b7f1 100644 --- a/src/mac/blake2/blake2bmac.c +++ b/src/mac/blake2/blake2bmac.c @@ -16,7 +16,7 @@ int blake2bmac_init(blake2bmac_state *st, unsigned long outlen, const unsigned char *key, unsigned long keylen) { LTC_ARGCHK(st != NULL); - LTC_ARGCHK(key != NULL); + LTC_ARGCHK(key != NULL || keylen == 0); return blake2b_init(st, outlen, key, keylen); } diff --git a/src/mac/blake2/blake2smac.c b/src/mac/blake2/blake2smac.c index f42c7aec3..5ba39c86f 100644 --- a/src/mac/blake2/blake2smac.c +++ b/src/mac/blake2/blake2smac.c @@ -16,7 +16,7 @@ int blake2smac_init(blake2smac_state *st, unsigned long outlen, const unsigned char *key, unsigned long keylen) { LTC_ARGCHK(st != NULL); - LTC_ARGCHK(key != NULL); + LTC_ARGCHK(key != NULL || keylen == 0); return blake2s_init(st, outlen, key, keylen); }