AES-CCM authenticated encryption (NIST SP 800-38C, RFC 3610). CCM* mode extends CCM to support encryption-only mode when tag_len=0.
BlockCipher: Block cipher type (must have 16-byte blocks).
tag_len: Authentication tag length in bytes (0, 4, 6, 8, 10, 12, 14, or 16).
When tag_len=0, CCM* provides encryption-only (no authentication).
nonce_len: Nonce length in bytes (7 to 13).
fn AesCcm(comptime BlockCipher: type, comptime tag_len: usize, comptime nonce_len: usize) type
fn AesCcm(comptime BlockCipher: type, comptime tag_len: usize, comptime nonce_len: usize) type {
const block_length = BlockCipher.block.block_length;
comptime {
assert(block_length == 16); // CCM requires 16-byte blocks
if (tag_len != 0 and (tag_len < 4 or tag_len > 16 or tag_len % 2 != 0)) {
@compileError("CCM tag_length must be 0, 4, 6, 8, 10, 12, 14, or 16 bytes");
}
if (nonce_len < 7 or nonce_len > 13) {
@compileError("CCM nonce_length must be between 7 and 13 bytes");
}
}
const L = 15 - nonce_len; // Counter size in bytes (2 to 8)
return struct {
pub const key_length = BlockCipher.key_bits / 8;
pub const tag_length = tag_len;
pub const nonce_length = nonce_len;
/// `c`: Ciphertext output buffer (must be same length as m).
/// `tag`: Authentication tag output.
/// `m`: Plaintext message to encrypt.
/// `ad`: Associated data to authenticate.
/// `npub`: Public nonce (must be unique for each message with same key).
/// `key`: Encryption key.
pub fn encrypt(
c: []u8,
tag: *[tag_length]u8,
m: []const u8,
ad: []const u8,
npub: [nonce_length]u8,
key: [key_length]u8,
) void {
assert(c.len == m.len);
// Validate message length fits in L bytes
const max_msg_len: u64 = if (L >= 8) std.math.maxInt(u64) else (@as(u64, 1) << @as(u6, @intCast(L * 8))) - 1;
assert(m.len <= max_msg_len);
const cipher_ctx = BlockCipher.initEnc(key);
// CCM*: Skip authentication if tag_length is 0 (encryption-only mode)
if (tag_length > 0) {
// Compute CBC-MAC using the reusable CBC-MAC module
var mac_result: [block_length]u8 = undefined;
computeCbcMac(&mac_result, &key, m, ad, npub);
// Construct counter block for tag encryption (counter = 0)
var ctr_block: [block_length]u8 = undefined;
formatCtrBlock(&ctr_block, npub, 0);
// Encrypt the MAC tag
var s0: [block_length]u8 = undefined;
cipher_ctx.encrypt(&s0, &ctr_block);
for (tag, mac_result[0..tag_length], s0[0..tag_length]) |*t, mac_byte, s_byte| {
t.* = mac_byte ^ s_byte;
}
crypto.secureZero(u8, &mac_result);
crypto.secureZero(u8, &s0);
}
// Encrypt the plaintext using CTR mode (starting from counter = 1)
var ctr_block: [block_length]u8 = undefined;
formatCtrBlock(&ctr_block, npub, 1);
// CCM counter is in the last L bytes of the block
modes.ctrSlice(@TypeOf(cipher_ctx), cipher_ctx, c, m, ctr_block, .big, 1 + nonce_len, L);
}
/// `m`: Plaintext output buffer (must be same length as c).
/// `c`: Ciphertext to decrypt.
/// `tag`: Authentication tag to verify.
/// `ad`: Associated data (must match encryption).
/// `npub`: Public nonce (must match encryption).
/// `key`: Private key.
///
/// Asserts `c.len == m.len`.
/// Contents of `m` are undefined if an error is returned.
pub fn decrypt(
m: []u8,
c: []const u8,
tag: [tag_length]u8,
ad: []const u8,
npub: [nonce_length]u8,
key: [key_length]u8,
) AuthenticationError!void {
assert(m.len == c.len);
const max_msg_len: u64 = if (L >= 8) std.math.maxInt(u64) else (@as(u64, 1) << @as(u6, @intCast(L * 8))) - 1;
if (c.len > max_msg_len) return error.AuthenticationFailed;
const cipher_ctx = BlockCipher.initEnc(key);
// Decrypt the ciphertext using CTR mode (starting from counter = 1)
var ctr_block: [block_length]u8 = undefined;
formatCtrBlock(&ctr_block, npub, 1);
// CCM counter is in the last L bytes of the block
modes.ctrSlice(@TypeOf(cipher_ctx), cipher_ctx, m, c, ctr_block, .big, 1 + nonce_len, L);
// CCM*: Skip authentication if tag_length is 0 (encryption-only mode)
if (tag_length > 0) {
// Compute CBC-MAC over decrypted plaintext
var mac_result: [block_length]u8 = undefined;
computeCbcMac(&mac_result, &key, m, ad, npub);
// Decrypt the received tag
formatCtrBlock(&ctr_block, npub, 0);
var s0: [block_length]u8 = undefined;
cipher_ctx.encrypt(&s0, &ctr_block);
// Reconstruct the expected MAC
var expected_mac: [tag_length]u8 = undefined;
for (&expected_mac, mac_result[0..tag_length], s0[0..tag_length]) |*e, mac_byte, s_byte| {
e.* = mac_byte ^ s_byte;
}
// Constant-time tag comparison
const valid = crypto.timing_safe.eql([tag_length]u8, expected_mac, tag);
if (!valid) {
crypto.secureZero(u8, &expected_mac);
crypto.secureZero(u8, &mac_result);
crypto.secureZero(u8, &s0);
crypto.secureZero(u8, m);
return error.AuthenticationFailed;
}
crypto.secureZero(u8, &expected_mac);
crypto.secureZero(u8, &mac_result);
crypto.secureZero(u8, &s0);
}
}
/// Format the counter block for CTR mode
/// Counter block format: [flags | nonce | counter]
/// flags = L - 1
fn formatCtrBlock(block: *[block_length]u8, npub: [nonce_length]u8, counter: u64) void {
@memset(block, 0);
block[0] = L - 1; // flags
@memcpy(block[1..][0..nonce_length], &npub);
// Counter goes in the last L bytes
const CounterInt = @Int(.unsigned, L * 8);
mem.writeInt(CounterInt, block[1 + nonce_length ..][0..L], @as(CounterInt, @intCast(counter)), .big);
}
/// Compute CBC-MAC over the message and associated data.
/// CCM uses plain CBC-MAC, not CMAC (RFC 3610).
fn computeCbcMac(mac: *[block_length]u8, key: *const [key_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8) void {
const CbcMac = cbc_mac.CbcMac(BlockCipher);
var ctx = CbcMac.init(key);
// Process B_0 block
var b0: [block_length]u8 = undefined;
formatB0Block(&b0, m.len, ad.len, npub);
ctx.update(&b0);
// Process associated data if present
// RFC 3610: AD is (encoded_length || ad) padded to block boundary
if (ad.len > 0) {
// Encode and add associated data length
var ad_len_encoding: [10]u8 = undefined;
const ad_len_size = encodeAdLength(&ad_len_encoding, ad.len);
// Process AD with padding to block boundary
ctx.update(ad_len_encoding[0..ad_len_size]);
ctx.update(ad);
// Add zero padding to reach block boundary
const total_ad_size = ad_len_size + ad.len;
const remainder = total_ad_size % block_length;
if (remainder > 0) {
const padding: [block_length]u8 = @splat(0);
ctx.update(padding[0 .. block_length - remainder]);
}
}
// Process plaintext message
ctx.update(m);
// Finalize MAC
ctx.final(mac);
}
/// Format the B_0 block for CBC-MAC
/// B_0 format: [flags | nonce | message_length]
/// flags = 64*Adata + 8*M' + L'
/// where: Adata = (ad.len > 0), M' = (tag_length - 2)/2 if M>0 else 0, L' = L - 1
/// CCM*: When tag_length=0, M' is encoded as 0
fn formatB0Block(block: *[block_length]u8, msg_len: usize, ad_len: usize, npub: [nonce_length]u8) void {
@memset(block, 0);
const Adata: u8 = if (ad_len > 0) 1 else 0;
const M_prime: u8 = if (tag_length > 0) @intCast((tag_length - 2) / 2) else 0;
const L_prime: u8 = L - 1;
block[0] = (Adata << 6) | (M_prime << 3) | L_prime;
@memcpy(block[1..][0..nonce_length], &npub);
// Encode message length in last L bytes
const LengthInt = @Int(.unsigned, L * 8);
mem.writeInt(LengthInt, block[1 + nonce_length ..][0..L], @as(LengthInt, @intCast(msg_len)), .big);
}
/// Encode associated data length according to CCM specification
/// Returns the number of bytes written
fn encodeAdLength(buf: *[10]u8, ad_len: usize) usize {
if (ad_len < 65280) { // 2^16 - 2^8
// Encode as 2 bytes
mem.writeInt(u16, buf[0..2], @as(u16, @intCast(ad_len)), .big);
return 2;
} else if (ad_len <= std.math.maxInt(u32)) {
// Encode as 0xff || 0xfe || 4 bytes
buf[0] = 0xff;
buf[1] = 0xfe;
mem.writeInt(u32, buf[2..6], @as(u32, @intCast(ad_len)), .big);
return 6;
} else {
// Encode as 0xff || 0xff || 8 bytes
buf[0] = 0xff;
buf[1] = 0xff;
mem.writeInt(u64, buf[2..10], @as(u64, @intCast(ad_len)), .big);
return 10;
}
}
};
}