mirror of
https://codeberg.org/ziglang/zig.git
synced 2025-12-06 22:04:21 +00:00
The Zig standard library lacked schemes that resist nonce reuse. AES-SIV and AES-GCM-SIV are the standard options for this. AES-GCM-SIV can be very useful when Zig is used to target embedded systems, and AES-SIV is especially useful for key wrapping. Also take it as an opportunity to add a bunch of test vectors to modes.ctr and make sure it works with block ciphers whose size is not 16.
227 lines
11 KiB
Zig
227 lines
11 KiB
Zig
// Based on Go stdlib implementation
|
|
|
|
const std = @import("../std.zig");
|
|
const mem = std.mem;
|
|
const debug = std.debug;
|
|
|
|
/// Counter mode.
|
|
///
|
|
/// This mode creates a key stream by encrypting an incrementing counter using a block cipher, and adding it to the source material.
|
|
///
|
|
/// Important: the counter mode doesn't provide authenticated encryption: the ciphertext can be trivially modified without this being detected.
|
|
/// As a result, applications should generally never use it directly, but only in a construction that includes a MAC.
|
|
pub fn ctr(comptime BlockCipher: anytype, block_cipher: BlockCipher, dst: []u8, src: []const u8, iv: [BlockCipher.block_length]u8, endian: std.builtin.Endian) void {
|
|
ctrSlice(BlockCipher, block_cipher, dst, src, iv, endian, 0, BlockCipher.block_length);
|
|
}
|
|
|
|
/// Counter mode with configurable counter position and size.
|
|
///
|
|
/// This extended version allows specifying where the counter is located within the IV block
|
|
/// and how many bytes it occupies. This is useful for modes like AES-GCM-SIV which use a
|
|
/// 32-bit counter at the beginning of the block.
|
|
///
|
|
/// @param counter_offset: Byte offset where the counter starts
|
|
/// @param counter_size: Size of the counter in bytes
|
|
pub fn ctrSlice(
|
|
comptime BlockCipher: anytype,
|
|
block_cipher: BlockCipher,
|
|
dst: []u8,
|
|
src: []const u8,
|
|
iv: [BlockCipher.block_length]u8,
|
|
endian: std.builtin.Endian,
|
|
comptime counter_offset: usize,
|
|
comptime counter_size: usize,
|
|
) void {
|
|
debug.assert(dst.len >= src.len);
|
|
const block_length = BlockCipher.block_length;
|
|
debug.assert(counter_offset + counter_size <= block_length);
|
|
debug.assert(counter_size > 0 and counter_size <= block_length);
|
|
|
|
var counterBlock = iv;
|
|
var i: usize = 0;
|
|
|
|
const CounterInt = std.meta.Int(.unsigned, counter_size * 8);
|
|
|
|
const parallel_count = BlockCipher.block.parallel.optimal_parallel_blocks;
|
|
const wide_block_length = parallel_count * block_length;
|
|
var cnt_val = mem.readInt(CounterInt, counterBlock[counter_offset..][0..counter_size], endian);
|
|
if (src.len >= wide_block_length) {
|
|
var counters: [parallel_count * block_length]u8 = undefined;
|
|
inline for (0..parallel_count) |j| {
|
|
counters[j * block_length ..][0..block_length].* = iv;
|
|
}
|
|
while (i + wide_block_length <= src.len) : (i += wide_block_length) {
|
|
comptime var j = 0;
|
|
inline while (j < parallel_count) : (j += 1) {
|
|
mem.writeInt(CounterInt, counters[j * block_length + counter_offset ..][0..counter_size], cnt_val +% j, endian);
|
|
}
|
|
cnt_val += parallel_count;
|
|
block_cipher.xorWide(parallel_count, dst[i .. i + wide_block_length][0..wide_block_length], src[i .. i + wide_block_length][0..wide_block_length], counters);
|
|
}
|
|
mem.writeInt(CounterInt, counterBlock[counter_offset..][0..counter_size], cnt_val, endian);
|
|
}
|
|
while (i + block_length <= src.len) : (i += block_length) {
|
|
block_cipher.xor(dst[i .. i + block_length][0..block_length], src[i .. i + block_length][0..block_length], counterBlock);
|
|
cnt_val +%= 1;
|
|
mem.writeInt(CounterInt, counterBlock[counter_offset..][0..counter_size], cnt_val, endian);
|
|
}
|
|
if (i < src.len) {
|
|
var pad: [block_length]u8 = @splat(0);
|
|
const src_slice = src[i..];
|
|
@memcpy(pad[0..src_slice.len], src_slice);
|
|
block_cipher.xor(&pad, &pad, counterBlock);
|
|
const pad_slice = pad[0 .. src.len - i];
|
|
@memcpy(dst[i..][0..pad_slice.len], pad_slice);
|
|
}
|
|
}
|
|
|
|
test "ctr mode" {
|
|
const testing = std.testing;
|
|
const aes = std.crypto.core.aes;
|
|
|
|
// Test key and IV from NIST SP 800-38A
|
|
const key = [_]u8{ 0x2b, 0x7e, 0x15, 0x16, 0x28, 0xae, 0xd2, 0xa6, 0xab, 0xf7, 0x15, 0x88, 0x09, 0xcf, 0x4f, 0x3c };
|
|
const iv = [_]u8{ 0xf0, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xfb, 0xfc, 0xfd, 0xfe, 0xff };
|
|
const ctx = aes.Aes128.initEnc(key);
|
|
|
|
// Test 1: Empty input
|
|
{
|
|
const in = [_]u8{};
|
|
const expected = [_]u8{};
|
|
var out: [0]u8 = undefined;
|
|
ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
|
|
try testing.expectEqualSlices(u8, expected[0..], out[0..]);
|
|
}
|
|
|
|
// Test 2: Single byte
|
|
{
|
|
const in = [_]u8{0x6b};
|
|
const expected = [_]u8{0x87};
|
|
var out: [1]u8 = undefined;
|
|
ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
|
|
try testing.expectEqualSlices(u8, expected[0..], out[0..]);
|
|
}
|
|
|
|
// Test 3: Less than one block (15 bytes)
|
|
{
|
|
const in = [_]u8{ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17 };
|
|
const expected = [_]u8{ 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6 };
|
|
var out: [15]u8 = undefined;
|
|
ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
|
|
try testing.expectEqualSlices(u8, expected[0..], out[0..]);
|
|
}
|
|
|
|
// Test 4: Exactly one block (16 bytes)
|
|
{
|
|
const in = [_]u8{ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a };
|
|
const expected = [_]u8{ 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce };
|
|
var out: [16]u8 = undefined;
|
|
ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
|
|
try testing.expectEqualSlices(u8, expected[0..], out[0..]);
|
|
}
|
|
|
|
// Test 5: One block plus one byte (17 bytes)
|
|
{
|
|
const in = [_]u8{ 0x6b, 0xc1, 0xbe, 0xe2, 0x2e, 0x40, 0x9f, 0x96, 0xe9, 0x3d, 0x7e, 0x11, 0x73, 0x93, 0x17, 0x2a, 0xae };
|
|
const expected = [_]u8{ 0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce, 0x98 };
|
|
var out: [17]u8 = undefined;
|
|
ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
|
|
try testing.expectEqualSlices(u8, expected[0..], out[0..]);
|
|
}
|
|
|
|
// Test 6: Exactly two blocks (32 bytes)
|
|
{
|
|
const in = [_]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,
|
|
};
|
|
const expected = [_]u8{
|
|
0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce,
|
|
0x98, 0x06, 0xf6, 0x6b, 0x79, 0x70, 0xfd, 0xff, 0x86, 0x17, 0x18, 0x7b, 0xb9, 0xff, 0xfd, 0xff,
|
|
};
|
|
var out: [32]u8 = undefined;
|
|
ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
|
|
try testing.expectEqualSlices(u8, expected[0..], out[0..]);
|
|
}
|
|
|
|
// Test 7: Two blocks plus 5 bytes (37 bytes)
|
|
{
|
|
const in = [_]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,
|
|
};
|
|
const expected = [_]u8{
|
|
0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce,
|
|
0x98, 0x06, 0xf6, 0x6b, 0x79, 0x70, 0xfd, 0xff, 0x86, 0x17, 0x18, 0x7b, 0xb9, 0xff, 0xfd, 0xff,
|
|
0x5a, 0xe4, 0xdf, 0x3e, 0xdb,
|
|
};
|
|
var out: [37]u8 = undefined;
|
|
ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
|
|
try testing.expectEqualSlices(u8, expected[0..], out[0..]);
|
|
}
|
|
|
|
// Test 8: Four blocks (64 bytes) - NIST test vector
|
|
{
|
|
const in = [_]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, 0xe5, 0xfb, 0xc1, 0x19, 0x1a, 0x0a, 0x52, 0xef,
|
|
0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, 0xe6, 0x6c, 0x37, 0x10,
|
|
};
|
|
const expected = [_]u8{
|
|
0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce,
|
|
0x98, 0x06, 0xf6, 0x6b, 0x79, 0x70, 0xfd, 0xff, 0x86, 0x17, 0x18, 0x7b, 0xb9, 0xff, 0xfd, 0xff,
|
|
0x5a, 0xe4, 0xdf, 0x3e, 0xdb, 0xd5, 0xd3, 0x5e, 0x5b, 0x4f, 0x09, 0x02, 0x0d, 0xb0, 0x3e, 0xab,
|
|
0x1e, 0x03, 0x1d, 0xda, 0x2f, 0xbe, 0x03, 0xd1, 0x79, 0x21, 0x70, 0xa0, 0xf3, 0x00, 0x9c, 0xee,
|
|
};
|
|
var out: [64]u8 = undefined;
|
|
ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
|
|
try testing.expectEqualSlices(u8, expected[0..], out[0..]);
|
|
}
|
|
|
|
// Test 9: Large input (> 2*block_length, 100 bytes)
|
|
{
|
|
// Create a 100-byte input by extending with zeros
|
|
var in: [100]u8 = [_]u8{0} ** 100;
|
|
@memcpy(in[0..64], &[_]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, 0xe5, 0xfb, 0xc1, 0x19, 0x1a, 0x0a, 0x52, 0xef,
|
|
0xf6, 0x9f, 0x24, 0x45, 0xdf, 0x4f, 0x9b, 0x17, 0xad, 0x2b, 0x41, 0x7b, 0xe6, 0x6c, 0x37, 0x10,
|
|
});
|
|
|
|
// Expected output: first 64 bytes from NIST, then CTR continues with zeros
|
|
var expected: [100]u8 = undefined;
|
|
@memcpy(expected[0..64], &[_]u8{
|
|
0x87, 0x4d, 0x61, 0x91, 0xb6, 0x20, 0xe3, 0x26, 0x1b, 0xef, 0x68, 0x64, 0x99, 0x0d, 0xb6, 0xce,
|
|
0x98, 0x06, 0xf6, 0x6b, 0x79, 0x70, 0xfd, 0xff, 0x86, 0x17, 0x18, 0x7b, 0xb9, 0xff, 0xfd, 0xff,
|
|
0x5a, 0xe4, 0xdf, 0x3e, 0xdb, 0xd5, 0xd3, 0x5e, 0x5b, 0x4f, 0x09, 0x02, 0x0d, 0xb0, 0x3e, 0xab,
|
|
0x1e, 0x03, 0x1d, 0xda, 0x2f, 0xbe, 0x03, 0xd1, 0x79, 0x21, 0x70, 0xa0, 0xf3, 0x00, 0x9c, 0xee,
|
|
});
|
|
// Compute the rest with zeros XORed with keystream
|
|
@memcpy(expected[64..], &[_]u8{
|
|
0xb0, 0x0d, 0x47, 0xf8, 0x14, 0x8a, 0x91, 0x0e, 0xf0, 0x68, 0x30, 0x97, 0x90, 0x4b, 0xa5, 0x02,
|
|
0x58, 0x99, 0x44, 0x5a, 0x4d, 0xe1, 0x01, 0xf5, 0x13, 0xca, 0xd1, 0x98, 0x7d, 0x89, 0xe9, 0x1b,
|
|
0x3b, 0xd9, 0xac, 0x79,
|
|
});
|
|
|
|
var out: [100]u8 = undefined;
|
|
ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], iv, std.builtin.Endian.big);
|
|
try testing.expectEqualSlices(u8, expected[0..], out[0..]);
|
|
}
|
|
|
|
// Test 10: Test with different endianness (little-endian counter)
|
|
{
|
|
const le_iv = [_]u8{ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };
|
|
const in = [_]u8{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff };
|
|
|
|
// We'll compute the expected value from the actual encryption
|
|
var out: [16]u8 = undefined;
|
|
ctr(aes.AesEncryptCtx(aes.Aes128), ctx, out[0..], in[0..], le_iv, std.builtin.Endian.little);
|
|
|
|
// The actual output for this test with little-endian counter=1
|
|
const expected = [_]u8{ 0x7e, 0x48, 0x15, 0xa8, 0x16, 0x66, 0xf0, 0xea, 0xad, 0x3c, 0x07, 0x97, 0x2f, 0xe8, 0x25, 0xc1 };
|
|
try testing.expectEqualSlices(u8, expected[0..], out[0..]);
|
|
}
|
|
}
|