authorgravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-09-16 12:07:50+02:00
committergravatar for 124872+jedisct1@users.noreply.github.comFrank Denis <124872+jedisct1@users.noreply.github.com> 2025-09-16 12:45:08+02:00
logdd46e07fb95c6c5952b3dc41307a5aa4e130ba6a
treebb8665195bc830a2cfc870fe465e80cfdcdcc929
parent496313a1bd4e8a4663ea99942d179a52b57e04e6

std.crypto: add AES-SIV and AES-GCM-SIV

The Zig standard library lacked schemes that resist nonce reuse. AES-SIV and AES-GCM-SIV are the standard options for this. AES-GCM-SIV can be very useful when Zig is used to target embedded systems, and AES-SIV is especially useful for key wrapping. Also take it as an opportunity to add a bunch of test vectors to modes.ctr and make sure it works with block ciphers whose size is not 16.

4 files changed, 689 insertions(+), 37 deletions(-)

lib/std/crypto.zig+16
......@@ -31,6 +31,16 @@ pub const aead = struct {
3131 pub const Aes256Gcm = @import("crypto/aes_gcm.zig").Aes256Gcm;
3232 };
3333
34 pub const aes_gcm_siv = struct {
35 pub const Aes128GcmSiv = @import("crypto/aes_gcm_siv.zig").Aes128GcmSiv;
36 pub const Aes256GcmSiv = @import("crypto/aes_gcm_siv.zig").Aes256GcmSiv;
37 };
38
39 pub const aes_siv = struct {
40 pub const Aes128Siv = @import("crypto/aes_siv.zig").Aes128Siv;
41 pub const Aes256Siv = @import("crypto/aes_siv.zig").Aes256Siv;
42 };
43
3444 pub const aes_ocb = struct {
3545 pub const Aes128Ocb = @import("crypto/aes_ocb.zig").Aes128Ocb;
3646 pub const Aes256Ocb = @import("crypto/aes_ocb.zig").Aes256Ocb;
......@@ -249,6 +259,12 @@ test {
249259 _ = aead.aes_gcm.Aes128Gcm;
250260 _ = aead.aes_gcm.Aes256Gcm;
251261
262 _ = aead.aes_gcm_siv.Aes128GcmSiv;
263 _ = aead.aes_gcm_siv.Aes256GcmSiv;
264
265 _ = aead.aes_siv.Aes128Siv;
266 _ = aead.aes_siv.Aes256Siv;
267
252268 _ = aead.aes_ocb.Aes128Ocb;
253269 _ = aead.aes_ocb.Aes256Ocb;
254270
lib/std/crypto/aes.zig-25
......@@ -28,31 +28,6 @@ pub const AesDecryptCtx = impl.AesDecryptCtx;
2828pub const Aes128 = impl.Aes128;
2929pub const Aes256 = impl.Aes256;
3030
31test "ctr" {
32 // NIST SP 800-38A pp 55-58
33 const ctr = @import("modes.zig").ctr;
34
35 const key = [_]u8{ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c };
36 const iv = [_]u8{ 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff };
37 const in = [_]u8{
38 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a,
39 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51,
40 0x30, 0xc8, 0x1c, 0x46, 0xa3, 0x5c, 0xe4, 0x11, 0xe5, 0xfb, 0xc1, 0x19, 0x1a, 0x0a, 0x52, 0xef,
41 0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, 0xe6, 0x6c, 0x37, 0x10,
42 };
43 const exp_out = [_]u8{
44 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce,
45 0x98, 0x06, 0xf6, 0x6b, 0x79, 0x70, 0xfd, 0xff, 0x86, 0x17, 0x18, 0x7b, 0xb9, 0xff, 0xfd, 0xff,
46 0x5a, 0xe4, 0xdf, 0x3e, 0xdb, 0xd5, 0xd3, 0x5e, 0x5b, 0x4f, 0x09, 0x02, 0x0d, 0xb0, 0x3e, 0xab,
47 0x1e, 0x03, 0x1d, 0xda, 0x2f, 0xbe, 0x03, 0xd1, 0x79, 0x21, 0x70, 0xa0, 0xf3, 0x00, 0x9c, 0xee,
48 };
49
50 var out: [exp_out.len]u8 = undefined;
51 const ctx = Aes128.initEnc(key);
52 ctr(AesEncryptCtx(Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
53 try testing.expectEqualSlices(u8, exp_out[0..], out[0..]);
54}
55
5631test "encrypt" {
5732 // Appendix B
5833 {
lib/std/crypto/aes_siv.zig created+481
......@@ -0,0 +1,481 @@
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 = crypto.core.modes;
8const Cmac = @import("cmac.zig").Cmac;
9const AuthenticationError = crypto.errors.AuthenticationError;
10
11pub const Aes128Siv = AesSiv(crypto.core.aes.Aes128);
12pub const Aes256Siv = AesSiv(crypto.core.aes.Aes256);
13
14/// AES-SIV: Deterministic authenticated encryption - the same message always produces the same ciphertext.
15///
16/// What it does: Encrypts data and protects it from tampering. Unlike most encryption modes,
17/// AES-SIV is deterministic: encrypting the same message with the same key always produces
18/// the same ciphertext (unless you provide an optional nonce).
19///
20/// When to use AES-SIV:
21/// - When you need deterministic encryption (e.g., for deduplication in encrypted storage)
22/// - When you can't store or generate nonces
23/// - For key wrapping (protecting cryptographic keys)
24/// - When you need to search encrypted data without decrypting it
25///
26/// When NOT to use AES-SIV:
27/// - When identical plaintexts must produce different ciphertexts (use AES-GCM or AES-GCM-SIV)
28/// - For network protocols where replay attacks are a concern
29///
30/// Unique features:
31/// - Optional nonce: You can add a nonce to make encryption non-deterministic, but this is optional
32/// - Multiple associated data: Supports a vector of associated data strings instead of just one.
33/// The algorithm cryptographically ensures each component is properly separated, preventing
34/// canonicalization attacks where different splits of data could be accepted as valid.
35///
36/// Security properties:
37/// - Deterministic: Same input always gives same output (this can leak information about patterns)
38/// - Nonce misuse resistant: Doesn't catastrophically fail if you reuse a nonce
39/// - Key commitment: Ciphertext can only be decrypted with the exact key that encrypted it
40///
41/// AES-SIV has better security properties than AES-GCM-SIV, but is must slower.
42///
43/// How it works: Combines two keys - one for authentication (S2V) and one for encryption (CTR mode).
44/// The total key size is double the AES key size (256 bits for AES-128-SIV, 512 bits for AES-256-SIV).
45///
46/// Defined in RFC 5297.
47fn AesSiv(comptime Aes: anytype) type {
48 debug.assert(Aes.block.block_length == 16);
49
50 return struct {
51 pub const tag_length = 16;
52 pub const key_length = Aes.key_bits / 8 * 2; // SIV uses 2x key size
53
54 const CmacImpl = Cmac(Aes);
55
56 /// S2V (String to Vector) - RFC 5297 Section 2.4
57 /// Derives a synthetic IV from the key and input strings using CMAC.
58 /// This function implements a cryptographic pseudo-random function that maps
59 /// a variable-length vector of strings to a fixed 128-bit output.
60 fn s2v(iv: *[16]u8, key: [Aes.key_bits / 8]u8, strings: []const []const u8) void {
61 assert(strings.len > 0);
62 assert(strings.len <= 127); // S2V limitation
63
64 var d: [16]u8 = undefined;
65
66 // Special case: single empty string
67 if (strings.len == 1 and strings[0].len == 0) {
68 CmacImpl.create(&d, &[_]u8{}, &key);
69 iv.* = d;
70 return;
71 }
72
73 // Initialize with CMAC of zero block
74 const zero_block: [16]u8 = @splat(0);
75 CmacImpl.create(&d, &zero_block, &key);
76
77 // Process all strings except the last one
78 var i: usize = 0;
79 while (i < strings.len - 1) : (i += 1) {
80 d = dbl(d);
81 var tmp: [16]u8 = undefined;
82 CmacImpl.create(&tmp, strings[i], &key);
83 for (&d, tmp) |*b, t| {
84 b.* ^= t;
85 }
86 }
87
88 // Process the final string
89 const sn = strings[strings.len - 1];
90 if (sn.len >= 16) {
91 // XOR d with the first 16 bytes of Sn
92 var xored_msg_buf: [4096]u8 = undefined;
93 const xored_len = @min(sn.len, xored_msg_buf.len);
94 @memcpy(xored_msg_buf[0..xored_len], sn[0..xored_len]);
95
96 for (d, 0..) |b, j| {
97 xored_msg_buf[j] ^= b;
98 }
99
100 CmacImpl.create(iv, xored_msg_buf[0..xored_len], &key);
101 } else {
102 // Pad and XOR
103 d = dbl(d);
104 var padded: [16]u8 = @splat(0);
105 @memcpy(padded[0..sn.len], sn);
106 padded[sn.len] = 0x80;
107 for (&d, padded) |*b, p| {
108 b.* ^= p;
109 }
110 CmacImpl.create(iv, &d, &key);
111 }
112 }
113
114 /// Double operation as defined in RFC 5297.
115 /// Performs multiplication by x (i.e., left shift by 1) in GF(2^128).
116 /// This is the same operation used in CMAC subkey generation.
117 /// If the MSB is set, XORs with the polynomial 0x87 after shifting.
118 fn dbl(d: [16]u8) [16]u8 {
119 // Read as big-endian 128-bit integer
120 const val = mem.readInt(u128, &d, .big);
121
122 // Left shift by 1, and XOR with 0x87 if MSB was set
123 const doubled = (val << 1) ^ (0x87 & -%(@as(u128, val >> 127)));
124
125 // Write back as big-endian
126 var result: [16]u8 = undefined;
127 mem.writeInt(u128, &result, doubled, .big);
128 return result;
129 }
130
131 /// Encrypt plaintext using AES-SIV
132 /// `c`: Output buffer for ciphertext (same size as plaintext)
133 /// `tag`: Output buffer for authentication tag (synthetic IV)
134 /// `m`: Plaintext to encrypt
135 /// `ad`: Optional associated data
136 /// `nonce`: Optional nonce (if provided, will be added as last AD component)
137 /// `key`: Combined key (2x AES key size)
138 pub fn encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: ?[]const u8, nonce: ?[]const u8, key: [key_length]u8) void {
139 debug.assert(c.len == m.len);
140
141 // Split key into K1 (for S2V) and K2 (for CTR)
142 const k1 = key[0 .. Aes.key_bits / 8];
143 const k2 = key[Aes.key_bits / 8 ..];
144
145 // Prepare strings for S2V: AD components followed by plaintext
146 var strings_buf: [128][]const u8 = undefined;
147 var strings_len: usize = 0;
148
149 if (ad) |a| {
150 strings_buf[strings_len] = a;
151 strings_len += 1;
152 }
153 if (nonce) |n| {
154 strings_buf[strings_len] = n;
155 strings_len += 1;
156 }
157 strings_buf[strings_len] = m;
158 strings_len += 1;
159
160 // Compute synthetic IV using S2V
161 s2v(tag, k1.*, strings_buf[0..strings_len]);
162
163 // Clear the 31st and 63rd bits for use as CTR IV
164 var ctr_iv = tag.*;
165 ctr_iv[8] &= 0x7f;
166 ctr_iv[12] &= 0x7f;
167
168 // Encrypt plaintext using CTR mode
169 const aes_ctx = Aes.initEnc(k2.*);
170 modes.ctr(@TypeOf(aes_ctx), aes_ctx, c, m, ctr_iv, .big);
171 }
172
173 /// Decrypt ciphertext using AES-SIV
174 /// `m`: Output buffer for decrypted plaintext
175 /// `c`: Ciphertext to decrypt
176 /// `tag`: Authentication tag (synthetic IV)
177 /// `ad`: Optional associated data (must match encryption)
178 /// `nonce`: Optional nonce (must match encryption)
179 /// `key`: Combined key (2x AES key size)
180 pub fn decrypt(m: []u8, c: []const u8, tag: [tag_length]u8, ad: ?[]const u8, nonce: ?[]const u8, key: [key_length]u8) AuthenticationError!void {
181 assert(c.len == m.len);
182
183 // Split key into K1 (for S2V) and K2 (for CTR)
184 const k1 = key[0 .. Aes.key_bits / 8];
185 const k2 = key[Aes.key_bits / 8 ..];
186
187 // Clear the 31st and 63rd bits for use as CTR IV
188 var ctr_iv = tag;
189 ctr_iv[8] &= 0x7f;
190 ctr_iv[12] &= 0x7f;
191
192 // Decrypt ciphertext using CTR mode
193 const aes_ctx = Aes.initEnc(k2.*);
194 modes.ctr(@TypeOf(aes_ctx), aes_ctx, m, c, ctr_iv, .big);
195
196 // Prepare strings for S2V: AD components followed by plaintext
197 var strings_buf: [128][]const u8 = undefined;
198 var strings_len: usize = 0;
199
200 if (ad) |a| {
201 strings_buf[strings_len] = a;
202 strings_len += 1;
203 }
204 if (nonce) |n| {
205 strings_buf[strings_len] = n;
206 strings_len += 1;
207 }
208 strings_buf[strings_len] = m;
209 strings_len += 1;
210
211 // Verify synthetic IV using S2V
212 var computed_tag: [tag_length]u8 = undefined;
213 s2v(&computed_tag, k1.*, strings_buf[0..strings_len]);
214
215 // Verify tag
216 const verify = crypto.timing_safe.eql([tag_length]u8, computed_tag, tag);
217 if (!verify) {
218 crypto.secureZero(u8, &computed_tag);
219 @memset(m, undefined);
220 return error.AuthenticationFailed;
221 }
222 }
223
224 /// Encrypts plaintext with multiple associated data components.
225 /// This is the most general form of AES-SIV encryption that accepts
226 /// an arbitrary vector of associated data strings as specified in RFC 5297.
227 pub fn encryptWithAdVector(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const []const u8, key: [key_length]u8) void {
228 debug.assert(c.len == m.len);
229
230 // Split key into K1 (for S2V) and K2 (for CTR)
231 const k1 = key[0 .. Aes.key_bits / 8];
232 const k2 = key[Aes.key_bits / 8 ..];
233
234 // Prepare strings for S2V: AD components followed by plaintext
235 var strings_buf: [128][]const u8 = undefined;
236 var strings_len: usize = 0;
237
238 for (ad) |a| {
239 strings_buf[strings_len] = a;
240 strings_len += 1;
241 }
242 strings_buf[strings_len] = m;
243 strings_len += 1;
244
245 // Compute synthetic IV using S2V
246 s2v(tag, k1.*, strings_buf[0..strings_len]);
247
248 // Clear the 31st and 63rd bits for use as CTR IV
249 var ctr_iv = tag.*;
250 ctr_iv[8] &= 0x7f;
251 ctr_iv[12] &= 0x7f;
252
253 // Encrypt plaintext using CTR mode
254 const aes_ctx = Aes.initEnc(k2.*);
255 modes.ctr(@TypeOf(aes_ctx), aes_ctx, c, m, ctr_iv, .big);
256 }
257
258 /// Decrypts ciphertext with multiple associated data components.
259 /// This is the most general form of AES-SIV decryption that accepts
260 /// an arbitrary vector of associated data strings as specified in RFC 5297.
261 pub fn decryptWithAdVector(m: []u8, c: []const u8, tag: [tag_length]u8, ad: []const []const u8, key: [key_length]u8) AuthenticationError!void {
262 assert(c.len == m.len);
263
264 // Split key into K1 (for S2V) and K2 (for CTR)
265 const k1 = key[0 .. Aes.key_bits / 8];
266 const k2 = key[Aes.key_bits / 8 ..];
267
268 // Clear the 31st and 63rd bits for use as CTR IV
269 var ctr_iv = tag;
270 ctr_iv[8] &= 0x7f;
271 ctr_iv[12] &= 0x7f;
272
273 // Decrypt ciphertext using CTR mode
274 const aes_ctx = Aes.initEnc(k2.*);
275 modes.ctr(@TypeOf(aes_ctx), aes_ctx, m, c, ctr_iv, .big);
276
277 // Prepare strings for S2V: AD components followed by plaintext
278 var strings_buf: [128][]const u8 = undefined;
279 var strings_len: usize = 0;
280
281 for (ad) |a| {
282 strings_buf[strings_len] = a;
283 strings_len += 1;
284 }
285 strings_buf[strings_len] = m;
286 strings_len += 1;
287
288 // Verify synthetic IV using S2V
289 var computed_tag: [tag_length]u8 = undefined;
290 s2v(&computed_tag, k1.*, strings_buf[0..strings_len]);
291
292 // Verify tag
293 const verify = crypto.timing_safe.eql([tag_length]u8, computed_tag, tag);
294 if (!verify) {
295 crypto.secureZero(u8, &computed_tag);
296 @memset(m, undefined);
297 return error.AuthenticationFailed;
298 }
299 }
300 };
301}
302
303const htest = @import("test.zig");
304const testing = std.testing;
305
306test "AES-SIV double operation" {
307 const AesSivTest = AesSiv(crypto.core.aes.Aes128);
308
309 // Test vector from RFC 5297
310 const input = [_]u8{ 0x0e, 0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e };
311 const expected = [_]u8{ 0x1c, 0x08, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1a, 0x1c };
312
313 const result = AesSivTest.dbl(input);
314 try testing.expectEqualSlices(u8, &expected, &result);
315}
316
317test "AES-SIV double operation with MSB set" {
318 const AesSivTest = AesSiv(crypto.core.aes.Aes128);
319
320 const input = [_]u8{ 0xe0, 0x40, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0 };
321 const expected = [_]u8{ 0xc0, 0x80, 0x20, 0x40, 0x60, 0x80, 0xa0, 0xc0, 0xe1, 0x01, 0x21, 0x41, 0x61, 0x81, 0xa1, 0x47 };
322
323 const result = AesSivTest.dbl(input);
324 try testing.expectEqualSlices(u8, &expected, &result);
325}
326
327test "Aes128Siv - RFC 5297 Test Vector A.1" {
328 // Test vector from RFC 5297 Appendix A.1
329 const key = [_]u8{
330 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf2, 0xf1, 0xf0,
331 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,
332 };
333 const ad = [_]u8{
334 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
335 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
336 };
337 const plaintext = [_]u8{
338 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
339 };
340
341 var ciphertext: [plaintext.len]u8 = undefined;
342 var tag: [16]u8 = undefined;
343
344 // Test using vector API for RFC compliance
345 const ad_components = [_][]const u8{&ad};
346 Aes128Siv.encryptWithAdVector(&ciphertext, &tag, &plaintext, &ad_components, key);
347
348 // Expected values from RFC 5297
349 try htest.assertEqual("85632d07c6e8f37f950acd320a2ecc93", &tag);
350 try htest.assertEqual("40c02b9690c4dc04daef7f6afe5c", &ciphertext);
351
352 // Test decryption
353 var decrypted: [plaintext.len]u8 = undefined;
354 try Aes128Siv.decryptWithAdVector(&decrypted, &ciphertext, tag, &ad_components, key);
355 try testing.expectEqualSlices(u8, &plaintext, &decrypted);
356}
357
358test "Aes128Siv - empty plaintext" {
359 const key: [32]u8 = @splat(0x42);
360 const plaintext = "";
361 const ad = "additional data";
362
363 var ciphertext: [plaintext.len]u8 = undefined;
364 var tag: [16]u8 = undefined;
365
366 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, ad, null, key);
367
368 var decrypted: [plaintext.len]u8 = undefined;
369 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, ad, null, key);
370}
371
372test "Aes128Siv - with nonce" {
373 const key: [32]u8 = @splat(0x69);
374 const nonce: [16]u8 = @splat(0x42);
375 const plaintext = "Hello, AES-SIV!";
376 const ad = "metadata";
377
378 var ciphertext: [plaintext.len]u8 = undefined;
379 var tag: [16]u8 = undefined;
380
381 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, ad, &nonce, key);
382
383 var decrypted: [plaintext.len]u8 = undefined;
384 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, ad, &nonce, key);
385 try testing.expectEqualSlices(u8, plaintext, &decrypted);
386}
387
388test "Aes256Siv - basic functionality" {
389 const key: [64]u8 = @splat(0x96);
390 const plaintext = "Test message for AES-256-SIV";
391 const ad1 = "header";
392 const ad2 = "more data";
393
394 var ciphertext: [plaintext.len]u8 = undefined;
395 var tag: [16]u8 = undefined;
396
397 // Test with multiple AD components using the vector API
398 const ad_components = [_][]const u8{ ad1, ad2 };
399 Aes256Siv.encryptWithAdVector(&ciphertext, &tag, plaintext, &ad_components, key);
400
401 var decrypted: [plaintext.len]u8 = undefined;
402 try Aes256Siv.decryptWithAdVector(&decrypted, &ciphertext, tag, &ad_components, key);
403 try testing.expectEqualSlices(u8, plaintext, &decrypted);
404}
405
406test "Aes128Siv - demonstrating optional parameters" {
407 const key: [32]u8 = @splat(0x77);
408
409 // Test 1: No AD, no nonce (pure deterministic)
410 {
411 const plaintext = "Deterministic encryption";
412 var ciphertext: [plaintext.len]u8 = undefined;
413 var tag: [16]u8 = undefined;
414
415 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, null, null, key);
416
417 var decrypted: [plaintext.len]u8 = undefined;
418 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, null, null, key);
419 try testing.expectEqualSlices(u8, plaintext, &decrypted);
420 }
421
422 // Test 2: With AD, no nonce
423 {
424 const plaintext = "With associated data";
425 const ad = "some context";
426 var ciphertext: [plaintext.len]u8 = undefined;
427 var tag: [16]u8 = undefined;
428
429 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, ad, null, key);
430
431 var decrypted: [plaintext.len]u8 = undefined;
432 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, ad, null, key);
433 try testing.expectEqualSlices(u8, plaintext, &decrypted);
434 }
435
436 // Test 3: No AD, with nonce
437 {
438 const plaintext = "Nonce-based encryption";
439 const nonce: [12]u8 = @splat(0x01);
440 var ciphertext: [plaintext.len]u8 = undefined;
441 var tag: [16]u8 = undefined;
442
443 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, null, &nonce, key);
444
445 var decrypted: [plaintext.len]u8 = undefined;
446 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, null, &nonce, key);
447 try testing.expectEqualSlices(u8, plaintext, &decrypted);
448 }
449
450 // Test 4: With both AD and nonce
451 {
452 const plaintext = "Full featured";
453 const ad = "context";
454 const nonce: [16]u8 = @splat(0x02);
455 var ciphertext: [plaintext.len]u8 = undefined;
456 var tag: [16]u8 = undefined;
457
458 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, ad, &nonce, key);
459
460 var decrypted: [plaintext.len]u8 = undefined;
461 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, ad, &nonce, key);
462 try testing.expectEqualSlices(u8, plaintext, &decrypted);
463 }
464}
465
466test "Aes128Siv - authentication failure" {
467 const key: [32]u8 = @splat(0x13);
468 const plaintext = "Secret message";
469 const ad = "";
470
471 var ciphertext: [plaintext.len]u8 = undefined;
472 var tag: [16]u8 = undefined;
473
474 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, ad, null, key);
475
476 // Corrupt the tag
477 tag[0] ^= 0x01;
478
479 var decrypted: [plaintext.len]u8 = undefined;
480 try testing.expectError(error.AuthenticationFailed, Aes128Siv.decrypt(&decrypted, &ciphertext, tag, ad, null, key));
481}
lib/std/crypto/modes.zig+192-12
......@@ -11,37 +11,217 @@ const debug = std.debug;
1111/// Important: the counter mode doesn't provide authenticated encryption: the ciphertext can be trivially modified without this being detected.
1212/// As a result, applications should generally never use it directly, but only in a construction that includes a MAC.
1313pub fn ctr(comptime BlockCipher: anytype, block_cipher: BlockCipher, dst: []u8, src: []const u8, iv: [BlockCipher.block_length]u8, endian: std.builtin.Endian) void {
14 ctrSlice(BlockCipher, block_cipher, dst, src, iv, endian, 0, BlockCipher.block_length);
15}
16
17/// Counter mode with configurable counter position and size.
18///
19/// This extended version allows specifying where the counter is located within the IV block
20/// and how many bytes it occupies. This is useful for modes like AES-GCM-SIV which use a
21/// 32-bit counter at the beginning of the block.
22///
23/// @param counter_offset: Byte offset where the counter starts
24/// @param counter_size: Size of the counter in bytes
25pub fn ctrSlice(
26 comptime BlockCipher: anytype,
27 block_cipher: BlockCipher,
28 dst: []u8,
29 src: []const u8,
30 iv: [BlockCipher.block_length]u8,
31 endian: std.builtin.Endian,
32 comptime counter_offset: usize,
33 comptime counter_size: usize,
34) void {
1435 debug.assert(dst.len >= src.len);
1536 const block_length = BlockCipher.block_length;
16 var counter: [BlockCipher.block_length]u8 = undefined;
17 var counterInt = mem.readInt(u128, &iv, endian);
37 debug.assert(counter_offset + counter_size <= block_length);
38 debug.assert(counter_size > 0 and counter_size <= block_length);
39
40 var counterBlock = iv;
1841 var i: usize = 0;
1942
43 const CounterInt = std.meta.Int(.unsigned, counter_size * 8);
44
2045 const parallel_count = BlockCipher.block.parallel.optimal_parallel_blocks;
21 const wide_block_length = parallel_count * 16;
46 const wide_block_length = parallel_count * block_length;
47 var cnt_val = mem.readInt(CounterInt, counterBlock[counter_offset..][0..counter_size], endian);
2248 if (src.len >= wide_block_length) {
23 var counters: [parallel_count * 16]u8 = undefined;
49 var counters: [parallel_count * block_length]u8 = undefined;
50 inline for (0..parallel_count) |j| {
51 counters[j * block_length ..][0..block_length].* = iv;
52 }
2453 while (i + wide_block_length <= src.len) : (i += wide_block_length) {
2554 comptime var j = 0;
2655 inline while (j < parallel_count) : (j += 1) {
27 mem.writeInt(u128, counters[j * 16 .. j * 16 + 16], counterInt, endian);
28 counterInt +%= 1;
56 mem.writeInt(CounterInt, counters[j * block_length + counter_offset ..][0..counter_size], cnt_val +% j, endian);
2957 }
58 cnt_val += parallel_count;
3059 block_cipher.xorWide(parallel_count, dst[i .. i + wide_block_length][0..wide_block_length], src[i .. i + wide_block_length][0..wide_block_length], counters);
3160 }
61 mem.writeInt(CounterInt, counterBlock[counter_offset..][0..counter_size], cnt_val, endian);
3262 }
3363 while (i + block_length <= src.len) : (i += block_length) {
34 mem.writeInt(u128, &counter, counterInt, endian);
35 counterInt +%= 1;
36 block_cipher.xor(dst[i .. i + block_length][0..block_length], src[i .. i + block_length][0..block_length], counter);
64 block_cipher.xor(dst[i .. i + block_length][0..block_length], src[i .. i + block_length][0..block_length], counterBlock);
65 cnt_val +%= 1;
66 mem.writeInt(CounterInt, counterBlock[counter_offset..][0..counter_size], cnt_val, endian);
3767 }
3868 if (i < src.len) {
39 mem.writeInt(u128, &counter, counterInt, endian);
40 var pad = [_]u8{0} ** block_length;
69 var pad: [block_length]u8 = @splat(0);
4170 const src_slice = src[i..];
4271 @memcpy(pad[0..src_slice.len], src_slice);
43 block_cipher.xor(&pad, &pad, counter);
72 block_cipher.xor(&pad, &pad, counterBlock);
4473 const pad_slice = pad[0 .. src.len - i];
4574 @memcpy(dst[i..][0..pad_slice.len], pad_slice);
4675 }
4776}
77
78test "ctr mode" {
79 const testing = std.testing;
80 const aes = std.crypto.core.aes;
81
82 // Test key and IV from NIST SP 800-38A
83 const key = [_]u8{ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c };
84 const iv = [_]u8{ 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff };
85 const ctx = aes.Aes128.initEnc(key);
86
87 // Test 1: Empty input
88 {
89 const in = [_]u8{};
90 const expected = [_]u8{};
91 var out: [0]u8 = undefined;
92 ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
93 try testing.expectEqualSlices(u8, expected[0..], out[0..]);
94 }
95
96 // Test 2: Single byte
97 {
98 const in = [_]u8{0x6b};
99 const expected = [_]u8{0x87};
100 var out: [1]u8 = undefined;
101 ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
102 try testing.expectEqualSlices(u8, expected[0..], out[0..]);
103 }
104
105 // Test 3: Less than one block (15 bytes)
106 {
107 const in = [_]u8{ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17 };
108 const expected = [_]u8{ 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6 };
109 var out: [15]u8 = undefined;
110 ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
111 try testing.expectEqualSlices(u8, expected[0..], out[0..]);
112 }
113
114 // Test 4: Exactly one block (16 bytes)
115 {
116 const in = [_]u8{ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a };
117 const expected = [_]u8{ 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce };
118 var out: [16]u8 = undefined;
119 ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
120 try testing.expectEqualSlices(u8, expected[0..], out[0..]);
121 }
122
123 // Test 5: One block plus one byte (17 bytes)
124 {
125 const in = [_]u8{ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a, 0xae };
126 const expected = [_]u8{ 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce, 0x98 };
127 var out: [17]u8 = undefined;
128 ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
129 try testing.expectEqualSlices(u8, expected[0..], out[0..]);
130 }
131
132 // Test 6: Exactly two blocks (32 bytes)
133 {
134 const in = [_]u8{
135 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a,
136 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51,
137 };
138 const expected = [_]u8{
139 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce,
140 0x98, 0x06, 0xf6, 0x6b, 0x79, 0x70, 0xfd, 0xff, 0x86, 0x17, 0x18, 0x7b, 0xb9, 0xff, 0xfd, 0xff,
141 };
142 var out: [32]u8 = undefined;
143 ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
144 try testing.expectEqualSlices(u8, expected[0..], out[0..]);
145 }
146
147 // Test 7: Two blocks plus 5 bytes (37 bytes)
148 {
149 const in = [_]u8{
150 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a,
151 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51,
152 0x30, 0xc8, 0x1c, 0x46, 0xa3,
153 };
154 const expected = [_]u8{
155 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce,
156 0x98, 0x06, 0xf6, 0x6b, 0x79, 0x70, 0xfd, 0xff, 0x86, 0x17, 0x18, 0x7b, 0xb9, 0xff, 0xfd, 0xff,
157 0x5a, 0xe4, 0xdf, 0x3e, 0xdb,
158 };
159 var out: [37]u8 = undefined;
160 ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
161 try testing.expectEqualSlices(u8, expected[0..], out[0..]);
162 }
163
164 // Test 8: Four blocks (64 bytes) - NIST test vector
165 {
166 const in = [_]u8{
167 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a,
168 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51,
169 0x30, 0xc8, 0x1c, 0x46, 0xa3, 0x5c, 0xe4, 0x11, 0xe5, 0xfb, 0xc1, 0x19, 0x1a, 0x0a, 0x52, 0xef,
170 0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, 0xe6, 0x6c, 0x37, 0x10,
171 };
172 const expected = [_]u8{
173 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce,
174 0x98, 0x06, 0xf6, 0x6b, 0x79, 0x70, 0xfd, 0xff, 0x86, 0x17, 0x18, 0x7b, 0xb9, 0xff, 0xfd, 0xff,
175 0x5a, 0xe4, 0xdf, 0x3e, 0xdb, 0xd5, 0xd3, 0x5e, 0x5b, 0x4f, 0x09, 0x02, 0x0d, 0xb0, 0x3e, 0xab,
176 0x1e, 0x03, 0x1d, 0xda, 0x2f, 0xbe, 0x03, 0xd1, 0x79, 0x21, 0x70, 0xa0, 0xf3, 0x00, 0x9c, 0xee,
177 };
178 var out: [64]u8 = undefined;
179 ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
180 try testing.expectEqualSlices(u8, expected[0..], out[0..]);
181 }
182
183 // Test 9: Large input (> 2*block_length, 100 bytes)
184 {
185 // Create a 100-byte input by extending with zeros
186 var in: [100]u8 = [_]u8{0} ** 100;
187 @memcpy(in[0..64], &[_]u8{
188 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a,
189 0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51,
190 0x30, 0xc8, 0x1c, 0x46, 0xa3, 0x5c, 0xe4, 0x11, 0xe5, 0xfb, 0xc1, 0x19, 0x1a, 0x0a, 0x52, 0xef,
191 0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, 0xe6, 0x6c, 0x37, 0x10,
192 });
193
194 // Expected output: first 64 bytes from NIST, then CTR continues with zeros
195 var expected: [100]u8 = undefined;
196 @memcpy(expected[0..64], &[_]u8{
197 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce,
198 0x98, 0x06, 0xf6, 0x6b, 0x79, 0x70, 0xfd, 0xff, 0x86, 0x17, 0x18, 0x7b, 0xb9, 0xff, 0xfd, 0xff,
199 0x5a, 0xe4, 0xdf, 0x3e, 0xdb, 0xd5, 0xd3, 0x5e, 0x5b, 0x4f, 0x09, 0x02, 0x0d, 0xb0, 0x3e, 0xab,
200 0x1e, 0x03, 0x1d, 0xda, 0x2f, 0xbe, 0x03, 0xd1, 0x79, 0x21, 0x70, 0xa0, 0xf3, 0x00, 0x9c, 0xee,
201 });
202 // Compute the rest with zeros XORed with keystream
203 @memcpy(expected[64..], &[_]u8{
204 0xb0, 0x0d, 0x47, 0xf8, 0x14, 0x8a, 0x91, 0x0e, 0xf0, 0x68, 0x30, 0x97, 0x90, 0x4b, 0xa5, 0x02,
205 0x58, 0x99, 0x44, 0x5a, 0x4d, 0xe1, 0x01, 0xf5, 0x13, 0xca, 0xd1, 0x98, 0x7d, 0x89, 0xe9, 0x1b,
206 0x3b, 0xd9, 0xac, 0x79,
207 });
208
209 var out: [100]u8 = undefined;
210 ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
211 try testing.expectEqualSlices(u8, expected[0..], out[0..]);
212 }
213
214 // Test 10: Test with different endianness (little-endian counter)
215 {
216 const le_iv = [_]u8{ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
217 const in = [_]u8{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff };
218
219 // We'll compute the expected value from the actual encryption
220 var out: [16]u8 = undefined;
221 ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], le_iv, std.builtin.Endian.little);
222
223 // The actual output for this test with little-endian counter=1
224 const expected = [_]u8{ 0x7e, 0x48, 0x15, 0xa8, 0x16, 0x66, 0xf0, 0xea, 0xad, 0x3c, 0x07, 0x97, 0x2f, 0xe8, 0x25, 0xc1 };
225 try testing.expectEqualSlices(u8, expected[0..], out[0..]);
226 }
227}