authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-09-16 23:01:16+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-09-16 23:01:16+02:00
log5eb7610112f58ff5dc07fb4b3bdf539fecafc17f
tree5cba8af32335693fefcf6990a3bda49d73c76ec7
parentdd46e07fb95c6c5952b3dc41307a5aa4e130ba6a

Import crypto/aes_gcm_siv.zig


1 files changed, 347 insertions(+), 0 deletions(-)

lib/std/crypto/aes_gcm_siv.zig created+347
......@@ -0,0 +1,347 @@
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 var key_block: [16]u8 = undefined;
53 var cipher_out: [16]u8 = undefined;
54
55 // Generate authentication key (128 bits = 2 * 8 bytes)
56 // Block 0: counter = 0 with nonce
57 mem.writeInt(u32, key_block[0..4], 0, .little);
58 key_block[4..16].* = nonce;
59 aes.encrypt(&cipher_out, &key_block);
60 @memcpy(auth_key[0..8], cipher_out[0..8]);
61
62 // Block 1: counter = 1 with nonce
63 mem.writeInt(u32, key_block[0..4], 1, .little);
64 aes.encrypt(&cipher_out, &key_block);
65 @memcpy(auth_key[8..16], cipher_out[0..8]);
66
67 // Generate message encryption key
68 if (key_length == 16) {
69 // AES-128-GCM-SIV: 128-bit message key = 2 * 8 bytes
70 // Block 2: counter = 2 with nonce
71 mem.writeInt(u32, key_block[0..4], 2, .little);
72 aes.encrypt(&cipher_out, &key_block);
73 @memcpy(message_key[0..8], cipher_out[0..8]);
74
75 // Block 3: counter = 3 with nonce
76 mem.writeInt(u32, key_block[0..4], 3, .little);
77 aes.encrypt(&cipher_out, &key_block);
78 @memcpy(message_key[8..16], cipher_out[0..8]);
79 } else {
80 // AES-256-GCM-SIV: 256-bit message key = 4 * 8 bytes
81 // Block 2: counter = 2 with nonce
82 mem.writeInt(u32, key_block[0..4], 2, .little);
83 aes.encrypt(&cipher_out, &key_block);
84 @memcpy(message_key[0..8], cipher_out[0..8]);
85
86 // Block 3: counter = 3 with nonce
87 mem.writeInt(u32, key_block[0..4], 3, .little);
88 aes.encrypt(&cipher_out, &key_block);
89 @memcpy(message_key[8..16], cipher_out[0..8]);
90
91 // Block 4: counter = 4 with nonce
92 mem.writeInt(u32, key_block[0..4], 4, .little);
93 aes.encrypt(&cipher_out, &key_block);
94 @memcpy(message_key[16..24], cipher_out[0..8]);
95
96 // Block 5: counter = 5 with nonce
97 mem.writeInt(u32, key_block[0..4], 5, .little);
98 aes.encrypt(&cipher_out, &key_block);
99 @memcpy(message_key[24..32], cipher_out[0..8]);
100 }
101 }
102
103 /// Encrypts and authenticates a message using AES-GCM-SIV.
104 ///
105 /// `c`: The ciphertext buffer to write the encrypted data to.
106 /// `tag`: The authentication tag buffer to write the computed tag to.
107 /// `m`: The plaintext message to encrypt.
108 /// `ad`: The associated data to authenticate.
109 /// `npub`: The nonce to use for encryption.
110 /// `key`: The encryption key.
111 pub fn encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, key: [key_length]u8) void {
112 debug.assert(c.len == m.len);
113 debug.assert(m.len <= (1 << 36));
114 debug.assert(ad.len <= (1 << 36));
115
116 var auth_key: [16]u8 = undefined;
117 var message_key: [key_length]u8 = undefined;
118 deriveKeys(&message_key, &auth_key, key, npub);
119
120 // Calculate POLYVAL over additional data and plaintext
121 const block_count = (math.divCeil(usize, ad.len, Polyval.block_length) catch unreachable) +
122 (math.divCeil(usize, m.len, Polyval.block_length) catch unreachable) + 1;
123 var mac = Polyval.initForBlockCount(&auth_key, block_count);
124
125 // Process additional data
126 mac.update(ad);
127 mac.pad();
128
129 // Process plaintext
130 mac.update(m);
131 mac.pad();
132
133 // Length block
134 var length_block: [16]u8 = undefined;
135 mem.writeInt(u64, length_block[0..8], @as(u64, ad.len) * 8, .little);
136 mem.writeInt(u64, length_block[8..16], @as(u64, m.len) * 8, .little);
137 mac.update(&length_block);
138
139 // Get POLYVAL result
140 var s: [16]u8 = undefined;
141 mac.final(&s);
142
143 // XOR with nonce to get pre-tag
144 for (npub, 0..) |b, i| {
145 s[i] ^= b;
146 }
147
148 // Clear most significant bit of last byte
149 s[15] &= 0x7f;
150
151 // Encrypt to get tag
152 const tag_aes = Aes.initEnc(message_key);
153 tag_aes.encrypt(tag, &s);
154
155 // Use tag as initial counter for CTR mode
156 var counter: [16]u8 = tag.*;
157 counter[15] |= 0x80; // Set most significant bit
158
159 // Encrypt message using CTR mode with 32-bit little-endian counter
160 const aes_ctx = Aes.initEnc(message_key);
161 modes.ctrSlice(@TypeOf(aes_ctx), aes_ctx, c, m, counter, .little, 0, 4);
162 }
163
164 /// Decrypts and authenticates a message using AES-GCM-SIV.
165 ///
166 /// `m`: Message buffer to write the decrypted data to.
167 /// `c`: The ciphertext to decrypt.
168 /// `tag`: The authentication tag.
169 /// `ad`: The associated data.
170 /// `npub`: The nonce.
171 /// `key`: The decryption key.
172 /// Asserts `c.len == m.len`.
173 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 {
174 assert(c.len == m.len);
175 assert(c.len <= (1 << 36));
176 assert(ad.len <= (1 << 36));
177
178 var auth_key: [16]u8 = undefined;
179 var message_key: [key_length]u8 = undefined;
180 deriveKeys(&message_key, &auth_key, key, npub);
181
182 // Decrypt message using CTR mode with 32-bit little-endian counter
183 var counter: [16]u8 = tag;
184 counter[15] |= 0x80; // Set most significant bit
185
186 const aes_ctx = Aes.initEnc(message_key);
187 modes.ctrSlice(@TypeOf(aes_ctx), aes_ctx, m, c, counter, .little, 0, 4);
188
189 // Verify tag by recalculating POLYVAL
190 const block_count = (math.divCeil(usize, ad.len, Polyval.block_length) catch unreachable) +
191 (math.divCeil(usize, m.len, Polyval.block_length) catch unreachable) + 1;
192 var mac = Polyval.initForBlockCount(&auth_key, block_count);
193
194 // Process additional data
195 mac.update(ad);
196 mac.pad();
197
198 // Process decrypted plaintext
199 mac.update(m);
200 mac.pad();
201
202 // Length block
203 var length_block: [16]u8 = undefined;
204 mem.writeInt(u64, length_block[0..8], @as(u64, ad.len) * 8, .little);
205 mem.writeInt(u64, length_block[8..16], @as(u64, m.len) * 8, .little);
206 mac.update(&length_block);
207
208 // Get POLYVAL result
209 var s: [16]u8 = undefined;
210 mac.final(&s);
211
212 // XOR with nonce to get pre-tag
213 for (npub, 0..) |b, i| {
214 s[i] ^= b;
215 }
216
217 // Clear most significant bit of last byte
218 s[15] &= 0x7f;
219
220 // Encrypt to get expected tag
221 const tag_aes = Aes.initEnc(message_key);
222 var computed_tag: [tag_length]u8 = undefined;
223 tag_aes.encrypt(&computed_tag, &s);
224
225 // Verify tag
226 const verify = crypto.timing_safe.eql([tag_length]u8, computed_tag, tag);
227 if (!verify) {
228 crypto.secureZero(u8, &computed_tag);
229 @memset(m, undefined);
230 return error.AuthenticationFailed;
231 }
232 }
233 };
234}
235
236const htest = @import("test.zig");
237const testing = std.testing;
238
239test "Aes128GcmSiv - RFC 8452 Test Vector 1" {
240 // Test vector from RFC 8452 Appendix C.1
241 const key = [_]u8{
242 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
243 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
244 };
245 const nonce = [_]u8{
246 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
247 0x00, 0x00, 0x00, 0x00,
248 };
249 const ad = "";
250 const m = "";
251 var c: [m.len]u8 = undefined;
252 var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
253
254 Aes128GcmSiv.encrypt(&c, &tag, m, ad, nonce, key);
255 try htest.assertEqual("dc20e2d83f25705bb49e439eca56de25", &tag);
256}
257
258test "Aes128GcmSiv - RFC 8452 Test Vector 2" {
259 // Test vector from RFC 8452 Appendix C.1
260 const key = [_]u8{
261 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
262 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
263 };
264 const nonce = [_]u8{
265 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
266 0x00, 0x00, 0x00, 0x00,
267 };
268 const plaintext = [_]u8{
269 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
270 };
271 const ad = "";
272 var c: [plaintext.len]u8 = undefined;
273 var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
274
275 Aes128GcmSiv.encrypt(&c, &tag, &plaintext, ad, nonce, key);
276 try htest.assertEqual("b5d839330ac7b786", &c);
277 try htest.assertEqual("578782fff6013b815b287c22493a364c", &tag);
278
279 var m2: [plaintext.len]u8 = undefined;
280 try Aes128GcmSiv.decrypt(&m2, &c, tag, ad, nonce, key);
281 try testing.expectEqualSlices(u8, &plaintext, &m2);
282}
283
284test "Aes128GcmSiv - RFC 8452 Test Vector 3" {
285 // Test vector from RFC 8452 Appendix C.1
286 const key = [_]u8{
287 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
288 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
289 };
290 const nonce = [_]u8{
291 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
292 0x00, 0x00, 0x00, 0x00,
293 };
294 const plaintext = [_]u8{
295 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
296 0x00, 0x00, 0x00, 0x00,
297 };
298 const ad = "";
299 var c: [plaintext.len]u8 = undefined;
300 var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
301
302 Aes128GcmSiv.encrypt(&c, &tag, &plaintext, ad, nonce, key);
303 try htest.assertEqual("7323ea61d05932260047d942", &c);
304 try htest.assertEqual("a4978db357391a0bc4fdec8b0d106639", &tag);
305
306 var m2: [plaintext.len]u8 = undefined;
307 try Aes128GcmSiv.decrypt(&m2, &c, tag, ad, nonce, key);
308 try testing.expectEqualSlices(u8, &plaintext, &m2);
309}
310
311test "Aes256GcmSiv - RFC 8452 Test Vector" {
312 // Test vector from RFC 8452 Appendix C.2
313 const key = [_]u8{
314 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
315 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
316 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
317 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
318 };
319 const nonce = [_]u8{
320 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
321 0x00, 0x00, 0x00, 0x00,
322 };
323 const ad = "";
324 const m = "";
325 var c: [m.len]u8 = undefined;
326 var tag: [Aes256GcmSiv.tag_length]u8 = undefined;
327
328 Aes256GcmSiv.encrypt(&c, &tag, m, ad, nonce, key);
329 try htest.assertEqual("07f5f4169bbf55a8400cd47ea6fd400f", &tag);
330}
331
332test "Aes128GcmSiv - Decrypt with wrong tag" {
333 const key: [Aes128GcmSiv.key_length]u8 = @splat(0x69);
334 const nonce: [Aes128GcmSiv.nonce_length]u8 = @splat(0x42);
335 const m = "Test message";
336 const ad = "";
337 var c: [m.len]u8 = undefined;
338 var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
339
340 Aes128GcmSiv.encrypt(&c, &tag, m, ad, nonce, key);
341
342 // Corrupt the tag
343 tag[0] ^= 0x01;
344
345 var m2: [m.len]u8 = undefined;
346 try testing.expectError(error.AuthenticationFailed, Aes128GcmSiv.decrypt(&m2, &c, tag, ad, nonce, key));
347}