zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

commit 8efd539305f9beea2da940d9e174a5a38f9c7edf (tree)
parent ccf8e223f4ba8ae2b0ae1128274ee963a97c7484
Author: David Rubin <david@vortan.dev>
Date:   Sun, 22 Mar 2026 06:55:03 -0700

crypto: correct aes-siv s2v

The first issue is that when len(Sn) >= 128,
we perform Sn xor D instead of the Sn xorend D
that is specified in RFC 5297.

The second issue is that we truncate the Sn
if it is larger than 4096 bytes, which could
lead to collisions between inputs. We solve
this by absoring the Sn into the CMAC state
perform the last 16 bytes, xoring those 16
bytes with D as described in the first issue,
and then updating and squeezing the CMAC.

Diffstat:
Mlib/std/crypto/aes_siv.zig | 61+++++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 53 insertions(+), 8 deletions(-)

diff --git a/lib/std/crypto/aes_siv.zig b/lib/std/crypto/aes_siv.zig @@ -88,16 +88,19 @@ fn AesSiv(comptime Aes: anytype) type { // Process the final string const sn = strings[strings.len - 1]; if (sn.len >= 16) { - // XOR d with the first 16 bytes of Sn - var xored_msg_buf: [4096]u8 = undefined; - const xored_len = @min(sn.len, xored_msg_buf.len); - @memcpy(xored_msg_buf[0..xored_len], sn[0..xored_len]); - - for (d, 0..) |b, j| { - xored_msg_buf[j] ^= b; + // XOR d with the last 16 bytes of Sn, + // and give the entire Sn to CMAC incrementally. + var cmac = CmacImpl.init(&key); + const prefix = sn.len - 16; + cmac.update(sn[0..prefix]); + + var tail: [16]u8 = undefined; + for (&tail, sn[prefix..][0..16], d) |*out, s, db| { + out.* = s ^ db; } + cmac.update(&tail); - CmacImpl.create(iv, xored_msg_buf[0..xored_len], &key); + cmac.final(iv); } else { // Pad and XOR d = dbl(d); @@ -355,6 +358,48 @@ test "Aes128Siv - RFC 5297 Test Vector A.1" { try testing.expectEqualSlices(u8, &plaintext, &decrypted); } +test "Aes128Siv - RFC 5297 Test Vector A.2" { + // Test vector from RFC 5297 Appendix A.2 + const key: [32]u8 = .{ + 0x7f, 0x7e, 0x7d, 0x7c, 0x7b, 0x7a, 0x79, 0x78, + 0x77, 0x76, 0x75, 0x74, 0x73, 0x72, 0x71, 0x70, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, + }; + const ad1 = [_]u8{ + 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, + 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, + 0xde, 0xad, 0xda, 0xda, 0xde, 0xad, 0xda, 0xda, + 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88, + 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00, + }; + const ad2 = [_]u8{ + 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, + 0x90, 0xa0, + }; + const nonce: [16]u8 = .{ + 0x09, 0xf9, 0x11, 0x02, 0x9d, 0x74, 0xe3, 0x5b, + 0xd8, 0x41, 0x56, 0xc5, 0x63, 0x56, 0x88, 0xc0, + }; + const plaintext = [_]u8{ + 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20, + 0x73, 0x6f, 0x6d, 0x65, 0x20, 0x70, 0x6c, 0x61, + 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x20, 0x74, + 0x6f, 0x20, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70, + 0x74, 0x20, 0x75, 0x73, 0x69, 0x6e, 0x67, 0x20, + 0x53, 0x49, 0x56, 0x2d, 0x41, 0x45, 0x53, + }; + + var ciphertext: [plaintext.len]u8 = undefined; + var tag: [16]u8 = undefined; + + Aes128Siv.encryptWithAdVector(&ciphertext, &tag, &plaintext, &.{ &ad1, &ad2, &nonce }, key); + + // Expected values from RFC 5297 + try htest.assertEqual("7bdb6e3b432667eb06f4d14bff2fbd0f", &tag); + try htest.assertEqual("cb900f2fddbe404326601965c889bf17dba77ceb094fa663b7a3f748ba8af829ea64ad544a272e9c485b62a3fd5c0d", &ciphertext); +} + test "Aes128Siv - empty plaintext" { const key: [32]u8 = @splat(0x42); const plaintext = "";