From 152001ef817c3dc4576ed950308f101ae0811115 Mon Sep 17 00:00:00 2001 From: blackshirt Date: Thu, 11 Sep 2025 05:30:06 +0000 Subject: [PATCH] x.crypto.ascon: improves one-shot of ascon hasing functions --- vlib/x/crypto/ascon/README.md | 2 +- vlib/x/crypto/ascon/bench/aead.v | 75 +++++++++++ vlib/x/crypto/ascon/bench/hashxof.v | 197 ++++++++++++++++++++++++++++ vlib/x/crypto/ascon/bench/sum.v | 36 +++++ vlib/x/crypto/ascon/digest.v | 36 +++++ vlib/x/crypto/ascon/hash.v | 14 +- vlib/x/crypto/ascon/util.v | 1 - vlib/x/crypto/ascon/xof.v | 23 ++-- 8 files changed, 361 insertions(+), 23 deletions(-) create mode 100644 vlib/x/crypto/ascon/bench/aead.v create mode 100644 vlib/x/crypto/ascon/bench/hashxof.v create mode 100644 vlib/x/crypto/ascon/bench/sum.v diff --git a/vlib/x/crypto/ascon/README.md b/vlib/x/crypto/ascon/README.md index 96faf63c9c..acd03e933d 100644 --- a/vlib/x/crypto/ascon/README.md +++ b/vlib/x/crypto/ascon/README.md @@ -7,7 +7,7 @@ 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 mostly implements all the features availables on the document. -Its currently implements: +Its currently implements: - `Ascon-Hash256`, Ascon-based hashing implementation that produces 256-bits output. - `Ascon-XOF128`, Ascon-based eXtendable Output Function (XOF) where the output size of the hash of the message can be selected by the user. diff --git a/vlib/x/crypto/ascon/bench/aead.v b/vlib/x/crypto/ascon/bench/aead.v new file mode 100644 index 0000000000..c7d1cd1043 --- /dev/null +++ b/vlib/x/crypto/ascon/bench/aead.v @@ -0,0 +1,75 @@ +// This benchmark is for Ascon-AEAD128 in `x.crypto.ascon` compared to +// already stocked `x.crypto.cacha20poly1305 for AEAD functionalities. +// +// Here is output in my tests, first item was `x.crypto.ascon` and the later +// for `x.crypto.chacha20poly1305` on encryption or decryption part. +// +// Encryption.. +// ----------- +// Iterations: 10000 Total Duration: 26.008ms ns/op: 2600 B/op: 16 allocs/op: 17 +// Iterations: 10000 Total Duration: 158.865ms ns/op: 15886 B/op: 16 allocs/op: 16 +// +// Decryption.. +// ----------- +// Iterations: 10000 Total Duration: 29.091ms ns/op: 2909 B/op: 6 allocs/op: 8 +// Iterations: 10000 Total Duration: 158.373ms ns/op: 15837 B/op: 8 allocs/op: 12 +// +import encoding.hex +import x.benchmark +import x.crypto.ascon +import x.crypto.chacha20poly1305 + +// randomly generated key and nonce, 16-bytes of ascon key and 32-bytes of chacha20poly1305 key. +const key_ascon = hex.decode('7857bfb462c654d1d1b02971be021235')! +const key_cpoly = hex.decode('9d9603f4fc460e273b80795ea50eab5873c04f589226c7d591b5336feb32fcba')! + +// 16-bytes ascon-nonce +const ascon_nonce = hex.decode('8b521028fb54591472d8d8ee14430835')! + +// 12-bytes chacha20poly1305 nonce +const cpoly_nonce = hex.decode('9a3c83e4236ea9a2c4e482da')! + +const ad = 'Ascon-AEAD128 additional data'.bytes() +const msg = 'Ascon-AEAD128 benchmarking message'.bytes() + +// expected ciphertext for aead128 := 4b21a18cbca65b11aaf73dc74241c89bfcec96a4c8973ae696a938e0a591e846c4eb7b2906664f2318c0fd6ec1c56424aa9b +const ciphertext_aead128 = hex.decode('4b21a18cbca65b11aaf73dc74241c89bfcec96a4c8973ae696a938e0a591e846c4eb7b2906664f2318c0fd6ec1c56424aa9b')! + +fn bench_ascon_aead128_encrypt() ! { + _ := ascon.encrypt(key_ascon, ascon_nonce, ad, msg)! +} + +fn bench_ascon_aead128_decrypt() ! { + _ := ascon.decrypt(key_ascon, ascon_nonce, ad, ciphertext_aead128)! +} + +// expected ciphertext for chacha20poly1305 +const ciphertext_chachapoly1305 = hex.decode('67dea3c65f0f326bcf587f024140a85d9535790d9b16129210a2289eda43bb9b62746450026fc1baf466bcb8a181843cd424')! + +fn bench_chacha20poly1305_encrypt() ! { + _ := chacha20poly1305.encrypt(msg, key_cpoly, cpoly_nonce, ad)! +} + +fn bench_chacha20poly1305_decrypt() ! { + _ := chacha20poly1305.decrypt(ciphertext_chachapoly1305, key_cpoly, cpoly_nonce, ad)! +} + +fn main() { + cf := benchmark.BenchmarkDefaults{ + n: 10000 + } + println('Encryption..') + println('-----------') + mut b0 := benchmark.setup(bench_ascon_aead128_encrypt, cf)! + b0.run() + mut b1 := benchmark.setup(bench_chacha20poly1305_encrypt, cf)! + b1.run() + + println('') + println('Decryption..') + println('-----------') + mut b2 := benchmark.setup(bench_ascon_aead128_decrypt, cf)! + b2.run() + mut b3 := benchmark.setup(bench_chacha20poly1305_decrypt, cf)! + b3.run() +} diff --git a/vlib/x/crypto/ascon/bench/hashxof.v b/vlib/x/crypto/ascon/bench/hashxof.v new file mode 100644 index 0000000000..6b7ec6ce54 --- /dev/null +++ b/vlib/x/crypto/ascon/bench/hashxof.v @@ -0,0 +1,197 @@ +// Ascon-Hash256 (and Ascon-XOF128) benchmark compared to builtin +// crypto.sha256 (for sum256) and sha3.shake256 (for xof outputing 256-bits) +// +// This benchmark code was adapted from argon2 benchmark by @fleximus, the creator argon2 module. +// Credit tributed to @fleximus +// See https://gist.github.com/fleximus/db5b867a9a37da46340db61bdac6e696 +// +// Output +// ====== +// Sum and Xof 256-bits output performance comparison +// ============================================================ +// Iterations per test: 10000 +// -------------------------------------------------------------------------------------------------- +// Data Size | Ascon256 | Sha256 | Ratio 256 || AsconXof128 | Shake256 | Ratio (Xof) | +// -------------------------------------------------------------------------------------------------- +// 4 B | 24.00ms | 33.00ms | 0.73x || 24.00ms | 208.00ms | 0.12x | +// 6 B | 23.00ms | 53.00ms | 0.45x || 25.00ms | 287.00ms | 0.08x | +// 8 B | 35.00ms | 37.00ms | 0.95x || 26.00ms | 202.00ms | 0.18x | +// 16 B | 30.00ms | 37.00ms | 0.83x || 30.00ms | 205.00ms | 0.15x | +// 64 B | 55.00ms | 61.00ms | 0.89x || 53.00ms | 241.00ms | 0.23x | +// 75 B | 61.00ms | 57.00ms | 1.07x || 58.00ms | 182.00ms | 0.34x | +// 256 B | 154.00ms | 123.00ms | 1.25x || 144.00ms | 398.00ms | 0.39x | +// 512 B | 273.00ms | 216.00ms | 1.26x || 265.00ms | 779.00ms | 0.35x | +// 1025 B | 610.00ms | 401.00ms | 1.52x || 509.00ms | 1.37s | 0.45x | +// -------------------------------------------------------------------------------------------------- +// Total | 1.27s | 1.02s | 1.24x || 1.14s | 3.87s | 0.294x| +// -------------------------------------------------------------------------------------------------- +// +// Per-operation averages: +// Ascon256: 14108 ns per hash +// Sha256: 11360 ns per hash +// AsconXof128: 12648 ns per hash +// Shake256: 43036 ns per hash +// +module main + +import time +import crypto.sha3 +import crypto.sha256 +import x.crypto.ascon + +const benchmark_iterations = 10000 + +// We include more small size because, Ascon-Hash256 working with more smaller block size. +const test_data_sizes = [ + 4, // below Ascon-Hash256 block size + 6, // Still below Ascon-Hash256 block size + 8, // align with Ascon-Hash256 block size + 16, // Small data + 64, // Medium data + 75, // above 64-bytes block + 256, // Large data + 512, + 1025, +] + +fn generate_test_data(size int) []u8 { + mut data := []u8{len: size} + for i in 0 .. size { + data[i] = u8(i % 256) + } + return data +} + +fn benchmark_ascon_sha256(data []u8, iterations int) time.Duration { + start := time.now() + for _ in 0 .. iterations { + _ := ascon.sum256(data) + } + return time.since(start) +} + +fn benchmark_sha256_sum256(data []u8, iterations int) time.Duration { + start := time.now() + for _ in 0 .. iterations { + _ := sha256.sum256(data) + } + return time.since(start) +} + +// for eXtendable output functions (XOF) +fn benchmark_ascon_xof128_32(data []u8, iterations int) time.Duration { + start := time.now() + for _ in 0 .. iterations { + _ := ascon.xof128(data, 32) or { panic(err) } + } + return time.since(start) +} + +fn benchmark_sha3_shake256(data []u8, iterations int) time.Duration { + start := time.now() + for _ in 0 .. iterations { + _ := sha3.shake256(data, 32) + } + return time.since(start) +} + +fn format_duration(d time.Duration) string { + if d.microseconds() < 1000 { + return '${d.microseconds():6}μs' + } else if d.milliseconds() < 1000 { + return '${f64(d.milliseconds()):6.2f}ms' + } else { + return '${f64(d.seconds()):6.2f}s' + } +} + +const data_title = 'Data Size' +const ascon_sum256_title = 'Ascon256' +const sha256_title = 'Sha256' +const ascon_xof128_title = 'AsconXof128' +const sha3_shake256_title = 'Shake256' +const ratio_ascon256_w_sha256 = 'Ratio 256' +const ratio_asconxof128_w_shake256 = 'Ratio (Xof)' + +fn main() { + println('') + println('Sum and Xof 256-bits output performance comparison') + println('============================================================') + println('Iterations per test: ${benchmark_iterations}') + + println('${'-'.repeat(98)}') + println('${data_title:12} | ${ascon_sum256_title:10} | ${sha256_title:10} | ${ratio_ascon256_w_sha256:12} || ${ascon_xof128_title:10} | ${sha3_shake256_title:10} | ${ratio_asconxof128_w_shake256:12} |') + println('${'-'.repeat(98)}') + + mut total_ascon256 := time.Duration(0) + mut total_sha256 := time.Duration(0) + mut total_shake256 := time.Duration(0) + mut total_asconxof128 := time.Duration(0) + + for size in test_data_sizes { + test_data := generate_test_data(size) + + // Warm up + _ := ascon.sum256(test_data) + _ := sha256.sum256(test_data) + + _ := ascon.xof128(test_data, 32)! + _ := sha3.shake256(test_data, 32) + + // Benchmark Ascon-HASH256 + ascon256_time := benchmark_ascon_sha256(test_data, benchmark_iterations) + + // Benchmark Sha256 implementation + sha256_time := benchmark_sha256_sum256(test_data, benchmark_iterations) + + // Benchmark Sha3 shake256 implementation + shake256_time := benchmark_sha3_shake256(test_data, benchmark_iterations) + + // Benchmark AsconXof128 256-bits output + asconxof128_time := benchmark_ascon_xof128_32(test_data, benchmark_iterations) + + // Calculate ratio ascon256 / sha256 + ratio_ascon256_sha256 := f64(ascon256_time.nanoseconds()) / f64(sha256_time.nanoseconds()) + + // Calculate ratio asconxof128 / shake256 + ratio_asconxof128_shake256 := f64(asconxof128_time.nanoseconds()) / f64(shake256_time.nanoseconds()) + + ascon256_str := format_duration(ascon256_time) + sha256_str := format_duration(sha256_time) + asconxof128_str := format_duration(asconxof128_time) + shake256_str := format_duration(shake256_time) + + ratio_ascon256_sha256_str := '${ratio_ascon256_sha256:6.2f}x' + ratio_asconxof128_shake256_str := '${ratio_asconxof128_shake256:6.2f}x' + + println('${size:10} B | ${ascon256_str:10} | ${sha256_str:10} | ${ratio_ascon256_sha256_str:12} || ${asconxof128_str:11} | ${shake256_str:10} | ${ratio_asconxof128_shake256_str:12} |') + + total_ascon256 += ascon256_time + total_sha256 += sha256_time + + total_asconxof128 += asconxof128_time + total_shake256 += shake256_time + } + + println('${'-'.repeat(98)}') + + // Overall performance comparison + overall_ascon256_w_sha256_ratio := f64(total_ascon256.nanoseconds()) / f64(total_sha256.nanoseconds()) + overall_asconxof128_w_shake256_ratio := f64(total_asconxof128.nanoseconds()) / f64(total_shake256.nanoseconds()) + total_title := 'Total' + println('${total_title:12} | ${format_duration(total_ascon256):10} | ${format_duration(total_sha256):10} | ${overall_ascon256_w_sha256_ratio:11.2f}x || ${format_duration(total_asconxof128):11} | ${format_duration(total_shake256):10} | ${overall_asconxof128_w_shake256_ratio:12.2f}x|') + println('${'-'.repeat(98)}') + + println('') + println('Per-operation averages:') + avg_ascon256 := total_ascon256.nanoseconds() / (benchmark_iterations * test_data_sizes.len) + avg_sha256 := total_sha256.nanoseconds() / (benchmark_iterations * test_data_sizes.len) + avg_shake256 := total_shake256.nanoseconds() / (benchmark_iterations * test_data_sizes.len) + avg_asconxof128 := total_asconxof128.nanoseconds() / (benchmark_iterations * test_data_sizes.len) + + println(' Ascon256:\t ${avg_ascon256:8} ns per hash') + println(' Sha256:\t ${avg_sha256:8} ns per hash') + println(' AsconXof128:\t ${avg_asconxof128:8} ns per hash') + println(' Shake256:\t ${avg_shake256:8} ns per hash') + println('') +} diff --git a/vlib/x/crypto/ascon/bench/sum.v b/vlib/x/crypto/ascon/bench/sum.v new file mode 100644 index 0000000000..3739d0efff --- /dev/null +++ b/vlib/x/crypto/ascon/bench/sum.v @@ -0,0 +1,36 @@ +import time +import x.crypto.ascon + +// Before: +// Benchmarking ascon.sum256 ... +// Average ascon.sum256 time: 8 µs +// Benchmarking ascon.sum256 ... +// Average ascon.sum256 time: 6 µs + +// For xof128 (32 bytes) +// Benchmarking ascon.xof128 ... +// Average ascon.xof128 time: 7 µs +// Benchmarking ascon.xof128 ... +// Average ascon.xof128 time: 6 µs + +// For cxof128 32 bytes +// Benchmarking ascon.cxof128 ... +// Average ascon.cxof128 time: 9 µs +// Benchmarking ascon.sum256 ... +// Average ascon.cxof128 time: 7 µs +// +fn main() { + iterations := 1000 + msg := [u8(0xff)].repeat(100) + + println('Benchmarking ascon.sum256 ...') + mut total_sum_time := i64(0) + for _ in 0 .. iterations { + sw := time.new_stopwatch() + _ := ascon.sum256(msg) + elapsed := sw.elapsed().microseconds() + total_sum_time += elapsed + } + avg_sum_time := total_sum_time / iterations + println('Average ascon.sum256 time: ${avg_sum_time} µs') +} diff --git a/vlib/x/crypto/ascon/digest.v b/vlib/x/crypto/ascon/digest.v index 6617b6e255..89a7dac7e1 100644 --- a/vlib/x/crypto/ascon/digest.v +++ b/vlib/x/crypto/ascon/digest.v @@ -133,3 +133,39 @@ fn (mut d Digest) squeeze(mut dst []u8) int { return pos } + +@[direct_array_access; inline] +fn ascon_generic_hash(mut s State, msg_ []u8, size int) []u8 { + // Assumed state was correctly initialized + // Absorbing the message + mut msg := msg_.clone() + for msg.len >= block_size { + s.e0 ^= binary.little_endian_u64(msg[0..block_size]) + unsafe { + msg = msg[block_size..] + } + ascon_pnr(mut s, ascon_prnd_12) + } + // Absorb the last partial message block + s.e0 ^= load_bytes(msg, msg.len) + s.e0 ^= pad(msg.len) + + // Squeezing phase + // + // The squeezing phase begins after msg is absorbed with an + // permutation 𝐴𝑠𝑐𝑜𝑛-𝑝[12] to the state: + ascon_pnr(mut s, ascon_prnd_12) + mut out := []u8{len: size} + mut pos := 0 + mut clen := out.len + for clen >= block_size { + binary.little_endian_put_u64(mut out[pos..pos + 8], s.e0) + ascon_pnr(mut s, ascon_prnd_12) + pos += block_size + clen -= block_size + } + // final output, the resulting 256-bit digest is the concatenation of hash blocks + store_bytes(mut out[pos..], s.e0, clen) + + return out +} diff --git a/vlib/x/crypto/ascon/hash.v b/vlib/x/crypto/ascon/hash.v index 2ec0dda97e..60c02af506 100644 --- a/vlib/x/crypto/ascon/hash.v +++ b/vlib/x/crypto/ascon/hash.v @@ -42,13 +42,13 @@ const hash256_initial_state = State{ } // 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 +pub fn sum256(msg_ []u8) []u8 { + // This is single-shot function, so, no need to use Hash256 opaque that process + // message in streaming way. To reduce this overhead, use raw processing instead. + // + // Initialize state + mut s := hash256_initial_state + return ascon_generic_hash(mut s, msg_, hash256_size) } // Hash256 is an opaque provides an implementation of Ascon-Hash256 from NIST.SP.800-232 standard. diff --git a/vlib/x/crypto/ascon/util.v b/vlib/x/crypto/ascon/util.v index 0618303648..345683ea66 100644 --- a/vlib/x/crypto/ascon/util.v +++ b/vlib/x/crypto/ascon/util.v @@ -5,7 +5,6 @@ // Utility helpers used across the module module ascon -import math.bits import encoding.binary // clear_bytes clears the bytes of x in n byte diff --git a/vlib/x/crypto/ascon/xof.v b/vlib/x/crypto/ascon/xof.v index febbf98968..eea14e35f1 100644 --- a/vlib/x/crypto/ascon/xof.v +++ b/vlib/x/crypto/ascon/xof.v @@ -30,13 +30,11 @@ const xof128_initial_state = State{ // 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 + if size > max_hash_size { + return error('xof128: invalid size') + } + mut s := xof128_initial_state + return ascon_generic_hash(mut s, msg, size) } // xof128_64 creates a 64-bytes of Ascon-XOF128 checksum of msg. @@ -170,13 +168,10 @@ const cxof128_initial_state = State{ // 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 + // Initialize CXof128 state with precomputed-value and absorb the customization string + mut s := cxof128_initial_state + cxof128_absorb_custom_string(mut s, cs) + return ascon_generic_hash(mut s, msg, size) } // cxof128_64 creates a 64-bytes of Ascon-CXOF128 checksum of msg with supplied custom string in cs.