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