diff --git a/src/primitive/blockcipher/index.zig b/src/primitive/blockcipher/index.zig index 6718624..78798ae 100644 --- a/src/primitive/blockcipher/index.zig +++ b/src/primitive/blockcipher/index.zig @@ -1,5 +1,6 @@ pub const aes = @import("aes.zig"); pub const des = @import("des.zig"); +pub const serpent = @import("serpent.zig"); pub const padding = @import("padding.zig"); diff --git a/src/primitive/blockcipher/serpent.zig b/src/primitive/blockcipher/serpent.zig new file mode 100644 index 0000000..5a99a88 --- /dev/null +++ b/src/primitive/blockcipher/serpent.zig @@ -0,0 +1,237 @@ +const std = @import("std"); +const testing = std.testing; + +const byte_operations = @import("utility").byte_operations; +const word_to_bytes = byte_operations.word_to_bytes_le; +const bytes_to_word = byte_operations.bytes_to_word_le; + +const CryptoError = error{ + InvalidKeyLength, +}; + +// ----------------------------------- CONSTANTS ----------------------------------- // + +pub const BLOCK_SIZE = 128 / 8; + +pub const KEY_SIZE_128 = 128 / 8; +pub const KEY_SIZE_192 = 192 / 8; +pub const KEY_SIZE_256 = 256 / 8; + +pub const MAX_KEY_SIZE = KEY_SIZE_256; + +const N_ROUNDS = 32; +const SUBKEY_COUNT = N_ROUNDS + 1; + +const SBOXES = [_][16]u8{ + .{ 3, 8, 15, 1, 10, 6, 5, 11, 14, 13, 4, 2, 7, 0, 9, 12 }, // S0 + .{ 15, 12, 2, 7, 9, 0, 5, 10, 1, 11, 14, 8, 6, 13, 3, 4 }, // S1 + .{ 8, 6, 7, 9, 3, 12, 10, 15, 13, 1, 14, 4, 0, 11, 5, 2 }, // S2 + .{ 0, 15, 11, 8, 12, 9, 6, 3, 13, 1, 2, 4, 10, 7, 5, 14 }, // S3 + .{ 1, 15, 8, 3, 12, 0, 11, 6, 2, 5, 4, 10, 9, 14, 7, 13 }, // S4 + .{ 15, 5, 2, 11, 4, 10, 9, 12, 0, 3, 14, 8, 13, 6, 7, 1 }, // S5 + .{ 7, 2, 12, 5, 8, 4, 6, 11, 14, 9, 1, 15, 13, 3, 10, 0 }, // S6 + .{ 1, 13, 15, 0, 14, 8, 2, 11, 7, 4, 12, 10, 9, 3, 5, 6 }, // S7 +}; + +const PHI_FRAC: u32 = 0x9e3779b9; + +// ----------------------------------- KEY EXPANSION ----------------------------------- // + +pub fn expand_key(key: []const u8) ![SUBKEY_COUNT][BLOCK_SIZE / 4]u32 { + // Pad short keys to 256 bits. + const full_key = try pad_user_key(key); + + // Prepare the prekey. + var prekey: [140]u32 = undefined; + @memcpy(prekey[0..8], full_key[0..]); + + for (0..132) |_j| { + const j: u32 = @intCast(_j); + const i: u32 = @intCast(j + 8); + const tmp = prekey[i - 8] ^ prekey[i - 5] ^ prekey[i - 3] ^ prekey[i - 1] ^ PHI_FRAC ^ j; + prekey[i] = (tmp << 11) | (tmp >> 21); + } + + //for (0..SUBKEY_COUNT) |i| { + // std.debug.print("{d}: {x} {x} {x} {x}\n", .{ + // i, + // prekey[8 + i * 4 + 0], + // prekey[8 + i * 4 + 1], + // prekey[8 + i * 4 + 2], + // prekey[8 + i * 4 + 3], + // }); + //} + + // Compute the subkeys. + var subkeys: [SUBKEY_COUNT][BLOCK_SIZE / 4]u32 = undefined; + var tmp: [4]u32 = undefined; + + inline for (0..SUBKEY_COUNT) |i| { + sbox_bitslice( + (32 + 3 - i) % 8, + prekey[8 + i * 4 + 0], + prekey[8 + i * 4 + 1], + prekey[8 + i * 4 + 2], + prekey[8 + i * 4 + 3], + &tmp[0], + &tmp[1], + &tmp[2], + &tmp[3], + ); + subkeys[i] = tmp; + } + + //for (0..SUBKEY_COUNT) |i| { + // std.debug.print("{d}: {x} {x} {x} {x}\n", .{ i, subkeys[i][0], subkeys[i][1], subkeys[i][2], subkeys[i][3] }); + //} + + return subkeys; +} + +pub fn pad_user_key(key: []const u8) ![MAX_KEY_SIZE / 4]u32 { + if (key.len > MAX_KEY_SIZE) + return CryptoError.InvalidKeyLength; + + var long_key: [MAX_KEY_SIZE]u8 = undefined; + @memcpy(long_key[0..key.len], key[0..]); + if (key.len < MAX_KEY_SIZE) { + long_key[key.len] = 0x01; + @memset(long_key[key.len + 1 ..], 0x00); + } + + //std.debug.print("{any}\n", .{long_key}); + + var key_words: [MAX_KEY_SIZE / 4]u32 = undefined; + inline for (0..MAX_KEY_SIZE / 4) |i| + key_words[i] = bytes_to_word(@ptrCast(long_key[i * 4 .. (i + 1) * 4])); + return key_words; +} + +// ----------------------------------- ENCRYPTION ----------------------------------- // + +pub fn encrypt_block( + block_in: *const [BLOCK_SIZE]u8, + block_out: *[BLOCK_SIZE]u8, + key_schedule: *const [SUBKEY_COUNT][BLOCK_SIZE / 4]u32, +) void { + var state = [BLOCK_SIZE / 4]u32{ + bytes_to_word(block_in[0..4]), + bytes_to_word(block_in[4..8]), + bytes_to_word(block_in[8..12]), + bytes_to_word(block_in[12..16]), + }; + + // Apply all rounds but the last. + inline for (0..N_ROUNDS - 1) |i| { + add_round_key(&state, &key_schedule[i]); + sbox_bitslice( + i % 8, + state[0], + state[1], + state[2], + state[3], + &state[0], + &state[1], + &state[2], + &state[3], + ); + linear_transformation(&state); + } + + // Apply the last round. + add_round_key(&state, &key_schedule[N_ROUNDS - 1]); + sbox_bitslice( + (N_ROUNDS - 1) % 8, + state[0], + state[1], + state[2], + state[3], + &state[0], + &state[1], + &state[2], + &state[3], + ); + add_round_key(&state, &key_schedule[N_ROUNDS]); + + block_out.* = word_to_bytes(state[0]) ++ word_to_bytes(state[1]) ++ word_to_bytes(state[2]) ++ word_to_bytes(state[3]); + std.crypto.utils.secureZero(u32, &state); +} + +// ----------------------------------- COMMON OPERATIONS ----------------------------------- // + +pub fn add_round_key(state: *[BLOCK_SIZE / 4]u32, subkey: *const [BLOCK_SIZE / 4]u32) void { + for (0..state.len) |i| + state[i] ^= subkey[i]; +} + +pub fn linear_transformation(state: *[BLOCK_SIZE / 4]u32) void { + var x0 = state[0]; + var x1 = state[1]; + var x2 = state[2]; + var x3 = state[3]; + + x0 = rol(x0, 13); + x2 = rol(x2, 3); + x1 ^= x0 ^ x2; + x3 ^= (x0 << 3) ^ x2; + x1 = rol(x1, 1); + x3 = rol(x3, 7); + x0 ^= x1 ^ x3; + x2 ^= (x1 << 7) ^ x3; + x0 = rol(x0, 5); + x2 = rol(x2, 22); + + state[0] = x0; + state[1] = x1; + state[2] = x2; + state[3] = x3; +} + +pub fn sbox_bitslice( + sbox_id: comptime_int, + x0: u32, + x1: u32, + x2: u32, + x3: u32, + y0: *u32, + y1: *u32, + y2: *u32, + y3: *u32, +) void { + y0.* = 0; + y1.* = 0; + y2.* = 0; + y3.* = 0; + + for (0..32) |i| { + const bitPos: u5 = @intCast(i); + + var z = ((x0 >> bitPos) & 1) << 0 | ((x1 >> bitPos) & 1) << 1 | ((x2 >> bitPos) & 1) << 2 | ((x3 >> bitPos) & 1) << 3; + + z = SBOXES[sbox_id][z]; + + if (((z >> 0) & 1) == 1) + y0.* |= (@as(u32, 1) << bitPos) + else + y0.* &= ~(@as(u32, 1) << bitPos); + + if (((z >> 1) & 1) == 1) + y1.* |= (@as(u32, 1) << bitPos) + else + y1.* &= ~(@as(u32, 1) << bitPos); + + if (((z >> 2) & 1) == 1) + y2.* |= (@as(u32, 1) << bitPos) + else + y2.* &= ~(@as(u32, 1) << bitPos); + + if (((z >> 3) & 1) == 1) + y3.* |= (@as(u32, 1) << bitPos) + else + y3.* &= ~(@as(u32, 1) << bitPos); + } +} + +fn rol(word: u32, by: comptime_int) u32 { + return (word << by) | (word >> (32 - by)); +} diff --git a/test/index.zig b/test/index.zig index fef0c3d..826b6c0 100644 --- a/test/index.zig +++ b/test/index.zig @@ -1,8 +1,9 @@ comptime { _ = .{ @import("./primitive/blockcipher/aes.zig"), - @import("./primitive/blockcipher/des.zig"), - @import("./primitive/blockcipher/operation_modes.zig"), + //@import("./primitive/blockcipher/des.zig"), + @import("./primitive/blockcipher/serpent.zig"), + //@import("./primitive/blockcipher/operation_modes.zig"), @import("./primitive/digest/sha.zig"), @import("./primitive/streamcipher/chacha20.zig"), @import("./primitive/streamcipher/salsa20.zig"), diff --git a/test/primitive/blockcipher/serpent.zig b/test/primitive/blockcipher/serpent.zig new file mode 100644 index 0000000..00079d6 --- /dev/null +++ b/test/primitive/blockcipher/serpent.zig @@ -0,0 +1,112 @@ +const std = @import("std"); +const testing = std.testing; + +const serpent = @import("primitive").blockcipher.serpent; + +// https://biham.cs.technion.ac.il/Reports/Serpent/Serpent-128-128.verified.test-vectors + +test "NESSIE Serpent-128 test vector set 1" { + try test_encryption( + &fromhex(16, "80000000000000000000000000000000"), + &fromhex(16, "00000000000000000000000000000000"), + &fromhex(16, "264e5481eff42a4606abda06c0bfda3d"), + ); + try test_encryption( + &fromhex(16, "40000000000000000000000000000000"), + &fromhex(16, "00000000000000000000000000000000"), + &fromhex(16, "4A231B3BC727993407AC6EC8350E8524"), + ); + try test_encryption( + &fromhex(16, "20000000000000000000000000000000"), + &fromhex(16, "00000000000000000000000000000000"), + &fromhex(16, "E03269F9E9FD853C7D8156DF14B98D56"), + ); + try test_encryption( + &fromhex(16, "10000000000000000000000000000000"), + &fromhex(16, "00000000000000000000000000000000"), + &fromhex(16, "A798181C3081AC59D5BA89754DACC48F"), + ); + // TODO: ... +} + +test "NESSIE Serpent-128 test vector set 2" { + try test_encryption( + &fromhex(16, "00000000000000000000000000000000"), + &fromhex(16, "80000000000000000000000000000000"), + &fromhex(16, "A3B35DE7C358DDD82644678C64B8BCBB"), + ); + try test_encryption( + &fromhex(16, "00000000000000000000000000000000"), + &fromhex(16, "40000000000000000000000000000000"), + &fromhex(16, "04ABCFE4E0AF27FF92A2BB10949D7DD2"), + ); + try test_encryption( + &fromhex(16, "00000000000000000000000000000000"), + &fromhex(16, "20000000000000000000000000000000"), + &fromhex(16, "8F773194B78EF2B2740237EF12D08608"), + ); + try test_encryption( + &fromhex(16, "00000000000000000000000000000000"), + &fromhex(16, "10000000000000000000000000000000"), + &fromhex(16, "8B1EA69EE8D7C8D95B1DE4A670EC6997"), + ); + // TODO: ... +} + +test "NESSIE Serpent-128 test vector set 3" { + try test_encryption( + &fromhex(16, "00000000000000000000000000000000"), + &fromhex(16, "00000000000000000000000000000000"), + &fromhex(16, "3620B17AE6A993D09618B8768266BAE9"), + ); + try test_encryption( + &fromhex(16, "01010101010101010101010101010101"), + &fromhex(16, "01010101010101010101010101010101"), + &fromhex(16, "5107E36DBE81D9996D1EF7F3656FFC63"), + ); + try test_encryption( + &fromhex(16, "02020202020202020202020202020202"), + &fromhex(16, "02020202020202020202020202020202"), + &fromhex(16, "1AE5355487F88F824B6462B45C4C6AA5"), + ); + try test_encryption( + &fromhex(16, "03030303030303030303030303030303"), + &fromhex(16, "03030303030303030303030303030303"), + &fromhex(16, "1F830AF7D2A1B18F7A011C6FD0EEE8FB"), + ); + // TODO: ... +} + +// Helpers + +fn test_encryption(key: *const [16]u8, plain: *const [16]u8, cipher: *const [16]u8) !void { + var output: [16]u8 = undefined; + + const key_schedule = try serpent.expand_key(key); + serpent.encrypt_block(plain, &output, &key_schedule); + + try testing.expectEqualSlices(u8, cipher, &output); +} + +// This monstrosity is only temporary... +fn fromhex(L: comptime_int, comptime s: *const [2 * L]u8) [L]u8 { + var result: [L]u8 = undefined; + inline for (0..L) |i| { + result[i] = (if (s[2 * i] >= '0' and s[2 * i] <= '9') + s[2 * i] - '0' + else if (s[2 * i] >= 'a' and s[2 * i] <= 'f') + s[2 * i] - 'a' + 10 + else if (s[2 * i] >= 'A' and s[2 * i] <= 'F') + s[2 * i] - 'A' + 10 + else + @compileError("Invalid hex string.")) * 16 + (if (s[2 * i + 1] >= '0' and s[2 * i + 1] <= '9') + s[2 * i + 1] - '0' + else if (s[2 * i + 1] >= 'a' and s[2 * i + 1] <= 'f') + s[2 * i + 1] - 'a' + 10 + else if (s[2 * i + 1] >= 'A' and s[2 * i + 1] <= 'F') + s[2 * i + 1] - 'A' + 10 + else + @compileError("Invalid hex string.")); + } + return result; +}