master
  1const std = @import("std");
  2const assert = std.debug.assert;
  3const crypto = std.crypto;
  4const debug = std.debug;
  5const mem = std.mem;
  6const math = std.math;
  7const modes = @import("modes.zig");
  8const Polyval = @import("ghash_polyval.zig").Polyval;
  9const AuthenticationError = crypto.errors.AuthenticationError;
 10
 11pub const Aes128GcmSiv = AesGcmSiv(crypto.core.aes.Aes128);
 12pub const Aes256GcmSiv = AesGcmSiv(crypto.core.aes.Aes256);
 13
 14/// AES-GCM-SIV: Authenticated encryption that remains secure even if you accidentally reuse a nonce.
 15///
 16/// What it does: Encrypts data and protects it from tampering. You can also attach
 17/// unencrypted metadata (like headers) that will be authenticated but not encrypted.
 18///
 19/// When to use AES-GCM-SIV:
 20/// - When you can't guarantee unique nonces (though you should still try to use unique nonces)
 21///
 22/// When to use regular AES-GCM instead:
 23/// - When you can guarantee unique nonces (e.g., using a counter)
 24/// - When you need slightly better performance
 25///
 26/// Security: If you accidentally reuse a nonce with the same key, AES-GCM-SIV only
 27/// reveals whether two messages are identical. Regular AES-GCM would be catastrophically
 28/// broken in this scenario, potentially revealing the authentication key.
 29///
 30/// Performance: Slightly slower than AES-GCM due to the additional key derivation step.
 31///
 32/// Defined in RFC 8452.
 33fn AesGcmSiv(comptime Aes: anytype) type {
 34    debug.assert(Aes.block.block_length == 16);
 35
 36    return struct {
 37        pub const tag_length = 16;
 38        pub const nonce_length = 12;
 39        pub const key_length = Aes.key_bits / 8;
 40
 41        const zeros: [16]u8 = @splat(0);
 42
 43        /// Derives the authentication and message encryption keys from the master key and nonce.
 44        /// This implements the key derivation as specified in RFC 8452 Section 4.
 45        /// Generates a 128-bit authentication key for POLYVAL and a message encryption key
 46        /// (128 or 256 bits depending on the AES variant).
 47        fn deriveKeys(message_key: *[key_length]u8, auth_key: *[16]u8, key: [key_length]u8, nonce: [nonce_length]u8) void {
 48            const aes = Aes.initEnc(key);
 49
 50            // Derive authentication and message keys per RFC 8452 Section 4
 51            // Each encryption produces 16 bytes, but we only use first 8 bytes of each block
 52
 53            if (key_length == 16) {
 54                // AES-128-GCM-SIV: Process 4 blocks in parallel
 55                var key_blocks: [4 * 16]u8 = undefined;
 56                var cipher_outs: [4 * 16]u8 = undefined;
 57
 58                // Set up all 4 blocks with counters 0-3 and nonce
 59                inline for (0..4) |i| {
 60                    mem.writeInt(u32, key_blocks[i * 16 ..][0..4], @intCast(i), .little);
 61                    key_blocks[i * 16 + 4 .. i * 16 + 16].* = nonce;
 62                }
 63
 64                // Encrypt all 4 blocks in parallel
 65                aes.encryptWide(4, &cipher_outs, &key_blocks);
 66
 67                // Extract the key material (first 8 bytes of each block)
 68                @memcpy(auth_key[0..8], cipher_outs[0..8]);
 69                @memcpy(auth_key[8..16], cipher_outs[16..24]);
 70                @memcpy(message_key[0..8], cipher_outs[32..40]);
 71                @memcpy(message_key[8..16], cipher_outs[48..56]);
 72            } else {
 73                // AES-256-GCM-SIV: Process 6 blocks in parallel
 74                var key_blocks: [6 * 16]u8 = undefined;
 75                var cipher_outs: [6 * 16]u8 = undefined;
 76
 77                // Set up all 6 blocks with counters 0-5 and nonce
 78                inline for (0..6) |i| {
 79                    mem.writeInt(u32, key_blocks[i * 16 ..][0..4], @intCast(i), .little);
 80                    key_blocks[i * 16 + 4 .. i * 16 + 16].* = nonce;
 81                }
 82
 83                // Encrypt all 6 blocks in parallel
 84                aes.encryptWide(6, &cipher_outs, &key_blocks);
 85
 86                // Extract the key material (first 8 bytes of each block)
 87                @memcpy(auth_key[0..8], cipher_outs[0..8]);
 88                @memcpy(auth_key[8..16], cipher_outs[16..24]);
 89                @memcpy(message_key[0..8], cipher_outs[32..40]);
 90                @memcpy(message_key[8..16], cipher_outs[48..56]);
 91                @memcpy(message_key[16..24], cipher_outs[64..72]);
 92                @memcpy(message_key[24..32], cipher_outs[80..88]);
 93            }
 94        }
 95
 96        /// Encrypts and authenticates a message using AES-GCM-SIV.
 97        ///
 98        /// `c`: The ciphertext buffer to write the encrypted data to.
 99        /// `tag`: The authentication tag buffer to write the computed tag to.
100        /// `m`: The plaintext message to encrypt.
101        /// `ad`: The associated data to authenticate.
102        /// `npub`: The nonce to use for encryption.
103        /// `key`: The encryption key.
104        pub fn encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, key: [key_length]u8) void {
105            debug.assert(c.len == m.len);
106            debug.assert(m.len <= (1 << 36));
107            debug.assert(ad.len <= (1 << 36));
108
109            var auth_key: [16]u8 = undefined;
110            var message_key: [key_length]u8 = undefined;
111            deriveKeys(&message_key, &auth_key, key, npub);
112
113            // Calculate POLYVAL over additional data and plaintext
114            const block_count = (math.divCeil(usize, ad.len, Polyval.block_length) catch unreachable) +
115                (math.divCeil(usize, m.len, Polyval.block_length) catch unreachable) + 1;
116            var mac = Polyval.initForBlockCount(&auth_key, block_count);
117
118            // Process additional data
119            mac.update(ad);
120            mac.pad();
121
122            // Process plaintext
123            mac.update(m);
124            mac.pad();
125
126            // Length block
127            var length_block: [16]u8 = undefined;
128            mem.writeInt(u64, length_block[0..8], @as(u64, ad.len) * 8, .little);
129            mem.writeInt(u64, length_block[8..16], @as(u64, m.len) * 8, .little);
130            mac.update(&length_block);
131
132            // Get POLYVAL result
133            var s: [16]u8 = undefined;
134            mac.final(&s);
135
136            // XOR with nonce to get pre-tag
137            for (npub, 0..) |b, i| {
138                s[i] ^= b;
139            }
140
141            // Clear most significant bit of last byte
142            s[15] &= 0x7f;
143
144            // Encrypt to get tag
145            const tag_aes = Aes.initEnc(message_key);
146            tag_aes.encrypt(tag, &s);
147
148            // Use tag as initial counter for CTR mode
149            var counter: [16]u8 = tag.*;
150            counter[15] |= 0x80; // Set most significant bit
151
152            // Encrypt message using CTR mode with 32-bit little-endian counter
153            const aes_ctx = Aes.initEnc(message_key);
154            modes.ctrSlice(@TypeOf(aes_ctx), aes_ctx, c, m, counter, .little, 0, 4);
155        }
156
157        /// Decrypts and authenticates a message using AES-GCM-SIV.
158        ///
159        /// `m`: Message buffer to write the decrypted data to.
160        /// `c`: The ciphertext to decrypt.
161        /// `tag`: The authentication tag.
162        /// `ad`: The associated data.
163        /// `npub`: The nonce.
164        /// `key`: The decryption key.
165        /// Asserts `c.len == m.len`.
166        pub fn decrypt(m: []u8, c: []const u8, tag: [tag_length]u8, ad: []const u8, npub: [nonce_length]u8, key: [key_length]u8) AuthenticationError!void {
167            assert(c.len == m.len);
168            assert(c.len <= (1 << 36));
169            assert(ad.len <= (1 << 36));
170
171            var auth_key: [16]u8 = undefined;
172            var message_key: [key_length]u8 = undefined;
173            deriveKeys(&message_key, &auth_key, key, npub);
174
175            // Decrypt message using CTR mode with 32-bit little-endian counter
176            var counter: [16]u8 = tag;
177            counter[15] |= 0x80; // Set most significant bit
178
179            const aes_ctx = Aes.initEnc(message_key);
180            modes.ctrSlice(@TypeOf(aes_ctx), aes_ctx, m, c, counter, .little, 0, 4);
181
182            // Verify tag by recalculating POLYVAL
183            const block_count = (math.divCeil(usize, ad.len, Polyval.block_length) catch unreachable) +
184                (math.divCeil(usize, m.len, Polyval.block_length) catch unreachable) + 1;
185            var mac = Polyval.initForBlockCount(&auth_key, block_count);
186
187            // Process additional data
188            mac.update(ad);
189            mac.pad();
190
191            // Process decrypted plaintext
192            mac.update(m);
193            mac.pad();
194
195            // Length block
196            var length_block: [16]u8 = undefined;
197            mem.writeInt(u64, length_block[0..8], @as(u64, ad.len) * 8, .little);
198            mem.writeInt(u64, length_block[8..16], @as(u64, m.len) * 8, .little);
199            mac.update(&length_block);
200
201            // Get POLYVAL result
202            var s: [16]u8 = undefined;
203            mac.final(&s);
204
205            // XOR with nonce to get pre-tag
206            for (npub, 0..) |b, i| {
207                s[i] ^= b;
208            }
209
210            // Clear most significant bit of last byte
211            s[15] &= 0x7f;
212
213            // Encrypt to get expected tag
214            const tag_aes = Aes.initEnc(message_key);
215            var computed_tag: [tag_length]u8 = undefined;
216            tag_aes.encrypt(&computed_tag, &s);
217
218            // Verify tag
219            const verify = crypto.timing_safe.eql([tag_length]u8, computed_tag, tag);
220            if (!verify) {
221                crypto.secureZero(u8, &computed_tag);
222                @memset(m, undefined);
223                return error.AuthenticationFailed;
224            }
225        }
226    };
227}
228
229const htest = @import("test.zig");
230const testing = std.testing;
231
232test "Aes128GcmSiv - RFC 8452 Test Vector 1" {
233    // Test vector from RFC 8452 Appendix C.1
234    const key = [_]u8{
235        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
236        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
237    };
238    const nonce = [_]u8{
239        0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
240        0x00, 0x00, 0x00, 0x00,
241    };
242    const ad = "";
243    const m = "";
244    var c: [m.len]u8 = undefined;
245    var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
246
247    Aes128GcmSiv.encrypt(&c, &tag, m, ad, nonce, key);
248    try htest.assertEqual("dc20e2d83f25705bb49e439eca56de25", &tag);
249}
250
251test "Aes128GcmSiv - RFC 8452 Test Vector 2" {
252    // Test vector from RFC 8452 Appendix C.1
253    const key = [_]u8{
254        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
255        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
256    };
257    const nonce = [_]u8{
258        0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
259        0x00, 0x00, 0x00, 0x00,
260    };
261    const plaintext = [_]u8{
262        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
263    };
264    const ad = "";
265    var c: [plaintext.len]u8 = undefined;
266    var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
267
268    Aes128GcmSiv.encrypt(&c, &tag, &plaintext, ad, nonce, key);
269    try htest.assertEqual("b5d839330ac7b786", &c);
270    try htest.assertEqual("578782fff6013b815b287c22493a364c", &tag);
271
272    var m2: [plaintext.len]u8 = undefined;
273    try Aes128GcmSiv.decrypt(&m2, &c, tag, ad, nonce, key);
274    try testing.expectEqualSlices(u8, &plaintext, &m2);
275}
276
277test "Aes128GcmSiv - RFC 8452 Test Vector 3" {
278    // Test vector from RFC 8452 Appendix C.1
279    const key = [_]u8{
280        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
281        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
282    };
283    const nonce = [_]u8{
284        0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
285        0x00, 0x00, 0x00, 0x00,
286    };
287    const plaintext = [_]u8{
288        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
289        0x00, 0x00, 0x00, 0x00,
290    };
291    const ad = "";
292    var c: [plaintext.len]u8 = undefined;
293    var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
294
295    Aes128GcmSiv.encrypt(&c, &tag, &plaintext, ad, nonce, key);
296    try htest.assertEqual("7323ea61d05932260047d942", &c);
297    try htest.assertEqual("a4978db357391a0bc4fdec8b0d106639", &tag);
298
299    var m2: [plaintext.len]u8 = undefined;
300    try Aes128GcmSiv.decrypt(&m2, &c, tag, ad, nonce, key);
301    try testing.expectEqualSlices(u8, &plaintext, &m2);
302}
303
304test "Aes256GcmSiv - RFC 8452 Test Vector" {
305    // Test vector from RFC 8452 Appendix C.2
306    const key = [_]u8{
307        0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
308        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
309        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
310        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
311    };
312    const nonce = [_]u8{
313        0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
314        0x00, 0x00, 0x00, 0x00,
315    };
316    const ad = "";
317    const m = "";
318    var c: [m.len]u8 = undefined;
319    var tag: [Aes256GcmSiv.tag_length]u8 = undefined;
320
321    Aes256GcmSiv.encrypt(&c, &tag, m, ad, nonce, key);
322    try htest.assertEqual("07f5f4169bbf55a8400cd47ea6fd400f", &tag);
323}
324
325test "Aes128GcmSiv - Decrypt with wrong tag" {
326    const key: [Aes128GcmSiv.key_length]u8 = @splat(0x69);
327    const nonce: [Aes128GcmSiv.nonce_length]u8 = @splat(0x42);
328    const m = "Test message";
329    const ad = "";
330    var c: [m.len]u8 = undefined;
331    var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
332
333    Aes128GcmSiv.encrypt(&c, &tag, m, ad, nonce, key);
334
335    // Corrupt the tag
336    tag[0] ^= 0x01;
337
338    var m2: [m.len]u8 = undefined;
339    try testing.expectError(error.AuthenticationFailed, Aes128GcmSiv.decrypt(&m2, &c, tag, ad, nonce, key));
340}