authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-09-28 12:52:33+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2026-10-04 14:03:54+02:00
loge782da3d89cfae1bac37d7c3bcebf0ac5b2cb6b0
tree0c733d4c7990d56ac66839629871436aa1529872
parent25cafe0b345635dfaa533557f7185e76d5f405ef

crypto.aegis: add specialized implementation for CPUs without AES

On CPUs that don't support AES acceleration (and in environments such as WebAssembly) we already use a generic bitsliced AES implementation, via std.crypto.core.aes. But AEGIS can be much faster by keeping the state bitsliced (so we only set and extract the required bits to update the state and get the keystream). Rotating the state is also as simple as a 1-bit shift over the entire bitsliced state. This PR implements techniques described in https://eprint.iacr.org/2026/1338 and makes AEGIS on CPUs without AES acceleration 2x-3x faster. On WebAssembly with SIMD, there's room for improvement (see https://00f.net/2026/07/16/aes-with-simd-swizzles/) but that requires swizzle operations that we don't support yet.

2 files changed, 455 insertions(+), 2 deletions(-)

lib/std/crypto/aegis.zig+6-2
......@@ -26,6 +26,10 @@ const crypto = std.crypto;
2626const mem = std.mem;
2727const assert = std.debug.assert;
2828const AuthenticationError = crypto.errors.AuthenticationError;
29const bitsliced = @import("aegis/bitsliced.zig");
30
31const use_bitsliced = !crypto.core.aes.has_hardware_support and
32 std.options.side_channels_mitigations != .none;
2933
3034/// AEGIS-128X4 with a 128 bit tag
3135pub const Aegis128X4 = Aegis128XGeneric(4, 128);
......@@ -276,7 +280,7 @@ fn Aegis128XGeneric(comptime degree: u7, comptime tag_bits: u9) type {
276280 comptime assert(tag_bits == 128 or tag_bits == 256); // tag must be 128 or 256 bits
277281
278282 return struct {
279 const State = State128X(degree);
283 const State = if (use_bitsliced) bitsliced.State128X(degree) else State128X(degree);
280284
281285 pub const tag_length = tag_bits / 8;
282286 pub const nonce_length = 16;
......@@ -571,7 +575,7 @@ fn Aegis256XGeneric(comptime degree: u7, comptime tag_bits: u9) type {
571575 comptime assert(tag_bits == 128 or tag_bits == 256); // tag must be 128 or 256 bits
572576
573577 return struct {
574 const State = State256X(degree);
578 const State = if (use_bitsliced) bitsliced.State256X(degree) else State256X(degree);
575579
576580 pub const tag_length = tag_bits / 8;
577581 pub const nonce_length = 32;
lib/std/crypto/aegis/bitsliced.zig created+449
......@@ -0,0 +1,449 @@
1//! Constant-time AEGIS states for targets without AES instructions.
2//!
3//! We keep the AEGIS state in bitsliced form as much as possible.
4
5const std = @import("../../std.zig");
6const mem = std.mem;
7const bitsliced_aes = @import("../aes/bitsliced.zig");
8
9const c1_bytes = [16]u8{ 0xdb, 0x3d, 0x18, 0x55, 0x6d, 0xc2, 0x2f, 0xf1, 0x20, 0x11, 0x31, 0x42, 0x73, 0xb5, 0x28, 0xdd };
10const c2_bytes = [16]u8{ 0x0, 0x1, 0x01, 0x02, 0x03, 0x05, 0x08, 0x0d, 0x15, 0x22, 0x37, 0x59, 0x90, 0xe9, 0x79, 0x62 };
11
12fn Lanes(comptime degree: u7) type {
13 return struct {
14 const Aes = bitsliced_aes.Batch(degree);
15 const Word = Aes.Word;
16 const block_length = @as(usize, degree) * 16;
17
18 fn mask(comptime b: u8) Word {
19 return comptime Aes.splatWord(@as(u32, 0x01010101) * b);
20 }
21
22 fn loadBlock(bytes: *const [block_length]u8) [4]Word {
23 var w: [4]Word = @splat(0);
24 inline for (0..degree) |g| {
25 inline for (0..4) |wd| {
26 const v = mem.readInt(u32, bytes[g * 16 + wd * 4 ..][0..4], .little);
27 w[wd] |= @as(Word, v) << (32 * g);
28 }
29 }
30 return w;
31 }
32
33 fn storeBlock(bytes: *[block_length]u8, w: [4]Word) void {
34 inline for (0..degree) |g| {
35 inline for (0..4) |wd| {
36 const v: u32 = @truncate(w[wd] >> (32 * g));
37 mem.writeInt(u32, bytes[g * 16 + wd * 4 ..][0..4], v, .little);
38 }
39 }
40 }
41
42 fn broadcastBlock(bytes: *const [16]u8) [4]Word {
43 var w: [4]Word = undefined;
44 inline for (&w, 0..) |*x, wd| {
45 x.* = Aes.splatWord(mem.readInt(u32, bytes[wd * 4 ..][0..4], .little));
46 }
47 return w;
48 }
49
50 /// Returns two little-endian 64-bit lengths in every lane.
51 fn sizesBlock(a: u64, b: u64) [4]Word {
52 return .{
53 Aes.splatWord(@truncate(a)),
54 Aes.splatWord(@truncate(a >> 32)),
55 Aes.splatWord(@truncate(b)),
56 Aes.splatWord(@truncate(b >> 32)),
57 };
58 }
59
60 fn xorBlocks(a: [4]Word, b: [4]Word) [4]Word {
61 var w: [4]Word = undefined;
62 inline for (&w, a, b) |*x, y, z| x.* = y ^ z;
63 return w;
64 }
65
66 fn contextMask(comptime slots: [2]usize) Aes.Bitsliced {
67 @setEvalBranchQuota(50000);
68 var ctx: [4]Word = @splat(0);
69 for (0..degree) |g| ctx[0] |= ((@as(Word, degree) - 1) * 256 + g) << (32 * g);
70 return packBlocks(&slots, .{ ctx, ctx });
71 }
72
73 const Swap = struct { usize, usize, u32, comptime_int };
74
75 /// Swaps, in execution order.
76 const pack_swaps = swaps: {
77 var swaps: []const Swap = &.{};
78 for (0..8) |g| {
79 const i = g * 4;
80 swaps = swaps ++ [_]Swap{
81 .{ i, i + 1, 0x00ff00ff, 8 },
82 .{ i + 2, i + 3, 0x00ff00ff, 8 },
83 .{ i, i + 2, 0x0000ffff, 16 },
84 .{ i + 1, i + 3, 0x0000ffff, 16 },
85 };
86 }
87 for (0..4) |k| {
88 swaps = swaps ++ [_]Swap{
89 .{ k + 4, k, 0x55555555, 1 },
90 .{ k + 12, k + 8, 0x55555555, 1 },
91 .{ k + 20, k + 16, 0x55555555, 1 },
92 .{ k + 28, k + 24, 0x55555555, 1 },
93 .{ k + 8, k, 0x33333333, 2 },
94 .{ k + 12, k + 4, 0x33333333, 2 },
95 .{ k + 24, k + 16, 0x33333333, 2 },
96 .{ k + 28, k + 20, 0x33333333, 2 },
97 .{ k + 16, k, 0x0f0f0f0f, 4 },
98 .{ k + 20, k + 4, 0x0f0f0f0f, 4 },
99 .{ k + 24, k + 8, 0x0f0f0f0f, 4 },
100 .{ k + 28, k + 12, 0x0f0f0f0f, 4 },
101 };
102 }
103 break :swaps swaps;
104 };
105
106 /// Returns the swaps needed when only `slots` contain input blocks.
107 fn nonzeroSwaps(comptime slots: []const usize) []const Swap {
108 var nonzero: [32]bool = @splat(false);
109 for (slots) |p| nonzero[p * 4 ..][0..4].* = @splat(true);
110 var swaps: []const Swap = &.{};
111 for (pack_swaps) |s| {
112 if (!nonzero[s[0]] and !nonzero[s[1]]) continue;
113 nonzero[s[0]] = true;
114 nonzero[s[1]] = true;
115 swaps = swaps ++ [_]Swap{s};
116 }
117 return swaps;
118 }
119
120 /// Packs blocks in `slots`, skipping unnecessary swaps.
121 fn packBlocks(comptime slots: []const usize, blocks: [slots.len][4]Word) Aes.Bitsliced {
122 var st: Aes.Bitsliced = @splat(0);
123 inline for (slots, blocks) |p, b| st[p * 4 ..][0..4].* = b;
124 inline for (comptime nonzeroSwaps(slots)) |s| Aes.swapMove(&st[s[0]], &st[s[1]], s[2], s[3]);
125 return st;
126 }
127
128 fn unpackBlocks(st: Aes.Bitsliced, comptime slots: []const usize) [slots.len][4]Word {
129 const swaps = comptime nonzeroSwaps(slots);
130 var u = st;
131 inline for (0..swaps.len) |i| {
132 const s = swaps[swaps.len - 1 - i];
133 Aes.swapMove(&u[s[0]], &u[s[1]], s[2], s[3]);
134 }
135 var blocks: [slots.len][4]Word = undefined;
136 inline for (&blocks, slots) |*b, p| b.* = u[p * 4 ..][0..4].*;
137 return blocks;
138 }
139
140 fn unpacked(words: Aes.Bitsliced) Aes.Bitsliced {
141 var u = words;
142 Aes.unpackState(&u);
143 return u;
144 }
145
146 /// Combines each range of state blocks into a unique tag.
147 fn laneTags(words: Aes.Bitsliced, comptime ranges: []const [2]usize) [ranges.len][block_length]u8 {
148 const u = unpacked(words);
149 var tags: [ranges.len][block_length]u8 = undefined;
150 inline for (ranges, &tags) |r, *t| {
151 var acc: [4]Word = @splat(0);
152 inline for (r[0]..r[1]) |p| acc = xorBlocks(acc, u[p * 4 ..][0..4].*);
153 storeBlock(t, acc);
154 }
155 return tags;
156 }
157
158 /// XORs all lane tags when `fold_lanes` is true; otherwise we return lane 0's tag.
159 fn tag(words: Aes.Bitsliced, comptime ranges: []const [2]usize, comptime fold_lanes: bool) [ranges.len * 16]u8 {
160 var out: [ranges.len * 16]u8 = undefined;
161 for (laneTags(words, ranges), 0..) |t, i| {
162 const lanes: [degree]@Vector(16, u8) = @bitCast(t);
163 var folded = lanes[0];
164 if (fold_lanes) {
165 for (lanes[1..]) |lane| folded ^= lane;
166 }
167 out[i * 16 ..][0..16].* = folded;
168 }
169 return out;
170 }
171 };
172}
173
174pub fn State128X(comptime degree: u7) type {
175 return struct {
176 const State = @This();
177 const V = Lanes(degree);
178 const Aes = V.Aes;
179 const Word = V.Word;
180
181 /// bit 7 - i of each byte belongs to state block i.
182 words: Aes.Bitsliced,
183
184 const aes_block_length = V.block_length;
185 pub const rate = aes_block_length * 2;
186
187 const context_mask = V.contextMask(.{ 3, 7 });
188
189 pub fn init(key: [16]u8, nonce: [16]u8) State {
190 const c1 = V.broadcastBlock(&c1_bytes);
191 const c2 = V.broadcastBlock(&c2_bytes);
192 const key_block = V.broadcastBlock(&key);
193 const nonce_block = V.broadcastBlock(&nonce);
194 const kxn = V.xorBlocks(key_block, nonce_block);
195 var state = State{ .words = V.packBlocks(&.{ 0, 1, 2, 3, 4, 5, 6, 7 }, .{
196 kxn,
197 c1,
198 c2,
199 c1,
200 kxn,
201 V.xorBlocks(key_block, c2),
202 V.xorBlocks(key_block, c1),
203 V.xorBlocks(key_block, c2),
204 }) };
205 const input = packRate(nonce_block, key_block);
206 for (0..10) |_| {
207 for (&state.words, context_mask) |*w, m| w.* ^= m;
208 state.update(&input);
209 }
210 return state;
211 }
212
213 fn update(state: *State, input: *const Aes.Bitsliced) void {
214 var st1 = state.words;
215 Aes.round(&st1);
216 for (&state.words, st1, input) |*w, r, in| {
217 w.* ^= (((r & V.mask(0xfe)) >> 1) | ((r & V.mask(0x01)) << 7)) ^ in;
218 }
219 }
220
221 fn packRate(m0: [4]Word, m1: [4]Word) Aes.Bitsliced {
222 return V.packBlocks(&.{ 0, 4 }, .{ m0, m1 });
223 }
224
225 fn keystream(state: *const State) [2][4]Word {
226 var z: Aes.Bitsliced = undefined;
227 for (&z, &state.words) |*zw, x| {
228 zw.* = ((x & V.mask(0x02)) << 6) ^ ((x & V.mask(0x40)) << 1) ^
229 (((x & V.mask(0x20)) << 2) & ((x & V.mask(0x10)) << 3)) ^
230 ((x & V.mask(0x20)) >> 2) ^ ((x & V.mask(0x04)) << 1) ^
231 (((x & V.mask(0x02)) << 2) & ((x & V.mask(0x01)) << 3));
232 }
233 return V.unpackBlocks(z, &.{ 0, 4 });
234 }
235
236 pub fn absorb(state: *State, src: *const [rate]u8) void {
237 const input = packRate(V.loadBlock(src[0..aes_block_length]), V.loadBlock(src[aes_block_length..rate]));
238 state.update(&input);
239 }
240
241 pub fn enc(state: *State, dst: *[rate]u8, src: *const [rate]u8) void {
242 const z = state.keystream();
243 const m0 = V.loadBlock(src[0..aes_block_length]);
244 const m1 = V.loadBlock(src[aes_block_length..rate]);
245 const input = packRate(m0, m1);
246 state.update(&input);
247 V.storeBlock(dst[0..aes_block_length], V.xorBlocks(m0, z[0]));
248 V.storeBlock(dst[aes_block_length..rate], V.xorBlocks(m1, z[1]));
249 }
250
251 pub fn dec(state: *State, dst: *[rate]u8, src: *const [rate]u8) void {
252 const z = state.keystream();
253 const m0 = V.xorBlocks(V.loadBlock(src[0..aes_block_length]), z[0]);
254 const m1 = V.xorBlocks(V.loadBlock(src[aes_block_length..rate]), z[1]);
255 const input = packRate(m0, m1);
256 state.update(&input);
257 V.storeBlock(dst[0..aes_block_length], m0);
258 V.storeBlock(dst[aes_block_length..rate], m1);
259 }
260
261 pub fn decLast(state: *State, dst: []u8, src: []const u8) void {
262 const z = state.keystream();
263 var pad: [rate]u8 = undefined;
264 V.storeBlock(pad[0..aes_block_length], z[0]);
265 V.storeBlock(pad[aes_block_length..rate], z[1]);
266 for (pad[0..src.len], src) |*p, x| p.* ^= x;
267 @memcpy(dst, pad[0..src.len]);
268 @memset(pad[src.len..], 0);
269 state.absorb(&pad);
270 }
271
272 fn finalizeRounds(state: *State, sizes: [4]Word) void {
273 const t = V.xorBlocks(sizes, V.unpacked(state.words)[8..12].*);
274 const input = packRate(t, t);
275 for (0..7) |_| state.update(&input);
276 }
277
278 fn tagRanges(comptime tag_bits: u9) []const [2]usize {
279 return switch (tag_bits) {
280 128 => &.{.{ 0, 7 }},
281 256 => &.{ .{ 0, 4 }, .{ 4, 8 } },
282 else => unreachable,
283 };
284 }
285
286 pub fn finalize(state: *State, comptime tag_bits: u9, adlen: usize, mlen: usize) [tag_bits / 8]u8 {
287 state.finalizeRounds(V.sizesBlock(@as(u64, adlen) * 8, @as(u64, mlen) * 8));
288 return V.tag(state.words, tagRanges(tag_bits), true);
289 }
290
291 pub fn finalizeMac(state: *State, comptime tag_bits: u9, datalen: usize) [tag_bits / 8]u8 {
292 state.finalizeRounds(V.sizesBlock(@as(u64, datalen) * 8, tag_bits));
293 if (degree > 1) {
294 const tags = V.laneTags(state.words, tagRanges(tag_bits));
295 var v: [rate]u8 = @splat(0);
296 switch (tag_bits) {
297 128 => for (0..degree / 2) |d| {
298 v[0..16].* = tags[0][d * 32 ..][0..16].*;
299 v[rate / 2 ..][0..16].* = tags[0][d * 32 ..][16..32].*;
300 state.absorb(&v);
301 },
302 256 => for (1..degree) |d| {
303 v[0..16].* = tags[0][d * 16 ..][0..16].*;
304 v[rate / 2 ..][0..16].* = tags[1][d * 16 ..][0..16].*;
305 state.absorb(&v);
306 },
307 else => unreachable,
308 }
309 state.finalizeRounds(V.sizesBlock(degree, tag_bits));
310 }
311 return V.tag(state.words, tagRanges(tag_bits), false);
312 }
313 };
314}
315
316pub fn State256X(comptime degree: u7) type {
317 return struct {
318 const State = @This();
319 const V = Lanes(degree);
320 const Aes = V.Aes;
321 const Word = V.Word;
322
323 /// bit 7 - i of each byte belongs to state block i.
324 words: Aes.Bitsliced,
325
326 pub const rate = V.block_length;
327
328 const context_mask = V.contextMask(.{ 3, 5 });
329
330 pub fn init(key: [32]u8, nonce: [32]u8) State {
331 const c1 = V.broadcastBlock(&c1_bytes);
332 const c2 = V.broadcastBlock(&c2_bytes);
333 const key_block1 = V.broadcastBlock(key[0..16]);
334 const key_block2 = V.broadcastBlock(key[16..32]);
335 const nonce_block1 = V.broadcastBlock(nonce[0..16]);
336 const nonce_block2 = V.broadcastBlock(nonce[16..32]);
337 const kxn1 = V.xorBlocks(key_block1, nonce_block1);
338 const kxn2 = V.xorBlocks(key_block2, nonce_block2);
339 var state = State{ .words = V.packBlocks(&.{ 0, 1, 2, 3, 4, 5 }, .{
340 kxn1,
341 kxn2,
342 c1,
343 c2,
344 V.xorBlocks(key_block1, c2),
345 V.xorBlocks(key_block2, c1),
346 }) };
347 const inputs = [4]Aes.Bitsliced{
348 packRate(key_block1),
349 packRate(key_block2),
350 packRate(kxn1),
351 packRate(kxn2),
352 };
353 for (0..4) |_| {
354 for (&inputs) |*input| {
355 for (&state.words, context_mask) |*w, m| w.* ^= m;
356 state.update(input);
357 }
358 }
359 return state;
360 }
361
362 fn update(state: *State, input: *const Aes.Bitsliced) void {
363 var st1 = state.words;
364 Aes.round(&st1);
365 for (&state.words, st1, input) |*w, r, in| {
366 w.* ^= (((r & V.mask(0xf8)) >> 1) | ((r & V.mask(0x04)) << 5)) ^ in;
367 }
368 }
369
370 fn packRate(m: [4]Word) Aes.Bitsliced {
371 return V.packBlocks(&.{0}, .{m});
372 }
373
374 fn keystream(state: *const State) [4]Word {
375 var z: Aes.Bitsliced = undefined;
376 for (&z, &state.words) |*zw, x| {
377 zw.* = ((x & V.mask(0x40)) << 1) ^ ((x & V.mask(0x08)) << 4) ^
378 ((x & V.mask(0x04)) << 5) ^
379 (((x & V.mask(0x20)) << 2) & ((x & V.mask(0x10)) << 3));
380 }
381 return V.unpackBlocks(z, &.{0})[0];
382 }
383
384 pub fn absorb(state: *State, src: *const [rate]u8) void {
385 const input = packRate(V.loadBlock(src));
386 state.update(&input);
387 }
388
389 pub fn enc(state: *State, dst: *[rate]u8, src: *const [rate]u8) void {
390 const z = state.keystream();
391 const m = V.loadBlock(src);
392 const input = packRate(m);
393 state.update(&input);
394 V.storeBlock(dst, V.xorBlocks(m, z));
395 }
396
397 pub fn dec(state: *State, dst: *[rate]u8, src: *const [rate]u8) void {
398 const z = state.keystream();
399 const m = V.xorBlocks(V.loadBlock(src), z);
400 const input = packRate(m);
401 state.update(&input);
402 V.storeBlock(dst, m);
403 }
404
405 pub fn decLast(state: *State, dst: []u8, src: []const u8) void {
406 var pad: [rate]u8 = undefined;
407 V.storeBlock(&pad, state.keystream());
408 for (pad[0..src.len], src) |*p, x| p.* ^= x;
409 @memcpy(dst, pad[0..src.len]);
410 @memset(pad[src.len..], 0);
411 state.absorb(&pad);
412 }
413
414 fn finalizeRounds(state: *State, sizes: [4]Word) void {
415 const t = V.xorBlocks(sizes, V.unpacked(state.words)[12..16].*);
416 const input = packRate(t);
417 for (0..7) |_| state.update(&input);
418 }
419
420 fn tagRanges(comptime tag_bits: u9) []const [2]usize {
421 return switch (tag_bits) {
422 128 => &.{.{ 0, 6 }},
423 256 => &.{ .{ 0, 3 }, .{ 3, 6 } },
424 else => unreachable,
425 };
426 }
427
428 pub fn finalize(state: *State, comptime tag_bits: u9, adlen: usize, mlen: usize) [tag_bits / 8]u8 {
429 state.finalizeRounds(V.sizesBlock(@as(u64, adlen) * 8, @as(u64, mlen) * 8));
430 return V.tag(state.words, tagRanges(tag_bits), true);
431 }
432
433 pub fn finalizeMac(state: *State, comptime tag_bits: u9, datalen: usize) [tag_bits / 8]u8 {
434 state.finalizeRounds(V.sizesBlock(@as(u64, datalen) * 8, tag_bits));
435 if (degree > 1) {
436 const tags = V.laneTags(state.words, tagRanges(tag_bits));
437 var v: [rate]u8 = @splat(0);
438 for (1..degree) |d| {
439 for (tags) |t| {
440 v[0..16].* = t[d * 16 ..][0..16].*;
441 state.absorb(&v);
442 }
443 }
444 state.finalizeRounds(V.sizesBlock(degree, tag_bits));
445 }
446 return V.tag(state.words, tagRanges(tag_bits), false);
447 }
448 };
449}