authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-11-26 12:07:09+01:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-11-26 10:08:09+01:00
log9ede8ee135961bcc268a5077771571437bd32d8c
tree3113b8d602db0ed856a7ab442aabd9cdc765bdbc
parente23af9d31d79af3636cea5bfb8fc4c9b26f1c854

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

KangarooTwelve is a family of two fast and secure extendable-output functions (XOFs): KT128 and KT256. These functions generalize traditional hash functions by allowing arbitrary output lengths. KangarooTwelve was designed by SHA-3 authors. It aims to deliver higher performance than the SHA-3 and SHAKE functions defined in FIPS 202, while preserving their flexibility and core security principles. On high-end platforms, it can take advantage of parallelism, whether through multiple CPU cores or SIMD instructions. As modern SHA-3 constructions, KT128 and KT256 can serve as general-purpose hash functions and can be used, for example, in key-derivation, and with arbitrarily large inputs. RFC9861: https://datatracker.ietf.org/doc/rfc9861/

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

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