// 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 }