zig/lib/std/crypto/ascon.zig

1312 lines
47 KiB
Zig
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! Ascon is a 320-bit permutation, selected as new standard for lightweight cryptography
//! in the NIST Lightweight Cryptography competition (20192023).
//! https://csrc.nist.gov/pubs/sp/800/232/ipd
//!
//! The permutation is compact, and optimized for timing and side channel resistance,
//! making it a good choice for embedded applications.
//!
//! It is not meant to be used directly, but as a building block for symmetric cryptography.
const std = @import("std");
const builtin = @import("builtin");
const crypto = std.crypto;
const debug = std.debug;
const mem = std.mem;
const testing = std.testing;
const rotr = std.math.rotr;
const native_endian = builtin.cpu.arch.endian();
/// An Ascon state.
///
/// The state is represented as 5 64-bit words.
///
/// The original NIST submission (v1.2) serializes these words as big-endian,
/// but NIST SP 800-232 switched to a little-endian representation.
/// Software implementations are free to use native endianness with no security degradation.
pub fn State(comptime endian: std.builtin.Endian) type {
return struct {
const Self = @This();
/// Number of bytes in the state.
pub const block_bytes = 40;
const Block = [5]u64;
st: Block,
/// Initialize the state from a slice of bytes.
///
/// Parameters:
/// - initial_state: A 40-byte array to initialize the state
///
/// Returns: A new State initialized with the provided bytes
pub fn init(initial_state: [block_bytes]u8) Self {
var state = Self{ .st = undefined };
@memcpy(state.asBytes(), &initial_state);
state.endianSwap();
return state;
}
/// Initialize the state from u64 words in native endianness.
///
/// Parameters:
/// - initial_state: An array of 5 u64 words in native endianness
///
/// Returns: A new State with the provided words
pub fn initFromWords(initial_state: [5]u64) Self {
return .{ .st = initial_state };
}
/// Initialize the state for Ascon XOF.
///
/// Returns: A new State initialized with the Ascon XOF initialization vector
pub fn initXof() Self {
return Self{ .st = Block{
0xb57e273b814cd416,
0x2b51042562ae2420,
0x66a3a7768ddf2218,
0x5aad0a7a8153650c,
0x4f3e0e32539493b6,
} };
}
/// Initialize the state for Ascon XOFa.
///
/// Returns: A new State initialized with the Ascon XOFa initialization vector
pub fn initXofA() Self {
return Self{ .st = Block{
0x44906568b77b9832,
0xcd8d6cae53455532,
0xf7b5212756422129,
0x246885e1de0d225b,
0xa8cb5ce33449973f,
} };
}
/// A representation of the state as bytes. The byte order is architecture-dependent.
///
/// Returns: A pointer to the state's internal byte representation
pub fn asBytes(self: *Self) *[block_bytes]u8 {
return mem.asBytes(&self.st);
}
/// Byte-swap the entire state if the architecture doesn't match the required endianness.
///
/// This ensures the state is in the correct endianness for the current platform.
pub fn endianSwap(self: *Self) void {
for (&self.st) |*w| {
w.* = mem.toNative(u64, w.*, endian);
}
}
/// Set bytes starting at the beginning of the state.
///
/// Parameters:
/// - bytes: Slice of bytes to write into the state (up to 40 bytes)
///
/// Note: If bytes.len < 40, remaining state words are zero-padded
pub fn setBytes(self: *Self, bytes: []const u8) void {
var i: usize = 0;
while (i + 8 <= bytes.len) : (i += 8) {
self.st[i / 8] = mem.readInt(u64, bytes[i..][0..8], endian);
}
if (i < bytes.len) {
var padded: [8]u8 = @splat(0);
@memcpy(padded[0 .. bytes.len - i], bytes[i..]);
self.st[i / 8] = mem.readInt(u64, padded[0..], endian);
}
}
/// XOR a byte into the state at a given offset.
///
/// Parameters:
/// - byte: The byte to XOR into the state
/// - offset: The byte offset in the state (0-39)
pub fn addByte(self: *Self, byte: u8, offset: usize) void {
const z = switch (endian) {
.big => 64 - 8 - 8 * @as(u6, @truncate(offset % 8)),
.little => 8 * @as(u6, @truncate(offset % 8)),
};
self.st[offset / 8] ^= @as(u64, byte) << z;
}
/// XOR bytes into the beginning of the state.
///
/// Parameters:
/// - bytes: Slice of bytes to XOR into the state (up to 40 bytes)
///
/// Note: Handles partial blocks with zero-padding
pub fn addBytes(self: *Self, bytes: []const u8) void {
var i: usize = 0;
while (i + 8 <= bytes.len) : (i += 8) {
self.st[i / 8] ^= mem.readInt(u64, bytes[i..][0..8], endian);
}
if (i < bytes.len) {
var padded: [8]u8 = @splat(0);
@memcpy(padded[0 .. bytes.len - i], bytes[i..]);
self.st[i / 8] ^= mem.readInt(u64, padded[0..], endian);
}
}
/// Extract the first bytes of the state.
///
/// Parameters:
/// - out: Output buffer to receive the extracted bytes
///
/// Note: Extracts up to out.len bytes from the beginning of the state
pub fn extractBytes(self: *Self, out: []u8) void {
var i: usize = 0;
while (i + 8 <= out.len) : (i += 8) {
mem.writeInt(u64, out[i..][0..8], self.st[i / 8], endian);
}
if (i < out.len) {
var padded: [8]u8 = @splat(0);
mem.writeInt(u64, padded[0..], self.st[i / 8], endian);
@memcpy(out[i..], padded[0 .. out.len - i]);
}
}
/// XOR the first bytes of the state into a slice of bytes.
///
/// Parameters:
/// - out: Output buffer for the XORed result
/// - in: Input bytes to XOR with the state
///
/// Requires: out.len == in.len
pub fn xorBytes(self: *Self, out: []u8, in: []const u8) void {
debug.assert(out.len == in.len);
var i: usize = 0;
while (i + 8 <= in.len) : (i += 8) {
const x = mem.readInt(u64, in[i..][0..8], native_endian) ^ mem.nativeTo(u64, self.st[i / 8], endian);
mem.writeInt(u64, out[i..][0..8], x, native_endian);
}
if (i < in.len) {
var padded: [8]u8 = @splat(0);
@memcpy(padded[0 .. in.len - i], in[i..]);
const x = mem.readInt(u64, &padded, native_endian) ^ mem.nativeTo(u64, self.st[i / 8], endian);
mem.writeInt(u64, &padded, x, native_endian);
@memcpy(out[i..], padded[0 .. in.len - i]);
}
}
/// Set the words storing the bytes of a given range to zero.
///
/// Parameters:
/// - from: Starting byte offset (inclusive)
/// - to: Ending byte offset (inclusive)
///
/// Note: Clears complete words that contain the specified byte range
pub fn clear(self: *Self, from: usize, to: usize) void {
@memset(self.st[from / 8 .. (to + 7) / 8], 0);
}
/// Clear the entire state, disabling compiler optimizations.
///
/// Uses secure zeroing to prevent the compiler from optimizing away
/// the clearing operation. Use for sensitive data cleanup.
pub fn secureZero(self: *Self) void {
crypto.secureZero(u64, &self.st);
}
/// Apply a reduced-round permutation to the state.
///
/// Parameters:
/// - rounds: Number of rounds to apply (1-12)
///
/// Note: Uses the last `rounds` round constants from the full set
pub fn permuteR(state: *Self, comptime rounds: u4) void {
const rks = [16]u64{ 0x3c, 0x2d, 0x1e, 0x0f, 0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87, 0x78, 0x69, 0x5a, 0x4b };
inline for (rks[rks.len - rounds ..]) |rk| {
state.round(rk);
}
}
/// Apply a full-round permutation to the state.
///
/// Applies the standard 12-round Ascon permutation.
pub fn permute(state: *Self) void {
state.permuteR(12);
}
/// Apply a permutation to the state and prevent backtracking.
///
/// Parameters:
/// - rounds: Number of permutation rounds to apply
/// - rate: Rate in bytes (must be multiple of 8, < 40)
///
/// The capacity portion is XORed before and after permutation to
/// provide forward security (ratcheting).
pub fn permuteRatchet(state: *Self, comptime rounds: u4, comptime rate: u6) void {
const capacity = block_bytes - rate;
debug.assert(capacity > 0 and capacity % 8 == 0); // capacity must be a multiple of 64 bits
var mask: [capacity / 8]u64 = undefined;
inline for (&mask, state.st[state.st.len - mask.len ..]) |*m, x| m.* = x;
state.permuteR(rounds);
inline for (mask, state.st[state.st.len - mask.len ..]) |m, *x| x.* ^= m;
}
/// Core Ascon permutation round function.
///
/// Parameters:
/// - rk: Round constant for this round
///
/// Implements one round of the Ascon permutation with S-box and linear layer.
fn round(state: *Self, rk: u64) void {
const x = &state.st;
x[2] ^= rk;
x[0] ^= x[4];
x[4] ^= x[3];
x[2] ^= x[1];
var t: Block = .{
x[0] ^ (~x[1] & x[2]),
x[1] ^ (~x[2] & x[3]),
x[2] ^ (~x[3] & x[4]),
x[3] ^ (~x[4] & x[0]),
x[4] ^ (~x[0] & x[1]),
};
t[1] ^= t[0];
t[3] ^= t[2];
t[0] ^= t[4];
x[2] = t[2] ^ rotr(u64, t[2], 6 - 1);
x[3] = t[3] ^ rotr(u64, t[3], 17 - 10);
x[4] = t[4] ^ rotr(u64, t[4], 41 - 7);
x[0] = t[0] ^ rotr(u64, t[0], 28 - 19);
x[1] = t[1] ^ rotr(u64, t[1], 61 - 39);
x[2] = t[2] ^ rotr(u64, x[2], 1);
x[3] = t[3] ^ rotr(u64, x[3], 10);
x[4] = t[4] ^ rotr(u64, x[4], 7);
x[0] = t[0] ^ rotr(u64, x[0], 19);
x[1] = t[1] ^ rotr(u64, x[1], 39);
x[2] = ~x[2];
}
};
}
test "ascon" {
const Ascon = State(.big);
var bytes: [Ascon.block_bytes]u8 = undefined;
@memset(&bytes, 1);
var st = Ascon.init(bytes);
var out: [Ascon.block_bytes]u8 = undefined;
st.permute();
st.extractBytes(&out);
const expected1 = [_]u8{ 148, 147, 49, 226, 218, 221, 208, 113, 186, 94, 96, 10, 183, 219, 119, 150, 169, 206, 65, 18, 215, 97, 78, 106, 118, 81, 211, 150, 52, 17, 117, 64, 216, 45, 148, 240, 65, 181, 90, 180 };
try testing.expectEqualSlices(u8, &expected1, &out);
st.clear(0, 10);
st.extractBytes(&out);
const expected2 = [_]u8{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 169, 206, 65, 18, 215, 97, 78, 106, 118, 81, 211, 150, 52, 17, 117, 64, 216, 45, 148, 240, 65, 181, 90, 180 };
try testing.expectEqualSlices(u8, &expected2, &out);
st.addByte(1, 5);
st.addByte(2, 5);
st.extractBytes(&out);
const expected3 = [_]u8{ 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 169, 206, 65, 18, 215, 97, 78, 106, 118, 81, 211, 150, 52, 17, 117, 64, 216, 45, 148, 240, 65, 181, 90, 180 };
try testing.expectEqualSlices(u8, &expected3, &out);
st.addBytes(&bytes);
st.extractBytes(&out);
const expected4 = [_]u8{ 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 168, 207, 64, 19, 214, 96, 79, 107, 119, 80, 210, 151, 53, 16, 116, 65, 217, 44, 149, 241, 64, 180, 91, 181 };
try testing.expectEqualSlices(u8, &expected4, &out);
}
const AsconState = State(.little);
const AuthenticationError = crypto.errors.AuthenticationError;
/// Ascon-AEAD128 as specified in NIST SP 800-232 Section 4
pub const AsconAead128 = struct {
pub const tag_length = 16;
pub const nonce_length = 16;
pub const key_length = 16;
pub const block_length = 16;
const AeadState = struct {
st: AsconState,
k0: u64,
k1: u64,
/// Initialize AEAD state with key and nonce.
///
/// Parameters:
/// - key: 16-byte secret key
/// - nonce: 16-byte nonce
///
/// Returns: Initialized AEAD state ready for processing
fn init(key: [16]u8, nonce: [16]u8) AeadState {
const k0 = mem.readInt(u64, key[0..8], .little);
const k1 = mem.readInt(u64, key[8..16], .little);
const n0 = mem.readInt(u64, nonce[0..8], .little);
const n1 = mem.readInt(u64, nonce[8..16], .little);
// IV for Ascon-AEAD128 (Ascon-128a)
const iv: u64 = 0x00001000808C0001;
const words: [5]u64 = .{ iv, k0, k1, n0, n1 };
var st = AsconState.initFromWords(words);
st.permuteR(12);
st.st[3] ^= k0;
st.st[4] ^= k1;
return AeadState{ .st = st, .k0 = k0, .k1 = k1 };
}
/// Process associated data for authentication.
///
/// Parameters:
/// - ad: Associated data to authenticate
///
/// Updates the state to include AD in authentication tag computation.
fn processAd(self: *AeadState, ad: []const u8) void {
if (ad.len == 0) return;
var i: usize = 0;
// Process full 128-bit blocks
while (i + 16 <= ad.len) : (i += 16) {
self.st.addBytes(ad[i..][0..16]);
self.st.permuteR(8);
}
// Process final partial AD block
const adrem = ad.len - i;
if (adrem > 0) {
if (adrem >= 8) {
var buf: [8]u8 = @splat(0);
@memcpy(buf[0..8], ad[i..][0..8]);
self.st.st[0] ^= mem.readInt(u64, &buf, .little);
buf = @splat(0);
@memcpy(buf[0 .. adrem - 8], ad[i + 8 ..]);
buf[adrem - 8] = 0x01;
self.st.st[1] ^= mem.readInt(u64, &buf, .little);
} else {
var buf: [8]u8 = @splat(0);
@memcpy(buf[0..adrem], ad[i..]);
buf[adrem] = 0x01;
self.st.st[0] ^= mem.readInt(u64, &buf, .little);
}
self.st.permuteR(8);
}
}
/// Finalize the AEAD operation and prepare tag.
///
/// Applies final permutation and XORs key for tag generation.
fn finalize(self: *AeadState) void {
// XOR key before final permutation
self.st.st[2] ^= self.k0;
self.st.st[3] ^= self.k1;
self.st.permuteR(12);
// XOR key again for tag generation
self.st.st[3] ^= self.k0;
self.st.st[4] ^= self.k1;
}
};
/// Encrypt a message with Ascon-AEAD128.
///
/// Parameters:
/// - c: Output buffer for ciphertext (must be same length as m)
/// - tag: Output buffer for authentication tag (16 bytes)
/// - m: Plaintext message to encrypt
/// - ad: Associated data to authenticate but not encrypt
/// - npub: Public nonce (16 bytes, must be unique per message)
/// - k: Secret key (16 bytes)
///
/// Note: The ciphertext and tag must be transmitted together for decryption
pub fn encrypt(c: []u8, tag: *[tag_length]u8, m: []const u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) void {
debug.assert(c.len == m.len);
var state = AeadState.init(k, npub);
// Process associated data
state.processAd(ad);
// Domain separation (DSEP = 0x80 at byte 7 in little-endian)
state.st.st[4] ^= 0x8000000000000000;
// Process plaintext
var i: usize = 0;
while (i + 16 <= m.len) : (i += 16) {
state.st.addBytes(m[i..][0..16]);
state.st.extractBytes(c[i..][0..16]);
state.st.permuteR(8);
}
// Process final partial block
const remaining = m.len - i;
if (remaining > 8) {
// Split between two words
state.st.addBytes(m[i..][0..8]);
state.st.extractBytes(c[i..][0..8]);
var buf: [8]u8 = @splat(0);
@memcpy(buf[0 .. remaining - 8], m[i + 8 ..]);
const m1 = mem.readInt(u64, &buf, .little);
state.st.st[1] ^= m1;
mem.writeInt(u64, buf[0..], state.st.st[1], .little);
@memcpy(c[i + 8 ..], buf[0 .. remaining - 8]);
// Add padding
state.st.st[1] ^= @as(u64, 0x01) << @intCast((remaining - 8) * 8);
} else if (remaining == 8) {
// Exactly 8 bytes - all in word 0, padding in word 1
state.st.addBytes(m[i..][0..8]);
state.st.extractBytes(c[i..][0..8]);
// Add padding to word 1 at position 0
state.st.st[1] ^= 0x01;
} else if (remaining > 0) {
// All in first word
var temp: [8]u8 = @splat(0);
@memcpy(temp[0..remaining], m[i..]);
state.st.addBytes(&temp);
state.st.extractBytes(c[i..][0..remaining]);
// Add padding
temp = @splat(0);
temp[remaining] = 0x01;
state.st.addBytes(&temp);
// Second word stays zero
} else {
// Empty message or exact multiple - add padding block
var padded: [16]u8 = @splat(0);
padded[0] = 0x01;
state.st.addBytes(&padded);
}
// Finalization
state.finalize();
// Extract tag
mem.writeInt(u64, tag[0..8], state.st.st[3], .little);
mem.writeInt(u64, tag[8..16], state.st.st[4], .little);
}
/// Decrypt a message with Ascon-AEAD128.
///
/// Parameters:
/// - m: Output buffer for plaintext (must be same length as c)
/// - c: Ciphertext to decrypt
/// - tag: Authentication tag (16 bytes)
/// - ad: Associated data that was authenticated
/// - npub: Public nonce used during encryption (16 bytes)
/// - k: Secret key (16 bytes)
///
/// Returns: AuthenticationError if tag verification fails
///
/// Note: On authentication failure, the output buffer is securely zeroed
pub fn decrypt(m: []u8, c: []const u8, tag: [tag_length]u8, ad: []const u8, npub: [nonce_length]u8, k: [key_length]u8) AuthenticationError!void {
debug.assert(m.len == c.len);
var state = AeadState.init(k, npub);
// Process associated data
state.processAd(ad);
// Domain separation (DSEP = 0x80 at byte 7 in little-endian)
state.st.st[4] ^= 0x8000000000000000;
// Process ciphertext
var i: usize = 0;
while (i + 16 <= c.len) : (i += 16) {
const ct_block = c[i..][0..16].*; // Save ciphertext block for in-place operation support
state.st.xorBytes(m[i..][0..16], &ct_block);
state.st.setBytes(&ct_block);
state.st.permuteR(8);
}
// Final partial ciphertext block
const crem = c.len - i;
if (crem > 8) {
// Save ciphertext for in-place operation support
var saved_ct: [16]u8 = undefined;
@memcpy(saved_ct[0..crem], c[i..]);
const c0 = mem.readInt(u64, saved_ct[0..8], .little);
state.st.st[0] ^= c0;
mem.writeInt(u64, m[i..][0..8], state.st.st[0], .little);
state.st.st[0] = c0;
var buf: [8]u8 = @splat(0);
@memcpy(buf[0 .. crem - 8], saved_ct[8..][0 .. crem - 8]);
const c1 = mem.readInt(u64, &buf, .little);
const m1 = state.st.st[1] ^ c1;
mem.writeInt(u64, buf[0..], m1, .little);
@memcpy(m[i + 8 ..], buf[0 .. crem - 8]);
// Replace only the bytes we've read, keeping upper bytes intact
const mask = (@as(u64, 1) << @intCast((crem - 8) * 8)) - 1;
state.st.st[1] = (state.st.st[1] & ~mask) | (c1 & mask);
state.st.st[1] ^= @as(u64, 0x01) << @intCast((crem - 8) * 8);
} else if (crem == 8) {
// Exactly 8 bytes - process only word 0, add padding to word 1
const saved_ct = c[i..][0..8].*;
const c0 = mem.readInt(u64, &saved_ct, .little);
state.st.st[0] ^= c0;
mem.writeInt(u64, m[i..][0..8], state.st.st[0], .little);
state.st.st[0] = c0;
// Add padding to word 1 at position 0
state.st.st[1] ^= 0x01;
} else if (crem > 0) {
var buf: [8]u8 = @splat(0);
@memcpy(buf[0..crem], c[i..]);
const c0 = mem.readInt(u64, &buf, .little);
const m0 = state.st.st[0] ^ c0;
mem.writeInt(u64, buf[0..], m0, .little);
@memcpy(m[i..], buf[0..crem]);
// Replace only the bytes we've read, keeping upper bytes intact
const mask = (@as(u64, 1) << @intCast(crem * 8)) - 1;
state.st.st[0] = (state.st.st[0] & ~mask) | (c0 & mask);
state.st.st[0] ^= @as(u64, 0x01) << @intCast(crem * 8);
} else {
state.st.st[0] ^= 0x01;
}
// Finalization
state.finalize();
// Verify tag
var computed_tag: [tag_length]u8 = undefined;
mem.writeInt(u64, computed_tag[0..8], state.st.st[3], .little);
mem.writeInt(u64, computed_tag[8..16], state.st.st[4], .little);
if (!crypto.timing_safe.eql([tag_length]u8, tag, computed_tag)) {
crypto.secureZero(u8, m);
return error.AuthenticationFailed;
}
}
};
/// Ascon-Hash256 as specified in NIST SP 800-232 Section 5
pub const AsconHash256 = struct {
pub const digest_length = 32;
pub const block_length = 8;
st: AsconState,
pub const Options = struct {};
/// Initialize a new Ascon-Hash256 hasher.
///
/// Parameters:
/// - options: Configuration options (currently unused)
///
/// Returns: An initialized AsconHash256 hasher
pub fn init(options: Options) AsconHash256 {
_ = options;
// IV for Ascon-Hash256: 0x0000080100cc0002
const iv: u64 = 0x0000080100cc0002;
const words: [5]u64 = .{ iv, 0, 0, 0, 0 };
var st = AsconState.initFromWords(words);
st.permuteR(12);
return AsconHash256{ .st = st };
}
/// Compute Ascon-Hash256 hash of input data in one call.
///
/// Parameters:
/// - b: Input data to hash
/// - out: Output buffer for 32-byte hash digest
/// - options: Configuration options (currently unused)
pub fn hash(b: []const u8, out: *[digest_length]u8, options: Options) void {
var h = init(options);
h.update(b);
h.final(out);
}
/// Update the hash state with additional data.
///
/// Parameters:
/// - b: Data to add to the hash
///
/// Note: Can be called multiple times before final()
pub fn update(self: *AsconHash256, b: []const u8) void {
var i: usize = 0;
// Process full 64-bit blocks
while (i + 8 <= b.len) : (i += 8) {
self.st.addBytes(b[i..][0..8]);
self.st.permuteR(12);
}
// Store partial block for finalization
if (i < b.len) {
var padded: [8]u8 = @splat(0);
const remaining = b.len - i;
@memcpy(padded[0..remaining], b[i..]);
padded[remaining] = 0x01;
self.st.addBytes(&padded);
} else {
// Add padding block
var padded: [8]u8 = @splat(0);
padded[0] = 0x01;
self.st.addBytes(&padded);
}
}
/// Finalize the hash and output the digest.
///
/// Parameters:
/// - out: Output buffer for 32-byte hash digest
///
/// Note: After calling final(), the hasher should not be used again
pub fn final(self: *AsconHash256, out: *[digest_length]u8) void {
// Final permutation after padding
self.st.permuteR(12);
// Extract hash output (4 × 64 bits = 256 bits)
var h: [4]u64 = undefined;
for (0..4) |i| {
h[i] = self.st.st[0];
self.st.permuteR(12);
}
// Write output
for (0..4) |i| {
mem.writeInt(u64, out[i * 8 ..][0..8], h[i], .little);
}
}
};
/// Ascon-XOF128 as specified in NIST SP 800-232 Section 5
pub const AsconXof128 = struct {
pub const block_length = 8;
st: AsconState,
squeezed: bool,
pub const Options = struct {};
/// Initialize a new Ascon-XOF128 extendable output function.
///
/// Parameters:
/// - options: Configuration options (currently unused)
///
/// Returns: An initialized AsconXof128 instance
pub fn init(options: Options) AsconXof128 {
_ = options;
// IV for Ascon-XOF128: 0x0000080000cc0003
const iv: u64 = 0x0000080000cc0003;
const words: [5]u64 = .{ iv, 0, 0, 0, 0 };
var st = AsconState.initFromWords(words);
st.permuteR(12);
return AsconXof128{ .st = st, .squeezed = false };
}
/// Hash a slice of bytes with variable-length output.
///
/// Parameters:
/// - bytes: Input data to hash
/// - out: Output buffer (can be any length)
/// - options: Configuration options (currently unused)
///
/// Note: Convenience function that combines init, update, and squeeze
pub fn hash(bytes: []const u8, out: []u8, options: Options) void {
var st = init(options);
st.update(bytes);
st.squeeze(out);
}
/// Update the XOF state with additional data.
///
/// Parameters:
/// - b: Data to absorb into the XOF state
///
/// Note: Cannot be called after squeeze() has been called
pub fn update(self: *AsconXof128, b: []const u8) void {
debug.assert(!self.squeezed); // Cannot update after squeezing
var i: usize = 0;
// Process full 64-bit blocks
while (i + 8 <= b.len) : (i += 8) {
self.st.addBytes(b[i..][0..8]);
self.st.permuteR(12);
}
// Store partial block for finalization
if (i < b.len) {
var padded: [8]u8 = @splat(0);
const remaining = b.len - i;
@memcpy(padded[0..remaining], b[i..]);
padded[remaining] = 0x01;
self.st.addBytes(&padded);
} else {
// Add padding block
var padded: [8]u8 = @splat(0);
padded[0] = 0x01;
self.st.addBytes(&padded);
}
}
/// Squeeze output bytes from the XOF.
///
/// Parameters:
/// - out: Output buffer to fill with pseudorandom bytes
///
/// Note: Can be called multiple times to generate more output.
/// After first call, no more data can be absorbed with update().
pub fn squeeze(self: *AsconXof128, out: []u8) void {
if (!self.squeezed) {
// First squeeze - apply final permutation
self.st.permuteR(12);
self.squeezed = true;
}
var i: usize = 0;
while (i < out.len) {
const to_copy = @min(8, out.len - i);
var block: [8]u8 = undefined;
mem.writeInt(u64, &block, self.st.st[0], .little);
@memcpy(out[i..][0..to_copy], block[0..to_copy]);
i += to_copy;
if (i < out.len) {
self.st.permuteR(12);
}
}
}
};
/// Ascon-CXOF128 as specified in NIST SP 800-232 Section 5
pub const AsconCxof128 = struct {
pub const block_length = 8;
pub const max_custom_length = 256; // 2048 bits
st: AsconState,
squeezed: bool,
pub const Options = struct { custom: []const u8 = "" };
/// Initialize a new Ascon-CXOF128 customizable XOF.
///
/// Parameters:
/// - options: Configuration with optional customization string
/// - custom: Customization string (max 256 bytes)
///
/// Returns: An initialized AsconCxof128 instance
///
/// Note: Different customization strings produce independent XOF instances
pub fn init(options: Options) AsconCxof128 {
debug.assert(options.custom.len <= max_custom_length);
// IV for Ascon-CXOF128: 0x0000080000cc0004
const iv: u64 = 0x0000080000cc0004;
const words: [5]u64 = .{ iv, 0, 0, 0, 0 };
var st = AsconState.initFromWords(words);
st.permuteR(12);
var self = AsconCxof128{ .st = st, .squeezed = false };
// Process customization string - always process length and padding
// First block: length of customization string
const len_block = @as(u64, options.custom.len * 8); // Length in bits
self.st.st[0] ^= len_block;
self.st.permuteR(12);
if (options.custom.len > 0) {
// Process customization string blocks
var i: usize = 0;
while (i + 8 <= options.custom.len) : (i += 8) {
self.st.addBytes(options.custom[i..][0..8]);
self.st.permuteR(12);
}
// Process final partial block with padding
if (i < options.custom.len) {
var padded: [8]u8 = @splat(0);
const remaining = options.custom.len - i;
@memcpy(padded[0..remaining], options.custom[i..]);
padded[remaining] = 0x01;
self.st.addBytes(&padded);
self.st.permuteR(12);
} else {
// Add padding block
var padded: [8]u8 = @splat(0);
padded[0] = 0x01;
self.st.addBytes(&padded);
self.st.permuteR(12);
}
} else {
// Empty customization still needs padding
var padded: [8]u8 = @splat(0);
padded[0] = 0x01;
self.st.addBytes(&padded);
self.st.permuteR(12);
}
return self;
}
/// Hash a slice of bytes with customization and variable-length output.
///
/// Parameters:
/// - bytes: Input data to hash
/// - out: Output buffer (can be any length)
/// - options: Configuration with optional customization string
///
/// Note: Convenience function that combines init, update, and squeeze
pub fn hash(bytes: []const u8, out: []u8, options: Options) void {
var st = init(options);
st.update(bytes);
st.squeeze(out);
}
/// Update the CXOF state with additional data.
///
/// Parameters:
/// - b: Data to absorb into the CXOF state
///
/// Note: Cannot be called after squeeze() has been called
pub fn update(self: *AsconCxof128, b: []const u8) void {
debug.assert(!self.squeezed);
var i: usize = 0;
// Process full 64-bit blocks
while (i + 8 <= b.len) : (i += 8) {
self.st.addBytes(b[i..][0..8]);
self.st.permuteR(12);
}
// Store partial block for finalization
if (i < b.len) {
var padded: [8]u8 = @splat(0);
const remaining = b.len - i;
@memcpy(padded[0..remaining], b[i..]);
padded[remaining] = 0x01;
self.st.addBytes(&padded);
} else {
// Add padding block
var padded: [8]u8 = @splat(0);
padded[0] = 0x01;
self.st.addBytes(&padded);
}
}
/// Squeeze output bytes from the customizable XOF.
///
/// Parameters:
/// - out: Output buffer to fill with pseudorandom bytes
///
/// Note: Can be called multiple times to generate more output.
/// After first call, no more data can be absorbed with update().
pub fn squeeze(self: *AsconCxof128, out: []u8) void {
if (!self.squeezed) {
// First squeeze - apply final permutation
self.st.permuteR(12);
self.squeezed = true;
}
var i: usize = 0;
while (i < out.len) {
const to_copy = @min(8, out.len - i);
var block: [8]u8 = undefined;
mem.writeInt(u64, &block, self.st.st[0], .little);
@memcpy(out[i..][0..to_copy], block[0..to_copy]);
i += to_copy;
if (i < out.len) {
self.st.permuteR(12);
}
}
}
};
test "Ascon-Hash256 basic test" {
const message = "The quick brown fox jumps over the lazy dog";
var hash: [32]u8 = undefined;
AsconHash256.hash(message, &hash, .{});
// Verify hash is generated (exact value depends on test vectors)
try testing.expect(hash.len == 32);
}
test "Ascon-XOF128 basic test" {
var xof = AsconXof128.init(.{});
xof.update("Hello, ");
xof.update("World!");
var out1: [16]u8 = undefined;
xof.squeeze(&out1);
var out2: [32]u8 = undefined;
xof.squeeze(&out2);
// XOF outputs should be continuous - out2 should NOT match out1
// Each squeeze produces new output
try testing.expect(!mem.eql(u8, &out1, out2[0..16]));
}
test "Ascon-CXOF128 with customization" {
const custom = "MyCustomString";
var xof = AsconCxof128.init(.{ .custom = custom });
xof.update("Test message");
var out: [32]u8 = undefined;
xof.squeeze(&out);
// Different customization should give different output
var xof2 = AsconCxof128.init(.{ .custom = "DifferentCustom" });
xof2.update("Test message");
var out2: [32]u8 = undefined;
xof2.squeeze(&out2);
try testing.expect(!mem.eql(u8, &out, &out2));
}
test "Ascon-AEAD128 round trip with various data sizes" {
if (builtin.cpu.has(.riscv, .v) and builtin.zig_backend == .stage2_llvm) return error.SkipZigTest;
const key = [_]u8{ 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF, 0xFE, 0xDC, 0xBA, 0x98, 0x76, 0x54, 0x32, 0x10 };
const nonce = [_]u8{ 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF };
// Test with empty plaintext
{
const plaintext = "";
const ad = "metadata";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
AsconAead128.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
var decrypted: [plaintext.len]u8 = undefined;
try AsconAead128.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
try testing.expectEqualStrings(plaintext, &decrypted);
}
// Test with small plaintext
{
const plaintext = "Short";
const ad = "";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
AsconAead128.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
var decrypted: [plaintext.len]u8 = undefined;
try AsconAead128.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
try testing.expectEqualStrings(plaintext, &decrypted);
}
// Test with longer plaintext and associated data
{
const plaintext = "This is a longer message to test the round trip encryption and decryption process";
const ad = "Additional authenticated data that is not encrypted but is authenticated";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
AsconAead128.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
var decrypted: [plaintext.len]u8 = undefined;
try AsconAead128.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
try testing.expectEqualStrings(plaintext, &decrypted);
}
// Test authentication failure with tampered ciphertext
{
const plaintext = "Tamper test";
const ad = "metadata";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
AsconAead128.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
// Tamper with ciphertext
ciphertext[0] ^= 0xFF;
var decrypted: [plaintext.len]u8 = undefined;
const result = AsconAead128.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
try testing.expectError(error.AuthenticationFailed, result);
}
// Test authentication failure with wrong tag
{
const plaintext = "Tag test";
const ad = "metadata";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
AsconAead128.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
// Tamper with tag
var wrong_tag = tag;
wrong_tag[0] ^= 0xFF;
var decrypted: [plaintext.len]u8 = undefined;
const result = AsconAead128.decrypt(&decrypted, &ciphertext, wrong_tag, ad, nonce, key);
try testing.expectError(error.AuthenticationFailed, result);
}
// Test authentication failure with wrong associated data
{
const plaintext = "AD test";
const ad = "original";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
AsconAead128.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
var decrypted: [plaintext.len]u8 = undefined;
const wrong_ad = "modified";
const result = AsconAead128.decrypt(&decrypted, &ciphertext, tag, wrong_ad, nonce, key);
try testing.expectError(error.AuthenticationFailed, result);
}
}
// Test vectors from NIST SP 800-232 / ascon-c reference implementation
test "Ascon-AEAD128 official test vectors" {
// Test vector 1: Empty PT, Empty AD
{
var key: [16]u8 = undefined;
var nonce: [16]u8 = undefined;
_ = std.fmt.hexToBytes(&key, "000102030405060708090A0B0C0D0E0F") catch unreachable;
_ = std.fmt.hexToBytes(&nonce, "101112131415161718191A1B1C1D1E1F") catch unreachable;
const plaintext = "";
const ad = "";
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
AsconAead128.encrypt(&ciphertext, &tag, plaintext, ad, nonce, key);
var expected_tag: [16]u8 = undefined;
_ = std.fmt.hexToBytes(&expected_tag, "4F9C278211BEC9316BF68F46EE8B2EC6") catch unreachable;
try testing.expectEqualSlices(u8, &expected_tag, &tag);
}
// Test vector 2: Empty PT, AD = "30"
{
var key: [16]u8 = undefined;
var nonce: [16]u8 = undefined;
_ = std.fmt.hexToBytes(&key, "000102030405060708090A0B0C0D0E0F") catch unreachable;
_ = std.fmt.hexToBytes(&nonce, "101112131415161718191A1B1C1D1E1F") catch unreachable;
const plaintext = "";
var ad: [1]u8 = undefined;
_ = std.fmt.hexToBytes(&ad, "30") catch unreachable;
var ciphertext: [plaintext.len]u8 = undefined;
var tag: [16]u8 = undefined;
AsconAead128.encrypt(&ciphertext, &tag, plaintext, &ad, nonce, key);
var expected_tag: [16]u8 = undefined;
_ = std.fmt.hexToBytes(&expected_tag, "CCCB674FE18A09A285D6AB11B35675C0") catch unreachable;
try testing.expectEqualSlices(u8, &expected_tag, &tag);
}
// Test vector 34: Single byte plaintext 0x20
{
var key: [16]u8 = undefined;
var nonce: [16]u8 = undefined;
_ = std.fmt.hexToBytes(&key, "000102030405060708090A0B0C0D0E0F") catch unreachable;
_ = std.fmt.hexToBytes(&nonce, "101112131415161718191A1B1C1D1E1F") catch unreachable;
var plaintext: [1]u8 = undefined;
_ = std.fmt.hexToBytes(&plaintext, "20") catch unreachable;
const ad = "";
var ciphertext: [1]u8 = undefined;
var tag: [16]u8 = undefined;
AsconAead128.encrypt(&ciphertext, &tag, &plaintext, ad, nonce, key);
var expected_ct: [1]u8 = undefined;
_ = std.fmt.hexToBytes(&expected_ct, "E8") catch unreachable;
var expected_tag: [16]u8 = undefined;
_ = std.fmt.hexToBytes(&expected_tag, "DD576ABA1CD3E6FC704DE02AEDB79588") catch unreachable;
try testing.expectEqualSlices(u8, &expected_ct, &ciphertext);
try testing.expectEqualSlices(u8, &expected_tag, &tag);
// Verify decryption
var decrypted: [1]u8 = undefined;
try AsconAead128.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
try testing.expectEqualSlices(u8, &plaintext, &decrypted);
}
// Test vector with 3-byte plaintext
{
var key: [16]u8 = undefined;
var nonce: [16]u8 = undefined;
_ = std.fmt.hexToBytes(&key, "000102030405060708090A0B0C0D0E0F") catch unreachable;
_ = std.fmt.hexToBytes(&nonce, "101112131415161718191A1B1C1D1E1F") catch unreachable;
var plaintext: [3]u8 = undefined;
_ = std.fmt.hexToBytes(&plaintext, "202122") catch unreachable;
const ad = "";
var ciphertext: [3]u8 = undefined;
var tag: [16]u8 = undefined;
AsconAead128.encrypt(&ciphertext, &tag, &plaintext, ad, nonce, key);
var expected_ct: [3]u8 = undefined;
_ = std.fmt.hexToBytes(&expected_ct, "E8C3DE") catch unreachable;
var expected_tag: [16]u8 = undefined;
_ = std.fmt.hexToBytes(&expected_tag, "AF8E12816B8EDF39AD1571A9492B7CA2") catch unreachable;
try testing.expectEqualSlices(u8, &expected_ct, &ciphertext);
try testing.expectEqualSlices(u8, &expected_tag, &tag);
// Verify decryption
var decrypted: [3]u8 = undefined;
try AsconAead128.decrypt(&decrypted, &ciphertext, tag, ad, nonce, key);
try testing.expectEqualSlices(u8, &plaintext, &decrypted);
}
}
test "Ascon-Hash256 official test vectors" {
// Test vector 1: Empty message
{
const message = "";
var hash: [32]u8 = undefined;
AsconHash256.hash(message, &hash, .{});
var expected: [32]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "0B3BE5850F2F6B98CAF29F8FDEA89B64A1FA70AA249B8F839BD53BAA304D92B2") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &hash);
}
// Test vector 2: Single byte 0x00
{
const message = [_]u8{0x00};
var hash: [32]u8 = undefined;
AsconHash256.hash(&message, &hash, .{});
var expected: [32]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "0728621035AF3ED2BCA03BF6FDE900F9456F5330E4B5EE23E7F6A1E70291BC80") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &hash);
}
// Test vector 3: 0x00, 0x01
{
const message = [_]u8{ 0x00, 0x01 };
var hash: [32]u8 = undefined;
AsconHash256.hash(&message, &hash, .{});
var expected: [32]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "6115E7C9C4081C2797FC8FE1BC57A836AFA1C5381E556DD583860CA2DFB48DD2") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &hash);
}
// Test vector 4: 0x00, 0x01, 0x02
{
const message = [_]u8{ 0x00, 0x01, 0x02 };
var hash: [32]u8 = undefined;
AsconHash256.hash(&message, &hash, .{});
var expected: [32]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "265AB89A609F5A05DCA57E83FBBA700F9A2D2C4211BA4CC9F0A1A369E17B915C") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &hash);
}
// Test vector 5: 0x00..0x03
{
const message = [_]u8{ 0x00, 0x01, 0x02, 0x03 };
var hash: [32]u8 = undefined;
AsconHash256.hash(&message, &hash, .{});
var expected: [32]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "D7E4C7ED9B8A325CD08B9EF259F8877054ECD8304FE1B2D7FD847137DF6727EE") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &hash);
}
}
test "Ascon-XOF128 official test vectors" {
// Test vector 1: Empty message, 64-byte output
{
var xof = AsconXof128.init(.{});
xof.update("");
var output: [64]u8 = undefined;
xof.squeeze(&output);
var expected: [64]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "473D5E6164F58B39DFD84AACDB8AE42EC2D91FED33388EE0D960D9B3993295C6AD77855A5D3B13FE6AD9E6098988373AF7D0956D05A8F1665D2C67D1A3AD10FF") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &output);
}
// Test vector 2: Single byte 0x00, 64-byte output
{
var xof = AsconXof128.init(.{});
const msg = [_]u8{0x00};
xof.update(&msg);
var output: [64]u8 = undefined;
xof.squeeze(&output);
var expected: [64]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "51430E0438ECDF642B393630D977625F5F337656BA58AB1E960784AC32A16E0D446405551F5469384F8EA283CF12E64FA72C426BFEBAEA3AA1529E2C4AB23A2F") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &output);
}
// Test vector 3: 0x00, 0x01, 64-byte output
{
var xof = AsconXof128.init(.{});
const msg = [_]u8{ 0x00, 0x01 };
xof.update(&msg);
var output: [64]u8 = undefined;
xof.squeeze(&output);
var expected: [64]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "A05383077AF971D3830BD37E7B981497A773D441DB077C6494CC73125953846EB6427FBA4CD308FF90A11385D51101341BF5379249217BFDACE9CCA1148CC966") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &output);
}
}
test "Ascon-CXOF128 official test vectors" {
// Test vector 1: Empty message, empty customization, 64-byte output
{
var xof = AsconCxof128.init(.{});
xof.update("");
var output: [64]u8 = undefined;
xof.squeeze(&output);
var expected: [64]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "4F50159EF70BB3DAD8807E034EAEBD44C4FA2CBBC8CF1F05511AB66CDCC529905CA12083FC186AD899B270B1473DC5F7EC88D1052082DCDFE69FB75D269E7B74") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &output);
}
// Test vector 2: Empty message, customization = 0x10, 64-byte output
{
const custom = [_]u8{0x10};
var xof = AsconCxof128.init(.{ .custom = &custom });
xof.update("");
var output: [64]u8 = undefined;
xof.squeeze(&output);
var expected: [64]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "0C93A483E7D574D49FE52CCE03EE646117977D57A8AA57704AB4DAF44B501430FF6AC11A5D1FD6F2154B5C65728268270C8BB578508487B8965718ADA6272FD6") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &output);
}
// Test vector 3: Empty message, customization = 0x10, 0x11, 64-byte output
{
const custom = [_]u8{ 0x10, 0x11 };
var xof = AsconCxof128.init(.{ .custom = &custom });
xof.update("");
var output: [64]u8 = undefined;
xof.squeeze(&output);
var expected: [64]u8 = undefined;
_ = std.fmt.hexToBytes(&expected, "D1106C7622E79FE955BD9D79E03B918E770FE0E0CDDDE28BEB924B02C5FC936B33ACCA299C89ECA5D71886CBBFA4D54A21C55FDE2B679F5E2488063A1719DC32") catch unreachable;
try testing.expectEqualSlices(u8, &expected, &output);
}
}