| 1 | // Copyright (c) 2024 blackshirt. |
| 2 | // Use of this source code is governed by an MIT license |
| 3 | // that can be found in the LICENSE file. |
| 4 | // |
| 5 | // Chacha20 symmetric key stream cipher encryption based on RFC 8439 |
| 6 | module chacha20 |
| 7 | |
| 8 | import crypto.internal.subtle |
| 9 | |
| 10 | // The size of ChaCha20 key, ie 256 bits size, in bytes |
| 11 | pub const key_size = 32 |
| 12 | // The size of standard IETF ChaCha20 nonce, ie 96 bits size, in bytes |
| 13 | pub const nonce_size = 12 |
| 14 | // The size of extended variant of standard ChaCha20 (XChaCha20) nonce, 192 bits |
| 15 | pub const x_nonce_size = 24 |
| 16 | // The size of original ChaCha20 nonce, 64 bits |
| 17 | pub const orig_nonce_size = 8 |
| 18 | // internal block size ChaCha20 operates on, in bytes |
| 19 | const block_size = 64 |
| 20 | |
| 21 | // four constants of ChaCha20 state. |
| 22 | const cc0 = u32(0x61707865) // expa |
| 23 | const cc1 = u32(0x3320646e) // nd 3 |
| 24 | const cc2 = u32(0x79622d32) // 2-by |
| 25 | const cc3 = u32(0x6b206574) // te k |
| 26 | |
| 27 | // CipherMode was enumeration of ChaCha20 supported variant. |
| 28 | enum CipherMode { |
| 29 | // The standard IETF ChaCha20 (and XChaCha20), with 32-bit internal counter. |
| 30 | standard |
| 31 | // The original ChaCha20 with 64-bit internal counter. |
| 32 | original |
| 33 | } |
| 34 | |
| 35 | // Configuration options |
| 36 | @[params] |
| 37 | pub struct Options { |
| 38 | pub mut: |
| 39 | // currently, used for XChaCha20 construct |
| 40 | use_64bit_counter bool |
| 41 | } |
| 42 | |
| 43 | // encrypt encrypts plaintext bytes with ChaCha20 cipher instance with provided key and nonce. |
| 44 | // It was a thin wrapper around two supported nonce size, ChaCha20 with 96 bits |
| 45 | // and XChaCha20 with 192 bits nonce. Internally, encrypt start with 0's counter value. |
| 46 | // If you want more control, use Cipher instance and setup the counter by your self. |
| 47 | pub fn encrypt(key []u8, nonce []u8, plaintext []u8, opt Options) ![]u8 { |
| 48 | mut stream := new_stream_with_options(key, nonce, opt)! |
| 49 | mut dst := []u8{len: plaintext.len} |
| 50 | stream.keystream_full(mut dst, plaintext)! |
| 51 | unsafe { stream.reset() } |
| 52 | return dst |
| 53 | } |
| 54 | |
| 55 | // decrypt does reverse of encrypt operation by decrypting ciphertext with ChaCha20 cipher |
| 56 | // instance with provided key and nonce. |
| 57 | pub fn decrypt(key []u8, nonce []u8, ciphertext []u8, opt Options) ![]u8 { |
| 58 | mut stream := new_stream_with_options(key, nonce)! |
| 59 | mut dst := []u8{len: ciphertext.len} |
| 60 | stream.keystream_full(mut dst, ciphertext)! |
| 61 | unsafe { stream.reset() } |
| 62 | return dst |
| 63 | } |
| 64 | |
| 65 | // Cipher represents ChaCha20 stream cipher instances. |
| 66 | @[noinit] |
| 67 | pub struct Cipher { |
| 68 | Stream |
| 69 | mut: |
| 70 | // internal buffer for storing key stream results |
| 71 | block []u8 = []u8{len: block_size} |
| 72 | // The last length of leftover unprocessed keystream from internal buffer |
| 73 | length int |
| 74 | } |
| 75 | |
| 76 | // new_cipher creates a new ChaCha20 stream cipher with the given 32 bytes key |
| 77 | // and bytes of nonce with supported size, ie, 8, 12 or 24 bytes nonce. |
| 78 | // Standard IETF variant use 12 bytes nonce's, if you want create original ChaCha20 cipher |
| 79 | // with support for 64-bit counter, use 8 bytes length nonce's instead |
| 80 | // If 24 bytes of nonce was provided, the XChaCha20 construction will be used. |
| 81 | // It returns new ChaCha20 cipher instance or an error if key or nonce have any other length. |
| 82 | pub fn new_cipher(key []u8, nonce []u8, opt Options) !&Cipher { |
| 83 | stream := new_stream_with_options(key, nonce, opt)! |
| 84 | return &Cipher{ |
| 85 | Stream: stream |
| 86 | } |
| 87 | } |
| 88 | |
| 89 | // xor_key_stream xors each byte in the given slice in the src with a byte from the |
| 90 | // cipher's key stream. It fulfills `cipher.Stream` interface. It encrypts the plaintext message |
| 91 | // in src and stores the ciphertext result in dst in a key stream fashion. |
| 92 | // You must never use the same (key, nonce) pair more than once for encryption. |
| 93 | // This would void any confidentiality guarantees for the messages encrypted with the same nonce and key. |
| 94 | @[direct_array_access] |
| 95 | pub fn (mut c Cipher) xor_key_stream(mut dst []u8, src []u8) { |
| 96 | if src.len == 0 { |
| 97 | return |
| 98 | } |
| 99 | if dst.len < src.len { |
| 100 | panic('chacha20/chacha: dst buffer is to small') |
| 101 | } |
| 102 | dst = unsafe { dst[..src.len] } |
| 103 | if subtle.inexact_overlap(dst, src) { |
| 104 | panic('chacha20: invalid buffer overlap') |
| 105 | } |
| 106 | // index of position within src bytes |
| 107 | mut idx := 0 |
| 108 | |
| 109 | // First, try to drain any remaining key stream from internal buffer |
| 110 | if c.length != 0 { |
| 111 | // remaining keystream on internal buffer |
| 112 | mut kstream := c.block[block_size - c.length..] |
| 113 | if src.len < kstream.len { |
| 114 | kstream = unsafe { kstream[..src.len] } |
| 115 | } |
| 116 | // xors every bytes in src with bytes from key stream and stored into dst |
| 117 | for i, b in kstream { |
| 118 | dst[idx + i] = src[idx + i] ^ b |
| 119 | } |
| 120 | // updates position and internal buffer length. |
| 121 | // when c.length reaches the block_size, we reset it for future use. |
| 122 | c.length -= kstream.len |
| 123 | idx += kstream.len |
| 124 | if c.length == block_size { |
| 125 | unsafe { c.block.reset() } |
| 126 | c.length = 0 |
| 127 | } |
| 128 | } |
| 129 | // process for remaining unprocessed src bytes |
| 130 | mut remains := unsafe { src[idx..] } |
| 131 | nr_blocks := remains.len / block_size |
| 132 | |
| 133 | // process for full block_size-d message |
| 134 | for i := 0; i < nr_blocks; i++ { |
| 135 | // for every block_sized message, we generates 64-bytes block key stream |
| 136 | // and then xor-ing this block with generated key stream |
| 137 | block := unsafe { remains[i * block_size..(i + 1) * block_size] } |
| 138 | ks := c.keystream() or { panic(err) } |
| 139 | for j, b in ks { |
| 140 | dst[idx + j] = block[j] ^ b |
| 141 | } |
| 142 | // updates position |
| 143 | idx += block_size |
| 144 | } |
| 145 | |
| 146 | // process for remaining partial block |
| 147 | if remains.len % block_size != 0 { |
| 148 | last_block := unsafe { remains[nr_blocks * block_size..] } |
| 149 | // generates one 64-bytes keystream block |
| 150 | c.block = c.keystream() or { panic(err) } |
| 151 | for i, b in last_block { |
| 152 | dst[idx + i] = b ^ c.block[i] |
| 153 | } |
| 154 | c.length = block_size - last_block.len |
| 155 | idx += last_block.len |
| 156 | } |
| 157 | } |
| 158 | |
| 159 | // encrypt encrypts src and stores into dst buffer. It works like `xor_key_stream` except |
| 160 | // its ignore key streaming process by ignoring remaining key stream in the internal buffer, |
| 161 | // so, its works in one shot of fashion. |
| 162 | // Its added to allow `chacha20poly1305` modules to work without key stream fashion. |
| 163 | // TODO: integrates it with the rest |
| 164 | @[direct_array_access] |
| 165 | pub fn (mut c Cipher) encrypt(mut dst []u8, src []u8) ! { |
| 166 | if src.len == 0 { |
| 167 | return |
| 168 | } |
| 169 | if dst.len < src.len { |
| 170 | return error('chacha20: dst buffer is to small') |
| 171 | } |
| 172 | if subtle.inexact_overlap(dst, src) { |
| 173 | return error('chacha20: invalid buffer overlap') |
| 174 | } |
| 175 | |
| 176 | c.Stream.keystream_full(mut dst, src)! |
| 177 | } |
| 178 | |
| 179 | // free the resources taken by the Cipher `c`. Dont use cipher after .free call |
| 180 | @[unsafe] |
| 181 | pub fn (mut c Cipher) free() { |
| 182 | $if prealloc { |
| 183 | return |
| 184 | } |
| 185 | unsafe { |
| 186 | c.block.free() |
| 187 | } |
| 188 | } |
| 189 | |
| 190 | // reset quickly sets all Cipher's fields to default value. |
| 191 | // This method will be deprecated. |
| 192 | @[deprecated: 'do not use .reset() at all, create a new Cipher instead'] |
| 193 | @[deprecated_after: '2025-11-30'] |
| 194 | @[unsafe] |
| 195 | pub fn (mut c Cipher) reset() { |
| 196 | c.Stream.reset() |
| 197 | unsafe { |
| 198 | c.block.reset() |
| 199 | } |
| 200 | c.length = 0 |
| 201 | } |
| 202 | |
| 203 | // set_counter sets Cipher's counter |
| 204 | pub fn (mut c Cipher) set_counter(ctr u64) { |
| 205 | c.Stream.set_ctr(ctr) |
| 206 | } |
| 207 | |
| 208 | // counter returns a current underlying counter value, as u64. |
| 209 | pub fn (c Cipher) counter() u64 { |
| 210 | return c.Stream.ctr() |
| 211 | } |
| 212 | |
| 213 | // rekey resets internal Cipher's state and reinitializes state with the provided key and nonce |
| 214 | pub fn (mut c Cipher) rekey(key []u8, nonce []u8) ! { |
| 215 | // start of .reset() inline |
| 216 | unsafe { |
| 217 | c.Stream.reset() |
| 218 | c.block.reset() |
| 219 | } |
| 220 | c.length = 0 |
| 221 | // end of .reset() inline |
| 222 | |
| 223 | // we use c.Stream.mode info to get 64-bit counter capability |
| 224 | w64 := if c.mode == .original { true } else { false } |
| 225 | opt := Options{ |
| 226 | use_64bit_counter: w64 |
| 227 | } |
| 228 | stream := new_stream_with_options(key, nonce, opt)! |
| 229 | c.Stream = stream |
| 230 | } |
| 231 | |