v/vlib/x/crypto/ascon/aead128.v
blackshirt f073169177
Some checks are pending
Graphics CI / gg-regressions (push) Waiting to run
vlib modules CI / build-module-docs (push) Waiting to run
native backend CI / native-backend-ubuntu (push) Waiting to run
native backend CI / native-backend-windows (push) Waiting to run
Shy and PV CI / v-compiles-puzzle-vibes (push) Waiting to run
Sanitized CI / sanitize-address-msvc (push) Waiting to run
Sanitized CI / sanitize-undefined-clang (push) Waiting to run
Sanitized CI / sanitize-undefined-gcc (push) Waiting to run
Sanitized CI / tests-sanitize-address-clang (push) Waiting to run
Sanitized CI / sanitize-address-gcc (push) Waiting to run
Sanitized CI / sanitize-memory-clang (push) Waiting to run
sdl CI / v-compiles-sdl-examples (push) Waiting to run
Time CI / time-linux (push) Waiting to run
Time CI / time-macos (push) Waiting to run
Time CI / time-windows (push) Waiting to run
toml CI / toml-module-pass-external-test-suites (push) Waiting to run
Tools CI / tools-linux (gcc) (push) Waiting to run
Tools CI / tools-linux (clang) (push) Waiting to run
Tools CI / tools-linux (tcc) (push) Waiting to run
Tools CI / tools-macos (clang) (push) Waiting to run
Tools CI / tools-windows (gcc) (push) Waiting to run
Tools CI / tools-windows (msvc) (push) Waiting to run
Tools CI / tools-windows (tcc) (push) Waiting to run
Tools CI / tools-docker-ubuntu-musl (push) Waiting to run
vab CI / vab-compiles-v-examples (push) Waiting to run
vab CI / v-compiles-os-android (push) Waiting to run
wasm backend CI / wasm-backend (ubuntu-22.04) (push) Waiting to run
wasm backend CI / wasm-backend (windows-2022) (push) Waiting to run
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)
2025-09-10 10:03:35 +03:00

464 lines
14 KiB
V
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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[0127] ← S[0127] ⊕ 𝐴𝑖,
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:
// 𝑇𝑆[192319] ⊕ 𝐾.
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)
}