From e0d20a0d9f5ecdd919cb3bc4a7d1da50cbc64f0c Mon Sep 17 00:00:00 2001 From: blackshirt Date: Wed, 24 Sep 2025 13:42:28 +0700 Subject: [PATCH] x.crypto.chacha20: expand the xchacha20 construction to support 64-bit counters (#25377) --- vlib/x/crypto/chacha20/README.md | 33 ++++++++++----- vlib/x/crypto/chacha20/chacha.v | 54 ++++++++++++------------- vlib/x/crypto/chacha20/chacha_test.v | 16 ++++++++ vlib/x/crypto/chacha20/stream.v | 19 +++++---- vlib/x/crypto/chacha20/stream_test.v | 4 +- vlib/x/crypto/chacha20/xchacha.v | 58 ++++++++++++++++++++++----- vlib/x/crypto/chacha20/xchacha_test.v | 4 +- 7 files changed, 128 insertions(+), 60 deletions(-) diff --git a/vlib/x/crypto/chacha20/README.md b/vlib/x/crypto/chacha20/README.md index ff5a44dc3..c0a3274c8 100644 --- a/vlib/x/crypto/chacha20/README.md +++ b/vlib/x/crypto/chacha20/README.md @@ -9,9 +9,10 @@ and inspired by Go version of the same library. ## Status This module already supports a 32-bit counter mode, and recently expanded to support a 64-bit counter mode. -The implemented features at the time of writing (2025/03/27) are: -- Support for standard IETF ChaCha20 with 32-bit counter, and 12 bytes nonce -- Support for extended ChaCha20 (XChaCha20) constructions with 24 bytes nonce +The implemented features at the time of writing (2025/09/22) are: +- Support for standard IETF ChaCha20 with 32-bit counter and 12 bytes nonce +- Support for eXtended ChaCha20 (XChaCha20) constructions with 24 bytes nonce, + with 32 or 64-bit counter. - Support for original ChaCha20 with 8 bytes nonce and 64-bit counter. Example @@ -22,11 +23,25 @@ import crypto.rand import x.crypto.chacha20 fn main() { - // Simplified examples to create cipher's with 64-bit counter - key := rand.read(32)! - nonce := rand.read(8)! - // just pass 32-bytes key and 8-bytes nonce to build cipher with 64-bit counter - mut c := chacha20.new_cipher(key, nonce)! + // 1. Creates a standard IETF variant, supplied with 12-bytes nonce + key0 := rand.read(32)! + nonce0 := rand.read(12)! + + mut c0 := chacha20.new_cipher(key0, nonce0)! + // and then, do work with the c0 that was just created + + // 2. Creates an original (DJ Bernstein) variant, supplied with 8-bytes nonce + key1 := rand.read(32)! + nonce1 := rand.read(8)! + + mut c1 := chacha20.new_cipher(key1, nonce1)! + // do with yours cipher + + // 3. Creates an eXtended ChaCha20 construction with 64-bit counter + key2 := rand.read(32)! + nonce2 := rand.read(24)! + + mut c2 := chacha20.new_cipher(key2, nonce2, use_64bit_counter: true)! // do with yours cipher } ``` @@ -56,4 +71,4 @@ fn main() { // should true assert input == input_back } -``` +``` \ No newline at end of file diff --git a/vlib/x/crypto/chacha20/chacha.v b/vlib/x/crypto/chacha20/chacha.v index 12346ff50..f19723dbc 100644 --- a/vlib/x/crypto/chacha20/chacha.v +++ b/vlib/x/crypto/chacha20/chacha.v @@ -32,12 +32,20 @@ enum CipherMode { original } +// Configuration options +@[params] +pub struct Options { +pub mut: + // currently, used for XChaCha20 construct + use_64bit_counter bool +} + // encrypt encrypts plaintext bytes with ChaCha20 cipher instance with provided key and nonce. // It was a thin wrapper around two supported nonce size, ChaCha20 with 96 bits // and XChaCha20 with 192 bits nonce. Internally, encrypt start with 0's counter value. // If you want more control, use Cipher instance and setup the counter by your self. -pub fn encrypt(key []u8, nonce []u8, plaintext []u8) ![]u8 { - mut stream := new_stream(key, nonce)! +pub fn encrypt(key []u8, nonce []u8, plaintext []u8, opt Options) ![]u8 { + mut stream := new_stream_with_options(key, nonce, opt)! mut dst := []u8{len: plaintext.len} stream.keystream_full(mut dst, plaintext)! unsafe { stream.reset() } @@ -46,8 +54,8 @@ pub fn encrypt(key []u8, nonce []u8, plaintext []u8) ![]u8 { // decrypt does reverse of encrypt operation by decrypting ciphertext with ChaCha20 cipher // instance with provided key and nonce. -pub fn decrypt(key []u8, nonce []u8, ciphertext []u8) ![]u8 { - mut stream := new_stream(key, nonce)! +pub fn decrypt(key []u8, nonce []u8, ciphertext []u8, opt Options) ![]u8 { + mut stream := new_stream_with_options(key, nonce)! mut dst := []u8{len: ciphertext.len} stream.keystream_full(mut dst, ciphertext)! unsafe { stream.reset() } @@ -71,8 +79,8 @@ mut: // with support for 64-bit counter, use 8 bytes length nonce's instead // If 24 bytes of nonce was provided, the XChaCha20 construction will be used. // It returns new ChaCha20 cipher instance or an error if key or nonce have any other length. -pub fn new_cipher(key []u8, nonce []u8) !&Cipher { - stream := new_stream(key, nonce)! +pub fn new_cipher(key []u8, nonce []u8, opt Options) !&Cipher { + stream := new_stream_with_options(key, nonce, opt)! return &Cipher{ Stream: stream } @@ -196,31 +204,19 @@ pub fn (mut c Cipher) set_counter(ctr u64) { c.Stream.set_ctr(ctr) } +// counter returns a current underlying counter value, as u64. +pub fn (c Cipher) counter() u64 { + return c.Stream.ctr() +} + // rekey resets internal Cipher's state and reinitializes state with the provided key and nonce pub fn (mut c Cipher) rekey(key []u8, nonce []u8) ! { unsafe { c.reset() } - stream := new_stream(key, nonce)! - c.Stream = stream -} - -// Helpers -// - -// derive_xchacha20_key_nonce derives a new key and nonces for extended -// variant of Standard IETF ChaCha20 variant. Its separated for simplify the access. -@[direct_array_access; inline] -fn derive_xchacha20_key_nonce(key []u8, nonce []u8) !([]u8, []u8) { - // Its only for x_nonce_size - if nonce.len != x_nonce_size { - return error('Bad nonce size for derive_xchacha20_key_nonce') + // we use c.Stream.mode info to get 64-bit counter capability + w64 := if c.mode == .original { true } else { false } + opt := Options{ + use_64bit_counter: w64 } - // derives a new key based on xchacha20 construction - // first 16 bytes of nonce used to derive the key - new_key := xchacha20(key, nonce[0..16])! - mut new_nonce := []u8{len: nonce_size} - // and the last of 8 bytes of nonce copied into new_nonce to build - // nonce_size length of new_nonce - _ := copy(mut new_nonce[4..12], nonce[16..24]) - - return new_key, new_nonce + stream := new_stream_with_options(key, nonce, opt)! + c.Stream = stream } diff --git a/vlib/x/crypto/chacha20/chacha_test.v b/vlib/x/crypto/chacha20/chacha_test.v index d2d965695..49b551ba4 100644 --- a/vlib/x/crypto/chacha20/chacha_test.v +++ b/vlib/x/crypto/chacha20/chacha_test.v @@ -1,6 +1,22 @@ +import rand import encoding.hex import x.crypto.chacha20 +// test for extended chacha20 construct with 64-bit counter support +fn test_xchacha20_cipher_with_64_counter() ! { + key := rand.bytes(32)! + // create 24-bytes nonce + xnonce := rand.bytes(24)! + mut c := chacha20.new_cipher(key, xnonce, use_64bit_counter: true)! + + // set counter value above 32-bit limit, to mark it support for 64-bit + c.set_counter(max_u32 + 1) + assert c.counter() == max_u32 + 1 + + c.set_counter(max_u64 - 2) + assert c.counter() == max_u64 - 2 +} + fn test_chacha20_block_function() ! { for val in blocks_testcases { key_bytes := hex.decode(val.key)! diff --git a/vlib/x/crypto/chacha20/stream.v b/vlib/x/crypto/chacha20/stream.v index d4d628e44..b52d785f7 100644 --- a/vlib/x/crypto/chacha20/stream.v +++ b/vlib/x/crypto/chacha20/stream.v @@ -42,9 +42,10 @@ mut: // vfmt on } -// new_stream creates a new chacha20 stream. The supported nonce size is 8, 12 or 24 bytes. +// new_stream_with_options creates a new chacha20 stream with provided options. +// The supported nonce size is 8, 12 or 24 bytes. @[direct_array_access; inline] -fn new_stream(key []u8, nonce []u8) !Stream { +fn new_stream_with_options(key []u8, nonce []u8, opt Options) !Stream { if key.len != key_size { return error('Bad key size provided') } @@ -52,23 +53,26 @@ fn new_stream(key []u8, nonce []u8) !Stream { mut mode := CipherMode.standard mut extended := false - // Based on the nonce.len supplied, it determines the variant (mode) and extended form of - // the new chacha20 stream intended to create. + // Based on the nonce.len and option supplied, it determines the variant (mode) and + // extended form of the new chacha20 stream intended to create. match nonce.len { nonce_size {} x_nonce_size { extended = true + if opt.use_64bit_counter { + mode = .original + } } orig_nonce_size { mode = .original } else { - return error('new_stream: unsupported nonce size') + return error('new_stream_with_options: unsupported nonce size') } } // if this an extended chacha20 construct, derives a new key and nonce - new_key, new_nonce := if mode == .standard && extended { - xkey, xnonce := derive_xchacha20_key_nonce(key, nonce)! + new_key, new_nonce := if extended { + xkey, xnonce := derive_xchacha20_key_nonce(key, nonce, opt.use_64bit_counter)! xkey, xnonce } else { // otherwise, use provided key and nonce @@ -110,6 +114,7 @@ fn new_stream(key []u8, nonce []u8) !Stream { // reset resets internal stream @[unsafe] fn (mut s Stream) reset() { + // we dont reset s.mode and s.extended unsafe { _ := vmemset(&s.key, 0, 32) _ := vmemset(&s.nonce, 0, 16) diff --git a/vlib/x/crypto/chacha20/stream_test.v b/vlib/x/crypto/chacha20/stream_test.v index f497e7f3c..37bb37f54 100644 --- a/vlib/x/crypto/chacha20/stream_test.v +++ b/vlib/x/crypto/chacha20/stream_test.v @@ -54,7 +54,7 @@ fn test_state_of_chacha20_block_simple() ! { nonce := '000000090000004a00000000' nonce_bytes := hex.decode(nonce)! - mut stream := new_stream(key_bytes, nonce_bytes)! + mut stream := new_stream_with_options(key_bytes, nonce_bytes)! mut block := []u8{len: block_size} stream.set_ctr(1) @@ -71,7 +71,7 @@ fn test_keystream_encryption() ! { key := hex.decode(val.key)! nonce := hex.decode(val.nonce)! - mut stream := new_stream(key, nonce)! + mut stream := new_stream_with_options(key, nonce)! stream.set_ctr(val.counter) mut block := []u8{len: block_size} diff --git a/vlib/x/crypto/chacha20/xchacha.v b/vlib/x/crypto/chacha20/xchacha.v index c27943fcd..e6d11a449 100644 --- a/vlib/x/crypto/chacha20/xchacha.v +++ b/vlib/x/crypto/chacha20/xchacha.v @@ -1,19 +1,26 @@ +// Copyright © 2025 blackshirt. +// Use of this source code is governed by an MIT license +// that can be found in the LICENSE file. +// +// This file contains a building block for eXtended ChaCha20 stream cipher (XChaCha20) construction. +// Its based on https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03 +// Note: so, its maybe outdated... +// Beside above draft that defines XChaCha20 construction with 32-bit internal counter, +// this XChaCha20 construction was expanded to support 64-bit counter. +// There are nothing RFC draft or published standard that can be used as a reference. +// Fortunatelly, this construct commonly implemented in popular chacha20 libraries. module chacha20 import encoding.binary -// This is building block for eXtended ChaCha20 stream cipher (XChaCha20) -// Its based on https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03 -// Note: so, its maybe outdated... - // HChaCha20 nonce size const h_nonce_size = 16 -// xchacha20 are intermediary step to build xchacha20 and initialized the same way as the ChaCha20 cipher, -// except xchacha20 use a 128-bit (16 byte) nonce and has no counter to derive subkey -// see https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03#section-2.2 +// hchacha20 are intermediary step to build XChaCha20 and initialized the same way as the ChaCha20 cipher, +// except hchacha20 use a 128-bit (16 byte) nonce and has no counter to derive subkey. +// See https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03#section-2.2 @[direct_array_access] -fn xchacha20(key []u8, nonce []u8) ![]u8 { +fn hchacha20(key []u8, nonce []u8) ![]u8 { // early bound check if key.len != key_size { return error('xchacha: Bad key size') @@ -46,13 +53,12 @@ fn xchacha20(key []u8, nonce []u8) ![]u8 { // After initialization, proceed through the ChaCha20 rounds as usual. for i := 0; i < 10; i++ { - // Diagonal round. + // Column round. qround_on_state(mut x, 0, 4, 8, 12) // 0 qround_on_state(mut x, 1, 5, 9, 13) // 1 qround_on_state(mut x, 2, 6, 10, 14) // 2 qround_on_state(mut x, 3, 7, 11, 15) // 3 - // quarter diagonal round // Diagonal round. // 0 \ 1 \ 2 \ 3 // 5 \ 6 \ 7 \ 4 @@ -64,7 +70,7 @@ fn xchacha20(key []u8, nonce []u8) ![]u8 { qround_on_state(mut x, 3, 4, 9, 14) } - // Once the 20 ChaCha rounds have been completed, the first 128 bits (16 bytes) and + // Once the 20 ChaCh20 rounds have been completed, the first 128 bits (16 bytes) and // last 128 bits (16 bytes) of the ChaCha state (both little-endian) are // concatenated, and this 256-bit (32 bytes) subkey is returned. mut out := []u8{len: 32} @@ -80,3 +86,33 @@ fn xchacha20(key []u8, nonce []u8) ![]u8 { return out } + +// derive_xchacha20_key_nonce derives a new key and nonce for eXtended ChaCha20 construction. +// It accepts boolean `flag64` flag as the last parameters. +// When its set into true, it would be used as an indicator of a 64-bit counter construction. +@[direct_array_access; inline] +fn derive_xchacha20_key_nonce(key []u8, nonce []u8, flag64 bool) !([]u8, []u8) { + // Its only for x_nonce_size + if nonce.len != x_nonce_size { + return error('Bad nonce size for derive_xchacha20_key_nonce') + } + // derives a new key based on XChaCha20 construction + // first, use 16 bytes of nonce used to derive the key + new_key := hchacha20(key, nonce[0..16])! + remaining_nonce := nonce[16..24].clone() + + // derive a new nonce based on the flag64 flag. + // If flag64 was true, its intended to build XChaCha20 original variant with 64-bit counter. + // Otherwise, its a XChaCha20 standard variant with 32-bit counter + new_nonce := if flag64 { + // use the remaining 8-bytes nonce + remaining_nonce + } else { + // and the last of 8 bytes of nonce copied into to build nonce_size length of new nonce. + mut nonce12 := []u8{len: nonce_size} + _ := copy(mut nonce12[4..12], remaining_nonce) + nonce12 + } + + return new_key, new_nonce +} diff --git a/vlib/x/crypto/chacha20/xchacha_test.v b/vlib/x/crypto/chacha20/xchacha_test.v index e258cd4f4..292d7e73c 100644 --- a/vlib/x/crypto/chacha20/xchacha_test.v +++ b/vlib/x/crypto/chacha20/xchacha_test.v @@ -5,13 +5,13 @@ import encoding.hex // Test Vector for the HChaCha20 Block Function // https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03#section-2.2.1 -fn test_xchacha20_function() ! { +fn test_hchacha20_function() ! { key := '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' key_bytes := hex.decode(key)! nonce := '000000090000004a0000000031415927' nonce_bytes := hex.decode(nonce)! - subkey := xchacha20(key_bytes, nonce_bytes)! + subkey := hchacha20(key_bytes, nonce_bytes)! assert subkey[0..4].hex() == '82413b42' assert subkey[4..8].hex() == '27b27bfe' -- 2.39.5