| ... | ... | @@ -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 | |
| 5 | const std = @import("../../std.zig"); |
| 6 | const mem = std.mem; |
| 7 | const bitsliced_aes = @import("../aes/bitsliced.zig"); |
| 8 | |
| 9 | const c1_bytes = [16]u8{ 0xdb, 0x3d, 0x18, 0x55, 0x6d, 0xc2, 0x2f, 0xf1, 0x20, 0x11, 0x31, 0x42, 0x73, 0xb5, 0x28, 0xdd }; |
| 10 | const c2_bytes = [16]u8{ 0x0, 0x1, 0x01, 0x02, 0x03, 0x05, 0x08, 0x0d, 0x15, 0x22, 0x37, 0x59, 0x90, 0xe9, 0x79, 0x62 }; |
| 11 | |
| 12 | fn 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 | |
| 174 | pub 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 | |
| 316 | pub 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 | } |