diff --git a/vlib/x/crypto/ascon/README.md b/vlib/x/crypto/ascon/README.md new file mode 100644 index 0000000000..61a826506c --- /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/ascon.v b/vlib/x/crypto/ascon/ascon.v new file mode 100644 index 0000000000..1d1368ba84 --- /dev/null +++ b/vlib/x/crypto/ascon/ascon.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. +// +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') + } + // handle other 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 +@[direct_array_access] +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 ^= rotate_right_64(s.e0, 19) ^ rotate_right_64(s.e0, 28) + s.e1 ^= rotate_right_64(s.e1, 61) ^ rotate_right_64(s.e1, 39) + s.e2 ^= rotate_right_64(s.e2, 1) ^ rotate_right_64(s.e2, 6) + s.e3 ^= rotate_right_64(s.e3, 10) ^ rotate_right_64(s.e3, 17) + s.e4 ^= rotate_right_64(s.e4, 7) ^ rotate_right_64(s.e4, 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 0000000000..763aa0c071 --- /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 0000000000..01c28df658 --- /dev/null +++ b/vlib/x/crypto/ascon/cxof_test.v @@ -0,0 +1,146 @@ +// 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 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 := cxof128_64(msg, z)! + assert md == out + + // With Xof128Digest opaque + mut cx := new_cxof128(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 0000000000..0bc7d6e50a --- /dev/null +++ b/vlib/x/crypto/ascon/digest.v @@ -0,0 +1,122 @@ +// 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 + block := unsafe { d.buf[..d.length] } + d.State.e0 ^= pad(block.len) + d.State.e0 ^= load_bytes(block, block.len) + + // 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 Ascon-HASH256 state +@[direct_array_access] +fn (mut d Digest) absorb(msg_ []u8) int { + // nothing to absorb, just return + if msg_.len == 0 { + return 0 + } + 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 Digest's 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 { + // check + if dst.len < 1 || 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 out := []u8{len: size} + 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 256-bit digest is the concatenation of hash blocks + store_bytes(mut dst[pos..], d.State.e0, clen) + pos += clen + + return pos +} diff --git a/vlib/x/crypto/ascon/example/cxof_example.v b/vlib/x/crypto/ascon/example/cxof_example.v new file mode 100644 index 0000000000..36ffe56521 --- /dev/null +++ b/vlib/x/crypto/ascon/example/cxof_example.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/example/hash256_example.v b/vlib/x/crypto/ascon/example/hash256_example.v new file mode 100644 index 0000000000..4facb44c3f --- /dev/null +++ b/vlib/x/crypto/ascon/example/hash256_example.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/example/xof_example.v b/vlib/x/crypto/ascon/example/xof_example.v new file mode 100644 index 0000000000..1d02f7e12f --- /dev/null +++ b/vlib/x/crypto/ascon/example/xof_example.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 0000000000..03519ae362 --- /dev/null +++ b/vlib/x/crypto/ascon/hash.v @@ -0,0 +1,136 @@ +// 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. +@[direct_array_access] +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} + n := h.Digest.squeeze(mut dst) + return dst[..n] +} + +// 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. +@[direct_array_access] +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 reinits internal Hash256 state into default initialized state. +@[direct_array_access] +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. +@[direct_array_access] +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 +@[direct_array_access] +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. +@[direct_array_access] +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} + n := h0.Digest.squeeze(mut dst) + h0.reset() + return dst[..n] +} diff --git a/vlib/x/crypto/ascon/hash_test.v b/vlib/x/crypto/ascon/hash_test.v new file mode 100644 index 0000000000..1ffc05f95d --- /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/profile.txt b/vlib/x/crypto/ascon/profile.txt new file mode 100644 index 0000000000..e5d9828e14 --- /dev/null +++ b/vlib/x/crypto/ascon/profile.txt @@ -0,0 +1,93 @@ + 2 0.016ms 0.011ms 8196ns strings__new_builder + 6 0.002ms 0.001ms 334ns strings__Builder_write_string + 2 0.002ms 0.001ms 1062ns strings__Builder_str + 2 0.000ms 0.000ms 130ns strings__Builder_free + 2880000 103.704ms 103.704ms 36ns math__bits__rotate_left_32 + 2 0.000ms 0.000ms 50ns array_clear + 1000 0.134ms 0.087ms 134ns array_reset + 4 0.000ms 0.000ms 76ns array_get + 61000 3.190ms 3.190ms 52ns array_slice + 1000 0.426ms 0.159ms 426ns array_clone_to_depth + 8000 0.952ms 0.657ms 119ns array_set + 6 0.001ms 0.001ms 194ns array_push_many + 2 0.001ms 0.000ms 270ns array_free + 4000 0.560ms 0.370ms 140ns copy + 14007 0.518ms 0.518ms 37ns panic_on_negative_len + 14007 0.538ms 0.538ms 38ns panic_on_negative_cap + 2000 0.625ms 0.383ms 313ns __new_array_noscan + 12005 4.232ms 2.128ms 353ns __new_array_with_default_noscan + 2 0.008ms 0.001ms 3762ns new_array_from_c_array_noscan + 6 0.001ms 0.001ms 170ns array_push_noscan + 1 0.000ms 0.000ms 241ns at_exit + 4 0.044ms 0.001ms 11080ns println + 4 0.043ms 0.001ms 10807ns _writeln_to_fd + 8 0.042ms 0.042ms 5286ns _write_buf_to_fd + 2003 0.279ms 0.279ms 139ns _v_malloc + 4 0.001ms 0.001ms 358ns malloc_noscan + 1000 0.211ms 0.211ms 211ns vcalloc + 14007 1.297ms 1.297ms 93ns vcalloc_noscan + 6 0.000ms 0.000ms 49ns _v_free + 2003 0.459ms 0.180ms 229ns memdup + 2 0.000ms 0.000ms 165ns memdup_noscan + 1 0.000ms 0.000ms 81ns gc_set_warn_proc + 1 0.005ms 0.005ms 5009ns builtin_init + 13015 0.523ms 0.523ms 40ns vmemcpy + 4002 0.190ms 0.190ms 48ns vmemmove + 1000 0.047ms 0.047ms 47ns vmemset + 4001 0.547ms 0.376ms 137ns _result_ok + 2 0.003ms 0.000ms 1492ns i64_str + 2 0.003ms 0.001ms 1287ns impl_i64_to_string + 2 0.000ms 0.000ms 130ns tos + 2 0.000ms 0.000ms 135ns u8_vstring_with_len + 2 0.000ms 0.000ms 226ns string_free + 2 0.005ms 0.001ms 2505ns StrIntpData_process_str_intp_data + 2 0.027ms 0.001ms 13294ns str_intp + 2 0.000ms 0.000ms 40ns ArrayFlags_has + 2 0.000ms 0.000ms 45ns ArrayFlags_set + 2000 0.082ms 0.082ms 41ns time__Duration_microseconds + 2000 0.321ms 0.179ms 160ns time__new_stopwatch + 2000 0.317ms 0.174ms 158ns time__StopWatch_elapsed + 4002 0.285ms 0.285ms 71ns time__sys_mono_now + 8000 0.330ms 0.330ms 41ns encoding__binary__big_endian_put_u32 + 1000 0.049ms 0.049ms 49ns encoding__binary__big_endian_put_u64 + 37000 1.497ms 1.497ms 40ns encoding__binary__little_endian_u64 + 4000 0.197ms 0.197ms 49ns encoding__binary__little_endian_put_u64 + 38 0.002ms 0.002ms 43ns hash__wymum + 1000 2.113ms 0.177ms 2113ns crypto__sha256__Digest_init + 1000 1.337ms 0.384ms 1337ns crypto__sha256__Digest_reset + 1000 3.111ms 0.223ms 3111ns crypto__sha256__new + 3000 229.010ms 0.827ms 76337ns crypto__sha256__Digest_write + 1000 50.366ms 1.068ms 50366ns crypto__sha256__Digest_checksum + 1000 234.793ms 0.171ms 234793ns crypto__sha256__sum256 + 2000 226.598ms 0.162ms 113299ns crypto__sha256__block + 2000 226.436ms 120.919ms 113218ns crypto__sha256__block_generic + 42000 449.579ms 22.965ms 10704ns x__crypto__ascon__ascon_pnr + 504000 426.614ms 241.540ms 846ns x__crypto__ascon__ascon_perm + 1000 0.929ms 0.232ms 929ns x__crypto__ascon__Digest_finish + 1000 55.255ms 0.781ms 55255ns x__crypto__ascon__Digest_squeeze + 1000 405.510ms 5.310ms 405510ns x__crypto__ascon__Digest_absorb_without_idx + 1000 462.994ms 0.236ms 462994ns x__crypto__ascon__sum256 + 1000 0.763ms 0.143ms 763ns x__crypto__ascon__new_hash256 + 1000 405.811ms 0.150ms 405811ns x__crypto__ascon__Hash256_write + 1000 0.040ms 0.040ms 40ns x__crypto__ascon__pad + 1000 0.046ms 0.046ms 46ns x__crypto__ascon__u64le + 4000 0.148ms 0.148ms 37ns x__crypto__ascon__set_byte + 1000 0.471ms 0.277ms 471ns x__crypto__ascon__load_bytes + 1000 0.038ms 0.038ms 38ns x__crypto__ascon__store_bytes + 5040000 185.074ms 185.074ms 37ns x__crypto__ascon__ascon_rotate_right + 8 0.000ms 0.000ms 42ns rand__seed__nr_next + 2 0.007ms 0.004ms 3732ns rand__seed__time_seed_array + 1 0.005ms 0.003ms 4879ns rand__seed__time_seed_64 + 1 0.000ms 0.000ms 220ns rand__wyrand__WyRandRNG_seed + 4 0.000ms 0.000ms 85ns rand__wyrand__WyRandRNG_u8 + 38 0.005ms 0.003ms 129ns rand__wyrand__WyRandRNG_u64 + 1 0.000ms 0.000ms 50ns rand__wyrand__WyRandRNG_block_size + 1 0.019ms 0.000ms 19066ns rand__init + 1 0.007ms 0.002ms 7243ns rand__read_64 + 1 0.008ms 0.000ms 7544ns rand__read_internal + 1 0.011ms 0.000ms 10600ns rand__PRNG_bytes + 1 0.012ms 0.000ms 12234ns rand__new_default + 1 0.000ms 0.000ms 40ns rand__bytes + 1 235.369ms 0.205ms 235368978ns main__bench_crypto_sha256 + 1 463.604ms 0.190ms 463603787ns main__bench_ascon_sum256 + 1 698.984ms 0.000ms 698983845ns main__main diff --git a/vlib/x/crypto/ascon/util.v b/vlib/x/crypto/ascon/util.v new file mode 100644 index 0000000000..6132709d83 --- /dev/null +++ b/vlib/x/crypto/ascon/util.v @@ -0,0 +1,110 @@ +// 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 +@[direct_array_access] +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. +@[direct_array_access] +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. +@[direct_array_access] +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) +} + +@[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) +} + +@[direct_array_access] +fn store_bytes(mut out []u8, x u64, n int) { + for i := 0; i < n; i++ { + out[i] = get_byte(x, i) + } +} + +@[direct_array_access; 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 0000000000..c66e2d6efc --- /dev/null +++ b/vlib/x/crypto/ascon/xof.v @@ -0,0 +1,332 @@ +// 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. +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. +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. +@[direct_array_access] +pub fn xof128(msg []u8, size int) ![]u8 { + mut x := new_xof128(size) + _ := x.write(msg)! + x.Digest.finish() + mut out := []u8{len: size} + n := x.Digest.squeeze(mut out) + x.reset() + return out[..n] +} + +// xof128_64 creates a 64-bytes of Ascon-XOF128 checksum of msg. +@[direct_array_access] +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. +@[direct_array_access] +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. +@[direct_array_access] +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. +@[direct_array_access] +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. +@[direct_array_access] +pub fn (mut x Xof128) read(mut dst []u8) !int { + if dst.len < 1 || 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. +@[direct_array_access] +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. +@[direct_array_access] +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} + n := cx.Digest.squeeze(mut out) + cx.reset() + return out[..n] +} + +// cxof128_64 creates a 64-bytes of Ascon-CXOF128 checksum of msg with supplied custom string in cs. +@[direct_array_access] +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. +@[direct_array_access] +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. +@[direct_array_access] +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. +@[direct_array_access] +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. +@[direct_array_access] +pub fn (mut x CXof128) read(mut dst []u8) !int { + if dst.len < 1 || 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. +@[direct_array_access] +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 0000000000..aeb0f4613a --- /dev/null +++ b/vlib/x/crypto/ascon/xof_test.v @@ -0,0 +1,113 @@ +// 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 + +// 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 := xof128_64(msg)! + assert out == md + + // With Xof128Digest opaque + mut x := new_xof128(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' + }, +]