From 60b0ee91a27d43864159a3162bbbf70fc9a2faf1 Mon Sep 17 00:00:00 2001 From: blackshirt Date: Fri, 10 Oct 2025 00:29:12 +0700 Subject: [PATCH] x.crypto.chacha20poly1305: add a nonce-misuse resistant and key-committing feature (#25459) --- vlib/x/crypto/chacha20poly1305/README.md | 11 +- vlib/x/crypto/chacha20poly1305/bench/bench.v | 182 +++++++ vlib/x/crypto/chacha20poly1305/psiv.v | 489 +++++++++++++++++++ vlib/x/crypto/chacha20poly1305/psiv_test.v | 207 ++++++++ 4 files changed, 887 insertions(+), 2 deletions(-) create mode 100644 vlib/x/crypto/chacha20poly1305/bench/bench.v create mode 100644 vlib/x/crypto/chacha20poly1305/psiv.v create mode 100644 vlib/x/crypto/chacha20poly1305/psiv_test.v diff --git a/vlib/x/crypto/chacha20poly1305/README.md b/vlib/x/crypto/chacha20poly1305/README.md index 01e1dacc7..776f290df 100644 --- a/vlib/x/crypto/chacha20poly1305/README.md +++ b/vlib/x/crypto/chacha20poly1305/README.md @@ -10,6 +10,13 @@ module and `x.crypto.poly1305` message authentication code (MAC) module. > This is an absolutely experimental module, which is subject to change. > Please use it carefully, thoroughly and wisely. +## Supported features +The implemented features at the time of writing (2025/10/08) are: +- The standard ChaCha20 and Poly1305 construct for IETF Protocols defined in [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) +- ChaCha20 Poly1305 AEAD construct with 8-bytes nonce. +- Support for eXtended ChaCha20 Poly1305 AEAD construct with 24 bytes nonce. +- Support for nonce-misuse resistent and key-commiting AEAD through ChaCha20-Poly1305-PSIV construction defined in the [A Robust Variant of ChaCha20-Poly1305](https://eprint.iacr.org/2025/222) paper. + ## Examples ```v @@ -18,8 +25,8 @@ import x.crypto.chacha20poly1305 fn main() { // plaintext message to be encrypted and authenticated - message := "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it." - .bytes() + message := "Ladies and Gentlemen of the class of '99: If I could offer you only one + tip for the future, sunscreen would be it.".bytes() // sets your secure random key key := hex.decode('808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f')! diff --git a/vlib/x/crypto/chacha20poly1305/bench/bench.v b/vlib/x/crypto/chacha20poly1305/bench/bench.v new file mode 100644 index 000000000..0bbfe9136 --- /dev/null +++ b/vlib/x/crypto/chacha20poly1305/bench/bench.v @@ -0,0 +1,182 @@ +// Copyright (c) 2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +// This file contains benchmarking code for standard AEAD_CHACHA20_POLY1305 encryption +// and decryption compared to AEAD_CHACHA20_POLY1305 with PSIV construct. +// +// This output on my test. +// Standard ChaCha20Poly1305 AEAD and PSIV construct output performance comparison +// =============================================================================== +// Iterations per test: 1000 +// ------------------------------------------------------------------------------------------------------ +// Data Size | Std | PSIV | Enc (std/psiv) || Std | PSIV | Dec (std/psiv)| +// ------------------------------------------------------------------------------------------------------ +// 6 B | 16.00ms | 19.00ms | 0.85x || 16.00ms | 19.00ms | 0.81x | +// 8 B | 15.00ms | 19.00ms | 0.81x || 16.00ms | 18.00ms | 0.87x | +// 12 B | 15.00ms | 20.00ms | 0.76x || 16.00ms | 20.00ms | 0.77x | +// 16 B | 15.00ms | 21.00ms | 0.72x || 15.00ms | 19.00ms | 0.80x | +// 64 B | 21.00ms | 25.00ms | 0.82x || 21.00ms | 26.00ms | 0.80x | +// 75 B | 28.00ms | 35.00ms | 0.81x || 34.00ms | 44.00ms | 0.77x | +// 256 B | 55.00ms | 59.00ms | 0.94x || 50.00ms | 63.00ms | 0.80x | +// 1028 B | 174.00ms | 242.00ms | 0.72x || 178.00ms | 227.00ms | 0.78x | +// 2049 B | 361.00ms | 522.00ms | 0.69x || 399.00ms | 558.00ms | 0.71x | +// ------------------------------------------------------------------------------------------------------ +// Total | 703.00ms | 965.00ms | 0.73x || 748.00ms | 1.00s | 0.75x| +// ------------------------------------------------------------------------------------------------------ +// +// Per-operation averages: +// Standard ChaCha20Poly1305 encrypt: 78209 ns per hash +// ChaCha20Poly1305 PSIV encrypt: 107325 ns per hash +// +// Standard ChaCha20Poly1305 decrypt: 83151 ns per hash +// ChaCha20Poly1305 PSIV decrypt: 111155 ns per hash +module main + +import rand +import time +import x.crypto.chacha20poly1305 + +const benchmark_iterations = 1000 + +const test_data_sizes = [6, 8, 12, 16, 64, 75, 256, 1028, 2049] + +// standard AEAD_CHACHA20_POLY1305 encryption +fn benchmark_aead_std_encrypt(msg []u8, key []u8, nonce []u8, ad []u8, iterations int) time.Duration { + start := time.now() + for _ in 0 .. iterations { + _ := chacha20poly1305.encrypt(msg, key, nonce, ad) or { panic(err) } + } + return time.since(start) +} + +// standard AEAD_CHACHA20_POLY1305 decryption +fn benchmark_aead_std_decrypt(data []u8, key []u8, nonce []u8, ad []u8, iterations int) time.Duration { + start := time.now() + for _ in 0 .. iterations { + _ := chacha20poly1305.decrypt(data, key, nonce, ad) or { panic(err) } + } + return time.since(start) +} + +// psiv AEAD_CHACHA20_POLY1305 encryption +fn benchmark_aead_psiv_encrypt(msg []u8, key []u8, nonce []u8, ad []u8, iterations int) time.Duration { + start := time.now() + for _ in 0 .. iterations { + _ := chacha20poly1305.psiv_encrypt(msg, key, nonce, ad) or { panic(err) } + } + return time.since(start) +} + +// psiv AEAD_CHACHA20_POLY1305 decryption +fn benchmark_aead_psiv_decrypt(data []u8, key []u8, nonce []u8, ad []u8, iterations int) time.Duration { + start := time.now() + for _ in 0 .. iterations { + _ := chacha20poly1305.psiv_decrypt(data, key, nonce, ad) or { panic(err) } + } + 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 aead_std_enc = 'Std' +const aead_psiv_enc = 'PSIV' +const aead_std_dec = 'Std' +const aead_psiv_dec = 'PSIV' +const ratio_std_psiv_enc_title = 'Enc (std/psiv)' +const ratio_std_psiv_dec_title = 'Dec (std/psiv)' + +fn main() { + println('Standard ChaCha20Poly1305 AEAD and PSIV construct output performance comparison') + println('===============================================================================') + println('Iterations per test: ${benchmark_iterations}') + + println('${'-'.repeat(102)}') + println('${data_title:12} | ${aead_std_enc:10} | ${aead_psiv_enc:10} | ${ratio_std_psiv_enc_title:12} || ${aead_std_dec:10} | ${aead_psiv_dec:12} | ${ratio_std_psiv_dec_title:12}|') + println('${'-'.repeat(102)}') + + mut total_std_encrypt := time.Duration(0) + mut total_std_decrypt := time.Duration(0) + mut total_psiv_encrypt := time.Duration(0) + mut total_psiv_decrypt := time.Duration(0) + + key := rand.bytes(32)! + nonce := rand.bytes(12)! + for size in test_data_sizes { + ad := rand.bytes(size)! + test_msg := rand.bytes(size)! + + // Warm up + out0 := chacha20poly1305.encrypt(test_msg, key, nonce, ad)! + _ := chacha20poly1305.decrypt(out0, key, nonce, ad)! + + out1 := chacha20poly1305.psiv_encrypt(test_msg, key, nonce, ad)! + _ := chacha20poly1305.psiv_decrypt(out1, key, nonce, ad)! + + // Benchmark Standard AEAD_CHACHA20_POLY1305 encryption + std_encrypt_time := benchmark_aead_std_encrypt(test_msg, key, nonce, ad, benchmark_iterations) + + // Benchmark Standard AEAD_CHACHA20_POLY1305 decryption + std_decrypt_time := benchmark_aead_std_decrypt(out0, key, nonce, ad, benchmark_iterations) + + // Benchmark AEAD_CHACHA20_POLY1305 PSIV encryption + psiv_encrypt_time := benchmark_aead_psiv_encrypt(test_msg, key, nonce, ad, benchmark_iterations) + + // Benchmark AEAD_CHACHA20_POLY1305 PSIV decryption + psiv_decrypt_time := benchmark_aead_psiv_decrypt(out1, key, nonce, ad, benchmark_iterations) + + // Calculate ratio Standard/PSIV encryption + ratio_std_psiv_encrypt := f64(std_encrypt_time.nanoseconds()) / f64(psiv_encrypt_time.nanoseconds()) + + // Calculate ratio Standard/PSIV decryption + ratio_std_psiv_decrypt := f64(std_decrypt_time.nanoseconds()) / f64(psiv_decrypt_time.nanoseconds()) + + stdencrypt_str := format_duration(std_encrypt_time) + stddecrypt_str := format_duration(std_decrypt_time) + psivencrypt_str := format_duration(psiv_encrypt_time) + psivdecrypt_str := format_duration(psiv_decrypt_time) + + ratio_std_psiv_encrypt_str := '${ratio_std_psiv_encrypt:6.2f}x' + ratio_std_psiv_decrypt_str := '${ratio_std_psiv_decrypt:6.2f}x' + + println('${size:10} B | ${stdencrypt_str:10} | ${psivencrypt_str:10} | ${ratio_std_psiv_encrypt_str:14} || ${stddecrypt_str:10} | ${psivdecrypt_str:11} | ${ratio_std_psiv_decrypt_str:12} |') + + total_std_encrypt += std_encrypt_time + total_std_decrypt += std_decrypt_time + total_psiv_encrypt += psiv_encrypt_time + total_psiv_decrypt += psiv_decrypt_time + } + + println('${'-'.repeat(102)}') + + // Overall performance comparison + overall_std_psiv_encrypt_ratio := f64(total_std_encrypt.nanoseconds()) / f64(total_psiv_encrypt.nanoseconds()) + overall_std_psiv_decrypt_ratio := f64(total_std_decrypt.nanoseconds()) / f64(total_psiv_decrypt.nanoseconds()) + + total_title := 'Total' + println('${total_title:12} | ${format_duration(total_std_encrypt):10} | ${format_duration(total_psiv_encrypt):10} | ${overall_std_psiv_encrypt_ratio:13.2f}x || ${format_duration(total_std_decrypt):10} | ${format_duration(total_psiv_decrypt):12} | ${overall_std_psiv_decrypt_ratio:12.2f}x|') + println('${'-'.repeat(102)}') + + println('') + println('Per-operation averages:') + avg_std_encrypt := total_std_encrypt.nanoseconds() / (benchmark_iterations * test_data_sizes.len) + avg_std_decrypt := total_std_decrypt.nanoseconds() / (benchmark_iterations * test_data_sizes.len) + avg_psiv_encrypt := total_psiv_encrypt.nanoseconds() / (benchmark_iterations * test_data_sizes.len) + avg_psiv_decrypt := total_psiv_decrypt.nanoseconds() / (benchmark_iterations * test_data_sizes.len) + + println(' Standard ChaCha20Poly1305 encrypt:\t ${avg_std_encrypt:8} ns per hash') + println(' ChaCha20Poly1305 PSIV encrypt:\t ${avg_psiv_encrypt:8} ns per hash') + println('') + println(' Standard ChaCha20Poly1305 decrypt:\t ${avg_std_decrypt:8} ns per hash') + println(' ChaCha20Poly1305 PSIV decrypt:\t ${avg_psiv_decrypt:8} ns per hash') + println('') +} diff --git a/vlib/x/crypto/chacha20poly1305/psiv.v b/vlib/x/crypto/chacha20poly1305/psiv.v new file mode 100644 index 000000000..de1017600 --- /dev/null +++ b/vlib/x/crypto/chacha20poly1305/psiv.v @@ -0,0 +1,489 @@ +// Copyright (c) 2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +// This file contains an experimental port of a Rust reference implementation of nonce-misuse +// resistant and key-committing authenticated encryption scheme called ChaCha20-Poly1305-PSIV, +// It backed by `chacha20` stream cipher and `poly1305` message authentication code module. +// Its originally described by Michiel Verbauwhede and the teams on his papers. +// See the detail on the [A Robust Variant of ChaCha20-Poly1305](https://eprint.iacr.org/2025/222). +module chacha20poly1305 + +import arrays +import encoding.binary +import crypto.internal.subtle +import x.crypto.chacha20 +import x.crypto.poly1305 + +// new_psiv creates a new Chacha20Poly1305RE with PSIV construct to operate on. +@[direct_array_access] +pub fn new_psiv(key []u8) !&Chacha20Poly1305RE { + if key.len != key_size { + return error('new_psiv: bad key size') + } + // derives and initializes the new key for later purposes + mac_key, enc_key, po := psiv_init(key)! + // set the values + c := &Chacha20Poly1305RE{ + key: key.clone() + precomp: true + mac_key: mac_key + enc_key: enc_key + po: po + } + return c +} + +// psiv_encrypt encrypts plaintext with provided key, nonce and additional data ad. +// It returns a ciphertext plus message authentication code (mac) contained +// within the end of ciphertext +@[direct_array_access] +pub fn psiv_encrypt(plaintext []u8, key []u8, nonce []u8, ad []u8) ![]u8 { + c := new_psiv(key)! + out := c.encrypt(plaintext, nonce, ad)! + unsafe { c.free() } + return out +} + +// psiv_decrypt decrypts the ciphertext with provided key, nonce and additional data in ad. +// It also tries to validate message authenticated code within ciphertext compared with +// calculated tag. It returns successfully decrypted message or error on fails. +@[direct_array_access] +pub fn psiv_decrypt(ciphertext []u8, key []u8, nonce []u8, ad []u8) ![]u8 { + c := new_psiv(key)! + out := c.decrypt(ciphertext, nonce, ad)! + unsafe { c.free() } + return out +} + +// Chacha20Poly1305RE is a Chacha20Poly1305 opaque with nonce-misuse resistent +// and key-commiting AEAD scheme with PSIV construct. +@[noinit] +pub struct Chacha20Poly1305RE implements AEAD { +mut: + // An underlying 32-bytes of key + key []u8 + // flags that tells derivation keys has been precomputed + precomp bool + mac_key []u8 + enc_key []u8 + po &poly1305.Poly1305 = unsafe { nil } +} + +// free releases resources taken by c. Dont use c after `.free` call. +@[unsafe] +pub fn (mut c Chacha20Poly1305RE) free() { + unsafe { + c.key.free() + c.mac_key.free() + c.enc_key.free() + c.po = nil + } + c.precomp = false +} + +// nonce_size return the size of the nonce of underlying c. +// Currently, it only support for standard 12-bytes nonce. +pub fn (c &Chacha20Poly1305RE) nonce_size() int { + return nonce_size +} + +// overhead returns difference between the lengths of a plaintext and its ciphertext. +// Its normally returns a tag size produced by this scheme. +pub fn (c &Chacha20Poly1305RE) overhead() int { + return tag_size +} + +// encrypt encrypts and authenticates the provided plaintext along with a nonce, and +// to be authenticated additional data in `ad`. It returns a ciphertext with message authenticated +// code stored within the end of ciphertext. +@[direct_array_access] +pub fn (c Chacha20Poly1305RE) encrypt(plaintext []u8, nonce []u8, ad []u8) ![]u8 { + if nonce.len != nonce_size { + return error('Chacha20Poly1305RE.encrypt: bad nonce length, only support 12-bytes nonce') + } + + // clone the initial poly1305 + mut po_ad := c.po.clone() + update_with_padding(mut po_ad, ad) + + mut po_ad_clone := po_ad.clone() + // build the tag + tag := psiv_gen_tag(mut po_ad_clone, plaintext, ad.len, c.mac_key, nonce) + enc := psiv_encrypt_internal(plaintext, c.enc_key, tag, nonce)! + + // setup destination buffer + mut out := []u8{cap: plaintext.len + tag_size} + out << enc + out << tag + + return out +} + +// decrypt decrypts the ciphertext with provided key, nonce and additional data in ad. +// It also tries to validate message authenticated code within ciphertext compared with +// calculated tag. It returns successfully decrypted message or error on fails. +@[direct_array_access] +pub fn (c Chacha20Poly1305RE) decrypt(ciphertext []u8, nonce []u8, ad []u8) ![]u8 { + if ciphertext.len < tag_size { + return error('Chacha20Poly1305RE.decrypt: insufficient ciphertext length') + } + if nonce.len != nonce_size { + return error('Chacha20Poly1305RE.decrypt: invalid nonce length provided') + } + enc := ciphertext[0..ciphertext.len - c.overhead()] + tag := ciphertext[ciphertext.len - c.overhead()..] + + mut po_with_ad := c.po.clone() + update_with_padding(mut po_with_ad, ad) + + out := psiv_encrypt_internal(enc, c.enc_key, tag, nonce)! + mut poad_clone := po_with_ad.clone() + mac := psiv_gen_tag(mut poad_clone, out, ad.len, c.mac_key, nonce) + if subtle.constant_time_compare(mac, tag) != 1 { + unsafe { + out.free() + mac.free() + } + return error('unmatching tag') + } + return out +} + +// The AEAD_CHACHA20_POLY1305 PSIV construct helpers +// + +// psiv_encrypt_internal is an internal encryption routine used by the core of psiv construct +// for encrypting (or decrypting) message. +@[direct_array_access] +fn psiv_encrypt_internal(plaintext []u8, key []u8, tag []u8, nonce []u8) ![]u8 { + tctr, trest := split_tag(tag) + mut ctr := binary.little_endian_u64(tctr) + + mut dst := []u8{cap: plaintext.len} + mut tc := []u8{len: 8} // counter buffer + mut s := chacha20.State{} + mut b64 := []u8{len: 64} // state buffer + + // split out plaintext messages into 64-bytes chunk, and process them + // chunk by chunk. + chunks := arrays.chunk[u8](plaintext, 64) + for chunk in chunks { + // loads current counter + binary.little_endian_put_u64(mut tc, ctr) + + // loads 64-bytes of merged key into state s and then perform chacha20 qround. + // then xor-ing every bytes of result with the bytes in chunk and appended into + // destination output buffer. + unpack_into_state(mut s, merge_drv_key(key, nonce, tc, trest)) + buf := chacha20_core(s) + pack64_from_state(mut b64, buf) + for i, v in chunk { + o := v ^ b64[i] + dst << o + } + // updates current counter and returns error on overflow. + ctr += 1 + if ctr == 0 { + return error('counter overflowing') + } + } + // reset (release) temporary allocated resources + unsafe { + tc.free() + s.reset() + b64.free() + } + return dst +} + +// psiv_gen_tag computes a tag from the key, nonce, and Poly1305 tag of the associated data +// and plaintext using the ChaCha20 permutation with the feed-forward, truncating the output. +@[direct_array_access] +fn psiv_gen_tag(mut po poly1305.Poly1305, input []u8, ad_len int, mac_key []u8, nonce []u8) []u8 { + // updates poly1305 mac by input message, associated data length and input length. + update_with_padding(mut po, input) + po.update(length_to_block(ad_len, input.len)) + + // produces 16-bytes of mac from current poly1305 state. + mut digest := []u8{len: tag_size} + po.finish(mut digest) + + // The tag was produced from derived key scrambled with chacha20 quarter round routine, + // and then truncating the output into 16-bytes tag. + drv_key := merge_drv_key(mac_key, nonce, digest[0..8], digest[8..16]) + mut x := chacha20.State{} + unpack_into_state(mut x, drv_key) + ws := chacha20_core(x) + + // truncating state output into tag sized bytes + mut tag := []u8{len: tag_size} + pack16_from_state(mut tag, ws) + + // releases (reset) temporary allocated resources + unsafe { + drv_key.free() + digest.free() + ws.reset() + x.reset() + } + return tag +} + +// psiv_init initializes and expands master key into desired psiv needed construct. +@[direct_array_access; inline] +fn psiv_init(key []u8) !([]u8, []u8, &poly1305.Poly1305) { + // derives some keys + pol_key := fk_k(key) + mac_key := fm_k(key) + enc_key := fe_k(key) + + mut x := chacha20.State{} + unpack_into_state(mut x, merge_drvk_zeros(pol_key)) + ws := chacha20_core(x) + + // For poly1305 mac, we only take a first 32-bytes of the state as a key + mut poly1305_key := []u8{len: 32} + pack32_from_state(mut poly1305_key, ws) + po := poly1305.new(poly1305_key)! + + // reset (release) temporary allocated resources + unsafe { + x.reset() + ws.reset() + pol_key.free() + poly1305_key.free() + } + return mac_key, enc_key, po +} + +// update_with_padding updates poly1305 mac with data, padding the tail block if necessary. +@[direct_array_access; inline] +fn update_with_padding(mut po poly1305.Poly1305, data []u8) { + po.update(data) + rem := data.len % tag_size + if rem != 0 { + block := []u8{len: tag_size} + po.update(block[..tag_size - rem]) + } +} + +// merge_drv_key merges provided bytes into 64-bytes key +@[direct_array_access; inline] +fn merge_drv_key(dkey []u8, nonce []u8, tag_ctr []u8, tag_rest []u8) []u8 { + mut x := []u8{len: 64} + + // 0..36 + for i := 0; i < dkey.len; i++ { + x[i] = dkey[i] + } + // 36..48 + for i := 0; i < nonce.len; i++ { + x[36 + i] = nonce[i] + } + // 48..56 + for i := 0; i < tag_ctr.len; i++ { + x[i + 48] = tag_ctr[i] + } + // 56..64 + for i := 0; i < tag_rest.len; i++ { + x[i + 56] = tag_rest[i] + } + + return x +} + +// merge_drvk_zeros merges derived key in dkey with zeros nonce and zeros tag into 64-bytes of key. +@[direct_array_access; inline] +fn merge_drvk_zeros(dkey []u8) []u8 { + mut x := []u8{len: 64} + _ := copy(mut x, dkey) + // the others was null bytes + return x +} + +// fk_k maps and transforms 32-bytes of key into 36-bytes of new key used to +// derive a poly1305 construction. +// See the papers doc on the 3.3 Additional Details part, on page 12-13 +@[direct_array_access; inline] +fn fk_k(k []u8) []u8 { + // fk(K) = K1 ∥ K2 ∥ K3 ∥ 03 ∥ K5 ∥ K6 ∥ K7 ∥ 0c ∥ K9 ∥ K10 ∥ K11 ∥ 30 + // ∥ K4 ∥ K8 ∥ K12 ∥ c0 ∥ K13 ∥ K14 ∥ · · · ∥ K32 + // with 0-based index + // K0 ∥ K1 ∥ K2 ∥ 03 ∥ K4 ∥ K5 ∥ K6 ∥ 0c ∥ K8 ∥ K9 ∥ K10 ∥ 30 + // ∥ K3 ∥ K7 ∥ K11 ∥ c0 ∥ K12 ∥ K13 ∥ · · · ∥ K31 + mut x := []u8{len: 36} + // 0 .. 4 + for i := 0; i < 3; i++ { + x[i] = k[i] + } + x[3] = u8(0x03) + + // 4 .. 8 + for i := 4; i < 7; i++ { + x[i] = k[i] + } + x[7] = 0x0c + + // 8 .. 12 + for i := 8; i < 11; i++ { + x[i] = k[i] + } + x[11] = 0x30 + + // 12 .. 16 + x[12] = k[3] + x[13] = k[7] + x[14] = k[11] + x[15] = 0xc0 + + // 16 .. 36 + for i := 16; i < 36; i++ { + x[i] = k[i - 4] + } + + return x +} + +// fm_k maps and transforms 32-bytes of key into 36-bytes of message authentication key. +// It later used for psiv tag generation. +@[direct_array_access; inline] +fn fm_k(k []u8) []u8 { + // fm(K) = K1 ∥ K2 ∥ K3 ∥ 05 ∥ K5 ∥ K6 ∥ K7 ∥ 0a ∥ K9 ∥ K10 ∥ K11 ∥ 50 ∥ + // K4 ∥ K8 ∥ K12 ∥ a0 ∥ K13 ∥ K14 ∥ · · · ∥ K32 , + // Or, with 0-based index + // fm(K) = K0 ∥ K1 ∥ K2 ∥ 05 ∥ K4 ∥ K5 ∥ K6 ∥ 0a ∥ K8 ∥ K9 ∥ K10 ∥ 50 ∥ + // K3 ∥ K7 ∥ K11 ∥ a0 ∥ K12 ∥ K13 ∥ · · · ∥ K31 + mut x := []u8{len: 36} + // 0 .. 4 + for i := 0; i < 3; i++ { + x[i] = k[i] + } + x[3] = u8(0x05) + + // 4 .. 8 + for i := 4; i < 7; i++ { + x[i] = k[i] + } + x[7] = 0x0a + + // 8 .. 12 + for i := 8; i < 11; i++ { + x[i] = k[i] + } + x[11] = 0x50 + + // 12 .. 16 + x[12] = k[3] + x[13] = k[7] + x[14] = k[11] + x[15] = 0xa0 + + // 16 .. 36 + for i := 16; i < 36; i++ { + x[i] = k[i - 4] + } + + return x +} + +// fe_k maps and transforms 32-bytes of key into 36-bytes of new encryption key +@[direct_array_access; inline] +fn fe_k(k []u8) []u8 { + // fe(K) = K1 ∥ K2 ∥ K3 ∥ 06 ∥ K5 ∥ K6 ∥ K7 ∥ 09 ∥ K9 ∥ K10 ∥ K11 ∥ 60 ∥ + // K4 ∥ K8 ∥ K12 ∥ 90 ∥ K13 ∥ K14 ∥ · · · ∥ K32 + // Or, with 0-based index + // fe(K) = K0 ∥ K1 ∥ K2 ∥ 06 ∥ K4 ∥ K5 ∥ K6 ∥ 09 ∥ K8 ∥ K9 ∥ K10 ∥ 60 ∥ + // K3 ∥ K7 ∥ K11 ∥ 90 ∥ K12 ∥ K13 ∥ · · · ∥ K31 + mut x := []u8{len: 36} + // 0 .. 4 + for i := 0; i < 3; i++ { + x[i] = k[i] + } + x[3] = u8(0x06) + + // 4 .. 8 + for i := 4; i < 7; i++ { + x[i] = k[i] + } + x[7] = 0x09 + + // 8 .. 12 + for i := 8; i < 11; i++ { + x[i] = k[i] + } + x[11] = 0x60 + + // 12 .. 16 + x[12] = k[3] + x[13] = k[7] + x[14] = k[11] + x[15] = 0x90 + + // 16 .. 36 + for i := 16; i < 36; i++ { + x[i] = k[i - 4] + } + + return x +} + +// unpack_into_state deserializes (in little-endian form) 64-bytes of data in x into state s. +@[direct_array_access; inline] +fn unpack_into_state(mut s chacha20.State, x []u8) { + for i := 0; i < 16; i++ { + s[i] = binary.little_endian_u32(x[i * 4..(i + 1) * 4]) + } +} + +// pack64_from_state serializes state s into 64-bytes output in little-endian form. +@[direct_array_access; inline] +fn pack64_from_state(mut out []u8, s chacha20.State) { + for i, v in s { + binary.little_endian_put_u32(mut out[i * 4..(i + 1) * 4], v) + } +} + +// pack32_from_state serializes only a half of state s into 32-bytes output in little-endian form. +@[direct_array_access; inline] +fn pack32_from_state(mut out []u8, s chacha20.State) { + for i := 0; i < 8; i++ { + binary.little_endian_put_u32(mut out[i * 4..(i + 1) * 4], s[i]) + } +} + +// pack16_from_state serializes the first quartet of state s into 16-bytes output in little-endian form. +@[direct_array_access; inline] +fn pack16_from_state(mut out []u8, s chacha20.State) { + for i := 0; i < 4; i++ { + binary.little_endian_put_u32(mut out[i * 4..(i + 1) * 4], s[i]) + } +} + +// chacha20_core performs chacha20 quarter round on the state s. +// It returns a copy of updated state after quarter round. +@[direct_array_access; inline] +fn chacha20_core(s chacha20.State) chacha20.State { + mut ws := s.clone() + ws.qround(10) + for i := 0; i < 16; i++ { + ws[i] += s[i] + } + return ws +} + +// split_tag splits 16-bytes of tag into two's 8-bytes block. +@[direct_array_access; inline] +fn split_tag(tag []u8) ([]u8, []u8) { + return tag[0..8].clone(), tag[8..16].clone() +} + +// length_to_block transforms two's length in len1 and len2 into 16-bytes block +@[inline] +fn length_to_block(len1 int, len2 int) []u8 { + mut block := []u8{len: 16} + binary.little_endian_put_u64(mut block[0..8], u64(len1)) + binary.little_endian_put_u64(mut block[8..16], u64(len2)) + + return block +} diff --git a/vlib/x/crypto/chacha20poly1305/psiv_test.v b/vlib/x/crypto/chacha20poly1305/psiv_test.v new file mode 100644 index 000000000..b40c6d831 --- /dev/null +++ b/vlib/x/crypto/chacha20poly1305/psiv_test.v @@ -0,0 +1,207 @@ +// Copyright (c) 2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +// ChaCha20-Poly1305-PSIV test. +// The test was adapted from Rust reference implementation of ChaCha20-Poly1305-PSIV +module chacha20poly1305 + +import rand +import encoding.hex + +struct PsivKatTest { + key string + nonce string + ad string + tag string + pt string + ct string +} + +// This Konwn Answer Test (KAT) test material was adapted from this link: +// https://codeberg.org/Gusted/chacha20-poly1305-psiv/raw/branch/main/testdata/KAT/chacha20-poly1305-psiv.json +// +const psiv_kattest_data = [ + PsivKatTest{ + key: 'da5e8b4dc96a45cbf6868996cf9374968639a0e462993a5a4ce0f50c30387b4d' + nonce: 'baf068c8aa4fecc4485f0673' + ad: 'fb69bf2be138f95831e2f80cd6' + tag: 'b414135310ce4430cddcdc883d0b8533' + pt: '53bc' + ct: 'be6d' + }, + PsivKatTest{ + key: '8c5f631e8f9d29efa780d031232022a1d388aeb3f4d50592dd0d05d7cc5d4bc1' + nonce: 'e5ddfb49845b9dc73dfa4b10' + ad: '126ce447818b55571d725495e6614a6f' + tag: '6d5c42a83ad867653625c10f6ef05385' + pt: '31d5c270085a9f80ef90ebf732d928e8' + ct: 'ae44247314351bfca354474c71998195' + }, + PsivKatTest{ + key: '81432e9fa573a3e0aaa352f668a15b754c81502ed14c8ee6fc9ec0fb7100344d' + nonce: '78904fc961c52e65e13d302e' + ad: '663f149d40338426e81e5257991630202dd06ced12a2bca83f89dc7296541782' + tag: '4e43af952bb555961f2910633ece24fc' + pt: 'd0037b9ad2e00b2f698c26a791c0a67ee5a298929ce5ce2ba4999a5d89befa92' + ct: '6266ba8132a9193af5e9d8511c27166050e01571e55a3d27d3ab98e5b6a3c11b' + }, + PsivKatTest{ + key: 'faad2687fe7fcaad896a50dd005089a8fee8d95e79e3705d8136b5c45ef03d1a' + nonce: '2748826716da2a887ff63885' + ad: 'e85d908a97f6673421e1c4ad674d792e914972147191be9e2b1158f489eb47a41a594112cc74854c521d84029a959cfd' + tag: '5bf71bbda8e147f51524395009d1bbd2' + pt: '93dd95401f779b9fcd64261c94e861ed0947c378654dfe8cda6776a0a5e5d35e83cbb595cbd1b449b2278f17e81ac83f' + ct: 'e898487b800760a38f2727161264ac3e439606a94a0525f8eb0f4ade565cccbd25e3df2415b5cf71fbf9e871816de8d7' + }, + PsivKatTest{ + key: '6df081bcb85c439a1def71c8aaf69d3c885b992ba9c8271fd6c969f48e0b11c6' + nonce: 'ebb85860f597fc51715eb5d6' + ad: '3e00e080c3bffc3ec4b2e45dc09faaf9caae0a92fb74eb4b245c0a6aa0109d4932208478022ed98a6815cd57cd34e0cc2ad0b85a37cd03d955012e2f69e6a030' + tag: '8beaf75797d5f4c316b946cfb0de31ee' + pt: '8525879de99305c0fdc4edeb66dc229faaaebf111804253afba2a2878313dda53009531fee0ff5613e39ea694efe7093ae7b349851700e5f6075cd5cbd784df0' + ct: 'f194e1e2f8288afd6a8013d8a14eb32851fedb97ce9151c25ca7678ec7eb7eb62652e1176666799f379307e92c79263c948bc3b6cd9aeeded02d98fcedb1c94f' + }, + PsivKatTest{ + key: 'fce8cb2c75f84f98f44ae5a48dbbebe46536ebae80a054aeeaefbdceb3a1a956' + nonce: '86c43a87f6082ca50a2cf552' + ad: 'c1d6753875f2f1a7223ef8769b2136f171124ce1bf502ded1429439407f91fabcb16a354d958dba41a475de6137b2cbd4e4c2dcf3334e351d09f504db6f78ed783c92b622637304e4e50faab674040f8' + tag: '056b0315e62c7925d20495f5ee1824cc' + pt: 'caa171079f29612875d0bac2772e563add27281d91376bc72c1e9ad7fbff3af3910fe2c1d6432df3cda86ccf931fb439f22c492d3e1cc1dd5aecbd312045aacafffabf3d4ef8cadafa685a371f299c38' + ct: '19c86459a6b3aa8ce0a99849e995a633461642553c0b8b5935d67d796b3bfc46888575636dfa7c4f8343a302dcc0296089115d0713c8a84376dbdd3afdb15b41b494c35feab382a876dec957abf6990a' + }, + PsivKatTest{ + key: '8b119e13f8ad308e0d2a15284b45dad10de7c700f30148e3bf12c137eaa4125b' + nonce: 'ce81fc611e7f1160413e6d52' + ad: '65536d29922a97570818bda61970f2130260bd404447552dc1b8be4f4fb3a7d403479926c82e18846bed7c326b43b605cdd9a589f5467bbac21735ae8d6000477936e294dde2ab0e58da59aa847be8b85207e437c53c61c879e2365e62357203' + tag: 'c463dea4dc1b6ccd711aef9823da39e9' + pt: '4e38a5eda409058b6ccfc9759f73042b3a91fa7f85757346f8199567b92c4702524942753e3f1b06aa2bd0a65b60fc555bd6e737e121ef27084938c5149e42d9421feba9201d9a553128b84c7de17fe34754829307f11a184057a05fc408a2fb' + ct: 'd70af5d8999d64ac57cf477e5b49b22ef39c0b9cde96b10312bfda93bbe4f2d0901dc3081c0439df34fe991383c1773f27e7c13a04d7c52ff2696fc60d076428ab03d3e7ef19618861fa99577da350ec7745779660f093c0bec764b7afd2a7df' + }, + PsivKatTest{ + key: '9d1efe472184bcedc72fc0ef1e1462d8f1e5a62a7430e08df26d04026e6135d8' + nonce: 'b5141ce04e81f40113337293' + ad: 'dbb0d72f901383b584eb7ab4cd070ad0dae9de2904fb03ae39d09bf96b9d1cd75cbb196154323321b2ed8ecb7dc14ece42008c889cab063250d15050f3281e4df55afcef1f411837a8ccc7546f4c571846e38c5afd846eadae7171b01f93f57c841ad02819bc2bfc7de607ef19a73ec3' + tag: '4d12a32e158b02657bd4a7c9fa676529' + pt: 'd1e946809fb78c7e4e145870f70982cf950dcb18856fb96477f5fdfc3e542227456cb3c501494facd3c2f08a31bdb5198993db98346241fb54559a650067d97e45227fd89408d33c322b70eb081f5b5b3ddd084147bb0cf8826790deb4bb0b5cd34e92475f141353e6dbdc839ff44697' + ct: 'fd1422017e046f8940669371ed905eda2cc8f0b33586794bdbd99625ae7694c9c6bb0f6d3cf3353d0ed732241800063b45c16ca0ddf616d1f82ac1fbb2a42a96e6fde25e7439c064d45766bbcd277b8908e6561cb52bc494635a6d1b7c4d86b05bac9f647f9197efee25ebaa1ab9a22c' + }, + PsivKatTest{ + key: 'af8ce66d984e6507f1a810bf372324cd91e1d52a2d232148f4ad240ac1620059' + nonce: '7d2a7c08cdc00e531bd8ef56' + ad: '88d029543383ecbb02783ddd4419d0e5af3b389a666dff57882b5c49a1e0fb50bc78f7c8b2f0d5cabd832d3a10d8ee2d06a414ce4f5e645bb8b6c032e3f9d17cbe31b1313d5345f109c8251759c6a415dcc0340779b6bb40aea06ca323c6ab5690b713e1dea52fc3fe99926bacaca9d7888b6dd39569d534494a63cc602dcc9b' + tag: '12c1ee0e495c42ef85fc602fa4393934' + pt: '505c5d76005285cd7f4dd9196023d466b4bdae3e03a9ad5ef6b63def0a74442fe27132496935e010a6a15f785eaaa7367bc0b2ca9381873dbb80eb22701113ca5a996faf3d97c85bb16d62c581ee90ff73ebaa6f7a34f7a1acf784a854d73aecbb5aede2c197a73b1ad057a8d1c7e798e7a686a94dcfb8b1047bc9442d42c535' + ct: 'ce98619c023027e08eeb67c44b3801547191c01db1bb840164490da254f2196669347b9db669feb505a7126fe29c2c3eb1a5aeb7a46dbc8f3966316bfb39b8e189d5e5be09e42ac668a6dd218b72652d3851c2895982bb52f055a172b729aaa351dc213db04738395565b3e31b4a4bb84823e33066dda7aca1c473dff2eaf964' + }, + PsivKatTest{ + key: 'd58e34f08258aadad1cdf264b893c34b6d8c9ea4048640310b96b937e3e344e3' + nonce: '2b0a958f3e2dfc64431b3410' + ad: '88b216f01541dd888e4fd04443e638b4498758643583a7b04d13277d48e6096f3f4d33e3e387a1d90ac6d1a53c0574c1290099386834f803f48ff8c21b68d9650dcebd2232c9c2775bdf0fe28765bd1f5bf1392f9ba262264ef19f371fa1351055a74c8566098e9e861759ffe58e14cacc909fcaaeac4fbe7466613ddc09ded32cec9e4a014d64ab7eccafd58914ed1b' + tag: '91329d284bad6b733d3d48467d4cce45' + pt: '4be34f526d428683def93efc38d2c4f710ee7637aba54ff086d278128d3b68420c31982a772b15c5438158ed090d65c6f8f5cba4e8a320eea7d500d1c490fc6c59f8991ef58164f46c8b2685c08b2d85bff6b480d30d5ba14e3d1e96e28e880ed63a6c1738b6575334076a42522fb9b22c9164faa9fcc69881976e3773345dacf232a6b7e1a852ee6ddf8dd51f84e6b0' + ct: '06e5d414215a1e2aefdfae18c54fe721e21f163dcf6454babce5b3a5be2f219a110e9b893addc5177b6ad48ddd6a6f4981c6309f67e3101387646a8970ab13593c3ac62cd57c608fa7f5f4be8c214171eaa223e7d291c9ee2979339d74438ad7084526224e8df5909b37d6a0fb2a97be1b2f1d26ac8a8024edabe05ac8828f1eb8bb0a9236669bf75613264272e81f2a' + }, +] + +// Test for Known Answert Test (KAT) of ChaCha20-Poly1305-PSIV +// based on https://codeberg.org/Gusted/chacha20-poly1305-psiv/raw/branch/main/testdata/KAT/chacha20-poly1305-psiv.json +// Thanks to @tankf33der to point this test vectors. +fn test_aead_psiv_for_kat_tests() ! { + for i, item in psiv_kattest_data { + key := hex.decode(item.key)! + nonce := hex.decode(item.nonce)! + ad := hex.decode(item.ad)! + tag := hex.decode(item.tag)! + pt := hex.decode(item.pt)! + ct := hex.decode(item.ct)! + + // full aead ciphertext = ct + tag + mut cxt := []u8{} + cxt << ct + cxt << tag + + // psiv encryption + enc_msg := psiv_encrypt(pt, key, nonce, ad)! + assert cxt == enc_msg + + // decrypts it back + dec_msg := psiv_decrypt(enc_msg, key, nonce, ad)! + assert dec_msg == pt + } +} + +// test for internal encryption routine +fn test_psiv_insternal_encryption_of_encrypted_text_is_plaintext() ! { + for i := 0; i < 1024; i++ { + input := rand.bytes(i)! + key := rand.bytes(36)! + tag := rand.bytes(16)! + nonce := rand.bytes(12)! + + out := psiv_encrypt_internal(input, key, tag, nonce)! + + // encrypting this output with the same params was result in original input + awal := psiv_encrypt_internal(out, key, tag, nonce)! + assert awal == input + } +} + +// test for AEAD of ChaCha20Poly1305-PSIV encrypt and decrypt +fn test_psiv_aead_encryption_of_encrypted_text_is_plaintext() ! { + for i := 0; i < 1024; i++ { + input := rand.bytes(i)! + key := rand.bytes(32)! + nonce := rand.bytes(12)! + ad := rand.bytes(i)! + + // encrypt message input + out := psiv_encrypt(input, key, nonce, ad)! + // decrypting this output with the same params was resulting an original input + awal := psiv_decrypt(out, key, nonce, ad)! + assert awal == input + + // test with object-based construct + mut c := new_psiv(key)! + out1 := c.encrypt(input, nonce, ad)! + back1 := c.decrypt(out1, nonce, ad)! + assert back1 == input + unsafe { c.free() } + } +} + +fn test_for_wrong_tag() ! { + for i := 0; i < 1024; i++ { + input := rand.bytes(i)! + key := rand.bytes(32)! + nonce := rand.bytes(12)! + ad := rand.bytes(i)! + + mut out := psiv_encrypt(input, key, nonce, ad)! + out[out.len - tag_size] ^= 1 + + // decrypting would fail + _ := psiv_decrypt(out, key, nonce, ad) or { + assert err == error('unmatching tag') + continue + } + } +} + +fn test_chacha20_core() ! { + // null input + s := [16]u32{} + x0 := chacha20_core(s) + assert x0 == s + + // u32 input + u32input := [u32(0x61707865), 0x3320646e, 0x79622d32, 0x6b206574, 0x03020100, 0x07060504, + 0x0b0a0908, 0x0f0e0d0c, 0x13121110, 0x17161514, 0x1b1a1918, 0x1f1e1d1c, 0x00000001, + 0x09000000, 0x4a000000, 0x00000000]! + // expected output + exp_x1 := [u32(0xe4e7f110), 0x15593bd1, 0x1fdd0f50, 0xc47120a3, 0xc7f4d1c7, 0x0368c033, + 0x9aaa2204, 0x4e6cd4c3, 0x466482d2, 0x09aa9f07, 0x05d7c214, 0xa2028bd9, 0xd19c12b5, + 0xb94e16de, 0xe883d0cb, 0x4e3c50a2]! + x1 := chacha20_core(u32input) + assert x1 == exp_x1 +} -- 2.39.5