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
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 .. @divCeil(to, 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];
}
};
}