Ascon-AEAD128 as specified in NIST SP 800-232 Section 4
pub const AsconAead128 = struct
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;
}
}
}