authorgravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-11-21 19:41:00-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-11-21 19:43:01-08:00
logbb3b5d09ccc994cf4349618928a8390e2fafcd23
tree82169e6060a329c504fdbba57e7f1a7bacce1d2e
parenta892e09435452f1a0305e976deaf72b5bb53fe27

Revert std.crypto kangarootwelve addition

I would like a chance to review this before it lands, please. Feel free to submit the work again without changes and I will make review comments. In the meantime, these reverts avoid intermittent CI failures, and remove bad patterns from occurring in the standard library that other users might copy. Revert "std.crypto: improve KT documentation, use key_length for B3 key length (#25807)" This reverts commit 4b593a6c24797484e68a668818736b0f6a8d81a2. Revert "crypto - threaded K12: separate context computation from thread spawning (#25793)" This reverts commit ee4df4ad3edad160fb737a1935cd86bc2f9cfbbe. Revert "crypto.kt128: when using incremental hashing, use SIMD when possible (#25783)" This reverts commit bf9082518c32ce7d53d011777bf8d8056472cbf9. Revert "Add std.crypto.hash.sha3.{KT128,KT256} - RFC 9861. (#25593)" This reverts commit 95c76b1b4aa7302966281c6b9b7f6cadea3cf7a6.

4 files changed, 9 insertions(+), 1985 deletions(-)

