// Copyright Β©2025 blackshirt. // Use of this source code is governed by an MIT license // that can be found in the LICENSE file. // // This file implements an Authenticated Encryption with Associated Data based on Ascon-AEAD128, // AEAD Scheme defined in NIST.SP.800-232 standard. module ascon import encoding.binary import crypto.internal.subtle // The constants for Ascon-AEAD128 // // key_size is 128-bit size of Ascon-AEAD128 key pub const key_size = 16 // nonce_size is 128-bit size of Ascon-AEAD128 nonce pub const nonce_size = 16 // tag_size is 128-bit size of Ascon-AEAD128 authentication tag pub const tag_size = 16 // aead128_iv is a precomputed initialization phase values for Ascon-AEAD128 // See Table 14 of NIST SP 800-232 doc. const aead128_iv = u64(0x0000_1000_808c_0001) // aead128_data_limit is a limit amount of data processed during encryption and decryption, // including the nonce, shall not exceed 2⁡⁴ bytes for a given key. const aead128_data_limit = u64(1) << 54 - 1 // aead128_block_size is a number (rate) of input bytes processed per invocation of the underlying of state. // Ascon-AEAD128 working with 128-bit rate const aead128_block_size = 16 // encrypt encrypts the message under provided key and nonce and supplied additional data in ad. // It returns an authenticated output of Ascon-AEAD128 ciphertext where authentication tag // was stored within the end of ciphertext. // Note: The Ascon-AEAD128 key shall be kept secret, pub fn encrypt(key []u8, nonce []u8, ad []u8, msg []u8) ![]u8 { // Preliminary check if key.len != key_size { return error('encrypt: invalid key size') } if nonce.len != nonce_size { return error('encrypt: invalid nonce size') } // The key shall be updated to a new key once the total amount of input data reaches the limit data_length := u64(nonce.len) + u64(msg.len) + u64(ad.len) if data_length > aead128_data_limit { return error('encrypt: exceed data limit') } mut s := State{} mut out := []u8{len: msg.len + tag_size} // Ascon-AEAD128 comprises four phases: // - initialization of the state, // - associated data processing, // - plaintext processing, // - and finalization (includes writing the tag). k0, k1 := aead128_init(mut s, key, nonce) aead128_process_ad(mut s, ad) loc := aead128_process_msg(mut out, mut s, msg) aead128_finalize(mut s, k0, k1) aead128_write_tag(mut out, s, loc) // clean out the intermediate Ascon state reset_state(mut s) return out } // decrypt decrypts authenticated encrypted messages in ciphertext that encrypted under // provided key and nonce with additional data in `ad`. // It would check if authentication tag mas matching and return decrypted message // if success or error on fails. @[direct_array_access] pub fn decrypt(key []u8, nonce []u8, ad []u8, ciphertext []u8) ![]u8 { // Preliminary check if key.len != key_size { return error('decrypt: invalid key size') } if nonce.len != nonce_size { return error('decrypt: invalid nonce size') } if ciphertext.len < tag_size { return error('decrypt: invalid ciphertext size') } data_length := u64(nonce.len) + u64(ciphertext.len) + u64(ad.len) if data_length > aead128_data_limit { return error('decrypt: exceed data limit') } mut s := State{} // Initialization phase and additional data processing k0, k1 := aead128_init(mut s, key, nonce) aead128_process_ad(mut s, ad) // Decryption phase, start by slicing the ciphertext cmsg := ciphertext[0..ciphertext.len - tag_size] stag := ciphertext[ciphertext.len - tag_size..ciphertext.len] mut msg := []u8{len: ciphertext.len - tag_size} // Partially decrypt the cmsg and stored into msg buffer aead128_partial_dec(mut msg, mut s, cmsg) // Finalizes the state and calc the tag and compares with expected tag. // It would return error if the tag was unmatching. aead128_finalize(mut s, k0, k1) mut ctag := []u8{len: tag_size} aead128_write_tag(mut ctag, s, 0) if subtle.constant_time_compare(ctag, stag) != 1 { // clean up unsafe { msg.reset() ctag.reset() } reset_state(mut s) return error('decrypt: unmatching tag') } return msg } // Aead128 is an opaque provides an implementation of Ascon-AEAD128 from NIST.SP.800-232 standard. // Its implements `x.crypto.chacha20poly1305.AEAD` interfaces. @[noinit] pub struct Aead128 { State mut: // 32-bytes of underlying key key [2]u64 } // new_aead128 creates a new Aead128 instance and initialized with supplied key. @[direct_array_access] pub fn new_aead128(key []u8) !&Aead128 { if key.len != key_size { return error('invalid cipher key size') } k0 := binary.little_endian_u64(key[0..8]) k1 := binary.little_endian_u64(key[8..16]) mut c := &Aead128{} // Partially initializes state c.State.e0 = aead128_iv c.State.e1 = k0 c.State.e2 = k1 // stores the key c.key[0] = k0 c.key[1] = k1 return c } // nonce_size returns the nonce size of Ascon-AEAD128 Aead128. pub fn (c &Aead128) nonce_size() int { return nonce_size } // overhead returns the maximum difference between the lengths of a plaintext and its ciphertext. pub fn (c &Aead128) overhead() int { return tag_size } // encrypt encrypts the message under provided key and nonce and supplied additional data in ad. // It returns an authenticated output of Ascon-AEAD128 ciphertext where authentication tag // was stored within the end of ciphertext. @[direct_array_access] pub fn (mut c Aead128) encrypt(msg []u8, nonce []u8, ad []u8) ![]u8 { // Check for the nonce if nonce.len != nonce_size { return error('encrypt: invalid nonce size') } data_length := u64(msg.len) + u64(ad.len) + 32 if data_length > aead128_data_limit { return error('encrypt: exceed data limit') } // Initialization phase n0 := binary.little_endian_u64(nonce[0..8]) n1 := binary.little_endian_u64(nonce[8..16]) // setup state c.State.e0 = aead128_iv c.State.e1 = c.key[0] c.State.e2 = c.key[1] c.State.e3 = n0 c.State.e4 = n1 // Update state by permutation ascon_pnr(mut c.State, ascon_prnd_12) // XOR-ing with the cipher's key c.State.e3 ^= c.key[0] c.State.e4 ^= c.key[1] // Associated data processing aead128_process_ad(mut c.State, ad) // Message processing mut dst := []u8{len: msg.len + tag_size} n := aead128_process_msg(mut dst, mut c.State, msg) // Finalization and writes out the tag into dst aead128_finalize(mut c.State, c.key[0], c.key[1]) aead128_write_tag(mut dst, c.State, n) return dst } // decrypt decrypts the ciphertext and validates authentication tag with // provided nonce and additional data ad. It returns error on fails or tag unmatching. @[direct_array_access] pub fn (mut c Aead128) decrypt(ciphertext []u8, nonce []u8, ad []u8) ![]u8 { // Check for nonce if nonce.len != nonce_size { return error('bad nonce size') } // Check for ciphertext length, its ahould have length >= tag_size if ciphertext.len < tag_size { return error('bad ciphertext size') } // check for data limit overflow data_length := u64(ciphertext.len) + u64(ad.len) + 32 if data_length > aead128_data_limit { return error('decrypt: exceed data limit') } // load nonce n0 := binary.little_endian_u64(nonce[0..8]) n1 := binary.little_endian_u64(nonce[8..16]) // Reinitialize internal state c.State.e0 = aead128_iv c.State.e1 = c.key[0] c.State.e2 = c.key[1] c.State.e3 = n0 c.State.e4 = n1 // scrambled with permutation routine ascon_pnr(mut c.State, ascon_prnd_12) // xor-ing with the cipher's key c.State.e3 ^= c.key[0] c.State.e4 ^= c.key[1] // Associated data processing // aead128_process_ad(mut c.State, ad) // As we know, ciphertext length was sum of encrypted mesage length plus tag_size // Lets slicing it cxt_len := ciphertext.len cmsg := ciphertext[0..cxt_len - tag_size] ctag := ciphertext[cxt_len - tag_size..cxt_len] mut out := []u8{len: cxt_len - tag_size} aead128_partial_dec(mut out, mut c.State, cmsg) aead128_finalize(mut c.State, c.key[0], c.key[1]) // tag verification mut tag := []u8{len: tag_size} aead128_write_tag(mut tag, c.State, 0) if subtle.constant_time_compare(ctag, tag) != 1 { // Cleans up previously produces bytes (state) reset_state(mut c.State) unsafe { tag.free() out.free() } return error('Aead128.decrypt: unmatching tag') } return out } // Helpers for Ascon-AEAD128 // // aead128_init initializes Ascon-AEAD128 state by provided key and nonce. // Its return two's u64 values from deserialized key bytes in little-endian form. @[direct_array_access; inline] fn aead128_init(mut s State, key []u8, nonce []u8) (u64, u64) { // load key and nonce into state in little-endian form, // The endianness has been switched from big endian to little endian k0 := binary.little_endian_u64(key[0..8]) k1 := binary.little_endian_u64(key[8..16]) n0 := binary.little_endian_u64(nonce[0..8]) n1 := binary.little_endian_u64(nonce[8..16]) // Given a 128-bit 𝐾 and a 128-bit 𝑁, the 320-bit internal // state S is initialized as the concatenation of 𝐼𝑉, 𝐾, and 𝑁: // S ← 𝐼𝑉 || 𝐾 || 𝑁, s.e0 = aead128_iv s.e1 = k0 s.e2 = k1 s.e3 = n0 s.e4 = n1 // updates State using the permutation π΄π‘ π‘π‘œπ‘›-𝑝[12], S ← π΄π‘ π‘π‘œπ‘›-𝑝[12](S) ascon_pnr(mut s, ascon_prnd_12) // Then XORing the secret key 𝐾 into the last 128 bits of internal state: // S ← S βŠ• (0¹⁹² βˆ₯ 𝐾). s.e3 ^= k0 s.e4 ^= k1 return k0, k1 } // aead128_process_ad absorbs associated data into Ascon-AEAD128 state. @[direct_array_access; inline] fn aead128_process_ad(mut s State, ad []u8) { mut ad_length := ad.len mut ad_idx := 0 if ad_length > 0 { for ad_length >= aead128_block_size { // Each associated data block 𝐴𝑖 (0 ≀ 𝑖 ≀ π‘š) is absorbed into the first 128 bits of // state as S[0∢127] ← S[0∢127] βŠ• 𝐴𝑖, block := unsafe { ad[ad_idx..ad_idx + aead128_block_size] } s.e0 ^= binary.little_endian_u64(block[0..8]) s.e1 ^= binary.little_endian_u64(block[8..16]) // Apply permutation π΄π‘ π‘π‘œπ‘›-𝑝[8] to the state ascon_pnr(mut s, ascon_prnd_8) // Updates index ad_length -= aead128_block_size ad_idx += aead128_block_size } // process partial block if it exists if ad_length >= 8 { first_block := unsafe { ad[ad_idx..ad_idx + 8] } s.e0 ^= binary.little_endian_u64(first_block) // Is there more bytes to process on? last_block := unsafe { ad[ad_idx + 8..] } s.e1 ^= pad(last_block.len) if last_block.len > 0 { s.e1 ^= u64_from_partial_bytes(last_block) } // update index ad_length -= first_block.len + last_block.len ad_idx += first_block.len + last_block.len } else { last_block := unsafe { ad[ad_idx..] } s.e0 ^= pad(last_block.len) if last_block.len > 0 { s.e0 ^= u64_from_partial_bytes(last_block) } } // Apply permutation π΄π‘ π‘π‘œπ‘›-𝑝[8] to the state ascon_pnr(mut s, ascon_prnd_8) } // The final step of processing associated data is to update the state // with a constant that provides domain separation. s.e4 ^= u64(0x8000_0000_0000_0000) } // aead128_process_msg process (encrypt) the messages msg and asborb it into Ascon-AEAD128 state // Its written the result into out buffer and return the number of bytes has been written. @[direct_array_access; inline] fn aead128_process_msg(mut out []u8, mut s State, msg []u8) int { mut pos := 0 mut mlen := msg.len mut midx := 0 for mlen >= aead128_block_size { block := unsafe { msg[midx..midx + aead128_block_size] } s.e0 ^= binary.little_endian_u64(block[0..8]) s.e1 ^= binary.little_endian_u64(block[8..16]) // stores binary.little_endian_put_u64(mut out[pos..pos + 8], s.e0) binary.little_endian_put_u64(mut out[pos + 8..], s.e1) // apply permutation ascon_pnr(mut s, ascon_prnd_8) // updates index mlen -= aead128_block_size pos += aead128_block_size midx += aead128_block_size } // process partial block if it exists if mlen >= 8 { mut first_block := unsafe { msg[midx..] } s.e0 ^= load_bytes(first_block[0..8], 8) binary.little_endian_put_u64(mut out[pos..pos + 8], s.e0) // Is there are reminder bytes to process on? last_block := unsafe { msg[midx + 8..] } if last_block.len > 0 { // We use `load_bytes` to handle length < 8 s.e1 ^= load_bytes(last_block, last_block.len) store_bytes(mut out[pos + 8..], s.e1, last_block.len) s.e1 ^= pad(last_block.len) } } else { last_block := unsafe { msg[midx..] } s.e0 ^= load_bytes(last_block, last_block.len) store_bytes(mut out[pos..], s.e0, last_block.len) s.e0 ^= pad(last_block.len) } // how much we have written pos += mlen return pos } // aead128_partial_dec partially decrypts the encrypted part of the message in the cmsg, // and stored into out buffer. // Note: The output buffer should have the same length with cmsg, ie, out.len == cmsg.len @[direct_array_access] fn aead128_partial_dec(mut out []u8, mut s State, cmsg []u8) { mut cmsg_len := cmsg.len mut pos := 0 // assert out.len == cmsg.len for cmsg_len >= aead128_block_size { block := unsafe { cmsg[pos..pos + aead128_block_size] } c0 := binary.little_endian_u64(block[0..8]) c1 := binary.little_endian_u64(block[8..16]) binary.little_endian_put_u64(mut out[pos..pos + 8], s.e0 ^ c0) binary.little_endian_put_u64(mut out[pos + 8..pos + 16], s.e1 ^ c1) s.e0 = c0 s.e1 = c1 ascon_pnr(mut s, ascon_prnd_8) // updates index pos += aead128_block_size cmsg_len -= aead128_block_size } // partial block if cmsg_len >= 8 { mut first_block := unsafe { cmsg[pos..pos + 8] } c0 := binary.little_endian_u64(first_block) binary.little_endian_put_u64(mut out[pos..pos + 8], c0 ^ s.e0) last_block := unsafe { cmsg[pos + 8..] } c1 := load_bytes(last_block, last_block.len) store_bytes(mut out[pos + 8..], c1 ^ s.e1, last_block.len) s.e0 = c0 s.e1 = clear_bytes(s.e1, last_block.len) s.e1 |= c1 s.e0 ^= pad(last_block.len) } else { last_block := unsafe { cmsg[pos..] } c0 := load_bytes(last_block, last_block.len) store_bytes(mut out[pos..], s.e0 ^ c0, last_block.len) s.e0 = clear_bytes(s.e0, last_block.len) s.e0 |= c0 s.e0 ^= pad(last_block.len) } } // aead128_finalize does finalization step and generates tag value. fn aead128_finalize(mut s State, k0 u64, k1 u64) { // Load the key into state, S ← S βŠ• (0¹²⁸ βˆ₯ 𝐾 βˆ₯ 0⁢⁴), s.e2 ^= k0 s.e3 ^= k1 // then updated using the permutation π΄π‘ π‘π‘œπ‘›-𝑝[12] ascon_pnr(mut s, ascon_prnd_12) // Finally, the tag 𝑇 is generated by XORing the key with the last 128 bits of the state: // 𝑇 ← 𝑆[192∢319] βŠ• 𝐾. s.e3 ^= k0 s.e4 ^= k1 } // aead128_write_tag writes tag from state into out at loc offset. @[direct_array_access; inline] fn aead128_write_tag(mut out []u8, s State, loc int) { binary.little_endian_put_u64(mut out[loc..loc + 8], s.e3) binary.little_endian_put_u64(mut out[loc + 8..loc + 16], s.e4) }