AES-SIV: Deterministic authenticated encryption - the same message always produces the same ciphertext.
What it does: Encrypts data and protects it from tampering. Unlike most encryption modes, AES-SIV is deterministic: encrypting the same message with the same key always produces the same ciphertext (unless you provide an optional nonce).
When to use AES-SIV:
When NOT to use AES-SIV:
Unique features:
Security properties:
AES-SIV has better security properties than AES-GCM-SIV, but is must slower.
How it works: Combines two keys - one for authentication (S2V) and one for encryption (CTR mode). The total key size is double the AES key size (256 bits for AES-128-SIV, 512 bits for AES-256-SIV).
Defined in RFC 5297.
fn AesSiv(comptime Aes: anytype) type
fn AesSiv(comptime Aes: anytype) type {
debug.assert(Aes.block.block_length == 16);
return struct {
pub const tag_length = 16;
pub const key_length = Aes.key_bits / 8 * 2; // SIV uses 2x key size
const CmacImpl = Cmac(Aes);
/// S2V (String to Vector) - RFC 5297 Section 2.4
/// Derives a synthetic IV from the key and input strings using CMAC.
/// This function implements a cryptographic pseudo-random function that maps
/// a variable-length vector of strings to a fixed 128-bit output.
fn s2v(iv: *[16]u8, key: [Aes.key_bits / 8]u8, strings: []const []const u8) void {
assert(strings.len > 0);
assert(strings.len <= 127); // S2V limitation
var d: [16]u8 = undefined;
// Special case: single empty string
if (strings.len == 1 and strings[0].len == 0) {
CmacImpl.create(&d, &[_]u8{}, &key);
iv.* = d;
return;
}
// Initialize with CMAC of zero block
const zero_block: [16]u8 = @splat(0);
CmacImpl.create(&d, &zero_block, &key);
// Process all strings except the last one
var i: usize = 0;
while (i < strings.len - 1) : (i += 1) {
d = dbl(d);
var tmp: [16]u8 = undefined;
CmacImpl.create(&tmp, strings[i], &key);
for (&d, tmp) |*b, t| {
b.* ^= t;
}
}
// Process the final string
const sn = strings[strings.len - 1];
if (sn.len >= 16) {
// XOR d with the last 16 bytes of Sn,
// and give the entire Sn to CMAC incrementally.
var cmac = CmacImpl.init(&key);
const prefix = sn.len - 16;
cmac.update(sn[0..prefix]);
var tail: [16]u8 = undefined;
for (&tail, sn[prefix..][0..16], d) |*out, s, db| {
out.* = s ^ db;
}
cmac.update(&tail);
cmac.final(iv);
} else {
// Pad and XOR
d = dbl(d);
var padded: [16]u8 = @splat(0);
@memcpy(padded[0..sn.len], sn);
padded[sn.len] = 0x80;
for (&d, padded) |*b, p| {
b.* ^= p;
}
CmacImpl.create(iv, &d, &key);
}
}
/// Double operation as defined in RFC 5297.
/// Performs multiplication by x (i.e., left shift by 1) in GF(2^128).
/// This is the same operation used in CMAC subkey generation.
/// If the MSB is set, XORs with the polynomial 0x87 after shifting.
fn dbl(d: [16]u8) [16]u8 {
// Read as big-endian 128-bit integer
const val = mem.readInt(u128, &d, .big);
// Left shift by 1, and XOR with 0x87 if MSB was set
const doubled = (val << 1) ^ (0x87 & -%(@as(u128, val >> 127)));
// Write back as big-endian
var result: [16]u8 = undefined;
mem.writeInt(u128, &result, doubled, .big);
return result;
}
/// Encrypt plaintext using AES-SIV
/// `c`: Output buffer for ciphertext (same size as plaintext)
/// `tag`: Output buffer for authentication tag (synthetic IV)
/// `m`: Plaintext to encrypt
/// `ad`: Optional associated data
/// `nonce`: Optional nonce (if provided, will be added as last AD component)
/// `key`: Combined key (2x AES key size)
pub fn encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: ?[]const u8, nonce: ?[]const u8, key: [key_length]u8) void {
debug.assert(c.len == m.len);
// Split key into K1 (for S2V) and K2 (for CTR)
const k1 = key[0 .. Aes.key_bits / 8];
const k2 = key[Aes.key_bits / 8 ..];
// Prepare strings for S2V: AD components followed by plaintext
var strings_buf: [128][]const u8 = undefined;
var strings_len: usize = 0;
if (ad) |a| {
strings_buf[strings_len] = a;
strings_len += 1;
}
if (nonce) |n| {
strings_buf[strings_len] = n;
strings_len += 1;
}
strings_buf[strings_len] = m;
strings_len += 1;
// Compute synthetic IV using S2V
s2v(tag, k1.*, strings_buf[0..strings_len]);
// Clear the 31st and 63rd bits for use as CTR IV
var ctr_iv = tag.*;
ctr_iv[8] &= 0x7f;
ctr_iv[12] &= 0x7f;
// Encrypt plaintext using CTR mode
const aes_ctx = Aes.initEnc(k2.*);
modes.ctr(@TypeOf(aes_ctx), aes_ctx, c, m, ctr_iv, .big);
}
/// Decrypt ciphertext using AES-SIV
/// `m`: Output buffer for decrypted plaintext
/// `c`: Ciphertext to decrypt
/// `tag`: Authentication tag (synthetic IV)
/// `ad`: Optional associated data (must match encryption)
/// `nonce`: Optional nonce (must match encryption)
/// `key`: Combined key (2x AES key size)
pub fn decrypt(m: []u8, c: []const u8, tag: [tag_length]u8, ad: ?[]const u8, nonce: ?[]const u8, key: [key_length]u8) AuthenticationError!void {
assert(c.len == m.len);
// Split key into K1 (for S2V) and K2 (for CTR)
const k1 = key[0 .. Aes.key_bits / 8];
const k2 = key[Aes.key_bits / 8 ..];
// Clear the 31st and 63rd bits for use as CTR IV
var ctr_iv = tag;
ctr_iv[8] &= 0x7f;
ctr_iv[12] &= 0x7f;
// Decrypt ciphertext using CTR mode
const aes_ctx = Aes.initEnc(k2.*);
modes.ctr(@TypeOf(aes_ctx), aes_ctx, m, c, ctr_iv, .big);
// Prepare strings for S2V: AD components followed by plaintext
var strings_buf: [128][]const u8 = undefined;
var strings_len: usize = 0;
if (ad) |a| {
strings_buf[strings_len] = a;
strings_len += 1;
}
if (nonce) |n| {
strings_buf[strings_len] = n;
strings_len += 1;
}
strings_buf[strings_len] = m;
strings_len += 1;
// Verify synthetic IV using S2V
var computed_tag: [tag_length]u8 = undefined;
s2v(&computed_tag, k1.*, strings_buf[0..strings_len]);
// 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;
}
}
/// Encrypts plaintext with multiple associated data components.
/// This is the most general form of AES-SIV encryption that accepts
/// a vector of up to 126 associated data strings as specified in RFC 5297.
pub fn encryptWithAdVector(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const []const u8, key: [key_length]u8) void {
debug.assert(c.len == m.len);
debug.assert(ad.len <= 126); // AES-SIV supports at most 126 associated data components
// Split key into K1 (for S2V) and K2 (for CTR)
const k1 = key[0 .. Aes.key_bits / 8];
const k2 = key[Aes.key_bits / 8 ..];
// Prepare strings for S2V: AD components followed by plaintext
var strings_buf: [128][]const u8 = undefined;
var strings_len: usize = 0;
for (ad) |a| {
strings_buf[strings_len] = a;
strings_len += 1;
}
strings_buf[strings_len] = m;
strings_len += 1;
// Compute synthetic IV using S2V
s2v(tag, k1.*, strings_buf[0..strings_len]);
// Clear the 31st and 63rd bits for use as CTR IV
var ctr_iv = tag.*;
ctr_iv[8] &= 0x7f;
ctr_iv[12] &= 0x7f;
// Encrypt plaintext using CTR mode
const aes_ctx = Aes.initEnc(k2.*);
modes.ctr(@TypeOf(aes_ctx), aes_ctx, c, m, ctr_iv, .big);
}
/// Decrypts ciphertext with multiple associated data components.
/// This is the most general form of AES-SIV decryption that accepts
/// a vector of up to 126 associated data strings as specified in RFC 5297.
pub fn decryptWithAdVector(m: []u8, c: []const u8, tag: [tag_length]u8, ad: []const []const u8, key: [key_length]u8) AuthenticationError!void {
assert(c.len == m.len);
assert(ad.len <= 126); // AES-SIV supports at most 126 associated data components
// Split key into K1 (for S2V) and K2 (for CTR)
const k1 = key[0 .. Aes.key_bits / 8];
const k2 = key[Aes.key_bits / 8 ..];
// Clear the 31st and 63rd bits for use as CTR IV
var ctr_iv = tag;
ctr_iv[8] &= 0x7f;
ctr_iv[12] &= 0x7f;
// Decrypt ciphertext using CTR mode
const aes_ctx = Aes.initEnc(k2.*);
modes.ctr(@TypeOf(aes_ctx), aes_ctx, m, c, ctr_iv, .big);
// Prepare strings for S2V: AD components followed by plaintext
var strings_buf: [128][]const u8 = undefined;
var strings_len: usize = 0;
for (ad) |a| {
strings_buf[strings_len] = a;
strings_len += 1;
}
strings_buf[strings_len] = m;
strings_len += 1;
// Verify synthetic IV using S2V
var computed_tag: [tag_length]u8 = undefined;
s2v(&computed_tag, k1.*, strings_buf[0..strings_len]);
// 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;
}
}
};
}