Created
October 7, 2025 08:09
-
-
Save aarvay/ee31fb93a984c759967fe1165ea4233d to your computer and use it in GitHub Desktop.
AES-CCM implementation in zig
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //! AES-CCM (Counter with CBC-MAC) authenticated encryption. | |
| //! | |
| //! References: | |
| //! - NIST SP 800-38C: https://csrc.nist.gov/publications/detail/sp/800-38c/final | |
| //! - RFC 3610: https://datatracker.ietf.org/doc/html/rfc3610 | |
| const std = @import("std"); | |
| const crypto = std.crypto; | |
| const mem = std.mem; | |
| const Allocator = mem.Allocator; | |
| const assert = std.debug.assert; | |
| const AuthenticationError = crypto.errors.AuthenticationError; | |
| pub const has_hardware_support = crypto.core.aes.has_hardware_support; | |
| pub fn AesCcm(comptime key_bits: u11, comptime tag_bits: u8) type { | |
| if (key_bits != 128 and key_bits != 256) { | |
| @compileError("AES-CCM only supports 128-bit and 256-bit keys"); | |
| } | |
| if (tag_bits < 32 or tag_bits > 128 or tag_bits % 8 != 0) { | |
| @compileError("Tag length must be 4-16 bytes (32-128 bits) in 8-bit increments"); | |
| } | |
| return struct { | |
| pub const key_length = key_bits / 8; | |
| pub const tag_length = tag_bits / 8; | |
| const AesType = switch (key_bits) { | |
| 128 => crypto.core.aes.Aes128, | |
| 256 => crypto.core.aes.Aes256, | |
| else => unreachable, | |
| }; | |
| pub fn encrypt( | |
| c: []u8, | |
| tag: *[tag_length]u8, | |
| m: []const u8, | |
| ad: []const u8, | |
| npub: []const u8, | |
| key: [key_length]u8, | |
| ) void { | |
| assert(c.len == m.len); | |
| assert(npub.len >= 7 and npub.len <= 13); | |
| const L = 15 - npub.len; | |
| const aes = AesType.initEnc(key); | |
| var j: [16]u8 = undefined; | |
| const flags = @as(u8, @intCast(L - 1)); | |
| j[0] = flags; | |
| @memcpy(j[1 .. 1 + npub.len], npub); | |
| @memset(j[1 + npub.len ..], 0); | |
| var mac: [tag_length]u8 = undefined; | |
| computeCbcMac(&aes, npub, ad, m, &mac, L); | |
| const counter_start = 16 - L; | |
| @memset(j[counter_start..], 0); | |
| var s0: [16]u8 = undefined; | |
| aes.encrypt(&s0, &j); | |
| for (0..tag_length) |i| { | |
| tag[i] = mac[i] ^ s0[i]; | |
| } | |
| encryptCtr(&aes, &j, m, c, L); | |
| crypto.secureZero(u8, &mac); | |
| crypto.secureZero(u8, &s0); | |
| } | |
| pub fn decrypt( | |
| m: []u8, | |
| c: []const u8, | |
| tag: [tag_length]u8, | |
| ad: []const u8, | |
| npub: []const u8, | |
| key: [key_length]u8, | |
| ) AuthenticationError!void { | |
| assert(c.len == m.len); | |
| assert(npub.len >= 7 and npub.len <= 13); | |
| const L = 15 - npub.len; | |
| const aes = AesType.initEnc(key); | |
| var j: [16]u8 = undefined; | |
| const flags = @as(u8, @intCast(L - 1)); | |
| j[0] = flags; | |
| @memcpy(j[1 .. 1 + npub.len], npub); | |
| @memset(j[1 + npub.len ..], 0); | |
| decryptCtr(&aes, &j, c, m, L); | |
| var computed_tag: [tag_length]u8 = undefined; | |
| computeCbcMac(&aes, npub, ad, m, &computed_tag, L); | |
| const counter_start = 16 - L; | |
| @memset(j[counter_start..], 0); | |
| var s0: [16]u8 = undefined; | |
| aes.encrypt(&s0, &j); | |
| var expected_tag: [tag_length]u8 = undefined; | |
| for (0..tag_length) |i| { | |
| expected_tag[i] = computed_tag[i] ^ s0[i]; | |
| } | |
| if (!crypto.timing_safe.eql([tag_length]u8, expected_tag, tag)) { | |
| crypto.secureZero(u8, &expected_tag); | |
| crypto.secureZero(u8, &computed_tag); | |
| crypto.secureZero(u8, &s0); | |
| crypto.secureZero(u8, m); | |
| return AuthenticationError.AuthenticationFailed; | |
| } | |
| crypto.secureZero(u8, &expected_tag); | |
| crypto.secureZero(u8, &computed_tag); | |
| crypto.secureZero(u8, &s0); | |
| } | |
| fn computeCbcMac( | |
| aes: *const crypto.core.aes.AesEncryptCtx(AesType), | |
| npub: []const u8, | |
| ad: []const u8, | |
| m: []const u8, | |
| mac: *[tag_length]u8, | |
| L: usize, | |
| ) void { | |
| var mac_block: [16]u8 = undefined; | |
| @memset(&mac_block, 0); | |
| const has_adata = ad.len > 0; | |
| const flags_val = @as(u8, if (has_adata) 0x40 else 0) | | |
| @as(u8, (tag_length - 2) / 2) << 3 | | |
| @as(u8, @intCast(L - 1)); | |
| mac_block[0] = flags_val; | |
| @memcpy(mac_block[1 .. 1 + npub.len], npub); | |
| var length = m.len; | |
| var i: usize = 0; | |
| while (i < L) : (i += 1) { | |
| mac_block[15 - i] = @intCast(length & 0xFF); | |
| length >>= 8; | |
| } | |
| aes.encrypt(&mac_block, &mac_block); | |
| if (has_adata) { | |
| var adata_len_block: [16]u8 = undefined; | |
| @memset(&adata_len_block, 0); | |
| // Encode associated data length according to CCM spec | |
| var header_len: usize = 0; | |
| if (ad.len < 0xFF00) { | |
| // Short form: 2 bytes | |
| adata_len_block[0] = @intCast((ad.len >> 8) & 0xFF); | |
| adata_len_block[1] = @intCast(ad.len & 0xFF); | |
| header_len = 2; | |
| } else { | |
| // Long form: 0xfffe || 4-byte length (for 2^16-2^8 <= len < 2^32) | |
| adata_len_block[0] = 0xff; | |
| adata_len_block[1] = 0xfe; | |
| mem.writeInt(u32, adata_len_block[2..6], @intCast(ad.len), .big); | |
| header_len = 6; | |
| } | |
| const adata_copy_len: usize = @min(16 - header_len, ad.len); | |
| const end_idx: usize = header_len + adata_copy_len; | |
| @memcpy(adata_len_block[header_len..end_idx], ad[0..adata_copy_len]); | |
| for (&mac_block, adata_len_block) |*mac_byte, a| mac_byte.* ^= a; | |
| aes.encrypt(&mac_block, &mac_block); | |
| var adata_pos: usize = adata_copy_len; | |
| while (adata_pos < ad.len) { | |
| var block: [16]u8 = undefined; | |
| @memset(&block, 0); | |
| const chunk_len: usize = @min(16, ad.len - adata_pos); | |
| @memcpy(block[0..chunk_len], ad[adata_pos .. adata_pos + chunk_len]); | |
| for (&mac_block, block) |*mac_byte, b| mac_byte.* ^= b; | |
| aes.encrypt(&mac_block, &mac_block); | |
| adata_pos += chunk_len; | |
| } | |
| } | |
| var pos: usize = 0; | |
| while (pos < m.len) { | |
| var block: [16]u8 = undefined; | |
| @memset(&block, 0); | |
| const chunk_len = @min(16, m.len - pos); | |
| @memcpy(block[0..chunk_len], m[pos .. pos + chunk_len]); | |
| for (&mac_block, block) |*mac_byte, b| mac_byte.* ^= b; | |
| aes.encrypt(&mac_block, &mac_block); | |
| pos += chunk_len; | |
| } | |
| @memcpy(mac, mac_block[0..tag_length]); | |
| } | |
| fn encryptCtr( | |
| aes: *const crypto.core.aes.AesEncryptCtx(AesType), | |
| j: *[16]u8, | |
| m: []const u8, | |
| c: []u8, | |
| L: usize, | |
| ) void { | |
| assert(m.len == c.len); | |
| var counter: usize = 1; | |
| var pos: usize = 0; | |
| while (pos < m.len) { | |
| var counter_val = counter; | |
| var i: usize = 0; | |
| while (i < L) : (i += 1) { | |
| j[15 - i] = @intCast(counter_val & 0xFF); | |
| counter_val >>= 8; | |
| } | |
| var keystream: [16]u8 = undefined; | |
| aes.encrypt(&keystream, j); | |
| const chunk_len = @min(16, m.len - pos); | |
| for (0..chunk_len) |k| { | |
| c[pos + k] = m[pos + k] ^ keystream[k]; | |
| } | |
| pos += chunk_len; | |
| counter += 1; | |
| } | |
| } | |
| fn decryptCtr( | |
| aes: *const crypto.core.aes.AesEncryptCtx(AesType), | |
| j: *[16]u8, | |
| c: []const u8, | |
| m: []u8, | |
| L: usize, | |
| ) void { | |
| encryptCtr(aes, j, c, m, L); | |
| } | |
| }; | |
| } | |
| pub const Aes128Ccm4 = AesCcm(128, 32); | |
| pub const Aes128Ccm6 = AesCcm(128, 48); | |
| pub const Aes128Ccm8 = AesCcm(128, 64); | |
| pub const Aes128Ccm14 = AesCcm(128, 112); | |
| pub const Aes128Ccm16 = AesCcm(128, 128); | |
| pub const Aes256Ccm8 = AesCcm(256, 64); | |
| pub const Aes256Ccm16 = AesCcm(256, 128); | |
| // ==================== Tests ==================== | |
| test "Aes256Ccm8 - Encrypt decrypt round-trip" { | |
| const testing = std.testing; | |
| const key: [32]u8 = [_]u8{0x42} ** 32; | |
| const nonce: [13]u8 = [_]u8{0x11} ** 13; | |
| const m = "Hello, World! This is a test message."; | |
| var c: [m.len]u8 = undefined; | |
| var m2: [m.len]u8 = undefined; | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(&c, &tag, m, "", &nonce, key); | |
| try Aes256Ccm8.decrypt(&m2, &c, tag, "", &nonce, key); | |
| try testing.expectEqualSlices(u8, m[0..], m2[0..]); | |
| } | |
| test "Aes256Ccm8 - Associated data" { | |
| const testing = std.testing; | |
| const key: [32]u8 = [_]u8{0x42} ** 32; | |
| const nonce: [13]u8 = [_]u8{0x11} ** 13; | |
| const m = "secret message"; | |
| const ad = "additional authenticated data"; | |
| var c: [m.len]u8 = undefined; | |
| var m2: [m.len]u8 = undefined; | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(&c, &tag, m, ad, &nonce, key); | |
| try Aes256Ccm8.decrypt(&m2, &c, tag, ad, &nonce, key); | |
| try testing.expectEqualSlices(u8, m[0..], m2[0..]); | |
| var m3: [m.len]u8 = undefined; | |
| const wrong_adata = "wrong data"; | |
| const result = Aes256Ccm8.decrypt(&m3, &c, tag, wrong_adata, &nonce, key); | |
| try testing.expectError(error.AuthenticationFailed, result); | |
| } | |
| test "Aes256Ccm8 - Wrong key" { | |
| const testing = std.testing; | |
| const key: [32]u8 = [_]u8{0x42} ** 32; | |
| const wrong_key: [32]u8 = [_]u8{0x43} ** 32; | |
| const nonce: [13]u8 = [_]u8{0x11} ** 13; | |
| const m = "secret"; | |
| var c: [m.len]u8 = undefined; | |
| var m2: [m.len]u8 = undefined; | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(&c, &tag, m, "", &nonce, key); | |
| const result = Aes256Ccm8.decrypt(&m2, &c, tag, "", &nonce, wrong_key); | |
| try testing.expectError(error.AuthenticationFailed, result); | |
| } | |
| test "Aes256Ccm8 - Corrupted ciphertext" { | |
| const testing = std.testing; | |
| const key: [32]u8 = [_]u8{0x42} ** 32; | |
| const nonce: [13]u8 = [_]u8{0x11} ** 13; | |
| const m = "secret message"; | |
| var c: [m.len]u8 = undefined; | |
| var m2: [m.len]u8 = undefined; | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(&c, &tag, m, "", &nonce, key); | |
| c[5] ^= 0xFF; | |
| const result = Aes256Ccm8.decrypt(&m2, &c, tag, "", &nonce, key); | |
| try testing.expectError(error.AuthenticationFailed, result); | |
| } | |
| test "Aes256Ccm8 - Empty plaintext" { | |
| const testing = std.testing; | |
| const key: [32]u8 = [_]u8{0x42} ** 32; | |
| const nonce: [13]u8 = [_]u8{0x11} ** 13; | |
| const m = ""; | |
| var c: [m.len]u8 = undefined; | |
| var m2: [m.len]u8 = undefined; | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(&c, &tag, m, "", &nonce, key); | |
| try Aes256Ccm8.decrypt(&m2, &c, tag, "", &nonce, key); | |
| try testing.expectEqual(@as(usize, 0), m2.len); | |
| } | |
| test "Aes128Ccm8 - Basic functionality" { | |
| const testing = std.testing; | |
| const key: [16]u8 = [_]u8{0x42} ** 16; | |
| const nonce: [13]u8 = [_]u8{0x11} ** 13; | |
| const m = "Test AES-128-CCM"; | |
| var c: [m.len]u8 = undefined; | |
| var m2: [m.len]u8 = undefined; | |
| var tag: [Aes128Ccm8.tag_length]u8 = undefined; | |
| Aes128Ccm8.encrypt(&c, &tag, m, "", &nonce, key); | |
| try Aes128Ccm8.decrypt(&m2, &c, tag, "", &nonce, key); | |
| try testing.expectEqualSlices(u8, m[0..], m2[0..]); | |
| } | |
| test "Aes256Ccm16 - 16-byte tag" { | |
| const testing = std.testing; | |
| const key: [32]u8 = [_]u8{0x42} ** 32; | |
| const nonce: [13]u8 = [_]u8{0x11} ** 13; | |
| const m = "Test 16-byte tag"; | |
| var c: [m.len]u8 = undefined; | |
| var m2: [m.len]u8 = undefined; | |
| var tag: [Aes256Ccm16.tag_length]u8 = undefined; | |
| Aes256Ccm16.encrypt(&c, &tag, m, "", &nonce, key); | |
| try testing.expectEqual(@as(usize, 16), tag.len); | |
| try Aes256Ccm16.decrypt(&m2, &c, tag, "", &nonce, key); | |
| try testing.expectEqualSlices(u8, m[0..], m2[0..]); | |
| } | |
| fn hexToBytes(comptime len: usize, hex: []const u8) ![len]u8 { | |
| if (hex.len != len * 2) return error.InvalidHexLength; | |
| var result: [len]u8 = undefined; | |
| for (0..len) |i| { | |
| result[i] = try std.fmt.parseInt(u8, hex[i * 2 .. i * 2 + 2], 16); | |
| } | |
| return result; | |
| } | |
| fn hexToBytesAlloc(allocator: Allocator, hex: []const u8) ![]u8 { | |
| if (hex.len % 2 != 0) return error.InvalidHexLength; | |
| const len = hex.len / 2; | |
| var result = try allocator.alloc(u8, len); | |
| for (0..len) |i| { | |
| result[i] = try std.fmt.parseInt(u8, hex[i * 2 .. i * 2 + 2], 16); | |
| } | |
| return result; | |
| } | |
| test "Aes256Ccm8 - Edge case short nonce" { | |
| const testing = std.testing; | |
| const allocator = testing.allocator; | |
| _ = allocator; | |
| const key = try hexToBytes(32, "eda32f751456e33195f1f499cf2dc7c97ea127b6d488f211ccc5126fbb24afa6"); | |
| const nonce = try hexToBytes(7, "a544218dadd3c1"); | |
| const m = try hexToBytes(1, "00"); | |
| var c: [m.len]u8 = undefined; | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(&c, &tag, &m, "", &nonce, key); | |
| var m2: [c.len]u8 = undefined; | |
| try Aes256Ccm8.decrypt(&m2, &c, tag, "", &nonce, key); | |
| try testing.expectEqualSlices(u8, &m, &m2); | |
| } | |
| test "Aes256Ccm8 - Edge case long nonce" { | |
| const testing = std.testing; | |
| const allocator = testing.allocator; | |
| _ = allocator; | |
| const key = try hexToBytes(32, "e1b8a927a95efe94656677b692662000278b441c79e879dd5c0ddc758bdc9ee8"); | |
| const nonce = try hexToBytes(13, "a544218dadd3c10583db49cf39"); | |
| const m = try hexToBytes(1, "00"); | |
| var c: [m.len]u8 = undefined; | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(&c, &tag, &m, "", &nonce, key); | |
| var m2: [c.len]u8 = undefined; | |
| try Aes256Ccm8.decrypt(&m2, &c, tag, "", &nonce, key); | |
| try testing.expectEqualSlices(u8, &m, &m2); | |
| } | |
| test "Aes256Ccm8 - With AAD and wrong AAD detection" { | |
| const testing = std.testing; | |
| const allocator = testing.allocator; | |
| _ = allocator; | |
| const key = try hexToBytes(32, "8c5cf3457ff22228c39c051c4e05ed4093657eb303f859a9d4b0f8be0127d88a"); | |
| const nonce = try hexToBytes(13, "a544218dadd3c10583db49cf39"); | |
| const m = try hexToBytes(1, "00"); | |
| const ad = try hexToBytes(32, "3c0e2815d37d844f7ac240ba9d6e3a0b2a86f706e885959e09a1005e024f6907"); | |
| var c: [m.len]u8 = undefined; | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(&c, &tag, &m, &ad, &nonce, key); | |
| var m2: [c.len]u8 = undefined; | |
| try Aes256Ccm8.decrypt(&m2, &c, tag, &ad, &nonce, key); | |
| try testing.expectEqualSlices(u8, &m, &m2); | |
| const wrong_ad = try hexToBytes(32, "0000000000000000000000000000000000000000000000000000000000000000"); | |
| var m3: [c.len]u8 = undefined; | |
| const result = Aes256Ccm8.decrypt(&m3, &c, tag, &wrong_ad, &nonce, key); | |
| try testing.expectError(error.AuthenticationFailed, result); | |
| } | |
| test "Aes256Ccm8 - Multi-block payload" { | |
| const testing = std.testing; | |
| const allocator = testing.allocator; | |
| // Test with 32-byte payload (2 AES blocks) | |
| const key = try hexToBytes(32, "af063639e66c284083c5cf72b70d8bc277f5978e80d9322d99f2fdc718cda569"); | |
| const nonce = try hexToBytes(13, "a544218dadd3c10583db49cf39"); | |
| const m = try hexToBytesAlloc(allocator, "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"); | |
| defer allocator.free(m); | |
| // Encrypt | |
| const c = try allocator.alloc(u8, m.len); | |
| defer allocator.free(c); | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(c, &tag, m, "", &nonce, key); | |
| // Decrypt and verify | |
| const m2 = try allocator.alloc(u8, c.len); | |
| defer allocator.free(m2); | |
| try Aes256Ccm8.decrypt(m2, c, tag, "", &nonce, key); | |
| try testing.expectEqualSlices(u8, m, m2); | |
| } | |
| test "Aes256Ccm8 - Multi-block with AAD" { | |
| const testing = std.testing; | |
| const allocator = testing.allocator; | |
| // Test with multi-block payload (3 AES blocks) and AAD | |
| const key = try hexToBytes(32, "f7079dfa3b5c7b056347d7e437bcded683abd6e2c9e069d333284082cbb5d453"); | |
| const nonce = try hexToBytes(12, "5b8e40746f6b98e00f1d13ff"); | |
| // 48-byte payload (3 AES blocks) | |
| const m = try hexToBytesAlloc(allocator, "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f"); | |
| defer allocator.free(m); | |
| // 16-byte AAD | |
| const ad = try hexToBytes(16, "000102030405060708090a0b0c0d0e0f"); | |
| // Encrypt | |
| const c = try allocator.alloc(u8, m.len); | |
| defer allocator.free(c); | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(c, &tag, m, &ad, &nonce, key); | |
| // Decrypt and verify | |
| const m2 = try allocator.alloc(u8, c.len); | |
| defer allocator.free(m2); | |
| try Aes256Ccm8.decrypt(m2, c, tag, &ad, &nonce, key); | |
| try testing.expectEqualSlices(u8, m, m2); | |
| } | |
| test "Aes256Ccm8 - Minimum nonce length" { | |
| const testing = std.testing; | |
| const allocator = testing.allocator; | |
| // Test with 7-byte nonce (minimum allowed by CCM spec) | |
| const key = try hexToBytes(32, "404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f"); | |
| const nonce = try hexToBytes(7, "10111213141516"); | |
| const m = "Test message with minimum nonce length"; | |
| // Encrypt | |
| const c = try allocator.alloc(u8, m.len); | |
| defer allocator.free(c); | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(c, &tag, m, "", &nonce, key); | |
| // Decrypt and verify | |
| const m2 = try allocator.alloc(u8, c.len); | |
| defer allocator.free(m2); | |
| try Aes256Ccm8.decrypt(m2, c, tag, "", &nonce, key); | |
| try testing.expectEqualSlices(u8, m[0..], m2[0..]); | |
| } | |
| test "Aes256Ccm8 - Maximum nonce length" { | |
| const testing = std.testing; | |
| const allocator = testing.allocator; | |
| // Test with 13-byte nonce (maximum allowed by CCM spec) | |
| const key = try hexToBytes(32, "606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f"); | |
| const nonce = try hexToBytes(13, "101112131415161718191a1b1c"); | |
| const m = "Test message with maximum nonce length"; | |
| // Encrypt | |
| const c = try allocator.alloc(u8, m.len); | |
| defer allocator.free(c); | |
| var tag: [Aes256Ccm8.tag_length]u8 = undefined; | |
| Aes256Ccm8.encrypt(c, &tag, m, "", &nonce, key); | |
| // Decrypt and verify | |
| const m2 = try allocator.alloc(u8, c.len); | |
| defer allocator.free(m2); | |
| try Aes256Ccm8.decrypt(m2, c, tag, "", &nonce, key); | |
| try testing.expectEqualSlices(u8, m[0..], m2[0..]); | |
| } | |
| // ==================== RFC 3610 Test Vectors ==================== | |
| // Official test vectors from RFC 3610 Appendix A | |
| // https://datatracker.ietf.org/doc/html/rfc3610 | |
| test "Aes128Ccm8 - RFC 3610 Packet Vector #1" { | |
| const testing = std.testing; | |
| // RFC 3610 Appendix A, Packet Vector #1 | |
| const key = try hexToBytes(16, "C0C1C2C3C4C5C6C7C8C9CACBCCCDCECF"); | |
| const nonce = try hexToBytes(13, "00000003020100A0A1A2A3A4A5"); | |
| const ad = try hexToBytes(8, "0001020304050607"); | |
| const plaintext = try hexToBytes(23, "08090A0B0C0D0E0F101112131415161718191A1B1C1D1E"); | |
| // Expected ciphertext and tag from RFC | |
| const expected_ciphertext = try hexToBytes(23, "588C979A61C663D2F066D0C2C0F989806D5F6B61DAC384"); | |
| const expected_tag = try hexToBytes(8, "17E8D12CFDF926E0"); | |
| // Encrypt | |
| var c: [plaintext.len]u8 = undefined; | |
| var tag: [Aes128Ccm8.tag_length]u8 = undefined; | |
| Aes128Ccm8.encrypt(&c, &tag, &plaintext, &ad, &nonce, key); | |
| // Verify ciphertext matches RFC expected output | |
| try testing.expectEqualSlices(u8, &expected_ciphertext, &c); | |
| // Verify tag matches RFC expected output | |
| try testing.expectEqualSlices(u8, &expected_tag, &tag); | |
| // Decrypt and verify round-trip | |
| var m: [plaintext.len]u8 = undefined; | |
| try Aes128Ccm8.decrypt(&m, &c, tag, &ad, &nonce, key); | |
| try testing.expectEqualSlices(u8, &plaintext, &m); | |
| } | |
| test "Aes128Ccm8 - RFC 3610 Packet Vector #2" { | |
| const testing = std.testing; | |
| // RFC 3610 Appendix A, Packet Vector #2 (8-byte tag, M=8) | |
| const key = try hexToBytes(16, "C0C1C2C3C4C5C6C7C8C9CACBCCCDCECF"); | |
| const nonce = try hexToBytes(13, "00000004030201A0A1A2A3A4A5"); | |
| const ad = try hexToBytes(8, "0001020304050607"); | |
| const plaintext = try hexToBytes(24, "08090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F"); | |
| // Expected ciphertext and tag from RFC (from total packet: header + ciphertext + tag) | |
| const expected_ciphertext = try hexToBytes(24, "72C91A36E135F8CF291CA894085C87E3CC15C439C9E43A3B"); | |
| const expected_tag = try hexToBytes(8, "A091D56E10400916"); | |
| // Encrypt | |
| var c: [plaintext.len]u8 = undefined; | |
| var tag: [Aes128Ccm8.tag_length]u8 = undefined; | |
| Aes128Ccm8.encrypt(&c, &tag, &plaintext, &ad, &nonce, key); | |
| // Verify ciphertext matches RFC expected output | |
| try testing.expectEqualSlices(u8, &expected_ciphertext, &c); | |
| // Verify tag matches RFC expected output | |
| try testing.expectEqualSlices(u8, &expected_tag, &tag); | |
| // Decrypt and verify round-trip | |
| var m: [plaintext.len]u8 = undefined; | |
| try Aes128Ccm8.decrypt(&m, &c, tag, &ad, &nonce, key); | |
| try testing.expectEqualSlices(u8, &plaintext, &m); | |
| } | |
| test "Aes128Ccm8 - RFC 3610 Packet Vector #3" { | |
| const testing = std.testing; | |
| // RFC 3610 Appendix A, Packet Vector #3 (8-byte tag, 25-byte payload) | |
| const key = try hexToBytes(16, "C0C1C2C3C4C5C6C7C8C9CACBCCCDCECF"); | |
| const nonce = try hexToBytes(13, "00000005040302A0A1A2A3A4A5"); | |
| const ad = try hexToBytes(8, "0001020304050607"); | |
| const plaintext = try hexToBytes(25, "08090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F20"); | |
| // Expected ciphertext and tag from RFC | |
| const expected_ciphertext = try hexToBytes(25, "51B1E5F44A197D1DA46B0F8E2D282AE871E838BB64DA859657"); | |
| const expected_tag = try hexToBytes(8, "4ADAA76FBD9FB0C5"); | |
| // Encrypt | |
| var c: [plaintext.len]u8 = undefined; | |
| var tag: [Aes128Ccm8.tag_length]u8 = undefined; | |
| Aes128Ccm8.encrypt(&c, &tag, &plaintext, &ad, &nonce, key); | |
| // Verify ciphertext matches RFC expected output | |
| try testing.expectEqualSlices(u8, &expected_ciphertext, &c); | |
| // Verify tag matches RFC expected output | |
| try testing.expectEqualSlices(u8, &expected_tag, &tag); | |
| // Decrypt and verify round-trip | |
| var m: [plaintext.len]u8 = undefined; | |
| try Aes128Ccm8.decrypt(&m, &c, tag, &ad, &nonce, key); | |
| try testing.expectEqualSlices(u8, &plaintext, &m); | |
| } | |
| // ==================== NIST SP 800-38C Test Vectors ==================== | |
| // Official test vectors from NIST Special Publication 800-38C Appendix C | |
| // https://csrc.nist.gov/publications/detail/sp/800-38c/final | |
| test "Aes128Ccm4 - NIST SP 800-38C Example 1" { | |
| const testing = std.testing; | |
| // Example 1 (C.1): Klen=128, Tlen=32, Nlen=56, Alen=64, Plen=32 | |
| const key = try hexToBytes(16, "404142434445464748494a4b4c4d4e4f"); | |
| const nonce = try hexToBytes(7, "10111213141516"); | |
| const ad = try hexToBytes(8, "0001020304050607"); | |
| const plaintext = try hexToBytes(4, "20212223"); | |
| // Expected ciphertext and tag from NIST | |
| const expected_ciphertext = try hexToBytes(4, "7162015b"); | |
| const expected_tag = try hexToBytes(4, "4dac255d"); | |
| // Encrypt | |
| var c: [plaintext.len]u8 = undefined; | |
| var tag: [Aes128Ccm4.tag_length]u8 = undefined; | |
| Aes128Ccm4.encrypt(&c, &tag, &plaintext, &ad, &nonce, key); | |
| // Verify ciphertext matches NIST expected output | |
| try testing.expectEqualSlices(u8, &expected_ciphertext, &c); | |
| // Verify tag matches NIST expected output | |
| try testing.expectEqualSlices(u8, &expected_tag, &tag); | |
| // Decrypt and verify round-trip | |
| var m: [plaintext.len]u8 = undefined; | |
| try Aes128Ccm4.decrypt(&m, &c, tag, &ad, &nonce, key); | |
| try testing.expectEqualSlices(u8, &plaintext, &m); | |
| } | |
| test "Aes128Ccm6 - NIST SP 800-38C Example 2" { | |
| const testing = std.testing; | |
| // Example 2 (C.2): Klen=128, Tlen=48, Nlen=64, Alen=128, Plen=128 | |
| const key = try hexToBytes(16, "404142434445464748494a4b4c4d4e4f"); | |
| const nonce = try hexToBytes(8, "1011121314151617"); | |
| const ad = try hexToBytes(16, "000102030405060708090a0b0c0d0e0f"); | |
| const plaintext = try hexToBytes(16, "202122232425262728292a2b2c2d2e2f"); | |
| // Expected ciphertext and tag from NIST | |
| const expected_ciphertext = try hexToBytes(16, "d2a1f0e051ea5f62081a7792073d593d"); | |
| const expected_tag = try hexToBytes(6, "1fc64fbfaccd"); | |
| // Encrypt | |
| var c: [plaintext.len]u8 = undefined; | |
| var tag: [Aes128Ccm6.tag_length]u8 = undefined; | |
| Aes128Ccm6.encrypt(&c, &tag, &plaintext, &ad, &nonce, key); | |
| // Verify ciphertext matches NIST expected output | |
| try testing.expectEqualSlices(u8, &expected_ciphertext, &c); | |
| // Verify tag matches NIST expected output | |
| try testing.expectEqualSlices(u8, &expected_tag, &tag); | |
| // Decrypt and verify round-trip | |
| var m: [plaintext.len]u8 = undefined; | |
| try Aes128Ccm6.decrypt(&m, &c, tag, &ad, &nonce, key); | |
| try testing.expectEqualSlices(u8, &plaintext, &m); | |
| } | |
| test "Aes128Ccm8 - NIST SP 800-38C Example 3" { | |
| const testing = std.testing; | |
| // Example 3 (C.3): Klen=128, Tlen=64, Nlen=96, Alen=160, Plen=192 | |
| const key = try hexToBytes(16, "404142434445464748494a4b4c4d4e4f"); | |
| const nonce = try hexToBytes(12, "101112131415161718191a1b"); | |
| const ad = try hexToBytes(20, "000102030405060708090a0b0c0d0e0f10111213"); | |
| const plaintext = try hexToBytes(24, "202122232425262728292a2b2c2d2e2f3031323334353637"); | |
| // Expected ciphertext and tag from NIST | |
| const expected_ciphertext = try hexToBytes(24, "e3b201a9f5b71a7a9b1ceaeccd97e70b6176aad9a4428aa5"); | |
| const expected_tag = try hexToBytes(8, "484392fbc1b09951"); | |
| // Encrypt | |
| var c: [plaintext.len]u8 = undefined; | |
| var tag: [Aes128Ccm8.tag_length]u8 = undefined; | |
| Aes128Ccm8.encrypt(&c, &tag, &plaintext, &ad, &nonce, key); | |
| // Verify ciphertext matches NIST expected output | |
| try testing.expectEqualSlices(u8, &expected_ciphertext, &c); | |
| // Verify tag matches NIST expected output | |
| try testing.expectEqualSlices(u8, &expected_tag, &tag); | |
| // Decrypt and verify round-trip | |
| var m: [plaintext.len]u8 = undefined; | |
| try Aes128Ccm8.decrypt(&m, &c, tag, &ad, &nonce, key); | |
| try testing.expectEqualSlices(u8, &plaintext, &m); | |
| } | |
| test "Aes128Ccm14 - NIST SP 800-38C Example 4" { | |
| const testing = std.testing; | |
| const allocator = testing.allocator; | |
| // Example 4 (C.4): Klen=128, Tlen=112, Nlen=104, Alen=524288, Plen=256 | |
| // Note: Associated data is 65536 bytes (256-byte pattern repeated 256 times) | |
| const key = try hexToBytes(16, "404142434445464748494a4b4c4d4e4f"); | |
| const nonce = try hexToBytes(13, "101112131415161718191a1b1c"); | |
| const plaintext = try hexToBytes(32, "202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f"); | |
| // Generate 65536-byte associated data (256-byte pattern repeated 256 times) | |
| const pattern = try hexToBytes(256, "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff"); | |
| const ad = try allocator.alloc(u8, 65536); | |
| defer allocator.free(ad); | |
| for (0..256) |i| { | |
| @memcpy(ad[i * 256 .. (i + 1) * 256], &pattern); | |
| } | |
| // Expected ciphertext and tag from NIST | |
| const expected_ciphertext = try hexToBytes(32, "69915dad1e84c6376a68c2967e4dab615ae0fd1faec44cc484828529463ccf72"); | |
| const expected_tag = try hexToBytes(14, "b4ac6bec93e8598e7f0dadbcea5b"); | |
| // Encrypt | |
| var c: [plaintext.len]u8 = undefined; | |
| var tag: [Aes128Ccm14.tag_length]u8 = undefined; | |
| Aes128Ccm14.encrypt(&c, &tag, &plaintext, ad, &nonce, key); | |
| // Verify ciphertext matches NIST expected output | |
| try testing.expectEqualSlices(u8, &expected_ciphertext, &c); | |
| // Verify tag matches NIST expected output | |
| try testing.expectEqualSlices(u8, &expected_tag, &tag); | |
| // Decrypt and verify round-trip | |
| var m: [plaintext.len]u8 = undefined; | |
| try Aes128Ccm14.decrypt(&m, &c, tag, ad, &nonce, key); | |
| try testing.expectEqualSlices(u8, &plaintext, &m); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment