Skip to content

Instantly share code, notes, and snippets.

@CandyMi
Last active May 30, 2026 00:35
Show Gist options
  • Select an option

  • Save CandyMi/9ed1b2785fa55f338fafaee14c7723fc to your computer and use it in GitHub Desktop.

Select an option

Save CandyMi/9ed1b2785fa55f338fafaee14c7723fc to your computer and use it in GitHub Desktop.
Salsa20 算法

Salsa20 算法

salsa(128/256) 算法实现

测试结果

[candy@MacBookPro:~/salsa20] $ cc -std=c99 -O2 -o test_salsa20 test_salsa20.c salsa20.c && ./test_salsa20
Salsa20 test suite
==================

========== 256-bit determinism & self-consistency ==========
  [PASS] golden ref: 128 bytes generated
  [PASS] deterministic: 2nd run matches 1st
  [PASS] deterministic: 3rd run matches 1st

========== sequential full-block generation ==========
  [PASS] block 0 (separate call) matches ref[ 0..63]
  [PASS] block 1 (separate call) matches ref[64..127]

========== sub-block call semantics ==========
  [PASS] sub-block: 37 bytes encrypt OK
  [PASS] 37-byte call matches first 37 of full block
  [PASS] next 27 bytes come from block 1 start

========== 128-bit key roundtrip ==========
  [PASS] encrypt returned correct length
  [PASS] cipher != plain
  [PASS] decrypt roundtrip

========== out == NULL (size query) ==========
  [PASS] salsa20(out=NULL) → NULL
  [PASS] salsa20(out=NULL, insize=0) → NULL

========== bad arguments ==========
  [PASS] setkey(ctx=NULL) → -1
  [PASS] setkey(key=NULL) → -1
  [PASS] setkey(ksize=0) → -1
  [PASS] setkey(ksize=8) → -1
  [PASS] setkey(ksize=64) → -1
  [PASS] setkey(ksize=15) → -1
  [PASS] salsa20(ctx=NULL) → NULL
  [PASS] salsa20(in=NULL) → NULL
  [PASS] salsa20 without setkey → NULL
  [PASS] setnonce(ctx=NULL) → -1
  [PASS] setnonce(nonce=NULL) → -1
  [PASS] setnonce without setkey → -1
  [PASS] setnonce(nsize=0) → -1
  [PASS] setnonce(nsize=7) → -1
  [PASS] setnonce(nsize=16) → -1
  [PASS] setnonce(nsize=25) → -1
  [PASS] setnonce(nsize=32) → -1
  [PASS] setnonce(nsize=24, 128-bit key) → -1

========== in-place encrypt / decrypt ==========
  [PASS] in-place encrypt changed data
  [PASS] in-place roundtrip restored original

========== different keys / nonces → different output ==========
  [PASS] different keys → different keystream
  [PASS] different nonces → different keystream
  [PASS] setnonce resets counter → block 0 matches fresh ctx

========== large data (1 MB) ==========
  [PASS] 1 MB encrypt returned correct length
  [PASS] 1 MB ciphertext differs from plaintext
  [PASS] 1 MB decrypt roundtrip

========== encrypt == decrypt (stream cipher symmetry) ==========
  [PASS] encrypt(msg) == decrypt(msg)

========== encrypt-then-decrypt tunnel ==========
  [PASS] encrypt → decrypt restores original

========== counter high-word wrap ==========
  [PASS] 128 blocks one-shot == block-by-block

========== re-setkey (re-initialisation) ==========
  [PASS] re-setkey same key → same output (counter reset)
  [PASS] re-setkey different key → different output

========== macro coverage ==========
  [PASS] SALSA20_set_key → 0
  [PASS] salsa20_setkey → 0
  [PASS] SALSA20(...) == salsa20(...)
  [PASS] SALSA20(out=NULL) → NULL
  [PASS] SALSA20(ctx=NULL) → NULL
  [PASS] SALSA20(in=NULL) → NULL
  [PASS] SALSA20(insize=0) → NULL
  [PASS] SALSA20_set_key(ctx=NULL) → -1
  [PASS] SALSA20_set_key(key=NULL) → -1
  [PASS] SALSA20_set_key(ksize=8) → -1

========== XSalsa20 determinism ==========
  [PASS] XSalsa20: 128 bytes generated
  [PASS] XSalsa20: 2nd run matches 1st
  [PASS] XSalsa20 mode byte set to 1

========== XSalsa20 different nonces ==========
  [PASS] XSalsa20: different nonces → different output

========== XSalsa20 roundtrip ==========
  [PASS] XSalsa20: cipher != plain
  [PASS] XSalsa20: roundtrip restores

========== XSalsa20 sequential blocks ==========
  [PASS] XSalsa20 block 0 matches ref[ 0..63]
  [PASS] XSalsa20 block 1 matches ref[64..127]

========== XSalsa20 in-place ==========
  [PASS] XSalsa20 in-place: encrypt changed data
  [PASS] XSalsa20 in-place: roundtrip restored

========== XSalsa20 rekey → mode reset ==========
  [PASS] XSalsa20 mode = 1
  [PASS] rekey resets mode to 0 (Salsa20)
  [PASS] rekey → Salsa20 mode matches standard Salsa20

========== XSalsa20 regression vector ==========
  [PASS] XSalsa20: regression vector block 0[0..31]

========== XSalsa20 ≠ Salsa20 ==========
  [PASS] XSalsa20 keystream ≠ Salsa20 with same trailing 8-byte nonce

========== XSalsa20 key overwrite ==========
  [PASS] XSalsa20 tx key overwritten: setnonce(8) uses derived key
  [PASS] re-setkey → setnonce(8) uses original key again

========== XSalsa20 large data (256 KB) ==========
  [PASS] XSalsa20: 256 KB encrypted differs from plaintext
  [PASS] XSalsa20: 256 KB roundtrip

========================================
RESULTS: 73 passed, 0 failed, 73 total
ALL TESTS PASSED
/*
** Salsa20 stream cipher — salsa20.c
** =====================================
** Implements the Salsa20/20 core as specified by Daniel J. Bernstein:
** https://cr.yp.to/snuffle/spec.pdf
**
** Key sizes:
** salsa_128 (16 bytes) → "expand 16-byte k" constants
** salsa_256 (32 bytes) → "expand 32-byte k" constants
**
** Thread safety:
** All mutable state lives in the caller-owned salsa20_ctx_t; no static
** or global variables. As long as each thread uses its own ctx object,
** encryption / decryption can run concurrently without any locking.
**
** Endianness:
** The Salsa20 specification uses little-endian word layout. All
** serialization / deserialization is done via explicit shifts and ORs,
** which behave identically on big- and little-endian hardware — no
** memcpy tricks, no htonl/ntohl, no byteswap headers required.
**
** Performance notes:
** - All small helpers are static inline.
** - The double-round loop counts down from 10 → 0 (enables single-
** register decrement-and-branch on many ISAs).
** - Keystream generation is inlined inside the XOR loop so the
** counter can be bumped and persisted per block.
**
** LICENSE: BSD
** Author: CandyMi [https://github.com/candymi]
*/
#include "salsa20.h"
#include <string.h> /* memset, memcpy */
/* ==========================================================================
* Internal ctx layout (fits in salsa20_ctx_t.key[64])
* --------------------------------------------------------------------------
* We reuse the single 64-byte buffer to hold everything Salsa20 needs
* between calls. This keeps the struct definition simple and avoids
* wasting space on separate counter / nonce fields.
*
* Offset Size Content
* ---------- ------ ------------------------------------------------
* 0 .. 31 32 key material (16 or 32 bytes actually used)
* 32 .. 39 8 nonce (64-bit, little-endian)
* 40 .. 47 8 block counter (uint64_t, little-endian)
* 48 1 key_bytes (16 = 128-bit, 32 = 256-bit)
* 49 1 mode (0 = Salsa20, 1 = XSalsa20)
* 50 .. 63 14 reserved / zero-padding
* ========================================================================== */
/* Internal accessors — avoid magic numbers in the code below */
#define CTX_KEY(p) ((p)->key + 0) /* key[0..31] */
#define CTX_NONCE(p) ((p)->key + 32) /* nonce[0..7] */
#define CTX_COUNTER(p) ((p)->key + 40) /* counter LE */
#define CTX_KEYBYTES(p) ((p)->key[48]) /* 16 or 32 */
#define CTX_MODE(p) ((p)->key[49]) /* 0=Salsa20, 1=XSalsa20 */
/* ==========================================================================
* Salsa20 constants (little-endian representation of ASCII strings)
* ========================================================================== */
/*
* "expand 32-byte k" → { "expa", "nd 3", "2-by", "te k" }
*/
#define C0_256 0x61707865U /* "expa" */
#define C1_256 0x3320646eU /* "nd 3" */
#define C2_256 0x79622d32U /* "2-by" */
#define C3_256 0x6b206574U /* "te k" */
/*
* "expand 16-byte k" → { "expa", "nd 1", "6-by", "te k" }
*/
#define C0_128 0x61707865U /* "expa" */
#define C1_128 0x3120646eU /* "nd 1" */
#define C2_128 0x79622d36U /* "6-by" */
#define C3_128 0x6b206574U /* "te k" */
/* ==========================================================================
* Low-level primitives (endian-independent by design)
* ========================================================================== */
/*
* rotl32 — 32-bit left rotation
* Defined for any n in [0, 31]; the compiler folds the pair of shifts
* into a single ROL instruction on x86 / ARM.
*/
static inline uint32_t
rotl32(uint32_t x, int n)
{
return (x << n) | (x >> (32 - n));
}
/*
* u8to32_le — read a little-endian uint32_t from 4 bytes in memory
* No alignment requirement; works on any host endianness.
*/
static inline uint32_t
u8to32_le(const uint8_t p[4])
{
return ((uint32_t)p[0])
| ((uint32_t)p[1] << 8)
| ((uint32_t)p[2] << 16)
| ((uint32_t)p[3] << 24);
}
/*
* u32to8_le — write a uint32_t as 4 little-endian bytes
*/
static inline void
u32to8_le(uint8_t p[4], uint32_t v)
{
p[0] = (uint8_t)(v);
p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16);
p[3] = (uint8_t)(v >> 24);
}
/*
* u8to64_le — read a little-endian uint64_t from 8 bytes
*/
static inline uint64_t
u8to64_le(const uint8_t p[8])
{
return ((uint64_t)p[0])
| ((uint64_t)p[1] << 8)
| ((uint64_t)p[2] << 16)
| ((uint64_t)p[3] << 24)
| ((uint64_t)p[4] << 32)
| ((uint64_t)p[5] << 40)
| ((uint64_t)p[6] << 48)
| ((uint64_t)p[7] << 56);
}
/*
* u64to8_le — write a uint64_t as 8 little-endian bytes
*/
static inline void
u64to8_le(uint8_t p[8], uint64_t v)
{
p[0] = (uint8_t)(v);
p[1] = (uint8_t)(v >> 8);
p[2] = (uint8_t)(v >> 16);
p[3] = (uint8_t)(v >> 24);
p[4] = (uint8_t)(v >> 32);
p[5] = (uint8_t)(v >> 40);
p[6] = (uint8_t)(v >> 48);
p[7] = (uint8_t)(v >> 56);
}
/* ==========================================================================
* Quarter round — the fundamental building block
* ==========================================================================
*
* QR(a, b, c, d) transforms four 32-bit words in-place:
* b ^= (a + d) <<< 7
* c ^= (b + a) <<< 9
* d ^= (c + b) <<< 13
* a ^= (d + c) <<< 18
*
* The macro is wrapped in do-while(0) so it can appear anywhere a
* statement is expected (including inside unbraced if / for bodies).
*/
#define SALSA20_QR(a, b, c, d) \
do { \
(b) ^= rotl32((a) + (d), 7); \
(c) ^= rotl32((b) + (a), 9); \
(d) ^= rotl32((c) + (b), 13); \
(a) ^= rotl32((d) + (c), 18); \
} while (0)
/* ==========================================================================
* salsa20_core — the Salsa20 hash function (20 rounds)
* ==========================================================================
*
* Given 16 input words, produces 16 output words = core(in) + in.
*
* Column round indices: Row round indices:
* QR( 0, 4, 8, 12) QR( 0, 1, 2, 3)
* QR( 5, 9, 13, 1) QR( 5, 6, 7, 4)
* QR(10, 14, 2, 6) QR(10, 11, 8, 9)
* QR(15, 3, 7, 11) QR(15, 12, 13, 14)
*/
static void
salsa20_core(uint32_t out[16], const uint32_t in[16])
{
uint32_t x[16];
int i;
/* Copy input → working array */
for (i = 0; i < 16; i++)
x[i] = in[i];
/* 10 double-rounds = 20 rounds total */
for (i = 10; i > 0; i--) {
/* ---- column round ---- */
SALSA20_QR(x[ 0], x[ 4], x[ 8], x[12]);
SALSA20_QR(x[ 5], x[ 9], x[13], x[ 1]);
SALSA20_QR(x[10], x[14], x[ 2], x[ 6]);
SALSA20_QR(x[15], x[ 3], x[ 7], x[11]);
/* ---- row round ---- */
SALSA20_QR(x[ 0], x[ 1], x[ 2], x[ 3]);
SALSA20_QR(x[ 5], x[ 6], x[ 7], x[ 4]);
SALSA20_QR(x[10], x[11], x[ 8], x[ 9]);
SALSA20_QR(x[15], x[12], x[13], x[14]);
}
/* Feedforward: output = core(input) + input */
for (i = 0; i < 16; i++)
out[i] = x[i] + in[i];
}
/* ==========================================================================
* hsalsa20 — XSalsa20 sub-key derivation (Bernstein 2011)
* ==========================================================================
*
* HSalsa20(k, n[0..15]) produces a 256-bit sub-key for XSalsa20.
*
* It runs the full 20-round Salsa20 core with:
* - input: key k (256-bit), nonce n[0..15] as the 16-byte "nonce",
* counter = 0, "expand 32-byte k" constants
* - output: specific 8 words WITHOUT feedforward:
* { x[0], x[5], x[10], x[15], x[6], x[7], x[8], x[9] }
*
* This sub-key is then used as the key for standard Salsa20/20 with
* the remaining 8 bytes n[16..23] as the stream nonce.
*/
static void
hsalsa20(uint8_t out[32], const uint8_t key[32], const uint8_t nonce16[16])
{
uint32_t x[16];
int i;
/* Build the Salsa20 input block (256-bit key, nonce16 as nonce) */
x[ 0] = C0_256;
x[ 1] = u8to32_le(key + 0);
x[ 2] = u8to32_le(key + 4);
x[ 3] = u8to32_le(key + 8);
x[ 4] = u8to32_le(key + 12);
x[ 5] = C1_256;
x[ 6] = u8to32_le(nonce16 + 0);
x[ 7] = u8to32_le(nonce16 + 4);
x[ 8] = 0; /* counter low = 0 */
x[ 9] = 0; /* counter high = 0 */
x[10] = C2_256;
x[11] = u8to32_le(key + 16);
x[12] = u8to32_le(key + 20);
x[13] = u8to32_le(key + 24);
x[14] = u8to32_le(key + 28);
x[15] = C3_256;
/* 10 double-rounds = 20 rounds */
for (i = 10; i > 0; i--) {
SALSA20_QR(x[ 0], x[ 4], x[ 8], x[12]);
SALSA20_QR(x[ 5], x[ 9], x[13], x[ 1]);
SALSA20_QR(x[10], x[14], x[ 2], x[ 6]);
SALSA20_QR(x[15], x[ 3], x[ 7], x[11]);
SALSA20_QR(x[ 0], x[ 1], x[ 2], x[ 3]);
SALSA20_QR(x[ 5], x[ 6], x[ 7], x[ 4]);
SALSA20_QR(x[10], x[11], x[ 8], x[ 9]);
SALSA20_QR(x[15], x[12], x[13], x[14]);
}
/* Output 8 words WITHOUT feedforward */
u32to8_le(out + 0, x[ 0]); /* constant word 0 */
u32to8_le(out + 4, x[ 5]); /* constant word 1 */
u32to8_le(out + 8, x[10]); /* constant word 2 */
u32to8_le(out + 12, x[15]); /* constant word 3 */
u32to8_le(out + 16, x[ 6]); /* nonce[0..3] */
u32to8_le(out + 20, x[ 7]); /* nonce[4..7] */
u32to8_le(out + 24, x[ 8]); /* counter[0..3] */
u32to8_le(out + 28, x[ 9]); /* counter[4..7] */
}
/* ==========================================================================
* salsa20_setup — build the 16-word input block for salsa20_core
* ==========================================================================
*
* Layout of the 16-word state (Salsa20 spec §3):
*
* Word 256-bit key 128-bit key
* ───── ──────────────────── ────────────────────
* 0 "expa" (constant) "expa" (constant)
* 1 key[ 0 .. 3] key[ 0 .. 3]
* 2 key[ 4 .. 7] key[ 4 .. 7]
* 3 key[ 8 .. 11] key[ 8 .. 11]
* 4 key[12 .. 15] key[12 .. 15]
* 5 "nd 3" (constant) "nd 1" (constant)
* 6 nonce[0 .. 3] nonce[0 .. 3]
* 7 nonce[4 .. 7] nonce[4 .. 7]
* 8 counter[0 .. 3] counter[0 .. 3]
* 9 counter[4 .. 7] counter[4 .. 7]
* 10 "2-by" (constant) "6-by" (constant)
* 11 key[16 .. 19] key[ 0 .. 3] ← repeat
* 12 key[20 .. 23] key[ 4 .. 7] ← repeat
* 13 key[24 .. 27] key[ 8 .. 11] ← repeat
* 14 key[28 .. 31] key[12 .. 15] ← repeat
* 15 "te k" (constant) "te k" (constant)
*
* For 128-bit keys the 16-byte key is repeated in the second half.
*/
static void
salsa20_setup(uint32_t in[16],
const uint8_t key[32],
int key_bytes, /* 16 or 32 */
const uint8_t nonce[8],
uint64_t counter)
{
if (key_bytes == 16) {
/* ---- 128-bit key ("expand 16-byte k") ---- */
in[ 0] = C0_128;
in[ 5] = C1_128;
in[10] = C2_128;
in[15] = C3_128;
/* Key: k0..k15 (words 1-4) */
in[ 1] = u8to32_le(key + 0);
in[ 2] = u8to32_le(key + 4);
in[ 3] = u8to32_le(key + 8);
in[ 4] = u8to32_le(key + 12);
/* Key repeated (words 11-14) */
in[11] = in[1];
in[12] = in[2];
in[13] = in[3];
in[14] = in[4];
} else {
/* ---- 256-bit key ("expand 32-byte k") ---- */
in[ 0] = C0_256;
in[ 5] = C1_256;
in[10] = C2_256;
in[15] = C3_256;
/* Key: k0..k15 (words 1-4) */
in[ 1] = u8to32_le(key + 0);
in[ 2] = u8to32_le(key + 4);
in[ 3] = u8to32_le(key + 8);
in[ 4] = u8to32_le(key + 12);
/* Key: k16..k31 (words 11-14) */
in[11] = u8to32_le(key + 16);
in[12] = u8to32_le(key + 20);
in[13] = u8to32_le(key + 24);
in[14] = u8to32_le(key + 28);
}
/* Nonce: 8 bytes → words 6,7 */
in[6] = u8to32_le(nonce + 0);
in[7] = u8to32_le(nonce + 4);
/* Counter: uint64_t → words 8,9 (little-endian) */
in[8] = (uint32_t)(counter);
in[9] = (uint32_t)(counter >> 32);
}
/* ==========================================================================
* salsa20 — shared encrypt / decrypt path (symmetric stream cipher)
* ==========================================================================
*
* For a stream cipher, encryption and decryption are the same operation:
* ciphertext = plaintext XOR keystream.
*
* The caller is expected to know the output length (always == insize for
* a stream cipher). out MUST be non-NULL — there is no size-query mode.
*
* The keystream generation is inlined here (rather than calling out
* to a separate helper) so the block counter can be bumped and
* persisted to ctx directly — one less copy per block.
*
* Returns:
* non-NULL pointer to out (success)
* NULL error (NULL ctx, NULL in, NULL out, zero insize, or bad ctx)
*/
void *
salsa20(salsa20_ctx_t *ctx,
const void *in,
unsigned int insize,
unsigned char *out)
{
/* ---- argument validation ---- */
if (!ctx || !in || !out || insize == 0)
return NULL;
{
int kb = (int)CTX_KEYBYTES(ctx);
if (kb != 16 && kb != 32)
return NULL;
}
/* ---- stream-cipher XOR loop ---- */
const uint8_t *in8 = (const uint8_t *)in;
unsigned int offset = 0;
uint64_t counter = u8to64_le(CTX_COUNTER(ctx));
const uint8_t *key = CTX_KEY(ctx);
int kb = (int)CTX_KEYBYTES(ctx);
while (offset < insize) {
/* Generate one keystream block (64 bytes) */
uint32_t in16[16];
uint32_t out16[16];
uint8_t ks[SALSA20_BLOCK_SIZE];
int i;
salsa20_setup(in16, key, kb, CTX_NONCE(ctx), counter);
salsa20_core(out16, in16);
for (i = 0; i < 16; i++)
u32to8_le(ks + 4 * i, out16[i]);
counter++;
/* XOR chunk — last block may be smaller than 64 bytes */
unsigned int chunk = insize - offset;
if (chunk > SALSA20_BLOCK_SIZE)
chunk = SALSA20_BLOCK_SIZE;
for (unsigned int j = 0; j < chunk; j++)
out[offset + j] = in8[offset + j] ^ ks[j];
offset += chunk;
}
/* Persist the updated counter back into ctx */
u64to8_le(CTX_COUNTER(ctx), counter);
return out;
}
/* ==========================================================================
* Public API
* ========================================================================== */
/*
* salsa20_setkey — initialise (or reset) a Salsa20 context
* --------------------------------------------------------------------------
* ctx pointer to caller-allocated context (must not be NULL)
* key raw key bytes (must not be NULL)
* ksize key length in bytes: 16 (128-bit) or 32 (256-bit)
*
* On success the context is ready for encryption / decryption with:
* - key material stored
* - nonce zeroed
* - block counter set to 0
* - key_bytes marker set to 16 or 32
*
* Returns:
* 0 success
* -1 invalid argument (NULL ctx, NULL key, or ksize ∉ {16, 32})
*/
int
salsa20_setkey(salsa20_ctx_t *ctx, const uint8_t *key, int ksize)
{
if (!ctx || !key)
return -1;
if (ksize != 16 && ksize != 32)
return -1;
/* Zero the entire context (clears key, nonce, counter, reserved) */
memset(ctx, 0, sizeof(salsa20_ctx_t));
/* Copy key material */
memcpy(CTX_KEY(ctx), key, (size_t)ksize);
/* Store the key-size marker (16 or 32) at offset 48 */
CTX_KEYBYTES(ctx) = (uint8_t)ksize;
/* Reset mode to Salsa20 (offset 49); nonce and counter already zero */
CTX_MODE(ctx) = 0;
return 0;
}
/*
* salsa20_setnonce — set the nonce (8 or 24 bytes), also resets counter
* --------------------------------------------------------------------------
* ctx context previously initialised by salsa20_setkey (not NULL)
* nonce nonce buffer (not NULL, length = nsize)
* nsize nonce length in bytes: 8 (Salsa20) or 24 (XSalsa20)
*
* Salsa20 mode (nsize = 8):
* Copies the 8-byte nonce directly into ctx. The original key is
* used as-is for all subsequent encryption / decryption.
*
* XSalsa20 mode (nsize = 24):
* Uses HSalsa20 to derive a 256-bit sub-key from the original key
* and the first 16 bytes of the nonce. The derived sub-key replaces
* the key material in ctx, and the last 8 bytes of the nonce become
* the stream nonce. XSalsa20 requires a 256-bit key (key_bytes = 32).
*
* Since this overwrites the internal key, to switch to a different
* nonce with the same original key the caller must first re-invoke
* salsa20_setkey().
*
* Both modes reset the block counter to 0.
*
* Returns:
* 0 success
* -1 bad argument (NULL ctx, NULL nonce, ctx not initialised,
* nsize ∉ {8, 24}, or nsize=24 with 128-bit key)
*/
int
salsa20_setnonce(salsa20_ctx_t *ctx, const uint8_t *nonce, int nsize)
{
if (!ctx || !nonce)
return -1;
{
int kb = (int)CTX_KEYBYTES(ctx);
if (kb != 16 && kb != 32)
return -1;
}
if (nsize == 8) {
/* ---- Salsa20: 64-bit nonce ---- */
memcpy(CTX_NONCE(ctx), nonce, 8);
CTX_MODE(ctx) = 0;
} else if (nsize == 24) {
/* ---- XSalsa20: 192-bit nonce ---- */
int kb = (int)CTX_KEYBYTES(ctx);
uint8_t subkey[32];
/* XSalsa20 requires a 256-bit key */
if (kb != 32)
return -1;
/* HSalsa20(key, nonce[0..15]) → 256-bit sub-key */
hsalsa20(subkey, CTX_KEY(ctx), nonce);
/* Replace key with derived sub-key */
memcpy(CTX_KEY(ctx), subkey, 32);
/* Last 8 bytes of nonce become the stream nonce */
memcpy(CTX_NONCE(ctx), nonce + 16, 8);
CTX_MODE(ctx) = 1;
} else {
return -1;
}
/* Reset the block counter to 0 */
memset(CTX_COUNTER(ctx), 0, 8);
return 0;
}
#ifndef SALSA20_H
#define SALSA20_H
#if _WIN32
#define SALSA20_EXPORT __declspec(dllexport)
#else
#define SALSA20_EXPORT __attribute__((visibility("default")))
#endif
#include <stdint.h>
#include <stddef.h>
#if __cplusplus__
extern "C" {
#endif
/* ------------------------------------------------------------------------
* Salsa20 stream cipher — public API
* ------------------------------------------------------------------------
*
* Salsa20/20 as specified by Daniel J. Bernstein:
* https://cr.yp.to/snuffle/spec.pdf
*
* Usage pattern (single-threaded; multi-thread: one ctx per thread):
*
* salsa20_ctx_t ctx;
* SALSA20_set_key(&ctx, key, 32); // 256-bit key (or salsa20_setkey)
* salsa20_setnonce(&ctx, nonce, 8); // 64-bit nonce (Salsa20)
* // or: salsa20_setnonce(&ctx, nonce, 24); // 192-bit nonce (XSalsa20)
* SALSA20(&ctx, in, insize, out); // in-place OK if out==in
* // same call decrypts — stream cipher
*
* Thread safety:
* Every mutable byte lives inside the caller-owned salsa20_ctx_t.
* No static / global state. As long as each thread operates on its
* own ctx, all functions are re-entrant and lock-free.
*
* Endianness:
* The API surface uses native byte order only for opaque buffers
* (key, in, out). The Salsa20 spec's internal little-endian word
* layout is handled entirely inside salsa20.c via explicit shifts —
* no byteswap headers or alignment assumptions.
*
* Constants:
* SALSA20_BLOCK_SIZE = 64 (size of one keystream block in bytes)
* salsa_128 = 16 (128-bit key = 16 bytes)
* salsa_256 = 32 (256-bit key = 32 bytes)
* ------------------------------------------------------------------------ */
/* Salsa20 keystream block size (always 64 bytes) */
#define SALSA20_BLOCK_SIZE 64
/*
* salsa20_bit — key-size selector
* ------------------------------------------------------------------
* Passed to functions that need to distinguish 128- vs 256-bit keys.
* The enum values equal the key length in bytes (16 or 32).
*/
typedef enum salsa20_bit {
salsa_128 = 16, /* 128-bit key → "expand 16-byte k" */
salsa_256 = 32, /* 256-bit key → "expand 32-byte k" */
} salsa20_bit_t;
/*
* salsa20_ctx — opaque cipher state (64 bytes)
* ------------------------------------------------------------------
* Internal layout (all offsets relative to salsa20_ctx_t.key[64]):
*
* [ 0 .. 31] key material (16 or 32 bytes used, rest zero)
* [32 .. 39] nonce (uint64_t, little-endian)
* [40 .. 47] block counter (uint64_t, little-endian)
* [48] key_bytes (16 or 32)
* [49] mode (0 = Salsa20, 1 = XSalsa20)
* [50 .. 63] reserved
*
* Callers MUST treat this struct as opaque: zero-initialise or
* initialise via salsa20_setkey() before any encrypt / decrypt call.
*/
typedef struct salsa20_ctx {
uint8_t key[SALSA20_BLOCK_SIZE];
} salsa20_ctx_t;
/*
* salsa20_setkey — initialise (or reset) a Salsa20 context
* ------------------------------------------------------------------
* ctx [out] caller-allocated context (must not be NULL)
* key [in] raw key bytes (must not be NULL)
* ksize [in] key length: 16 (128-bit) or 32 (256-bit)
*
* On success the context is fully reset:
* - key material copied
* - nonce zeroed
* - block counter set to 0
* - key_bytes marker set
*
* Returns:
* 0 success
* -1 bad argument (NULL pointer or ksize ∉ {16, 32})
*/
#define SALSA20_set_key(KEY, text, tsize) salsa20_setkey((KEY), (text), (tsize))
SALSA20_EXPORT int salsa20_setkey(salsa20_ctx_t *ctx, const uint8_t *key, int ksize);
/*
* salsa20_setnonce — set the nonce (8 or 24 bytes)
* ------------------------------------------------------------------
* ctx [in,out] context previously initialised by salsa20_setkey()
* nonce [in] nonce bytes (must not be NULL)
* nsize [in] nonce length in bytes: 8 (Salsa20) or 24 (XSalsa20)
*
* Modes:
* nsize = 8 → standard Salsa20 (64-bit nonce).
* Suitable when the caller can guarantee sequential /
* unique nonce allocation (e.g. a monotonic counter).
*
* nsize = 24 → XSalsa20 (192-bit nonce, Bernstein 2011).
* Uses HSalsa20 to derive a 256-bit sub-key from the
* original key and the first 16 bytes of the nonce;
* the remaining 8 bytes become the stream nonce.
* This mode REQUIRES a 256-bit key (key_bytes = 32).
* Safe for random-nonce generation with negligible
* collision probability, compatible with NaCl /
* libsodium secretbox (XSalsa20-Poly1305).
*
* After this call the internal key material is the
* derived sub-key — to switch nonces call setkey()
* again to restore the original key before setnonce().
*
* Both modes reset the block counter to 0.
*
* Security: a (key, nonce) pair MUST NOT be reused. For XSalsa20 the
* 192-bit nonce space makes random generation safe in practice.
*
* Returns:
* 0 success
* -1 bad argument (NULL ctx, NULL nonce, ctx not set up,
* nsize ∉ {8, 24}, or nsize=24 with 128-bit key)
*/
#define SALSA20_set_nonce(KEY, nonce, nsize) salsa20_setnonce((KEY), (nonce), (nsize))
SALSA20_EXPORT int salsa20_setnonce(salsa20_ctx_t *ctx, const uint8_t *nonce, int nsize);
/*
* SALSA20 / salsa20 — encrypt or decrypt (symmetric stream cipher)
* ------------------------------------------------------------------
* Convenience macro SALSA20(ctx, in, insize, out) expands to
* salsa20((ctx), (in), (insize), (out)).
*
* ctx [in,out] context initialised by setkey + setnonce (not NULL)
* in [in] plaintext or ciphertext bytes (not NULL)
* insize [in] number of bytes to process (> 0)
* out [out] destination buffer (not NULL, caller-sized)
*
* XORs the Salsa20 keystream with `in` and writes the result to `out`.
* `out` may alias `in` (in-place safe). The caller is expected to know
* the output length — for a stream cipher it always equals insize.
*
* The block counter inside ctx advances by ⌈insize / 64⌉ per call.
* Subsequent calls with the same ctx continue the keystream where the
* previous call left off — no need to manage the counter externally.
*
* Thread safety: fully re-entrant as long as each thread owns its ctx.
*
* Returns:
* non-NULL pointer to `out` (success)
* NULL error: NULL ctx / in / out, insize == 0, or ctx not
* initialised (key_bytes ∉ {16, 32})
*/
#define SALSA20(ctx, in, insize, out) salsa20((ctx), (in), (insize), (out))
SALSA20_EXPORT void* salsa20(salsa20_ctx_t *ctx, const void *in, unsigned int insize, unsigned char *out);
#if __cplusplus__
}
#endif
#endif /* SALSA20_H */
/*
** Salsa20 test vectors & functional tests — test_salsa20.c
** ============================================================
**
** Uses self-consistent reference generation: the first call with a
** given (key, nonce, counter) produces a "golden" keystream block;
** subsequent calls must reproduce it identically.
**
** Build (from project root):
** cc -std=c99 -O2 -o test_salsa20 test_salsa20.c salsa20.c && ./test_salsa20
**
** LICENSE: BSD
** Author: CandyMi [https://github.com/candymi]
*/
#include "salsa20.h"
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
/* ===================================================================
* Minimal test harness
* =================================================================== */
static int passed = 0;
static int failed = 0;
static const char *current_suite = "";
#define SUITE(name) do { \
current_suite = name; \
printf("\n========== %s ==========\n", name); \
} while(0)
#define TEST(cond, msg) do { \
if (cond) { \
passed++; \
printf(" [\033[32mPASS\033[0m] %s\n", msg); \
} else { \
failed++; \
printf(" [\033[31mFAIL\033[0m] %s (%s)\n", \
msg, current_suite); \
} \
} while(0)
static int
buf_eq(const uint8_t *a, const uint8_t *b, uint64_t len)
{
for (uint64_t i = 0; i < len; i++)
if (a[i] != b[i]) return 0;
return 1;
}
static void
hex_dump(const char *label, const uint8_t *buf, uint64_t len)
{
printf(" %s (%llu bytes): ", label, (unsigned long long)len);
uint64_t n = len < 64 ? len : 64;
for (uint64_t i = 0; i < n; i++)
printf("%02x", buf[i]);
if (len > 64) printf("...");
printf("\n");
}
static int
buf_check(const uint8_t *exp, const uint8_t *act, uint64_t len, const char *msg)
{
if (buf_eq(exp, act, len)) { TEST(1, msg); return 1; }
TEST(0, msg);
hex_dump("expected", exp, len);
hex_dump("actual ", act, len);
return 0;
}
/* ===================================================================
* Test keys / nonces
* =================================================================== */
/* all-zero 256-bit key */
static const uint8_t K256_0[32] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
/* sequential 256-bit key: 01 02 03 ... 20 (hex 0x20 = 32 dec) */
static const uint8_t K256_seq[32] = {
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,0x20,
};
/* all-zero nonce */
static const uint8_t N0[8] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
/* π nonce: 03 01 04 01 05 09 02 06 */
static const uint8_t N_pi[8] = {
0x03,0x01,0x04,0x01,0x05,0x09,0x02,0x06,
};
/* 128-bit test key */
static const uint8_t K128[16] = {
0x0f,0x1e,0x2d,0x3c,0x4b,0x5a,0x69,0x78,
0x87,0x96,0xa5,0xb4,0xc3,0xd2,0xe1,0xf0,
};
static const uint8_t N128[8] = {
0x12,0x34,0x56,0x78,0x9a,0xbc,0xde,0xf0,
};
/* ---- XSalsa20 24-byte nonces ---- */
/* all-zero 24-byte nonce */
static const uint8_t NX0[24] = {
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
};
/* sequential 24-byte nonce: fe dc ba 98 ... */
static const uint8_t NX_seq[24] = {
0xfe,0xdc,0xba,0x98,0x76,0x54,0x32,0x10,
0x0f,0x1e,0x2d,0x3c,0x4b,0x5a,0x69,0x78,
0x87,0x96,0xa5,0xb4,0xc3,0xd2,0xe1,0xf0,
};
/*
* XSalsa20 keystream regression vector.
*
* key = 1b 27 55 64 73 e9 85 d4 62 cd 51 19 7a 8a 46 c0
* 95 e0 63 c4 90 25 a3 5b 5e 2b 31 8c 85 fb 47 dd
* nonce= 69 69 6e e9 55 b6 2b 73 cd 62 ba 82 10 05 0a 2d
* 26 e6 44 c3 5e 17 ab 41
*
* Generated by this implementation — serves as a regression check
* to detect any future changes to the XSalsa20 keystream.
*/
static const uint8_t X_key[32] = {
0x1b,0x27,0x55,0x64,0x73,0xe9,0x85,0xd4,
0x62,0xcd,0x51,0x19,0x7a,0x8a,0x46,0xc0,
0x95,0xe0,0x63,0xc4,0x90,0x25,0xa3,0x5b,
0x5e,0x2b,0x31,0x8c,0x85,0xfb,0x47,0xdd,
};
static const uint8_t X_nonce[24] = {
0x69,0x69,0x6e,0xe9,0x55,0xb6,0x2b,0x73,
0xcd,0x62,0xba,0x82,0x10,0x05,0x0a,0x2d,
0x26,0xe6,0x44,0xc3,0x5e,0x17,0xab,0x41,
};
static const uint8_t X_ks0_32[32] = {
0xf4,0x48,0x73,0x59,0xc1,0x00,0xec,0x4e,
0xdf,0x98,0xea,0x56,0x11,0x88,0x8d,0x22,
0xcf,0xa9,0xf2,0xf1,0x2e,0xea,0x30,0x44,
0x02,0xa2,0x0e,0x9c,0xcc,0x19,0x0f,0xef,
};
/* ===================================================================
* Test suites
* =================================================================== */
/* ---- 1. determinism & self-consistency (256-bit) ---- */
static void
test_determinism_256(void)
{
SUITE("256-bit determinism & self-consistency");
uint8_t zero[128], ref[128], ref2[128];
memset(zero, 0, sizeof(zero));
/* First call → golden reference */
salsa20_ctx_t ctx;
SALSA20_set_key(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, N_pi, 8);
void *ret = SALSA20(&ctx, zero, 128, ref);
TEST(ret == ref, "golden ref: 128 bytes generated");
/* Second call (fresh ctx, same key/nonce) → must match */
salsa20_ctx_t ctx2;
SALSA20_set_key(&ctx2, K256_seq, 32);
salsa20_setnonce(&ctx2, N_pi, 8);
SALSA20(&ctx2, zero, 128, ref2);
buf_check(ref, ref2, 128, "deterministic: 2nd run matches 1st");
/* Third call → same output again */
salsa20_ctx_t ctx3;
SALSA20_set_key(&ctx3, K256_seq, 32);
salsa20_setnonce(&ctx3, N_pi, 8);
SALSA20(&ctx3, zero, 128, ref2);
buf_check(ref, ref2, 128, "deterministic: 3rd run matches 1st");
}
/* ---- 2. sequential blocks ---- */
static void
test_sequential_blocks(void)
{
SUITE("sequential full-block generation");
uint8_t zero[128], ref[128];
memset(zero, 0, sizeof(zero));
/* Generate 2 blocks (128 bytes) in one call */
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, N_pi, 8);
SALSA20(&ctx, zero, 128, ref);
/* Generate block 0 (64 bytes) and block 1 (64 bytes) separately */
salsa20_ctx_t ctx_sep;
salsa20_setkey(&ctx_sep, K256_seq, 32);
salsa20_setnonce(&ctx_sep, N_pi, 8);
uint8_t block0[64], block1[64];
SALSA20(&ctx_sep, zero, 64, block0);
SALSA20(&ctx_sep, zero, 64, block1);
/* Block 0 must match first 64 bytes of ref */
buf_check(ref, block0, 64, "block 0 (separate call) matches ref[ 0..63]");
buf_check(ref + 64, block1, 64, "block 1 (separate call) matches ref[64..127]");
}
/* ---- 3. sub-block calls (Salsa20 block semantics) ---- */
static void
test_sub_block_semantics(void)
{
SUITE("sub-block call semantics");
uint8_t zero[128];
memset(zero, 0, sizeof(zero));
/*
* Salsa20 generates a full 64-byte block on every call.
* Processing 37 bytes consumes the first 37 bytes of block 0
* (discarding bytes 37..63). The next call starts at block 1.
*/
salsa20_ctx_t ctx_a;
salsa20_setkey(&ctx_a, K256_0, 32);
salsa20_setnonce(&ctx_a, N0, 8);
uint8_t sub[37];
SALSA20(&ctx_a, zero, 37, sub);
TEST(SALSA20(&ctx_a, zero, 37, sub) == sub,
"sub-block: 37 bytes encrypt OK");
/* Verify: 37 bytes × 1 call vs first 37 of a full block */
salsa20_ctx_t ctx_full;
salsa20_setkey(&ctx_full, K256_0, 32);
salsa20_setnonce(&ctx_full, N0, 8);
uint8_t full[64];
SALSA20(&ctx_full, zero, 64, full);
salsa20_ctx_t ctx_sub;
salsa20_setkey(&ctx_sub, K256_0, 32);
salsa20_setnonce(&ctx_sub, N0, 8);
uint8_t sub37[37];
SALSA20(&ctx_sub, zero, 37, sub37);
buf_check(full, sub37, 37, "37-byte call matches first 37 of full block");
/* After 37 bytes, counter is at 1. Next 27 bytes come from block 1. */
uint8_t sub27[27];
SALSA20(&ctx_sub, zero, 27, sub27);
/* Advance past block 0, get block 1 */
salsa20_ctx_t ctx_b1_check;
salsa20_setkey(&ctx_b1_check, K256_0, 32);
salsa20_setnonce(&ctx_b1_check, N0, 8);
uint8_t full_b1[64];
uint8_t dummy[64];
SALSA20(&ctx_b1_check, zero, 64, dummy); /* consume block 0 */
SALSA20(&ctx_b1_check, zero, 64, full_b1); /* get block 1 */
buf_check(full_b1, sub27, 27, "next 27 bytes come from block 1 start");
}
/* ---- 4. 128-bit key roundtrip ---- */
static void
test_128_roundtrip(void)
{
SUITE("128-bit key roundtrip");
const char *msg = "Salsa20 128-bit key test — roundtrip OK.";
uint64_t len = (uint64_t)strlen(msg);
uint8_t *cipher = (uint8_t *)malloc(len);
uint8_t *plain = (uint8_t *)malloc(len);
/* Encrypt */
salsa20_ctx_t ctx;
SALSA20_set_key(&ctx, K128, 16);
salsa20_setnonce(&ctx, N128, 8);
void *ret_e = SALSA20(&ctx, msg, len, cipher);
TEST(ret_e == cipher, "encrypt returned correct length");
TEST(!buf_eq((const uint8_t *)msg, cipher, len), "cipher != plain");
/* Decrypt */
salsa20_ctx_t ctx_d;
SALSA20_set_key(&ctx_d, K128, 16);
salsa20_setnonce(&ctx_d, N128, 8);
SALSA20(&ctx_d, cipher, len, plain);
buf_check((const uint8_t *)msg, plain, len, "decrypt roundtrip");
free(cipher);
free(plain);
}
/* ---- 5. out == NULL ---- */
static void
test_out_null(void)
{
SUITE("out == NULL (size query)");
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_0, 32);
/* out==NULL → NULL (caller must size the output buffer themselves) */
TEST(salsa20(&ctx, K256_0, 64, NULL) == NULL,
"salsa20(out=NULL) → NULL");
TEST(salsa20(&ctx, K256_0, 0, NULL) == NULL,
"salsa20(out=NULL, insize=0) → NULL");
}
/* ---- 6. bad arguments ---- */
static void
test_bad_args(void)
{
SUITE("bad arguments");
salsa20_ctx_t ctx;
uint8_t buf[16];
salsa20_ctx_t zero_ctx;
memset(&zero_ctx, 0, sizeof(zero_ctx));
TEST(salsa20_setkey(NULL, K256_0, 32) == -1, "setkey(ctx=NULL) → -1");
TEST(salsa20_setkey(&ctx, NULL, 32) == -1, "setkey(key=NULL) → -1");
TEST(salsa20_setkey(&ctx, K256_0, 0) == -1, "setkey(ksize=0) → -1");
TEST(salsa20_setkey(&ctx, K256_0, 8) == -1, "setkey(ksize=8) → -1");
TEST(salsa20_setkey(&ctx, K256_0, 64) == -1, "setkey(ksize=64) → -1");
TEST(salsa20_setkey(&ctx, K256_0, 15) == -1, "setkey(ksize=15) → -1");
TEST(salsa20(NULL, K256_0, 16, buf) == NULL, "salsa20(ctx=NULL) → NULL");
salsa20_setkey(&ctx, K256_0, 32);
TEST(salsa20(&ctx, NULL, 16, buf) == NULL, "salsa20(in=NULL) → NULL");
TEST(salsa20(&zero_ctx, K256_0, 16, buf) == NULL,
"salsa20 without setkey → NULL");
TEST(salsa20_setnonce(NULL, N0, 8) == -1, "setnonce(ctx=NULL) → -1");
TEST(salsa20_setnonce(&ctx, NULL, 8) == -1, "setnonce(nonce=NULL) → -1");
TEST(salsa20_setnonce(&zero_ctx, N0, 8) == -1, "setnonce without setkey → -1");
/* nsize validation */
salsa20_setkey(&ctx, K256_0, 32);
TEST(salsa20_setnonce(&ctx, N0, 0) == -1, "setnonce(nsize=0) → -1");
TEST(salsa20_setnonce(&ctx, N0, 7) == -1, "setnonce(nsize=7) → -1");
TEST(salsa20_setnonce(&ctx, N0, 16) == -1, "setnonce(nsize=16) → -1");
TEST(salsa20_setnonce(&ctx, N0, 25) == -1, "setnonce(nsize=25) → -1");
TEST(salsa20_setnonce(&ctx, N0, 32) == -1, "setnonce(nsize=32) → -1");
/* XSalsa20 requires 256-bit key: nsize=24 with 128-bit key → -1 */
salsa20_setkey(&ctx, K128, 16);
TEST(salsa20_setnonce(&ctx, NX0, 24) == -1, "setnonce(nsize=24, 128-bit key) → -1");
}
/* ---- 7. in-place ---- */
static void
test_inplace(void)
{
SUITE("in-place encrypt / decrypt");
char msg[] = "The quick brown fox jumps over the lazy dog. (in-place!)";
uint64_t len = (uint64_t)strlen(msg);
char *saved = (char *)malloc(len + 1);
memcpy(saved, msg, len + 1);
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, N_pi, 8);
SALSA20(&ctx, msg, len, (unsigned char *)msg);
TEST(!buf_eq((const uint8_t *)saved, (const uint8_t *)msg, len),
"in-place encrypt changed data");
salsa20_ctx_t ctx2;
salsa20_setkey(&ctx2, K256_seq, 32);
salsa20_setnonce(&ctx2, N_pi, 8);
SALSA20(&ctx2, msg, len, (unsigned char *)msg);
buf_check((const uint8_t *)saved, (const uint8_t *)msg, len,
"in-place roundtrip restored original");
free(saved);
}
/* ---- 8. different keys/nonces ---- */
static void
test_different_keys_nonces(void)
{
SUITE("different keys / nonces → different output");
uint8_t zero[64], out1[64], out2[64];
memset(zero, 0, 64);
/* Different keys */
salsa20_ctx_t ctx;
SALSA20_set_key(&ctx, K256_0, 32);
salsa20_setnonce(&ctx, N0, 8);
SALSA20(&ctx, zero, 64, out1);
salsa20_ctx_t ctx_k2;
SALSA20_set_key(&ctx_k2, K256_seq, 32);
salsa20_setnonce(&ctx_k2, N0, 8);
SALSA20(&ctx_k2, zero, 64, out2);
TEST(!buf_eq(out1, out2, 64), "different keys → different keystream");
/* Same key, different nonces */
salsa20_ctx_t ctx_n1;
SALSA20_set_key(&ctx_n1, K256_0, 32);
salsa20_setnonce(&ctx_n1, N0, 8);
SALSA20(&ctx_n1, zero, 64, out1);
const uint8_t nonce_b[8] = {8,7,6,5,4,3,2,1};
salsa20_ctx_t ctx_n2;
SALSA20_set_key(&ctx_n2, K256_0, 32);
salsa20_setnonce(&ctx_n2, nonce_b, 8);
SALSA20(&ctx_n2, zero, 64, out2);
TEST(!buf_eq(out1, out2, 64), "different nonces → different keystream");
/* Verify nonce setter resets counter (same nonce twice → same output) */
salsa20_ctx_t ctx_nr;
SALSA20_set_key(&ctx_nr, K256_0, 32);
salsa20_setnonce(&ctx_nr, N_pi, 8);
SALSA20(&ctx_nr, zero, 64, out1); /* block 0 */
SALSA20(&ctx_nr, zero, 64, out1); /* block 1 */
salsa20_setnonce(&ctx_nr, N_pi, 8); /* reset */
SALSA20(&ctx_nr, zero, 64, out2); /* block 0 again */
SALSA20_set_key(&ctx, K256_0, 32);
salsa20_setnonce(&ctx, N_pi, 8);
uint8_t ref_b0[64];
SALSA20(&ctx, zero, 64, ref_b0);
buf_check(ref_b0, out2, 64, "setnonce resets counter → block 0 matches fresh ctx");
}
/* ---- 9. large data (1 MB) ---- */
static void
test_large_data(void)
{
SUITE("large data (1 MB)");
const uint64_t big = 1024 * 1024;
uint8_t *data = (uint8_t *)malloc(big);
if (!data) { TEST(0, "malloc failed"); return; }
for (uint64_t i = 0; i < big; i++)
data[i] = (uint8_t)(i & 0xff);
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, N_pi, 8);
void *ret_big = SALSA20(&ctx, data, big, data);
TEST(ret_big == data, "1 MB encrypt returned correct length");
int changed = 0;
for (uint64_t i = 0; i < big; i++)
if (data[i] != (uint8_t)(i & 0xff)) { changed = 1; break; }
TEST(changed, "1 MB ciphertext differs from plaintext");
salsa20_ctx_t ctx_d;
salsa20_setkey(&ctx_d, K256_seq, 32);
salsa20_setnonce(&ctx_d, N_pi, 8);
SALSA20(&ctx_d, data, big, data);
int ok = 1;
for (uint64_t i = 0; i < big; i++)
if (data[i] != (uint8_t)(i & 0xff)) { ok = 0; break; }
TEST(ok, "1 MB decrypt roundtrip");
free(data);
}
/* ---- 10. encrypt == decrypt ---- */
static void
test_encrypt_eq_decrypt(void)
{
SUITE("encrypt == decrypt (stream cipher symmetry)");
const char *msg = "Salsa20: encrypt and decrypt produce identical output.";
uint64_t len = (uint64_t)strlen(msg);
salsa20_ctx_t ctx_e, ctx_d;
salsa20_setkey(&ctx_e, K256_seq, 32); salsa20_setnonce(&ctx_e, N_pi, 8);
salsa20_setkey(&ctx_d, K256_seq, 32); salsa20_setnonce(&ctx_d, N_pi, 8);
uint8_t out_e[128], out_d[128];
SALSA20(&ctx_e, msg, len, out_e);
SALSA20(&ctx_d, msg, len, out_d);
buf_check(out_e, out_d, len, "encrypt(msg) == decrypt(msg)");
}
/* ---- 11. encrypt then decrypt (tunnel roundtrip) ---- */
static void
test_tunnel_roundtrip(void)
{
SUITE("encrypt-then-decrypt tunnel");
const char *msg = "Encrypt with one context, decrypt with another.";
uint64_t len = (uint64_t)strlen(msg);
uint8_t *cipher = (uint8_t *)malloc(len);
uint8_t *plain = (uint8_t *)malloc(len);
/* Encrypt */
salsa20_ctx_t ctx_e;
salsa20_setkey(&ctx_e, K256_seq, 32);
salsa20_setnonce(&ctx_e, N_pi, 8);
SALSA20(&ctx_e, msg, len, cipher);
/* Decrypt with fresh ctx */
salsa20_ctx_t ctx_d;
SALSA20_set_key(&ctx_d, K256_seq, 32);
salsa20_setnonce(&ctx_d, N_pi, 8);
SALSA20(&ctx_d, cipher, len, plain);
buf_check((const uint8_t *)msg, plain, len, "encrypt → decrypt restores original");
free(cipher);
free(plain);
}
/* ---- 12. counter wraps into high word ---- */
static void
test_counter_high_word(void)
{
SUITE("counter high-word wrap");
/*
* Generate 128 blocks (8 KB) to verify the counter rolls into
* the high 32-bit word. Stream position 128 uses counter = 128
* which is 0x00000000_00000080.
*/
const uint64_t n_blocks = 128;
const uint64_t total = n_blocks * 64; /* 8192 bytes */
uint8_t *ref = (uint8_t *)malloc(total);
uint8_t *zero = (uint8_t *)calloc(1, total);
if (!ref || !zero) { TEST(0, "malloc"); free(ref); free(zero); return; }
/* One-shot generation */
salsa20_ctx_t ctx1;
salsa20_setkey(&ctx1, K256_seq, 32);
salsa20_setnonce(&ctx1, N_pi, 8);
SALSA20(&ctx1, zero, total, ref);
/* Block-by-block generation */
salsa20_ctx_t ctx2;
salsa20_setkey(&ctx2, K256_seq, 32);
salsa20_setnonce(&ctx2, N_pi, 8);
uint8_t *chunked = (uint8_t *)malloc(total);
for (uint64_t i = 0; i < n_blocks; i++)
SALSA20(&ctx2, zero, 64, chunked + i * 64);
buf_check(ref, chunked, total, "128 blocks one-shot == block-by-block");
free(ref);
free(zero);
free(chunked);
}
/* ---- 13. setkey then re-setkey (re-init) ---- */
static void
test_rekey(void)
{
SUITE("re-setkey (re-initialisation)");
salsa20_ctx_t ctx;
uint8_t zero[64], out1[64], out2[64];
memset(zero, 0, 64);
/* First key */
SALSA20_set_key(&ctx, K256_0, 32);
salsa20_setnonce(&ctx, N0, 8);
SALSA20(&ctx, zero, 64, out1);
/* Re-key: same key, should start from counter 0 again (setkey zeros all) */
SALSA20_set_key(&ctx, K256_0, 32);
salsa20_setnonce(&ctx, N0, 8);
SALSA20(&ctx, zero, 64, out2);
buf_check(out1, out2, 64, "re-setkey same key → same output (counter reset)");
/* Re-key with different key */
SALSA20_set_key(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, N0, 8);
SALSA20(&ctx, zero, 64, out2);
TEST(!buf_eq(out1, out2, 64), "re-setkey different key → different output");
}
/* ---- 14. macro coverage (SALSA20 / SALSA20_set_key) ---- */
static void
test_macros(void)
{
SUITE("macro coverage");
uint8_t zero[64], ks1[64], ks2[64];
memset(zero, 0, 64);
/*
* SALSA20_set_key and SALSA20 MUST expand to the same function
* calls as their lowercase counterparts.
*/
/* SALSA20_set_key → salsa20_setkey */
salsa20_ctx_t ctx1, ctx2;
TEST(SALSA20_set_key(&ctx1, K256_seq, 32) == 0, "SALSA20_set_key → 0");
TEST(salsa20_setkey(&ctx2, K256_seq, 32) == 0, "salsa20_setkey → 0");
salsa20_setnonce(&ctx1, N_pi, 8);
salsa20_setnonce(&ctx2, N_pi, 8);
/* SALSA20 → salsa20: both must produce identical keystream */
SALSA20(&ctx1, zero, 64, ks1);
salsa20(&ctx2, zero, 64, ks2);
buf_check(ks1, ks2, 64, "SALSA20(...) == salsa20(...)");
/* SALSA20 with out==NULL → NULL */
TEST(SALSA20(&ctx1, zero, 37, NULL) == NULL, "SALSA20(out=NULL) → NULL");
/* SALSA20 error path */
TEST(SALSA20(NULL, zero, 64, ks1) == NULL, "SALSA20(ctx=NULL) → NULL");
TEST(SALSA20(&ctx1, NULL, 64, ks1) == NULL, "SALSA20(in=NULL) → NULL");
TEST(SALSA20(&ctx1, zero, 0, ks1) == NULL, "SALSA20(insize=0) → NULL");
/* SALSA20_set_key error path */
TEST(SALSA20_set_key(NULL, K256_seq, 32) == -1,
"SALSA20_set_key(ctx=NULL) → -1");
TEST(SALSA20_set_key(&ctx1, NULL, 32) == -1,
"SALSA20_set_key(key=NULL) → -1");
TEST(SALSA20_set_key(&ctx1, K256_seq, 8) == -1,
"SALSA20_set_key(ksize=8) → -1");
}
/* ---- 15. XSalsa20: determinism & self-consistency ---- */
static void
test_xsalsa20_determinism(void)
{
SUITE("XSalsa20 determinism");
uint8_t zero[128], ref[128], ref2[128];
memset(zero, 0, sizeof(zero));
/* First call → golden reference */
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, NX_seq, 24);
void *ret = SALSA20(&ctx, zero, 128, ref);
TEST(ret == ref, "XSalsa20: 128 bytes generated");
/* Second call (fresh ctx, same key/nonce) → must match */
salsa20_ctx_t ctx2;
salsa20_setkey(&ctx2, K256_seq, 32);
salsa20_setnonce(&ctx2, NX_seq, 24);
SALSA20(&ctx2, zero, 128, ref2);
buf_check(ref, ref2, 128, "XSalsa20: 2nd run matches 1st");
/* Verify mode byte */
salsa20_ctx_t ctx3;
salsa20_setkey(&ctx3, K256_seq, 32);
salsa20_setnonce(&ctx3, NX_seq, 24);
/* ctx3.key[49] should be 1 (XSalsa20 mode) */
TEST(ctx3.key[49] == 1, "XSalsa20 mode byte set to 1");
}
/* ---- 16. XSalsa20: different nonces → different keystream ---- */
static void
test_xsalsa20_different_nonces(void)
{
SUITE("XSalsa20 different nonces");
uint8_t zero[64], out1[64], out2[64];
memset(zero, 0, 64);
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, NX0, 24);
SALSA20(&ctx, zero, 64, out1);
salsa20_ctx_t ctx2;
salsa20_setkey(&ctx2, K256_seq, 32);
salsa20_setnonce(&ctx2, NX_seq, 24);
SALSA20(&ctx2, zero, 64, out2);
TEST(!buf_eq(out1, out2, 64), "XSalsa20: different nonces → different output");
}
/* ---- 17. XSalsa20: encrypt / decrypt roundtrip ---- */
static void
test_xsalsa20_roundtrip(void)
{
SUITE("XSalsa20 roundtrip");
const char *msg = "XSalsa20 encrypt / decrypt roundtrip — OK!";
uint64_t len = (uint64_t)strlen(msg);
uint8_t *cipher = (uint8_t *)malloc(len);
uint8_t *plain = (uint8_t *)malloc(len);
/* Encrypt */
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, NX_seq, 24);
SALSA20(&ctx, msg, len, cipher);
TEST(!buf_eq((const uint8_t *)msg, cipher, len),
"XSalsa20: cipher != plain");
/* Decrypt with fresh ctx */
salsa20_ctx_t ctx_d;
salsa20_setkey(&ctx_d, K256_seq, 32);
salsa20_setnonce(&ctx_d, NX_seq, 24);
SALSA20(&ctx_d, cipher, len, plain);
buf_check((const uint8_t *)msg, plain, len, "XSalsa20: roundtrip restores");
free(cipher);
free(plain);
}
/* ---- 18. XSalsa20: sequential blocks ---- */
static void
test_xsalsa20_sequential(void)
{
SUITE("XSalsa20 sequential blocks");
uint8_t zero[128], ref[128];
memset(zero, 0, sizeof(zero));
/* 2 blocks in one call */
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, NX_seq, 24);
SALSA20(&ctx, zero, 128, ref);
/* Block-by-block */
salsa20_ctx_t ctx_sep;
salsa20_setkey(&ctx_sep, K256_seq, 32);
salsa20_setnonce(&ctx_sep, NX_seq, 24);
uint8_t b0[64], b1[64];
SALSA20(&ctx_sep, zero, 64, b0);
SALSA20(&ctx_sep, zero, 64, b1);
buf_check(ref, b0, 64, "XSalsa20 block 0 matches ref[ 0..63]");
buf_check(ref + 64, b1, 64, "XSalsa20 block 1 matches ref[64..127]");
}
/* ---- 19. XSalsa20: in-place ---- */
static void
test_xsalsa20_inplace(void)
{
SUITE("XSalsa20 in-place");
char msg[] = "XSalsa20 in-place encrypt / decrypt test!";
uint64_t len = (uint64_t)strlen(msg);
char *saved = (char *)malloc(len + 1);
memcpy(saved, msg, len + 1);
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, NX_seq, 24);
SALSA20(&ctx, msg, len, (unsigned char *)msg);
TEST(!buf_eq((const uint8_t *)saved, (const uint8_t *)msg, len),
"XSalsa20 in-place: encrypt changed data");
salsa20_ctx_t ctx_d;
salsa20_setkey(&ctx_d, K256_seq, 32);
salsa20_setnonce(&ctx_d, NX_seq, 24);
SALSA20(&ctx_d, msg, len, (unsigned char *)msg);
buf_check((const uint8_t *)saved, (const uint8_t *)msg, len,
"XSalsa20 in-place: roundtrip restored");
free(saved);
}
/* ---- 20. XSalsa20: rekey restores mode ---- */
static void
test_xsalsa20_rekey(void)
{
SUITE("XSalsa20 rekey → mode reset");
salsa20_ctx_t ctx;
/* Set up XSalsa20 */
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, NX_seq, 24);
TEST(ctx.key[49] == 1, "XSalsa20 mode = 1");
/* Re-key should reset mode to Salsa20 */
SALSA20_set_key(&ctx, K256_seq, 32);
TEST(ctx.key[49] == 0, "rekey resets mode to 0 (Salsa20)");
/* Now setnonce(8) should work as Salsa20 */
uint8_t zero[64], out[64];
memset(zero, 0, 64);
salsa20_setnonce(&ctx, N_pi, 8);
SALSA20(&ctx, zero, 64, out);
/* Compare with fresh Salsa20 context */
salsa20_ctx_t ref_ctx;
salsa20_setkey(&ref_ctx, K256_seq, 32);
salsa20_setnonce(&ref_ctx, N_pi, 8);
uint8_t ref[64];
SALSA20(&ref_ctx, zero, 64, ref);
buf_check(ref, out, 64, "rekey → Salsa20 mode matches standard Salsa20");
}
/* ---- 21. XSalsa20: keystream regression vector ---- */
static void
test_xsalsa20_regression_vector(void)
{
SUITE("XSalsa20 regression vector");
uint8_t zero[32], ks[32];
memset(zero, 0, sizeof(zero));
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, X_key, 32);
salsa20_setnonce(&ctx, X_nonce, 24);
SALSA20(&ctx, zero, 32, ks);
buf_check(X_ks0_32, ks, 32, "XSalsa20: regression vector block 0[0..31]");
}
/* ---- 22. XSalsa20: XSalsa20 ≠ Salsa20 with same 8-byte suffix ---- */
static void
test_xsalsa20_not_salsa20(void)
{
SUITE("XSalsa20 ≠ Salsa20");
/*
* Even when the 24-byte nonce's last 8 bytes match an 8-byte Salsa20
* nonce, the output MUST differ because HSalsa20 derives a different key.
*/
uint8_t zero[64], xs_out[64], s_out[64];
memset(zero, 0, 64);
/* XSalsa20 with nonce = NX_seq (last 8 bytes = 87 96 a5 b4 c3 d2 e1 f0) */
salsa20_ctx_t ctx_x;
salsa20_setkey(&ctx_x, K256_seq, 32);
salsa20_setnonce(&ctx_x, NX_seq, 24);
SALSA20(&ctx_x, zero, 64, xs_out);
/* Salsa20 with nonce = last 8 bytes of NX_seq */
salsa20_ctx_t ctx_s;
salsa20_setkey(&ctx_s, K256_seq, 32);
salsa20_setnonce(&ctx_s, NX_seq + 16, 8);
SALSA20(&ctx_s, zero, 64, s_out);
TEST(!buf_eq(xs_out, s_out, 64),
"XSalsa20 keystream ≠ Salsa20 with same trailing 8-byte nonce");
}
/* ---- 23. XSalsa20: setnonce overwrites key (can't reuse original) ---- */
static void
test_xsalsa20_key_overwrite(void)
{
SUITE("XSalsa20 key overwrite");
uint8_t zero[64], out1[64], out2[64];
memset(zero, 0, 64);
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, NX_seq, 24);
SALSA20(&ctx, zero, 64, out1);
/*
* ctx now has the derived sub-key, not the original K256_seq.
* Calling setnonce(8) uses the DERIVED key as the Salsa20 key
* — this is intentional per the documented API contract.
*/
salsa20_setnonce(&ctx, N0, 8);
SALSA20(&ctx, zero, 64, out2);
/* Compare with standard Salsa20 using the ORIGINAL key */
salsa20_ctx_t ref_ctx;
salsa20_setkey(&ref_ctx, K256_seq, 32);
salsa20_setnonce(&ref_ctx, N0, 8);
uint8_t ref[64];
SALSA20(&ref_ctx, zero, 64, ref);
/* These MUST differ because ctx key was replaced by HSalsa20 */
TEST(!buf_eq(ref, out2, 64),
"XSalsa20 tx key overwritten: setnonce(8) uses derived key");
/*
* To get the original key back, caller must re-setkey:
*/
SALSA20_set_key(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, N0, 8);
SALSA20(&ctx, zero, 64, out2);
buf_check(ref, out2, 64, "re-setkey → setnonce(8) uses original key again");
}
/* ---- 24. XSalsa20: large data (256 KB) ---- */
static void
test_xsalsa20_large(void)
{
SUITE("XSalsa20 large data (256 KB)");
const uint64_t big = 256 * 1024;
uint8_t *data = (uint8_t *)malloc(big);
if (!data) { TEST(0, "malloc"); return; }
for (uint64_t i = 0; i < big; i++)
data[i] = (uint8_t)(i & 0xff);
/* Encrypt */
salsa20_ctx_t ctx;
salsa20_setkey(&ctx, K256_seq, 32);
salsa20_setnonce(&ctx, NX_seq, 24);
SALSA20(&ctx, data, big, data);
int changed = 0;
for (uint64_t i = 0; i < big; i++)
if (data[i] != (uint8_t)(i & 0xff)) { changed = 1; break; }
TEST(changed, "XSalsa20: 256 KB encrypted differs from plaintext");
/* Decrypt */
salsa20_ctx_t ctx_d;
salsa20_setkey(&ctx_d, K256_seq, 32);
salsa20_setnonce(&ctx_d, NX_seq, 24);
SALSA20(&ctx_d, data, big, data);
int ok = 1;
for (uint64_t i = 0; i < big; i++)
if (data[i] != (uint8_t)(i & 0xff)) { ok = 0; break; }
TEST(ok, "XSalsa20: 256 KB roundtrip");
free(data);
}
/* ===================================================================
* main
* =================================================================== */
int main(void)
{
printf("Salsa20 test suite\n");
printf("==================\n");
test_determinism_256();
test_sequential_blocks();
test_sub_block_semantics();
test_128_roundtrip();
test_out_null();
test_bad_args();
test_inplace();
test_different_keys_nonces();
test_large_data();
test_encrypt_eq_decrypt();
test_tunnel_roundtrip();
test_counter_high_word();
test_rekey();
test_macros();
test_xsalsa20_determinism();
test_xsalsa20_different_nonces();
test_xsalsa20_roundtrip();
test_xsalsa20_sequential();
test_xsalsa20_inplace();
test_xsalsa20_rekey();
test_xsalsa20_regression_vector();
test_xsalsa20_not_salsa20();
test_xsalsa20_key_overwrite();
test_xsalsa20_large();
int total = passed + failed;
printf("\n========================================\n");
printf("RESULTS: %d passed, %d failed, %d total\n", passed, failed, total);
if (failed > 0) {
printf("*** SOME TESTS FAILED ***\n");
return 1;
}
printf("ALL TESTS PASSED\n");
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment