| 1 | // Copyright © 2025 blackshirt. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | // |
| 5 | // This file contains a building block for eXtended ChaCha20 stream cipher (XChaCha20) construction. |
| 6 | // Its based on https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03 |
| 7 | // Note: so, its maybe outdated... |
| 8 | // Beside above draft that defines XChaCha20 construction with 32-bit internal counter, |
| 9 | // this XChaCha20 construction was expanded to support 64-bit counter. |
| 10 | // There are nothing RFC draft or published standard that can be used as a reference. |
| 11 | // Fortunatelly, this construct commonly implemented in popular chacha20 libraries. |
| 12 | module chacha20 |
| 13 | |
| 14 | import encoding.binary |
| 15 | |
| 16 | // HChaCha20 nonce size |
| 17 | const h_nonce_size = 16 |
| 18 | |
| 19 | // hchacha20 are intermediary step to build XChaCha20 and initialized the same way as the ChaCha20 cipher, |
| 20 | // except hchacha20 use a 128-bit (16 byte) nonce and has no counter to derive subkey. |
| 21 | // See https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03#section-2.2 |
| 22 | @[direct_array_access] |
| 23 | fn hchacha20(key []u8, nonce []u8) ![]u8 { |
| 24 | // early bound check |
| 25 | if key.len != key_size { |
| 26 | return error('xchacha: Bad key size') |
| 27 | } |
| 28 | if nonce.len != h_nonce_size { |
| 29 | return error('xchacha: Bad nonce size') |
| 30 | } |
| 31 | // initializes ChaCha20 state |
| 32 | mut x := State{} |
| 33 | x[0] = cc0 |
| 34 | x[1] = cc1 |
| 35 | x[2] = cc2 |
| 36 | x[3] = cc3 |
| 37 | |
| 38 | x[4] = binary.little_endian_u32(key[0..4]) |
| 39 | x[5] = binary.little_endian_u32(key[4..8]) |
| 40 | x[6] = binary.little_endian_u32(key[8..12]) |
| 41 | x[7] = binary.little_endian_u32(key[12..16]) |
| 42 | |
| 43 | x[8] = binary.little_endian_u32(key[16..20]) |
| 44 | x[9] = binary.little_endian_u32(key[20..24]) |
| 45 | x[10] = binary.little_endian_u32(key[24..28]) |
| 46 | x[11] = binary.little_endian_u32(key[28..32]) |
| 47 | |
| 48 | // we have no counter |
| 49 | x[12] = binary.little_endian_u32(nonce[0..4]) |
| 50 | x[13] = binary.little_endian_u32(nonce[4..8]) |
| 51 | x[14] = binary.little_endian_u32(nonce[8..12]) |
| 52 | x[15] = binary.little_endian_u32(nonce[12..16]) |
| 53 | |
| 54 | // After initialization, proceed through the ChaCha20 rounds as usual. |
| 55 | x.qround(10) |
| 56 | |
| 57 | // Once the 20 ChaCh20 rounds have been completed, the first 128 bits (16 bytes) and |
| 58 | // last 128 bits (16 bytes) of the ChaCha state (both little-endian) are |
| 59 | // concatenated, and this 256-bit (32 bytes) subkey is returned. |
| 60 | mut out := []u8{len: 32} |
| 61 | binary.little_endian_put_u32(mut out[0..4], x[0]) |
| 62 | binary.little_endian_put_u32(mut out[4..8], x[1]) |
| 63 | binary.little_endian_put_u32(mut out[8..12], x[2]) |
| 64 | binary.little_endian_put_u32(mut out[12..16], x[3]) |
| 65 | |
| 66 | binary.little_endian_put_u32(mut out[16..20], x[12]) |
| 67 | binary.little_endian_put_u32(mut out[20..24], x[13]) |
| 68 | binary.little_endian_put_u32(mut out[24..28], x[14]) |
| 69 | binary.little_endian_put_u32(mut out[28..32], x[15]) |
| 70 | |
| 71 | return out |
| 72 | } |
| 73 | |
| 74 | // derive_xchacha20_key_nonce derives a new key and nonce for eXtended ChaCha20 construction. |
| 75 | // It accepts boolean `flag64` flag as the last parameters. |
| 76 | // When its set into true, it would be used as an indicator of a 64-bit counter construction. |
| 77 | @[direct_array_access; inline] |
| 78 | fn derive_xchacha20_key_nonce(key []u8, nonce []u8, flag64 bool) !([]u8, []u8) { |
| 79 | // Its only for x_nonce_size |
| 80 | if nonce.len != x_nonce_size { |
| 81 | return error('Bad nonce size for derive_xchacha20_key_nonce') |
| 82 | } |
| 83 | // derives a new key based on XChaCha20 construction |
| 84 | // first, use 16 bytes of nonce used to derive the key |
| 85 | new_key := hchacha20(key, nonce[0..16])! |
| 86 | remaining_nonce := nonce[16..24].clone() |
| 87 | |
| 88 | // derive a new nonce based on the flag64 flag. |
| 89 | // If flag64 was true, its intended to build XChaCha20 original variant with 64-bit counter. |
| 90 | // Otherwise, its a XChaCha20 standard variant with 32-bit counter |
| 91 | new_nonce := if flag64 { |
| 92 | // use the remaining 8-bytes nonce |
| 93 | remaining_nonce |
| 94 | } else { |
| 95 | // and the last of 8 bytes of nonce copied into to build nonce_size length of new nonce. |
| 96 | mut nonce12 := []u8{len: nonce_size} |
| 97 | _ := copy(mut nonce12[4..12], remaining_nonce) |
| 98 | nonce12 |
| 99 | } |
| 100 | |
| 101 | return new_key, new_nonce |
| 102 | } |
| 103 | |