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}