zig/lib/std / crypto/ascon.zig

Ascon is a 320-bit permutation, selected as new standard for lightweight cryptography in the NIST Lightweight Cryptography competition (2019–2023). 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.

//! Ascon is a 320-bit permutation, selected as new standard for lightweight cryptography
//! in the NIST Lightweight Cryptography competition (2019–2023).
//! 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.

State()

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.


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();

block_bytes

Number of bytes in the state.


/// 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();

init()

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


        /// Number of bytes in the state.
        pub const block_bytes = 40;

initFromWords()

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


        const Block = [5]u64;

initXof()

Initialize the state for Ascon XOF. Returns: A new State initialized with the Ascon XOF initialization vector


        st: Block,

initXofA()

Initialize the state for Ascon XOFa. Returns: A new State initialized with the Ascon XOFa initialization vector


        /// 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;
        }

asBytes()

A representation of the state as bytes. The byte order is architecture-dependent. Returns: A pointer to the state's internal byte representation


        /// 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 };
        }

endianSwap()

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.


        /// 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,
            } };
        }

setBytes()

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


        /// 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,
            } };
        }

addByte()

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)


        /// 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);
        }

addBytes()

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


        /// 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);
            }
        }

extractBytes()

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


        /// 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);
            }
        }

xorBytes()

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


        /// 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;
        }

clear()

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


        /// 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);
            }
        }

secureZero()

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.


        /// 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]);
            }
        }

permuteR()

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


        /// 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);

permute()

Apply a full-round permutation to the state. Applies the standard 12-round Ascon permutation.


            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]);
            }
        }

permuteRatchet()

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).


        /// 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);
        }

Test:

ascon

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.


        /// 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);
        }

AsconAead128

Ascon-AEAD128 as specified in NIST SP 800-232 Section 4


        /// 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);
            }
        }

tag_length

Initialize AEAD state with key and nonce. Parameters: - key: 16-byte secret key - nonce: 16-byte nonce Returns: Initialized AEAD state ready for processing


        /// Apply a full-round permutation to the state.
        ///
        /// Applies the standard 12-round Ascon permutation.
        pub fn permute(state: *Self) void {
            state.permuteR(12);
        }

nonce_length

Process associated data for authentication. Parameters: - ad: Associated data to authenticate Updates the state to include AD in authentication tag computation.


        /// 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;
        }

key_length

Finalize the AEAD operation and prepare tag. Applies final permutation and XORs key for tag generation.


        /// 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;

block_length

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


            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];

encrypt()

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


            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];
        }
    };

Options

Ascon-Hash256 as specified in NIST SP 800-232 Section 5

}

AsconHash256

Initialize a new Ascon-Hash256 hasher. Parameters: - options: Configuration options (currently unused) Returns: An initialized AsconHash256 hasher


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);

Options

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)

}

block_length

Update the hash state with additional data. Parameters: - b: Data to add to the hash Note: Can be called multiple times before final()


const AsconState = State(.little);
const AuthenticationError = crypto.errors.AuthenticationError;

Options

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


/// 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;

init()

Ascon-XOF128 as specified in NIST SP 800-232 Section 5


    const AeadState = struct {
        st: AsconState,
        k0: u64,
        k1: u64,

hash()

Initialize a new Ascon-XOF128 extendable output function. Parameters: - options: Configuration options (currently unused) Returns: An initialized AsconXof128 instance


        /// 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);

update()

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


            // IV for Ascon-AEAD128 (Ascon-128a)
            const iv: u64 = 0x00001000808C0001;
            const words: [5]u64 = .{ iv, k0, k1, n0, n1 };

final()

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


            var st = AsconState.initFromWords(words);
            st.permuteR(12);

AsconXof128

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().


            st.st[3] ^= k0;
            st.st[4] ^= k1;

block_length

Ascon-CXOF128 as specified in NIST SP 800-232 Section 5


            return AeadState{ .st = st, .k0 = k0, .k1 = k1 };
        }

Options

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


        /// 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;

init()

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


            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);
            }

hash()

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


            // 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);

update()

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().


                    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);
            }
        }

squeeze()


        /// 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);

AsconCxof128


            // XOR key again for tag generation
            self.st.st[3] ^= self.k0;
            self.st.st[4] ^= self.k1;
        }
    };

block_length


    /// 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);

max_custom_length


        var state = AeadState.init(k, npub);

Options


        // Process associated data
        state.processAd(ad);

init()


        // Domain separation (DSEP = 0x80 at byte 7 in little-endian)
        state.st.st[4] ^= 0x8000000000000000;

hash()


        // 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);
        }

update()


        // 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]);

squeeze()


            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]);

Test:

Ascon-Hash256 basic test


            // 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]);

Test:

Ascon-XOF128 basic test


            // 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);
        }

Test:

Ascon-CXOF128 with customization


        // Finalization
        state.finalize();

Test:

Ascon-AEAD128 round trip with various data sizes


        // Extract tag
        mem.writeInt(u64, tag[0..8], state.st.st[3], .little);
        mem.writeInt(u64, tag[8..16], state.st.st[4], .little);
    }

Test:

Ascon-AEAD128 official test vectors


    /// 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);

Test:

Ascon-Hash256 official test vectors


        var state = AeadState.init(k, npub);

Test:

Ascon-XOF128 official test vectors


        // Process associated data
        state.processAd(ad);

Test:

Ascon-CXOF128 official test vectors


        // 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);
    }
}