Initial project import

This commit is contained in:
Agila Forge Importer committed 2026-07-13 15:20:32 +00:00
commit 23f2d29e32
11957 files changed
+1754577

No files matched your search

@@ -0,0 +1,46 @@
# chacha20poly1305
Chacha20Poly1305 Authenticated Encryption with Additional Data (AEAD) module for V Language
This module provides authenticated encryption with additional data (AEAD) algorithm in V Language.
Its backed by experimental `x.crypto.chacha20` symmetric key stream cipher encryption
module and `x.crypto.poly1305` message authentication code (MAC) module.
> [!Warning]
> 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
import encoding.hex
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()
// sets your secure random key
key := hex.decode('808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f')!
// give yours nonce
nonce := hex.decode('070000004041424344454647')!
// yours additional data
aad := hex.decode('50515253c0c1c2c3c4c5c6c7')!
// lets doing authenticated encryption
ciphertext := chacha20poly1305.encrypt(message, key, nonce, aad)!
// lets perform decryption back
plaintext := chacha20poly1305.decrypt(ciphertext, key, nonce, aad)!
assert plaintext == message
}
```
@@ -0,0 +1,184 @@
// 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('')
}
@@ -0,0 +1,249 @@
// Copyright (c) 2024 blackshirt.
// Use of this source code is governed by an MIT license
// that can be found in the LICENSE file.
//
// AEAD_CHACHA20_POLY1305 is an authenticated encryption with additional data algorithm.
// The inputs to AEAD_CHACHA20_POLY1305 are:
// A 256-bit key
// A 64-bit nonce, 96-bit (or bigger 192 bit nonce) -- different for each invocation with the same key
// An arbitrary length plaintext
// Arbitrary length additional authenticated data (AAD)
module chacha20poly1305
import crypto.cipher
import encoding.binary
import crypto.internal.subtle
import x.crypto.chacha20
import x.crypto.poly1305
// key_size is the size of key (in bytes) which the Chacha20Poly1305 AEAD accepts.
pub const key_size = 32
// orig_nonce_size is the size (in bytes) of nonce of the original (DJ Bernstein) variant
// which the Chacha20Poly1305 AEAD accepts.
pub const orig_nonce_size = 8
// nonce_size is the size of the standard nonce (in bytes) which the Chacha20Poly1305 AEAD accepts.
pub const nonce_size = 12
// nonce_size is the size of the extended nonce (in bytes) which the Chacha20Poly1305 AEAD accepts.
pub const x_nonce_size = 24
// tag_size is the size of the message authenticated code (in bytes) produced by Chacha20Poly1305 AEAD.
pub const tag_size = 16
// encrypt does one-shot encryption of given plaintext with associated key, nonce and additional data.
// It return ciphertext output and authenticated tag appended into it.
pub fn encrypt(plaintext []u8, key []u8, nonce []u8, ad []u8, opt chacha20.Options) ![]u8 {
mut c := new(key, nonce.len, opt)!
return c.encrypt(plaintext, nonce, ad)!
}
// decrypt does one-shot decryption of given ciphertext with associated key, nonce and additional data.
// It return plaintext output and verify if resulting tag is a valid message authenticated code (mac)
// for given message, key and additional data.
pub fn decrypt(ciphertext []u8, key []u8, nonce []u8, ad []u8, opt chacha20.Options) ![]u8 {
mut c := new(key, nonce.len, opt)!
return c.decrypt(ciphertext, nonce, ad)!
}
// Chacha20Poly1305 represents AEAD algorithm backed by `x.crypto.chacha20` and `x.crypto.poly1305`.
@[noinit]
struct Chacha20Poly1305 {
mut:
key []u8 = []u8{len: key_size}
nsize int
opt chacha20.Options // for XChaCha20 construct
}
// new creates a new Chacha20Poly1305 AEAD instance with given 32 bytes of key
// and the nonce size in nsize. The nsize should be 8, 12 or 24 length, otherwise it would return error.
pub fn new(key []u8, nsize int, opt chacha20.Options) !&cipher.AEAD {
if key.len != key_size {
return error('chacha20poly1305: bad key size')
}
if nsize != orig_nonce_size && nsize != nonce_size && nsize != x_nonce_size {
return error('chacha20poly1305: bad nonce size supplied, its should 8, 12 or 24')
}
return &Chacha20Poly1305{
key: key
nsize: nsize
opt: opt
}
}
// nonce_size returns the size of underlying nonce (in bytes) of AEAD algorithm.
pub fn (c Chacha20Poly1305) nonce_size() int {
return c.nsize
}
// overhead returns maximum difference between the lengths of a plaintext to be encrypted and
// ciphertext's output. In the context of Chacha20Poly1305, `.overhead() == .tag_size`.
pub fn (c Chacha20Poly1305) overhead() int {
return tag_size
}
// encrypt encrypts plaintext, along with nonce and additional data and generates
// authenticated tag appended into ciphertext's output.
pub fn (c Chacha20Poly1305) encrypt(plaintext []u8, nonce []u8, ad []u8) ![]u8 {
// makes sure if the nonce length is matching with internal nonce size
if nonce.len != c.nonce_size() {
return error('chacha20poly1305: unmatching nonce size')
}
// check if the plaintext length doesn't exceed the amount of limit.
// its comes from the internal of chacha20 mechanism, where the counter are u32
// with the facts of chacha20 operates on 64 bytes block, we can measure the amount
// of encrypted data possible in a single invocation, ie.,
// amount = (2^32-1)*64 = 274,877,906,880 bytes, or nearly 256 GB
if u64(plaintext.len) > (u64(1) << 38) - 64 {
panic('chacha20poly1305: plaintext too large')
}
if ad.len > max_u64 {
return error('chacha20poly1305: something bad in your additional data')
}
return c.generic_crypt(plaintext, nonce, ad, .encrypt)!
}
// decrypt decrypts ciphertext along with provided nonce and additional data.
// Decryption is similar with the encryption process with slight differences in:
// The roles of ciphertext and plaintext are reversed, so the ChaCha20 encryption
// function is applied to the ciphertext, producing the plaintext.
// The Poly1305 function is still run on the AAD and the ciphertext, not the plaintext.
// The calculated mac is bitwise compared to the received mac.
// The message is authenticated if and only if the tags match, return error if failed to verify.
pub fn (c Chacha20Poly1305) decrypt(ciphertext []u8, nonce []u8, ad []u8) ![]u8 {
// Preliminary check
if ciphertext.len < tag_size {
return error('chacha20poly1305: ciphertext length does not meet minimum required length')
}
if nonce.len != c.nonce_size() {
return error('chacha20poly1305: unmatching nonce size')
}
// ciphertext max = plaintext max length + tag length
// ie, (2^32-1)*64 + overhead = (u64(1) << 38) - 64 + 16 = 274,877,906,896 octets.
if u64(ciphertext.len) > (u64(1) << 38) - 48 {
return error('chacha20poly1305: ciphertext too large')
}
return c.generic_crypt(ciphertext, nonce, ad, .decrypt)!
}
// Helpers
// generic_crypt direction
enum Mode {
encrypt = 0
decrypt = 1
}
// generic_crypt does generic encryption or decryption based on the mode flag was passed.
// See AEAD Construction at https://datatracker.ietf.org/doc/html/rfc8439#section-2.8
@[direct_array_access; inline]
fn (c Chacha20Poly1305) generic_crypt(msg []u8, nonce []u8, ad []u8, mode Mode) ![]u8 {
// Setup some values
mut src := unsafe { msg }
mut mac := []u8{} // used in decryption
if mode == .decrypt {
src = unsafe { msg[0..msg.len - c.overhead()] }
mac = unsafe { msg[msg.len - c.overhead()..] }
}
// generates 32-bytes of one-time key for later poly1305 operation
mut otkey := []u8{len: key_size}
mut s := chacha20.new_cipher(c.key, nonce, c.opt)!
s.encrypt(mut otkey, otkey)!
// destination buffer, with overhead spaces for generated tag without reallocating
mut dst := []u8{len: src.len, cap: src.len + c.overhead()}
// Next, the ChaCha20 encryption function is called to encrypt (decrypt) message input,
// using the same key and nonce, and with the initial counter set to 1.
s.set_counter(1)
s.encrypt(mut dst, src)!
// Finally, the Poly1305 function is called with the Poly1305 key calculated above
// to build message authentication code (tag).
// length of constructed message
cm_length := if mode == .encrypt {
length_constructed_msg(ad, dst)
} else {
length_constructed_msg(ad, src)
}
mut constructed_msg := []u8{len: cm_length}
if mode == .encrypt {
construct_msg(mut constructed_msg, ad, dst)
} else {
construct_msg(mut constructed_msg, ad, src)
}
mut tag := []u8{len: tag_size}
mut po := poly1305.new(otkey)!
po.update(constructed_msg)
po.finish(mut tag)
// If this a decryption mode, lets verify whether this calculated tag was matching
// with the supplied mac, otherwise return error on fails and free allocated resources.
if mode == .decrypt {
if subtle.constant_time_compare(mac, tag) != 1 {
// free allocated resource
unsafe {
s.free()
tag.free()
otkey.free()
dst.free()
constructed_msg.free()
}
return error('chacha20poly1305: unmatching tag')
} else {
// return the decrypted message (plaintext) when the tag was matching
return dst
}
}
// In the encryption mode, appends the tag into end of destination buffer
dst << tag
return dst
}
// pad x to 16 bytes block
@[direct_array_access; inline]
fn pad_to_16(x []u8) []u8 {
if x.len % 16 == 0 {
return x
}
mut out := []u8{len: x.len + (16 - x.len % 16)}
_ := copy(mut out, x)
return out
}
// The length of padded x
@[inline]
fn length_pad_to_16(x []u8) int {
if x.len % 16 == 0 {
return x.len
}
//
return x.len + (16 - x.len % 16)
}
// The length of constructed message
@[inline]
fn length_constructed_msg(ad []u8, bytes []u8) int {
mut n := 0
n += length_pad_to_16(ad)
n += length_pad_to_16(bytes)
n += 16 // 2 * 8 bytes
return n
}
// construct_msg builds a message for later usage and stored into out.
// The last step on the AEAD Construction on the how the message was constructed.
// See the details on the [2.8](https://datatracker.ietf.org/doc/html/rfc8439#section-2.8)
// The message constructed as a concatenation of the following:
// * padded to multiple of 16 bytes block of the additional data bytes
// * padded to multiple of 16 bytes block of the ciphertext (or plaintext) bytes
// * The length of the additional data in octets (as a 64-bit little-endian integer).
// * The length of the ciphertext (or plaintext) in octets (as a 64-bit little-endian integer).
// Assumed the output buffer length was correctly initialized.
@[direct_array_access; inline]
fn construct_msg(mut out []u8, ad []u8, bytes []u8) {
n0 := copy(mut out, pad_to_16(ad))
n1 := copy(mut out[n0..], pad_to_16(bytes))
binary.little_endian_put_u64(mut out[n0 + n1..], u64(ad.len))
binary.little_endian_put_u64(mut out[n0 + n1 + 8..], u64(bytes.len))
}
@@ -0,0 +1,229 @@
import encoding.hex
import x.crypto.chacha20
import x.crypto.poly1305
import x.crypto.chacha20poly1305
const key_size = chacha20poly1305.key_size
const tag_size = chacha20poly1305.tag_size
// Example and Test Vector for AEAD_CHACHA20_POLY1305
// https://datatracker.ietf.org/doc/html/rfc8439#section-2.8.2
fn test_encrypt_example_test() ! {
plaintext :=
"Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it.".bytes()
aad := hex.decode('50515253c0c1c2c3c4c5c6c7')!
key := hex.decode('808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f')!
iv := '4041424344454647'
fixed := '07000000'
nonce := hex.decode(fixed + iv)!
exp_onetime_key :=
hex.decode('7bac2b252db447af09b67a55a4e955840ae1d6731075d9eb2a9375783ed553ff')!
ncsize := nonce.len
mut c := chacha20poly1305.new(key, ncsize)!
mut cs := chacha20.new_cipher(key, nonce)!
// and then performing Chacha20 block function
mut polykey := []u8{len: key_size}
cs.xor_key_stream(mut polykey, polykey)
assert polykey == exp_onetime_key
exp_ciphertext :=
hex.decode('d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116')!
expected_tag := hex.decode('1ae10b594f09e26a7e902ecbd0600691')!
cipherout := c.encrypt(plaintext, nonce, aad)!
// cipherout is concatenation of ciphertext and overhead (tag) bytes
ciphertext := cipherout[0..cipherout.len - tag_size]
mac := cipherout[cipherout.len - tag_size..]
assert ciphertext == exp_ciphertext
assert mac == expected_tag
}
// A.5. ChaCha20-Poly1305 AEAD Decryption test
fn test_aead_decrypt_vector_test_51() ! {
key := hex.decode('1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0')!
aad := hex.decode('f33388860000000000004e91')!
nonce := hex.decode('000000000102030405060708')!
ciphertext :=
hex.decode('64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb24c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c8559797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523eaf4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a1049e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29a6ad5cb4022b02709b')!
// Received Tag
expected_tag := hex.decode('eead9d67890cbb22392336fea1851f38')!
// Ciphertext msg is concatenation of ciphertext and tag
mut ciphertext_msg := []u8{}
ciphertext_msg << ciphertext
ciphertext_msg << expected_tag
ncsize := nonce.len
mut c := chacha20poly1305.new(key, ncsize)!
mut cs := chacha20.new_cipher(key, nonce)!
// and then performing Chacha20 block function
mut polykey := []u8{len: key_size}
cs.xor_key_stream(mut polykey, polykey)
expected_otk := hex.decode('bdf04aa95ce4de8995b14bb6a18fecaf26478f50c054f563dbc0a21e261572aa')!
assert polykey == expected_otk
constructed_msg :=
hex.decode('f33388860000000000004e910000000064a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb24c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c8559797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523eaf4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a1049e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29a6ad5cb4022b02709b000000000000000c000000000000000901000000000000')!
mut mac := []u8{len: tag_size}
poly1305.create_tag(mut mac, constructed_msg, polykey)!
assert mac == expected_tag
expected_plaintext :=
hex.decode('496e7465726e65742d4472616674732061726520647261667420646f63756d656e74732076616c696420666f722061206d6178696d756d206f6620736978206d6f6e74687320616e64206d617920626520757064617465642c207265706c616365642c206f72206f62736f6c65746564206279206f7468657220646f63756d656e747320617420616e792074696d652e20497420697320696e617070726f70726961746520746f2075736520496e7465726e65742d447261667473206173207265666572656e6365206d6174657269616c206f7220746f2063697465207468656d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67726573732e2fe2809d')!
plaintext := c.decrypt(ciphertext_msg, nonce, aad)!
assert plaintext == expected_plaintext
}
fn test_decrypt_and_verify_tag() ! {
for i, c in aead_cases {
key := hex.decode(c.key)!
aad := hex.decode(c.aad)!
nonce := hex.decode(c.nonce)!
msg := hex.decode(c.msg)!
exp_out := hex.decode(c.out)!
exp_tag := hex.decode(c.mac)!
ncsize := nonce.len
mut co := chacha20poly1305.new(key, ncsize)!
ciphertext := co.encrypt(msg, nonce, aad)!
text0 := ciphertext[0..ciphertext.len - co.overhead()]
mac0 := ciphertext[ciphertext.len - co.overhead()..]
assert text0 == exp_out
assert mac0 == exp_tag
plaintext := co.decrypt(ciphertext, nonce, aad)!
assert plaintext == msg
}
}
struct AeadCase {
key string
aad string
nonce string
msg string
out string
mac string
}
// from libressl vector test at https://fossies.org/linux/libressl/tests/aeadtests.txt
const aead_cases = [
AeadCase{
key: '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'
aad: '50515253c0c1c2c3c4c5c6c7'
nonce: '404142434445464748494a4b4c4d4e4f5051525354555657'
msg: '4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e'
out: 'bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b4522f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff921f9664c97637da9768812f615c68b13b52e'
mac: 'c0875924c1c7987947deafd8780acf49'
},
AeadCase{
key: '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'
aad: 'c0c1c2c3c4c5c6c7d0d1d2d3d4d5d6d72e202500000000090000004529000029'
nonce: 'a0a1a2a31011121314151617'
msg: '0000000c000040010000000a00'
out: '610394701f8d017f7c12924889'
mac: '6b71bfe25236efd7cdc67066906315b2'
},
AeadCase{
// Test vector from RFC7539 2.8.2
// AEAD: chacha20-poly1305
key: '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'
nonce: '070000004041424344454647'
msg: '4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e'
aad: '50515253c0c1c2c3c4c5c6c7'
out: 'd31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116'
mac: '1ae10b594f09e26a7e902ecbd0600691'
},
AeadCase{
//# Test vector from RFC7539 Appendix A.5
// AEAD: chacha20-poly1305
key: '1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0'
nonce: '000000000102030405060708'
msg: '496e7465726e65742d4472616674732061726520647261667420646f63756d656e74732076616c696420666f722061206d6178696d756d206f6620736978206d6f6e74687320616e64206d617920626520757064617465642c207265706c616365642c206f72206f62736f6c65746564206279206f7468657220646f63756d656e747320617420616e792074696d652e20497420697320696e617070726f70726961746520746f2075736520496e7465726e65742d447261667473206173207265666572656e6365206d6174657269616c206f7220746f2063697465207468656d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67726573732e2fe2809d'
aad: 'f33388860000000000004e91'
out: '64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb24c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c8559797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523eaf4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a1049e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29a6ad5cb4022b02709b'
mac: 'eead9d67890cbb22392336fea1851f38'
},
AeadCase{
//# Test vector from RFC7634 Appendix A
// AEAD: chacha20-poly1305
key: '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f'
nonce: 'a0a1a2a31011121314151617'
msg: '45000054a6f200004001e778c6336405c000020508005b7a3a080000553bec100007362708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f303132333435363701020204'
aad: '0102030400000005'
out: '24039428b97f417e3c13753a4f05087b67c352e6a7fab1b982d466ef407ae5c614ee8099d52844eb61aa95dfab4c02f72aa71e7c4c4f64c9befe2facc638e8f3cbec163fac469b502773f6fb94e664da9165b82829f641e0'
mac: '76aaa8266b7fb0f7b11b369907e1ad43'
},
]
struct ChapolyTest {
plaintext string
aad string
key string
nonce string
out string
}
// this test data come from golang vector tests of the same module
fn test_cha20poly1305_vector_data() ! {
for i, t in chapoly_testcases {
plaintext := hex.decode(t.plaintext)!
aad := hex.decode(t.aad)!
key := hex.decode(t.key)!
nonce := hex.decode(t.nonce)!
// This output from golang was concatenation of ciphertext+tag
out := hex.decode(t.out)!
// split out
out_ciphertext := out[..plaintext.len]
out_tag := out[plaintext.len..]
ncsize := nonce.len
mut co := chacha20poly1305.new(key, ncsize)!
ciphertext := co.encrypt(plaintext, nonce, aad)!
assert ciphertext[0..ciphertext.len - co.overhead()] == out_ciphertext
assert ciphertext[ciphertext.len - co.overhead()..] == out_tag
assert ciphertext == out
message := co.decrypt(ciphertext, nonce, aad)!
assert message == plaintext
}
}
const chapoly_testcases = [
ChapolyTest{'', '', '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f', '070000004041424344454647', 'a0784d7a4716f3feb4f64e7f4b39bf04'},
ChapolyTest{'4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e', '50515253c0c1c2c3c4c5c6c7', '808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f', '404142434445464748494a4b4c4d4e4f5051525354555657', 'bd6d179d3e83d43b9576579493c0e939572a1700252bfaccbed2902c21396cbb731c7f1b0b4aa6440bf3a82f4eda7e39ae64c6708c54c216cb96b72e1213b4522f8c9ba40db5d945b11b69b982c1bb9e3f3fac2bc369488f76b2383565d3fff921f9664c97637da9768812f615c68b13b52ec0875924c1c7987947deafd8780acf49'},
ChapolyTest{'1400000cebccee3bf561b292340fec60', '00000000000000001603030010', 'a5117e70953568bf750862df9e6f92af81677c3a188e847917a4a915bda7792e', '129039b5572e8a7a8131f76a', '2b487a2941bc07f3cc76d1a531662588ee7c2598e59778c24d5b27559a80d163'},
ChapolyTest{'0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', '00000000000000000000000000', 'a5117e70953568bf750862df9e6f92af81677c3a188e847917a4a915bda7792e', '129039b5572e8a7a8131f76a', '3f487a25aa70e9c8391763370569c9e83b7650dd1921c8b78869f241f25d2096c910b180930c5b8747fd90959fe8ca2dcadb4fa50fa1439f916b2301e1cc0810d6725775d3ab86721700f96e22709b0a7a8bef32627dd929b2dd3ba15772b669062bb558bc92e6c241a1d60d9f0035e80c335f854815fe1138ab8af653eab3e122135feeec7dfaba1cc24af82a2b7acccdd824899a7e03cc29c25be8a4f56a66673845b93bae1556f09dafc89a0d22af207718e2a6bb022e9d917597295992ea3b750cc0e7a7c3d33b23c5a8aeab45f5bb542f6c9e6c1747ae5a344aff483ba38577ad534b33b3abc7d284776ea33ed488c2a2475648a4fcda561745ea7787ed60f2368deb27c75adce6ff9b6cc6de1f5e72a741e2d59f64751b3ae482d714e0c90e83c671ff98ed611823afb39e6e5019a6ba548a2a72e829c7b7b4a101ac9deb90a25d3e0c50d22e1fc26c7c02296fa13c6d9c14767f68aaf46450a8d0fd5feb60d9d73c6e68623425b4984a79d619dd6bf896459aa77a681ec9c1a97f645e121f47779b051f8948a817f84d1f55da170d5bbbaf2f64e18b97ed3fd822db2819f523314f1e5ac72e8f69bbe6c87c22daddb0e1ac6790f8534071de2f258064b99789bfb165b065b8fe96f9127cd7dca9f7cb0368420f1e802faa3ca23792f2a5b93773dd405e71c320b211b54f7a26626b03c060e1ab87f32ac588abfa056ce090bd7c69913a700c80f325bfe824fa'},
ChapolyTest{'0967de57eefe1aaa999b9b746d88a1a248000d8734e0e938c6aa87', 'e4f0a3a4f90a8250f8806aa319053e8d73c62f150e2f239563037e9cc92823ad18c65111d0d462c954cc6c6ed2aafb45702a5a7e597d13bd8091594ab97cf7d1', 'f2db28620582e05f00f31c808475ca3df1c20e340bf14828352499466d79295f', '4349e2131d44dc711148dfe3', 'bd06cc144fdc0d8b735fa4452eabbf78fd4ad2966ea41a84f68da40ca2da439777bc2ba6c4ec2de0d003eb'},
ChapolyTest{'135a28170fe89066da7bcff3a9ccc1b27dfe942a6f47b23835ef746aaea63dc10066d90f4e697528e5451b8e11dd408fdbd4b94a1c6c82515bf7bc099df9cb9d5fa4acad0d22d5f267f18078cec107a995c1f3b12d7603886dbf910ab85ca7180053c50e759b00dc8c81555a425c03d71df6894a6c8cd2d94b64e303c08a1bc1dee1cf537ccf300850856292e1656aff5bf349c87f1ca1ca8085cd400fe901edcad04146a0714ef0f6b083d715edd670e020385f3cda29bc5ff6fc6edffe5ca9ce9def6e0e3d5f04ede2db02cfb2', '73afd2ab0e0e8537cae42dc6530dc4afb6934ca6', 'a5117e70953568bf750862df9e6f92af81677c3a188e847917a4a915bda7792e', '129039b5572e8a7a8131f76a', '2c125232a59879aee36cacc4aca5085a4688c4f776667a8fbd86862b5cfb1d57c976688fdd652eafa2b88b1b8e358aa2110ff6ef13cdc1ceca9c9f087c35c38d89d6fbd8de89538070f17916ecb19ca3ef4a1c834f0bdaa1df62aaabef2e117106787056c909e61ecd208357dd5c363f11c5d6cf24992cc873cf69f59360a820fcf290bd90b2cab24c47286acb4e1033962b6d41e562a206a94796a8ab1c6b8bade804ff9bdf5ba6062d2c1f8fe0f4dfc05720bd9a612b92c26789f9f6a7ce43f5e8e3aee99a9cd7d6c11eaa611983c36935b0dda57d898a60a0ab7c4b54'},
// some XChaCha20-Poly1305 vectors
ChapolyTest{'000000000000000000000000000000', '', '0000000000000000000000000000000000000000000000000000000000000000', '000000000000000000000000000000000000000000000000', '789e9689e5208d7fd9e1f3c5b5341fb2f7033812ac9ebd3745e2c99c7bbfeb'},
ChapolyTest{'02dc819b71875e49f5e1e5a768141cfd3f14307ae61a34d81decd9a3367c00c7', '', 'b7bbfe61b8041658ddc95d5cbdc01bbe7626d24f3a043b70ddee87541234cff7', 'e293239d4c0a07840c5f83cb515be7fd59c333933027e99c', '7a51f271bd2e547943c7be3316c05519a5d16803712289aa2369950b1504dd8267222e47b13280077ecada7b8795d535'},
ChapolyTest{'7afc5f3f24155002e17dc176a8f1f3a097ff5a991b02ff4640f70b90db0c15c328b696d6998ea7988edfe3b960e47824e4ae002fbe589be57896a9b7bf5578599c6ba0153c7c', 'd499bb9758debe59a93783c61974b7', '4ea8fab44a07f7ffc0329b2c2f8f994efdb6d505aec32113ae324def5d929ba1', '404d5086271c58bf27b0352a205d21ce4367d7b6a7628961', '26d2b46ad58b6988e2dcf1d09ba8ab6f532dc7e0847cdbc0ed00284225c02bbdb278ee8381ebd127a06926107d1b731cfb1521b267168926492e8f77219ad922257a5be2c5e52e6183ca4dfd0ad3912d7bd1ec968065'},
ChapolyTest{'', '', '48d8bd02c2e9947eae58327114d35e055407b5519c8019535efcb4fc875b5e2b', 'cc0a587a475caba06f8dbc09afec1462af081fe1908c2cba', 'fc3322d0a9d6fac3eb4a9e09b00b361e'},
ChapolyTest{'e0862731e5', '', '6579e7ee96151131a1fcd06fe0d52802c0021f214960ecceec14b2b8591f62cd', 'e2230748649bc22e2b71e46a7814ecabe3a7005e949bd491', 'e991efb85d8b1cfa3f92cb72b8d3c882e88f4529d9'},
ChapolyTest{'00c7dd8f440af1530b44', '', 'ffb733657c849d50ab4ab40c4ae18f8ee2f0acf7c907afefdc04dff3537fdff3', '02c6fd8032a8d89edbedcd1db024c09d29f08b1e74325085', '13dbcdb8c60c3ed28449a57688edfaea89e309ab4faa6d51e532'},
ChapolyTest{'f7e11b4d372ed7cb0c0e157f2f9488d8efea0f9bbe089a345f51bdc77e30d1392813c5d22ca7e2c7dfc2e2d0da67efb2a559058d4de7a11bd2a2915e', '', '194b1190fa31d483c222ec475d2d6117710dd1ac19a6f1a1e8e894885b7fa631', '6b07ea26bb1f2d92e04207b447f2fd1dd2086b442a7b6852', '25ae14585790d71d39a6e88632228a70b1f6a041839dc89a74701c06bfa7c4de3288b7772cb2919818d95777ab58fe5480d6e49958f5d2481431014a8f88dab8f7e08d2a9aebbe691430011d'},
ChapolyTest{'', '1e2b11e3', '70cd96817da85ede0efdf03a358103a84561b25453dee73735e5fb0161b0d493', '5ddeba49f7266d11827a43931d1c300dd47a3c33f9f8bf9b', '592fc4c19f3cddec517b2a00f9df9665'},
ChapolyTest{'81b3cb7eb3', 'efcfd0cf', 'a977412f889281a6d75c24186f1bfaa00dcc5132f0929f20ef15bbf9e63c4c91', '3f26ca997fb9166d9c615babe3e543ca43ab7cab20634ac5', '8e4ade3e254cf52e93eace5c46667f150832725594'},
ChapolyTest{'f955183b1f762d4536d3f6885ea7f5ac27414caf46c2e24a2fd3bd56b91c53d840fb657224565e0a6f686f8ba320e04a401057399d9a3d995ab17c13', 'c372ddc5', 'a188be3795b2ca2e69b6aa263244f0963c492d694cf6c9b705a1d7045f3f2a26', '51bb484ea094ee140474681e1c838e4442fd148de2cc345a', '48759a5ddfdd829d11de8e0c538ce4a9c475faab6912039b568ad92d737d172fc1eb0c00c3793de6dddbfacfdbbc7f44aeba33684e18005aa982b6fc6c556e63bb90ff7a1dde8153a63eabe0'},
ChapolyTest{'', 'e013cd0bfafd486d', 'af3d3ba094d38299ecb91c17bfe3d085da5bd42e11acf8acb5bc26a4be9a7583', '7dd63c14173831f109761b1c1abe18f6ba937d825957011b', '8bc685a7d9d501952295cd25d8c92517'},
ChapolyTest{'284b64597e', '31d013e53aa3ea79', '93c77409d7f805f97fe683b2dd6ee06152a5e918b3eed5b731acccffdcb2cc04', '3d331e90c4597cf0c30d1b7cfbd07bcb6ab927eda056873c', '3538a449d6c18d148a8c6cb76f1bc288657ac7036a'},
ChapolyTest{'acd08d4938a224b4cb2d723bf75420f3ea27b698fadd815bb7db9548a05651398644354334e69f8e4e5503bf1a6f92b38e860044a7edca6874038ce1', '28a137808d0225b8', 'a031203b963a395b08be55844d81af39d19b23b7cc24b21afa31edc1eea6edd6', 'e8b31c52b6690f10f4ae62ba9d50ba39fb5edcfb78400e35', '35cf39ba31da95ac9b661cdbd5e9c9655d13b8ff065c4ec10c810833a47a87d8057dd1948a7801bfe6904b49fed0aabfb3cd755a1a262d372786908ddcf64cae9f71cb9ed199c3ddacc50116'},
ChapolyTest{'', 'cda7ee2857e09e9054ef6806', 'd91dffb18132d8dd3d144a2f10ba28bc5df36cb60369f3b19893ec91db3cf904', 'ee56f19c62b0438da6a0d9e01844313902be44f84a6a4ce7', 'ccd48b61a5683c195d4424009eb1d147'},
ChapolyTest{'350f4c7ac2', '7c104b539c1d2ae022434cd6', 'cbb61e369117f9250f68fa707240c554359262a4d66c757f80e3aeb6920894fb', 'fbb14c9943444eac5413c6f5c8095451eddece02c9461043', 'b5c6a35865ed8e5216ff6c77339ee1ab570de50e51'},
ChapolyTest{'66234d7a5b71eef134d60eccf7d5096ee879a33983d6f7a575e3a5e3a4022edccffe7865dde20b5b0a37252e31cb9a3650c63e35b057a1bc200a5b5b', 'ccc2406f997bcae737ddd0f5', 'd009eeb5b9b029577b14d200b7687b655eedb7d74add488f092681787999d66d', '99319712626b400f9458dbb7a9abc9f5810f25b47fc90b39', '543a2bbf52fd999027ae7c297353f3ce986f810bc2382583d0a81fda5939e4c87b6e8d262790cd614d6f753d8035b32adf43acc7f6d4c2c44289538928564b6587c2fcb99de1d8e34ffff323'},
]
@@ -0,0 +1,498 @@
// 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 crypto.cipher
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
pol_key := fk_k(key)
mac_key := fm_k(key)
enc_key := fe_k(key)
mut s := chacha20.State{}
mut x64 := [64]u8{}
unsafe { vmemcpy(x64, pol_key[0], 36) }
unpack_into_state(mut s, x64)
ws := chacha20_core(s)
// 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)!
// set the values
c := &Chacha20Poly1305RE{
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 {
mut 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 authentication 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 {
mut 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 cipher.AEAD {
mut:
// flag that marked this instance should not be used again, set on .free call
done bool
// underlying derived keys, set on instance creation with new_psiv.
mac_key [36]u8
enc_key [36]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() {
// if it already marked as done, just return
if c.done {
return
}
unsafe {
// we reset derived keys
vmemset(c.mac_key, 0, c.mac_key.len)
vmemset(c.enc_key, 0, c.enc_key.len)
c.po = nil
}
// mark this instance as done, no longer usable
c.done = true
}
// 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 c.done {
return error('Chacha20Poly1305RE.encrypt: instance marked as done, no longer usable')
}
if nonce.len != nonce_size {
return error('Chacha20Poly1305RE.encrypt: bad nonce length, only support 12-bytes nonce')
}
// clone the initial poly1305 state and updates it with additional data ad
mut po_ad := c.po.clone()
update_with_padding(mut po_ad, ad)
// make a clone of updated poly1305
mut po_ad_clone := po_ad.clone()
// setup output buffer
mut out := []u8{len: plaintext.len + tag_size}
// write out an authentication tag into the last tag_size bytes of output
psiv_gen_tag(mut out[plaintext.len..], mut po_ad_clone, plaintext, ad.len, c.mac_key, nonce)
// write out authenticated encrypted plaintext into the first plaintext.len bytes of output
psiv_encrypt_internal(mut out[0..plaintext.len], plaintext, c.enc_key, out[plaintext.len..],
nonce)!
// return the result
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 c.done {
return error('Chacha20Poly1305RE.decrypt: instance marked as done, no longer usable')
}
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()..]
// updates a clone of poly1305 with additional data
mut po_with_ad := c.po.clone()
update_with_padding(mut po_with_ad, ad)
mut poad_clone := po_with_ad.clone()
// generates authenticated encrypted plaintext with associated mac
mut out := []u8{len: enc.len}
psiv_encrypt_internal(mut out, enc, c.enc_key, tag, nonce)!
mut mac := []u8{len: tag_size}
psiv_gen_tag(mut mac, mut poad_clone, out, ad.len, c.mac_key, nonce)
// check if authentication tag was matching or error on fails.
if subtle.constant_time_compare(mac, tag) != 1 {
unsafe {
out.free()
mac.free()
}
return error('unmatching tag')
}
// return the decrypted ciphertext
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(mut dst []u8, plaintext []u8, dkey [36]u8, tag []u8, nonce []u8) ! {
// loads the counter from the first 8-bytes of the tag input
mut ctr := binary.little_endian_u64(tag[0..8])
// setup some temporary vars
mut tc := []u8{len: 8} // counter buffer
mut s := chacha20.State{}
mut b64 := [64]u8{} // state buffer
mut tt := merge_drv_key(dkey, nonce, tag[0..8], tag[8..16])
mut j := 0
mut n := 0
// process for every bytes on plaintext input
for plaintext[n..].len > 0 {
// how many block of bytes available to process on
want_len := if plaintext[n..].len < 64 { plaintext[n..].len } else { 64 }
// loads current counter
binary.little_endian_put_u64(mut tc, ctr)
// updates derived keys with current counter, scrambled with chacha20_core and
// puts state into b64 buffer
unsafe { vmemcpy(tt[48], tc.data, tc.len) }
unpack_into_state(mut s, tt)
ws := chacha20_core(s)
pack64_from_state(mut b64, ws)
// xor every bytes of plaintext with bytes on b64, stores result in dst
for i in 0 .. want_len {
dst[j] = plaintext[j] ^ b64[i]
j++
}
// updates current counter and returns error on overflow.
ctr += 1
if ctr == 0 {
return error('counter overflowing')
}
n += want_len
}
// explicitly reset (release) temporary allocated resources and return the result.
unsafe {
tc.free()
s.reset()
vmemset(b64, 0, b64.len)
vmemset(tt, 0, tt.len)
}
}
// 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 out []u8, mut po poly1305.Poly1305, input []u8, ad_len int, mac_key [36]u8, nonce []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.
po.finish(mut out)
// 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, out[0..8], out[8..16])
mut x := chacha20.State{}
unpack_into_state(mut x, drv_key)
ws := chacha20_core(x)
// truncating state output into tag_sized bytes. As a note, we reuse buffer previously allocated
// to store the result.
pack16_from_state(mut out, ws)
// explicitly releases (reset) temporary allocated resources
unsafe {
vmemset(drv_key, 0, drv_key.len)
ws.reset()
x.reset()
}
}
// 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 [36]u8, nonce []u8, tag_ctr []u8, tag_rest []u8) [64]u8 {
mut x64 := [64]u8{}
// 0..36
for i := 0; i < dkey.len; i++ {
x64[i] = dkey[i]
}
// 36..48
for i := 0; i < nonce.len; i++ {
x64[36 + i] = nonce[i]
}
// 48..56
for i := 0; i < tag_ctr.len; i++ {
x64[i + 48] = tag_ctr[i]
}
// 56..64
for i := 0; i < tag_rest.len; i++ {
x64[i + 56] = tag_rest[i]
}
return x64
}
// 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(key []u8) [36]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 := [36]u8{}
// 0 .. 4
for i := 0; i < 3; i++ {
x[i] = key[i]
}
x[3] = u8(0x03)
// 4 .. 8
for i := 4; i < 7; i++ {
x[i] = key[i]
}
x[7] = 0x0c
// 8 .. 12
for i := 8; i < 11; i++ {
x[i] = key[i]
}
x[11] = 0x30
// 12 .. 16
x[12] = key[3]
x[13] = key[7]
x[14] = key[11]
x[15] = 0xc0
// 16 .. 36
for i := 16; i < 36; i++ {
x[i] = key[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(key []u8) [36]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 := [36]u8{}
// 0 .. 4
for i := 0; i < 3; i++ {
x[i] = key[i]
}
x[3] = u8(0x05)
// 4 .. 8
for i := 4; i < 7; i++ {
x[i] = key[i]
}
x[7] = 0x0a
// 8 .. 12
for i := 8; i < 11; i++ {
x[i] = key[i]
}
x[11] = 0x50
// 12 .. 16
x[12] = key[3]
x[13] = key[7]
x[14] = key[11]
x[15] = 0xa0
// 16 .. 36
for i := 16; i < 36; i++ {
x[i] = key[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(key []u8) [36]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 := [36]u8{}
// 0 .. 4
for i := 0; i < 3; i++ {
x[i] = key[i]
}
x[3] = u8(0x06)
// 4 .. 8
for i := 4; i < 7; i++ {
x[i] = key[i]
}
x[7] = 0x09
// 8 .. 12
for i := 8; i < 11; i++ {
x[i] = key[i]
}
x[11] = 0x60
// 12 .. 16
x[12] = key[3]
x[13] = key[7]
x[14] = key[11]
x[15] = 0x90
// 16 .. 36
for i := 16; i < 36; i++ {
x[i] = key[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 [64]u8) {
for i := 0; i < 16; i++ {
s[i] = u32(x[i * 4]) | (u32(x[i * 4 + 1]) << u32(8)) | (u32(x[i * 4 + 2]) << u32(16)) | (u32(x[
i * 4 + 3]) << u32(24))
}
}
// pack64_from_state serializes state s into 64-bytes output in little-endian form.
@[direct_array_access; inline]
fn pack64_from_state(mut out [64]u8, s chacha20.State) {
mut j := 0
for v in s {
out[j] = u8(v)
out[j + 1] = u8(v >> u32(8))
out[j + 2] = u8(v >> u32(16))
out[j + 3] = u8(v >> u32(24))
j += 4
}
}
// 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) {
mut j := 0
for i in 0 .. 8 {
out[j] = u8(s[i])
out[j + 1] = u8(s[i] >> u32(8))
out[j + 2] = u8(s[i] >> u32(16))
out[j + 3] = u8(s[i] >> u32(24))
j += 4
}
}
// 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) {
mut j := 0
for i in 0 .. 4 {
out[j] = u8(s[i])
out[j + 1] = u8(s[i] >> u32(8))
out[j + 2] = u8(s[i] >> u32(16))
out[j + 3] = u8(s[i] >> u32(24))
j += 4
}
}
// 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
}
// 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
}
@@ -0,0 +1,212 @@
// 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)!
mut dkey := [36]u8{}
unsafe { vmemcpy(dkey, key.data, key.len) }
tag := rand.bytes(16)!
nonce := rand.bytes(12)!
mut out := []u8{len: input.len}
psiv_encrypt_internal(mut out, input, dkey, tag, nonce)!
// encrypting this output with the same params was result in original input
// make a clone of ciphertext output as an input into internal encrypt routine
text := out.clone()
psiv_encrypt_internal(mut out, text, dkey, tag, nonce)!
assert out == 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
}
@@ -0,0 +1,68 @@
import encoding.hex
import x.crypto.chacha20poly1305
// test public function based encrypt and decrypt of chacha20poly1305
fn test_aead_chacha20poly1305_fn_encrypt_and_decrypt_operation() ! {
// the data mostly based on rfc
// gives yours secure key
key := hex.decode('1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0')!
// gives your additional data
aad := hex.decode('f33388860000000000004e91')!
// gives your nonce
nonce := hex.decode('000000000102030405060708')!
// your message to be encrypted and authenticated
plaintext :=
'Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as /“work in progress./”'.bytes()
ciphertext_out := chacha20poly1305.encrypt(plaintext, key, nonce, aad)!
ciphertext :=
hex.decode('64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb24c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c8559797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523eaf4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a1049e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29a6ad5cb4022b02709b')!
// Received Tag
expected_tag := hex.decode('eead9d67890cbb22392336fea1851f38')!
// aead output is concatenation of ciphertext and the tag
mut encrypted_output := []u8{}
encrypted_output << ciphertext
encrypted_output << expected_tag
assert ciphertext_out == encrypted_output
assert ciphertext_out[ciphertext_out.len - 16..] == expected_tag
// Lets decrypt back
message := chacha20poly1305.decrypt(ciphertext_out, key, nonce, aad)!
assert message == plaintext
}
// we going to use same data with the first test, but testing for instance based encrypt
fn test_aead_chacha20poly1305_instance_encrypt_and_decrypt_operation() ! {
// gives yours secure key
key := hex.decode('1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0')!
// gives your additional data
aad := hex.decode('f33388860000000000004e91')!
// gives your nonce
nonce := hex.decode('000000000102030405060708')!
// your message to be encrypted and authenticated
plaintext :=
hex.decode('496e7465726e65742d4472616674732061726520647261667420646f63756d656e74732076616c696420666f722061206d6178696d756d206f6620736978206d6f6e74687320616e64206d617920626520757064617465642c207265706c616365642c206f72206f62736f6c65746564206279206f7468657220646f63756d656e747320617420616e792074696d652e20497420697320696e617070726f70726961746520746f2075736520496e7465726e65742d447261667473206173207265666572656e6365206d6174657269616c206f7220746f2063697465207468656d206f74686572207468616e206173202fe2809c776f726b20696e2070726f67726573732e2fe2809d')!
// lets create chacha20poly1305 AEAD
mut co := chacha20poly1305.new(key, nonce.len)!
ciphertext_out := co.encrypt(plaintext, nonce, aad)!
// we have expected ciphertext
ciphertext :=
hex.decode('64a0861575861af460f062c79be643bd5e805cfd345cf389f108670ac76c8cb24c6cfc18755d43eea09ee94e382d26b0bdb7b73c321b0100d4f03b7f355894cf332f830e710b97ce98c8a84abd0b948114ad176e008d33bd60f982b1ff37c8559797a06ef4f0ef61c186324e2b3506383606907b6a7c02b0f9f6157b53c867e4b9166c767b804d46a59b5216cde7a4e99040c5a40433225ee282a1b0a06c523eaf4534d7f83fa1155b0047718cbc546a0d072b04b3564eea1b422273f548271a0bb2316053fa76991955ebd63159434ecebb4e466dae5a1073a6727627097a1049e617d91d361094fa68f0ff77987130305beaba2eda04df997b714d6c6f2c29a6ad5cb4022b02709b')!
// Received Tag
expected_tag := hex.decode('eead9d67890cbb22392336fea1851f38')!
// aead output is concatenation of ciphertext and the tag
mut encrypted_output := []u8{}
encrypted_output << ciphertext
encrypted_output << expected_tag
assert ciphertext_out == encrypted_output
assert ciphertext_out[ciphertext_out.len - 16..] == expected_tag
// Lets decrypt back
message := co.decrypt(ciphertext_out, nonce, aad)!
assert message == plaintext
}
File diff suppressed because it is too large. Load diff