lib/std/crypto/benchmark.zig-2
......@@ -30,7 +30,6 @@ const hashes = [_]Crypto{
3030 Crypto{ .ty = crypto.hash.sha3.Shake256, .name = "shake-256" },
3131 Crypto{ .ty = crypto.hash.sha3.TurboShake128(null), .name = "turboshake-128" },
3232 Crypto{ .ty = crypto.hash.sha3.TurboShake256(null), .name = "turboshake-256" },
33 Crypto{ .ty = crypto.hash.sha3.KT128, .name = "kt128" },
3433 Crypto{ .ty = crypto.hash.blake2.Blake2s256, .name = "blake2s" },
3534 Crypto{ .ty = crypto.hash.blake2.Blake2b512, .name = "blake2b" },
3635 Crypto{ .ty = crypto.hash.Blake3, .name = "blake3" },
......@@ -38,7 +37,6 @@ const hashes = [_]Crypto{
3837
3938const parallel_hashes = [_]Crypto{
4039 Crypto{ .ty = crypto.hash.Blake3, .name = "blake3-parallel" },
41 Crypto{ .ty = crypto.hash.sha3.KT128, .name = "kt128-parallel" },
4240};
4341
4442const block_size: usize = 8 * 8192;
lib/std/crypto/blake3.zig+9-7
......@@ -12,8 +12,8 @@ const Vec16 = @Vector(16, u32);
1212const chunk_length = 1024;
1313const max_depth = 54;
1414
15const simd_degree = std.simd.suggestVectorLength(u32) orelse 1;
16const max_simd_degree = simd_degree;
15pub const simd_degree = std.simd.suggestVectorLength(u32) orelse 1;
16pub const max_simd_degree = simd_degree;
1717const max_simd_degree_or_2 = if (max_simd_degree > 2) max_simd_degree else 2;
1818
1919/// Threshold for switching to parallel processing.
......@@ -502,7 +502,9 @@ fn hashManySimd(
502502 var out_ptr = out.ptr;
503503 var cnt = counter;
504504
505 if (simd_degree >= 16) {
505 const simd_deg = comptime simd_degree;
506
507 if (comptime simd_deg >= 16) {
506508 while (remaining >= 16) {
507509 const sixteen_inputs = [16][*]const u8{
508510 inp[0], inp[1], inp[2], inp[3],
......@@ -523,7 +525,7 @@ fn hashManySimd(
523525 }
524526 }
525527
526 if (simd_degree >= 8) {
528 if (comptime simd_deg >= 8) {
527529 while (remaining >= 8) {
528530 const eight_inputs = [8][*]const u8{
529531 inp[0], inp[1], inp[2], inp[3],
......@@ -542,7 +544,7 @@ fn hashManySimd(
542544 }
543545 }
544546
545 if (simd_degree >= 4) {
547 if (comptime simd_deg >= 4) {
546548 while (remaining >= 4) {
547549 const four_inputs = [4][*]const u8{
548550 inp[0],
......@@ -569,7 +571,7 @@ fn hashManySimd(
569571}
570572
571573fn hashMany(inputs: [][*]const u8, num_inputs: usize, blocks: usize, key: [8]u32, counter: u64, increment_counter: bool, flags: Flags, flags_start: Flags, flags_end: Flags, out: []u8) void {
572 if (max_simd_degree >= 4) {
574 if (comptime max_simd_degree >= 4) {
573575 hashManySimd(inputs, num_inputs, blocks, key, counter, increment_counter, flags, flags_start, flags_end, out);
574576 } else {
575577 hashManyPortable(inputs, num_inputs, blocks, key, counter, increment_counter, flags, flags_start, flags_end, out);
......@@ -907,7 +909,7 @@ pub const Blake3 = struct {
907909 pub const digest_length = 32;
908910 pub const key_length = 32;
909911
910 pub const Options = struct { key: ?[key_length]u8 = null };
912 pub const Options = struct { key: ?[digest_length]u8 = null };
911913 pub const KdfOptions = struct {};
912914
913915 key: [8]u32,
lib/std/crypto/kangarootwelve.zig deleted-1967
......@@ -1,1967 +0,0 @@
1const std = @import("std");
2const builtin = @import("builtin");
3const crypto = std.crypto;
4const Allocator = std.mem.Allocator;
5const Io = std.Io;
6const Thread = std.Thread;
7
8const TurboSHAKE128State = crypto.hash.sha3.TurboShake128(0x06);
9const TurboSHAKE256State = crypto.hash.sha3.TurboShake256(0x06);
10
11const chunk_size: usize = 8192; // Chunk size for tree hashing (8 KiB)
12const cache_line_size = std.atomic.cache_line;
13
14// Optimal SIMD vector length for u64 on this target platform
15const optimal_vector_len = std.simd.suggestVectorLength(u64) orelse 1;
16
17// Multi-threading threshold: inputs larger than this will use parallel processing.
18// Benchmarked optimal value for ReleaseFast mode.
19const large_file_threshold: usize = 2 * 1024 * 1024; // 2 MB
20
21// Round constants for Keccak-p[1600,12]
22const RC = [12]u64{
23 0x000000008000808B,
24 0x800000000000008B,
25 0x8000000000008089,
26 0x8000000000008003,
27 0x8000000000008002,
28 0x8000000000000080,
29 0x000000000000800A,
30 0x800000008000000A,
31 0x8000000080008081,
32 0x8000000000008080,
33 0x0000000080000001,
34 0x8000000080008008,
35};
36
37/// Generic KangarooTwelve variant builder.
38/// Creates a variant type with specific cryptographic parameters.
39fn KangarooVariant(
40 comptime security_level_bits: comptime_int,
41 comptime rate_bytes: usize,
42 comptime cv_size_bytes: usize,
43 comptime StateTypeParam: type,
44 comptime sep_x: usize,
45 comptime sep_y: usize,
46 comptime pad_x: usize,
47 comptime pad_y: usize,
48 comptime toBufferFn: fn (*const MultiSliceView, u8, []u8) void,
49 comptime allocFn: fn (Allocator, *const MultiSliceView, u8, usize) anyerror![]u8,
50) type {
51 return struct {
52 const security_level = security_level_bits;
53 const rate = rate_bytes;
54 const rate_in_lanes = rate_bytes / 8;
55 const cv_size = cv_size_bytes;
56 const StateType = StateTypeParam;
57 const separation_byte_pos = .{ .x = sep_x, .y = sep_y };
58 const padding_pos = .{ .x = pad_x, .y = pad_y };
59
60 inline fn turboSHAKEToBuffer(view: *const MultiSliceView, separation_byte: u8, output: []u8) void {
61 toBufferFn(view, separation_byte, output);
62 }
63
64 inline fn turboSHAKEMultiSliceAlloc(
65 allocator: Allocator,
66 view: *const MultiSliceView,
67 separation_byte: u8,
68 output_len: usize,
69 ) ![]u8 {
70 return allocFn(allocator, view, separation_byte, output_len);
71 }
72 };
73}
74
75/// KangarooTwelve with 128-bit security parameters
76const KT128Variant = KangarooVariant(
77 128, // Security level in bits
78 168, // TurboSHAKE128 rate in bytes
79 32, // Chaining value size in bytes
80 TurboSHAKE128State,
81 1, // separation_byte_pos.x (lane 11: 88 bytes into 168-byte rate)
82 3, // separation_byte_pos.y
83 0, // padding_pos.x (lane 20: last lane of 168-byte rate)
84 4, // padding_pos.y
85 turboSHAKE128MultiSliceToBuffer,
86 turboSHAKE128MultiSlice,
87);
88
89/// KangarooTwelve with 256-bit security parameters
90const KT256Variant = KangarooVariant(
91 256, // Security level in bits
92 136, // TurboSHAKE256 rate in bytes
93 64, // Chaining value size in bytes
94 TurboSHAKE256State,
95 4, // separation_byte_pos.x (lane 4: 32 bytes into 136-byte rate)
96 0, // separation_byte_pos.y
97 1, // padding_pos.x (lane 16: last lane of 136-byte rate)
98 3, // padding_pos.y
99 turboSHAKE256MultiSliceToBuffer,
100 turboSHAKE256MultiSlice,
101);
102
103/// Rotate left for u64 vector
104inline fn rol64Vec(comptime N: usize, v: @Vector(N, u64), comptime n: u6) @Vector(N, u64) {
105 if (n == 0) return v;
106 const left: @Vector(N, u64) = @splat(n);
107 const right_shift: u64 = 64 - @as(u64, n);
108 const right: @Vector(N, u64) = @splat(right_shift);
109 return (v << left) | (v >> right);
110}
111
112/// Load a 64-bit little-endian value
113inline fn load64(bytes: []const u8) u64 {
114 return std.mem.readInt(u64, bytes[0..8], .little);
115}
116
117/// Store a 64-bit little-endian value
118inline fn store64(value: u64, bytes: []u8) void {
119 std.mem.writeInt(u64, bytes[0..8], value, .little);
120}
121
122/// Right-encode result type (max 9 bytes for 64-bit usize)
123const RightEncoded = struct {
124 bytes: [9]u8,
125 len: u8,
126
127 fn slice(self: *const RightEncoded) []const u8 {
128 return self.bytes[0..self.len];
129 }
130};
131
132/// Right-encode: encodes a number as bytes with length suffix (no allocation)
133fn rightEncode(x: usize) RightEncoded {
134 var result: RightEncoded = undefined;
135
136 if (x == 0) {
137 result.bytes[0] = 0;
138 result.len = 1;
139 return result;
140 }
141
142 var temp: [9]u8 = undefined;
143 var len: usize = 0;
144 var val = x;
145
146 while (val > 0) : (val /= 256) {
147 temp[len] = @intCast(val % 256);
148 len += 1;
149 }
150
151 // Reverse bytes (MSB first)
152 for (0..len) |i| {
153 result.bytes[i] = temp[len - 1 - i];
154 }
155 result.bytes[len] = @intCast(len);
156 result.len = @intCast(len + 1);
157
158 return result;
159}
160
161/// Virtual contiguous view over multiple slices (zero-copy)
162const MultiSliceView = struct {
163 slices: [3][]const u8,
164 offsets: [4]usize,
165
166 fn init(s1: []const u8, s2: []const u8, s3: []const u8) MultiSliceView {
167 return .{
168 .slices = .{ s1, s2, s3 },
169 .offsets = .{
170 0,
171 s1.len,
172 s1.len + s2.len,
173 s1.len + s2.len + s3.len,
174 },
175 };
176 }
177
178 fn totalLen(self: *const MultiSliceView) usize {
179 return self.offsets[3];
180 }
181
182 /// Get byte at position (zero-copy)
183 fn getByte(self: *const MultiSliceView, pos: usize) u8 {
184 for (0..3) |i| {
185 if (pos >= self.offsets[i] and pos < self.offsets[i + 1]) {
186 return self.slices[i][pos - self.offsets[i]];
187 }
188 }
189 unreachable;
190 }
191
192 /// Try to get a contiguous slice [start..end) - returns null if spans boundaries
193 fn tryGetSlice(self: *const MultiSliceView, start: usize, end: usize) ?[]const u8 {
194 for (0..3) |i| {
195 if (start >= self.offsets[i] and end <= self.offsets[i + 1]) {
196 const local_start = start - self.offsets[i];
197 const local_end = end - self.offsets[i];
198 return self.slices[i][local_start..local_end];
199 }
200 }
201 return null;
202 }
203
204 /// Copy range [start..end) to buffer (used when slice spans boundaries)
205 fn copyRange(self: *const MultiSliceView, start: usize, end: usize, buffer: []u8) void {
206 var pos: usize = 0;
207 for (start..end) |i| {
208 buffer[pos] = self.getByte(i);
209 pos += 1;
210 }
211 }
212};
213
214/// Apply Keccak-p[1600,12] to N states in parallel
215fn keccakP1600timesN(comptime N: usize, states: *[5][5]@Vector(N, u64)) void {
216 @setEvalBranchQuota(10000);
217
218 // Pre-computed rotation offsets for rho-pi step
219 const rho_offsets = comptime blk: {
220 var offsets: [24]u6 = undefined;
221 var px: usize = 1;
222 var py: usize = 0;
223 for (0..24) |t| {
224 const rot_amount = ((t + 1) * (t + 2) / 2) % 64;
225 offsets[t] = @intCast(rot_amount);
226 const temp_x = py;
227 py = (2 * px + 3 * py) % 5;
228 px = temp_x;
229 }
230 break :blk offsets;
231 };
232
233 var round: usize = 0;
234 while (round < 12) : (round += 2) {
235 inline for (0..2) |i| {
236 // θ (theta)
237 var C: [5]@Vector(N, u64) = undefined;
238 inline for (0..5) |x| {
239 C[x] = states[x][0] ^ states[x][1] ^ states[x][2] ^ states[x][3] ^ states[x][4];
240 }
241
242 var D: [5]@Vector(N, u64) = undefined;
243 inline for (0..5) |x| {
244 D[x] = C[(x + 4) % 5] ^ rol64Vec(N, C[(x + 1) % 5], 1);
245 }
246
247 // Apply D to all lanes
248 inline for (0..5) |x| {
249 states[x][0] ^= D[x];
250 states[x][1] ^= D[x];
251 states[x][2] ^= D[x];
252 states[x][3] ^= D[x];
253 states[x][4] ^= D[x];
254 }
255
256 // ρ (rho) and π (pi) - optimized with pre-computed offsets
257 var current = states[1][0];
258 var px: usize = 1;
259 var py: usize = 0;
260 inline for (rho_offsets) |rot| {
261 const next_y = (2 * px + 3 * py) % 5;
262 const next = states[py][next_y];
263 states[py][next_y] = rol64Vec(N, current, rot);
264 current = next;
265 px = py;
266 py = next_y;
267 }
268
269 // χ (chi) - optimized with better register usage
270 inline for (0..5) |y| {
271 const t0 = states[0][y];
272 const t1 = states[1][y];
273 const t2 = states[2][y];
274 const t3 = states[3][y];
275 const t4 = states[4][y];
276
277 states[0][y] = t0 ^ (~t1 & t2);
278 states[1][y] = t1 ^ (~t2 & t3);
279 states[2][y] = t2 ^ (~t3 & t4);
280 states[3][y] = t3 ^ (~t4 & t0);
281 states[4][y] = t4 ^ (~t0 & t1);
282 }
283
284 // ι (iota)
285 const rc_splat: @Vector(N, u64) = @splat(RC[round + i]);
286 states[0][0] ^= rc_splat;
287 }
288 }
289}
290
291/// Add lanes from data to N states in parallel with stride - optimized version
292fn addLanesAll(
293 comptime N: usize,
294 states: *[5][5]@Vector(N, u64),
295 data: []const u8,
296 lane_count: usize,
297 lane_offset: usize,
298) void {
299
300 // Process lanes (at most 25 lanes in Keccak state)
301 inline for (0..25) |xy| {
302 if (xy < lane_count) {
303 const x = xy % 5;
304 const y = xy / 5;
305
306 // Load N lanes with stride - optimized memory access pattern
307 var loaded_data: @Vector(N, u64) = undefined;
308 inline for (0..N) |i| {
309 loaded_data[i] = load64(data[8 * (i * lane_offset + xy) ..]);
310 }
311 states[x][y] ^= loaded_data;
312 }
313 }
314}
315
316/// Apply Keccak-p[1600,12] to a single state (byte representation)
317fn keccakP(state: *[200]u8) void {
318 @setEvalBranchQuota(10000);
319 var lanes: [5][5]u64 = undefined;
320
321 // Load state into lanes
322 inline for (0..5) |x| {
323 inline for (0..5) |y| {
324 lanes[x][y] = load64(state[8 * (x + 5 * y) ..]);
325 }
326 }
327
328 // Apply 12 rounds
329 var round: usize = 0;
330 while (round < 12) : (round += 2) {
331 inline for (0..2) |i| {
332 // θ
333 var C: [5]u64 = undefined;
334 inline for (0..5) |x| {
335 C[x] = lanes[x][0] ^ lanes[x][1] ^ lanes[x][2] ^ lanes[x][3] ^ lanes[x][4];
336 }
337 var D: [5]u64 = undefined;
338 inline for (0..5) |x| {
339 D[x] = C[(x + 4) % 5] ^ std.math.rotl(u64, C[(x + 1) % 5], 1);
340 }
341 inline for (0..5) |x| {
342 inline for (0..5) |y| {
343 lanes[x][y] ^= D[x];
344 }
345 }
346
347 // ρ and π
348 var current = lanes[1][0];
349 var px: usize = 1;
350 var py: usize = 0;
351 inline for (0..24) |t| {
352 const temp = lanes[py][(2 * px + 3 * py) % 5];
353 const rot_amount = ((t + 1) * (t + 2) / 2) % 64;
354 lanes[py][(2 * px + 3 * py) % 5] = std.math.rotl(u64, current, @as(u6, @intCast(rot_amount)));
355 current = temp;
356 const temp_x = py;
357 py = (2 * px + 3 * py) % 5;
358 px = temp_x;
359 }
360
361 // χ
362 inline for (0..5) |y| {
363 const T = [5]u64{ lanes[0][y], lanes[1][y], lanes[2][y], lanes[3][y], lanes[4][y] };
364 inline for (0..5) |x| {
365 lanes[x][y] = T[x] ^ (~T[(x + 1) % 5] & T[(x + 2) % 5]);
366 }
367 }
368
369 // ι
370 lanes[0][0] ^= RC[round + i];
371 }
372 }
373
374 // Store lanes back to state
375 inline for (0..5) |x| {
376 inline for (0..5) |y| {
377 store64(lanes[x][y], state[8 * (x + 5 * y) ..]);
378 }
379 }
380}
381
382/// Apply Keccak-p[1600,12] to a single state (u64 lane representation)
383fn keccakPLanes(lanes: *[25]u64) void {
384 @setEvalBranchQuota(10000);
385
386 // Apply 12 rounds
387 inline for (RC) |rc| {
388 // θ
389 var C: [5]u64 = undefined;
390 inline for (0..5) |x| {
391 C[x] = lanes[x] ^ lanes[x + 5] ^ lanes[x + 10] ^ lanes[x + 15] ^ lanes[x + 20];
392 }
393 var D: [5]u64 = undefined;
394 inline for (0..5) |x| {
395 D[x] = C[(x + 4) % 5] ^ std.math.rotl(u64, C[(x + 1) % 5], 1);
396 }
397 inline for (0..5) |x| {
398 inline for (0..5) |y| {
399 lanes[x + 5 * y] ^= D[x];
400 }
401 }
402
403 // ρ and π
404 var current = lanes[1];
405 var px: usize = 1;
406 var py: usize = 0;
407 inline for (0..24) |t| {
408 const next_y = (2 * px + 3 * py) % 5;
409 const next_idx = py + 5 * next_y;
410 const temp = lanes[next_idx];
411 const rot_amount = ((t + 1) * (t + 2) / 2) % 64;
412 lanes[next_idx] = std.math.rotl(u64, current, @as(u6, @intCast(rot_amount)));
413 current = temp;
414 px = py;
415 py = next_y;
416 }
417
418 // χ
419 inline for (0..5) |y| {
420 const idx = 5 * y;
421 const T = [5]u64{ lanes[idx], lanes[idx + 1], lanes[idx + 2], lanes[idx + 3], lanes[idx + 4] };
422 inline for (0..5) |x| {
423 lanes[idx + x] = T[x] ^ (~T[(x + 1) % 5] & T[(x + 2) % 5]);
424 }
425 }
426
427 // ι
428 lanes[0] ^= rc;
429 }
430}
431
432/// Generic non-allocating TurboSHAKE: write output to provided buffer
433fn turboSHAKEMultiSliceToBuffer(
434 comptime rate: usize,
435 view: *const MultiSliceView,
436 separation_byte: u8,
437 output: []u8,
438) void {
439 var state: [200]u8 = @splat(0);
440 var state_pos: usize = 0;
441
442 // Absorb all bytes from the multi-slice view
443 const total = view.totalLen();
444 var pos: usize = 0;
445 while (pos < total) {
446 state[state_pos] ^= view.getByte(pos);
447 state_pos += 1;
448 pos += 1;
449
450 if (state_pos == rate) {
451 keccakP(&state);
452 state_pos = 0;
453 }
454 }
455
456 // Add separation byte and padding
457 state[state_pos] ^= separation_byte;
458 state[rate - 1] ^= 0x80;
459 keccakP(&state);
460
461 // Squeeze
462 var out_offset: usize = 0;
463 while (out_offset < output.len) {
464 const chunk = @min(rate, output.len - out_offset);
465 @memcpy(output[out_offset..][0..chunk], state[0..chunk]);
466 out_offset += chunk;
467 if (out_offset < output.len) {
468 keccakP(&state);
469 }
470 }
471}
472
473/// Generic allocating TurboSHAKE
474fn turboSHAKEMultiSlice(
475 comptime rate: usize,
476 allocator: Allocator,
477 view: *const MultiSliceView,
478 separation_byte: u8,
479 output_len: usize,
480) ![]u8 {
481 const output = try allocator.alloc(u8, output_len);
482 turboSHAKEMultiSliceToBuffer(rate, view, separation_byte, output);
483 return output;
484}
485
486/// Non-allocating TurboSHAKE128: write output to provided buffer
487fn turboSHAKE128MultiSliceToBuffer(
488 view: *const MultiSliceView,
489 separation_byte: u8,
490 output: []u8,
491) void {
492 turboSHAKEMultiSliceToBuffer(168, view, separation_byte, output);
493}
494
495/// Allocating TurboSHAKE128
496fn turboSHAKE128MultiSlice(
497 allocator: Allocator,
498 view: *const MultiSliceView,
499 separation_byte: u8,
500 output_len: usize,
501) ![]u8 {
502 return turboSHAKEMultiSlice(168, allocator, view, separation_byte, output_len);
503}
504
505/// Non-allocating TurboSHAKE256: write output to provided buffer
506fn turboSHAKE256MultiSliceToBuffer(
507 view: *const MultiSliceView,
508 separation_byte: u8,
509 output: []u8,
510) void {
511 turboSHAKEMultiSliceToBuffer(136, view, separation_byte, output);
512}
513
514/// Allocating TurboSHAKE256
515fn turboSHAKE256MultiSlice(
516 allocator: Allocator,
517 view: *const MultiSliceView,
518 separation_byte: u8,
519 output_len: usize,
520) ![]u8 {
521 return turboSHAKEMultiSlice(136, allocator, view, separation_byte, output_len);
522}
523
524/// Process N leaves (8KiB chunks) in parallel - generic version
525fn processLeaves(
526 comptime Variant: type,
527 comptime N: usize,
528 data: []const u8,
529 result: *[N * Variant.cv_size]u8,
530) void {
531 const rate_in_lanes: usize = Variant.rate_in_lanes;
532 const rate_in_bytes: usize = rate_in_lanes * 8;
533 const cv_size: usize = Variant.cv_size;
534
535 // Initialize N all-zero states with cache alignment
536 var states: [5][5]@Vector(N, u64) align(cache_line_size) = undefined;
537 inline for (0..5) |x| {
538 inline for (0..5) |y| {
539 states[x][y] = @splat(0);
540 }
541 }
542
543 // Process complete blocks
544 var j: usize = 0;
545 while (j + rate_in_bytes <= chunk_size) : (j += rate_in_bytes) {
546 addLanesAll(N, &states, data[j..], rate_in_lanes, chunk_size / 8);
547 keccakP1600timesN(N, &states);
548 }
549
550 // Process last incomplete block
551 const remaining_lanes = (chunk_size - j) / 8;
552 if (remaining_lanes > 0) {
553 addLanesAll(N, &states, data[j..], remaining_lanes, chunk_size / 8);
554 }
555
556 // Add suffix 0x0B and padding
557 const suffix_pos = Variant.separation_byte_pos;
558 const padding_pos = Variant.padding_pos;
559
560 const suffix_splat: @Vector(N, u64) = @splat(0x0B);
561 states[suffix_pos.x][suffix_pos.y] ^= suffix_splat;
562 const padding_splat: @Vector(N, u64) = @splat(0x8000000000000000);
563 states[padding_pos.x][padding_pos.y] ^= padding_splat;
564
565 keccakP1600timesN(N, &states);
566
567 // Extract chaining values from each state
568 const lanes_to_extract = cv_size / 8;
569 comptime var lane_idx: usize = 0;
570 inline while (lane_idx < lanes_to_extract) : (lane_idx += 1) {
571 const x = lane_idx % 5;
572 const y = lane_idx / 5;
573 inline for (0..N) |i| {
574 store64(states[x][y][i], result[i * cv_size + lane_idx * 8 ..]);
575 }
576 }
577}
578
579/// Context for processing a batch of leaves in a thread
580const LeafBatchContext = struct {
581 output_cvs: []u8,
582 batch_start: usize,
583 batch_count: usize,
584 view: *const MultiSliceView,
585 scratch_buffer: []u8, // Pre-allocated scratch space (no allocations in worker)
586 total_len: usize, // Total length of input data (for boundary checking)
587};
588
589/// Helper function to process N leaves in parallel, reducing code duplication
590inline fn processNLeaves(
591 comptime Variant: type,
592 comptime N: usize,
593 view: *const MultiSliceView,
594 j: usize,
595 leaf_buffer: []u8,
596 output: []u8,
597) void {
598 const cv_size = Variant.cv_size;
599 if (view.tryGetSlice(j, j + N * chunk_size)) |leaf_data| {
600 var leaf_cvs: [N * cv_size]u8 = undefined;
601 processLeaves(Variant, N, leaf_data, &leaf_cvs);
602 @memcpy(output[0..leaf_cvs.len], &leaf_cvs);
603 } else {
604 view.copyRange(j, j + N * chunk_size, leaf_buffer[0 .. N * chunk_size]);
605 var leaf_cvs: [N * cv_size]u8 = undefined;
606 processLeaves(Variant, N, leaf_buffer[0 .. N * chunk_size], &leaf_cvs);
607 @memcpy(output[0..leaf_cvs.len], &leaf_cvs);
608 }
609}
610
611/// Process a batch of leaves in a single thread using SIMD
612fn processLeafBatch(comptime Variant: type, ctx: LeafBatchContext) void {
613 const cv_size = Variant.cv_size;
614 const leaf_buffer = ctx.scratch_buffer[0 .. 8 * chunk_size];
615 const cv_scratch = ctx.scratch_buffer[8 * chunk_size .. 8 * chunk_size + cv_size];
616
617 var cvs_offset: usize = 0;
618 var j: usize = ctx.batch_start;
619 const batch_end = @min(ctx.batch_start + ctx.batch_count * chunk_size, ctx.total_len);
620
621 // Process leaves using SIMD (8x, 4x, 2x) based on optimal vector length
622 inline for ([_]usize{ 8, 4, 2 }) |batch_size| {
623 while (optimal_vector_len >= batch_size and j + batch_size * chunk_size <= batch_end) {
624 processNLeaves(Variant, batch_size, ctx.view, j, leaf_buffer, ctx.output_cvs[cvs_offset..]);
625 cvs_offset += batch_size * cv_size;
626 j += batch_size * chunk_size;
627 }
628 }
629
630 // Process remaining single leaves
631 while (j < batch_end) {
632 const chunk_len = @min(chunk_size, batch_end - j);
633 if (ctx.view.tryGetSlice(j, j + chunk_len)) |leaf_data| {
634 const cv_slice = MultiSliceView.init(leaf_data, &[_]u8{}, &[_]u8{});
635 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_scratch[0..cv_size]);
636 @memcpy(ctx.output_cvs[cvs_offset..][0..cv_size], cv_scratch[0..cv_size]);
637 } else {
638 ctx.view.copyRange(j, j + chunk_len, leaf_buffer[0..chunk_len]);
639 const cv_slice = MultiSliceView.init(leaf_buffer[0..chunk_len], &[_]u8{}, &[_]u8{});
640 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_scratch[0..cv_size]);
641 @memcpy(ctx.output_cvs[cvs_offset..][0..cv_size], cv_scratch[0..cv_size]);
642 }
643 cvs_offset += cv_size;
644 j += chunk_size;
645 }
646}
647
648/// Helper to process N leaves in SIMD and absorb CVs into state
649inline fn processAndAbsorbNLeaves(
650 comptime Variant: type,
651 comptime N: usize,
652 view: *const MultiSliceView,
653 j: usize,
654 leaf_buffer: []u8,
655 final_state: anytype,
656) void {
657 const cv_size = Variant.cv_size;
658 if (view.tryGetSlice(j, j + N * chunk_size)) |leaf_data| {
659 var leaf_cvs: [N * cv_size]u8 align(cache_line_size) = undefined;
660 processLeaves(Variant, N, leaf_data, &leaf_cvs);
661 final_state.update(&leaf_cvs);
662 } else {
663 view.copyRange(j, j + N * chunk_size, leaf_buffer[0 .. N * chunk_size]);
664 var leaf_cvs: [N * cv_size]u8 align(cache_line_size) = undefined;
665 processLeaves(Variant, N, leaf_buffer[0 .. N * chunk_size], &leaf_cvs);
666 final_state.update(&leaf_cvs);
667 }
668}
669
670/// Generic single-threaded implementation
671fn ktSingleThreaded(comptime Variant: type, view: *const MultiSliceView, total_len: usize, output: []u8) void {
672 const cv_size = Variant.cv_size;
673 const StateType = Variant.StateType;
674
675 // Initialize streaming TurboSHAKE state for final node (delimiter 0x06 is set in the type)
676 var final_state = StateType.init(.{});
677
678 // Absorb first B bytes from input
679 var first_b_buffer: [chunk_size]u8 = undefined;
680 if (view.tryGetSlice(0, chunk_size)) |first_chunk| {
681 final_state.update(first_chunk);
682 } else {
683 view.copyRange(0, chunk_size, &first_b_buffer);
684 final_state.update(&first_b_buffer);
685 }
686
687 // Absorb padding bytes (8 bytes: 0x03 followed by 7 zeros)
688 const padding = [_]u8{ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
689 final_state.update(&padding);
690
691 var j: usize = chunk_size;
692 var n: usize = 0;
693
694 // Temporary buffers for boundary-spanning leaves and CV computation
695 var leaf_buffer: [chunk_size * 8]u8 align(cache_line_size) = undefined;
696 var cv_buffer: [64]u8 = undefined; // Max CV size is 64 bytes
697
698 // Process leaves in SIMD batches (8x, 4x, 2x)
699 inline for ([_]usize{ 8, 4, 2 }) |batch_size| {
700 while (optimal_vector_len >= batch_size and j + batch_size * chunk_size <= total_len) {
701 processAndAbsorbNLeaves(Variant, batch_size, view, j, &leaf_buffer, &final_state);
702 j += batch_size * chunk_size;
703 n += batch_size;
704 }
705 }
706
707 // Process remaining leaves one at a time
708 while (j < total_len) {
709 const chunk_len = @min(chunk_size, total_len - j);
710 if (view.tryGetSlice(j, j + chunk_len)) |leaf_data| {
711 const cv_slice = MultiSliceView.init(leaf_data, &[_]u8{}, &[_]u8{});
712 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_buffer[0..cv_size]);
713 final_state.update(cv_buffer[0..cv_size]); // Absorb CV immediately
714 } else {
715 view.copyRange(j, j + chunk_len, leaf_buffer[0..chunk_len]);
716 const cv_slice = MultiSliceView.init(leaf_buffer[0..chunk_len], &[_]u8{}, &[_]u8{});
717 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_buffer[0..cv_size]);
718 final_state.update(cv_buffer[0..cv_size]);
719 }
720 j += chunk_size;
721 n += 1;
722 }
723
724 // Absorb right_encode(n) and terminator
725 const n_enc = rightEncode(n);
726 final_state.update(n_enc.slice());
727 const terminator = [_]u8{ 0xFF, 0xFF };
728 final_state.update(&terminator);
729
730 // Finalize and squeeze output
731 final_state.final(output);
732}
733
734/// Generic multi-threaded implementation
735fn ktMultiThreaded(
736 comptime Variant: type,
737 allocator: Allocator,
738 io: Io,
739 view: *const MultiSliceView,
740 total_len: usize,
741 output: []u8,
742) !void {
743 const cv_size = Variant.cv_size;
744
745 // Calculate total number of leaves
746 const total_leaves: usize = (total_len - 1) / chunk_size;
747
748 // Check if we have enough threads to benefit from parallelization
749 const thread_count = Thread.getCpuCount() catch 1;
750 if (thread_count <= 1) {
751 // Single-threaded fallback - more efficient than using group.async
752 ktSingleThreaded(Variant, view, total_len, output);
753 return;
754 }
755
756 // Allocate buffer for all chaining values
757 const cvs = try allocator.alloc(u8, total_leaves * cv_size);
758 defer allocator.free(cvs);
759
760 // Divide work among threads
761 const leaves_per_thread = (total_leaves + thread_count - 1) / thread_count;
762
763 // Pre-allocate scratch buffers for all threads (8 leaves + CV size)
764 const scratch_size = 8 * chunk_size + cv_size;
765 const all_scratch = try allocator.alloc(u8, thread_count * scratch_size);
766 defer allocator.free(all_scratch);
767
768 const contexts = try allocator.alloc(LeafBatchContext, thread_count);
769 defer allocator.free(contexts);
770
771 var leaves_assigned: usize = 0;
772 var context_count: usize = 0;
773
774 while (leaves_assigned < total_leaves) {
775 const batch_count = @min(leaves_per_thread, total_leaves - leaves_assigned);
776 const batch_start = chunk_size + leaves_assigned * chunk_size;
777 const cvs_offset = leaves_assigned * cv_size;
778
779 contexts[context_count] = LeafBatchContext{
780 .output_cvs = cvs[cvs_offset .. cvs_offset + batch_count * cv_size],
781 .batch_start = batch_start,
782 .batch_count = batch_count,
783 .view = view,
784 .scratch_buffer = all_scratch[context_count * scratch_size .. (context_count + 1) * scratch_size],
785 .total_len = total_len,
786 };
787
788 leaves_assigned += batch_count;
789 context_count += 1;
790 }
791
792 var group: Io.Group = .init;
793 for (contexts[0..context_count]) |ctx| {
794 group.async(io, struct {
795 fn process(c: LeafBatchContext) void {
796 processLeafBatch(Variant, c);
797 }
798 }.process, .{ctx});
799 }
800
801 // Wait for all threads to complete
802 group.wait(io);
803
804 // Build final node
805 const n_enc = rightEncode(total_leaves);
806 const final_node_len = chunk_size + 8 + total_leaves * cv_size + n_enc.len + 2;
807 const final_node = try allocator.alloc(u8, final_node_len);
808 defer allocator.free(final_node);
809
810 // Copy first B bytes
811 if (view.tryGetSlice(0, chunk_size)) |first_chunk| {
812 @memcpy(final_node[0..chunk_size], first_chunk);
813 } else {
814 view.copyRange(0, chunk_size, final_node[0..chunk_size]);
815 }
816
817 @memset(final_node[chunk_size..][0..8], 0);
818 final_node[chunk_size] = 0x03;
819 @memcpy(final_node[chunk_size + 8 ..][0 .. total_leaves * cv_size], cvs);
820 @memcpy(final_node[chunk_size + 8 + total_leaves * cv_size ..][0..n_enc.len], n_enc.slice());
821 final_node[final_node_len - 2] = 0xFF;
822 final_node[final_node_len - 1] = 0xFF;
823
824 const final_view = MultiSliceView.init(final_node, &[_]u8{}, &[_]u8{});
825 Variant.turboSHAKEToBuffer(&final_view, 0x06, output);
826}
827
828/// Generic KangarooTwelve hash function builder.
829/// Creates a public API type with hash and hashParallel methods for a specific variant.
830fn KTHash(
831 comptime Variant: type,
832 comptime singleChunkFn: fn (*const MultiSliceView, u8, []u8) void,
833) type {
834 return struct {
835 const Self = @This();
836 const StateType = Variant.StateType;
837
838 /// The recommended output length, in bytes.
839 pub const digest_length = Variant.security_level / 8 * 2;
840 /// The block length, or rate, in bytes.
841 pub const block_length = Variant.rate;
842
843 /// Configuration options for KangarooTwelve hashing.
844 ///
845 /// Options include an optional customization string that provides domain separation,
846 /// ensuring that identical inputs with different customization strings
847 /// produce completely distinct hash outputs.
848 ///
849 /// This prevents hash collisions when the same data is hashed in different contexts.
850 ///
851 /// Customization strings can be of any length.
852 ///
853 /// Common options for customization::
854 ///
855 /// - Key derivation or MAC: 16-byte secret for KT128, 32-byte secret for KT256
856 /// - Context Separation: domain-specific strings (e.g., "email", "password", "session")
857 /// - Composite Keys: concatenation of secret key + context string
858 pub const Options = struct {
859 customization: ?[]const u8 = null,
860 };
861
862 // Message buffer (accumulates message data only, not customization)
863 buffer: [chunk_size]u8,
864 buffer_len: usize,
865 message_len: usize,
866
867 // Customization string (fixed at init)
868 customization: []const u8,
869 custom_len_enc: RightEncoded,
870
871 // Tree mode state (lazy initialization when buffer overflows first time)
872 first_chunk: ?[chunk_size]u8, // Saved first chunk for tree mode
873 final_state: ?StateType, // Running TurboSHAKE state for final node
874 num_leaves: usize, // Count of leaves processed (after first chunk)
875
876 // SIMD chunk batching
877 pending_chunks: [8 * chunk_size]u8 align(cache_line_size), // Buffer for up to 8 chunks
878 pending_count: usize, // Number of complete chunks in pending_chunks
879
880 /// Initialize a KangarooTwelve hashing context.
881 ///
882 /// Options include an optional customization string that provides domain separation,
883 /// ensuring that identical inputs with different customization strings
884 /// produce completely distinct hash outputs.
885 ///
886 /// This prevents hash collisions when the same data is hashed in different contexts.
887 ///
888 /// Customization strings can be of any length.
889 ///
890 /// Common options for customization::
891 ///
892 /// - Key derivation or MAC: 16-byte secret for KT128, 32-byte secret for KT256
893 /// - Context Separation: domain-specific strings (e.g., "email", "password", "session")
894 /// - Composite Keys: concatenation of secret key + context string
895 pub fn init(options: Options) Self {
896 const custom = options.customization orelse &[_]u8{};
897 return .{
898 .buffer = undefined,
899 .buffer_len = 0,
900 .message_len = 0,
901 .customization = custom,
902 .custom_len_enc = rightEncode(custom.len),
903 .first_chunk = null,
904 .final_state = null,
905 .num_leaves = 0,
906 .pending_chunks = undefined,
907 .pending_count = 0,
908 };
909 }
910
911 /// Flush all pending chunks using SIMD when possible
912 fn flushPendingChunks(self: *Self) void {
913 const cv_size = Variant.cv_size;
914
915 // Process all pending chunks using the largest SIMD batch sizes possible
916 while (self.pending_count > 0) {
917 // Try SIMD batches in decreasing size order
918 inline for ([_]usize{ 8, 4, 2 }) |batch_size| {
919 if (optimal_vector_len >= batch_size and self.pending_count >= batch_size) {
920 var leaf_cvs: [batch_size * cv_size]u8 align(cache_line_size) = undefined;
921 processLeaves(Variant, batch_size, self.pending_chunks[0 .. batch_size * chunk_size], &leaf_cvs);
922 self.final_state.?.update(&leaf_cvs);
923 self.num_leaves += batch_size;
924 self.pending_count -= batch_size;
925
926 // Shift remaining chunks to the front
927 if (self.pending_count > 0) {
928 const remaining_bytes = self.pending_count * chunk_size;
929 @memcpy(self.pending_chunks[0..remaining_bytes], self.pending_chunks[batch_size * chunk_size ..][0..remaining_bytes]);
930 }
931 break; // Continue outer loop to try next batch
932 }
933 }
934
935 // If no SIMD batch was possible, process one chunk with scalar code
936 if (self.pending_count > 0 and self.pending_count < 2) {
937 var cv_buffer: [64]u8 = undefined;
938 const cv_slice = MultiSliceView.init(self.pending_chunks[0..chunk_size], &[_]u8{}, &[_]u8{});
939 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_buffer[0..cv_size]);
940 self.final_state.?.update(cv_buffer[0..cv_size]);
941 self.num_leaves += 1;
942 self.pending_count -= 1;
943 break; // No more chunks to process
944 }
945 }
946 }
947
948 /// Absorb data into the hash state.
949 /// Can be called multiple times to incrementally add data.
950 pub fn update(self: *Self, data: []const u8) void {
951 if (data.len == 0) return;
952
953 var remaining = data;
954
955 while (remaining.len > 0) {
956 const space_in_buffer = chunk_size - self.buffer_len;
957 const to_copy = @min(space_in_buffer, remaining.len);
958
959 // Copy data into buffer
960 @memcpy(self.buffer[self.buffer_len..][0..to_copy], remaining[0..to_copy]);
961 self.buffer_len += to_copy;
962 self.message_len += to_copy;
963 remaining = remaining[to_copy..];
964
965 // If buffer is full, process it
966 if (self.buffer_len == chunk_size) {
967 if (self.first_chunk == null) {
968 // First time buffer fills - initialize tree mode
969 self.first_chunk = self.buffer;
970 self.final_state = StateType.init(.{});
971
972 // Absorb first chunk into final state
973 self.final_state.?.update(&self.buffer);
974
975 // Absorb padding (8 bytes: 0x03 followed by 7 zeros)
976 const padding = [_]u8{ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
977 self.final_state.?.update(&padding);
978 } else {
979 // Add chunk to pending buffer for SIMD batch processing
980 @memcpy(self.pending_chunks[self.pending_count * chunk_size ..][0..chunk_size], &self.buffer);
981 self.pending_count += 1;
982
983 // Flush when we have enough chunks for optimal SIMD batch
984 // Determine best batch size for this architecture
985 const optimal_batch_size = comptime blk: {
986 if (optimal_vector_len >= 8) break :blk 8;
987 if (optimal_vector_len >= 4) break :blk 4;
988 if (optimal_vector_len >= 2) break :blk 2;
989 break :blk 1;
990 };
991 if (self.pending_count >= optimal_batch_size) {
992 self.flushPendingChunks();
993 }
994 }
995 self.buffer_len = 0;
996 }
997 }
998 }
999
1000 /// Finalize the hash and produce output.
1001 ///
1002 /// Unlike traditional hash functions, the output can be of any length.
1003 ///
1004 /// When using as a regular hash function, use the recommended `digest_length` value (32 bytes for KT128, 64 bytes for KT256).
1005 ///
1006 /// After calling this method, the context should not be reused. However, the structure can be cloned before finalizing
1007 /// to compute multiple hashes with the same prefix.
1008 pub fn final(self: *Self, out: []u8) void {
1009 const cv_size = Variant.cv_size;
1010
1011 // Calculate total length: message + customization + right_encode(customization.len)
1012 const total_len = self.message_len + self.customization.len + self.custom_len_enc.len;
1013
1014 // Single chunk mode: total data fits in one chunk
1015 if (total_len <= chunk_size) {
1016 // Build the complete input: buffer + customization + encoded length
1017 var single_chunk: [chunk_size]u8 = undefined;
1018 @memcpy(single_chunk[0..self.buffer_len], self.buffer[0..self.buffer_len]);
1019 @memcpy(single_chunk[self.buffer_len..][0..self.customization.len], self.customization);
1020 @memcpy(single_chunk[self.buffer_len + self.customization.len ..][0..self.custom_len_enc.len], self.custom_len_enc.slice());
1021
1022 const view = MultiSliceView.init(single_chunk[0..total_len], &[_]u8{}, &[_]u8{});
1023 singleChunkFn(&view, 0x07, out);
1024 return;
1025 }
1026
1027 // Flush any pending chunks with SIMD
1028 self.flushPendingChunks();
1029
1030 // Build view over remaining data (buffer + customization + encoding)
1031 const remaining_view = MultiSliceView.init(
1032 self.buffer[0..self.buffer_len],
1033 self.customization,
1034 self.custom_len_enc.slice(),
1035 );
1036 const remaining_len = remaining_view.totalLen();
1037
1038 var final_leaves = self.num_leaves;
1039 var leaf_start: usize = 0;
1040
1041 // Tree mode: initialize if not already done (lazy initialization)
1042 if (self.final_state == null and remaining_len > 0) {
1043 self.final_state = StateType.init(.{});
1044
1045 // Absorb first chunk (up to chunk_size bytes from remaining data)
1046 const first_chunk_len = @min(chunk_size, remaining_len);
1047 if (remaining_view.tryGetSlice(0, first_chunk_len)) |first_chunk| {
1048 // Data is contiguous, use it directly
1049 self.final_state.?.update(first_chunk);
1050 } else {
1051 // Data spans boundaries, copy to buffer
1052 var first_chunk_buf: [chunk_size]u8 = undefined;
1053 remaining_view.copyRange(0, first_chunk_len, first_chunk_buf[0..first_chunk_len]);
1054 self.final_state.?.update(first_chunk_buf[0..first_chunk_len]);
1055 }
1056
1057 // Absorb padding (8 bytes: 0x03 followed by 7 zeros)
1058 const padding = [_]u8{ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
1059 self.final_state.?.update(&padding);
1060
1061 // Process remaining data as leaves
1062 leaf_start = first_chunk_len;
1063 }
1064
1065 // Process all remaining data as leaves (starting from leaf_start)
1066 var offset = leaf_start;
1067 while (offset < remaining_len) {
1068 const leaf_end = @min(offset + chunk_size, remaining_len);
1069 const leaf_size = leaf_end - offset;
1070
1071 var cv_buffer: [64]u8 = undefined;
1072 if (remaining_view.tryGetSlice(offset, leaf_end)) |leaf_data| {
1073 // Data is contiguous, use it directly
1074 const cv_slice = MultiSliceView.init(leaf_data, &[_]u8{}, &[_]u8{});
1075 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_buffer[0..cv_size]);
1076 } else {
1077 // Data spans boundaries, copy to buffer
1078 var leaf_buf: [chunk_size]u8 = undefined;
1079 remaining_view.copyRange(offset, leaf_end, leaf_buf[0..leaf_size]);
1080 const cv_slice = MultiSliceView.init(leaf_buf[0..leaf_size], &[_]u8{}, &[_]u8{});
1081 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_buffer[0..cv_size]);
1082 }
1083 self.final_state.?.update(cv_buffer[0..cv_size]);
1084 final_leaves += 1;
1085 offset = leaf_end;
1086 }
1087
1088 // Absorb right_encode(num_leaves) and terminator
1089 const n_enc = rightEncode(final_leaves);
1090 self.final_state.?.update(n_enc.slice());
1091 const terminator = [_]u8{ 0xFF, 0xFF };
1092 self.final_state.?.update(&terminator);
1093
1094 // Squeeze output
1095 self.final_state.?.final(out);
1096 }
1097
1098 /// Hash a message using sequential processing with SIMD acceleration.
1099 ///
1100 /// Parameters:
1101 /// - message: Input data to hash (any length)
1102 /// - out: Output buffer (any length, arbitrary output sizes supported, `digest_length` recommended for standard use)
1103 /// - options: Optional settings to include a secret key or a context separation string
1104 pub fn hash(message: []const u8, out: []u8, options: Options) !void {
1105 const custom = options.customization orelse &[_]u8{};
1106
1107 // Right-encode customization length
1108 const custom_len_enc = rightEncode(custom.len);
1109
1110 // Create zero-copy multi-slice view (no concatenation)
1111 const view = MultiSliceView.init(message, custom, custom_len_enc.slice());
1112 const total_len = view.totalLen();
1113
1114 // Single chunk case - zero-copy absorption!
1115 if (total_len <= chunk_size) {
1116 singleChunkFn(&view, 0x07, out);
1117 return;
1118 }
1119
1120 // Tree mode - single-threaded SIMD processing
1121 ktSingleThreaded(Variant, &view, total_len, out);
1122 }
1123
1124 /// Hash with automatic parallelization for large inputs (>2MB).
1125 /// Automatically uses sequential processing for smaller inputs to avoid thread overhead.
1126 /// Allocator required for temporary buffers. IO object required for thread management.
1127 pub fn hashParallel(message: []const u8, out: []u8, options: Options, allocator: Allocator, io: Io) !void {
1128 const custom = options.customization orelse &[_]u8{};
1129
1130 const custom_len_enc = rightEncode(custom.len);
1131 const view = MultiSliceView.init(message, custom, custom_len_enc.slice());
1132 const total_len = view.totalLen();
1133
1134 // Single chunk case
1135 if (total_len <= chunk_size) {
1136 singleChunkFn(&view, 0x07, out);
1137 return;
1138 }
1139
1140 // Use single-threaded processing if below threshold
1141 if (total_len < large_file_threshold) {
1142 ktSingleThreaded(Variant, &view, total_len, out);
1143 return;
1144 }
1145
1146 // Tree mode - multi-threaded processing
1147 try ktMultiThreaded(Variant, allocator, io, &view, total_len, out);
1148 }
1149 };
1150}
1151
1152/// KangarooTwelve is a fast, secure cryptographic hash function that uses tree-hashing
1153/// on top of TurboSHAKE. It is built on the Keccak permutation, the same primitive
1154/// underlying SHA-3, which has undergone over 15 years of intensive cryptanalysis
1155/// since the SHA-3 competition (2008-2012) and remains secure.
1156///
1157/// K12 uses Keccak-p[1600,12] with 12 rounds (half of SHA-3's 24 rounds), providing
1158/// 128-bit security strength equivalent to AES-128 and SHAKE128. While this offers
1159/// less conservative margin than SHA-3, current cryptanalysis reaches only 6 rounds,
1160/// leaving a substantial security margin. This deliberate trade-off delivers
1161/// significantly better performance while maintaining strong practical security.
1162///
1163/// Standardized as RFC 9861 after 8 years of public scrutiny. Supports arbitrary-length
1164/// output and optional customization strings for domain separation.
1165pub const KT128 = KTHash(KT128Variant, turboSHAKE128MultiSliceToBuffer);
1166
1167/// KangarooTwelve is a fast, secure cryptographic hash function that uses tree-hashing
1168/// on top of TurboSHAKE. It is built on the Keccak permutation, the same primitive
1169/// underlying SHA-3, which has undergone over 15 years of intensive cryptanalysis
1170/// since the SHA-3 competition (2008-2012) and remains secure.
1171///
1172/// KT256 provides 256-bit security strength and achieves NIST post-quantum security
1173/// level 2 when using at least 256-bit outputs. Like KT128, it uses Keccak-p[1600,12]
1174/// with 12 rounds, offering a deliberate trade-off between conservative margin and
1175/// significantly better performance while maintaining strong practical security.
1176///
1177/// Use KT256 when you need extra conservative margins.
1178/// For most applications, KT128 offers better performance with adequate security.
1179pub const KT256 = KTHash(KT256Variant, turboSHAKE256MultiSliceToBuffer);
1180
1181test "KT128 sequential and parallel produce same output for small inputs" {
1182 const allocator = std.testing.allocator;
1183 const io = std.testing.io;
1184
1185 // Test with different small input sizes
1186 const test_sizes = [_]usize{ 100, 1024, 4096, 8192 }; // 100B, 1KB, 4KB, 8KB
1187
1188 for (test_sizes) |size| {
1189 const input = try allocator.alloc(u8, size);
1190 defer allocator.free(input);
1191
1192 // Fill with random data
1193 crypto.random.bytes(input);
1194
1195 var output_seq: [32]u8 = undefined;
1196 var output_par: [32]u8 = undefined;
1197
1198 // Hash with sequential method
1199 try KT128.hash(input, &output_seq, .{});
1200
1201 // Hash with parallel method
1202 try KT128.hashParallel(input, &output_par, .{}, allocator, io);
1203
1204 // Verify outputs match
1205 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1206 }
1207}
1208
1209test "KT128 sequential and parallel produce same output for large inputs" {
1210 const allocator = std.testing.allocator;
1211 const io = std.testing.io;
1212
1213 // Test with large input sizes that trigger parallel processing
1214 // The threshold is 3-10MB depending on CPU count, so we test above that
1215 const test_sizes = [_]usize{ 11 * 1024 * 1024, 20 * 1024 * 1024 }; // 11MB, 20MB
1216
1217 for (test_sizes) |size| {
1218 const input = try allocator.alloc(u8, size);
1219 defer allocator.free(input);
1220
1221 // Fill with random data
1222 crypto.random.bytes(input);
1223
1224 var output_seq: [64]u8 = undefined;
1225 var output_par: [64]u8 = undefined;
1226
1227 // Hash with sequential method
1228 try KT128.hash(input, &output_seq, .{});
1229
1230 // Hash with parallel method
1231 try KT128.hashParallel(input, &output_par, .{}, allocator, io);
1232
1233 // Verify outputs match
1234 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1235 }
1236}
1237
1238test "KT128 sequential and parallel produce same output with customization" {
1239 const allocator = std.testing.allocator;
1240 const io = std.testing.io;
1241
1242 const input_size = 15 * 1024 * 1024; // 15MB
1243 const input = try allocator.alloc(u8, input_size);
1244 defer allocator.free(input);
1245
1246 // Fill with random data
1247 crypto.random.bytes(input);
1248
1249 const customization = "test domain";
1250 var output_seq: [48]u8 = undefined;
1251 var output_par: [48]u8 = undefined;
1252
1253 // Hash with sequential method
1254 try KT128.hash(input, &output_seq, .{ .customization = customization });
1255
1256 // Hash with parallel method
1257 try KT128.hashParallel(input, &output_par, .{ .customization = customization }, allocator, io);
1258
1259 // Verify outputs match
1260 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1261}
1262
1263test "KT256 sequential and parallel produce same output for small inputs" {
1264 const allocator = std.testing.allocator;
1265 const io = std.testing.io;
1266
1267 // Test with different small input sizes
1268 const test_sizes = [_]usize{ 100, 1024, 4096, 8192 }; // 100B, 1KB, 4KB, 8KB
1269
1270 for (test_sizes) |size| {
1271 const input = try allocator.alloc(u8, size);
1272 defer allocator.free(input);
1273
1274 // Fill with random data
1275 crypto.random.bytes(input);
1276
1277 var output_seq: [64]u8 = undefined;
1278 var output_par: [64]u8 = undefined;
1279
1280 // Hash with sequential method
1281 try KT256.hash(input, &output_seq, .{});
1282
1283 // Hash with parallel method
1284 try KT256.hashParallel(input, &output_par, .{}, allocator, io);
1285
1286 // Verify outputs match
1287 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1288 }
1289}
1290
1291test "KT256 sequential and parallel produce same output for large inputs" {
1292 const allocator = std.testing.allocator;
1293 const io = std.testing.io;
1294
1295 // Test with large input sizes that trigger parallel processing
1296 const test_sizes = [_]usize{ 11 * 1024 * 1024, 20 * 1024 * 1024 }; // 11MB, 20MB
1297
1298 for (test_sizes) |size| {
1299 const input = try allocator.alloc(u8, size);
1300 defer allocator.free(input);
1301
1302 // Fill with random data
1303 crypto.random.bytes(input);
1304
1305 var output_seq: [64]u8 = undefined;
1306 var output_par: [64]u8 = undefined;
1307
1308 // Hash with sequential method
1309 try KT256.hash(input, &output_seq, .{});
1310
1311 // Hash with parallel method
1312 try KT256.hashParallel(input, &output_par, .{}, allocator, io);
1313
1314 // Verify outputs match
1315 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1316 }
1317}
1318
1319test "KT256 sequential and parallel produce same output with customization" {
1320 const allocator = std.testing.allocator;
1321 const io = std.testing.io;
1322
1323 const input_size = 15 * 1024 * 1024; // 15MB
1324 const input = try allocator.alloc(u8, input_size);
1325 defer allocator.free(input);
1326
1327 // Fill with random data
1328 crypto.random.bytes(input);
1329
1330 const customization = "test domain";
1331 var output_seq: [80]u8 = undefined;
1332 var output_par: [80]u8 = undefined;
1333
1334 // Hash with sequential method
1335 try KT256.hash(input, &output_seq, .{ .customization = customization });
1336
1337 // Hash with parallel method
1338 try KT256.hashParallel(input, &output_par, .{ .customization = customization }, allocator, io);
1339
1340 // Verify outputs match
1341 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1342}
1343
1344/// Helper: Generate pattern data where data[i] = (i % 251)
1345fn generatePattern(allocator: Allocator, len: usize) ![]u8 {
1346 const data = try allocator.alloc(u8, len);
1347 for (data, 0..) |*byte, i| {
1348 byte.* = @intCast(i % 251);
1349 }
1350 return data;
1351}
1352
1353test "KT128: empty message, empty customization, 32 bytes" {
1354 var output: [32]u8 = undefined;
1355 try KT128.hash(&[_]u8{}, &output, .{});
1356
1357 var expected: [32]u8 = undefined;
1358 _ = try std.fmt.hexToBytes(&expected, "1AC2D450FC3B4205D19DA7BFCA1B37513C0803577AC7167F06FE2CE1F0EF39E5");
1359 try std.testing.expectEqualSlices(u8, &expected, &output);
1360}
1361
1362test "KT128: empty message, empty customization, 64 bytes" {
1363 var output: [64]u8 = undefined;
1364 try KT128.hash(&[_]u8{}, &output, .{});
1365
1366 var expected: [64]u8 = undefined;
1367 _ = try std.fmt.hexToBytes(&expected, "1AC2D450FC3B4205D19DA7BFCA1B37513C0803577AC7167F06FE2CE1F0EF39E54269C056B8C82E48276038B6D292966CC07A3D4645272E31FF38508139EB0A71");
1368 try std.testing.expectEqualSlices(u8, &expected, &output);
1369}
1370
1371test "KT128: empty message, empty customization, 10032 bytes (last 32)" {
1372 const allocator = std.testing.allocator;
1373 const output = try allocator.alloc(u8, 10032);
1374 defer allocator.free(output);
1375
1376 try KT128.hash(&[_]u8{}, output, .{});
1377
1378 var expected: [32]u8 = undefined;
1379 _ = try std.fmt.hexToBytes(&expected, "E8DC563642F7228C84684C898405D3A834799158C079B12880277A1D28E2FF6D");
1380 try std.testing.expectEqualSlices(u8, &expected, output[10000..]);
1381}
1382
1383test "KT128: pattern message (1 byte), empty customization, 32 bytes" {
1384 const allocator = std.testing.allocator;
1385 const message = try generatePattern(allocator, 1);
1386 defer allocator.free(message);
1387
1388 var output: [32]u8 = undefined;
1389 try KT128.hash(message, &output, .{});
1390
1391 var expected: [32]u8 = undefined;
1392 _ = try std.fmt.hexToBytes(&expected, "2BDA92450E8B147F8A7CB629E784A058EFCA7CF7D8218E02D345DFAA65244A1F");
1393 try std.testing.expectEqualSlices(u8, &expected, &output);
1394}
1395
1396test "KT128: pattern message (17 bytes), empty customization, 32 bytes" {
1397 const allocator = std.testing.allocator;
1398 const message = try generatePattern(allocator, 17);
1399 defer allocator.free(message);
1400
1401 var output: [32]u8 = undefined;
1402 try KT128.hash(message, &output, .{});
1403
1404 var expected: [32]u8 = undefined;
1405 _ = try std.fmt.hexToBytes(&expected, "6BF75FA2239198DB4772E36478F8E19B0F371205F6A9A93A273F51DF37122888");
1406 try std.testing.expectEqualSlices(u8, &expected, &output);
1407}
1408
1409test "KT128: pattern message (289 bytes), empty customization, 32 bytes" {
1410 const allocator = std.testing.allocator;
1411 const message = try generatePattern(allocator, 289);
1412 defer allocator.free(message);
1413
1414 var output: [32]u8 = undefined;
1415 try KT128.hash(message, &output, .{});
1416
1417 var expected: [32]u8 = undefined;
1418 _ = try std.fmt.hexToBytes(&expected, "0C315EBCDEDBF61426DE7DCF8FB725D1E74675D7F5327A5067F367B108ECB67C");
1419 try std.testing.expectEqualSlices(u8, &expected, &output);
1420}
1421
1422test "KT128: 0xFF message (1 byte), pattern customization (1 byte), 32 bytes" {
1423 const allocator = std.testing.allocator;
1424 const customization = try generatePattern(allocator, 1);
1425 defer allocator.free(customization);
1426
1427 const message = [_]u8{0xFF};
1428 var output: [32]u8 = undefined;
1429 try KT128.hash(&message, &output, .{ .customization = customization });
1430
1431 var expected: [32]u8 = undefined;
1432 _ = try std.fmt.hexToBytes(&expected, "A20B92B251E3D62443EC286E4B9B470A4E8315C156EEB24878B038ABE20650BE");
1433 try std.testing.expectEqualSlices(u8, &expected, &output);
1434}
1435
1436test "KT128: pattern message (8191 bytes), empty customization, 32 bytes" {
1437 const allocator = std.testing.allocator;
1438 const message = try generatePattern(allocator, 8191);
1439 defer allocator.free(message);
1440
1441 var output: [32]u8 = undefined;
1442 try KT128.hash(message, &output, .{});
1443
1444 var expected: [32]u8 = undefined;
1445 _ = try std.fmt.hexToBytes(&expected, "1B577636F723643E990CC7D6A659837436FD6A103626600EB8301CD1DBE553D6");
1446 try std.testing.expectEqualSlices(u8, &expected, &output);
1447}
1448
1449test "KT128: pattern message (8192 bytes), empty customization, 32 bytes" {
1450 const allocator = std.testing.allocator;
1451 const message = try generatePattern(allocator, 8192);
1452 defer allocator.free(message);
1453
1454 var output: [32]u8 = undefined;
1455 try KT128.hash(message, &output, .{});
1456
1457 var expected: [32]u8 = undefined;
1458 _ = try std.fmt.hexToBytes(&expected, "48F256F6772F9EDFB6A8B661EC92DC93B95EBD05A08A17B39AE3490870C926C3");
1459 try std.testing.expectEqualSlices(u8, &expected, &output);
1460}
1461
1462test "KT256: empty message, empty customization, 64 bytes" {
1463 var output: [64]u8 = undefined;
1464 try KT256.hash(&[_]u8{}, &output, .{});
1465
1466 var expected: [64]u8 = undefined;
1467 _ = try std.fmt.hexToBytes(&expected, "B23D2E9CEA9F4904E02BEC06817FC10CE38CE8E93EF4C89E6537076AF8646404E3E8B68107B8833A5D30490AA33482353FD4ADC7148ECB782855003AAEBDE4A9");
1468 try std.testing.expectEqualSlices(u8, &expected, &output);
1469}
1470
1471test "KT256: empty message, empty customization, 128 bytes" {
1472 var output: [128]u8 = undefined;
1473 try KT256.hash(&[_]u8{}, &output, .{});
1474
1475 var expected: [128]u8 = undefined;
1476 _ = try std.fmt.hexToBytes(&expected, "B23D2E9CEA9F4904E02BEC06817FC10CE38CE8E93EF4C89E6537076AF8646404E3E8B68107B8833A5D30490AA33482353FD4ADC7148ECB782855003AAEBDE4A9B0925319D8EA1E121A609821EC19EFEA89E6D08DAEE1662B69C840289F188BA860F55760B61F82114C030C97E5178449608CCD2CD2D919FC7829FF69931AC4D0");
1477 try std.testing.expectEqualSlices(u8, &expected, &output);
1478}
1479
1480test "KT256: pattern message (1 byte), empty customization, 64 bytes" {
1481 const allocator = std.testing.allocator;
1482 const message = try generatePattern(allocator, 1);
1483 defer allocator.free(message);
1484
1485 var output: [64]u8 = undefined;
1486 try KT256.hash(message, &output, .{});
1487
1488 var expected: [64]u8 = undefined;
1489 _ = try std.fmt.hexToBytes(&expected, "0D005A194085360217128CF17F91E1F71314EFA5564539D444912E3437EFA17F82DB6F6FFE76E781EAA068BCE01F2BBF81EACB983D7230F2FB02834A21B1DDD0");
1490 try std.testing.expectEqualSlices(u8, &expected, &output);
1491}
1492
1493test "KT256: pattern message (17 bytes), empty customization, 64 bytes" {
1494 const allocator = std.testing.allocator;
1495 const message = try generatePattern(allocator, 17);
1496 defer allocator.free(message);
1497
1498 var output: [64]u8 = undefined;
1499 try KT256.hash(message, &output, .{});
1500
1501 var expected: [64]u8 = undefined;
1502 _ = try std.fmt.hexToBytes(&expected, "1BA3C02B1FC514474F06C8979978A9056C8483F4A1B63D0DCCEFE3A28A2F323E1CDCCA40EBF006AC76EF0397152346837B1277D3E7FAA9C9653B19075098527B");
1503 try std.testing.expectEqualSlices(u8, &expected, &output);
1504}
1505
1506test "KT256: pattern message (8191 bytes), empty customization, 64 bytes" {
1507 const allocator = std.testing.allocator;
1508 const message = try generatePattern(allocator, 8191);
1509 defer allocator.free(message);
1510
1511 var output: [64]u8 = undefined;
1512 try KT256.hash(message, &output, .{});
1513
1514 var expected: [64]u8 = undefined;
1515 _ = try std.fmt.hexToBytes(&expected, "3081434D93A4108D8D8A3305B89682CEBEDC7CA4EA8A3CE869FBB73CBE4A58EEF6F24DE38FFC170514C70E7AB2D01F03812616E863D769AFB3753193BA045B20");
1516 try std.testing.expectEqualSlices(u8, &expected, &output);
1517}
1518
1519test "KT256: pattern message (8192 bytes), empty customization, 64 bytes" {
1520 const allocator = std.testing.allocator;
1521 const message = try generatePattern(allocator, 8192);
1522 defer allocator.free(message);
1523
1524 var output: [64]u8 = undefined;
1525 try KT256.hash(message, &output, .{});
1526
1527 var expected: [64]u8 = undefined;
1528 _ = try std.fmt.hexToBytes(&expected, "C6EE8E2AD3200C018AC87AAA031CDAC22121B412D07DC6E0DCCBB53423747E9A1C18834D99DF596CF0CF4B8DFAFB7BF02D139D0C9035725ADC1A01B7230A41FA");
1529 try std.testing.expectEqualSlices(u8, &expected, &output);
1530}
1531
1532test "KT128: pattern message (8193 bytes), empty customization, 32 bytes" {
1533 const allocator = std.testing.allocator;
1534 const message = try generatePattern(allocator, 8193);
1535 defer allocator.free(message);
1536
1537 var output: [32]u8 = undefined;
1538 try KT128.hash(message, &output, .{});
1539
1540 var expected: [32]u8 = undefined;
1541 _ = try std.fmt.hexToBytes(&expected, "BB66FE72EAEA5179418D5295EE1344854D8AD7F3FA17EFCB467EC152341284CF");
1542 try std.testing.expectEqualSlices(u8, &expected, &output);
1543}
1544
1545test "KT128: pattern message (16384 bytes), empty customization, 32 bytes" {
1546 const allocator = std.testing.allocator;
1547 const message = try generatePattern(allocator, 16384);
1548 defer allocator.free(message);
1549
1550 var output: [32]u8 = undefined;
1551 try KT128.hash(message, &output, .{});
1552
1553 var expected: [32]u8 = undefined;
1554 _ = try std.fmt.hexToBytes(&expected, "82778F7F7234C83352E76837B721FBDBB5270B88010D84FA5AB0B61EC8CE0956");
1555 try std.testing.expectEqualSlices(u8, &expected, &output);
1556}
1557
1558test "KT128: pattern message (16385 bytes), empty customization, 32 bytes" {
1559 const allocator = std.testing.allocator;
1560 const message = try generatePattern(allocator, 16385);
1561 defer allocator.free(message);
1562
1563 var output: [32]u8 = undefined;
1564 try KT128.hash(message, &output, .{});
1565
1566 var expected: [32]u8 = undefined;
1567 _ = try std.fmt.hexToBytes(&expected, "5F8D2B943922B451842B4E82740D02369E2D5F9F33C5123509A53B955FE177B2");
1568 try std.testing.expectEqualSlices(u8, &expected, &output);
1569}
1570
1571test "KT256: pattern message (8193 bytes), empty customization, 64 bytes" {
1572 const allocator = std.testing.allocator;
1573 const message = try generatePattern(allocator, 8193);
1574 defer allocator.free(message);
1575
1576 var output: [64]u8 = undefined;
1577 try KT256.hash(message, &output, .{});
1578
1579 var expected: [64]u8 = undefined;
1580 _ = try std.fmt.hexToBytes(&expected, "65FF03335900E5197ACBD5F41B797F0E7E36AD4FF7D89C09FA6F28AE58D1E8BC2DF1779B86F988C3B13690172914EA172423B23EF4057255BB0836AB3A99836E");
1581 try std.testing.expectEqualSlices(u8, &expected, &output);
1582}
1583
1584test "KT256: pattern message (16384 bytes), empty customization, 64 bytes" {
1585 const allocator = std.testing.allocator;
1586 const message = try generatePattern(allocator, 16384);
1587 defer allocator.free(message);
1588
1589 var output: [64]u8 = undefined;
1590 try KT256.hash(message, &output, .{});
1591
1592 var expected: [64]u8 = undefined;
1593 _ = try std.fmt.hexToBytes(&expected, "74604239A14847CB79069B4FF0E51070A93034C9AC4DFF4D45E0F2C5DA81D930DE6055C2134B4DF4E49F27D1B2C66E95491858B182A924BD0504DA5976BC516D");
1594 try std.testing.expectEqualSlices(u8, &expected, &output);
1595}
1596
1597test "KT256: pattern message (16385 bytes), empty customization, 64 bytes" {
1598 const allocator = std.testing.allocator;
1599 const message = try generatePattern(allocator, 16385);
1600 defer allocator.free(message);
1601
1602 var output: [64]u8 = undefined;
1603 try KT256.hash(message, &output, .{});
1604
1605 var expected: [64]u8 = undefined;
1606 _ = try std.fmt.hexToBytes(&expected, "C814F23132DADBFD55379F18CB988CB39B751F119322823FD982644A897485397B9F40EB11C6E416359B8AE695A5CE0FA79D1ADA1EEC745D82E0A5AB08A9F014");
1607 try std.testing.expectEqualSlices(u8, &expected, &output);
1608}
1609
1610test "KT128 incremental: empty message matches one-shot" {
1611 var output_oneshot: [32]u8 = undefined;
1612 var output_incremental: [32]u8 = undefined;
1613
1614 try KT128.hash(&[_]u8{}, &output_oneshot, .{});
1615
1616 var hasher = KT128.init(.{});
1617 hasher.final(&output_incremental);
1618
1619 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1620}
1621
1622test "KT128 incremental: small message matches one-shot" {
1623 const message = "Hello, KangarooTwelve!";
1624
1625 var output_oneshot: [32]u8 = undefined;
1626 var output_incremental: [32]u8 = undefined;
1627
1628 try KT128.hash(message, &output_oneshot, .{});
1629
1630 var hasher = KT128.init(.{});
1631 hasher.update(message);
1632 hasher.final(&output_incremental);
1633
1634 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1635}
1636
1637test "KT128 incremental: multiple updates match single update" {
1638 const part1 = "Hello, ";
1639 const part2 = "Kangaroo";
1640 const part3 = "Twelve!";
1641
1642 var output_single: [32]u8 = undefined;
1643 var output_multi: [32]u8 = undefined;
1644
1645 // Single update
1646 var hasher1 = KT128.init(.{});
1647 hasher1.update(part1 ++ part2 ++ part3);
1648 hasher1.final(&output_single);
1649
1650 // Multiple updates
1651 var hasher2 = KT128.init(.{});
1652 hasher2.update(part1);
1653 hasher2.update(part2);
1654 hasher2.update(part3);
1655 hasher2.final(&output_multi);
1656
1657 try std.testing.expectEqualSlices(u8, &output_single, &output_multi);
1658}
1659
1660test "KT128 incremental: exactly chunk_size matches one-shot" {
1661 const allocator = std.testing.allocator;
1662 const message = try allocator.alloc(u8, 8192);
1663 defer allocator.free(message);
1664 @memset(message, 0xAB);
1665
1666 var output_oneshot: [32]u8 = undefined;
1667 var output_incremental: [32]u8 = undefined;
1668
1669 try KT128.hash(message, &output_oneshot, .{});
1670
1671 var hasher = KT128.init(.{});
1672 hasher.update(message);
1673 hasher.final(&output_incremental);
1674
1675 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1676}
1677
1678test "KT128 incremental: larger than chunk_size matches one-shot" {
1679 const allocator = std.testing.allocator;
1680 const message = try generatePattern(allocator, 16384);
1681 defer allocator.free(message);
1682
1683 var output_oneshot: [32]u8 = undefined;
1684 var output_incremental: [32]u8 = undefined;
1685
1686 try KT128.hash(message, &output_oneshot, .{});
1687
1688 var hasher = KT128.init(.{});
1689 hasher.update(message);
1690 hasher.final(&output_incremental);
1691
1692 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1693}
1694
1695test "KT128 incremental: with customization matches one-shot" {
1696 const message = "Test message";
1697 const customization = "my custom domain";
1698
1699 var output_oneshot: [32]u8 = undefined;
1700 var output_incremental: [32]u8 = undefined;
1701
1702 try KT128.hash(message, &output_oneshot, .{ .customization = customization });
1703
1704 var hasher = KT128.init(.{ .customization = customization });
1705 hasher.update(message);
1706 hasher.final(&output_incremental);
1707
1708 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1709}
1710
1711test "KT128 incremental: large message with customization" {
1712 const allocator = std.testing.allocator;
1713 const message = try generatePattern(allocator, 20000);
1714 defer allocator.free(message);
1715 const customization = "test domain";
1716
1717 var output_oneshot: [48]u8 = undefined;
1718 var output_incremental: [48]u8 = undefined;
1719
1720 try KT128.hash(message, &output_oneshot, .{ .customization = customization });
1721
1722 var hasher = KT128.init(.{ .customization = customization });
1723 hasher.update(message);
1724 hasher.final(&output_incremental);
1725
1726 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1727}
1728
1729test "KT128 incremental: streaming chunks matches one-shot" {
1730 const allocator = std.testing.allocator;
1731 const message = try generatePattern(allocator, 25000);
1732 defer allocator.free(message);
1733
1734 var output_oneshot: [32]u8 = undefined;
1735 var output_incremental: [32]u8 = undefined;
1736
1737 try KT128.hash(message, &output_oneshot, .{});
1738
1739 var hasher = KT128.init(.{});
1740
1741 // Feed in 1KB chunks
1742 var offset: usize = 0;
1743 while (offset < message.len) {
1744 const chunk_size_local = @min(1024, message.len - offset);
1745 hasher.update(message[offset..][0..chunk_size_local]);
1746 offset += chunk_size_local;
1747 }
1748 hasher.final(&output_incremental);
1749
1750 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1751}
1752
1753test "KT256 incremental: empty message matches one-shot" {
1754 var output_oneshot: [64]u8 = undefined;
1755 var output_incremental: [64]u8 = undefined;
1756
1757 try KT256.hash(&[_]u8{}, &output_oneshot, .{});
1758
1759 var hasher = KT256.init(.{});
1760 hasher.final(&output_incremental);
1761
1762 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1763}
1764
1765test "KT256 incremental: small message matches one-shot" {
1766 const message = "Hello, KangarooTwelve with 256-bit security!";
1767
1768 var output_oneshot: [64]u8 = undefined;
1769 var output_incremental: [64]u8 = undefined;
1770
1771 try KT256.hash(message, &output_oneshot, .{});
1772
1773 var hasher = KT256.init(.{});
1774 hasher.update(message);
1775 hasher.final(&output_incremental);
1776
1777 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1778}
1779
1780test "KT256 incremental: large message matches one-shot" {
1781 const allocator = std.testing.allocator;
1782 const message = try generatePattern(allocator, 30000);
1783 defer allocator.free(message);
1784
1785 var output_oneshot: [64]u8 = undefined;
1786 var output_incremental: [64]u8 = undefined;
1787
1788 try KT256.hash(message, &output_oneshot, .{});
1789
1790 var hasher = KT256.init(.{});
1791 hasher.update(message);
1792 hasher.final(&output_incremental);
1793
1794 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1795}
1796
1797test "KT256 incremental: with customization matches one-shot" {
1798 const allocator = std.testing.allocator;
1799 const message = try generatePattern(allocator, 15000);
1800 defer allocator.free(message);
1801 const customization = "KT256 custom domain";
1802
1803 var output_oneshot: [80]u8 = undefined;
1804 var output_incremental: [80]u8 = undefined;
1805
1806 try KT256.hash(message, &output_oneshot, .{ .customization = customization });
1807
1808 var hasher = KT256.init(.{ .customization = customization });
1809 hasher.update(message);
1810 hasher.final(&output_incremental);
1811
1812 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1813}
1814
1815test "KT128 incremental: random small message with random chunk sizes" {
1816 const allocator = std.testing.allocator;
1817
1818 const test_sizes = [_]usize{ 100, 500, 2000, 5000, 10000 };
1819
1820 for (test_sizes) |total_size| {
1821 const message = try allocator.alloc(u8, total_size);
1822 defer allocator.free(message);
1823 crypto.random.bytes(message);
1824
1825 var output_oneshot: [32]u8 = undefined;
1826 var output_incremental: [32]u8 = undefined;
1827
1828 try KT128.hash(message, &output_oneshot, .{});
1829
1830 var hasher = KT128.init(.{});
1831 var offset: usize = 0;
1832
1833 while (offset < message.len) {
1834 const remaining = message.len - offset;
1835 const max_chunk = @min(1000, remaining);
1836 const chunk_size_local = if (max_chunk == 1) 1 else crypto.random.intRangeAtMost(usize, 1, max_chunk);
1837
1838 hasher.update(message[offset..][0..chunk_size_local]);
1839 offset += chunk_size_local;
1840 }
1841 hasher.final(&output_incremental);
1842
1843 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1844 }
1845}
1846
1847test "KT128 incremental: random large message (1MB) with random chunk sizes" {
1848 const allocator = std.testing.allocator;
1849
1850 const total_size: usize = 1024 * 1024; // 1 MB
1851 const message = try allocator.alloc(u8, total_size);
1852 defer allocator.free(message);
1853 crypto.random.bytes(message);
1854
1855 var output_oneshot: [32]u8 = undefined;
1856 var output_incremental: [32]u8 = undefined;
1857
1858 try KT128.hash(message, &output_oneshot, .{});
1859
1860 var hasher = KT128.init(.{});
1861 var offset: usize = 0;
1862
1863 while (offset < message.len) {
1864 const remaining = message.len - offset;
1865 const max_chunk = @min(10000, remaining);
1866 const chunk_size_local = if (max_chunk == 1) 1 else crypto.random.intRangeAtMost(usize, 1, max_chunk);
1867
1868 hasher.update(message[offset..][0..chunk_size_local]);
1869 offset += chunk_size_local;
1870 }
1871 hasher.final(&output_incremental);
1872
1873 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1874}
1875
1876test "KT256 incremental: random small message with random chunk sizes" {
1877 const allocator = std.testing.allocator;
1878
1879 const test_sizes = [_]usize{ 100, 500, 2000, 5000, 10000 };
1880
1881 for (test_sizes) |total_size| {
1882 // Generate random message
1883 const message = try allocator.alloc(u8, total_size);
1884 defer allocator.free(message);
1885 crypto.random.bytes(message);
1886
1887 var output_oneshot: [64]u8 = undefined;
1888 var output_incremental: [64]u8 = undefined;
1889
1890 try KT256.hash(message, &output_oneshot, .{});
1891
1892 var hasher = KT256.init(.{});
1893 var offset: usize = 0;
1894
1895 while (offset < message.len) {
1896 const remaining = message.len - offset;
1897 const max_chunk = @min(1000, remaining);
1898 const chunk_size_local = if (max_chunk == 1) 1 else crypto.random.intRangeAtMost(usize, 1, max_chunk);
1899
1900 hasher.update(message[offset..][0..chunk_size_local]);
1901 offset += chunk_size_local;
1902 }
1903 hasher.final(&output_incremental);
1904
1905 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1906 }
1907}
1908
1909test "KT256 incremental: random large message (1MB) with random chunk sizes" {
1910 const allocator = std.testing.allocator;
1911
1912 const total_size: usize = 1024 * 1024; // 1 MB
1913 const message = try allocator.alloc(u8, total_size);
1914 defer allocator.free(message);
1915 crypto.random.bytes(message);
1916
1917 var output_oneshot: [64]u8 = undefined;
1918 var output_incremental: [64]u8 = undefined;
1919
1920 try KT256.hash(message, &output_oneshot, .{});
1921
1922 var hasher = KT256.init(.{});
1923 var offset: usize = 0;
1924
1925 while (offset < message.len) {
1926 const remaining = message.len - offset;
1927 const max_chunk = @min(10000, remaining);
1928 const chunk_size_local = if (max_chunk == 1) 1 else crypto.random.intRangeAtMost(usize, 1, max_chunk);
1929
1930 hasher.update(message[offset..][0..chunk_size_local]);
1931 offset += chunk_size_local;
1932 }
1933 hasher.final(&output_incremental);
1934
1935 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1936}
1937
1938test "KT128 incremental: random message with customization and random chunks" {
1939 const allocator = std.testing.allocator;
1940
1941 const total_size: usize = 50000;
1942 const message = try allocator.alloc(u8, total_size);
1943 defer allocator.free(message);
1944 crypto.random.bytes(message);
1945
1946 const customization = "random test domain";
1947
1948 var output_oneshot: [48]u8 = undefined;
1949 var output_incremental: [48]u8 = undefined;
1950
1951 try KT128.hash(message, &output_oneshot, .{ .customization = customization });
1952
1953 var hasher = KT128.init(.{ .customization = customization });
1954 var offset: usize = 0;
1955
1956 while (offset < message.len) {
1957 const remaining = message.len - offset;
1958 const max_chunk = @min(5000, remaining);
1959 const chunk_size_local = if (max_chunk == 1) 1 else crypto.random.intRangeAtMost(usize, 1, max_chunk);
1960
1961 hasher.update(message[offset..][0..chunk_size_local]);
1962 offset += chunk_size_local;
1963 }
1964 hasher.final(&output_incremental);
1965
1966 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1967}
lib/std/crypto/sha3.zig-9
......@@ -4,8 +4,6 @@ const assert = std.debug.assert;
44const math = std.math;
55const mem = std.mem;
66
7const kangarootwelve = @import("kangarootwelve.zig");
8
97const KeccakState = std.crypto.core.keccak.State;
108
119pub const Sha3_224 = Keccak(1600, 224, 0x06, 24);
......@@ -28,9 +26,6 @@ pub const KMac256 = KMac(256);
2826pub const TupleHash128 = TupleHash(128);
2927pub const TupleHash256 = TupleHash(256);
3028
31pub const KT128 = kangarootwelve.KT128;
32pub const KT256 = kangarootwelve.KT256;
33
3429/// TurboSHAKE128 is a XOF (a secure hash function with a variable output length), with a 128 bit security level.
3530/// It is based on the same permutation as SHA3 and SHAKE128, but which much higher performance.
3631/// The delimiter is 0x1f by default, but can be changed for context-separation.
......@@ -486,10 +481,6 @@ pub const NistLengthEncoding = enum {
486481
487482const htest = @import("test.zig");
488483
489test {
490 _ = kangarootwelve;
491}
492
493484test "sha3-224 single" {
494485 try htest.assertEqualHash(Sha3_224, "6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7", "");
495486 try htest.assertEqualHash(Sha3_224, "e642824c3f8cf24ad09234ee7d3c766fc9a3a5168d0c94ad73b46fdf", "abc");