From 3afe9895c5e828826c5a98ca491a9e0f793f4fcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milan=20=C5=A0pinka?= Date: Sat, 1 Feb 2025 23:23:51 +0100 Subject: [PATCH] Finish the refactor. --- README.md | 3 +- build.zig | 33 +- src/error.zig | 7 - src/primitive/blockcipher/aes.zig | 2 +- src/primitive/blockcipher/mode_gcm.zig | 248 ++++------- src/primitive/streamcipher/chacha20.zig | 2 +- src/primitive/streamcipher/salsa20.zig | 415 ++++-------------- src/root.zig | 4 +- src/utility/byte_operations.zig | 21 + test/primitive/blockcipher/aes.zig | 2 +- .../primitive/blockcipher/operation_modes.zig | 66 ++- test/primitive/digest/sha.zig | 29 +- test/primitive/streamcipher/chacha20.zig | 4 +- test/primitive/streamcipher/salsa20.zig | 257 +++++++++++ 14 files changed, 521 insertions(+), 572 deletions(-) delete mode 100644 src/error.zig diff --git a/README.md b/README.md index 99c1acc..b6e4029 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ Most (**theoretical!**) users should directly use one of the cryptographic *prot ### Primitives - Advanced Encryption Standard (FIPS 197): AES-128, AES-192, AES-256 +- Block Cipher Operation Modes: CBC-PKCS7 - ChaCha20 (RFC 7539): ChaCha20 with 64-bit nonce and 64-bit counter, ChaCha20 with 96-bit nonce and 32-bit counter - Salsa20 (Bernstein, not standardized): Salsa20/20 with 256-key, Salsa20/20 with 128-bit key - Secure Hash Standard (FIPS 180-4): SHA-1, SHA-224, SHA-256, SHA-384, SHA-512, SHA-512/224, SHA-512/256 @@ -38,7 +39,7 @@ Most (**theoretical!**) users should directly use one of the cryptographic *prot ### Primitives - DES, 3DES -- Block cipher modes: CBC-PKCS7, CFB, OFB, CTR, GCM +- Block cipher modes: CFB, OFB, CTR, GCM - Poly1305 - SHA-512/t, SHA-3, HMAC - BigIntegers & modular arithmetic diff --git a/build.zig b/build.zig index 8802832..3266dcc 100644 --- a/build.zig +++ b/build.zig @@ -1,24 +1,16 @@ const std = @import("std"); -// Although this function looks imperative, note that its job is to -// declaratively construct a build graph that will be executed by an external -// runner. pub fn build(b: *std.Build) void { - // Standard target options allows the person running `zig build` to choose - // what target to build for. Here we do not override the defaults, which - // means any target is allowed, and the default is native. Other options - // for restricting supported target set are available. const target = b.standardTargetOptions(.{}); - - // Standard optimization options allow the person running `zig build` to select - // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not - // set a preferred release mode, allowing the user to decide how to optimize. const optimize = b.standardOptimizeOption(.{}); - // Define a private module for importing source files in tests. - const src_module = b.createModule(.{ - .root_source_file = .{ .cwd_relative = "src/root.zig" }, + const utility_module = b.createModule(.{ + .root_source_file = .{ .cwd_relative = "src/utility//index.zig" }, }); + const primitive_module = b.createModule(.{ + .root_source_file = .{ .cwd_relative = "src/primitive/index.zig" }, + }); + primitive_module.addImport("utility", utility_module); const lib = b.addStaticLibrary(.{ .name = "crypto", @@ -26,26 +18,21 @@ pub fn build(b: *std.Build) void { .target = target, .optimize = optimize, }); + lib.root_module.addImport("primitive", primitive_module); + lib.root_module.addImport("utility", utility_module); - // This declares intent for the library to be installed into the standard - // location when the user invokes the "install" step (the default step when - // running `zig build`). b.installArtifact(lib); - // Creates a step for unit testing. This only builds the test executable - // but does not run it. const lib_unit_tests = b.addTest(.{ .root_source_file = b.path("test/index.zig"), .target = target, .optimize = optimize, }); - lib_unit_tests.root_module.addImport("ziggy", src_module); + lib_unit_tests.root_module.addImport("primitive", primitive_module); + lib_unit_tests.root_module.addImport("utility", utility_module); const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); - // Similar to creating the run step earlier, this exposes a `test` step to - // the `zig build --help` menu, providing a way for the user to request - // running the unit tests. const test_step = b.step("test", "Run unit tests"); test_step.dependOn(&run_lib_unit_tests.step); } diff --git a/src/error.zig b/src/error.zig deleted file mode 100644 index 9132978..0000000 --- a/src/error.zig +++ /dev/null @@ -1,7 +0,0 @@ -pub const CryptoError = error{ - MessageLengthLimitExceeded, - BufferSizeMismatch, - InvalidIVLength, - InvalidTagLength, - IncorrectAuthenticationTag, -}; diff --git a/src/primitive/blockcipher/aes.zig b/src/primitive/blockcipher/aes.zig index dcc54f1..d312f70 100644 --- a/src/primitive/blockcipher/aes.zig +++ b/src/primitive/blockcipher/aes.zig @@ -1,7 +1,7 @@ const std = @import("std"); const testing = std.testing; -const byte_operations = @import("../../utility/byte_operations.zig"); +const byte_operations = @import("utility").byte_operations; const word_to_bytes = byte_operations.word_to_bytes_be; const bytes_to_word = byte_operations.bytes_to_word_be; diff --git a/src/primitive/blockcipher/mode_gcm.zig b/src/primitive/blockcipher/mode_gcm.zig index 069225d..3a6bd86 100644 --- a/src/primitive/blockcipher/mode_gcm.zig +++ b/src/primitive/blockcipher/mode_gcm.zig @@ -9,22 +9,22 @@ const CryptoError = error{ IncorrectAuthenticationTag, }; -pub const GCM_128_BLOCK_SIZE = 128 / 8; +pub const GCM128_BLOCK_SIZE = 128 / 8; -pub fn gcm_128_cipher_fn_t(comptime key_size: usize) type { +pub fn gcm128_cipher_fn_t(comptime key_size: usize) type { return fn ( key: *const [key_size]u8, - block_in: *const [GCM_128_BLOCK_SIZE]u8, - block_out: *[GCM_128_BLOCK_SIZE]u8, + block_in: *const [GCM128_BLOCK_SIZE]u8, + block_out: *[GCM128_BLOCK_SIZE]u8, ) void; } fn Gcm128Ctx( comptime cipher_key_size: usize, - cipher_encrypt_fn: gcm_128_cipher_fn_t(cipher_key_size), + cipher_encrypt_fn: gcm128_cipher_fn_t(cipher_key_size), ) type { return struct { - const BLOCK_SIZE = GCM_128_BLOCK_SIZE; + const BLOCK_SIZE = GCM128_BLOCK_SIZE; const AAD_MAX_BYTES = (1 << 64) / 8; const TEXT_MAX_BYTES = ((1 << 39) - 256) / 8; const TAG_MAX_BYTES = 128 / 8; @@ -37,21 +37,21 @@ fn Gcm128Ctx( counter: [BLOCK_SIZE]u8, ghash_x: [BLOCK_SIZE]u8, h: [BLOCK_SIZE]u8, + y0: [BLOCK_SIZE]u8, ciphertext_buffer: [BLOCK_SIZE]u8, ciphertext_length: u64, aad_length: u64, }; } -pub fn gcm_128_new( +pub fn gcm128_new( comptime cipher_key_size: usize, - cipher_encrypt_fn: gcm_128_cipher_fn_t(cipher_key_size), + cipher_encrypt_fn: gcm128_cipher_fn_t(cipher_key_size), key: *const [cipher_key_size]u8, iv: []const u8, ) !Gcm128Ctx(cipher_key_size, cipher_encrypt_fn) { - if (iv.len == 0) { + if (iv.len == 0) return CryptoError.InvalidIVLength; - } var ctx = Gcm128Ctx(cipher_key_size, cipher_encrypt_fn){ .key = undefined, @@ -59,6 +59,7 @@ pub fn gcm_128_new( .counter = undefined, .ghash_x = undefined, .h = undefined, + .y0 = undefined, .ciphertext_buffer = undefined, .ciphertext_length = 0, .aad_length = 0, @@ -68,7 +69,7 @@ pub fn gcm_128_new( @memcpy(&ctx.key, key); // Compute the `H` value. - cipher_encrypt_fn(&ctx.key, &std.mem.zeroes([GCM_128_BLOCK_SIZE]u8), &ctx.h); + cipher_encrypt_fn(&ctx.key, &std.mem.zeroes([GCM128_BLOCK_SIZE]u8), &ctx.h); // Set the counter initial value (`Y_0`). if (iv.len == 96 / 8) { @@ -76,15 +77,16 @@ pub fn gcm_128_new( @memset(ctx.counter[(96 / 8)..(ctx.counter.len - 1)], 0x00); ctx.counter[ctx.counter.len - 1] = 0x01; } else { - gcm_128_ghash(&ctx.h, &.{}, iv, &ctx.counter); + gcm128_ghash(&ctx.h, &.{}, iv, &ctx.counter); } + @memcpy(&ctx.y0, &ctx.counter); return ctx; } -pub fn gcm_128_destroy( +pub fn gcm128_destroy( comptime cipher_key_size: usize, - cipher_encrypt_fn: gcm_128_cipher_fn_t(cipher_key_size), + cipher_encrypt_fn: gcm128_cipher_fn_t(cipher_key_size), ctx: *Gcm128Ctx(cipher_key_size, cipher_encrypt_fn), ) void { @memset(&ctx, 0); @@ -93,9 +95,9 @@ pub fn gcm_128_destroy( // Important: this function must only be called at most once // AFTER the context is initialized but BEFORE any data is en/decrypted. // TODO: Enforce at API level. -pub fn gcm_128_authenticate_data_once( +pub fn gcm128_authenticate_data_once( comptime cipher_key_size: usize, - cipher_encrypt_fn: gcm_128_cipher_fn_t(cipher_key_size), + cipher_encrypt_fn: gcm128_cipher_fn_t(cipher_key_size), ctx: *Gcm128Ctx(cipher_key_size, cipher_encrypt_fn), data: []const u8, ) !void { @@ -104,15 +106,15 @@ pub fn gcm_128_authenticate_data_once( return CryptoError.MessageLengthLimitExceeded; // Compute and store `X_m` where m is the length in blocks of the AAD. - gcm_128_ghash_padded_chunk(&std.mem.zeroes([]u8), &ctx.h, data, &ctx.ghash_x); + gcm128_ghash_pad_chunk(&std.mem.zeroes([]u8), &ctx.h, data, &ctx.ghash_x); // Store the AAD length for the tag computation. ctx.aad_length = data.len; } -pub fn gcm_128_encrypt( +pub fn gcm128_encrypt( comptime cipher_key_size: usize, - cipher_encrypt_fn: gcm_128_cipher_fn_t(cipher_key_size), + cipher_encrypt_fn: gcm128_cipher_fn_t(cipher_key_size), ctx: *Gcm128Ctx(cipher_key_size, cipher_encrypt_fn), plaintext: []const u8, ciphertext: []u8, @@ -123,41 +125,25 @@ pub fn gcm_128_encrypt( if (ctx.ciphertext_length + plaintext.len >= @TypeOf(ctx.*).TEXT_MAX_BYTES) return CryptoError.MessageLengthLimitExceeded; - const BS = GCM_128_BLOCK_SIZE; - var bytes_processed: usize = 0; + const BS = GCM128_BLOCK_SIZE; + const KS = @TypeOf(ctx.*).KEY_SIZE; + const ENC = @TypeOf(ctx.*).CIPHER_FN; const bytes_buffered_ct = ctx.ciphertext_length % BS; + var bytes_processed: usize = 0; // If we're not at the beginning of a new block, use the existing keystream to encrypt the plaintext. if (bytes_buffered_ct != 0) { // Simplest case - no block border is reached - just XOR-encrypt and we're done. if (bytes_buffered_ct + plaintext.len < BS) { - gcm_128_crypt_xor( - @TypeOf(ctx.*).KEY_SIZE, - @TypeOf(ctx.*).CIPHER_FN, - ctx, - plaintext, - ciphertext, - false, - ); - return; + return gcm128_crypt_xor(KS, ENC, ctx, plaintext, ciphertext, false); } // Otherwise, encrypt the remainder of the block with the existing // keystream and then authenticate the completed ciphertext block. else { - gcm_128_crypt_xor( - @TypeOf(ctx.*).KEY_SIZE, - @TypeOf(ctx.*).CIPHER_FN, - ctx, - plaintext[0 .. BS - bytes_buffered_ct], - ciphertext[0 .. BS - bytes_buffered_ct], - false, - ); - gcm_128_authenticate_ciphertext_block( - @TypeOf(ctx.*).KEY_SIZE, - @TypeOf(ctx.*).CIPHER_FN, - ctx, - ); + const len = BS - bytes_buffered_ct; + gcm128_crypt_xor(KS, ENC, ctx, plaintext[0..len], ciphertext[0..len], false); + gcm128_authenticate_ciphertext_block(KS, ENC, ctx); bytes_processed += BS - bytes_buffered_ct; } } @@ -165,58 +151,34 @@ pub fn gcm_128_encrypt( // As long as we have another whole block worth of plaintext, encrypt it // and authenticate the CT. while ((plaintext.len - bytes_processed) / BS > 0) : (bytes_processed += BS) { - gcm_128_update_keystream( - @TypeOf(ctx.*).KEY_SIZE, - @TypeOf(ctx.*).CIPHER_FN, - ctx, - ); - gcm_128_crypt_xor( - @TypeOf(ctx.*).KEY_SIZE, - @TypeOf(ctx.*).CIPHER_FN, - ctx, - plaintext[bytes_processed .. bytes_processed + BS], - ciphertext[bytes_processed .. bytes_processed + BS], - false, - ); - gcm_128_authenticate_ciphertext_block( - @TypeOf(ctx.*).KEY_SIZE, - @TypeOf(ctx.*).CIPHER_FN, - ctx, - ); + const start = bytes_processed; + const end = bytes_processed + BS; + gcm128_update_keystream(KS, ENC, ctx); + gcm128_crypt_xor(KS, ENC, ctx, plaintext[start..end], ciphertext[start..end], false); + gcm128_authenticate_ciphertext_block(KS, ENC, ctx); } // Finally, if there's still plaintext left, update the keystream and encrypt the rest of it. if (bytes_processed < plaintext.len) { - gcm_128_update_keystream( - @TypeOf(ctx.*).KEY_SIZE, - @TypeOf(ctx.*).CIPHER_FN, - ctx, - ); - gcm_128_crypt_xor( - @TypeOf(ctx.*).KEY_SIZE, - @TypeOf(ctx.*).CIPHER_FN, - ctx, - plaintext[bytes_processed..], - ciphertext[bytes_processed..], - false, - ); + gcm128_update_keystream(KS, ENC, ctx); + gcm128_crypt_xor(KS, ENC, ctx, plaintext[bytes_processed..], ciphertext[bytes_processed..], false); bytes_processed += plaintext.len - bytes_processed; } } -fn gcm_128_crypt_xor( +fn gcm128_crypt_xor( comptime cipher_key_size: usize, - cipher_encrypt_fn: gcm_128_cipher_fn_t(cipher_key_size), + cipher_encrypt_fn: gcm128_cipher_fn_t(cipher_key_size), ctx: *Gcm128Ctx(cipher_key_size, cipher_encrypt_fn), input_slice: []const u8, output_slice: []u8, decrypt: bool, ) void { - const block_offset = ctx.ciphertext_length % GCM_128_BLOCK_SIZE; + const block_offset = ctx.ciphertext_length % GCM128_BLOCK_SIZE; if (input_slice.len != output_slice.len or - block_offset + input_slice.len > GCM_128_BLOCK_SIZE) - @panic("gcm_128_crypt_xor contract violated!"); + block_offset + input_slice.len > GCM128_BLOCK_SIZE) + @panic("gcm128_crypt_xor contract violated!"); const len = input_slice.len; for (0..len, block_offset..block_offset + len) |ct_i, ks_i| { @@ -230,111 +192,108 @@ fn gcm_128_crypt_xor( ctx.ciphertext_length += len; } -fn gcm_128_update_keystream( +fn gcm128_update_keystream( comptime cipher_key_size: usize, - cipher_encrypt_fn: gcm_128_cipher_fn_t(cipher_key_size), + cipher_encrypt_fn: gcm128_cipher_fn_t(cipher_key_size), ctx: *Gcm128Ctx(cipher_key_size, cipher_encrypt_fn), ) void { - gcm_128_incr(&ctx.counter); + gcm128_incr(&ctx.counter); cipher_encrypt_fn(&ctx.key, &ctx.counter, &ctx.keystream); } -fn gcm_128_authenticate_ciphertext_block( +fn gcm128_authenticate_ciphertext_block( comptime cipher_key_size: usize, - cipher_encrypt_fn: gcm_128_cipher_fn_t(cipher_key_size), + cipher_encrypt_fn: gcm128_cipher_fn_t(cipher_key_size), ctx: *Gcm128Ctx(cipher_key_size, cipher_encrypt_fn), ) void { - xor(GCM_128_BLOCK_SIZE, &ctx.ghash_x, &ctx.ciphertext_buffer, &ctx.ghash_x); - gcm_128_mult(&ctx.ghash_x, &ctx.h, &ctx.ghash_x); + xor(GCM128_BLOCK_SIZE, &ctx.ghash_x, &ctx.ciphertext_buffer, &ctx.ghash_x); + gcm128_mult(&ctx.ghash_x, &ctx.h, &ctx.ghash_x); } -pub fn gcm_128_encrypt_final( +pub fn gcm128_encrypt_final( comptime cipher_key_size: usize, - cipher_encrypt_fn: gcm_128_cipher_fn_t(cipher_key_size), + cipher_encrypt_fn: gcm128_cipher_fn_t(cipher_key_size), ctx: *Gcm128Ctx(cipher_key_size, cipher_encrypt_fn), tag: []u8, ) !void { if (tag.len > @TypeOf(ctx.*).TAG_MAX_BYTES) return CryptoError.InvalidTagLength; + const KS = @TypeOf(ctx.*).KEY_SIZE; + const ENC = @TypeOf(ctx.*).CIPHER_FN; + // The last ciphertext block is 0-padded and authenticated. - const ct_residue_bytes = ctx.ciphertext_length % GCM_128_BLOCK_SIZE; + const ct_residue_bytes = ctx.ciphertext_length % GCM128_BLOCK_SIZE; @memset(ctx.ciphertext_buffer[ct_residue_bytes..], 0); - gcm_128_authenticate_ciphertext_block( - @TypeOf(ctx.*).KEY_SIZE, - @TypeOf(ctx.*).CIPHER_FN, - ctx, - ); + gcm128_authenticate_ciphertext_block(KS, ENC, ctx); // Lastly, authenticate the lengths of the AAD and the ciphertext. std.mem.writeInt(u64, ctx.ciphertext_buffer[0..8], ctx.aad_length, .big); std.mem.writeInt(u64, ctx.ciphertext_buffer[8..], ctx.ciphertext_length, .big); - gcm_128_authenticate_ciphertext_block( - @TypeOf(ctx.*).KEY_SIZE, - @TypeOf(ctx.*).CIPHER_FN, - ctx, - ); + gcm128_authenticate_ciphertext_block(KS, ENC, ctx); + + // The authentication tag is the [tag.len] leftmost bytes of the final `X` XORed with `E(K,Y0)`. + cipher_encrypt_fn(&ctx.key, &ctx.y0, &ctx.keystream); + xor(GCM128_BLOCK_SIZE, &ctx.ghash_x, &ctx.keystream, &ctx.ghash_x); - // The authentication tag is the [tag.len] leftmost bytes of the final `X` XORed with `H`. - xor(GCM_128_BLOCK_SIZE, &ctx.ghash_x, &ctx.h, &ctx.ghash_x); @memcpy(tag[0..], ctx.ghash_x[0..tag.len]); } -pub fn gcm_128_incr(ctr: *[GCM_128_BLOCK_SIZE]u8) void { +pub fn gcm128_incr(ctr: *[GCM128_BLOCK_SIZE]u8) void { const val32 = std.mem.readInt(u32, ctr[ctr.len - 4 .. ctr.len], .big); std.mem.writeInt(u32, ctr[ctr.len - 4 .. ctr.len], val32 +% 1, .big); } -pub fn gcm_128_ghash_padded_chunk( - iv: *const [GCM_128_BLOCK_SIZE]u8, - h: *const [GCM_128_BLOCK_SIZE]u8, +pub fn gcm128_ghash_pad_chunk( + iv: *const [GCM128_BLOCK_SIZE]u8, + h: *const [GCM128_BLOCK_SIZE]u8, chunk: []const u8, - out: *[GCM_128_BLOCK_SIZE]u8, + out: *[GCM128_BLOCK_SIZE]u8, ) void { - const BS = GCM_128_BLOCK_SIZE; + const BS = GCM128_BLOCK_SIZE; const m = chunk.len / BS; // Note: This definition of `v` is different from the one in the standard, // it is the number of residue *bytes* in the last block, not bits. const v = chunk.len % BS; - var x_i: [GCM_128_BLOCK_SIZE]u8 = iv.*; + var x_i: [GCM128_BLOCK_SIZE]u8 = iv.*; var i: usize = 0; while (i < m) : (i += 1) { xor(BS, x_i[0..], @ptrCast(chunk[i * BS .. (i + 1) * BS]), &x_i); - gcm_128_mult(&x_i, h, &x_i); + gcm128_mult(&x_i, h, &x_i); } - var padded: [GCM_128_BLOCK_SIZE]u8 = undefined; + var padded: [GCM128_BLOCK_SIZE]u8 = undefined; @memcpy(padded[0..v], chunk[i * BS ..]); @memset(padded[v..], 0x00); xor(BS, x_i[0..], padded[0..], &x_i); - gcm_128_mult(&x_i, h, out); + gcm128_mult(&x_i, h, out); } -pub fn gcm_128_ghash( - h: *const [GCM_128_BLOCK_SIZE]u8, +pub fn gcm128_ghash( + h: *const [GCM128_BLOCK_SIZE]u8, aad_chunk: []const u8, ciphertext_chunk: []const u8, - out: *[GCM_128_BLOCK_SIZE]u8, + out: *[GCM128_BLOCK_SIZE]u8, ) void { - gcm_128_ghash_padded_chunk(&std.mem.zeroes([GCM_128_BLOCK_SIZE]u8), h, aad_chunk, out); - gcm_128_ghash_padded_chunk(out, h, ciphertext_chunk, out); + gcm128_ghash_pad_chunk(&std.mem.zeroes([GCM128_BLOCK_SIZE]u8), h, aad_chunk, out); + gcm128_ghash_pad_chunk(out, h, ciphertext_chunk, out); - const BS = GCM_128_BLOCK_SIZE; + const BS = GCM128_BLOCK_SIZE; var lengths: [BS]u8 = undefined; std.mem.writeInt(u64, lengths[0 .. BS / 2], aad_chunk.len * 8, .big); std.mem.writeInt(u64, lengths[BS / 2 ..], ciphertext_chunk.len * 8, .big); xor(BS, lengths[0..], out[0..], out); - gcm_128_mult(out, h, out); + gcm128_mult(out, h, out); } // TODO: Naive algorithm: implement optimized versions with table lookups. -pub fn gcm_128_mult( +pub fn gcm128_mult( a: *const [128 / 8]u8, b: *const [128 / 8]u8, out: *[128 / 8]u8, @@ -363,54 +322,3 @@ fn xor(L: comptime_int, a: *const [L]u8, b: *const [L]u8, out: *[L]u8) void { for (0..L) |i| out[i] = a[i] ^ b[i]; } - -inline fn bytes_from_be_int(Int: type, value: Int) [@sizeOf(Int)]u8 { - var res: [@sizeOf(Int)]u8 = undefined; - std.mem.writeInt(Int, &res, value, .big); - return res; -} - -const aes = @import("aes.zig"); - -// https://csrc.nist.rip/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf -test "AES-GCM Test Case 1" { - const K = bytes_from_be_int(u128, 0x00000000000000000000000000000000); - const P = [0]u8{}; - const IV = std.mem.zeroes([96 / 8]u8); - const H = bytes_from_be_int(u128, 0x66e94bd4ef8a2c3b884cfa59ca342b2e); - const Y0 = bytes_from_be_int(u128, 0x00000000000000000000000000000001); - // const E_K_Y0 = bytes_from_be_int(u128, 0x58e2fccefa7e3061367f1d57a4e7455a); - // const len_A_len_C = bytes_from_be_int(u128, 0x00000000000000000000000000000000); - // const GHASH_H_A_C = bytes_from_be_int(u128, 0x00000000000000000000000000000000); - const C = [0]u8{}; - const T = bytes_from_be_int(u128, 0x58e2fccefa7e3061367f1d57a4e7455a); - - var ctx = try gcm_128_new( - aes.Aes128Parameters.KEY_SIZE, - aes.aes_128_encrypt_block, - &K, - &IV, - ); - - try testing.expectEqualSlices(u8, &H, &ctx.h); - try testing.expectEqualSlices(u8, &Y0, &ctx.counter); - - var ciphertext_buffer = [0]u8{}; - try gcm_128_encrypt( - aes.Aes128Parameters.KEY_SIZE, - aes.aes_128_encrypt_block, - &ctx, - &P, - &ciphertext_buffer, - ); - try testing.expectEqualSlices(u8, &C, &ciphertext_buffer); - - var tag_buffer: @TypeOf(T) = undefined; - try gcm_128_encrypt_final( - aes.Aes128Parameters.KEY_SIZE, - aes.aes_128_encrypt_block, - &ctx, - &tag_buffer, - ); - try testing.expectEqualSlices(u8, &T, &tag_buffer); -} diff --git a/src/primitive/streamcipher/chacha20.zig b/src/primitive/streamcipher/chacha20.zig index 61eb80a..846c206 100644 --- a/src/primitive/streamcipher/chacha20.zig +++ b/src/primitive/streamcipher/chacha20.zig @@ -1,7 +1,7 @@ const std = @import("std"); const testing = std.testing; -const byte_operations = @import("TODO").utility.byte_operations; +const byte_operations = @import("utility").byte_operations; const word_to_bytes = byte_operations.word_to_bytes_le; const bytes_to_word = byte_operations.bytes_to_word_le; diff --git a/src/primitive/streamcipher/salsa20.zig b/src/primitive/streamcipher/salsa20.zig index 22fcebb..eac4576 100644 --- a/src/primitive/streamcipher/salsa20.zig +++ b/src/primitive/streamcipher/salsa20.zig @@ -1,58 +1,62 @@ const std = @import("std"); const testing = std.testing; -// ----------------------------------- ERROR DEFINITIONS ----------------------------------- // +const byte_operations = @import("utility").byte_operations; +const bytes_to_word = byte_operations.bytes_to_word_le; +const word_to_bytes = byte_operations.word_to_bytes_le; -const KeyStreamDepleted = error.KeyStreamDepleted; +// ----------------------------------- ERROR DEFINITIONS ----------------------------------- // -// ----------------------------------- ChaCha20 CONSTANTS ----------------------------------- // +pub const KeyStreamDepleted = error.KeyStreamDepleted; -const SALSA20_BLOCK_SIZE = 512 / 8; -const SALSA20_BLOCK_WORDS = SALSA20_BLOCK_SIZE / 4; +// ----------------------------------- ChaCha20 CONSTANTS ----------------------------------- // -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; +pub const BLOCK_SIZE = 512 / 8; +pub const BLOCK_WORDS = BLOCK_SIZE / 4; -const SALSA20_NONCE_SIZE = 64 / 8; -const SALSA20_NONCE_WORDS = SALSA20_NONCE_SIZE / 4; +pub const KEY_SIZE = 256 / 8; +pub const KEY_WORDS = KEY_SIZE / 4; +pub const KEY_SIZE_128 = 128 / 8; +pub const KEY_WORDS_128 = KEY_SIZE_128 / 4; -const SALSA20_COUNTER_SIZE = 64 / 8; -const SALSA20_COUNTER_WORDS = SALSA20_COUNTER_SIZE / 4; +pub const NONCE_SIZE = 64 / 8; +pub const NONCE_WORDS = NONCE_SIZE / 4; -const SALSA20_CONSTANT_WORDS = 128 / 8 / 4; +pub const COUNTER_SIZE = 64 / 8; +pub const COUNTER_WORDS = COUNTER_SIZE / 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"), +pub const CONSTANT_WORDS = 128 / 8 / 4; + +pub const CONSTANTS_256 = [CONSTANT_WORDS]u32{ + bytes_to_word("expa"), + bytes_to_word("nd 3"), + bytes_to_word("2-by"), + bytes_to_word("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"), +pub const CONSTANTS_128 = [CONSTANT_WORDS]u32{ + bytes_to_word("expa"), + bytes_to_word("nd 1"), + bytes_to_word("6-by"), + bytes_to_word("te k"), }; -// ----------------------------------- CONTEXT MANAGEMENT ----------------------------------- // +// ----------------------------------- 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, + key: [KEY_WORDS]u32, + nonce: [NONCE_WORDS]u32, + counter: [COUNTER_WORDS]u32, + constants: [CONSTANT_WORDS]u32, + state: [BLOCK_WORDS]u32, + working_state: [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, + key: *const [KEY_SIZE]u8, + nonce: *const [NONCE_SIZE]u8, + counter: *const [COUNTER_SIZE]u8, + constants: *const [CONSTANT_WORDS]u32, ) Salsa20Ctx { var ctx = Salsa20Ctx{ .key = undefined, @@ -64,12 +68,12 @@ pub fn salsa20_new( .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); + deserialize(KEY_WORDS, key, &ctx.key); + deserialize(COUNTER_WORDS, counter, &ctx.counter); + deserialize(NONCE_WORDS, nonce, &ctx.nonce); @memcpy(&ctx.constants, constants); - salsa20_block_function(&ctx); + block_function(&ctx); ctx.keystream_idx = 0; return ctx; @@ -83,11 +87,11 @@ pub fn salsa20_destroy(ctx: *Salsa20Ctx) void { ctx.keystream_idx = 0; } -// ----------------------------------- ENCRYPTION/DECRYPTION ----------------------------------- // +// ----------------------------------- ENCRYPTION/DECRYPTION ----------------------------------- // -pub fn salsa20_encrypt_inplace(ctx: *Salsa20Ctx, plaintext: []u8) !void { +pub fn encrypt_inplace(ctx: *Salsa20Ctx, plaintext: []u8) !void { for (0..plaintext.len) |i| { - try salsa20_ensure_keystream_expanded(ctx); + try ensure_keystream_expanded(ctx); const keystream: [*]const u8 = @ptrCast(&ctx.state); plaintext[i] ^= keystream[ctx.keystream_idx]; @@ -95,20 +99,20 @@ pub fn salsa20_encrypt_inplace(ctx: *Salsa20Ctx, plaintext: []u8) !void { } } -pub fn salsa20_decrypt_inplace(ctx: *Salsa20Ctx, ciphertext: []u8) !void { - return salsa20_encrypt_inplace(ctx, ciphertext); +pub fn decrypt_inplace(ctx: *Salsa20Ctx, ciphertext: []u8) !void { + return 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); +pub fn ensure_keystream_expanded(ctx: *Salsa20Ctx) !void { + if (ctx.keystream_idx == BLOCK_SIZE) { + try increment_counter(ctx); + block_function(ctx); ctx.keystream_idx = 0; } } -fn salsa20_increment_counter(ctx: *Salsa20Ctx) !void { - for (0..SALSA20_COUNTER_WORDS) |idx| { +pub fn increment_counter(ctx: *Salsa20Ctx) !void { + for (0..COUNTER_WORDS) |idx| { const ov = @addWithOverflow(ctx.counter[idx], 1); ctx.counter[idx] = ov[0]; @@ -118,9 +122,9 @@ fn salsa20_increment_counter(ctx: *Salsa20Ctx) !void { return KeyStreamDepleted; } -// ----------------------------------- KEYSTREAM EXPANSION ----------------------------------- // +// ----------------------------------- KEYSTREAM EXPANSION ----------------------------------- // -pub fn salsa20_quarter_round(state: *[SALSA20_BLOCK_WORDS]u32, ia: u8, ib: u8, ic: u8, id: u8) void { +pub fn quarter_round(state: *[BLOCK_WORDS]u32, ia: u8, ib: u8, ic: u8, id: u8) void { var a = state[ia]; var b = state[ib]; var c = state[ic]; @@ -137,41 +141,41 @@ pub fn salsa20_quarter_round(state: *[SALSA20_BLOCK_WORDS]u32, ia: u8, ib: u8, i 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 column_round(state: *[BLOCK_WORDS]u32) void { + quarter_round(state, 0, 4, 8, 12); + quarter_round(state, 5, 9, 13, 1); + quarter_round(state, 10, 14, 2, 6); + 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 row_round(state: *[BLOCK_WORDS]u32) void { + quarter_round(state, 0, 1, 2, 3); + quarter_round(state, 5, 6, 7, 4); + quarter_round(state, 10, 11, 8, 9); + 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 double_round(state: *[BLOCK_WORDS]u32) void { + column_round(state); + row_round(state); } -pub fn salsa20_hash_function(ctx: *Salsa20Ctx) void { +pub fn 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); + double_round(&ctx.working_state); } // Add the working_state to the state. - for (0..SALSA20_BLOCK_WORDS) |i| { + for (0..BLOCK_WORDS) |i| { ctx.state[i] +%= ctx.working_state[i]; } } -pub fn salsa20_block_function(ctx: *Salsa20Ctx) void { +pub fn block_function(ctx: *Salsa20Ctx) void { // Reset state. { ctx.state[0] = ctx.constants[0]; @@ -195,286 +199,21 @@ pub fn salsa20_block_function(ctx: *Salsa20Ctx) void { } // Perform the hashing. - salsa20_hash_function(ctx); + hash_function(ctx); } -// ----------------------------------- LITTLE ENDIAN HELPERS ----------------------------------- // +// ----------------------------------- HELPERS ----------------------------------- // -fn salsa20_serialize(L: comptime_int, words: *const [L]u32, bytes: *[L * 4]u8) void { +pub fn serialize(L: comptime_int, words: *const [L]u32, bytes: *[L * 4]u8) void { for (0..L) |i| std.mem.writeInt(u32, @ptrCast(bytes[(i * 4)..(i * 4 + 4)]), words[i], .little); } -fn salsa20_deserialize(L: comptime_int, bytes: *const [L * 4]u8, words: *[L]u32) void { +pub fn deserialize(L: comptime_int, bytes: *const [L * 4]u8, words: *[L]u32) void { for (0..L) |i| words[i] = std.mem.readInt(u32, @ptrCast(bytes[(i * 4)..(i * 4 + 4)]), .little); } -fn bytes_to_word_le(bytes: *const [4]u8) u32 { - return std.mem.readInt(u32, bytes, .little); -} - -fn word_to_bytes_le(word: u32) [4]u8 { - var bytes: [4]u8 = undefined; - std.mem.writeInt(u32, &bytes, word, .little); - 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..]); -} diff --git a/src/root.zig b/src/root.zig index a546842..1141cb1 100644 --- a/src/root.zig +++ b/src/root.zig @@ -1,2 +1,2 @@ -pub const primitive = @import("./primitive/index.zig"); -pub const utility = @import("./utility/index.zig"); +pub const primitive = @import("primitive"); +pub const utility = @import("utility"); diff --git a/src/utility/byte_operations.zig b/src/utility/byte_operations.zig index 7b2c24d..976d959 100644 --- a/src/utility/byte_operations.zig +++ b/src/utility/byte_operations.zig @@ -1,5 +1,26 @@ const std = @import("std"); +fn hex_nibble_to_int(ascii_hex: u8) u4 { + const x = ascii_hex; + return @intCast(if (x >= '0' and x <= '9') + x - '0' + else if (x >= 'a' and x <= 'f') + 10 + (x - 'a') + else if (x >= 'A' and x <= 'F') + 10 + (x - 'A') + else + @panic("Argument is not a valid hex digit!")); +} + +pub fn hex_to_bytes(L: comptime_int, hex_string: *const [2 * L]u8) [L]u8 { + var res: [L]u8 = undefined; + for (0..L) |i| { + res[i] = @as(u8, hex_nibble_to_int(hex_string[2 * i])) << 4; + res[i] |= hex_nibble_to_int(hex_string[2 * i + 1]); + } + return res; +} + pub fn word_to_bytes_le(word: u32) [4]u8 { var bytes: [4]u8 = undefined; std.mem.writeInt(u32, &bytes, word, .little); diff --git a/test/primitive/blockcipher/aes.zig b/test/primitive/blockcipher/aes.zig index 540b256..f61e871 100644 --- a/test/primitive/blockcipher/aes.zig +++ b/test/primitive/blockcipher/aes.zig @@ -1,7 +1,7 @@ const std = @import("std"); const testing = std.testing; -const aes = @import("ziggy").primitive.blockcipher.aes; +const aes = @import("primitive").blockcipher.aes; test "AES-128 key expansion (FIPS 197)" { const key = [aes.KEY_SIZE_128]u8{ diff --git a/test/primitive/blockcipher/operation_modes.zig b/test/primitive/blockcipher/operation_modes.zig index b5ee2d1..9689e26 100644 --- a/test/primitive/blockcipher/operation_modes.zig +++ b/test/primitive/blockcipher/operation_modes.zig @@ -1,12 +1,15 @@ const std = @import("std"); const testing = std.testing; +const blockcipher = @import("primitive").blockcipher; +const hex_to_bytes = @import("utility").byte_operations.hex_to_bytes; + // Use AES to test all of the operation modes. -const aes = @import("ziggy").primitive.blockcipher.aes; +const aes = blockcipher.aes; // ----------------------------------- CBC MODE ----------------------------------- // -const cbc = @import("ziggy").primitive.blockcipher.operation_mode.cbc; +const cbc = blockcipher.operation_mode.cbc; test "AES-128-CBC basic test" { const BS = aes.BLOCK_SIZE; @@ -76,3 +79,62 @@ test "AES-128-CBC basic test" { try testing.expectEqualSlices(u8, plaintext[0..], pt_buffer[0..plaintext.len]); } } + +// ----------------------------------- GCM MODE ----------------------------------- // + +const gcm = @import("primitive").blockcipher.operation_mode.gcm; + +// https://csrc.nist.rip/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-spec.pdf +test "AES-GCM Test Case 1" { + const K = hex_to_bytes(128 / 8, "00000000000000000000000000000000"); + const P = hex_to_bytes(0, ""); + const IV = hex_to_bytes(96 / 8, "000000000000000000000000"); + + const H = hex_to_bytes(128 / 8, "66e94bd4ef8a2c3b884cfa59ca342b2e"); + const Y0 = hex_to_bytes(128 / 8, "00000000000000000000000000000001"); + // E_K_Y0 = 58e2fccefa7e3061367f1d57a4e7455a + // len_A_len_C = 00000000000000000000000000000000 + // GHASH_H_A_C = 00000000000000000000000000000000 + const C = hex_to_bytes(0, ""); + const T = hex_to_bytes(128 / 8, "58e2fccefa7e3061367f1d57a4e7455a"); + + var ctx = try gcm.gcm128_new(aes.KEY_SIZE_128, aes.aes128_encrypt_block, &K, &IV); + try testing.expectEqualSlices(u8, &H, &ctx.h); + try testing.expectEqualSlices(u8, &Y0, &ctx.counter); + + var ciphertext_buffer: @TypeOf(P) = undefined; + try gcm.gcm128_encrypt(aes.KEY_SIZE_128, aes.aes128_encrypt_block, &ctx, &P, &ciphertext_buffer); + try testing.expectEqualSlices(u8, &C, &ciphertext_buffer); + + var tag_buffer: @TypeOf(T) = undefined; + try gcm.gcm128_encrypt_final(aes.KEY_SIZE_128, aes.aes128_encrypt_block, &ctx, &tag_buffer); + try testing.expectEqualSlices(u8, &T, &tag_buffer); +} + +test "AES-GCM Test Case 2" { + const K = hex_to_bytes(128 / 8, "00000000000000000000000000000000"); + const P = hex_to_bytes(128 / 8, "00000000000000000000000000000000"); + const IV = hex_to_bytes(96 / 8, "000000000000000000000000"); + const H = hex_to_bytes(128 / 8, "66e94bd4ef8a2c3b884cfa59ca342b2e"); + const Y0 = hex_to_bytes(128 / 8, "00000000000000000000000000000001"); + // E_K_Y0 = 58e2fccefa7e3061367f1d57a4e7455a + // Y1 = 00000000000000000000000000000002 + // E_K_Y1 = 0388dace60b6a392f328c2b971b2fe78 + // X1 = 5e2ec746917062882c85b0685353deb7 + // len_A_len_C = 00000000000000000000000000000080 + // GHASH_H_A_C = f38cbb1ad69223dcc3457ae5b6b0f885 + const C = hex_to_bytes(128 / 8, "0388dace60b6a392f328c2b971b2fe78"); + const T = hex_to_bytes(128 / 8, "ab6e47d42cec13bdf53a67b21257bddf"); + + var ctx = try gcm.gcm128_new(aes.KEY_SIZE_128, aes.aes128_encrypt_block, &K, &IV); + try testing.expectEqualSlices(u8, &H, &ctx.h); + try testing.expectEqualSlices(u8, &Y0, &ctx.counter); + + var ciphertext_buffer: @TypeOf(P) = undefined; + try gcm.gcm128_encrypt(aes.KEY_SIZE_128, aes.aes128_encrypt_block, &ctx, &P, &ciphertext_buffer); + try testing.expectEqualSlices(u8, &C, &ciphertext_buffer); + + var tag_buffer: @TypeOf(T) = undefined; + try gcm.gcm128_encrypt_final(aes.KEY_SIZE_128, aes.aes128_encrypt_block, &ctx, &tag_buffer); + try testing.expectEqualSlices(u8, &T, &tag_buffer); +} diff --git a/test/primitive/digest/sha.zig b/test/primitive/digest/sha.zig index b7b8bf0..f3cafd1 100644 --- a/test/primitive/digest/sha.zig +++ b/test/primitive/digest/sha.zig @@ -1,8 +1,10 @@ const std = @import("std"); -const testing = @import("std").testing; +const testing = std.testing; -const sha1 = @import("ziggy").primitive.digest.sha1; -const sha2 = @import("ziggy").primitive.digest.sha2; +const sha1 = @import("primitive").digest.sha1; +const sha2 = @import("primitive").digest.sha2; + +const hex_to_bytes = @import("utility").byte_operations.hex_to_bytes; // ----------------------------------- SHA-1 ----------------------------------- // @@ -285,27 +287,6 @@ test "SHA-384 basic test" { // ----------------------------------- TEST HELPERS ----------------------------------- // -fn hex_nibble_to_int(ascii_hex: u8) u4 { - const x = ascii_hex; - return @intCast(if (x >= '0' and x <= '9') - x - '0' - else if (x >= 'a' and x <= 'f') - 10 + (x - 'a') - else if (x >= 'A' and x <= 'F') - 10 + (x - 'A') - else - @panic("Argument is not a valid hex digit!")); -} - -fn hex_to_bytes(L: comptime_int, hex_string: *const [2 * L]u8) [L]u8 { - var res: [L]u8 = undefined; - for (0..L) |i| { - res[i] = @as(u8, hex_nibble_to_int(hex_string[2 * i])) << 4; - res[i] |= hex_nibble_to_int(hex_string[2 * i + 1]); - } - return res; -} - fn run_hash_precomputed_tests( Ctx: type, L: comptime_int, diff --git a/test/primitive/streamcipher/chacha20.zig b/test/primitive/streamcipher/chacha20.zig index 8e59206..5158fd9 100644 --- a/test/primitive/streamcipher/chacha20.zig +++ b/test/primitive/streamcipher/chacha20.zig @@ -1,11 +1,11 @@ const std = @import("std"); const testing = std.testing; -const byte_operations = @import("ziggy").utility.byte_operations; +const byte_operations = @import("utility").byte_operations; const word_to_bytes = byte_operations.word_to_bytes_le; const bytes_to_word = byte_operations.bytes_to_word_le; -const chacha20 = @import("ziggy").primitive.streamcipher.chacha20; +const chacha20 = @import("primitive").streamcipher.chacha20; // ----------------------------------- TEST VECTORS ----------------------------------- // diff --git a/test/primitive/streamcipher/salsa20.zig b/test/primitive/streamcipher/salsa20.zig index e69de29..7d84bc4 100644 --- a/test/primitive/streamcipher/salsa20.zig +++ b/test/primitive/streamcipher/salsa20.zig @@ -0,0 +1,257 @@ +const std = @import("std"); +const testing = std.testing; + +const salsa20 = @import("primitive").streamcipher.salsa20; + +// 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..]); +} + +// 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.salsa20_new(&key1, &nonce1, &pos1, &salsa20.CONSTANTS_256); + defer salsa20.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.salsa20_new(&key2, &nonce1, &pos1, &salsa20.CONSTANTS_128); + defer salsa20.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 = std.mem.zeroes([salsa20.COUNTER_SIZE]u8); + 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.salsa20_new(&key, &nonce, &counter, &salsa20.CONSTANTS_256); + defer salsa20.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..]); +} + +// ----------------------------------- TEST HELPERS ----------------------------------- // + +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]); +} + +fn test_hash_function(count: comptime_int, input: *const [salsa20.BLOCK_SIZE]u8, reference: *const [salsa20.BLOCK_SIZE]u8) !void { + var dummy_ctx = salsa20.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..]); +}