| 1 | // Copyright ©2025 blackshirt. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | // |
| 5 | // Common helpers used by Ascon-Hash256, Ascon-XOF128 and Ascon-CXOF128 |
| 6 | module ascon |
| 7 | |
| 8 | import encoding.binary |
| 9 | |
| 10 | // The Digest is an internal structure used by Ascon-hashing variants. |
| 11 | @[noinit] |
| 12 | struct Digest { |
| 13 | State |
| 14 | mut: |
| 15 | // buffer for storing unprocessed data for streaming-way |
| 16 | buf []u8 = []u8{len: block_size} |
| 17 | // length of leftover unprocessed bytes on the digest buffer |
| 18 | length int |
| 19 | |
| 20 | // internal flag |
| 21 | done bool |
| 22 | } |
| 23 | |
| 24 | // finish finalizes state by writing last block in the Digest internal buffer. |
| 25 | // and consequently updates Digest state. |
| 26 | @[direct_array_access; inline] |
| 27 | fn (mut d Digest) finish() { |
| 28 | if d.length >= d.buf.len { |
| 29 | panic('Digest.finish: internal error') |
| 30 | } |
| 31 | // Process for the last block stored on the internal buffer |
| 32 | d.State.e0 ^= pad(d.length) |
| 33 | d.State.e0 ^= load_bytes(d.buf[..d.length], d.length) |
| 34 | |
| 35 | // Permutation step was done in squeezing-phase |
| 36 | // ascon_pnr(mut d.State, .ascon_prnd_12) |
| 37 | |
| 38 | // zeroing Digest buffer |
| 39 | d.length = 0 |
| 40 | unsafe { d.buf.reset() } |
| 41 | |
| 42 | // After finishing phase, we can't write data anymore into state |
| 43 | d.done = true |
| 44 | } |
| 45 | |
| 46 | // absorb absorbs message msg_ into Digest state. |
| 47 | @[direct_array_access] |
| 48 | fn (mut d Digest) absorb(msg_ []u8) int { |
| 49 | // nothing to absorb, just return |
| 50 | if msg_.len == 0 { |
| 51 | return 0 |
| 52 | } |
| 53 | // Absorbing messages into Digest state working in streaming-way. |
| 54 | // Its continuesly updates internal state until you call `.finish` or `.free` it. |
| 55 | // Firstly, it would checking unprocessed bytes on internal buffer, and append it |
| 56 | // with bytes from messasge to align with block_size. |
| 57 | // And then absorb this buffered message into state. |
| 58 | // The process continues until the last partial block thats maybe below the block_size. |
| 59 | // If its happens, it will be stored on the Digest internal buffer for later processing. |
| 60 | mut msg := msg_.clone() |
| 61 | unsafe { |
| 62 | // Check if internal buffer has previous unprocessed bytes. |
| 63 | // If its on there, try to empty the buffer. |
| 64 | if d.length > 0 { |
| 65 | // There are bytes in the d.buf, append it with bytes taken from msg |
| 66 | if d.length + msg.len >= block_size { |
| 67 | n := copy(mut d.buf[d.length..], msg) |
| 68 | msg = msg[n..] |
| 69 | d.length += n |
| 70 | // If this d.buf length has reached block_size bytes, absorb it. |
| 71 | if d.length == block_size { |
| 72 | d.State.e0 ^= binary.little_endian_u64(d.buf) |
| 73 | ascon_pnr(mut d.State, .ascon_prnd_12) |
| 74 | // reset the internal buffer |
| 75 | d.length = 0 |
| 76 | d.buf.reset() |
| 77 | } |
| 78 | } else { |
| 79 | // Otherwise, still fit to buffer, but nof fully fills the d.buf |
| 80 | // just stores into buffer without processing |
| 81 | n := copy(mut d.buf[d.length..], msg) |
| 82 | msg = msg[n..] |
| 83 | d.length += n |
| 84 | } |
| 85 | } |
| 86 | // process for full block |
| 87 | for msg.len >= block_size { |
| 88 | d.State.e0 ^= binary.little_endian_u64(msg[0..block_size]) |
| 89 | msg = msg[block_size..] |
| 90 | ascon_pnr(mut d.State, .ascon_prnd_12) |
| 91 | } |
| 92 | // If there are partial block, just stored into buffer. |
| 93 | if msg.len > 0 { |
| 94 | n := copy(mut d.buf[d.length..], msg) |
| 95 | msg = msg[n..] |
| 96 | d.length += n |
| 97 | } |
| 98 | return msg_.len - msg.len |
| 99 | } |
| 100 | } |
| 101 | |
| 102 | // squeeze squeezes the state and calculates checksum output for the current state. |
| 103 | // It accepts destination buffer with desired buffer length you want to output. |
| 104 | @[direct_array_access; inline] |
| 105 | fn (mut d Digest) squeeze(mut dst []u8) int { |
| 106 | // nothing to store, just return unchanged |
| 107 | if dst.len == 0 { |
| 108 | return 0 |
| 109 | } |
| 110 | // check |
| 111 | if dst.len > max_hash_size { |
| 112 | panic('Digest.squeeze: invalid dst.len') |
| 113 | } |
| 114 | // The squeezing phase begins after msg is absorbed with an |
| 115 | // permutation 𝐴𝑠𝑐𝑜𝑛-𝑝[12] to the state: |
| 116 | ascon_pnr(mut d.State, .ascon_prnd_12) |
| 117 | |
| 118 | mut pos := 0 |
| 119 | mut clen := dst.len |
| 120 | // process for full block size |
| 121 | for clen >= block_size { |
| 122 | binary.little_endian_put_u64(mut dst[pos..pos + 8], d.State.e0) |
| 123 | ascon_pnr(mut d.State, .ascon_prnd_12) |
| 124 | pos += block_size |
| 125 | clen -= block_size |
| 126 | } |
| 127 | // final output, the resulting hash is the concatenation of hash blocks |
| 128 | store_bytes(mut dst[pos..], d.State.e0, clen) |
| 129 | pos += clen |
| 130 | |
| 131 | // for make sure, assert it here |
| 132 | assert pos == dst.len |
| 133 | |
| 134 | return pos |
| 135 | } |
| 136 | |
| 137 | @[direct_array_access; inline] |
| 138 | fn ascon_generic_hash(mut s State, msg []u8, size int) []u8 { |
| 139 | // Assumed state was correctly initialized |
| 140 | // Absorbing the message |
| 141 | mut pos := 0 |
| 142 | // Check if msg has non-null length, if yes, absorb it. |
| 143 | // Otherwise, just pad it |
| 144 | if _likely_(msg.len > 0) { |
| 145 | mut msg_len := msg.len |
| 146 | for msg_len >= block_size { |
| 147 | block := unsafe { msg[pos..pos + block_size] } |
| 148 | s.e0 ^= binary.little_endian_u64(block) |
| 149 | pos += block_size |
| 150 | msg_len -= block_size |
| 151 | ascon_pnr(mut s, .ascon_prnd_12) |
| 152 | } |
| 153 | // Absorb the last partial message block |
| 154 | last_block := unsafe { msg[pos..] } |
| 155 | s.e0 ^= u64(0x01) << (8 * last_block.len) // pad(last_block.len) |
| 156 | if last_block.len > 0 { |
| 157 | s.e0 ^= load_bytes(last_block, last_block.len) |
| 158 | } |
| 159 | } else { |
| 160 | // Otherwise, just pad it |
| 161 | s.e0 ^= u64(0x01) |
| 162 | } |
| 163 | // reset pos |
| 164 | pos = 0 |
| 165 | |
| 166 | // Squeezing phase |
| 167 | // |
| 168 | // The squeezing phase begins after msg is absorbed with an |
| 169 | // permutation 𝐴𝑠𝑐𝑜𝑛-𝑝[12] to the state: |
| 170 | ascon_pnr(mut s, .ascon_prnd_12) |
| 171 | mut out := []u8{len: size} |
| 172 | mut clen := out.len |
| 173 | for clen >= block_size { |
| 174 | binary.little_endian_put_u64(mut out[pos..pos + 8], s.e0) |
| 175 | ascon_pnr(mut s, .ascon_prnd_12) |
| 176 | pos += block_size |
| 177 | clen -= block_size |
| 178 | } |
| 179 | // final output, the resulting 256-bit digest is the concatenation of hash blocks |
| 180 | store_bytes(mut out[pos..], s.e0, clen) |
| 181 | |
| 182 | return out |
| 183 | } |
| 184 | |