Skip to content

Instantly share code, notes, and snippets.

@jedisct1
Created October 8, 2025 08:25
Show Gist options
  • Save jedisct1/4227b9c5026fb095a0f007e4fad25a34 to your computer and use it in GitHub Desktop.
Save jedisct1/4227b9c5026fb095a0f007e4fad25a34 to your computer and use it in GitHub Desktop.
const std = @import("std");
const crypto = std.crypto;
const mem = std.mem;
/// CBC-MAC (Cipher Block Chaining Message Authentication Code)
///
/// CBC-MAC is a simple MAC construction: MAC = Encrypt(prev_mac XOR block)
/// Unlike CMAC (RFC 4493), CBC-MAC does not derive subkeys or perform special
/// final block processing. It is less secure than CMAC (vulnerable to length
/// extension attacks), but is required by certain standards like CCM (RFC 3610).
///
/// WARNING: CBC-MAC is vulnerable when used with variable-length messages.
/// Only use CBC-MAC when required by a specific protocol (like CCM) that
/// mitigates this vulnerability. For general-purpose MACs, use CMAC instead.
///
/// Common instance with AES-128
pub const CbcMacAes128 = CbcMac(crypto.core.aes.Aes128);
/// Generic CBC-MAC implementation
///
/// This follows the same interface pattern as CMAC for consistency,
/// but implements the simpler CBC-MAC algorithm without subkey derivation.
pub fn CbcMac(comptime BlockCipher: type) type {
const BlockCipherCtx = @typeInfo(@TypeOf(BlockCipher.initEnc)).@"fn".return_type.?;
const Block = [BlockCipher.block.block_length]u8;
return struct {
const Self = @This();
pub const key_length = BlockCipher.key_bits / 8;
pub const block_length = BlockCipher.block.block_length;
pub const mac_length = block_length;
cipher_ctx: BlockCipherCtx,
buf: Block = [_]u8{0} ** block_length,
pos: usize = 0,
/// Compute CBC-MAC in one shot
pub fn create(out: *[mac_length]u8, msg: []const u8, key: *const [key_length]u8) void {
var ctx = Self.init(key);
ctx.update(msg);
ctx.final(out);
}
/// Initialize CBC-MAC with a key
pub fn init(key: *const [key_length]u8) Self {
const cipher_ctx = BlockCipher.initEnc(key.*);
return Self{
.cipher_ctx = cipher_ctx,
};
}
/// Process message data incrementally
pub fn update(self: *Self, msg: []const u8) void {
const left = block_length - self.pos;
var m = msg;
// Fill remaining buffer if we have partial data
if (m.len > left) {
for (self.buf[self.pos..], 0..) |*b, i| b.* ^= m[i];
m = m[left..];
self.cipher_ctx.encrypt(&self.buf, &self.buf);
self.pos = 0;
}
// Process complete blocks
while (m.len > block_length) {
for (self.buf[0..block_length], 0..) |*b, i| b.* ^= m[i];
m = m[block_length..];
self.cipher_ctx.encrypt(&self.buf, &self.buf);
self.pos = 0;
}
// Buffer remaining partial block
if (m.len > 0) {
for (self.buf[self.pos..][0..m.len], 0..) |*b, i| b.* ^= m[i];
self.pos += m.len;
}
}
/// Finalize and output the MAC
///
/// Note: Unlike CMAC, CBC-MAC does not perform special processing
/// for the final block. Incomplete blocks are implicitly zero-padded.
pub fn final(self: *Self, out: *[mac_length]u8) void {
// For CBC-MAC, we simply encrypt the current buffer state
// (which is implicitly zero-padded if incomplete)
self.cipher_ctx.encrypt(out, &self.buf);
}
};
}
const testing = std.testing;
test "CbcMacAes128 - Empty message" {
const key = [_]u8{ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c };
var msg: [0]u8 = undefined;
// CBC-MAC of empty message = Encrypt(0)
const expected = [_]u8{ 0x7d, 0xf7, 0x6b, 0x0c, 0x1a, 0xb8, 0x99, 0xb3, 0x3e, 0x42, 0xf0, 0x47, 0xb9, 0x1b, 0x54, 0x6f };
var out: [CbcMacAes128.mac_length]u8 = undefined;
CbcMacAes128.create(&out, &msg, &key);
try testing.expectEqualSlices(u8, &expected, &out);
}
test "CbcMacAes128 - Single block (16 bytes)" {
const key = [_]u8{ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c };
const msg = [_]u8{ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a };
// CBC-MAC = Encrypt(msg XOR 0)
const expected = [_]u8{ 0x3a, 0xd7, 0x7b, 0xb4, 0x0d, 0x7a, 0x36, 0x60, 0xa8, 0x9e, 0xca, 0xf3, 0x24, 0x66, 0xef, 0x97 };
var out: [CbcMacAes128.mac_length]u8 = undefined;
CbcMacAes128.create(&out, &msg, &key);
try testing.expectEqualSlices(u8, &expected, &out);
}
test "CbcMacAes128 - Multiple blocks (40 bytes)" {
const key = [_]u8{ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c };
const msg = [_]u8{
0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a,
0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51,
0x30, 0xc8, 0x1c, 0x46, 0xa3, 0x5c, 0xe4, 0x11,
};
// CBC-MAC processes: block1 | block2 | block3 (last 8 bytes zero-padded)
const expected = [_]u8{ 0x07, 0xd1, 0x92, 0xe3, 0xe6, 0xf0, 0x99, 0xed, 0xcc, 0x39, 0xfd, 0xe6, 0xd0, 0x9c, 0x76, 0x2d };
var out: [CbcMacAes128.mac_length]u8 = undefined;
CbcMacAes128.create(&out, &msg, &key);
try testing.expectEqualSlices(u8, &expected, &out);
}
test "CbcMacAes128 - Incremental update" {
const key = [_]u8{ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c };
const msg = [_]u8{
0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a,
0xae, 0x2d, 0x8a, 0x57, 0x1e, 0x03, 0xac, 0x9c, 0x9e, 0xb7, 0x6f, 0xac, 0x45, 0xaf, 0x8e, 0x51,
};
// Process in chunks
var ctx = CbcMacAes128.init(&key);
ctx.update(msg[0..10]);
ctx.update(msg[10..20]);
ctx.update(msg[20..]);
var out1: [CbcMacAes128.mac_length]u8 = undefined;
ctx.final(&out1);
// Compare with one-shot processing
var out2: [CbcMacAes128.mac_length]u8 = undefined;
CbcMacAes128.create(&out2, &msg, &key);
try testing.expectEqualSlices(u8, &out1, &out2);
}
test "CbcMacAes128 - Different from CMAC" {
// Verify that CBC-MAC and CMAC produce different outputs
const key = [_]u8{ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c };
const msg = [_]u8{ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a };
var cbc_mac_out: [CbcMacAes128.mac_length]u8 = undefined;
CbcMacAes128.create(&cbc_mac_out, &msg, &key);
// CMAC output for same input (from RFC 4493)
const cmac_out = [_]u8{ 0x07, 0x0a, 0x16, 0xb4, 0x6b, 0x4d, 0x41, 0x44, 0xf7, 0x9b, 0xdd, 0x9d, 0xd0, 0x4a, 0x28, 0x7c };
// They should be different
try testing.expect(!mem.eql(u8, &cbc_mac_out, &cmac_out));
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment