From 8227c57abcfc30e76a49ce4eaf5aa3c8c231da55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20=C5=A0pinka?= Date: Mon, 27 Jan 2025 16:42:55 +0100 Subject: [PATCH] Implement Salsa20. --- README.md | 3 +- src/primitive/streamcipher/chacha20.zig | 3 +- src/primitive/streamcipher/salsa20.zig | 498 ++++++++++++++++++++++++ 3 files changed, 502 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 875ad7e..fd824c4 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ Most (**theoretical!**) users should directly use one of the cryptographic *prot - Advanced Encryption Standard (FIPS 197): AES-128, AES-192, AES-256 - ChaCha20 (RFC 7539): ChaCha20 with 64-bit nonce and 64-bit counter, ChaCha20 with 96-bit nonce and 32-bit counter +- Salsa20: Salsa20/20 with 256-key, Salsa20/20 with 128-bit key ### Protocols @@ -30,7 +31,7 @@ Most (**theoretical!**) users should directly use one of the cryptographic *prot ### Primitives - DES, 3DES -- Salsa20 +- Block cipher modes: CBC-PKCS7, CFB, OFB, CTR, GCM - Poly1305 - SHA-256 - BigIntegers & modular arithmetic diff --git a/src/primitive/streamcipher/chacha20.zig b/src/primitive/streamcipher/chacha20.zig index ea9cc65..8e8ab61 100644 --- a/src/primitive/streamcipher/chacha20.zig +++ b/src/primitive/streamcipher/chacha20.zig @@ -79,9 +79,10 @@ pub fn chacha20_new( } pub fn chacha20_destroy(ctx: *ChaCha20Ctx) void { - @memset(&ctx.state, 0); @memset(&ctx.key, 0); @memset(&ctx.nonce, 0); + @memset(&ctx.state, 0); + @memset(&ctx.working_state, 0); ctx.keystream_idx = 0; } diff --git a/src/primitive/streamcipher/salsa20.zig b/src/primitive/streamcipher/salsa20.zig index e69de29..a9b75b9 100644 --- a/src/primitive/streamcipher/salsa20.zig +++ b/src/primitive/streamcipher/salsa20.zig @@ -0,0 +1,498 @@ +const std = @import("std"); +const testing = std.testing; + +// ----------------------------------- ERROR DEFINITIONS ----------------------------------- // + +const KeyStreamDepleted = error.KeyStreamDepleted; + +// ----------------------------------- ChaCha20 CONSTANTS ----------------------------------- // + +const SALSA20_BLOCK_SIZE = 512 / 8; +const SALSA20_BLOCK_WORDS = SALSA20_BLOCK_SIZE / 4; + +const SALSA20_KEY_SIZE = 256 / 8; +const SALSA20_KEY_WORDS = SALSA20_KEY_SIZE / 4; +const SALSA20_128_KEY_SIZE = 128 / 8; +const SALSA20_128_KEY_WORDS = SALSA20_128_KEY_SIZE / 4; + +const SALSA20_NONCE_SIZE = 64 / 8; +const SALSA20_NONCE_WORDS = SALSA20_NONCE_SIZE / 4; + +const SALSA20_COUNTER_SIZE = 64 / 8; +const SALSA20_COUNTER_WORDS = SALSA20_COUNTER_SIZE / 4; + +const SALSA20_CONSTANT_WORDS = 128 / 8 / 4; + +const SALSA20_256_CONSTANTS = [SALSA20_CONSTANT_WORDS]u32{ + bytes_to_word_le("expa"), + bytes_to_word_le("nd 3"), + bytes_to_word_le("2-by"), + bytes_to_word_le("te k"), +}; +const SALSA20_128_CONSTANTS = [SALSA20_CONSTANT_WORDS]u32{ + bytes_to_word_le("expa"), + bytes_to_word_le("nd 1"), + bytes_to_word_le("6-by"), + bytes_to_word_le("te k"), +}; + +// ----------------------------------- CONTEXT MANAGEMENT ----------------------------------- // + +pub const Salsa20Ctx = struct { + key: [SALSA20_KEY_WORDS]u32, + nonce: [SALSA20_NONCE_WORDS]u32, + counter: [SALSA20_COUNTER_WORDS]u32, + constants: [SALSA20_CONSTANT_WORDS]u32, + state: [SALSA20_BLOCK_WORDS]u32, + working_state: [SALSA20_BLOCK_WORDS]u32, + keystream_idx: u8, +}; + +pub fn salsa20_new( + key: *const [SALSA20_KEY_SIZE]u8, + nonce: *const [SALSA20_NONCE_SIZE]u8, + counter: *const [SALSA20_COUNTER_SIZE]u8, + constants: *const [SALSA20_CONSTANT_WORDS]u32, +) Salsa20Ctx { + var ctx = Salsa20Ctx{ + .key = undefined, + .nonce = undefined, + .counter = undefined, + .constants = undefined, + .state = undefined, + .working_state = undefined, + .keystream_idx = undefined, + }; + + salsa20_deserialize(SALSA20_KEY_WORDS, key, &ctx.key); + salsa20_deserialize(SALSA20_COUNTER_WORDS, counter, &ctx.counter); + salsa20_deserialize(SALSA20_NONCE_WORDS, nonce, &ctx.nonce); + @memcpy(&ctx.constants, constants); + + salsa20_block_function(&ctx); + ctx.keystream_idx = 0; + + return ctx; +} + +pub fn salsa20_destroy(ctx: *Salsa20Ctx) void { + @memset(&ctx.key, 0); + @memset(&ctx.nonce, 0); + @memset(&ctx.state, 0); + @memset(&ctx.working_state, 0); + ctx.keystream_idx = 0; +} + +// ----------------------------------- ENCRYPTION/DECRYPTION ----------------------------------- // + +pub fn salsa20_encrypt_inplace(ctx: *Salsa20Ctx, plaintext: []u8) !void { + for (0..plaintext.len) |i| { + try salsa20_ensure_keystream_expanded(ctx); + const keystream: [*]const u8 = @ptrCast(&ctx.state); + + plaintext[i] ^= keystream[ctx.keystream_idx]; + ctx.keystream_idx += 1; + } +} + +pub fn salsa20_decrypt_inplace(ctx: *Salsa20Ctx, ciphertext: []u8) !void { + return salsa20_encrypt_inplace(ctx, ciphertext); +} + +fn salsa20_ensure_keystream_expanded(ctx: *Salsa20Ctx) !void { + if (ctx.keystream_idx == SALSA20_BLOCK_SIZE) { + try salsa20_increment_counter(ctx); + salsa20_block_function(ctx); + ctx.keystream_idx = 0; + } +} + +fn salsa20_increment_counter(ctx: *Salsa20Ctx) !void { + for (0..SALSA20_COUNTER_WORDS) |idx| { + const ov = @addWithOverflow(ctx.counter[idx], 1); + ctx.counter[idx] = ov[0]; + + if (ov[1] == 0) + return; + } + return KeyStreamDepleted; +} + +// ----------------------------------- KEYSTREAM EXPANSION ----------------------------------- // + +pub fn salsa20_quarter_round(state: *[SALSA20_BLOCK_WORDS]u32, ia: u8, ib: u8, ic: u8, id: u8) void { + var a = state[ia]; + var b = state[ib]; + var c = state[ic]; + var d = state[id]; + + b ^= rol(a +% d, 7); + c ^= rol(b +% a, 9); + d ^= rol(c +% b, 13); + a ^= rol(d +% c, 18); + + state[ia] = a; + state[ib] = b; + state[ic] = c; + state[id] = d; +} + +pub fn salsa20_column_round(state: *[SALSA20_BLOCK_WORDS]u32) void { + salsa20_quarter_round(state, 0, 4, 8, 12); + salsa20_quarter_round(state, 5, 9, 13, 1); + salsa20_quarter_round(state, 10, 14, 2, 6); + salsa20_quarter_round(state, 15, 3, 7, 11); +} + +pub fn salsa20_row_round(state: *[SALSA20_BLOCK_WORDS]u32) void { + salsa20_quarter_round(state, 0, 1, 2, 3); + salsa20_quarter_round(state, 5, 6, 7, 4); + salsa20_quarter_round(state, 10, 11, 8, 9); + salsa20_quarter_round(state, 15, 12, 13, 14); +} + +pub fn salsa20_double_round(state: *[SALSA20_BLOCK_WORDS]u32) void { + salsa20_column_round(state); + salsa20_row_round(state); +} + +pub fn salsa20_hash_function(ctx: *Salsa20Ctx) void { + // Copy state to working_state. + @memcpy(&ctx.working_state, &ctx.state); + + // Perform all 20 rounds (10 row and 10 column rounds). + for (0..10) |_| { + salsa20_double_round(&ctx.working_state); + } + + // Add the working_state to the state. + for (0..SALSA20_BLOCK_WORDS) |i| { + ctx.state[i] +%= ctx.working_state[i]; + } +} + +pub fn salsa20_block_function(ctx: *Salsa20Ctx) void { + // Reset state. + { + ctx.state[0] = ctx.constants[0]; + ctx.state[5] = ctx.constants[1]; + ctx.state[10] = ctx.constants[2]; + ctx.state[15] = ctx.constants[3]; + + ctx.state[1] = ctx.key[0]; + ctx.state[2] = ctx.key[1]; + ctx.state[3] = ctx.key[2]; + ctx.state[4] = ctx.key[3]; + ctx.state[11] = ctx.key[4]; + ctx.state[12] = ctx.key[5]; + ctx.state[13] = ctx.key[6]; + ctx.state[14] = ctx.key[7]; + + ctx.state[6] = ctx.nonce[0]; + ctx.state[7] = ctx.nonce[1]; + ctx.state[8] = ctx.counter[0]; + ctx.state[9] = ctx.counter[1]; + } + + // Perform the hashing. + salsa20_hash_function(ctx); +} + +// ----------------------------------- LITTLE ENDIAN HELPERS ----------------------------------- // + +fn salsa20_serialize(L: comptime_int, words: *const [L]u32, bytes: *[L * 4]u8) void { + if (comptime @import("builtin").target.cpu.arch.endian() == .little) { + @memcpy(bytes, @as(*const [L * 4]u8, @ptrCast(words))); + } else { + var tmp: [4]u8 = undefined; + for (0..L) |i| { + tmp = word_to_bytes_le(words[i]); + bytes[i * 4] = tmp[0]; + bytes[i * 4 + 1] = tmp[1]; + bytes[i * 4 + 2] = tmp[2]; + bytes[i * 4 + 3] = tmp[3]; + } + } +} + +fn salsa20_deserialize(L: comptime_int, bytes: *const [L * 4]u8, words: *[L]u32) void { + if (comptime @import("builtin").target.cpu.arch.endian() == .little) { + @memcpy(@as(*[L * 4]u8, @ptrCast(words)), bytes); + } else { + for (0..L) |i| { + words[i] = bytes_to_word_le(@ptrCast(bytes[(i * 4)..(i * 4 + 4)])); + } + } +} + +fn bytes_to_word_le(bytes: *const [4]u8) u32 { + return (@as(u32, bytes[3]) << 24) | (@as(u32, bytes[2]) << 16) | (@as(u32, bytes[1]) << 8) | @as(u32, bytes[0]); +} + +fn word_to_bytes_le(word: u32) [4]u8 { + var bytes: [4]u8 = undefined; + bytes[3] = @truncate(word >> 24); + bytes[2] = @truncate(word >> 16); + bytes[1] = @truncate(word >> 8); + bytes[0] = @truncate(word); + return bytes; +} + +// ----------------------------------- GENERIC HELPERS ----------------------------------- // + +fn rol(word: u32, bits: comptime_int) u32 { + return word << bits | word >> (32 - bits); +} + +// ----------------------------------- TEST VECTORS ----------------------------------- // + +fn test_quarter_round_function(y0: u32, y1: u32, y2: u32, y3: u32, z0: u32, z1: u32, z2: u32, z3: u32) !void { + var dummy_state: [SALSA20_BLOCK_WORDS]u32 = undefined; + dummy_state[0] = y0; + dummy_state[1] = y1; + dummy_state[2] = y2; + dummy_state[3] = y3; + salsa20_quarter_round(&dummy_state, 0, 1, 2, 3); + try testing.expectEqualSlices(u32, &.{ z0, z1, z2, z3 }, dummy_state[0..4]); +} + +// https://cr.yp.to/snuffle/spec.pdf +test "Salsa20 quarterround function" { + try test_quarter_round_function(0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000); + try test_quarter_round_function(0x00000001, 0x00000000, 0x00000000, 0x00000000, 0x08008145, 0x00000080, 0x00010200, 0x20500000); + try test_quarter_round_function(0x00000000, 0x00000001, 0x00000000, 0x00000000, 0x88000100, 0x00000001, 0x00000200, 0x00402000); + try test_quarter_round_function(0x00000000, 0x00000000, 0x00000001, 0x00000000, 0x80040000, 0x00000000, 0x00000001, 0x00002000); + try test_quarter_round_function(0x00000000, 0x00000000, 0x00000000, 0x00000001, 0x00048044, 0x00000080, 0x00010000, 0x20100001); + try test_quarter_round_function(0xe7e8c006, 0xc4f9417d, 0x6479b4b2, 0x68c67137, 0xe876d72b, 0x9361dfd5, 0xf1460244, 0x948541a3); + try test_quarter_round_function(0xd3917c5b, 0x55f1c407, 0x52a58a7a, 0x8f887a3b, 0x3e2f308c, 0xd90a8f36, 0x6ab2a923, 0x2883524c); +} + +// https://cr.yp.to/snuffle/spec.pdf +test "Salsa20 rowround function" { + var y1 = [_]u32{ + 0x00000001, 0x00000000, 0x00000000, 0x00000000, + 0x00000001, 0x00000000, 0x00000000, 0x00000000, + 0x00000001, 0x00000000, 0x00000000, 0x00000000, + 0x00000001, 0x00000000, 0x00000000, 0x00000000, + }; + const z1 = [_]u32{ + 0x08008145, 0x00000080, 0x00010200, 0x20500000, + 0x20100001, 0x00048044, 0x00000080, 0x00010000, + 0x00000001, 0x00002000, 0x80040000, 0x00000000, + 0x00000001, 0x00000200, 0x00402000, 0x88000100, + }; + salsa20_row_round(&y1); + try testing.expectEqualSlices(u32, z1[0..], y1[0..]); + + var y2 = [_]u32{ + 0x08521bd6, 0x1fe88837, 0xbb2aa576, 0x3aa26365, + 0xc54c6a5b, 0x2fc74c2f, 0x6dd39cc3, 0xda0a64f6, + 0x90a2f23d, 0x067f95a6, 0x06b35f61, 0x41e4732e, + 0xe859c100, 0xea4d84b7, 0x0f619bff, 0xbc6e965a, + }; + const z2 = [_]u32{ + 0xa890d39d, 0x65d71596, 0xe9487daa, 0xc8ca6a86, + 0x949d2192, 0x764b7754, 0xe408d9b9, 0x7a41b4d1, + 0x3402e183, 0x3c3af432, 0x50669f96, 0xd89ef0a8, + 0x0040ede5, 0xb545fbce, 0xd257ed4f, 0x1818882d, + }; + salsa20_row_round(&y2); + try testing.expectEqualSlices(u32, z2[0..], y2[0..]); +} + +// https://cr.yp.to/snuffle/spec.pdf +test "Salsa20 columnround function" { + var x1 = [_]u32{ + 0x00000001, 0x00000000, 0x00000000, 0x00000000, + 0x00000001, 0x00000000, 0x00000000, 0x00000000, + 0x00000001, 0x00000000, 0x00000000, 0x00000000, + 0x00000001, 0x00000000, 0x00000000, 0x00000000, + }; + const y1 = [_]u32{ + 0x10090288, 0x00000000, 0x00000000, 0x00000000, + 0x00000101, 0x00000000, 0x00000000, 0x00000000, + 0x00020401, 0x00000000, 0x00000000, 0x00000000, + 0x40a04001, 0x00000000, 0x00000000, 0x00000000, + }; + salsa20_column_round(&x1); + try testing.expectEqualSlices(u32, y1[0..], x1[0..]); + + var x2 = [_]u32{ + 0x08521bd6, 0x1fe88837, 0xbb2aa576, 0x3aa26365, + 0xc54c6a5b, 0x2fc74c2f, 0x6dd39cc3, 0xda0a64f6, + 0x90a2f23d, 0x067f95a6, 0x06b35f61, 0x41e4732e, + 0xe859c100, 0xea4d84b7, 0x0f619bff, 0xbc6e965a, + }; + const y2 = [_]u32{ + 0x8c9d190a, 0xce8e4c90, 0x1ef8e9d3, 0x1326a71a, + 0x90a20123, 0xead3c4f3, 0x63a091a0, 0xf0708d69, + 0x789b010c, 0xd195a681, 0xeb7d5504, 0xa774135c, + 0x481c2027, 0x53a8e4b5, 0x4c1f89c5, 0x3f78c9c8, + }; + salsa20_column_round(&x2); + try testing.expectEqualSlices(u32, y2[0..], x2[0..]); +} + +// https://cr.yp.to/snuffle/spec.pdf +test "Salsa20 doubleround function" { + var x1 = [_]u32{ + 0x00000001, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + 0x00000000, 0x00000000, 0x00000000, 0x00000000, + }; + const z1 = [_]u32{ + 0x8186a22d, 0x0040a284, 0x82479210, 0x06929051, + 0x08000090, 0x02402200, 0x00004000, 0x00800000, + 0x00010200, 0x20400000, 0x08008104, 0x00000000, + 0x20500000, 0xa0000040, 0x0008180a, 0x612a8020, + }; + salsa20_double_round(&x1); + try testing.expectEqualSlices(u32, z1[0..], x1[0..]); + + var x2 = [_]u32{ + 0xde501066, 0x6f9eb8f7, 0xe4fbbd9b, 0x454e3f57, + 0xb75540d3, 0x43e93a4c, 0x3a6f2aa0, 0x726d6b36, + 0x9243f484, 0x9145d1e8, 0x4fa9d247, 0xdc8dee11, + 0x054bf545, 0x254dd653, 0xd9421b6d, 0x67b276c1, + }; + const z2 = [_]u32{ + 0xccaaf672, 0x23d960f7, 0x9153e63a, 0xcd9a60d0, + 0x50440492, 0xf07cad19, 0xae344aa0, 0xdf4cfdfc, + 0xca531c29, 0x8e7943db, 0xac1680cd, 0xd503ca00, + 0xa74b2ad6, 0xbc331c5c, 0x1dda24c7, 0xee928277, + }; + salsa20_double_round(&x2); + try testing.expectEqualSlices(u32, z2[0..], x2[0..]); +} + +fn test_hash_function(count: comptime_int, input: *const [SALSA20_BLOCK_SIZE]u8, reference: *const [SALSA20_BLOCK_SIZE]u8) !void { + var dummy_ctx = Salsa20Ctx{ + .key = undefined, + .nonce = undefined, + .counter = undefined, + .constants = undefined, + .state = undefined, + .working_state = undefined, + .keystream_idx = undefined, + }; + + salsa20_deserialize(SALSA20_BLOCK_WORDS, input, dummy_ctx.state[0..]); + for (0..count) |_| + salsa20_hash_function(&dummy_ctx); + + var serialized_result: [SALSA20_BLOCK_SIZE]u8 = undefined; + salsa20_serialize(SALSA20_BLOCK_WORDS, dummy_ctx.state[0..], serialized_result[0..]); + + try testing.expectEqualSlices(u8, reference, serialized_result[0..]); +} + +// https://cr.yp.to/snuffle/spec.pdf +test "Salsa20 hash function" { + try test_hash_function(1, &std.mem.zeroes([SALSA20_BLOCK_SIZE]u8), &std.mem.zeroes([SALSA20_BLOCK_SIZE]u8)); + try test_hash_function(1, &.{ + 211, 159, 13, 115, 76, 55, 82, 183, 3, 117, 222, 37, 191, 187, 234, 136, + 49, 237, 179, 48, 1, 106, 178, 219, 175, 199, 166, 48, 86, 16, 179, 207, + 31, 240, 32, 63, 15, 83, 93, 161, 116, 147, 48, 113, 238, 55, 204, 36, + 79, 201, 235, 79, 3, 81, 156, 47, 203, 26, 244, 243, 88, 118, 104, 54, + }, &.{ + 109, 42, 178, 168, 156, 240, 248, 238, 168, 196, 190, 203, 26, 110, 170, 154, + 29, 29, 150, 26, 150, 30, 235, 249, 190, 163, 251, 48, 69, 144, 51, 57, + 118, 40, 152, 157, 180, 57, 27, 94, 107, 42, 236, 35, 27, 111, 114, 114, + 219, 236, 232, 135, 111, 155, 110, 18, 24, 232, 95, 158, 179, 19, 48, 202, + }); + try test_hash_function(1, &.{ + 88, 118, 104, 54, 79, 201, 235, 79, 3, 81, 156, 47, 203, 26, 244, 243, + 191, 187, 234, 136, 211, 159, 13, 115, 76, 55, 82, 183, 3, 117, 222, 37, + 86, 16, 179, 207, 49, 237, 179, 48, 1, 106, 178, 219, 175, 199, 166, 48, + 238, 55, 204, 36, 31, 240, 32, 63, 15, 83, 93, 161, 116, 147, 48, 113, + }, &.{ + 179, 19, 48, 202, 219, 236, 232, 135, 111, 155, 110, 18, 24, 232, 95, 158, + 26, 110, 170, 154, 109, 42, 178, 168, 156, 240, 248, 238, 168, 196, 190, 203, + 69, 144, 51, 57, 29, 29, 150, 26, 150, 30, 235, 249, 190, 163, 251, 48, + 27, 111, 114, 114, 118, 40, 152, 157, 180, 57, 27, 94, 107, 42, 236, 35, + }); + try test_hash_function(1000000, &.{ + 6, 124, 83, 146, 38, 191, 9, 50, 4, 161, 47, 222, 122, 182, 223, 185, + 75, 27, 0, 216, 16, 122, 7, 89, 162, 104, 101, 147, 213, 21, 54, 95, + 225, 253, 139, 176, 105, 132, 23, 116, 76, 41, 176, 207, 221, 34, 157, 108, + 94, 94, 99, 52, 90, 117, 91, 220, 146, 190, 239, 143, 196, 176, 130, 186, + }, &.{ + 8, 18, 38, 199, 119, 76, 215, 67, 173, 127, 144, 162, 103, 212, 176, 217, + 192, 19, 233, 33, 159, 197, 154, 160, 128, 243, 219, 65, 171, 136, 135, 225, + 123, 11, 68, 86, 237, 82, 20, 155, 133, 189, 9, 83, 167, 116, 194, 78, + 122, 127, 195, 185, 185, 204, 188, 90, 245, 9, 183, 248, 226, 85, 245, 104, + }); +} + +// https://cr.yp.to/snuffle/spec.pdf +test "Salsa20 expansion function" { + const key1 = [SALSA20_KEY_SIZE]u8{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, + }; + const nonce1 = [SALSA20_NONCE_SIZE]u8{ 101, 102, 103, 104, 105, 106, 107, 108 }; + const pos1 = [SALSA20_NONCE_SIZE]u8{ 109, 110, 111, 112, 113, 114, 115, 116 }; + const reference1 = [SALSA20_BLOCK_SIZE]u8{ + 69, 37, 68, 39, 41, 15, 107, 193, 255, 139, 122, 6, 170, 233, 217, 98, + 89, 144, 182, 106, 21, 51, 200, 65, 239, 49, 222, 34, 215, 114, 40, 126, + 104, 197, 7, 225, 197, 153, 31, 2, 102, 78, 76, 176, 84, 245, 246, 184, + 177, 160, 133, 130, 6, 72, 149, 119, 192, 195, 132, 236, 234, 103, 246, 74, + }; + + var ctx1 = salsa20_new(&key1, &nonce1, &pos1, &SALSA20_256_CONSTANTS); + defer salsa20_destroy(&ctx1); + + var keystream_buffer: [SALSA20_BLOCK_SIZE]u8 = undefined; + salsa20_serialize(SALSA20_BLOCK_WORDS, ctx1.state[0..], keystream_buffer[0..]); + + try testing.expectEqualSlices(u8, reference1[0..], keystream_buffer[0..]); + + const key2 = [SALSA20_KEY_SIZE]u8{ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + }; + const reference2 = [SALSA20_BLOCK_SIZE]u8{ + 39, 173, 46, 248, 30, 200, 82, 17, 48, 67, 254, 239, 37, 18, 13, 247, + 241, 200, 61, 144, 10, 55, 50, 185, 6, 47, 246, 253, 143, 86, 187, 225, + 134, 85, 110, 246, 161, 163, 43, 235, 231, 94, 171, 51, 145, 214, 112, 29, + 14, 232, 5, 16, 151, 140, 183, 141, 171, 9, 122, 181, 104, 182, 177, 193, + }; + + var ctx2 = salsa20_new(&key2, &nonce1, &pos1, &SALSA20_128_CONSTANTS); + defer salsa20_destroy(&ctx2); + + salsa20_serialize(SALSA20_BLOCK_WORDS, ctx2.state[0..], keystream_buffer[0..]); + try testing.expectEqualSlices(u8, reference2[0..], keystream_buffer[0..]); +} + +test "Salsa20 encryption" { + const key = [SALSA20_KEY_SIZE]u8{ + 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 nonce = [SALSA20_NONCE_SIZE]u8{ + 0x00, 0x00, 0x00, 0x4a, 0x00, 0x00, 0x00, 0x00, + }; + const counter = word_to_bytes_le(0) ++ word_to_bytes_le(0); + const plaintext = "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."; + const reference = [_]u8{ + 0x13, 0xfc, 0x5e, 0x28, 0x51, 0xf0, 0x79, 0x9d, 0x97, 0x52, 0x1b, 0xa6, 0xa9, 0x37, 0x11, 0x87, + 0x00, 0x95, 0x7c, 0x2e, 0xf9, 0x96, 0xe4, 0x2c, 0x59, 0x84, 0x9d, 0x45, 0x0c, 0x6e, 0xa2, 0x2e, + 0x91, 0xcd, 0xcc, 0xea, 0x39, 0x1e, 0xe0, 0xa3, 0x57, 0xbb, 0x56, 0xcd, 0xf5, 0x2c, 0x56, 0xce, + 0x01, 0x38, 0x07, 0xa9, 0x45, 0xb9, 0x17, 0xee, 0x6c, 0xcb, 0x18, 0xce, 0xca, 0xbe, 0x4b, 0xf4, + 0x09, 0xba, 0x72, 0xfb, 0xdf, 0x90, 0xdb, 0x02, 0x8d, 0x22, 0x61, 0x7f, 0x1c, 0xa9, 0x84, 0x15, + 0x6c, 0xa2, 0x72, 0x47, 0x3a, 0xf4, 0xf0, 0xe4, 0xcb, 0x3d, 0x85, 0x8b, 0x7a, 0xb4, 0x67, 0xae, + 0x14, 0x71, 0x87, 0xab, 0xac, 0xb7, 0xc6, 0xe9, 0xaf, 0x6f, 0x2f, 0x47, 0x28, 0x7e, 0x2e, 0x0c, + 0xb3, 0x18, + }; + + var ctx = salsa20_new(&key, &nonce, &counter, &SALSA20_256_CONSTANTS); + defer salsa20_destroy(&ctx); + + var buffer: [plaintext.len]u8 = undefined; + @memcpy(&buffer, plaintext); + + try salsa20_encrypt_inplace(&ctx, &buffer); + try testing.expectEqualSlices(u8, reference[0..], buffer[0..]); +}