mirror of
https://github.com/ziglang/zig.git
synced 2025-12-06 06:13:07 +00:00
341 lines
13 KiB
Zig
341 lines
13 KiB
Zig
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const crypto = std.crypto;
|
|
const debug = std.debug;
|
|
const mem = std.mem;
|
|
const math = std.math;
|
|
const modes = @import("modes.zig");
|
|
const Polyval = @import("ghash_polyval.zig").Polyval;
|
|
const AuthenticationError = crypto.errors.AuthenticationError;
|
|
|
|
pub const Aes128GcmSiv = AesGcmSiv(crypto.core.aes.Aes128);
|
|
pub const Aes256GcmSiv = AesGcmSiv(crypto.core.aes.Aes256);
|
|
|
|
/// AES-GCM-SIV: Authenticated encryption that remains secure even if you accidentally reuse a nonce.
|
|
///
|
|
/// What it does: Encrypts data and protects it from tampering. You can also attach
|
|
/// unencrypted metadata (like headers) that will be authenticated but not encrypted.
|
|
///
|
|
/// When to use AES-GCM-SIV:
|
|
/// - When you can't guarantee unique nonces (though you should still try to use unique nonces)
|
|
///
|
|
/// When to use regular AES-GCM instead:
|
|
/// - When you can guarantee unique nonces (e.g., using a counter)
|
|
/// - When you need slightly better performance
|
|
///
|
|
/// Security: If you accidentally reuse a nonce with the same key, AES-GCM-SIV only
|
|
/// reveals whether two messages are identical. Regular AES-GCM would be catastrophically
|
|
/// broken in this scenario, potentially revealing the authentication key.
|
|
///
|
|
/// Performance: Slightly slower than AES-GCM due to the additional key derivation step.
|
|
///
|
|
/// Defined in RFC 8452.
|
|
fn AesGcmSiv(comptime Aes: anytype) type {
|
|
debug.assert(Aes.block.block_length == 16);
|
|
|
|
return struct {
|
|
pub const tag_length = 16;
|
|
pub const nonce_length = 12;
|
|
pub const key_length = Aes.key_bits / 8;
|
|
|
|
const zeros: [16]u8 = @splat(0);
|
|
|
|
/// Derives the authentication and message encryption keys from the master key and nonce.
|
|
/// This implements the key derivation as specified in RFC 8452 Section 4.
|
|
/// Generates a 128-bit authentication key for POLYVAL and a message encryption key
|
|
/// (128 or 256 bits depending on the AES variant).
|
|
fn deriveKeys(message_key: *[key_length]u8, auth_key: *[16]u8, key: [key_length]u8, nonce: [nonce_length]u8) void {
|
|
const aes = Aes.initEnc(key);
|
|
|
|
// Derive authentication and message keys per RFC 8452 Section 4
|
|
// Each encryption produces 16 bytes, but we only use first 8 bytes of each block
|
|
|
|
if (key_length == 16) {
|
|
// AES-128-GCM-SIV: Process 4 blocks in parallel
|
|
var key_blocks: [4 * 16]u8 = undefined;
|
|
var cipher_outs: [4 * 16]u8 = undefined;
|
|
|
|
// Set up all 4 blocks with counters 0-3 and nonce
|
|
inline for (0..4) |i| {
|
|
mem.writeInt(u32, key_blocks[i * 16 ..][0..4], @intCast(i), .little);
|
|
key_blocks[i * 16 + 4 .. i * 16 + 16].* = nonce;
|
|
}
|
|
|
|
// Encrypt all 4 blocks in parallel
|
|
aes.encryptWide(4, &cipher_outs, &key_blocks);
|
|
|
|
// Extract the key material (first 8 bytes of each block)
|
|
@memcpy(auth_key[0..8], cipher_outs[0..8]);
|
|
@memcpy(auth_key[8..16], cipher_outs[16..24]);
|
|
@memcpy(message_key[0..8], cipher_outs[32..40]);
|
|
@memcpy(message_key[8..16], cipher_outs[48..56]);
|
|
} else {
|
|
// AES-256-GCM-SIV: Process 6 blocks in parallel
|
|
var key_blocks: [6 * 16]u8 = undefined;
|
|
var cipher_outs: [6 * 16]u8 = undefined;
|
|
|
|
// Set up all 6 blocks with counters 0-5 and nonce
|
|
inline for (0..6) |i| {
|
|
mem.writeInt(u32, key_blocks[i * 16 ..][0..4], @intCast(i), .little);
|
|
key_blocks[i * 16 + 4 .. i * 16 + 16].* = nonce;
|
|
}
|
|
|
|
// Encrypt all 6 blocks in parallel
|
|
aes.encryptWide(6, &cipher_outs, &key_blocks);
|
|
|
|
// Extract the key material (first 8 bytes of each block)
|
|
@memcpy(auth_key[0..8], cipher_outs[0..8]);
|
|
@memcpy(auth_key[8..16], cipher_outs[16..24]);
|
|
@memcpy(message_key[0..8], cipher_outs[32..40]);
|
|
@memcpy(message_key[8..16], cipher_outs[48..56]);
|
|
@memcpy(message_key[16..24], cipher_outs[64..72]);
|
|
@memcpy(message_key[24..32], cipher_outs[80..88]);
|
|
}
|
|
}
|
|
|
|
/// Encrypts and authenticates a message using AES-GCM-SIV.
|
|
///
|
|
/// `c`: The ciphertext buffer to write the encrypted data to.
|
|
/// `tag`: The authentication tag buffer to write the computed tag to.
|
|
/// `m`: The plaintext message to encrypt.
|
|
/// `ad`: The associated data to authenticate.
|
|
/// `npub`: The nonce to use for encryption.
|
|
/// `key`: The 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);
|
|
debug.assert(m.len <= (1 << 36));
|
|
debug.assert(ad.len <= (1 << 36));
|
|
|
|
var auth_key: [16]u8 = undefined;
|
|
var message_key: [key_length]u8 = undefined;
|
|
deriveKeys(&message_key, &auth_key, key, npub);
|
|
|
|
// Calculate POLYVAL over additional data and plaintext
|
|
const block_count = (math.divCeil(usize, ad.len, Polyval.block_length) catch unreachable) +
|
|
(math.divCeil(usize, m.len, Polyval.block_length) catch unreachable) + 1;
|
|
var mac = Polyval.initForBlockCount(&auth_key, block_count);
|
|
|
|
// Process additional data
|
|
mac.update(ad);
|
|
mac.pad();
|
|
|
|
// Process plaintext
|
|
mac.update(m);
|
|
mac.pad();
|
|
|
|
// Length block
|
|
var length_block: [16]u8 = undefined;
|
|
mem.writeInt(u64, length_block[0..8], @as(u64, ad.len) * 8, .little);
|
|
mem.writeInt(u64, length_block[8..16], @as(u64, m.len) * 8, .little);
|
|
mac.update(&length_block);
|
|
|
|
// Get POLYVAL result
|
|
var s: [16]u8 = undefined;
|
|
mac.final(&s);
|
|
|
|
// XOR with nonce to get pre-tag
|
|
for (npub, 0..) |b, i| {
|
|
s[i] ^= b;
|
|
}
|
|
|
|
// Clear most significant bit of last byte
|
|
s[15] &= 0x7f;
|
|
|
|
// Encrypt to get tag
|
|
const tag_aes = Aes.initEnc(message_key);
|
|
tag_aes.encrypt(tag, &s);
|
|
|
|
// Use tag as initial counter for CTR mode
|
|
var counter: [16]u8 = tag.*;
|
|
counter[15] |= 0x80; // Set most significant bit
|
|
|
|
// Encrypt message using CTR mode with 32-bit little-endian counter
|
|
const aes_ctx = Aes.initEnc(message_key);
|
|
modes.ctrSlice(@TypeOf(aes_ctx), aes_ctx, c, m, counter, .little, 0, 4);
|
|
}
|
|
|
|
/// Decrypts and authenticates a message using AES-GCM-SIV.
|
|
///
|
|
/// `m`: Message buffer to write the decrypted data to.
|
|
/// `c`: The ciphertext to decrypt.
|
|
/// `tag`: The authentication tag.
|
|
/// `ad`: The associated data.
|
|
/// `npub`: The nonce.
|
|
/// `key`: The decryption key.
|
|
/// Asserts `c.len == m.len`.
|
|
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(c.len == m.len);
|
|
assert(c.len <= (1 << 36));
|
|
assert(ad.len <= (1 << 36));
|
|
|
|
var auth_key: [16]u8 = undefined;
|
|
var message_key: [key_length]u8 = undefined;
|
|
deriveKeys(&message_key, &auth_key, key, npub);
|
|
|
|
// Decrypt message using CTR mode with 32-bit little-endian counter
|
|
var counter: [16]u8 = tag;
|
|
counter[15] |= 0x80; // Set most significant bit
|
|
|
|
const aes_ctx = Aes.initEnc(message_key);
|
|
modes.ctrSlice(@TypeOf(aes_ctx), aes_ctx, m, c, counter, .little, 0, 4);
|
|
|
|
// Verify tag by recalculating POLYVAL
|
|
const block_count = (math.divCeil(usize, ad.len, Polyval.block_length) catch unreachable) +
|
|
(math.divCeil(usize, m.len, Polyval.block_length) catch unreachable) + 1;
|
|
var mac = Polyval.initForBlockCount(&auth_key, block_count);
|
|
|
|
// Process additional data
|
|
mac.update(ad);
|
|
mac.pad();
|
|
|
|
// Process decrypted plaintext
|
|
mac.update(m);
|
|
mac.pad();
|
|
|
|
// Length block
|
|
var length_block: [16]u8 = undefined;
|
|
mem.writeInt(u64, length_block[0..8], @as(u64, ad.len) * 8, .little);
|
|
mem.writeInt(u64, length_block[8..16], @as(u64, m.len) * 8, .little);
|
|
mac.update(&length_block);
|
|
|
|
// Get POLYVAL result
|
|
var s: [16]u8 = undefined;
|
|
mac.final(&s);
|
|
|
|
// XOR with nonce to get pre-tag
|
|
for (npub, 0..) |b, i| {
|
|
s[i] ^= b;
|
|
}
|
|
|
|
// Clear most significant bit of last byte
|
|
s[15] &= 0x7f;
|
|
|
|
// Encrypt to get expected tag
|
|
const tag_aes = Aes.initEnc(message_key);
|
|
var computed_tag: [tag_length]u8 = undefined;
|
|
tag_aes.encrypt(&computed_tag, &s);
|
|
|
|
// Verify tag
|
|
const verify = crypto.timing_safe.eql([tag_length]u8, computed_tag, tag);
|
|
if (!verify) {
|
|
crypto.secureZero(u8, &computed_tag);
|
|
@memset(m, undefined);
|
|
return error.AuthenticationFailed;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
const htest = @import("test.zig");
|
|
const testing = std.testing;
|
|
|
|
test "Aes128GcmSiv - RFC 8452 Test Vector 1" {
|
|
// Test vector from RFC 8452 Appendix C.1
|
|
const key = [_]u8{
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
const nonce = [_]u8{
|
|
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
const ad = "";
|
|
const m = "";
|
|
var c: [m.len]u8 = undefined;
|
|
var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
|
|
|
|
Aes128GcmSiv.encrypt(&c, &tag, m, ad, nonce, key);
|
|
try htest.assertEqual("dc20e2d83f25705bb49e439eca56de25", &tag);
|
|
}
|
|
|
|
test "Aes128GcmSiv - RFC 8452 Test Vector 2" {
|
|
// Test vector from RFC 8452 Appendix C.1
|
|
const key = [_]u8{
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
const nonce = [_]u8{
|
|
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
const plaintext = [_]u8{
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
const ad = "";
|
|
var c: [plaintext.len]u8 = undefined;
|
|
var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
|
|
|
|
Aes128GcmSiv.encrypt(&c, &tag, &plaintext, ad, nonce, key);
|
|
try htest.assertEqual("b5d839330ac7b786", &c);
|
|
try htest.assertEqual("578782fff6013b815b287c22493a364c", &tag);
|
|
|
|
var m2: [plaintext.len]u8 = undefined;
|
|
try Aes128GcmSiv.decrypt(&m2, &c, tag, ad, nonce, key);
|
|
try testing.expectEqualSlices(u8, &plaintext, &m2);
|
|
}
|
|
|
|
test "Aes128GcmSiv - RFC 8452 Test Vector 3" {
|
|
// Test vector from RFC 8452 Appendix C.1
|
|
const key = [_]u8{
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
const nonce = [_]u8{
|
|
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
const plaintext = [_]u8{
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
const ad = "";
|
|
var c: [plaintext.len]u8 = undefined;
|
|
var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
|
|
|
|
Aes128GcmSiv.encrypt(&c, &tag, &plaintext, ad, nonce, key);
|
|
try htest.assertEqual("7323ea61d05932260047d942", &c);
|
|
try htest.assertEqual("a4978db357391a0bc4fdec8b0d106639", &tag);
|
|
|
|
var m2: [plaintext.len]u8 = undefined;
|
|
try Aes128GcmSiv.decrypt(&m2, &c, tag, ad, nonce, key);
|
|
try testing.expectEqualSlices(u8, &plaintext, &m2);
|
|
}
|
|
|
|
test "Aes256GcmSiv - RFC 8452 Test Vector" {
|
|
// Test vector from RFC 8452 Appendix C.2
|
|
const key = [_]u8{
|
|
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
const nonce = [_]u8{
|
|
0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
|
0x00, 0x00, 0x00, 0x00,
|
|
};
|
|
const ad = "";
|
|
const m = "";
|
|
var c: [m.len]u8 = undefined;
|
|
var tag: [Aes256GcmSiv.tag_length]u8 = undefined;
|
|
|
|
Aes256GcmSiv.encrypt(&c, &tag, m, ad, nonce, key);
|
|
try htest.assertEqual("07f5f4169bbf55a8400cd47ea6fd400f", &tag);
|
|
}
|
|
|
|
test "Aes128GcmSiv - Decrypt with wrong tag" {
|
|
const key: [Aes128GcmSiv.key_length]u8 = @splat(0x69);
|
|
const nonce: [Aes128GcmSiv.nonce_length]u8 = @splat(0x42);
|
|
const m = "Test message";
|
|
const ad = "";
|
|
var c: [m.len]u8 = undefined;
|
|
var tag: [Aes128GcmSiv.tag_length]u8 = undefined;
|
|
|
|
Aes128GcmSiv.encrypt(&c, &tag, m, ad, nonce, key);
|
|
|
|
// Corrupt the tag
|
|
tag[0] ^= 0x01;
|
|
|
|
var m2: [m.len]u8 = undefined;
|
|
try testing.expectError(error.AuthenticationFailed, Aes128GcmSiv.decrypt(&m2, &c, tag, ad, nonce, key));
|
|
}
|