v / vlib / x / crypto / chacha20 / chacha.v
230 lines · 209 sloc · 7.25 KB · 3ffc951cf555dc4818309a507ccb9d0da4de748f
Raw
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
6module chacha20
7
8import crypto.internal.subtle
9
10// The size of ChaCha20 key, ie 256 bits size, in bytes
11pub const key_size = 32
12// The size of standard IETF ChaCha20 nonce, ie 96 bits size, in bytes
13pub const nonce_size = 12
14// The size of extended variant of standard ChaCha20 (XChaCha20) nonce, 192 bits
15pub const x_nonce_size = 24
16// The size of original ChaCha20 nonce, 64 bits
17pub const orig_nonce_size = 8
18// internal block size ChaCha20 operates on, in bytes
19const block_size = 64
20
21// four constants of ChaCha20 state.
22const cc0 = u32(0x61707865) // expa
23const cc1 = u32(0x3320646e) // nd 3
24const cc2 = u32(0x79622d32) // 2-by
25const cc3 = u32(0x6b206574) // te k
26
27// CipherMode was enumeration of ChaCha20 supported variant.
28enum 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]
37pub struct Options {
38pub 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.
47pub 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.
57pub 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]
67pub struct Cipher {
68 Stream
69mut:
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.
82pub 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]
95pub 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]
165pub 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]
181pub 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]
195pub 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
204pub 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.
209pub 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
214pub 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