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 last 16 bytes of Sn,
92 // and give the entire Sn to CMAC incrementally.
93 var cmac = CmacImpl.init(&key);
94 const prefix = sn.len - 16;
95 cmac.update(sn[0..prefix]);
96
97 var tail: [16]u8 = undefined;
98 for (&tail, sn[prefix..][0..16], d) |*out, s, db| {
99 out.* = s ^ db;
100 }
101 cmac.update(&tail);
102
103 cmac.final(iv);
104 } else {
105 // Pad and XOR
106 d = dbl(d);
107 var padded: [16]u8 = @splat(0);
108 @memcpy(padded[0..sn.len], sn);
109 padded[sn.len] = 0x80;
110 for (&d, padded) |*b, p| {
111 b.* ^= p;
112 }
113 CmacImpl.create(iv, &d, &key);
114 }
115 }
116
117 /// Double operation as defined in RFC 5297.
118 /// Performs multiplication by x (i.e., left shift by 1) in GF(2^128).
119 /// This is the same operation used in CMAC subkey generation.
120 /// If the MSB is set, XORs with the polynomial 0x87 after shifting.
121 fn dbl(d: [16]u8) [16]u8 {
122 // Read as big-endian 128-bit integer
123 const val = mem.readInt(u128, &d, .big);
124
125 // Left shift by 1, and XOR with 0x87 if MSB was set
126 const doubled = (val << 1) ^ (0x87 & -%(@as(u128, val >> 127)));
127
128 // Write back as big-endian
129 var result: [16]u8 = undefined;
130 mem.writeInt(u128, &result, doubled, .big);
131 return result;
132 }
133
134 /// Encrypt plaintext using AES-SIV
135 /// `c`: Output buffer for ciphertext (same size as plaintext)
136 /// `tag`: Output buffer for authentication tag (synthetic IV)
137 /// `m`: Plaintext to encrypt
138 /// `ad`: Optional associated data
139 /// `nonce`: Optional nonce (if provided, will be added as last AD component)
140 /// `key`: Combined key (2x AES key size)
141 pub fn encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: ?[]const u8, nonce: ?[]const u8, key: [key_length]u8) void {
142 debug.assert(c.len == m.len);
143
144 // Split key into K1 (for S2V) and K2 (for CTR)
145 const k1 = key[0 .. Aes.key_bits / 8];
146 const k2 = key[Aes.key_bits / 8 ..];
147
148 // Prepare strings for S2V: AD components followed by plaintext
149 var strings_buf: [128][]const u8 = undefined;
150 var strings_len: usize = 0;
151
152 if (ad) |a| {
153 strings_buf[strings_len] = a;
154 strings_len += 1;
155 }
156 if (nonce) |n| {
157 strings_buf[strings_len] = n;
158 strings_len += 1;
159 }
160 strings_buf[strings_len] = m;
161 strings_len += 1;
162
163 // Compute synthetic IV using S2V
164 s2v(tag, k1.*, strings_buf[0..strings_len]);
165
166 // Clear the 31st and 63rd bits for use as CTR IV
167 var ctr_iv = tag.*;
168 ctr_iv[8] &= 0x7f;
169 ctr_iv[12] &= 0x7f;
170
171 // Encrypt plaintext using CTR mode
172 const aes_ctx = Aes.initEnc(k2.*);
173 modes.ctr(@TypeOf(aes_ctx), aes_ctx, c, m, ctr_iv, .big);
174 }
175
176 /// Decrypt ciphertext using AES-SIV
177 /// `m`: Output buffer for decrypted plaintext
178 /// `c`: Ciphertext to decrypt
179 /// `tag`: Authentication tag (synthetic IV)
180 /// `ad`: Optional associated data (must match encryption)
181 /// `nonce`: Optional nonce (must match encryption)
182 /// `key`: Combined key (2x AES key size)
183 pub fn decrypt(m: []u8, c: []const u8, tag: [tag_length]u8, ad: ?[]const u8, nonce: ?[]const u8, key: [key_length]u8) AuthenticationError!void {
184 assert(c.len == m.len);
185
186 // Split key into K1 (for S2V) and K2 (for CTR)
187 const k1 = key[0 .. Aes.key_bits / 8];
188 const k2 = key[Aes.key_bits / 8 ..];
189
190 // Clear the 31st and 63rd bits for use as CTR IV
191 var ctr_iv = tag;
192 ctr_iv[8] &= 0x7f;
193 ctr_iv[12] &= 0x7f;
194
195 // Decrypt ciphertext using CTR mode
196 const aes_ctx = Aes.initEnc(k2.*);
197 modes.ctr(@TypeOf(aes_ctx), aes_ctx, m, c, ctr_iv, .big);
198
199 // Prepare strings for S2V: AD components followed by plaintext
200 var strings_buf: [128][]const u8 = undefined;
201 var strings_len: usize = 0;
202
203 if (ad) |a| {
204 strings_buf[strings_len] = a;
205 strings_len += 1;
206 }
207 if (nonce) |n| {
208 strings_buf[strings_len] = n;
209 strings_len += 1;
210 }
211 strings_buf[strings_len] = m;
212 strings_len += 1;
213
214 // Verify synthetic IV using S2V
215 var computed_tag: [tag_length]u8 = undefined;
216 s2v(&computed_tag, k1.*, strings_buf[0..strings_len]);
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 /// Encrypts plaintext with multiple associated data components.
228 /// This is the most general form of AES-SIV encryption that accepts
229 /// a vector of up to 126 associated data strings as specified in RFC 5297.
230 pub fn encryptWithAdVector(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const []const u8, key: [key_length]u8) void {
231 debug.assert(c.len == m.len);
232 debug.assert(ad.len <= 126); // AES-SIV supports at most 126 associated data components
233
234 // Split key into K1 (for S2V) and K2 (for CTR)
235 const k1 = key[0 .. Aes.key_bits / 8];
236 const k2 = key[Aes.key_bits / 8 ..];
237
238 // Prepare strings for S2V: AD components followed by plaintext
239 var strings_buf: [128][]const u8 = undefined;
240 var strings_len: usize = 0;
241
242 for (ad) |a| {
243 strings_buf[strings_len] = a;
244 strings_len += 1;
245 }
246 strings_buf[strings_len] = m;
247 strings_len += 1;
248
249 // Compute synthetic IV using S2V
250 s2v(tag, k1.*, strings_buf[0..strings_len]);
251
252 // Clear the 31st and 63rd bits for use as CTR IV
253 var ctr_iv = tag.*;
254 ctr_iv[8] &= 0x7f;
255 ctr_iv[12] &= 0x7f;
256
257 // Encrypt plaintext using CTR mode
258 const aes_ctx = Aes.initEnc(k2.*);
259 modes.ctr(@TypeOf(aes_ctx), aes_ctx, c, m, ctr_iv, .big);
260 }
261
262 /// Decrypts ciphertext with multiple associated data components.
263 /// This is the most general form of AES-SIV decryption that accepts
264 /// a vector of up to 126 associated data strings as specified in RFC 5297.
265 pub fn decryptWithAdVector(m: []u8, c: []const u8, tag: [tag_length]u8, ad: []const []const u8, key: [key_length]u8) AuthenticationError!void {
266 assert(c.len == m.len);
267 assert(ad.len <= 126); // AES-SIV supports at most 126 associated data components
268
269 // Split key into K1 (for S2V) and K2 (for CTR)
270 const k1 = key[0 .. Aes.key_bits / 8];
271 const k2 = key[Aes.key_bits / 8 ..];
272
273 // Clear the 31st and 63rd bits for use as CTR IV
274 var ctr_iv = tag;
275 ctr_iv[8] &= 0x7f;
276 ctr_iv[12] &= 0x7f;
277
278 // Decrypt ciphertext using CTR mode
279 const aes_ctx = Aes.initEnc(k2.*);
280 modes.ctr(@TypeOf(aes_ctx), aes_ctx, m, c, ctr_iv, .big);
281
282 // Prepare strings for S2V: AD components followed by plaintext
283 var strings_buf: [128][]const u8 = undefined;
284 var strings_len: usize = 0;
285
286 for (ad) |a| {
287 strings_buf[strings_len] = a;
288 strings_len += 1;
289 }
290 strings_buf[strings_len] = m;
291 strings_len += 1;
292
293 // Verify synthetic IV using S2V
294 var computed_tag: [tag_length]u8 = undefined;
295 s2v(&computed_tag, k1.*, strings_buf[0..strings_len]);
296
297 // Verify tag
298 const verify = crypto.timing_safe.eql([tag_length]u8, computed_tag, tag);
299 if (!verify) {
300 crypto.secureZero(u8, &computed_tag);
301 @memset(m, undefined);
302 return error.AuthenticationFailed;
303 }
304 }
305 };
306}
307
308const htest = @import("test.zig");
309const testing = std.testing;
310
311test "AES-SIV double operation" {
312 const AesSivTest = AesSiv(crypto.core.aes.Aes128);
313
314 // Test vector from RFC 5297
315 const input = [_]u8{ 0x0e, 0x04, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e };
316 const expected = [_]u8{ 0x1c, 0x08, 0x02, 0x04, 0x06, 0x08, 0x0a, 0x0c, 0x0e, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1a, 0x1c };
317
318 const result = AesSivTest.dbl(input);
319 try testing.expectEqualSlices(u8, &expected, &result);
320}
321
322test "AES-SIV double operation with MSB set" {
323 const AesSivTest = AesSiv(crypto.core.aes.Aes128);
324
325 const input = [_]u8{ 0xe0, 0x40, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80, 0x90, 0xa0, 0xb0, 0xc0, 0xd0, 0xe0 };
326 const expected = [_]u8{ 0xc0, 0x80, 0x20, 0x40, 0x60, 0x80, 0xa0, 0xc0, 0xe1, 0x01, 0x21, 0x41, 0x61, 0x81, 0xa1, 0x47 };
327
328 const result = AesSivTest.dbl(input);
329 try testing.expectEqualSlices(u8, &expected, &result);
330}
331
332test "Aes128Siv - RFC 5297 Test Vector A.1" {
333 // Test vector from RFC 5297 Appendix A.1
334 const key = [_]u8{
335 0xff, 0xfe, 0xfd, 0xfc, 0xfb, 0xfa, 0xf9, 0xf8, 0xf7, 0xf6, 0xf5, 0xf4, 0xf3, 0xf2, 0xf1, 0xf0,
336 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff,
337 };
338 const ad = [_]u8{
339 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
340 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27,
341 };
342 const plaintext = [_]u8{
343 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee,
344 };
345
346 var ciphertext: [plaintext.len]u8 = undefined;
347 var tag: [16]u8 = undefined;
348
349 // Test using vector API for RFC compliance
350 const ad_components = [_][]const u8{&ad};
351 Aes128Siv.encryptWithAdVector(&ciphertext, &tag, &plaintext, &ad_components, key);
352
353 // Expected values from RFC 5297
354 try htest.assertEqual("85632d07c6e8f37f950acd320a2ecc93", &tag);
355 try htest.assertEqual("40c02b9690c4dc04daef7f6afe5c", &ciphertext);
356
357 // Test decryption
358 var decrypted: [plaintext.len]u8 = undefined;
359 try Aes128Siv.decryptWithAdVector(&decrypted, &ciphertext, tag, &ad_components, key);
360 try testing.expectEqualSlices(u8, &plaintext, &decrypted);
361}
362
363test "Aes128Siv - RFC 5297 Test Vector A.2" {
364 // Test vector from RFC 5297 Appendix A.2
365 const key: [32]u8 = .{
366 0x7f, 0x7e, 0x7d, 0x7c, 0x7b, 0x7a, 0x79, 0x78,
367 0x77, 0x76, 0x75, 0x74, 0x73, 0x72, 0x71, 0x70,
368 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
369 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f,
370 };
371 const ad1 = [_]u8{
372 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77,
373 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff,
374 0xde, 0xad, 0xda, 0xda, 0xde, 0xad, 0xda, 0xda,
375 0xff, 0xee, 0xdd, 0xcc, 0xbb, 0xaa, 0x99, 0x88,
376 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x00,
377 };
378 const ad2 = [_]u8{
379 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80,
380 0x90, 0xa0,
381 };
382 const nonce: [16]u8 = .{
383 0x09, 0xf9, 0x11, 0x02, 0x9d, 0x74, 0xe3, 0x5b,
384 0xd8, 0x41, 0x56, 0xc5, 0x63, 0x56, 0x88, 0xc0,
385 };
386 const plaintext = [_]u8{
387 0x74, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73, 0x20,
388 0x73, 0x6f, 0x6d, 0x65, 0x20, 0x70, 0x6c, 0x61,
389 0x69, 0x6e, 0x74, 0x65, 0x78, 0x74, 0x20, 0x74,
390 0x6f, 0x20, 0x65, 0x6e, 0x63, 0x72, 0x79, 0x70,
391 0x74, 0x20, 0x75, 0x73, 0x69, 0x6e, 0x67, 0x20,
392 0x53, 0x49, 0x56, 0x2d, 0x41, 0x45, 0x53,
393 };
394
395 var ciphertext: [plaintext.len]u8 = undefined;
396 var tag: [16]u8 = undefined;
397
398 Aes128Siv.encryptWithAdVector(&ciphertext, &tag, &plaintext, &.{ &ad1, &ad2, &nonce }, key);
399
400 // Expected values from RFC 5297
401 try htest.assertEqual("7bdb6e3b432667eb06f4d14bff2fbd0f", &tag);
402 try htest.assertEqual("cb900f2fddbe404326601965c889bf17dba77ceb094fa663b7a3f748ba8af829ea64ad544a272e9c485b62a3fd5c0d", &ciphertext);
403}
404
405test "Aes128Siv - empty plaintext" {
406 const key: [32]u8 = @splat(0x42);
407 const plaintext = "";
408 const ad = "additional data";
409
410 var ciphertext: [plaintext.len]u8 = undefined;
411 var tag: [16]u8 = undefined;
412
413 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, ad, null, key);
414
415 var decrypted: [plaintext.len]u8 = undefined;
416 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, ad, null, key);
417}
418
419test "Aes128Siv - with nonce" {
420 const key: [32]u8 = @splat(0x69);
421 const nonce: [16]u8 = @splat(0x42);
422 const plaintext = "Hello, AES-SIV!";
423 const ad = "metadata";
424
425 var ciphertext: [plaintext.len]u8 = undefined;
426 var tag: [16]u8 = undefined;
427
428 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, ad, &nonce, key);
429
430 var decrypted: [plaintext.len]u8 = undefined;
431 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, ad, &nonce, key);
432 try testing.expectEqualSlices(u8, plaintext, &decrypted);
433}
434
435test "Aes256Siv - basic functionality" {
436 const key: [64]u8 = @splat(0x96);
437 const plaintext = "Test message for AES-256-SIV";
438 const ad1 = "header";
439 const ad2 = "more data";
440
441 var ciphertext: [plaintext.len]u8 = undefined;
442 var tag: [16]u8 = undefined;
443
444 // Test with multiple AD components using the vector API
445 const ad_components = [_][]const u8{ ad1, ad2 };
446 Aes256Siv.encryptWithAdVector(&ciphertext, &tag, plaintext, &ad_components, key);
447
448 var decrypted: [plaintext.len]u8 = undefined;
449 try Aes256Siv.decryptWithAdVector(&decrypted, &ciphertext, tag, &ad_components, key);
450 try testing.expectEqualSlices(u8, plaintext, &decrypted);
451}
452
453test "Aes128Siv - demonstrating optional parameters" {
454 const key: [32]u8 = @splat(0x77);
455
456 // Test 1: No AD, no nonce (pure deterministic)
457 {
458 const plaintext = "Deterministic encryption";
459 var ciphertext: [plaintext.len]u8 = undefined;
460 var tag: [16]u8 = undefined;
461
462 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, null, null, key);
463
464 var decrypted: [plaintext.len]u8 = undefined;
465 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, null, null, key);
466 try testing.expectEqualSlices(u8, plaintext, &decrypted);
467 }
468
469 // Test 2: With AD, no nonce
470 {
471 const plaintext = "With associated data";
472 const ad = "some context";
473 var ciphertext: [plaintext.len]u8 = undefined;
474 var tag: [16]u8 = undefined;
475
476 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, ad, null, key);
477
478 var decrypted: [plaintext.len]u8 = undefined;
479 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, ad, null, key);
480 try testing.expectEqualSlices(u8, plaintext, &decrypted);
481 }
482
483 // Test 3: No AD, with nonce
484 {
485 const plaintext = "Nonce-based encryption";
486 const nonce: [12]u8 = @splat(0x01);
487 var ciphertext: [plaintext.len]u8 = undefined;
488 var tag: [16]u8 = undefined;
489
490 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, null, &nonce, key);
491
492 var decrypted: [plaintext.len]u8 = undefined;
493 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, null, &nonce, key);
494 try testing.expectEqualSlices(u8, plaintext, &decrypted);
495 }
496
497 // Test 4: With both AD and nonce
498 {
499 const plaintext = "Full featured";
500 const ad = "context";
501 const nonce: [16]u8 = @splat(0x02);
502 var ciphertext: [plaintext.len]u8 = undefined;
503 var tag: [16]u8 = undefined;
504
505 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, ad, &nonce, key);
506
507 var decrypted: [plaintext.len]u8 = undefined;
508 try Aes128Siv.decrypt(&decrypted, &ciphertext, tag, ad, &nonce, key);
509 try testing.expectEqualSlices(u8, plaintext, &decrypted);
510 }
511}
512
513test "Aes128Siv - authentication failure" {
514 const key: [32]u8 = @splat(0x13);
515 const plaintext = "Secret message";
516 const ad = "";
517
518 var ciphertext: [plaintext.len]u8 = undefined;
519 var tag: [16]u8 = undefined;
520
521 Aes128Siv.encrypt(&ciphertext, &tag, plaintext, ad, null, key);
522
523 // Corrupt the tag
524 tag[0] ^= 0x01;
525
526 var decrypted: [plaintext.len]u8 = undefined;
527 try testing.expectError(error.AuthenticationFailed, Aes128Siv.decrypt(&decrypted, &ciphertext, tag, ad, null, key));
528}