authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-11-01 15:03:43+01:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2025-11-01 14:03:43+00:00
log95c76b1b4aa7302966281c6b9b7f6cadea3cf7a6
treece61f52851efb5aa96d0beb349db996a51179340
parent4e943fc847befb477fbb76e42b199e1f61868a4a
signaturebadge-check Signed by PGP key B5690EEEBB952194

Add std.crypto.hash.sha3.{KT128,KT256} - RFC 9861. (#25593)

KT128 and KT256 are fast, secure cryptographic hash functions based on Keccak (SHA-3). They can be seen as the modern version of SHA-3, and evolution of SHAKE, with better performance. After the SHA-3 competition, the Keccak team proposed these variants in 2016, and the constructions underwent 8 years of public scrutiny before being standardized in October 2025 as RFC 9861. They uses a tree-hashing mode on top of TurboSHAKE, providing both high security and excellent performance, especially on large inputs. They support arbitrary-length output and optional customization strings. Hashing of very large inputs can be done using multiple threads, for high throughput. KT128 provides 128-bit security strength, equivalent to AES-128 and SHAKE128, which is sufficient for virtually all applications. KT256 provides 256-bit security strength, equivalent to SHA-512. For virtually all applications, KT128 is enough (equivalent to SHA-256 or BLAKE3). For small inputs, TurboSHAKE128 and TurboSHAKE256 (which KT128 and KT256 are based on) can be used instead as they have less overhead.

3 files changed, 1844 insertions(+), 0 deletions(-)

lib/std/crypto/benchmark.zig+1
......@@ -37,6 +37,7 @@ const hashes = [_]Crypto{
3737
3838const parallel_hashes = [_]Crypto{
3939 Crypto{ .ty = crypto.hash.Blake3, .name = "blake3-parallel" },
40 Crypto{ .ty = crypto.hash.sha3.KT128, .name = "kt128-parallel" },
4041};
4142
4243const block_size: usize = 8 * 8192;
lib/std/crypto/kangarootwelve.zig created+1834
......@@ -0,0 +1,1834 @@
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 inline for (RC) |rc| {
234 // θ (theta)
235 var C: [5]@Vector(N, u64) = undefined;
236 inline for (0..5) |x| {
237 C[x] = states[x][0] ^ states[x][1] ^ states[x][2] ^ states[x][3] ^ states[x][4];
238 }
239
240 var D: [5]@Vector(N, u64) = undefined;
241 inline for (0..5) |x| {
242 D[x] = C[(x + 4) % 5] ^ rol64Vec(N, C[(x + 1) % 5], 1);
243 }
244
245 // Apply D to all lanes
246 inline for (0..5) |x| {
247 states[x][0] ^= D[x];
248 states[x][1] ^= D[x];
249 states[x][2] ^= D[x];
250 states[x][3] ^= D[x];
251 states[x][4] ^= D[x];
252 }
253
254 // ρ (rho) and π (pi) - optimized with pre-computed offsets
255 var current = states[1][0];
256 var px: usize = 1;
257 var py: usize = 0;
258 inline for (rho_offsets) |rot| {
259 const next_y = (2 * px + 3 * py) % 5;
260 const next = states[py][next_y];
261 states[py][next_y] = rol64Vec(N, current, rot);
262 current = next;
263 px = py;
264 py = next_y;
265 }
266
267 // χ (chi) - optimized with better register usage
268 inline for (0..5) |y| {
269 const t0 = states[0][y];
270 const t1 = states[1][y];
271 const t2 = states[2][y];
272 const t3 = states[3][y];
273 const t4 = states[4][y];
274
275 states[0][y] = t0 ^ (~t1 & t2);
276 states[1][y] = t1 ^ (~t2 & t3);
277 states[2][y] = t2 ^ (~t3 & t4);
278 states[3][y] = t3 ^ (~t4 & t0);
279 states[4][y] = t4 ^ (~t0 & t1);
280 }
281
282 // ι (iota)
283 const rc_splat: @Vector(N, u64) = @splat(rc);
284 states[0][0] ^= rc_splat;
285 }
286}
287
288/// Add lanes from data to N states in parallel with stride - optimized version
289fn addLanesAll(
290 comptime N: usize,
291 states: *[5][5]@Vector(N, u64),
292 data: []const u8,
293 lane_count: usize,
294 lane_offset: usize,
295) void {
296
297 // Process lanes (at most 25 lanes in Keccak state)
298 inline for (0..25) |xy| {
299 if (xy < lane_count) {
300 const x = xy % 5;
301 const y = xy / 5;
302
303 // Load N lanes with stride - optimized memory access pattern
304 var loaded_data: @Vector(N, u64) = undefined;
305 inline for (0..N) |i| {
306 loaded_data[i] = load64(data[8 * (i * lane_offset + xy) ..]);
307 }
308 states[x][y] ^= loaded_data;
309 }
310 }
311}
312
313/// Apply Keccak-p[1600,12] to a single state (byte representation)
314fn keccakP(state: *[200]u8) void {
315 @setEvalBranchQuota(10000);
316 var lanes: [5][5]u64 = undefined;
317
318 // Load state into lanes
319 inline for (0..5) |x| {
320 inline for (0..5) |y| {
321 lanes[x][y] = load64(state[8 * (x + 5 * y) ..]);
322 }
323 }
324
325 // Apply 12 rounds
326 inline for (RC) |rc| {
327 // θ
328 var C: [5]u64 = undefined;
329 inline for (0..5) |x| {
330 C[x] = lanes[x][0] ^ lanes[x][1] ^ lanes[x][2] ^ lanes[x][3] ^ lanes[x][4];
331 }
332 var D: [5]u64 = undefined;
333 inline for (0..5) |x| {
334 D[x] = C[(x + 4) % 5] ^ std.math.rotl(u64, C[(x + 1) % 5], 1);
335 }
336 inline for (0..5) |x| {
337 inline for (0..5) |y| {
338 lanes[x][y] ^= D[x];
339 }
340 }
341
342 // ρ and π
343 var current = lanes[1][0];
344 var px: usize = 1;
345 var py: usize = 0;
346 inline for (0..24) |t| {
347 const temp = lanes[py][(2 * px + 3 * py) % 5];
348 const rot_amount = ((t + 1) * (t + 2) / 2) % 64;
349 lanes[py][(2 * px + 3 * py) % 5] = std.math.rotl(u64, current, @as(u6, @intCast(rot_amount)));
350 current = temp;
351 const temp_x = py;
352 py = (2 * px + 3 * py) % 5;
353 px = temp_x;
354 }
355
356 // χ
357 inline for (0..5) |y| {
358 const T = [5]u64{ lanes[0][y], lanes[1][y], lanes[2][y], lanes[3][y], lanes[4][y] };
359 inline for (0..5) |x| {
360 lanes[x][y] = T[x] ^ (~T[(x + 1) % 5] & T[(x + 2) % 5]);
361 }
362 }
363
364 // ι
365 lanes[0][0] ^= rc;
366 }
367
368 // Store lanes back to state
369 inline for (0..5) |x| {
370 inline for (0..5) |y| {
371 store64(lanes[x][y], state[8 * (x + 5 * y) ..]);
372 }
373 }
374}
375
376/// Apply Keccak-p[1600,12] to a single state (u64 lane representation)
377fn keccakPLanes(lanes: *[25]u64) void {
378 @setEvalBranchQuota(10000);
379
380 // Apply 12 rounds
381 inline for (RC) |rc| {
382 // θ
383 var C: [5]u64 = undefined;
384 inline for (0..5) |x| {
385 C[x] = lanes[x] ^ lanes[x + 5] ^ lanes[x + 10] ^ lanes[x + 15] ^ lanes[x + 20];
386 }
387 var D: [5]u64 = undefined;
388 inline for (0..5) |x| {
389 D[x] = C[(x + 4) % 5] ^ std.math.rotl(u64, C[(x + 1) % 5], 1);
390 }
391 inline for (0..5) |x| {
392 inline for (0..5) |y| {
393 lanes[x + 5 * y] ^= D[x];
394 }
395 }
396
397 // ρ and π
398 var current = lanes[1];
399 var px: usize = 1;
400 var py: usize = 0;
401 inline for (0..24) |t| {
402 const next_y = (2 * px + 3 * py) % 5;
403 const next_idx = py + 5 * next_y;
404 const temp = lanes[next_idx];
405 const rot_amount = ((t + 1) * (t + 2) / 2) % 64;
406 lanes[next_idx] = std.math.rotl(u64, current, @as(u6, @intCast(rot_amount)));
407 current = temp;
408 px = py;
409 py = next_y;
410 }
411
412 // χ
413 inline for (0..5) |y| {
414 const idx = 5 * y;
415 const T = [5]u64{ lanes[idx], lanes[idx + 1], lanes[idx + 2], lanes[idx + 3], lanes[idx + 4] };
416 inline for (0..5) |x| {
417 lanes[idx + x] = T[x] ^ (~T[(x + 1) % 5] & T[(x + 2) % 5]);
418 }
419 }
420
421 // ι
422 lanes[0] ^= rc;
423 }
424}
425
426/// Generic non-allocating TurboSHAKE: write output to provided buffer
427fn turboSHAKEMultiSliceToBuffer(
428 comptime rate: usize,
429 view: *const MultiSliceView,
430 separation_byte: u8,
431 output: []u8,
432) void {
433 var state: [200]u8 = @splat(0);
434 var state_pos: usize = 0;
435
436 // Absorb all bytes from the multi-slice view
437 const total = view.totalLen();
438 var pos: usize = 0;
439 while (pos < total) {
440 state[state_pos] ^= view.getByte(pos);
441 state_pos += 1;
442 pos += 1;
443
444 if (state_pos == rate) {
445 keccakP(&state);
446 state_pos = 0;
447 }
448 }
449
450 // Add separation byte and padding
451 state[state_pos] ^= separation_byte;
452 state[rate - 1] ^= 0x80;
453 keccakP(&state);
454
455 // Squeeze
456 var out_offset: usize = 0;
457 while (out_offset < output.len) {
458 const chunk = @min(rate, output.len - out_offset);
459 @memcpy(output[out_offset..][0..chunk], state[0..chunk]);
460 out_offset += chunk;
461 if (out_offset < output.len) {
462 keccakP(&state);
463 }
464 }
465}
466
467/// Generic allocating TurboSHAKE
468fn turboSHAKEMultiSlice(
469 comptime rate: usize,
470 allocator: Allocator,
471 view: *const MultiSliceView,
472 separation_byte: u8,
473 output_len: usize,
474) ![]u8 {
475 const output = try allocator.alloc(u8, output_len);
476 turboSHAKEMultiSliceToBuffer(rate, view, separation_byte, output);
477 return output;
478}
479
480/// Non-allocating TurboSHAKE128: write output to provided buffer
481fn turboSHAKE128MultiSliceToBuffer(
482 view: *const MultiSliceView,
483 separation_byte: u8,
484 output: []u8,
485) void {
486 turboSHAKEMultiSliceToBuffer(168, view, separation_byte, output);
487}
488
489/// Allocating TurboSHAKE128
490fn turboSHAKE128MultiSlice(
491 allocator: Allocator,
492 view: *const MultiSliceView,
493 separation_byte: u8,
494 output_len: usize,
495) ![]u8 {
496 return turboSHAKEMultiSlice(168, allocator, view, separation_byte, output_len);
497}
498
499/// Non-allocating TurboSHAKE256: write output to provided buffer
500fn turboSHAKE256MultiSliceToBuffer(
501 view: *const MultiSliceView,
502 separation_byte: u8,
503 output: []u8,
504) void {
505 turboSHAKEMultiSliceToBuffer(136, view, separation_byte, output);
506}
507
508/// Allocating TurboSHAKE256
509fn turboSHAKE256MultiSlice(
510 allocator: Allocator,
511 view: *const MultiSliceView,
512 separation_byte: u8,
513 output_len: usize,
514) ![]u8 {
515 return turboSHAKEMultiSlice(136, allocator, view, separation_byte, output_len);
516}
517
518/// Process N leaves (8KiB chunks) in parallel - generic version
519fn processLeaves(
520 comptime Variant: type,
521 comptime N: usize,
522 data: []const u8,
523 result: *[N * Variant.cv_size]u8,
524) void {
525 const rate_in_lanes: usize = Variant.rate_in_lanes;
526 const rate_in_bytes: usize = rate_in_lanes * 8;
527 const cv_size: usize = Variant.cv_size;
528
529 // Initialize N all-zero states with cache alignment
530 var states: [5][5]@Vector(N, u64) align(cache_line_size) = undefined;
531 inline for (0..5) |x| {
532 inline for (0..5) |y| {
533 states[x][y] = @splat(0);
534 }
535 }
536
537 // Process complete blocks
538 var j: usize = 0;
539 while (j + rate_in_bytes <= chunk_size) : (j += rate_in_bytes) {
540 addLanesAll(N, &states, data[j..], rate_in_lanes, chunk_size / 8);
541 keccakP1600timesN(N, &states);
542 }
543
544 // Process last incomplete block
545 const remaining_lanes = (chunk_size - j) / 8;
546 if (remaining_lanes > 0) {
547 addLanesAll(N, &states, data[j..], remaining_lanes, chunk_size / 8);
548 }
549
550 // Add suffix 0x0B and padding
551 const suffix_pos = Variant.separation_byte_pos;
552 const padding_pos = Variant.padding_pos;
553
554 const suffix_splat: @Vector(N, u64) = @splat(0x0B);
555 states[suffix_pos.x][suffix_pos.y] ^= suffix_splat;
556 const padding_splat: @Vector(N, u64) = @splat(0x8000000000000000);
557 states[padding_pos.x][padding_pos.y] ^= padding_splat;
558
559 keccakP1600timesN(N, &states);
560
561 // Extract chaining values from each state
562 const lanes_to_extract = cv_size / 8;
563 comptime var lane_idx: usize = 0;
564 inline while (lane_idx < lanes_to_extract) : (lane_idx += 1) {
565 const x = lane_idx % 5;
566 const y = lane_idx / 5;
567 inline for (0..N) |i| {
568 store64(states[x][y][i], result[i * cv_size + lane_idx * 8 ..]);
569 }
570 }
571}
572
573/// Context for processing a batch of leaves in a thread
574const LeafBatchContext = struct {
575 output_cvs: []u8,
576 batch_start: usize,
577 batch_count: usize,
578 view: *const MultiSliceView,
579 scratch_buffer: []u8, // Pre-allocated scratch space (no allocations in worker)
580 total_len: usize, // Total length of input data (for boundary checking)
581};
582
583/// Helper function to process N leaves in parallel, reducing code duplication
584inline fn processNLeaves(
585 comptime Variant: type,
586 comptime N: usize,
587 view: *const MultiSliceView,
588 j: usize,
589 leaf_buffer: []u8,
590 output: []u8,
591) void {
592 const cv_size = Variant.cv_size;
593 if (view.tryGetSlice(j, j + N * chunk_size)) |leaf_data| {
594 var leaf_cvs: [N * cv_size]u8 = undefined;
595 processLeaves(Variant, N, leaf_data, &leaf_cvs);
596 @memcpy(output[0..leaf_cvs.len], &leaf_cvs);
597 } else {
598 view.copyRange(j, j + N * chunk_size, leaf_buffer[0 .. N * chunk_size]);
599 var leaf_cvs: [N * cv_size]u8 = undefined;
600 processLeaves(Variant, N, leaf_buffer[0 .. N * chunk_size], &leaf_cvs);
601 @memcpy(output[0..leaf_cvs.len], &leaf_cvs);
602 }
603}
604
605/// Process a batch of leaves in a single thread using SIMD
606fn processLeafBatch(comptime Variant: type, ctx: LeafBatchContext) void {
607 const cv_size = Variant.cv_size;
608 const leaf_buffer = ctx.scratch_buffer[0 .. 8 * chunk_size];
609 const cv_scratch = ctx.scratch_buffer[8 * chunk_size .. 8 * chunk_size + cv_size];
610
611 var cvs_offset: usize = 0;
612 var j: usize = ctx.batch_start;
613 const batch_end = @min(ctx.batch_start + ctx.batch_count * chunk_size, ctx.total_len);
614
615 // Process leaves using SIMD (8x, 4x, 2x) based on optimal vector length
616 inline for ([_]usize{ 8, 4, 2 }) |batch_size| {
617 while (optimal_vector_len >= batch_size and j + batch_size * chunk_size <= batch_end) {
618 processNLeaves(Variant, batch_size, ctx.view, j, leaf_buffer, ctx.output_cvs[cvs_offset..]);
619 cvs_offset += batch_size * cv_size;
620 j += batch_size * chunk_size;
621 }
622 }
623
624 // Process remaining single leaves
625 while (j < batch_end) {
626 const chunk_len = @min(chunk_size, batch_end - j);
627 if (ctx.view.tryGetSlice(j, j + chunk_len)) |leaf_data| {
628 const cv_slice = MultiSliceView.init(leaf_data, &[_]u8{}, &[_]u8{});
629 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_scratch[0..cv_size]);
630 @memcpy(ctx.output_cvs[cvs_offset..][0..cv_size], cv_scratch[0..cv_size]);
631 } else {
632 ctx.view.copyRange(j, j + chunk_len, leaf_buffer[0..chunk_len]);
633 const cv_slice = MultiSliceView.init(leaf_buffer[0..chunk_len], &[_]u8{}, &[_]u8{});
634 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_scratch[0..cv_size]);
635 @memcpy(ctx.output_cvs[cvs_offset..][0..cv_size], cv_scratch[0..cv_size]);
636 }
637 cvs_offset += cv_size;
638 j += chunk_size;
639 }
640}
641
642/// Helper to process N leaves in SIMD and absorb CVs into state
643inline fn processAndAbsorbNLeaves(
644 comptime Variant: type,
645 comptime N: usize,
646 view: *const MultiSliceView,
647 j: usize,
648 leaf_buffer: []u8,
649 final_state: anytype,
650) void {
651 const cv_size = Variant.cv_size;
652 if (view.tryGetSlice(j, j + N * chunk_size)) |leaf_data| {
653 var leaf_cvs: [N * cv_size]u8 align(cache_line_size) = undefined;
654 processLeaves(Variant, N, leaf_data, &leaf_cvs);
655 final_state.update(&leaf_cvs);
656 } else {
657 view.copyRange(j, j + N * chunk_size, leaf_buffer[0 .. N * chunk_size]);
658 var leaf_cvs: [N * cv_size]u8 align(cache_line_size) = undefined;
659 processLeaves(Variant, N, leaf_buffer[0 .. N * chunk_size], &leaf_cvs);
660 final_state.update(&leaf_cvs);
661 }
662}
663
664/// Generic single-threaded implementation
665fn ktSingleThreaded(comptime Variant: type, view: *const MultiSliceView, total_len: usize, output: []u8) void {
666 const cv_size = Variant.cv_size;
667 const StateType = Variant.StateType;
668
669 // Initialize streaming TurboSHAKE state for final node (delimiter 0x06 is set in the type)
670 var final_state = StateType.init(.{});
671
672 // Absorb first B bytes from input
673 var first_b_buffer: [chunk_size]u8 = undefined;
674 if (view.tryGetSlice(0, chunk_size)) |first_chunk| {
675 final_state.update(first_chunk);
676 } else {
677 view.copyRange(0, chunk_size, &first_b_buffer);
678 final_state.update(&first_b_buffer);
679 }
680
681 // Absorb padding bytes (8 bytes: 0x03 followed by 7 zeros)
682 const padding = [_]u8{ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
683 final_state.update(&padding);
684
685 var j: usize = chunk_size;
686 var n: usize = 0;
687
688 // Temporary buffers for boundary-spanning leaves and CV computation
689 var leaf_buffer: [chunk_size * 8]u8 align(cache_line_size) = undefined;
690 var cv_buffer: [64]u8 = undefined; // Max CV size is 64 bytes
691
692 // Process leaves in SIMD batches (8x, 4x, 2x)
693 inline for ([_]usize{ 8, 4, 2 }) |batch_size| {
694 while (optimal_vector_len >= batch_size and j + batch_size * chunk_size <= total_len) {
695 processAndAbsorbNLeaves(Variant, batch_size, view, j, &leaf_buffer, &final_state);
696 j += batch_size * chunk_size;
697 n += batch_size;
698 }
699 }
700
701 // Process remaining leaves one at a time
702 while (j < total_len) {
703 const chunk_len = @min(chunk_size, total_len - j);
704 if (view.tryGetSlice(j, j + chunk_len)) |leaf_data| {
705 const cv_slice = MultiSliceView.init(leaf_data, &[_]u8{}, &[_]u8{});
706 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_buffer[0..cv_size]);
707 final_state.update(cv_buffer[0..cv_size]); // Absorb CV immediately
708 } else {
709 view.copyRange(j, j + chunk_len, leaf_buffer[0..chunk_len]);
710 const cv_slice = MultiSliceView.init(leaf_buffer[0..chunk_len], &[_]u8{}, &[_]u8{});
711 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_buffer[0..cv_size]);
712 final_state.update(cv_buffer[0..cv_size]);
713 }
714 j += chunk_size;
715 n += 1;
716 }
717
718 // Absorb right_encode(n) and terminator
719 const n_enc = rightEncode(n);
720 final_state.update(n_enc.slice());
721 const terminator = [_]u8{ 0xFF, 0xFF };
722 final_state.update(&terminator);
723
724 // Finalize and squeeze output
725 final_state.final(output);
726}
727
728/// Generic multi-threaded implementation
729fn ktMultiThreaded(
730 comptime Variant: type,
731 allocator: Allocator,
732 io: Io,
733 view: *const MultiSliceView,
734 total_len: usize,
735 output: []u8,
736) !void {
737 const cv_size = Variant.cv_size;
738
739 // Calculate total number of leaves
740 const total_leaves: usize = (total_len - 1) / chunk_size;
741
742 // Check if we have enough threads to benefit from parallelization
743 const thread_count = Thread.getCpuCount() catch 1;
744 if (thread_count <= 1) {
745 // Single-threaded fallback - more efficient than using group.async
746 ktSingleThreaded(Variant, view, total_len, output);
747 return;
748 }
749
750 // Allocate buffer for all chaining values
751 const cvs = try allocator.alloc(u8, total_leaves * cv_size);
752 defer allocator.free(cvs);
753
754 // Divide work among threads
755 const leaves_per_thread = (total_leaves + thread_count - 1) / thread_count;
756
757 // Pre-allocate scratch buffers for all threads (8 leaves + CV size)
758 const scratch_size = 8 * chunk_size + cv_size;
759 const all_scratch = try allocator.alloc(u8, thread_count * scratch_size);
760 defer allocator.free(all_scratch);
761
762 var group: Io.Group = .init;
763 var leaves_assigned: usize = 0;
764 var thread_idx: usize = 0;
765
766 while (leaves_assigned < total_leaves) {
767 const batch_count = @min(leaves_per_thread, total_leaves - leaves_assigned);
768 const batch_start = chunk_size + leaves_assigned * chunk_size;
769 const cvs_offset = leaves_assigned * cv_size;
770
771 const ctx = LeafBatchContext{
772 .output_cvs = cvs[cvs_offset .. cvs_offset + batch_count * cv_size],
773 .batch_start = batch_start,
774 .batch_count = batch_count,
775 .view = view,
776 .scratch_buffer = all_scratch[thread_idx * scratch_size .. (thread_idx + 1) * scratch_size],
777 .total_len = total_len,
778 };
779
780 group.async(io, struct {
781 fn process(c: LeafBatchContext) void {
782 processLeafBatch(Variant, c);
783 }
784 }.process, .{ctx});
785
786 leaves_assigned += batch_count;
787 thread_idx += 1;
788 }
789
790 // Wait for all threads to complete
791 group.wait(io);
792
793 // Build final node
794 const n_enc = rightEncode(total_leaves);
795 const final_node_len = chunk_size + 8 + total_leaves * cv_size + n_enc.len + 2;
796 const final_node = try allocator.alloc(u8, final_node_len);
797 defer allocator.free(final_node);
798
799 // Copy first B bytes
800 if (view.tryGetSlice(0, chunk_size)) |first_chunk| {
801 @memcpy(final_node[0..chunk_size], first_chunk);
802 } else {
803 view.copyRange(0, chunk_size, final_node[0..chunk_size]);
804 }
805
806 @memset(final_node[chunk_size..][0..8], 0);
807 final_node[chunk_size] = 0x03;
808 @memcpy(final_node[chunk_size + 8 ..][0 .. total_leaves * cv_size], cvs);
809 @memcpy(final_node[chunk_size + 8 + total_leaves * cv_size ..][0..n_enc.len], n_enc.slice());
810 final_node[final_node_len - 2] = 0xFF;
811 final_node[final_node_len - 1] = 0xFF;
812
813 const final_view = MultiSliceView.init(final_node, &[_]u8{}, &[_]u8{});
814 Variant.turboSHAKEToBuffer(&final_view, 0x06, output);
815}
816
817/// Generic KangarooTwelve hash function builder.
818/// Creates a public API type with hash and hashParallel methods for a specific variant.
819fn KTHash(
820 comptime Variant: type,
821 comptime singleChunkFn: fn (*const MultiSliceView, u8, []u8) void,
822) type {
823 return struct {
824 const Self = @This();
825 const StateType = Variant.StateType;
826
827 /// The recommended output length, in bytes.
828 pub const digest_length = Variant.security_level / 8 * 2;
829 /// The block length, or rate, in bytes.
830 pub const block_length = Variant.rate;
831
832 /// Options for KangarooTwelve can include a customization string for domain separation.
833 pub const Options = struct {
834 customization: ?[]const u8 = null,
835 };
836
837 // Message buffer (accumulates message data only, not customization)
838 buffer: [chunk_size]u8,
839 buffer_len: usize,
840 message_len: usize,
841
842 // Customization string (fixed at init)
843 customization: []const u8,
844 custom_len_enc: RightEncoded,
845
846 // Tree mode state (lazy initialization when buffer overflows first time)
847 first_chunk: ?[chunk_size]u8, // Saved first chunk for tree mode
848 final_state: ?StateType, // Running TurboSHAKE state for final node
849 num_leaves: usize, // Count of leaves processed (after first chunk)
850
851 /// Initialize a KangarooTwelve hashing context.
852 /// The customization string is optional and used for domain separation.
853 pub fn init(options: Options) Self {
854 const custom = options.customization orelse &[_]u8{};
855 return .{
856 .buffer = undefined,
857 .buffer_len = 0,
858 .message_len = 0,
859 .customization = custom,
860 .custom_len_enc = rightEncode(custom.len),
861 .first_chunk = null,
862 .final_state = null,
863 .num_leaves = 0,
864 };
865 }
866
867 /// Absorb data into the hash state.
868 /// Can be called multiple times to incrementally add data.
869 pub fn update(self: *Self, data: []const u8) void {
870 if (data.len == 0) return;
871
872 var remaining = data;
873
874 while (remaining.len > 0) {
875 const space_in_buffer = chunk_size - self.buffer_len;
876 const to_copy = @min(space_in_buffer, remaining.len);
877
878 // Copy data into buffer
879 @memcpy(self.buffer[self.buffer_len..][0..to_copy], remaining[0..to_copy]);
880 self.buffer_len += to_copy;
881 self.message_len += to_copy;
882 remaining = remaining[to_copy..];
883
884 // If buffer is full, process it
885 if (self.buffer_len == chunk_size) {
886 if (self.first_chunk == null) {
887 // First time buffer fills - initialize tree mode
888 self.first_chunk = self.buffer;
889 self.final_state = StateType.init(.{});
890
891 // Absorb first chunk into final state
892 self.final_state.?.update(&self.buffer);
893
894 // Absorb padding (8 bytes: 0x03 followed by 7 zeros)
895 const padding = [_]u8{ 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
896 self.final_state.?.update(&padding);
897 } else {
898 // Subsequent chunks - process as leaf and absorb CV
899 const cv_size = Variant.cv_size;
900 var cv_buffer: [64]u8 = undefined; // Max CV size
901 const cv_slice = MultiSliceView.init(&self.buffer, &[_]u8{}, &[_]u8{});
902 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_buffer[0..cv_size]);
903
904 // Absorb CV into final state immediately
905 self.final_state.?.update(cv_buffer[0..cv_size]);
906 self.num_leaves += 1;
907 }
908 self.buffer_len = 0;
909 }
910 }
911 }
912
913 /// Finalize the hash and produce output.
914 /// After calling this, the context should not be reused.
915 pub fn final(self: *Self, out: []u8) void {
916 const cv_size = Variant.cv_size;
917
918 // Calculate total length: message + customization + right_encode(customization.len)
919 const total_len = self.message_len + self.customization.len + self.custom_len_enc.len;
920
921 // Single chunk mode: total data fits in one chunk
922 if (total_len <= chunk_size) {
923 // Build the complete input: buffer + customization + encoded length
924 var single_chunk: [chunk_size]u8 = undefined;
925 @memcpy(single_chunk[0..self.buffer_len], self.buffer[0..self.buffer_len]);
926 @memcpy(single_chunk[self.buffer_len..][0..self.customization.len], self.customization);
927 @memcpy(single_chunk[self.buffer_len + self.customization.len ..][0..self.custom_len_enc.len], self.custom_len_enc.slice());
928
929 const view = MultiSliceView.init(single_chunk[0..total_len], &[_]u8{}, &[_]u8{});
930 singleChunkFn(&view, 0x07, out);
931 return;
932 }
933
934 // Tree mode: we've already absorbed first_chunk + padding + intermediate CVs
935 // Now handle remaining buffer data
936 const remaining_with_custom_len = self.buffer_len + self.customization.len + self.custom_len_enc.len;
937 var final_leaves = self.num_leaves;
938
939 if (remaining_with_custom_len > 0) {
940 // Build final leaf data with customization
941 var final_leaf_buffer: [chunk_size + 256]u8 = undefined; // Extra space for customization
942 @memcpy(final_leaf_buffer[0..self.buffer_len], self.buffer[0..self.buffer_len]);
943 @memcpy(final_leaf_buffer[self.buffer_len..][0..self.customization.len], self.customization);
944 @memcpy(final_leaf_buffer[self.buffer_len + self.customization.len ..][0..self.custom_len_enc.len], self.custom_len_enc.slice());
945
946 // Generate CV for final leaf and absorb it
947 var cv_buffer: [64]u8 = undefined; // Max CV size
948 const cv_slice = MultiSliceView.init(final_leaf_buffer[0..remaining_with_custom_len], &[_]u8{}, &[_]u8{});
949 Variant.turboSHAKEToBuffer(&cv_slice, 0x0B, cv_buffer[0..cv_size]);
950 self.final_state.?.update(cv_buffer[0..cv_size]);
951 final_leaves += 1;
952 }
953
954 // Absorb right_encode(num_leaves) and terminator
955 const n_enc = rightEncode(final_leaves);
956 self.final_state.?.update(n_enc.slice());
957 const terminator = [_]u8{ 0xFF, 0xFF };
958 self.final_state.?.update(&terminator);
959
960 // Squeeze output
961 self.final_state.?.final(out);
962 }
963
964 /// Hash a message using sequential processing with SIMD acceleration.
965 /// Best performance for inputs under 10MB. Never allocates memory.
966 ///
967 /// Parameters:
968 /// - message: Input data to hash (any length)
969 /// - out: Output buffer (any length, arbitrary output sizes supported)
970 /// - options: Optional settings including customization string for domain separation
971 pub fn hash(message: []const u8, out: []u8, options: Options) !void {
972 const custom = options.customization orelse &[_]u8{};
973
974 // Right-encode customization length
975 const custom_len_enc = rightEncode(custom.len);
976
977 // Create zero-copy multi-slice view (no concatenation)
978 const view = MultiSliceView.init(message, custom, custom_len_enc.slice());
979 const total_len = view.totalLen();
980
981 // Single chunk case - zero-copy absorption!
982 if (total_len <= chunk_size) {
983 singleChunkFn(&view, 0x07, out);
984 return;
985 }
986
987 // Tree mode - single-threaded SIMD processing
988 ktSingleThreaded(Variant, &view, total_len, out);
989 }
990
991 /// Hash with automatic parallelization for large inputs (>2MB).
992 /// Automatically uses sequential processing for smaller inputs to avoid thread overhead.
993 /// Allocator required for temporary buffers. IO object required for thread management.
994 pub fn hashParallel(message: []const u8, out: []u8, options: Options, allocator: Allocator, io: Io) !void {
995 const custom = options.customization orelse &[_]u8{};
996
997 const custom_len_enc = rightEncode(custom.len);
998 const view = MultiSliceView.init(message, custom, custom_len_enc.slice());
999 const total_len = view.totalLen();
1000
1001 // Single chunk case
1002 if (total_len <= chunk_size) {
1003 singleChunkFn(&view, 0x07, out);
1004 return;
1005 }
1006
1007 // Use single-threaded processing if below threshold
1008 if (total_len < large_file_threshold) {
1009 ktSingleThreaded(Variant, &view, total_len, out);
1010 return;
1011 }
1012
1013 // Tree mode - multi-threaded processing
1014 try ktMultiThreaded(Variant, allocator, io, &view, total_len, out);
1015 }
1016 };
1017}
1018
1019/// KangarooTwelve is a fast, secure cryptographic hash function that uses tree-hashing
1020/// on top of TurboSHAKE. It is built on the Keccak permutation, the same primitive
1021/// underlying SHA-3, which has undergone over 15 years of intensive cryptanalysis
1022/// since the SHA-3 competition (2008-2012) and remains secure.
1023///
1024/// K12 uses Keccak-p[1600,12] with 12 rounds (half of SHA-3's 24 rounds), providing
1025/// 128-bit security strength equivalent to AES-128 and SHAKE128. While this offers
1026/// less conservative margin than SHA-3, current cryptanalysis reaches only 6 rounds,
1027/// leaving a substantial security margin. This deliberate trade-off delivers
1028/// significantly better performance while maintaining strong practical security.
1029///
1030/// Standardized as RFC 9861 after 8 years of public scrutiny. Supports arbitrary-length
1031/// output and optional customization strings for domain separation.
1032pub const KT128 = KTHash(KT128Variant, turboSHAKE128MultiSliceToBuffer);
1033
1034/// KangarooTwelve is a fast, secure cryptographic hash function that uses tree-hashing
1035/// on top of TurboSHAKE. It is built on the Keccak permutation, the same primitive
1036/// underlying SHA-3, which has undergone over 15 years of intensive cryptanalysis
1037/// since the SHA-3 competition (2008-2012) and remains secure.
1038///
1039/// KT256 provides 256-bit security strength and achieves NIST post-quantum security
1040/// level 2 when using at least 256-bit outputs. Like KT128, it uses Keccak-p[1600,12]
1041/// with 12 rounds, offering a deliberate trade-off between conservative margin and
1042/// significantly better performance while maintaining strong practical security.
1043///
1044/// Use KT256 when you need extra conservative margins.
1045/// For most applications, KT128 offers better performance with adequate security.
1046pub const KT256 = KTHash(KT256Variant, turboSHAKE256MultiSliceToBuffer);
1047
1048test "KT128 sequential and parallel produce same output for small inputs" {
1049 const allocator = std.testing.allocator;
1050 const io = std.testing.io;
1051
1052 // Test with different small input sizes
1053 const test_sizes = [_]usize{ 100, 1024, 4096, 8192 }; // 100B, 1KB, 4KB, 8KB
1054
1055 for (test_sizes) |size| {
1056 const input = try allocator.alloc(u8, size);
1057 defer allocator.free(input);
1058
1059 // Fill with random data
1060 crypto.random.bytes(input);
1061
1062 var output_seq: [32]u8 = undefined;
1063 var output_par: [32]u8 = undefined;
1064
1065 // Hash with sequential method
1066 try KT128.hash(input, &output_seq, .{});
1067
1068 // Hash with parallel method
1069 try KT128.hashParallel(input, &output_par, .{}, allocator, io);
1070
1071 // Verify outputs match
1072 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1073 }
1074}
1075
1076test "KT128 sequential and parallel produce same output for large inputs" {
1077 const allocator = std.testing.allocator;
1078 const io = std.testing.io;
1079
1080 // Test with large input sizes that trigger parallel processing
1081 // The threshold is 3-10MB depending on CPU count, so we test above that
1082 const test_sizes = [_]usize{ 11 * 1024 * 1024, 20 * 1024 * 1024 }; // 11MB, 20MB
1083
1084 for (test_sizes) |size| {
1085 const input = try allocator.alloc(u8, size);
1086 defer allocator.free(input);
1087
1088 // Fill with random data
1089 crypto.random.bytes(input);
1090
1091 var output_seq: [64]u8 = undefined;
1092 var output_par: [64]u8 = undefined;
1093
1094 // Hash with sequential method
1095 try KT128.hash(input, &output_seq, .{});
1096
1097 // Hash with parallel method
1098 try KT128.hashParallel(input, &output_par, .{}, allocator, io);
1099
1100 // Verify outputs match
1101 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1102 }
1103}
1104
1105test "KT128 sequential and parallel produce same output with customization" {
1106 const allocator = std.testing.allocator;
1107 const io = std.testing.io;
1108
1109 const input_size = 15 * 1024 * 1024; // 15MB
1110 const input = try allocator.alloc(u8, input_size);
1111 defer allocator.free(input);
1112
1113 // Fill with random data
1114 crypto.random.bytes(input);
1115
1116 const customization = "test domain";
1117 var output_seq: [48]u8 = undefined;
1118 var output_par: [48]u8 = undefined;
1119
1120 // Hash with sequential method
1121 try KT128.hash(input, &output_seq, .{ .customization = customization });
1122
1123 // Hash with parallel method
1124 try KT128.hashParallel(input, &output_par, .{ .customization = customization }, allocator, io);
1125
1126 // Verify outputs match
1127 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1128}
1129
1130test "KT256 sequential and parallel produce same output for small inputs" {
1131 const allocator = std.testing.allocator;
1132 const io = std.testing.io;
1133
1134 // Test with different small input sizes
1135 const test_sizes = [_]usize{ 100, 1024, 4096, 8192 }; // 100B, 1KB, 4KB, 8KB
1136
1137 for (test_sizes) |size| {
1138 const input = try allocator.alloc(u8, size);
1139 defer allocator.free(input);
1140
1141 // Fill with random data
1142 crypto.random.bytes(input);
1143
1144 var output_seq: [64]u8 = undefined;
1145 var output_par: [64]u8 = undefined;
1146
1147 // Hash with sequential method
1148 try KT256.hash(input, &output_seq, .{});
1149
1150 // Hash with parallel method
1151 try KT256.hashParallel(input, &output_par, .{}, allocator, io);
1152
1153 // Verify outputs match
1154 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1155 }
1156}
1157
1158test "KT256 sequential and parallel produce same output for large inputs" {
1159 const allocator = std.testing.allocator;
1160 const io = std.testing.io;
1161
1162 // Test with large input sizes that trigger parallel processing
1163 const test_sizes = [_]usize{ 11 * 1024 * 1024, 20 * 1024 * 1024 }; // 11MB, 20MB
1164
1165 for (test_sizes) |size| {
1166 const input = try allocator.alloc(u8, size);
1167 defer allocator.free(input);
1168
1169 // Fill with random data
1170 crypto.random.bytes(input);
1171
1172 var output_seq: [64]u8 = undefined;
1173 var output_par: [64]u8 = undefined;
1174
1175 // Hash with sequential method
1176 try KT256.hash(input, &output_seq, .{});
1177
1178 // Hash with parallel method
1179 try KT256.hashParallel(input, &output_par, .{}, allocator, io);
1180
1181 // Verify outputs match
1182 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1183 }
1184}
1185
1186test "KT256 sequential and parallel produce same output with customization" {
1187 const allocator = std.testing.allocator;
1188 const io = std.testing.io;
1189
1190 const input_size = 15 * 1024 * 1024; // 15MB
1191 const input = try allocator.alloc(u8, input_size);
1192 defer allocator.free(input);
1193
1194 // Fill with random data
1195 crypto.random.bytes(input);
1196
1197 const customization = "test domain";
1198 var output_seq: [80]u8 = undefined;
1199 var output_par: [80]u8 = undefined;
1200
1201 // Hash with sequential method
1202 try KT256.hash(input, &output_seq, .{ .customization = customization });
1203
1204 // Hash with parallel method
1205 try KT256.hashParallel(input, &output_par, .{ .customization = customization }, allocator, io);
1206
1207 // Verify outputs match
1208 try std.testing.expectEqualSlices(u8, &output_seq, &output_par);
1209}
1210
1211/// Helper: Generate pattern data where data[i] = (i % 251)
1212fn generatePattern(allocator: Allocator, len: usize) ![]u8 {
1213 const data = try allocator.alloc(u8, len);
1214 for (data, 0..) |*byte, i| {
1215 byte.* = @intCast(i % 251);
1216 }
1217 return data;
1218}
1219
1220test "KT128: empty message, empty customization, 32 bytes" {
1221 var output: [32]u8 = undefined;
1222 try KT128.hash(&[_]u8{}, &output, .{});
1223
1224 var expected: [32]u8 = undefined;
1225 _ = try std.fmt.hexToBytes(&expected, "1AC2D450FC3B4205D19DA7BFCA1B37513C0803577AC7167F06FE2CE1F0EF39E5");
1226 try std.testing.expectEqualSlices(u8, &expected, &output);
1227}
1228
1229test "KT128: empty message, empty customization, 64 bytes" {
1230 var output: [64]u8 = undefined;
1231 try KT128.hash(&[_]u8{}, &output, .{});
1232
1233 var expected: [64]u8 = undefined;
1234 _ = try std.fmt.hexToBytes(&expected, "1AC2D450FC3B4205D19DA7BFCA1B37513C0803577AC7167F06FE2CE1F0EF39E54269C056B8C82E48276038B6D292966CC07A3D4645272E31FF38508139EB0A71");
1235 try std.testing.expectEqualSlices(u8, &expected, &output);
1236}
1237
1238test "KT128: empty message, empty customization, 10032 bytes (last 32)" {
1239 const allocator = std.testing.allocator;
1240 const output = try allocator.alloc(u8, 10032);
1241 defer allocator.free(output);
1242
1243 try KT128.hash(&[_]u8{}, output, .{});
1244
1245 var expected: [32]u8 = undefined;
1246 _ = try std.fmt.hexToBytes(&expected, "E8DC563642F7228C84684C898405D3A834799158C079B12880277A1D28E2FF6D");
1247 try std.testing.expectEqualSlices(u8, &expected, output[10000..]);
1248}
1249
1250test "KT128: pattern message (1 byte), empty customization, 32 bytes" {
1251 const allocator = std.testing.allocator;
1252 const message = try generatePattern(allocator, 1);
1253 defer allocator.free(message);
1254
1255 var output: [32]u8 = undefined;
1256 try KT128.hash(message, &output, .{});
1257
1258 var expected: [32]u8 = undefined;
1259 _ = try std.fmt.hexToBytes(&expected, "2BDA92450E8B147F8A7CB629E784A058EFCA7CF7D8218E02D345DFAA65244A1F");
1260 try std.testing.expectEqualSlices(u8, &expected, &output);
1261}
1262
1263test "KT128: pattern message (17 bytes), empty customization, 32 bytes" {
1264 const allocator = std.testing.allocator;
1265 const message = try generatePattern(allocator, 17);
1266 defer allocator.free(message);
1267
1268 var output: [32]u8 = undefined;
1269 try KT128.hash(message, &output, .{});
1270
1271 var expected: [32]u8 = undefined;
1272 _ = try std.fmt.hexToBytes(&expected, "6BF75FA2239198DB4772E36478F8E19B0F371205F6A9A93A273F51DF37122888");
1273 try std.testing.expectEqualSlices(u8, &expected, &output);
1274}
1275
1276test "KT128: pattern message (289 bytes), empty customization, 32 bytes" {
1277 const allocator = std.testing.allocator;
1278 const message = try generatePattern(allocator, 289);
1279 defer allocator.free(message);
1280
1281 var output: [32]u8 = undefined;
1282 try KT128.hash(message, &output, .{});
1283
1284 var expected: [32]u8 = undefined;
1285 _ = try std.fmt.hexToBytes(&expected, "0C315EBCDEDBF61426DE7DCF8FB725D1E74675D7F5327A5067F367B108ECB67C");
1286 try std.testing.expectEqualSlices(u8, &expected, &output);
1287}
1288
1289test "KT128: 0xFF message (1 byte), pattern customization (1 byte), 32 bytes" {
1290 const allocator = std.testing.allocator;
1291 const customization = try generatePattern(allocator, 1);
1292 defer allocator.free(customization);
1293
1294 const message = [_]u8{0xFF};
1295 var output: [32]u8 = undefined;
1296 try KT128.hash(&message, &output, .{ .customization = customization });
1297
1298 var expected: [32]u8 = undefined;
1299 _ = try std.fmt.hexToBytes(&expected, "A20B92B251E3D62443EC286E4B9B470A4E8315C156EEB24878B038ABE20650BE");
1300 try std.testing.expectEqualSlices(u8, &expected, &output);
1301}
1302
1303test "KT128: pattern message (8191 bytes), empty customization, 32 bytes" {
1304 const allocator = std.testing.allocator;
1305 const message = try generatePattern(allocator, 8191);
1306 defer allocator.free(message);
1307
1308 var output: [32]u8 = undefined;
1309 try KT128.hash(message, &output, .{});
1310
1311 var expected: [32]u8 = undefined;
1312 _ = try std.fmt.hexToBytes(&expected, "1B577636F723643E990CC7D6A659837436FD6A103626600EB8301CD1DBE553D6");
1313 try std.testing.expectEqualSlices(u8, &expected, &output);
1314}
1315
1316test "KT128: pattern message (8192 bytes), empty customization, 32 bytes" {
1317 const allocator = std.testing.allocator;
1318 const message = try generatePattern(allocator, 8192);
1319 defer allocator.free(message);
1320
1321 var output: [32]u8 = undefined;
1322 try KT128.hash(message, &output, .{});
1323
1324 var expected: [32]u8 = undefined;
1325 _ = try std.fmt.hexToBytes(&expected, "48F256F6772F9EDFB6A8B661EC92DC93B95EBD05A08A17B39AE3490870C926C3");
1326 try std.testing.expectEqualSlices(u8, &expected, &output);
1327}
1328
1329test "KT256: empty message, empty customization, 64 bytes" {
1330 var output: [64]u8 = undefined;
1331 try KT256.hash(&[_]u8{}, &output, .{});
1332
1333 var expected: [64]u8 = undefined;
1334 _ = try std.fmt.hexToBytes(&expected, "B23D2E9CEA9F4904E02BEC06817FC10CE38CE8E93EF4C89E6537076AF8646404E3E8B68107B8833A5D30490AA33482353FD4ADC7148ECB782855003AAEBDE4A9");
1335 try std.testing.expectEqualSlices(u8, &expected, &output);
1336}
1337
1338test "KT256: empty message, empty customization, 128 bytes" {
1339 var output: [128]u8 = undefined;
1340 try KT256.hash(&[_]u8{}, &output, .{});
1341
1342 var expected: [128]u8 = undefined;
1343 _ = try std.fmt.hexToBytes(&expected, "B23D2E9CEA9F4904E02BEC06817FC10CE38CE8E93EF4C89E6537076AF8646404E3E8B68107B8833A5D30490AA33482353FD4ADC7148ECB782855003AAEBDE4A9B0925319D8EA1E121A609821EC19EFEA89E6D08DAEE1662B69C840289F188BA860F55760B61F82114C030C97E5178449608CCD2CD2D919FC7829FF69931AC4D0");
1344 try std.testing.expectEqualSlices(u8, &expected, &output);
1345}
1346
1347test "KT256: pattern message (1 byte), empty customization, 64 bytes" {
1348 const allocator = std.testing.allocator;
1349 const message = try generatePattern(allocator, 1);
1350 defer allocator.free(message);
1351
1352 var output: [64]u8 = undefined;
1353 try KT256.hash(message, &output, .{});
1354
1355 var expected: [64]u8 = undefined;
1356 _ = try std.fmt.hexToBytes(&expected, "0D005A194085360217128CF17F91E1F71314EFA5564539D444912E3437EFA17F82DB6F6FFE76E781EAA068BCE01F2BBF81EACB983D7230F2FB02834A21B1DDD0");
1357 try std.testing.expectEqualSlices(u8, &expected, &output);
1358}
1359
1360test "KT256: pattern message (17 bytes), empty customization, 64 bytes" {
1361 const allocator = std.testing.allocator;
1362 const message = try generatePattern(allocator, 17);
1363 defer allocator.free(message);
1364
1365 var output: [64]u8 = undefined;
1366 try KT256.hash(message, &output, .{});
1367
1368 var expected: [64]u8 = undefined;
1369 _ = try std.fmt.hexToBytes(&expected, "1BA3C02B1FC514474F06C8979978A9056C8483F4A1B63D0DCCEFE3A28A2F323E1CDCCA40EBF006AC76EF0397152346837B1277D3E7FAA9C9653B19075098527B");
1370 try std.testing.expectEqualSlices(u8, &expected, &output);
1371}
1372
1373test "KT256: pattern message (8191 bytes), empty customization, 64 bytes" {
1374 const allocator = std.testing.allocator;
1375 const message = try generatePattern(allocator, 8191);
1376 defer allocator.free(message);
1377
1378 var output: [64]u8 = undefined;
1379 try KT256.hash(message, &output, .{});
1380
1381 var expected: [64]u8 = undefined;
1382 _ = try std.fmt.hexToBytes(&expected, "3081434D93A4108D8D8A3305B89682CEBEDC7CA4EA8A3CE869FBB73CBE4A58EEF6F24DE38FFC170514C70E7AB2D01F03812616E863D769AFB3753193BA045B20");
1383 try std.testing.expectEqualSlices(u8, &expected, &output);
1384}
1385
1386test "KT256: pattern message (8192 bytes), empty customization, 64 bytes" {
1387 const allocator = std.testing.allocator;
1388 const message = try generatePattern(allocator, 8192);
1389 defer allocator.free(message);
1390
1391 var output: [64]u8 = undefined;
1392 try KT256.hash(message, &output, .{});
1393
1394 var expected: [64]u8 = undefined;
1395 _ = try std.fmt.hexToBytes(&expected, "C6EE8E2AD3200C018AC87AAA031CDAC22121B412D07DC6E0DCCBB53423747E9A1C18834D99DF596CF0CF4B8DFAFB7BF02D139D0C9035725ADC1A01B7230A41FA");
1396 try std.testing.expectEqualSlices(u8, &expected, &output);
1397}
1398
1399test "KT128: pattern message (8193 bytes), empty customization, 32 bytes" {
1400 const allocator = std.testing.allocator;
1401 const message = try generatePattern(allocator, 8193);
1402 defer allocator.free(message);
1403
1404 var output: [32]u8 = undefined;
1405 try KT128.hash(message, &output, .{});
1406
1407 var expected: [32]u8 = undefined;
1408 _ = try std.fmt.hexToBytes(&expected, "BB66FE72EAEA5179418D5295EE1344854D8AD7F3FA17EFCB467EC152341284CF");
1409 try std.testing.expectEqualSlices(u8, &expected, &output);
1410}
1411
1412test "KT128: pattern message (16384 bytes), empty customization, 32 bytes" {
1413 const allocator = std.testing.allocator;
1414 const message = try generatePattern(allocator, 16384);
1415 defer allocator.free(message);
1416
1417 var output: [32]u8 = undefined;
1418 try KT128.hash(message, &output, .{});
1419
1420 var expected: [32]u8 = undefined;
1421 _ = try std.fmt.hexToBytes(&expected, "82778F7F7234C83352E76837B721FBDBB5270B88010D84FA5AB0B61EC8CE0956");
1422 try std.testing.expectEqualSlices(u8, &expected, &output);
1423}
1424
1425test "KT128: pattern message (16385 bytes), empty customization, 32 bytes" {
1426 const allocator = std.testing.allocator;
1427 const message = try generatePattern(allocator, 16385);
1428 defer allocator.free(message);
1429
1430 var output: [32]u8 = undefined;
1431 try KT128.hash(message, &output, .{});
1432
1433 var expected: [32]u8 = undefined;
1434 _ = try std.fmt.hexToBytes(&expected, "5F8D2B943922B451842B4E82740D02369E2D5F9F33C5123509A53B955FE177B2");
1435 try std.testing.expectEqualSlices(u8, &expected, &output);
1436}
1437
1438test "KT256: pattern message (8193 bytes), empty customization, 64 bytes" {
1439 const allocator = std.testing.allocator;
1440 const message = try generatePattern(allocator, 8193);
1441 defer allocator.free(message);
1442
1443 var output: [64]u8 = undefined;
1444 try KT256.hash(message, &output, .{});
1445
1446 var expected: [64]u8 = undefined;
1447 _ = try std.fmt.hexToBytes(&expected, "65FF03335900E5197ACBD5F41B797F0E7E36AD4FF7D89C09FA6F28AE58D1E8BC2DF1779B86F988C3B13690172914EA172423B23EF4057255BB0836AB3A99836E");
1448 try std.testing.expectEqualSlices(u8, &expected, &output);
1449}
1450
1451test "KT256: pattern message (16384 bytes), empty customization, 64 bytes" {
1452 const allocator = std.testing.allocator;
1453 const message = try generatePattern(allocator, 16384);
1454 defer allocator.free(message);
1455
1456 var output: [64]u8 = undefined;
1457 try KT256.hash(message, &output, .{});
1458
1459 var expected: [64]u8 = undefined;
1460 _ = try std.fmt.hexToBytes(&expected, "74604239A14847CB79069B4FF0E51070A93034C9AC4DFF4D45E0F2C5DA81D930DE6055C2134B4DF4E49F27D1B2C66E95491858B182A924BD0504DA5976BC516D");
1461 try std.testing.expectEqualSlices(u8, &expected, &output);
1462}
1463
1464test "KT256: pattern message (16385 bytes), empty customization, 64 bytes" {
1465 const allocator = std.testing.allocator;
1466 const message = try generatePattern(allocator, 16385);
1467 defer allocator.free(message);
1468
1469 var output: [64]u8 = undefined;
1470 try KT256.hash(message, &output, .{});
1471
1472 var expected: [64]u8 = undefined;
1473 _ = try std.fmt.hexToBytes(&expected, "C814F23132DADBFD55379F18CB988CB39B751F119322823FD982644A897485397B9F40EB11C6E416359B8AE695A5CE0FA79D1ADA1EEC745D82E0A5AB08A9F014");
1474 try std.testing.expectEqualSlices(u8, &expected, &output);
1475}
1476
1477test "KT128 incremental: empty message matches one-shot" {
1478 var output_oneshot: [32]u8 = undefined;
1479 var output_incremental: [32]u8 = undefined;
1480
1481 try KT128.hash(&[_]u8{}, &output_oneshot, .{});
1482
1483 var hasher = KT128.init(.{});
1484 hasher.final(&output_incremental);
1485
1486 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1487}
1488
1489test "KT128 incremental: small message matches one-shot" {
1490 const message = "Hello, KangarooTwelve!";
1491
1492 var output_oneshot: [32]u8 = undefined;
1493 var output_incremental: [32]u8 = undefined;
1494
1495 try KT128.hash(message, &output_oneshot, .{});
1496
1497 var hasher = KT128.init(.{});
1498 hasher.update(message);
1499 hasher.final(&output_incremental);
1500
1501 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1502}
1503
1504test "KT128 incremental: multiple updates match single update" {
1505 const part1 = "Hello, ";
1506 const part2 = "Kangaroo";
1507 const part3 = "Twelve!";
1508
1509 var output_single: [32]u8 = undefined;
1510 var output_multi: [32]u8 = undefined;
1511
1512 // Single update
1513 var hasher1 = KT128.init(.{});
1514 hasher1.update(part1 ++ part2 ++ part3);
1515 hasher1.final(&output_single);
1516
1517 // Multiple updates
1518 var hasher2 = KT128.init(.{});
1519 hasher2.update(part1);
1520 hasher2.update(part2);
1521 hasher2.update(part3);
1522 hasher2.final(&output_multi);
1523
1524 try std.testing.expectEqualSlices(u8, &output_single, &output_multi);
1525}
1526
1527test "KT128 incremental: exactly chunk_size matches one-shot" {
1528 const allocator = std.testing.allocator;
1529 const message = try allocator.alloc(u8, 8192);
1530 defer allocator.free(message);
1531 @memset(message, 0xAB);
1532
1533 var output_oneshot: [32]u8 = undefined;
1534 var output_incremental: [32]u8 = undefined;
1535
1536 try KT128.hash(message, &output_oneshot, .{});
1537
1538 var hasher = KT128.init(.{});
1539 hasher.update(message);
1540 hasher.final(&output_incremental);
1541
1542 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1543}
1544
1545test "KT128 incremental: larger than chunk_size matches one-shot" {
1546 const allocator = std.testing.allocator;
1547 const message = try generatePattern(allocator, 16384);
1548 defer allocator.free(message);
1549
1550 var output_oneshot: [32]u8 = undefined;
1551 var output_incremental: [32]u8 = undefined;
1552
1553 try KT128.hash(message, &output_oneshot, .{});
1554
1555 var hasher = KT128.init(.{});
1556 hasher.update(message);
1557 hasher.final(&output_incremental);
1558
1559 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1560}
1561
1562test "KT128 incremental: with customization matches one-shot" {
1563 const message = "Test message";
1564 const customization = "my custom domain";
1565
1566 var output_oneshot: [32]u8 = undefined;
1567 var output_incremental: [32]u8 = undefined;
1568
1569 try KT128.hash(message, &output_oneshot, .{ .customization = customization });
1570
1571 var hasher = KT128.init(.{ .customization = customization });
1572 hasher.update(message);
1573 hasher.final(&output_incremental);
1574
1575 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1576}
1577
1578test "KT128 incremental: large message with customization" {
1579 const allocator = std.testing.allocator;
1580 const message = try generatePattern(allocator, 20000);
1581 defer allocator.free(message);
1582 const customization = "test domain";
1583
1584 var output_oneshot: [48]u8 = undefined;
1585 var output_incremental: [48]u8 = undefined;
1586
1587 try KT128.hash(message, &output_oneshot, .{ .customization = customization });
1588
1589 var hasher = KT128.init(.{ .customization = customization });
1590 hasher.update(message);
1591 hasher.final(&output_incremental);
1592
1593 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1594}
1595
1596test "KT128 incremental: streaming chunks matches one-shot" {
1597 const allocator = std.testing.allocator;
1598 const message = try generatePattern(allocator, 25000);
1599 defer allocator.free(message);
1600
1601 var output_oneshot: [32]u8 = undefined;
1602 var output_incremental: [32]u8 = undefined;
1603
1604 try KT128.hash(message, &output_oneshot, .{});
1605
1606 var hasher = KT128.init(.{});
1607
1608 // Feed in 1KB chunks
1609 var offset: usize = 0;
1610 while (offset < message.len) {
1611 const chunk_size_local = @min(1024, message.len - offset);
1612 hasher.update(message[offset..][0..chunk_size_local]);
1613 offset += chunk_size_local;
1614 }
1615 hasher.final(&output_incremental);
1616
1617 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1618}
1619
1620test "KT256 incremental: empty message matches one-shot" {
1621 var output_oneshot: [64]u8 = undefined;
1622 var output_incremental: [64]u8 = undefined;
1623
1624 try KT256.hash(&[_]u8{}, &output_oneshot, .{});
1625
1626 var hasher = KT256.init(.{});
1627 hasher.final(&output_incremental);
1628
1629 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1630}
1631
1632test "KT256 incremental: small message matches one-shot" {
1633 const message = "Hello, KangarooTwelve with 256-bit security!";
1634
1635 var output_oneshot: [64]u8 = undefined;
1636 var output_incremental: [64]u8 = undefined;
1637
1638 try KT256.hash(message, &output_oneshot, .{});
1639
1640 var hasher = KT256.init(.{});
1641 hasher.update(message);
1642 hasher.final(&output_incremental);
1643
1644 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1645}
1646
1647test "KT256 incremental: large message matches one-shot" {
1648 const allocator = std.testing.allocator;
1649 const message = try generatePattern(allocator, 30000);
1650 defer allocator.free(message);
1651
1652 var output_oneshot: [64]u8 = undefined;
1653 var output_incremental: [64]u8 = undefined;
1654
1655 try KT256.hash(message, &output_oneshot, .{});
1656
1657 var hasher = KT256.init(.{});
1658 hasher.update(message);
1659 hasher.final(&output_incremental);
1660
1661 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1662}
1663
1664test "KT256 incremental: with customization matches one-shot" {
1665 const allocator = std.testing.allocator;
1666 const message = try generatePattern(allocator, 15000);
1667 defer allocator.free(message);
1668 const customization = "KT256 custom domain";
1669
1670 var output_oneshot: [80]u8 = undefined;
1671 var output_incremental: [80]u8 = undefined;
1672
1673 try KT256.hash(message, &output_oneshot, .{ .customization = customization });
1674
1675 var hasher = KT256.init(.{ .customization = customization });
1676 hasher.update(message);
1677 hasher.final(&output_incremental);
1678
1679 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1680}
1681
1682test "KT128 incremental: random small message with random chunk sizes" {
1683 const allocator = std.testing.allocator;
1684
1685 const test_sizes = [_]usize{ 100, 500, 2000, 5000, 10000 };
1686
1687 for (test_sizes) |total_size| {
1688 const message = try allocator.alloc(u8, total_size);
1689 defer allocator.free(message);
1690 crypto.random.bytes(message);
1691
1692 var output_oneshot: [32]u8 = undefined;
1693 var output_incremental: [32]u8 = undefined;
1694
1695 try KT128.hash(message, &output_oneshot, .{});
1696
1697 var hasher = KT128.init(.{});
1698 var offset: usize = 0;
1699
1700 while (offset < message.len) {
1701 const remaining = message.len - offset;
1702 const max_chunk = @min(1000, remaining);
1703 const chunk_size_local = if (max_chunk == 1) 1 else crypto.random.intRangeAtMost(usize, 1, max_chunk);
1704
1705 hasher.update(message[offset..][0..chunk_size_local]);
1706 offset += chunk_size_local;
1707 }
1708 hasher.final(&output_incremental);
1709
1710 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1711 }
1712}
1713
1714test "KT128 incremental: random large message (1MB) with random chunk sizes" {
1715 const allocator = std.testing.allocator;
1716
1717 const total_size: usize = 1024 * 1024; // 1 MB
1718 const message = try allocator.alloc(u8, total_size);
1719 defer allocator.free(message);
1720 crypto.random.bytes(message);
1721
1722 var output_oneshot: [32]u8 = undefined;
1723 var output_incremental: [32]u8 = undefined;
1724
1725 try KT128.hash(message, &output_oneshot, .{});
1726
1727 var hasher = KT128.init(.{});
1728 var offset: usize = 0;
1729
1730 while (offset < message.len) {
1731 const remaining = message.len - offset;
1732 const max_chunk = @min(10000, remaining);
1733 const chunk_size_local = if (max_chunk == 1) 1 else crypto.random.intRangeAtMost(usize, 1, max_chunk);
1734
1735 hasher.update(message[offset..][0..chunk_size_local]);
1736 offset += chunk_size_local;
1737 }
1738 hasher.final(&output_incremental);
1739
1740 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1741}
1742
1743test "KT256 incremental: random small message with random chunk sizes" {
1744 const allocator = std.testing.allocator;
1745
1746 const test_sizes = [_]usize{ 100, 500, 2000, 5000, 10000 };
1747
1748 for (test_sizes) |total_size| {
1749 // Generate random message
1750 const message = try allocator.alloc(u8, total_size);
1751 defer allocator.free(message);
1752 crypto.random.bytes(message);
1753
1754 var output_oneshot: [64]u8 = undefined;
1755 var output_incremental: [64]u8 = undefined;
1756
1757 try KT256.hash(message, &output_oneshot, .{});
1758
1759 var hasher = KT256.init(.{});
1760 var offset: usize = 0;
1761
1762 while (offset < message.len) {
1763 const remaining = message.len - offset;
1764 const max_chunk = @min(1000, remaining);
1765 const chunk_size_local = if (max_chunk == 1) 1 else crypto.random.intRangeAtMost(usize, 1, max_chunk);
1766
1767 hasher.update(message[offset..][0..chunk_size_local]);
1768 offset += chunk_size_local;
1769 }
1770 hasher.final(&output_incremental);
1771
1772 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1773 }
1774}
1775
1776test "KT256 incremental: random large message (1MB) with random chunk sizes" {
1777 const allocator = std.testing.allocator;
1778
1779 const total_size: usize = 1024 * 1024; // 1 MB
1780 const message = try allocator.alloc(u8, total_size);
1781 defer allocator.free(message);
1782 crypto.random.bytes(message);
1783
1784 var output_oneshot: [64]u8 = undefined;
1785 var output_incremental: [64]u8 = undefined;
1786
1787 try KT256.hash(message, &output_oneshot, .{});
1788
1789 var hasher = KT256.init(.{});
1790 var offset: usize = 0;
1791
1792 while (offset < message.len) {
1793 const remaining = message.len - offset;
1794 const max_chunk = @min(10000, remaining);
1795 const chunk_size_local = if (max_chunk == 1) 1 else crypto.random.intRangeAtMost(usize, 1, max_chunk);
1796
1797 hasher.update(message[offset..][0..chunk_size_local]);
1798 offset += chunk_size_local;
1799 }
1800 hasher.final(&output_incremental);
1801
1802 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1803}
1804
1805test "KT128 incremental: random message with customization and random chunks" {
1806 const allocator = std.testing.allocator;
1807
1808 const total_size: usize = 50000;
1809 const message = try allocator.alloc(u8, total_size);
1810 defer allocator.free(message);
1811 crypto.random.bytes(message);
1812
1813 const customization = "random test domain";
1814
1815 var output_oneshot: [48]u8 = undefined;
1816 var output_incremental: [48]u8 = undefined;
1817
1818 try KT128.hash(message, &output_oneshot, .{ .customization = customization });
1819
1820 var hasher = KT128.init(.{ .customization = customization });
1821 var offset: usize = 0;
1822
1823 while (offset < message.len) {
1824 const remaining = message.len - offset;
1825 const max_chunk = @min(5000, remaining);
1826 const chunk_size_local = if (max_chunk == 1) 1 else crypto.random.intRangeAtMost(usize, 1, max_chunk);
1827
1828 hasher.update(message[offset..][0..chunk_size_local]);
1829 offset += chunk_size_local;
1830 }
1831 hasher.final(&output_incremental);
1832
1833 try std.testing.expectEqualSlices(u8, &output_oneshot, &output_incremental);
1834}
lib/std/crypto/sha3.zig+9
......@@ -4,6 +4,8 @@ const assert = std.debug.assert;
44const math = std.math;
55const mem = std.mem;
66
7const kangarootwelve = @import("kangarootwelve.zig");
8
79const KeccakState = std.crypto.core.keccak.State;
810
911pub const Sha3_224 = Keccak(1600, 224, 0x06, 24);
......@@ -26,6 +28,9 @@ pub const KMac256 = KMac(256);
2628pub const TupleHash128 = TupleHash(128);
2729pub const TupleHash256 = TupleHash(256);
2830
31pub const KT128 = kangarootwelve.KT128;
32pub const KT256 = kangarootwelve.KT256;
33
2934/// TurboSHAKE128 is a XOF (a secure hash function with a variable output length), with a 128 bit security level.
3035/// It is based on the same permutation as SHA3 and SHAKE128, but which much higher performance.
3136/// The delimiter is 0x1f by default, but can be changed for context-separation.
......@@ -481,6 +486,10 @@ pub const NistLengthEncoding = enum {
481486
482487const htest = @import("test.zig");
483488
489test {
490 _ = kangarootwelve;
491}
492
484493test "sha3-224 single" {
485494 try htest.assertEqualHash(Sha3_224, "6b4e03423667dbb73b6e15454f0eb1abd4597f9a1b078e3f5b5a6bc7", "");
486495 try htest.assertEqualHash(Sha3_224, "e642824c3f8cf24ad09234ee7d3c766fc9a3a5168d0c94ad73b46fdf", "abc");