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}