Skip to content

Instantly share code, notes, and snippets.

@jedisct1
Created October 8, 2025 08:25
Show Gist options
  • Select an option

  • Save jedisct1/140251214fbf3938e5c0196a7fb37f8f to your computer and use it in GitHub Desktop.

Select an option

Save jedisct1/140251214fbf3938e5c0196a7fb37f8f to your computer and use it in GitHub Desktop.
const std = @import("std");
const crypto = std.crypto;
const mem = std.mem;
const debug = std.debug;
const modes = crypto.core.modes;
const AuthenticationError = crypto.errors.AuthenticationError;
const cbc_mac = @import("cbc_mac.zig");
/// CCM (Counter with CBC-MAC) authenticated encryption mode
/// RFC 3610: https://www.rfc-editor.org/rfc/rfc3610
/// NIST SP 800-38C: https://nvlpubs.nist.gov/nistpubs/SpecialPublications/NIST.SP.800-38C.pdf
///
/// CCM is an AEAD (Authenticated Encryption with Associated Data) mode that combines
/// CTR mode encryption with CBC-MAC authentication.
///
/// Common instances with AES-128 and standard parameters (tag=16, nonce=12)
pub const Aes128Ccm = Ccm(crypto.core.aes.Aes128, 16, 12);
pub const Aes256Ccm = Ccm(crypto.core.aes.Aes256, 16, 12);
/// Generic CCM mode implementation
///
/// Parameters:
/// - BlockCipher: Block cipher type (must have 16-byte blocks)
/// - tag_length: Authentication tag length in bytes (4, 6, 8, 10, 12, 14, or 16)
/// - nonce_length: Nonce length in bytes (7 to 13)
pub fn Ccm(comptime BlockCipher: type, comptime tag_length: usize, comptime nonce_length: usize) type {
const block_length = BlockCipher.block.block_length;
debug.assert(block_length == 16); // CCM requires 16-byte blocks
// Validate tag_length
comptime {
if (tag_length < 4 or tag_length > 16 or tag_length % 2 != 0) {
@compileError("CCM tag_length must be 4, 6, 8, 10, 12, 14, or 16 bytes");
}
}
// Validate nonce_length
comptime {
if (nonce_length < 7 or nonce_length > 13) {
@compileError("CCM nonce_length must be between 7 and 13 bytes");
}
}
const L = 15 - nonce_length; // Counter size in bytes (1 to 8)
const BlockCipherCtx = @typeInfo(@TypeOf(BlockCipher.initEnc)).@"fn".return_type.?;
return struct {
pub const key_length = BlockCipher.key_bits / 8;
/// Encrypt a message with associated data
///
/// Parameters:
/// - c: Ciphertext output buffer (must be same length as m)
/// - tag: Authentication tag output buffer
/// - m: Plaintext message
/// - ad: Associated data (authenticated but not encrypted)
/// - npub: Nonce (public, 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 {
debug.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;
debug.assert(m.len <= max_msg_len);
const cipher_ctx = BlockCipher.initEnc(key);
// 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;
}
// Encrypt the plaintext using CTR mode (starting from counter = 1)
formatCtrBlock(&ctr_block, npub, 1);
const counter_offset = 1 + nonce_length;
const counter_size = L;
modes.ctrSlice(BlockCipherCtx, cipher_ctx, c, m, ctr_block, .big, counter_offset, counter_size);
}
/// Decrypt a message with associated data
///
/// Parameters:
/// - m: Plaintext output buffer (must be same length as c)
/// - c: Ciphertext
/// - tag: Authentication tag
/// - ad: Associated data
/// - npub: Nonce
/// - key: Encryption key
///
/// Returns: AuthenticationError if tag verification fails
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 {
debug.assert(m.len == c.len);
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);
const counter_offset = 1 + nonce_length;
const counter_size = L;
modes.ctrSlice(BlockCipherCtx, cipher_ctx, m, c, ctr_block, .big, counter_offset, counter_size);
// Compute CBC-MAC over decrypted plaintext using the reusable CBC-MAC module
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, m);
return error.AuthenticationFailed;
}
}
/// 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 = std.meta.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
///
/// Note: CCM uses plain CBC-MAC, NOT CMAC (despite stdlib having CMAC available).
/// CBC-MAC: Simple MAC = Encrypt(prev_mac XOR block), no subkey derivation
/// CMAC: Derives K1/K2 subkeys and has special final block handling
/// These produce DIFFERENT outputs. RFC 3610 requires CBC-MAC for CCM.
///
/// This function uses the reusable CbcMac module from cbc_mac.zig.
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 = [_]u8{0} ** block_length;
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, L' = L - 1
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 = @intCast((tag_length - 2) / 2);
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 = std.meta.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;
}
}
};
}
const testing = std.testing;
test "Aes128Ccm - RFC 3610 Packet Vector #1" {
// MAC: 8 octets, Message: 23 octets, Nonce: 13 octets
const Ccm813 = Ccm(crypto.core.aes.Aes128, 8, 13);
const key = [_]u8{ 0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCC, 0xCD, 0xCE, 0xCF };
const nonce = [_]u8{ 0x00, 0x00, 0x00, 0x03, 0x02, 0x01, 0x00, 0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5 };
const ad = [_]u8{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07 };
const plaintext = [_]u8{ 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E };
const expected_ciphertext = [_]u8{ 0x58, 0x8C, 0x97, 0x9A, 0x61, 0xC6, 0x63, 0xD2, 0xF0, 0x66, 0xD0, 0xC2, 0xC0, 0xF9, 0x89, 0x80, 0x6D, 0x5F, 0x6B, 0x61, 0xDA, 0xC3, 0x84 };
const expected_tag = [_]u8{ 0x17, 0xE8, 0xD1, 0x2C, 0xFD, 0xF9, 0x26, 0xE0 };
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [8]u8 = undefined;
Ccm813.encrypt(&ciphertext, &tag, &plaintext, &ad, nonce, key);
try testing.expectEqualSlices(u8, &expected_ciphertext, &ciphertext);
try testing.expectEqualSlices(u8, &expected_tag, &tag);
// Test decryption
var decrypted: [plaintext.len]u8 = undefined;
try Ccm813.decrypt(&decrypted, &ciphertext, tag, &ad, nonce, key);
try testing.expectEqualSlices(u8, &plaintext, &decrypted);
}
test "Aes128Ccm - Standard parameters (tag=16, nonce=12)" {
const key: [Aes128Ccm.key_length]u8 = [_]u8{0x42} ** Aes128Ccm.key_length;
const nonce: [12]u8 = [_]u8{0x01} ** 12;
const ad = "Additional data";
const plaintext = "Hello, CCM mode!";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
Aes128Ccm.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
// Test decryption
var decrypted: [plaintext.len]u8 = undefined;
try Aes128Ccm.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
try testing.expectEqualSlices(u8, plaintext, &decrypted);
// Test that wrong tag fails
tag[0] ^= 1;
try testing.expectError(error.AuthenticationFailed, Aes128Ccm.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key));
}
test "Aes128Ccm - Empty message" {
const key: [Aes128Ccm.key_length]u8 = [_]u8{0x42} ** Aes128Ccm.key_length;
const nonce: [12]u8 = [_]u8{0x01} ** 12;
const ad = "Additional data";
const plaintext = "";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
Aes128Ccm.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
var decrypted: [plaintext.len]u8 = undefined;
try Aes128Ccm.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
try testing.expectEqualSlices(u8, plaintext, &decrypted);
}
test "Aes128Ccm - No associated data" {
const key: [Aes128Ccm.key_length]u8 = [_]u8{0x42} ** Aes128Ccm.key_length;
const nonce: [12]u8 = [_]u8{0x01} ** 12;
const ad = "";
const plaintext = "Hello, CCM mode!";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
Aes128Ccm.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
var decrypted: [plaintext.len]u8 = undefined;
try Aes128Ccm.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
try testing.expectEqualSlices(u8, plaintext, &decrypted);
}
test "Aes256Ccm - Basic test" {
const key: [Aes256Ccm.key_length]u8 = [_]u8{0x42} ** Aes256Ccm.key_length;
const nonce: [12]u8 = [_]u8{0x01} ** 12;
const ad = "Additional data";
const plaintext = "Hello, AES-256-CCM!";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
Aes256Ccm.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
var decrypted: [plaintext.len]u8 = undefined;
try Aes256Ccm.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
try testing.expectEqualSlices(u8, plaintext, &decrypted);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment