From f0731691771ba7e644eeacb67695382fb4660142 Mon Sep 17 00:00:00 2001 From: blackshirt Date: Wed, 10 Sep 2025 14:03:35 +0700 Subject: [PATCH] x.crypto: add a new `ascon` cryptographic module, based on https://doi.org/10.6028/NIST.SP.800-232 (Lightweight Cryptography Standards for Constrained Devices) (#25260) --- vlib/x/crypto/ascon/README.md | 15 + vlib/x/crypto/ascon/aead128.v | 464 +++++++++++++++++++ vlib/x/crypto/ascon/aead128_test.v | 247 ++++++++++ vlib/x/crypto/ascon/ascon.v | 127 +++++ vlib/x/crypto/ascon/ascon_test.v | 71 +++ vlib/x/crypto/ascon/cxof_test.v | 145 ++++++ vlib/x/crypto/ascon/digest.v | 135 ++++++ vlib/x/crypto/ascon/examples/use_of_cxof.v | 42 ++ vlib/x/crypto/ascon/examples/use_of_hash26.v | 16 + vlib/x/crypto/ascon/examples/use_of_xof.v | 40 ++ vlib/x/crypto/ascon/hash.v | 130 ++++++ vlib/x/crypto/ascon/hash_test.v | 169 +++++++ vlib/x/crypto/ascon/util.v | 107 +++++ vlib/x/crypto/ascon/xof.v | 327 +++++++++++++ vlib/x/crypto/ascon/xof_test.v | 112 +++++ 15 files changed, 2147 insertions(+) create mode 100644 vlib/x/crypto/ascon/README.md create mode 100644 vlib/x/crypto/ascon/aead128.v create mode 100644 vlib/x/crypto/ascon/aead128_test.v create mode 100644 vlib/x/crypto/ascon/ascon.v create mode 100644 vlib/x/crypto/ascon/ascon_test.v create mode 100644 vlib/x/crypto/ascon/cxof_test.v create mode 100644 vlib/x/crypto/ascon/digest.v create mode 100644 vlib/x/crypto/ascon/examples/use_of_cxof.v create mode 100644 vlib/x/crypto/ascon/examples/use_of_hash26.v create mode 100644 vlib/x/crypto/ascon/examples/use_of_xof.v create mode 100644 vlib/x/crypto/ascon/hash.v create mode 100644 vlib/x/crypto/ascon/hash_test.v create mode 100644 vlib/x/crypto/ascon/util.v create mode 100644 vlib/x/crypto/ascon/xof.v create mode 100644 vlib/x/crypto/ascon/xof_test.v diff --git a/vlib/x/crypto/ascon/README.md b/vlib/x/crypto/ascon/README.md new file mode 100644 index 000000000..61a826506 --- /dev/null +++ b/vlib/x/crypto/ascon/README.md @@ -0,0 +1,15 @@ +# ascon + +`ascon` is a implementation of Ascon-Based Cryptography module implemented in pure V language. +This module was mostly based on NIST Special Publication of 800 NIST SP 800-232 document. +Its describes an Ascon-Based Lightweight Cryptography Standards for Constrained Devices +thats provides Authenticated Encryption, Hash, and Extendable Output Functions. +See the [NIST.SP.800-232 Standard](https://doi.org/10.6028/NIST.SP.800-232) for more detail. + +This module does not fully implements all the features availables on the document. +Its currently implements: +- `Ascon-Hash256`, Ascon-based hashing implementation that produces 256-bits output. +- `Ascon-XOF128`, Ascon-based eXtendible Output Function (XOF) where the output size of +the hash of the message can be selected by the user. +- `Ascon-CXOF128`, a customized XOF that allows users to specify a customization +string and choose the output size of the message hash. diff --git a/vlib/x/crypto/ascon/aead128.v b/vlib/x/crypto/ascon/aead128.v new file mode 100644 index 000000000..da215e5e1 --- /dev/null +++ b/vlib/x/crypto/ascon/aead128.v @@ -0,0 +1,464 @@ +// 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, 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, 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, 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, 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, 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, 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, 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, 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) +} diff --git a/vlib/x/crypto/ascon/aead128_test.v b/vlib/x/crypto/ascon/aead128_test.v new file mode 100644 index 000000000..8d86f6b3b --- /dev/null +++ b/vlib/x/crypto/ascon/aead128_test.v @@ -0,0 +1,247 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +import encoding.hex +import x.crypto.ascon + +// This test materials was taken and adapted into v from references implementation of Ascon-aead128 +// especially for the known answer test data, but, its not all fully-taken, just randomly choosen item. +// See at https://github.com/ascon/ascon-c/blob/main/crypto_aead/asconaead128/LWC_AEAD_KAT_128_128.txt +struct KatTest { + cnt int + key string + nonce string + pt string + ad string + ct string +} + +// testing for Ascon-AEAD128 encryption and decryption. +fn test_ascon_aead128_enc_dec() ! { + for item in aead128_kat_tests_data { + key := hex.decode(item.key)! + nonce := hex.decode(item.nonce)! + pt := hex.decode(item.pt)! + ad := hex.decode(item.ad)! + ct := hex.decode(item.ct)! + + out := ascon.encrypt(key, nonce, ad, pt)! + assert out == ct + + msg := ascon.decrypt(key, nonce, ad, ct)! + assert msg == pt + + // Work with object-based Cipher + mut c := ascon.new_aead128(key)! + // Lets encrypt the message + exp_ct := c.encrypt(msg, nonce, ad)! + assert exp_ct == ct + // Lets decrypt it back + exp_msg := c.decrypt(exp_ct, nonce, ad)! + assert exp_msg == msg + } +} + +const aead128_kat_tests_data = [ + KatTest{ + cnt: 1 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '' + ct: '4F9C278211BEC9316BF68F46EE8B2EC6' + }, + KatTest{ + cnt: 2 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '30' + ct: 'CCCB674FE18A09A285D6AB11B35675C0' + }, + KatTest{ + cnt: 3 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '3031' + ct: 'F65B191550C4DF9CFDD4460EBBCCA782' + }, + KatTest{ + cnt: 4 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '303132' + ct: 'D127CF7D2CD4DA8930616C70B3619F42' + }, + KatTest{ + cnt: 5 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '30313233' + ct: '000BA92E52B5ED6B97C9D913CC4C82DF' + }, + KatTest{ + cnt: 6 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '3031323334' + ct: 'F7CC167F8FED3AEEA99B385B8622157E' + }, + KatTest{ + cnt: 7 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '303132333435' + ct: '51CCBC46D56E93B89B1A3BFDAD0AA4D5' + }, + KatTest{ + cnt: 8 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '30313233343536' + ct: 'B38ABBD573E071C6265EEAC4A68F65AB' + }, + KatTest{ + cnt: 9 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '3031323334353637' + ct: '865C594093A9EDEE2C1D6384CCB4939E' + }, + KatTest{ + cnt: 10 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '303132333435363738' + ct: '24F13284A0F90F906B18C7E4061C0896' + }, + KatTest{ + cnt: 27 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '303132333435363738393A3B3C3D3E3F40414243444546474849' + ct: '4ED362C4407B1D3BE17A51465659DECF' + }, + KatTest{ + cnt: 28 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '303132333435363738393A3B3C3D3E3F404142434445464748494A' + ct: 'A35C52EC6E7C78C051B23D03F691916F' + }, + KatTest{ + cnt: 29 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '303132333435363738393A3B3C3D3E3F404142434445464748494A4B' + ct: 'F1C946363A21CCFFE291A289202FC64C' + }, + KatTest{ + cnt: 30 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C' + ct: 'F1D453E933904578EEC3EA8E85550CE5' + }, + KatTest{ + cnt: 31 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D' + ct: '82E22C860881C0485EC5F5E8CEA42CEA' + }, + KatTest{ + cnt: 32 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E' + ct: 'C6306F1F154C78833984173360AAE874' + }, + KatTest{ + cnt: 33 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '' + ad: '303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F' + ct: 'EFC3E78B02AD9A80A6F0548C5B0BB5BA' + }, + KatTest{ + cnt: 34 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '20' + ad: '' + ct: 'E8DD576ABA1CD3E6FC704DE02AEDB79588' + }, + KatTest{ + cnt: 35 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '20' + ad: '30' + ct: '962B8016836C75A7D86866588CA245D886' + }, + KatTest{ + cnt: 49 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '20' + ad: '303132333435363738393A3B3C3D3E' + ct: '2089CB1DE2AE7D3E45BA7E9CC293548546' + }, + KatTest{ + cnt: 599 + key: '000102030405060708090a0b0c0d0e0f' + nonce: '101112131415161718191a1b1c1d1e1f' + pt: '202122232425262728292a2b2c2d2e2f3031' + ad: '30313233' + ct: 'cf5337fcb70ec45d179e0c3f51bb25ac967a2e7062ee9bd80da6c72e3a9b43aed9e0' + }, + KatTest{ + cnt: 600 + key: '000102030405060708090a0b0c0d0e0f' + nonce: '101112131415161718191a1b1c1d1e1f' + pt: '202122232425262728292a2b2c2d2e2f3031' + ad: '3031323334' + ct: '3076658cba8bf3bb6dccaa2f1255ee2e7db6f6493c7698f65f6860a7433a0f561e6c' + }, + KatTest{ + cnt: 601 + key: '000102030405060708090a0b0c0d0e0f' + nonce: '101112131415161718191a1b1c1d1e1f' + pt: '202122232425262728292a2b2c2d2e2f3031' + ad: '303132333435' + ct: '9310c6dd8e9cbc3e406c0ebfbea312435f2c6975faf3b6b2b17ef1ea2503c3d31ef5' + }, + KatTest{ + cnt: 602 + key: '000102030405060708090a0b0c0d0e0f' + nonce: '101112131415161718191a1b1c1d1e1f' + pt: '202122232425262728292a2b2c2d2e2f3031' + ad: '30313233343536' + ct: '6e024bd403f386eb9d1c56f459cfdcde1b2fdf8fd8be2faf0576c81e8d21c0dd8f8a' + }, + KatTest{ + cnt: 603 + key: '000102030405060708090A0B0C0D0E0F' + nonce: '101112131415161718191A1B1C1D1E1F' + pt: '202122232425262728292A2B2C2D2E2F3031' + ad: '3031323334353637' + ct: 'fabe2cb1e7eba6329a30080f26e7dc72503dfc57f4de06a334b7ebadca03b44b73e9' + }, +] diff --git a/vlib/x/crypto/ascon/ascon.v b/vlib/x/crypto/ascon/ascon.v new file mode 100644 index 000000000..4f6726960 --- /dev/null +++ b/vlib/x/crypto/ascon/ascon.v @@ -0,0 +1,127 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +module ascon + +// max_nr_perm is the maximum number of permutations round supported on this module. +// The previous Ascon submission defined three Ascon permutations with 6, 8, and 12 rounds. +// The NIST SP 800-232 standard specifies additional Ascon permutations by providing round +// constants for up to 16 rounds to accommodate potential functionality extensions in the future. +const max_nr_perm = 16 + +// The constants to derive round constants of the Ascon permutations +// See Table 5. of NIST SP 800-232 docs +// +// 0 0x000000000000003c 8 0x00000000000000b4 +// 1 0x000000000000002d 9 0x00000000000000a5 +// 2 0x000000000000001e 10 0x0000000000000096 +// 3 0x000000000000000f 11 0x0000000000000087 +// 4 0x00000000000000f0 12 0x0000000000000078 +// 5 0x00000000000000e1 13 0x0000000000000069 +// 6 0x00000000000000d2 14 0x000000000000005a +// 7 0x00000000000000c3 15 0x000000000000004b +// +// We use u8 instead, since the first 56 bits of the constants are zero +const rnc = [u8(0x3c), 0x2d, 0x1e, 0x0f, 0xf0, 0xe1, 0xd2, 0xc3, 0xb4, 0xa5, 0x96, 0x87, 0x78, + 0x69, 0x5a, 0x4b] + +// ascon_pnr is ascon permutation routine with specified numbers of round nr, where 1 ≀ nr ≀ 16 +@[direct_array_access] +fn ascon_pnr(mut s State, nr int) { + // We dont allow nr == 0 + if nr < 1 || nr > 16 { + panic('Invalid round number') + } + for i := max_nr_perm - nr; i < max_nr_perm; i++ { + ascon_perm(mut s, rnc[i]) + } +} + +// ascon_perm was the main permutations routine in Ascon-family crypto. Its consist of +// iterations of the round function that is defined as the composition of three steps, ie: +// 1. the constant-addition layer (see Sec. 3.2), +// 2. the substitution layer (see Sec.3.3), and, +// 3. the linear diffusion layer +fn ascon_perm(mut s State, c u8) { + // 3.2 Constant-Addition Layer step + // + // The constant-addition layer adds a 64-bit round constant 𝑐𝑖 + // to 𝑆₂ in round 𝑖, for 𝑖 β‰₯ 0, ie, this is equivalent to applying + // the constant to only the least significant eight bits of 𝑆₂ + s.e2 ^= c + + // 3.3. Substitution Layer + // The substitution layer updates the state S with 64 parallel applications of the 5-bit + // substitution box SBOX + s.e0 ^= s.e4 + s.e4 ^= s.e3 + s.e2 ^= s.e1 + + t0 := s.e4 ^ (~s.e0 & s.e1) + t1 := s.e0 ^ (~s.e1 & s.e2) + t2 := s.e1 ^ (~s.e2 & s.e3) + t3 := s.e2 ^ (~s.e3 & s.e4) + t4 := s.e3 ^ (~s.e4 & s.e0) + + s.e0 = t1 + s.e1 = t2 + s.e2 = t3 + s.e3 = t4 + s.e4 = t0 + + s.e1 ^= s.e0 + s.e0 ^= s.e4 + s.e3 ^= s.e2 + s.e2 = ~(s.e2) + + // 3.4. Linear Diffusion Layer + // + // The linear diffusion layer provides diffusion within each 64-bit word S, + // defined as : + // Ξ£0(𝑆0) = 𝑆0 βŠ• (𝑆0 β‹™ 19) βŠ• (𝑆0 β‹™ 28) + // Ξ£1(𝑆1) = 𝑆1 βŠ• (𝑆1 β‹™ 61) βŠ• (𝑆1 β‹™ 39) + // Ξ£2(𝑆2) = 𝑆2 βŠ• (𝑆2 β‹™ 1) βŠ• (𝑆2 β‹™ 6) + // Ξ£3(𝑆3) = 𝑆3 βŠ• (𝑆3 β‹™ 10) βŠ• (𝑆3 β‹™ 17) + // Ξ£4(𝑆4) = 𝑆4 βŠ• (𝑆4 β‹™ 7) βŠ• (𝑆4 β‹™ 41) + + s.e0 ^= ascon_rotate_right(s.e0, 19) ^ ascon_rotate_right(s.e0, 28) + s.e1 ^= ascon_rotate_right(s.e1, 61) ^ ascon_rotate_right(s.e1, 39) + s.e2 ^= ascon_rotate_right(s.e2, 1) ^ ascon_rotate_right(s.e2, 6) + s.e3 ^= ascon_rotate_right(s.e3, 10) ^ ascon_rotate_right(s.e3, 17) + s.e4 ^= ascon_rotate_right(s.e4, 7) ^ ascon_rotate_right(s.e4, 41) +} + +// State is structure represents Ascon state. Its operates on the 320-bit opaque, +// which is represented as five of 64-bit words. +@[noinit] +struct State { +mut: + e0 u64 + e1 u64 + e2 u64 + e3 u64 + e4 u64 +} + +// clone returns a clone of current state. +@[inline] +fn clone_state(s State) State { + return State{ + e0: s.e0 + e1: s.e1 + e2: s.e2 + e3: s.e3 + e4: s.e4 + } +} + +// reset this state with default value +@[inline] +fn reset_state(mut s State) { + s.e0 = 0 + s.e1 = 0 + s.e2 = 0 + s.e3 = 0 + s.e4 = 0 +} diff --git a/vlib/x/crypto/ascon/ascon_test.v b/vlib/x/crypto/ascon/ascon_test.v new file mode 100644 index 000000000..763aa0c07 --- /dev/null +++ b/vlib/x/crypto/ascon/ascon_test.v @@ -0,0 +1,71 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +module ascon + +// This test mostly taken from https://docs.rs/ascon/latest/src/ascon/lib.rs.html +fn test_ascon_round_one() { + mut s := State{ + e0: u64(0x0123456789abcdef) + e1: 0x23456789abcdef01 + e2: 0x456789abcdef0123 + e3: 0x6789abcdef012345 + e4: 0x89abcde01234567f + } + ascon_perm(mut s, 0x1f) + + assert s.e0 == u64(0x3c1748c9be2892ce) + assert s.e1 == u64(0x5eafb305cd26164f) + assert s.e2 == u64(0xf9470254bb3a4213) + assert s.e3 == u64(0xf0428daf0c5d3948) + assert s.e4 == u64(0x281375af0b294899) +} + +fn test_ascon_round_p6() { + mut s := State{ + e0: u64(0x0123456789abcdef) + e1: 0xef0123456789abcd + e2: 0xcdef0123456789ab + e3: 0xabcdef0123456789 + e4: 0x89abcdef01234567 + } + ascon_pnr(mut s, 6) + assert s.e0 == u64(0xc27b505c635eb07f) + assert s.e1 == u64(0xd388f5d2a72046fa) + assert s.e2 == u64(0x9e415c204d7b15e7) + assert s.e3 == u64(0xce0d71450fe44581) + assert s.e4 == u64(0xdd7c5fef57befe48) +} + +fn test_ascon_round_p8() { + mut s := State{ + e0: u64(0x0123456789abcdef) + e1: 0xef0123456789abcd + e2: 0xcdef0123456789ab + e3: 0xabcdef0123456789 + e4: 0x89abcdef01234567 + } + ascon_pnr(mut s, 8) + assert s.e0 == u64(0x67ed228272f46eee) + assert s.e1 == u64(0x80bc0b097aad7944) + assert s.e2 == u64(0x2fa599382c6db215) + assert s.e3 == u64(0x368133fae2f7667a) + assert s.e4 == u64(0x28cefb195a7c651c) +} + +fn test_ascon_round_p12() { + mut s := State{ + e0: u64(0x0123456789abcdef) + e1: 0xef0123456789abcd + e2: 0xcdef0123456789ab + e3: 0xabcdef0123456789 + e4: 0x89abcdef01234567 + } + ascon_pnr(mut s, 12) + assert s.e0 == u64(0x206416dfc624bb14) + assert s.e1 == u64(0x1b0c47a601058aab) + assert s.e2 == u64(0x8934cfc93814cddd) + assert s.e3 == u64(0xa9738d287a748e4b) + assert s.e4 == u64(0xddd934f058afc7e1) +} diff --git a/vlib/x/crypto/ascon/cxof_test.v b/vlib/x/crypto/ascon/cxof_test.v new file mode 100644 index 000000000..55d23ceb3 --- /dev/null +++ b/vlib/x/crypto/ascon/cxof_test.v @@ -0,0 +1,145 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +import arrays +import encoding.hex +import x.crypto.ascon + +struct CxofTest { + count int + msg string + z string + md string +} + +// Known Answer Tests (KAT) with 512-bits output for Ascon-CXOF128 +fn test_cxof128() ! { + for item in cxof128_test_data { + msg := hex.decode(item.msg)! + z := hex.decode(item.z)! + md := hex.decode(item.md)! + + out := ascon.cxof128_64(msg, z)! + assert md == out + + // With Xof128Digest opaque + mut cx := ascon.new_cxof128(ascon.default_xof_size, z)! + md0 := cx.sum(msg) + assert md0 == md + + // test .read() + cx.reset() + mut dst := []u8{len: 64} + _ := cx.write(msg)! + nr := cx.read(mut dst)! + assert nr == 64 + assert dst == md + + // with chunked messages + cx.reset() + chunks := arrays.chunk[u8](msg, 20) + mut tot := 0 + for chunk in chunks { + n := cx.write(chunk)! + tot += n + } + assert msg.len == tot + + chunked_md := cx.sum([]u8{}) + assert chunked_md == md + } +} + +const cxof128_test_data = [ + CxofTest{ + count: 1 + msg: '' + z: '' + md: '4f50159ef70bb3dad8807e034eaebd44c4fa2cbbc8cf1f05511ab66cdcc529905ca12083fc186ad899b270b1473dc5f7ec88d1052082dcdfe69fb75d269e7b74' + }, + CxofTest{ + count: 2 + msg: '' + z: '10' + md: '0c93a483e7d574d49fe52cce03ee646117977d57a8aa57704ab4daf44b501430ff6ac11a5d1fd6f2154b5c65728268270c8bb578508487b8965718ada6272fd6' + }, + CxofTest{ + count: 3 + msg: '' + z: '1011' + md: 'd1106c7622e79fe955bd9d79e03b918e770fe0e0cddde28beb924b02c5fc936b33acca299c89eca5d71886cbbfa4d54a21c55fde2b679f5e2488063a1719dc32' + }, + CxofTest{ + count: 4 + msg: '' + z: '101112' + md: '6a53a6dbf1bec15a79ce1214ff76a4d6bb16f60cfa56bf2c218aec5e160372117d2a2e647b128624e9b1d2259faf083f2bedd0fc751a2e2ff268d0ee026b6449' + }, + CxofTest{ + count: 5 + msg: '' + z: '10111213' + md: 'cc333dd5b4ea61abe4376d61058b16df5eda7056299865ed7d25f43ac5b8541574608bd95ab0a3c3b74abd4abf9e50e63be6efe1b836b58595d8c47705c4dffb' + }, + CxofTest{ + count: 6 + msg: '' + z: '1011121314' + md: '8ee69d28b1bf3eafacf1e169fd10b6b7b72a7e2aaf0625e8e7c00153833b7224ed8c8c127b9808352c5647f9e862958d6de9eb93c4a236d59ecd84665e7164d9' + }, + CxofTest{ + count: 7 + msg: '' + z: '101112131415' + md: '3681695c40d83f60b401ecfa14bc03780ad474438f74b823eec9f0d5a375c13488803d3b4b8c8d4acd03186039f905fa15c7860dd0e9d566f31cd9e5822a937c' + }, + CxofTest{ + count: 8 + msg: '' + z: '10111213141516' + md: '717a9ba3b3c00f4078572b2d3fb3f0a86d45f70bc4e1cd89cb7a952bfa64162383735534ecbe0a7e62e7592cf447404db0361d98c2237245688ead15c05ae59b' + }, + CxofTest{ + count: 9 + msg: '' + z: '1011121314151617' + md: '61324766441dd6c11e1736bad1d2185820885ed76fe2ce537775a6e855eeafd2a6651b5e862a44982765f8b4c7cbe9c8b354f569ead6abc62cc9b7cdd72e0cb3' + }, + CxofTest{ + count: 10 + msg: '' + z: '101112131415161718' + md: '32fde6b9d290f56fc74aac9368f32c69973e1bab35d96118db7181aae577687673c01a9e35327aded556987eed3441d4f42ec36b0c198498d9e7f357b948d560' + }, + CxofTest{ + count: 11 + msg: '' + z: '10111213141516171819' + md: '690fc893055910d7d1d38055cf5589bbbe6b82bf175847ab3e0fd9a578b044dcb42be2932067eaa563a09e634581f34c2b4cfb38e1b06841b45b7b34c746d6dd' + }, + CxofTest{ + count: 12 + msg: '' + z: '101112131415161718191a' + md: 'ecdab5b15324f99a1709be26fc329d305bd475e5f39bc2b63788792166ad08fe720ccd14e0a4de7d83ede1c7744929dc509c73748d6661a3d3215995357d3f88' + }, + CxofTest{ + count: 13 + msg: '' + z: '101112131415161718191a1b' + md: 'ec2ba3309cfa6d6d0b581374e7c020ad17c330ea2b76d48724a415dceca3859c11146c2f64e52e44d27b1c44fd27476990a2e959b9998827a527a7e69089895f' + }, + CxofTest{ + count: 14 + msg: '' + z: '101112131415161718191a1b1c' + md: '659a59bc7fdece2f1bafa9f1bfb4c262043f74da550f85c902c9c4302adfcf898fbcd74c92d67bded153137e0d32ccba88767354be99103dfeb59c686ca98dea' + }, + CxofTest{ + count: 15 + msg: '' + z: '101112131415161718191a1b1c1d' + md: '17756044b742028b508d797c2c75a0722dde763c59d3fe5f70435b82faca5a80fe9c5ec9f3c59072ae48f37a241281c25d2e903c9d9290128265f1fe92b80bed' + }, +] diff --git a/vlib/x/crypto/ascon/digest.v b/vlib/x/crypto/ascon/digest.v new file mode 100644 index 000000000..0b9a6db98 --- /dev/null +++ b/vlib/x/crypto/ascon/digest.v @@ -0,0 +1,135 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +// Common helpers used by Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 +module ascon + +import encoding.binary + +// The Digest is an internal structure used by Ascon-hashing variants. +@[noinit] +struct Digest { + State +mut: + // buffer for storing unprocessed data for streaming-way + buf []u8 = []u8{len: block_size} + // length of leftover unprocessed bytes on the digest buffer + length int + + // internal flag + done bool +} + +// finish finalizes state by writing last block in the Digest internal buffer. +// and consequently updates Digest state. +@[direct_array_access; inline] +fn (mut d Digest) finish() { + if d.length >= d.buf.len { + panic('Digest.finish: internal error') + } + // Process for the last block stored on the internal buffer + d.State.e0 ^= pad(d.length) + d.State.e0 ^= load_bytes(d.buf[..d.length], d.length) + + // Permutation step was done in squeezing-phase + // ascon_pnr(mut d.State, 12) + + // zeroing Digest buffer + d.length = 0 + unsafe { d.buf.reset() } + + // After finishing phase, we can't write data anymore into state + d.done = true +} + +// absorb absorbs message msg_ into Digest state. +@[direct_array_access] +fn (mut d Digest) absorb(msg_ []u8) int { + // nothing to absorb, just return + if msg_.len == 0 { + return 0 + } + // Absorbing messages into Digest state working in streaming-way. + // Its continuesly updates internal state until you call `.finish` or `.free` it. + // Firstly, it would checking unprocessed bytes on internal buffer, and append it + // with bytes from messasge to align with block_size. + // And then absorb this buffered message into state. + // The process continues until the last partial block thats maybe below the block_size. + // If its happens, it will be stored on the Digest internal buffer for later processing. + mut msg := msg_.clone() + unsafe { + // Check if internal buffer has previous unprocessed bytes. + // If its on there, try to empty the buffer. + if d.length > 0 { + // There are bytes in the d.buf, append it with bytes taken from msg + if d.length + msg.len >= block_size { + n := copy(mut d.buf[d.length..], msg) + msg = msg[n..] + d.length += n + // If this d.buf length has reached block_size bytes, absorb it. + if d.length == block_size { + d.State.e0 ^= binary.little_endian_u64(d.buf) + ascon_pnr(mut d.State, 12) + // reset the internal buffer + d.length = 0 + d.buf.reset() + } + } else { + // Otherwise, still fit to buffer, but nof fully fills the d.buf + // just stores into buffer without processing + n := copy(mut d.buf[d.length..], msg) + msg = msg[n..] + d.length += n + } + } + // process for full block + for msg.len >= block_size { + d.State.e0 ^= binary.little_endian_u64(msg[0..block_size]) + msg = msg[block_size..] + ascon_pnr(mut d.State, 12) + } + // If there are partial block, just stored into buffer. + if msg.len > 0 { + n := copy(mut d.buf[d.length..], msg) + msg = msg[n..] + d.length += n + } + return msg_.len - msg.len + } +} + +// squeeze squeezes the state and calculates checksum output for the current state. +// It accepts destination buffer with desired buffer length you want to output. +@[direct_array_access; inline] +fn (mut d Digest) squeeze(mut dst []u8) int { + // nothing to store, just return unchanged + if dst.len == 0 { + return 0 + } + // check + if dst.len > max_hash_size { + panic('Digest.squeeze: invalid dst.len') + } + // The squeezing phase begins after msg is absorbed with an + // permutation π΄π‘ π‘π‘œπ‘›-𝑝[12] to the state: + ascon_pnr(mut d.State, 12) + + mut pos := 0 + mut clen := dst.len + // process for full block size + for clen >= block_size { + binary.little_endian_put_u64(mut dst[pos..pos + 8], d.State.e0) + ascon_pnr(mut d.State, 12) + pos += block_size + clen -= block_size + } + // final output, the resulting hash is the concatenation of hash blocks + store_bytes(mut dst[pos..], d.State.e0, clen) + pos += clen + + // for make sure, assert it here + assert pos == dst.len + + return pos +} diff --git a/vlib/x/crypto/ascon/examples/use_of_cxof.v b/vlib/x/crypto/ascon/examples/use_of_cxof.v new file mode 100644 index 000000000..36ffe5652 --- /dev/null +++ b/vlib/x/crypto/ascon/examples/use_of_cxof.v @@ -0,0 +1,42 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +import encoding.hex +import x.crypto.ascon + +// The material was generated from https://hashing.tools/ascon/ascon-hash +fn main() { + msg := 'Example of CXof128 message'.bytes() + cs := 'custom-string-cxof128'.bytes() + + // expected output generated from the tool, with 32, 64 dan 75-bytes output + digest32 := hex.decode('d71492b816b1ac27f53f9c13be45c1d2d0530b8dde7fde8d34cb563f79b3d3d3')! + digest64 := hex.decode('d71492b816b1ac27f53f9c13be45c1d2d0530b8dde7fde8d34cb563f79b3d3d3601d03474ec6fe1f6b8dc5dd79bea20aff4c95ca3549202b1aaeb9e66b5df398')! + digest75 := hex.decode('d71492b816b1ac27f53f9c13be45c1d2d0530b8dde7fde8d34cb563f79b3d3d3601d03474ec6fe1f6b8dc5dd79bea20aff4c95ca3549202b1aaeb9e66b5df3985a88fd8bce0f9570962321')! + + out32 := ascon.cxof128(msg, 32, cs)! + out64 := ascon.cxof128(msg, 64, cs)! + out75 := ascon.cxof128(msg, 75, cs)! + dump(out32 == digest32) // out32 == digest32: true + dump(out64 == digest64) // out64 == digest64: true + dump(out75 == digest75) // out75 == digest75: true + + // With object based + mut x := ascon.new_cxof128(32, cs)! + s32 := x.sum(msg) + dump(s32 == digest32) // s32 == digest32: true + + // with sized output + x.reset() + _ := x.write(msg)! + mut b64 := []u8{len: 64} + _ := x.read(mut b64)! + dump(b64 == digest64) // b64 == digest64: true + + x.reset() + _ := x.write(msg)! + mut b75 := []u8{len: 75} + _ := x.read(mut b75)! + dump(b75 == digest75) // b75 == digest75: true +} diff --git a/vlib/x/crypto/ascon/examples/use_of_hash26.v b/vlib/x/crypto/ascon/examples/use_of_hash26.v new file mode 100644 index 000000000..4facb44c3 --- /dev/null +++ b/vlib/x/crypto/ascon/examples/use_of_hash26.v @@ -0,0 +1,16 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +import encoding.hex +import x.crypto.ascon + +// The material was generated from https://hashing.tools/ascon/ascon-hash +fn main() { + msg := 'Example of hash256 message'.bytes() + // expected output generated from the tool + digest := hex.decode('0889515a9cfe28ab3a43882884d5933bb74aa09f3c767f8c699b5d7114811340')! + + out := ascon.sum256(msg) + dump(out == digest) // out == digest: true +} diff --git a/vlib/x/crypto/ascon/examples/use_of_xof.v b/vlib/x/crypto/ascon/examples/use_of_xof.v new file mode 100644 index 000000000..1d02f7e12 --- /dev/null +++ b/vlib/x/crypto/ascon/examples/use_of_xof.v @@ -0,0 +1,40 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +import encoding.hex +import x.crypto.ascon + +// The material was generated from https://hashing.tools/ascon/ascon-hash +fn main() { + msg := 'Example of Xof128 message'.bytes() + // expected output generated from the tool, with 32, 64 dan 75-bytes output + digest32 := hex.decode('424caaf68eb94aa251536bb6b565c0695b8944f932d011b1049df85b7f27d2f3')! + digest64 := hex.decode('424caaf68eb94aa251536bb6b565c0695b8944f932d011b1049df85b7f27d2f3bf704643a643c3f2dcfb1e0bc73ec55781b5283966d2d1da85d89794ca5c292e')! + digest75 := hex.decode('424caaf68eb94aa251536bb6b565c0695b8944f932d011b1049df85b7f27d2f3bf704643a643c3f2dcfb1e0bc73ec55781b5283966d2d1da85d89794ca5c292e36260815a8f10088e3804c')! + + out32 := ascon.xof128(msg, 32)! + out64 := ascon.xof128(msg, 64)! + out75 := ascon.xof128(msg, 75)! + dump(out32 == digest32) // out32 == digest32: true + dump(out64 == digest64) // out64 == digest64: true + dump(out75 == digest75) // out75 == digest75: true + + // With object based + mut x := ascon.new_xof128(32) + s32 := x.sum(msg) + dump(s32 == digest32) // s32 == digest32: true + + // with sized output + x.reset() + _ := x.write(msg)! + mut b64 := []u8{len: 64} + _ := x.read(mut b64)! + dump(b64 == digest64) // b64 == digest64: true + + x.reset() + _ := x.write(msg)! + mut b75 := []u8{len: 75} + _ := x.read(mut b75)! + dump(b75 == digest75) // b75 == digest75: true +} diff --git a/vlib/x/crypto/ascon/hash.v b/vlib/x/crypto/ascon/hash.v new file mode 100644 index 000000000..2ec0dda97 --- /dev/null +++ b/vlib/x/crypto/ascon/hash.v @@ -0,0 +1,130 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +// This file implement hashing routines based on Ascon-Hash256 schema defined in NIST SP 800-232 standard, +// Ascon-Hash256 hashing produces 256-bits output. +module ascon + +// block_size is the size (rate) of Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 operates on. +const block_size = 8 + +// hash256_size is the length of the Ascon-Hash256 checksum output, in bytes +const hash256_size = 32 + +// hash256_initial_state is a precomputed value for Ascon-Hash256 state. +// +// The 320-bit internal state of Ascon-Hash256 is initialized with the +// concatenation of the 64-bit 𝐼𝑉 = 0x0000080100cc0002 and 256 zeroes, followed +// by the π΄π‘ π‘π‘œπ‘›-𝑝[12] permutation as S ← π΄π‘ π‘π‘œπ‘›-𝑝[12](𝐼𝑉 βˆ₯0256). +// +// s.e0 = 0x0000080100cc0002 +// s.e1 = 0 +// s.e2 = 0 +// s.e3 = 0 +// s.e4 = 0 +// ascon_pnr(mut s, 12) +// +// Above step can be replaced with precomputed value to reduce runtime computations. +// See the detail on the NIST SP 800-232 standard on Sec A.3. Precomputation +// 𝑆0 ← 0x9b1e5494e934d681 +// 𝑆1 ← 0x4bc3a01e333751d2 +// 𝑆2 ← 0xae65396c6b34b81a +// 𝑆3 ← 0x3c7fd4a4d56a4db3 +// 𝑆4 ← 0x1a5c464906c5976d +// +const hash256_initial_state = State{ + e0: u64(0x9b1e5494e934d681) + e1: 0x4bc3a01e333751d2 + e2: 0xae65396c6b34b81a + e3: 0x3c7fd4a4d56a4db3 + e4: 0x1a5c464906c5976d +} + +// sum256 creates an Ascon-Hash256 checksum for bytes on msg and produces a 256-bit hash. +pub fn sum256(msg []u8) []u8 { + mut h := new_hash256() + _ := h.write(msg) or { panic(err) } + h.Digest.finish() + mut dst := []u8{len: hash256_size} + _ := h.Digest.squeeze(mut dst) + return dst +} + +// Hash256 is an opaque provides an implementation of Ascon-Hash256 from NIST.SP.800-232 standard. +// Its implements `hash.Hash` interface. +@[noinit] +pub struct Hash256 { + Digest +} + +// new_hash256 creates a new Ascon-Hash256 instance. +pub fn new_hash256() &Hash256 { + return &Hash256{ + Digest: Digest{ + State: hash256_initial_state + } + } +} + +// size returns an underlying size of Hash256 checksum, ie, 32-bytes +pub fn (h &Hash256) size() int { + return hash256_size +} + +// block_size returns an underlying Hash256 block size operates on, ie, 8-bytes +pub fn (h &Hash256) block_size() int { + return block_size +} + +// reset resets and reinit internal Hash256 state into default initialized state. +pub fn (mut h Hash256) reset() { + h.Digest.State = hash256_initial_state + unsafe { h.Digest.buf.reset() } + h.Digest.length = 0 + h.Digest.done = false +} + +// free releases out the resources taken by the `h`. Dont use x after .free call. +@[unsafe] +pub fn (mut h Hash256) free() { + $if prealloc { + return + } + unsafe { + h.Digest.buf.free() + } + // Mark it as unusable + h.Digest.done = true +} + +// write writes out the content of message and updates internal Hash256 state. +pub fn (mut h Hash256) write(msg []u8) !int { + if h.Digest.done { + panic('Digest: writing after done ') + } + return h.absorb(msg) +} + +// clone returns the clone of the current Hash256 +fn (h &Hash256) clone() &Hash256 { + digest := Digest{ + State: h.Digest.State + buf: h.Digest.buf.clone() + length: h.Digest.length + done: h.Digest.done + } + return &Hash256{digest} +} + +// sum returns an Ascon-Hash256 checksum of the bytes in data. +pub fn (mut h Hash256) sum(data []u8) []u8 { + // working on the clone of the h, so we can keep writing + mut h0 := h.clone() + _ := h0.write(data) or { panic(err) } + h0.Digest.finish() + mut dst := []u8{len: hash256_size} + _ := h0.Digest.squeeze(mut dst) + h0.reset() + return dst +} diff --git a/vlib/x/crypto/ascon/hash_test.v b/vlib/x/crypto/ascon/hash_test.v new file mode 100644 index 000000000..1ffc05f95 --- /dev/null +++ b/vlib/x/crypto/ascon/hash_test.v @@ -0,0 +1,169 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +module ascon + +import arrays +import encoding.hex + +struct HashTest { + count int + msg string + md string +} + +// This test material mostly taken and adapted from Known-Answer-Test (KAT) of reference implementation. +// See at https://github.com/ascon/ascon-c/blob/main/crypto_hash/asconhash256/LWC_HASH_KAT_128_256.txt +fn test_hash256_sum_chunked() ! { + item := HashTest{ + count: 500 + msg: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2' + md: '8174F5E8DFC9441DBB7A183D56F431B1AEF9513E747A3878BB806BAE27655E3E' + } + msg := hex.decode(item.msg)! + md := hex.decode(item.md)! + // One shoot sum256 function + md0 := sum256(msg) + assert md0 == md + + // Digest based + mut h := new_hash256() + expected_md := h.sum(msg) + assert expected_md == md + + // with multistep + h.reset() + _ := h.Digest.absorb(msg) + h.Digest.finish() + mut dst := []u8{len: hash256_size} + h.Digest.squeeze(mut dst) + assert dst == md + + // with splitted message + msg0 := msg[0..200] + msg1 := msg[200..400] + msg2 := msg[400..] + h.reset() + _ := h.Digest.absorb(msg0) + _ := h.Digest.absorb(msg1) + _ := h.Digest.absorb(msg2) + h.Digest.finish() + h.Digest.squeeze(mut dst) + assert dst == md + + // with arrays chunk + h.reset() + chunks := arrays.chunk[u8](msg, 200) + mut n := 0 + for chunk in chunks { + n += h.Digest.absorb(chunk) + } + assert n == msg.len + h.Digest.finish() + h.Digest.squeeze(mut dst) + assert dst == md + + // with sum + h.reset() + for chunk in chunks { + n += h.write(chunk)! + } + chunked_md := h.sum([]u8{}) + assert chunked_md == md +} + +fn test_hash256_sum_kat() ! { + for item in ascon_hash256_test_data { + msg := hex.decode(item.msg)! + md := hex.decode(item.md)! + out := sum256(msg) + assert out == md + + // work with Digest opaque + mut h := new_hash256() + exp_md := h.sum(msg) + assert exp_md == md + + // Lets work in streaming-way + chunks := arrays.chunk[u8](msg, 7) + h.reset() + mut tot := 0 + for chunk in chunks { + n := h.write(chunk)! + tot += n + } + assert msg.len == tot + + chunked_md := h.sum([]u8{}) + assert chunked_md == md + } +} + +const ascon_hash256_test_data = [ + HashTest{ + count: 1 + msg: '' + md: '0b3be5850f2f6b98caf29f8fdea89b64a1fa70aa249b8f839bd53baa304d92b2' + }, + HashTest{ + count: 2 + msg: '00' + md: '0728621035af3ed2bca03bf6fde900f9456f5330e4b5ee23e7f6a1e70291bc80' + }, + HashTest{ + count: 3 + msg: '0001' + md: '6115e7c9c4081c2797fc8fe1bc57a836afa1c5381e556dd583860ca2dfb48dd2' + }, + HashTest{ + count: 4 + msg: '000102' + md: '265ab89a609f5a05dca57e83fbba700f9a2d2c4211ba4cc9f0a1a369e17b915c' + }, + HashTest{ + count: 5 + msg: '00010203' + md: 'd7e4c7ed9b8a325cd08b9ef259f8877054ecd8304fe1b2d7fd847137df6727ee' + }, + HashTest{ + count: 6 + msg: '0001020304' + md: 'c7b28962d4f5c2211f466f83d3c57ae1504387e2a326949747a8376447a6bb51' + }, + HashTest{ + count: 7 + msg: '000102030405' + md: 'dc0c6748af8ffe63e1084aa3e5786a194685c88c21348b29e184fb50409703bc' + }, + HashTest{ + count: 8 + msg: '00010203040506' + md: '3e4d273ba69b3b9c53216107e88b75cdbeedbcbf8faf0219c3928ab62b116577' + }, + HashTest{ + count: 9 + msg: '0001020304050607' + md: 'b88e497ae8e6fb641b87ef622eb8f2fca0ed95383f7ffebe167acf1099ba764f' + }, + HashTest{ + count: 10 + msg: '000102030405060708' + md: '94269C30E0296E1EC86655041841823EFA1927F520FD58C8E9BCE6197878C1A6' + }, + HashTest{ + count: 500 + msg: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2' + md: '8174F5E8DFC9441DBB7A183D56F431B1AEF9513E747A3878BB806BAE27655E3E' + }, + HashTest{ + count: 501 + msg: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3' + md: 'E73D4DDB9D248BF2C0F8D49892D7455A4C3053153DE7F79BA4487C7D823F605C' + }, + HashTest{ + count: 502 + msg: '000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4F5F6F7F8F9FAFBFCFDFEFF000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F202122232425262728292A2B2C2D2E2F303132333435363738393A3B3C3D3E3F404142434445464748494A4B4C4D4E4F505152535455565758595A5B5C5D5E5F606162636465666768696A6B6C6D6E6F707172737475767778797A7B7C7D7E7F808182838485868788898A8B8C8D8E8F909192939495969798999A9B9C9D9E9FA0A1A2A3A4A5A6A7A8A9AAABACADAEAFB0B1B2B3B4B5B6B7B8B9BABBBCBDBEBFC0C1C2C3C4C5C6C7C8C9CACBCCCDCECFD0D1D2D3D4D5D6D7D8D9DADBDCDDDEDFE0E1E2E3E4E5E6E7E8E9EAEBECEDEEEFF0F1F2F3F4' + md: 'B73FA04079263F6A733B67466552784B436138F41F80B72C4D5D03934B72207D' + }, +] diff --git a/vlib/x/crypto/ascon/util.v b/vlib/x/crypto/ascon/util.v new file mode 100644 index 000000000..dd123e4a8 --- /dev/null +++ b/vlib/x/crypto/ascon/util.v @@ -0,0 +1,107 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +// Utility helpers used across the module +module ascon + +import math.bits +import encoding.binary + +// rotate_right_64 rotates x right by k bits +fn rotate_right_64(x u64, k int) u64 { + // call rotate_left_64(x, -k). + return bits.rotate_left_64(x, -k) +} + +// clear_bytes clears the bytes of x in n byte +@[inline] +fn clear_bytes(x u64, n int) u64 { + mut c := x + for i := 0; i < n; i++ { + c &= ~set_byte(0xff, i) + } + return c +} + +// pad appends a one followed by one or more zeroes to data +@[inline] +fn pad(n int) u64 { + return u64(0x01) << (8 * n) +} + +// assume input.len < 8 +fn u64_from_partial_bytes(input []u8) u64 { + mut tmp := []u8{len: 8} + ct_copy_first(mut tmp, input) + return binary.little_endian_u64(tmp) +} + +// ct_copy_at copies of y into x start from at position in constant-time manner. +fn ct_copy_at(mut x []u8, y []u8, at int) { + ct_copy_internal(1, mut x, y, at) +} + +// ct_copy_first copies of y into first y.len of x in constant-time manner. +fn ct_copy_first(mut x []u8, y []u8) { + ct_copy_internal(1, mut x, y, 0) +} + +@[direct_array_access] +fn ct_copy_internal(v int, mut x []u8, y []u8, at int) { + if at > x.len { + panic('at > x.len') + } + if i64(x.len) < i64(y.len) + i64(at) { + panic('invalid pos') + } + if x.len < y.len { + panic('length x < y') + } + xmask := u8(v - 1) + ymask := u8(~(v - 1)) + for i := 0; i < y.len; i++ { + x[i + at] = x[i + at] & xmask | y[i] & ymask + } +} + +// portable little-endian helper +@[inline] +fn u64le(x u64) u64 { + $if little_endian { + return x + } + // otherwise, change into little-endian format + return ((u64(0x00000000000000FF) & x) << 56) | ((u64(0x000000000000FF00) & x) << 40) | ((u64(0x0000000000FF0000) & x) << 24) | ((u64(0x00000000FF000000) & x) << 8) | ((u64(0x000000FF00000000) & x) >> 8) | ((u64(0x0000FF0000000000) & x) >> 24) | ((u64(0x00FF000000000000) & x) >> 40) | ((u64(0xFF00000000000000) & x) >> 56) +} + +@[inline] +fn get_byte(x u64, i int) u8 { + return u8(x >> (8 * i)) +} + +@[inline] +fn set_byte(b u8, i int) u64 { + return u64(b) << (8 * i) +} + +// load_bytes load partial bytes with length n, used internally. +@[direct_array_access] +fn load_bytes(bytes []u8, n int) u64 { + mut x := u64(0) + for i := 0; i < n; i++ { + x |= set_byte(bytes[i], i) + } + return u64le(x) +} + +fn store_bytes(mut out []u8, x u64, n int) { + for i := 0; i < n; i++ { + out[i] = get_byte(x, i) + } +} + +@[inline] +fn ascon_rotate_right(x u64, n int) u64 { + return (x >> n) | x << (64 - n) +} diff --git a/vlib/x/crypto/ascon/xof.v b/vlib/x/crypto/ascon/xof.v new file mode 100644 index 000000000..c6d48ce14 --- /dev/null +++ b/vlib/x/crypto/ascon/xof.v @@ -0,0 +1,327 @@ +// 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 Ascon-XOF128, an Ascon-based Extendable-output Function (XOF) +// and their variant, Ascon-CXOF128 from NIST.SP.800-232 standard. +// Ascon-XOF128 is similar to Ascon-Hash256 where Ascon-XOF128 accepts an additional input +// which specifies the desired output length in bits. +module ascon + +import encoding.binary + +// max_hash_size is a maximum size of Ascon-XOF128 (and Ascon-CXOF128) checksum output. +// Its very rare where checksum output bigger than 512-bits, So, we limiting it to prevent unconditional thing. +// This limitation was only occurs on this module, wee can change it later. +pub const max_hash_size = 4096 // in bytes + +// default_xof_size is the size of Ascon-XOF128 (and Ascon-CXOF128) checksum that has 512-bits length. +pub const default_xof_size = 64 + +// xof128_initial_state is a precomputed value for Ascon-XOF128 state. +// See the comment on `hash256_initial_state` about the values +const xof128_initial_state = State{ + e0: 0xda82ce768d9447eb + e1: 0xcc7ce6c75f1ef969 + e2: 0xe7508fd780085631 + e3: 0x0ee0ea53416b58cc + e4: 0xe0547524db6f0bde +} + +// xof128 creates an Ascon-XOF128 checksum of msg with specified desired size of output. +pub fn xof128(msg []u8, size int) ![]u8 { + mut x := new_xof128(size) + _ := x.write(msg)! + x.Digest.finish() + mut out := []u8{len: size} + _ := x.Digest.squeeze(mut out) + x.reset() + return out +} + +// xof128_64 creates a 64-bytes of Ascon-XOF128 checksum of msg. +pub fn xof128_64(msg []u8) ![]u8 { + return xof128(msg, default_xof_size)! +} + +// Xof128 is an opaque provides an implementation of Ascon-XOF128 from NIST.SP.800-232 standard. +// Its implements `hash.Hash` interface with checksum size stored on instance creation. +@[noinit] +pub struct Xof128 { + Digest +mut: + // The size of Xof128 cheksum, when you dont specify it + size int = default_xof_size +} + +// new_xof128 creates a new Ascon-XOF128 instance with provided size parameter. +pub fn new_xof128(size int) &Xof128 { + if size < 1 || size > max_hash_size { + panic('new_xof128: invalid size') + } + return &Xof128{ + Digest: Digest{ + State: xof128_initial_state + } + size: size + } +} + +// size returns an underlying size of Xof128 checksum in fixed-sized manner. +// Internally, its return underlying size stored on current Xof128 instance. +pub fn (x &Xof128) size() int { + return x.size +} + +// block_size returns an underlying Xof128 block size operates on, ie, 8-bytes +pub fn (x &Xof128) block_size() int { + return block_size +} + +// clone returns a clone of x on the current state. +fn (x &Xof128) clone() &Xof128 { + return &Xof128{ + Digest: x.Digest + size: x.size + } +} + +// write writes out the content of message and updates internal Xof128 state. +pub fn (mut x Xof128) write(msg []u8) !int { + if x.Digest.done { + panic('Digest: writing after done ') + } + return x.Digest.absorb(msg) +} + +// sum returns an Ascon-XOF128 checksum of the bytes in msg. +// Its produces `x.size` of checksum bytes. If you want to more +// extendible output, use `.read` call instead. +pub fn (mut x Xof128) sum(msg []u8) []u8 { + // working on the clone of the h, so we can keep writing + mut x0 := x.clone() + _ := x0.write(msg) or { panic(err) } + x0.Digest.finish() + mut dst := []u8{len: x.size} + x0.Digest.squeeze(mut dst) + x0.reset() + return dst +} + +// read tries to read `dst.len` bytes of hash output from current Xof128 state and stored into dst. +// Note: 1 ≀ dst.len ≀ max_hash_size. +pub fn (mut x Xof128) read(mut dst []u8) !int { + // Left unchanged + if dst.len == 0 { + return 0 + } + // Check output size + if dst.len > max_hash_size { + panic('Xof128.read: invalid size') + } + mut x0 := x.clone() + x0.Digest.finish() + n := x0.Digest.squeeze(mut dst) + x0.reset() + return n +} + +// reset resets internal Xof128 state into default initialized state. +pub fn (mut x Xof128) reset() { + // we dont reset the x.size + unsafe { x.Digest.buf.reset() } + x.Digest.length = 0 + x.Digest.done = false + x.Digest.State = xof128_initial_state +} + +// free releases out the resources taken by the `x`. Dont use x after .free call. +@[unsafe] +pub fn (mut x Xof128) free() { + $if prealloc { + return + } + unsafe { + x.Digest.buf.free() + } + // Mark it as unusable + x.Digest.done = true +} + +// Ascon-CXOF128 +// +// Ascon-CXOF128 is customized variant of Ascon-XOF128 that extends its +// functionality by incorporating a user-defined customization string. +// In addition to the message 𝑀 and output length 𝐿, Ascon-CXOF128 takes +// the customization string 𝑍 as input. + +// The length of the customization string shall be at most 2048 bits (i.e., 256 bytes) +const max_cxof128_cstring = 256 + +// cxof128_initial_state is a precomputed value for Ascon-CXOF128 state. +// See the comment on `hash256_initial_state` about the values +const cxof128_initial_state = State{ + e0: 0x675527c2a0e8de03 + e1: 0x43d12d7dc0377bbc + e2: 0xe9901dec426e81b5 + e3: 0x2ab14907720780b6 + e4: 0x8f3f1d02d432bc46 +} + +// cxof128 creates an Ascon-CXOF128 checksum of msg with supplied size and custom string cs. +pub fn cxof128(msg []u8, size int, cs []u8) ![]u8 { + mut cx := new_cxof128(size, cs)! + _ := cx.write(msg)! + cx.Digest.finish() + mut out := []u8{len: size} + _ := cx.Digest.squeeze(mut out) + cx.reset() + return out +} + +// cxof128_64 creates a 64-bytes of Ascon-CXOF128 checksum of msg with supplied custom string in cs. +pub fn cxof128_64(msg []u8, cs []u8) ![]u8 { + return cxof128(msg, default_xof_size, cs)! +} + +// CXof128 is an opaque provides an implementation of Ascon-CXOF128 from NIST.SP.800-232 standard. +// Its implements `hash.Hash` interface with checksum-size stored on instance creation. +@[noinit] +pub struct CXof128 { + Digest +mut: + // Customization string + cs []u8 + // checksum size, for fixed-output + size int = default_xof_size +} + +// new_cxof128 creates a new Ascon-CXOF128 instanace with cheksum size +// was set to size and custom string in cs. It returns error on fails. +pub fn new_cxof128(size int, cs []u8) !&CXof128 { + if cs.len > max_cxof128_cstring { + return error('CXof128: custom string length exceed limit') + } + if size < 1 || size > max_hash_size { + return error('CXof128: invalid size') + } + // Initialize CXof128 state with precomputed-value and absorb the customization string + mut s := cxof128_initial_state + cxof128_absorb_custom_string(mut s, cs) + + return &CXof128{ + Digest: Digest{ + State: s + } + cs: cs + size: size + } +} + +// size returns an underlying size of CXof128 checksum in fixed-sized manner. +// Internally, its return underlying size stored on current CXof128 instance. +pub fn (x &CXof128) size() int { + return x.size +} + +// block_size returns an underlying CXof128 block size operates on, ie, 8-bytes +pub fn (x &CXof128) block_size() int { + return block_size +} + +// write writes out the content of message and updates internal CXof128 state. +pub fn (mut x CXof128) write(msg []u8) !int { + if x.Digest.done { + panic('CXof128: writing after done ') + } + return x.Digest.absorb(msg) +} + +// sum returns an Ascon-CXOF128 checksum of the bytes in msg. +// Its produces `x.size` of checksum bytes. If you want to more +// extendible output, use `.read` call instead. +pub fn (mut x CXof128) sum(msg []u8) []u8 { + // working on the clone of the h, so we can keep writing + mut x0 := x.clone() + _ := x0.write(msg) or { panic(err) } + x0.Digest.finish() + mut dst := []u8{len: x.size} + x0.Digest.squeeze(mut dst) + x0.reset() + return dst +} + +// read tries to read `dst.len` bytes of hash output from current CXof128 state and stored into dst. +pub fn (mut x CXof128) read(mut dst []u8) !int { + // Left unchanged, nothing space to store checksum + if dst.len == 0 { + return 0 + } + if dst.len > max_hash_size { + panic('CXof128.read: invalid size') + } + mut x0 := x.clone() + x0.Digest.finish() + n := x0.Digest.squeeze(mut dst) + x0.reset() + return n +} + +// clone returns a clone of x on the current state. +fn (x &CXof128) clone() &CXof128 { + return &CXof128{ + Digest: x.Digest + size: x.size + cs: x.cs + } +} + +// reset resets internal CXof128 state into default initialized state. +pub fn (mut x CXof128) reset() { + // we dont reset the x.size and custom string + unsafe { x.Digest.buf.reset() } + x.Digest.length = 0 + x.Digest.done = false + x.Digest.State = cxof128_initial_state + // reabsorbs custom string + cxof128_absorb_custom_string(mut x.Digest.State, x.cs) +} + +// free releases out the resources taken by the `x`. Dont use x after .free call. +@[unsafe] +pub fn (mut x CXof128) free() { + $if prealloc { + return + } + unsafe { + x.Digest.buf.free() + } + // Mark it as unusable + x.Digest.done = true +} + +// cxof128_absorb_custom_string performs absorbing phase of custom string in cs for Ascon-CXOF128. +@[direct_array_access; inline] +fn cxof128_absorb_custom_string(mut s State, cs []u8) { + // absorb Z0, the length of the customization string (in bits) encoded as a u64 + s.e0 ^= u64(cs.len) << 3 + ascon_pnr(mut s, 12) + + // absorb the customization string + mut zlen := cs.len + mut zidx := 0 + for zlen >= block_size { + block := unsafe { cs[zidx..zidx + block_size] } + s.e0 ^= binary.little_endian_u64(block) + ascon_pnr(mut s, 12) + + // updates a index + zlen -= block_size + zidx += block_size + } + // absorb final customization string + last_block := unsafe { cs[zidx..] } + s.e0 ^= load_bytes(last_block, last_block.len) + s.e0 ^= pad(last_block.len) + ascon_pnr(mut s, 12) +} diff --git a/vlib/x/crypto/ascon/xof_test.v b/vlib/x/crypto/ascon/xof_test.v new file mode 100644 index 000000000..0832bfc3e --- /dev/null +++ b/vlib/x/crypto/ascon/xof_test.v @@ -0,0 +1,112 @@ +// Copyright Β©2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +import arrays +import encoding.hex +import x.crypto.ascon + +// This test for Xof128 with 512-bits outputs +struct XofTest { + count int + msg string + md string +} + +fn test_xof128() ! { + for item in xof128_test_data { + msg := hex.decode(item.msg)! + md := hex.decode(item.md)! + out := ascon.xof128_64(msg)! + assert out == md + + // With Xof128Digest opaque + mut x := ascon.new_xof128(ascon.default_xof_size) + md0 := x.sum(msg) + assert md0 == md + + // test .read() + x.reset() + mut dst := []u8{len: 64} + _ := x.write(msg)! + nr := x.read(mut dst)! + assert nr == 64 + assert dst == md + + // with chunked messages + x.reset() + chunks := arrays.chunk[u8](msg, 20) + mut tot := 0 + for chunk in chunks { + n := x.write(chunk)! + tot += n + } + assert msg.len == tot + + chunked_md := x.sum([]u8{}) + assert chunked_md == md + } +} + +const xof128_test_data = [ + XofTest{ + count: 1 + msg: '' + md: '473d5e6164f58b39dfd84aacdb8ae42ec2d91fed33388ee0d960d9b3993295c6ad77855a5d3b13fe6ad9e6098988373af7d0956d05a8f1665d2c67d1a3ad10ff' + }, + XofTest{ + count: 2 + msg: '00' + md: '51430e0438ecdf642b393630d977625f5f337656ba58ab1e960784ac32a16e0d446405551f5469384f8ea283cf12e64fa72c426bfebaea3aa1529e2c4ab23a2f' + }, + XofTest{ + count: 3 + msg: '0001' + md: 'a05383077af971d3830bd37e7b981497a773d441db077c6494cc73125953846eb6427fba4cd308ff90a11385d51101341bf5379249217bfdace9cca1148cc966' + }, + XofTest{ + count: 4 + msg: '000102' + md: '9c96f31c3e7bdfdc5ef6ba836f760a0d6548d94dd0a512033022c9242e8ba916c30c3961d37d7dd7282e2191494d60dc5058588b276c60c90be2aaa7e7013d96' + }, + XofTest{ + count: 5 + msg: '00010203' + md: '21f7fd74588e244af45f9016b8db19b857ec5e6208978cfc1b4611ed91fb38f87e8f82a6409fb2b77acfbba8862aa22a7b0c98c1c01d5a4fdf64827b450fa1eb' + }, + XofTest{ + count: 6 + msg: '0001020304' + md: 'd647cc91aafff06a486f00a33fdfe9222f08b94da3b17804da9aaae167b4285dd6395e2a61fded3cf73c99774aff7066f74f7698f4824ba538602087d7c267fa' + }, + XofTest{ + count: 7 + msg: '000102030405' + md: '4793fbe6aa7688e52cd3a97a2685c68b218e0ca8754307956509974ab107d8ba19070424d5dfd336c3fc1250a273b9146f9f26d7658b9e213c37aebbe74abc6e' + }, + XofTest{ + count: 8 + msg: '00010203040506' + md: '7ae562db37212a9acd2673ecfd5b4f1c5cb2e6f64ebf00aa7f6ef8dc82c448d5fe11cd91f4368c37690d79e5de0ca8ad419e1918ce8dab2d42363e9476638a7b' + }, + XofTest{ + count: 9 + msg: '0001020304050607' + md: '8d1886f5d3ec4af8d15b44bc62b74da6ea91bc28fb82f9c34079b5ed6e38b6c951803d7dfb3c5e512a0ef5e4060062a6fd067f9c73ef9bee527411bda67fc896' + }, + XofTest{ + count: 10 + msg: '000102030405060708' + md: 'db3013bfbbd132dc1d3152fd955ed48f7cbb675e9ad2a2fecf92b74c957592e0c89959e81c16fd07ead9eeb8e40359c497aa20258b43d87ec69ad0bb0993fd38' + }, + XofTest{ + count: 601 + msg: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f5051525354555657' + md: '8fbbce48a72faf257e06df9408d4b6e11d35a86dfc29c0f106cb86128a2fe94208dcc8df4a439978cdb77f53317cbe5507b7f33da590be10a424d4ef60b37036' + }, + XofTest{ + count: 602 + msg: '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebfc0c1c2c3c4c5c6c7c8c9cacbcccdcecfd0d1d2d3d4d5d6d7d8d9dadbdcdddedfe0e1e2e3e4e5e6e7e8e9eaebecedeeeff0f1f2f3f4f5f6f7f8f9fafbfcfdfeff000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f404142434445464748494a4b4c4d4e4f505152535455565758' + md: 'b4ca30047dc94ed770e45f4fa07b25a049d4a149a78227e25625cec2cbee7cfdd7d3cfb2635a38770e04009de348726364f648984ef47e9bc90c2a3c0526b227' + }, +] -- 2.39.5