Zig 0.17.0-dev (Split by item)

This is an example of documentation generated by ZigDoc, an alternative to Zig's built-in Auto Doc feature. See also examples in other modes/formats. The project being documented here (as the example) is the Zig library itself.

AesCcm

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).

aes_ccm.AesCcm
fn AesCcm(comptime BlockCipher: type, comptime tag_len: usize, comptime nonce_len: usize) type

File

lib/std/crypto/aes_ccm.zig:36

Code

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;
            }
        }
    };
}