Skip to content

Instantly share code, notes, and snippets.

@aarvay
Created October 7, 2025 08:09
Show Gist options
  • Select an option

  • Save aarvay/ee31fb93a984c759967fe1165ea4233d to your computer and use it in GitHub Desktop.

Select an option

Save aarvay/ee31fb93a984c759967fe1165ea4233d to your computer and use it in GitHub Desktop.
AES-CCM implementation in zig
//! 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