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 = 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}