Skip to content

Commit 66d222c

Browse files
authored
x.crypto: add x.crypto.chacha20 stream cipher module (#20417)
1 parent 46a467f commit 66d222c

File tree

7 files changed

+1908
-0
lines changed

7 files changed

+1908
-0
lines changed

vlib/x/crypto/chacha20/README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# module chacha20
2+
3+
chacha20
4+
-------
5+
6+
Chacha20 (and XChacha20) stream cipher encryption algorithm in pure V.
7+
Its mostly based on [RFC 8439](https://datatracker.ietf.org/doc/html/rfc8439) and inspired by Go version of the same library.
8+
9+
Examples
10+
--------
11+
```v
12+
module main
13+
14+
import encoding.hex
15+
import x.crypto.chacha20
16+
17+
fn main() {
18+
// example of random key
19+
// you should make sure the key (and nonce) are random enough.
20+
// The security guarantees of the ChaCha20 require that the same nonce
21+
// value is never used twice with the same key.
22+
key := hex.decode('bf32a829ebf86d23f6a32a74ef0333401e54a6b2900d35bfadef82c5d49da15f')!
23+
nonce := hex.decode('a7d7cf3405631f25cc1054bd')!
24+
25+
input := 'Good of gambler'.bytes()
26+
27+
// encrypt and the decrypt back
28+
output := chacha20.encrypt(key, nonce, input)!
29+
input_back := chacha20.decrypt(key, nonce, output)!
30+
31+
// should true
32+
assert input == input_back
33+
}
34+
```

vlib/x/crypto/chacha20/chacha.v

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
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 symetric key stream cipher encryption based on RFC 8439
6+
module chacha20
7+
8+
import math
9+
import crypto.cipher
10+
import crypto.internal.subtle
11+
import encoding.binary
12+
13+
// size of ChaCha20 key, ie 256 bits size, in bytes
14+
pub const key_size = 32
15+
// size of ietf ChaCha20 nonce, ie 96 bits size, in bytes
16+
pub const nonce_size = 12
17+
// size of extended ChaCha20 nonce, called XChaCha20, 192 bits
18+
pub const x_nonce_size = 24
19+
// internal block size ChaCha20 operates on, in bytes
20+
const block_size = 64
21+
22+
// vfmt off
23+
24+
// four constants of ChaCha20 state.
25+
const cc0 = u32(0x61707865) // expa
26+
const cc1 = u32(0x3320646e) // nd 3
27+
const cc2 = u32(0x79622d32) // 2-by
28+
const cc3 = u32(0x6b206574) // te k
29+
30+
// Cipher represents ChaCha20 stream cipher instances.
31+
struct Cipher {
32+
mut:
33+
// internal's of ChaCha20 states, ie, 16 of u32 words, 4 of ChaCha20 constants,
34+
// 8 word (32 bytes) of keys, 3 word (24 bytes) of nonces and 1 word of counter
35+
key [8]u32
36+
nonce [3]u32
37+
counter u32
38+
overflow bool
39+
// internal buffer for storing key stream results
40+
block []u8 = []u8{len: chacha20.block_size}
41+
// additional fields, follow the go version
42+
precomp bool
43+
p1 u32 p5 u32 p9 u32 p13 u32
44+
p2 u32 p6 u32 p10 u32 p14 u32
45+
p3 u32 p7 u32 p11 u32 p15 u32
46+
}
47+
// vfmt on
48+
49+
// new_cipher creates a new ChaCha20 stream cipher with the given 32 bytes key, a 12 or 24 bytes nonce.
50+
// If 24 bytes of nonce was provided, the XChaCha20 construction will be used.
51+
// It returns new ChaCha20 cipher instance or an error if key or nonce have any other length.
52+
pub fn new_cipher(key []u8, nonce []u8) !&Cipher {
53+
mut c := &Cipher{}
54+
// we dont need reset on new cipher instance
55+
c.do_rekey(key, nonce)!
56+
57+
return c
58+
}
59+
60+
// encrypt encrypts plaintext bytes with ChaCha20 cipher instance with provided key and nonce.
61+
// It was a thin wrapper around two supported nonce size, ChaCha20 with 96 bits
62+
// and XChaCha20 with 192 bits nonce. Internally, encrypt start with 0's counter value.
63+
// If you want more control, use Cipher instance and setup the counter by your self.
64+
pub fn encrypt(key []u8, nonce []u8, plaintext []u8) ![]u8 {
65+
return encrypt_with_counter(key, nonce, u32(0), plaintext)
66+
}
67+
68+
// decrypt does reverse of encrypt operation by decrypting ciphertext with ChaCha20 cipher
69+
// instance with provided key and nonce.
70+
pub fn decrypt(key []u8, nonce []u8, ciphertext []u8) ![]u8 {
71+
return encrypt_with_counter(key, nonce, u32(0), ciphertext)
72+
}
73+
74+
// xor_key_stream xors each byte in the given slice in the src with a byte from the
75+
// cipher's key stream. It fullfills `cipher.Stream` interface. It encrypts the plaintext message
76+
// in src and stores the ciphertext result in dst in a single run of encryption.
77+
// You must never use the same (key, nonce) pair more than once for encryption.
78+
// This would void any confidentiality guarantees for the messages encrypted with the same nonce and key.
79+
pub fn (mut c Cipher) xor_key_stream(mut dst []u8, src []u8) {
80+
if src.len == 0 {
81+
return
82+
}
83+
if dst.len < src.len {
84+
panic('chacha20/chacha: dst buffer is to small')
85+
}
86+
if subtle.inexact_overlap(dst, src) {
87+
panic('chacha20: invalid buffer overlap')
88+
}
89+
mut ciphertext := []u8{}
90+
91+
// ChaCha20's encryption mechanism is a relatively simple operation.
92+
// for every block_sized block from src bytes, build ChaCha20 keystream block,
93+
// then xor each byte in the block with keystresm block and then append xor-ed bytes
94+
// to the output buffer. If there are remaining (trailing) partial bytes,
95+
// generate one more keystream block, xors keystream block with partial bytes
96+
// and append to the result.
97+
//
98+
// Let's process for multiple blocks
99+
// number of blocks the src bytes should be split into
100+
nr_blocks := src.len / chacha20.block_size
101+
for i := 0; i < nr_blocks; i++ {
102+
// generate ciphers keystream block, stored in c.block
103+
c.generic_key_stream()
104+
// get current src block to be xor-ed
105+
block := unsafe { src[i * chacha20.block_size..(i + 1) * chacha20.block_size] }
106+
107+
// xor current block of plaintext with keystream in c.block and store result in out
108+
mut out := []u8{len: block.len}
109+
n := cipher.xor_bytes(mut out, block, c.block)
110+
assert n == c.block.len
111+
112+
// append current output to the ciphertext buffer
113+
ciphertext << out
114+
}
115+
// process for partial block
116+
if src.len % chacha20.block_size != 0 {
117+
c.generic_key_stream()
118+
// get the remaining partial block
119+
block := unsafe { src[nr_blocks * chacha20.block_size..] }
120+
// xor block with keystream
121+
mut out := []u8{len: block.len}
122+
n := cipher.xor_bytes(mut out, block, c.block)
123+
assert n == block.len
124+
125+
// make sure to take only remaining bytes
126+
out = unsafe { out[0..src.len % chacha20.block_size] }
127+
128+
// append last output to the ciphertext buffer
129+
ciphertext << out
130+
}
131+
// copy ciphertext message results to the dst buffer
132+
n := copy(mut dst, ciphertext)
133+
assert n == src.len
134+
}
135+
136+
// free the resources taken by the Cipher `c`. Dont use cipher after .free call
137+
@[unsafe]
138+
pub fn (mut c Cipher) free() {
139+
$if prealloc {
140+
return
141+
}
142+
unsafe {
143+
c.block.free()
144+
}
145+
}
146+
147+
// reset quickly sets all Cipher's fields to default value
148+
@[unsafe]
149+
pub fn (mut c Cipher) reset() {
150+
for i, _ in c.key {
151+
c.key[i] = u32(0)
152+
}
153+
for j, _ in c.nonce {
154+
c.nonce[j] = u32(0)
155+
}
156+
unsafe {
157+
c.block.reset()
158+
}
159+
160+
c.counter = u32(0)
161+
c.overflow = false
162+
c.precomp = false
163+
//
164+
c.p1 = u32(0)
165+
c.p5 = u32(0)
166+
c.p9 = u32(0)
167+
c.p13 = u32(0)
168+
//
169+
c.p2 = u32(0)
170+
c.p6 = u32(0)
171+
c.p10 = u32(0)
172+
c.p14 = u32(0)
173+
//
174+
c.p3 = u32(0)
175+
c.p7 = u32(0)
176+
c.p11 = u32(0)
177+
c.p15 = u32(0)
178+
}
179+
180+
// set_counter sets Cipher's counter
181+
pub fn (mut c Cipher) set_counter(ctr u32) {
182+
if ctr == math.max_u32 {
183+
c.overflow = true
184+
}
185+
if c.overflow {
186+
panic('counter would overflow')
187+
}
188+
c.counter = ctr
189+
}
190+
191+
// rekey resets internal Cipher's state and reinitializes state with the provided key and nonce
192+
@[unsafe]
193+
pub fn (mut c Cipher) rekey(key []u8, nonce []u8) ! {
194+
unsafe { c.reset() }
195+
c.do_rekey(key, nonce)!
196+
}
197+
198+
// do_rekey reinitializes ChaCha20 instance with the provided key and nonce.
199+
fn (mut c Cipher) do_rekey(key []u8, nonce []u8) ! {
200+
// check for correctness of key and nonce length
201+
if key.len != chacha20.key_size {
202+
return error('chacha20: bad key size provided ')
203+
}
204+
// check for nonce's length is 12 or 24
205+
if nonce.len != chacha20.nonce_size && nonce.len != chacha20.x_nonce_size {
206+
return error('chacha20: bad nonce size provided')
207+
}
208+
mut nonces := nonce.clone()
209+
mut keys := key.clone()
210+
211+
// if nonce's length is 24 bytes, we derive a new key and nonce with xchacha20 function
212+
// and supplied to setup process.
213+
if nonces.len == chacha20.x_nonce_size {
214+
keys = xchacha20(keys, nonces[0..16])!
215+
mut cnonce := []u8{len: chacha20.nonce_size}
216+
_ := copy(mut cnonce[4..12], nonces[16..24])
217+
nonces = cnonce.clone()
218+
} else if nonces.len != chacha20.nonce_size {
219+
return error('chacha20: wrong nonce size')
220+
}
221+
222+
// bounds check elimination hint
223+
_ = keys[chacha20.key_size - 1]
224+
_ = nonces[chacha20.nonce_size - 1]
225+
226+
// setup ChaCha20 cipher key
227+
c.key[0] = binary.little_endian_u32(keys[0..4])
228+
c.key[1] = binary.little_endian_u32(keys[4..8])
229+
c.key[2] = binary.little_endian_u32(keys[8..12])
230+
c.key[3] = binary.little_endian_u32(keys[12..16])
231+
c.key[4] = binary.little_endian_u32(keys[16..20])
232+
c.key[5] = binary.little_endian_u32(keys[20..24])
233+
c.key[6] = binary.little_endian_u32(keys[24..28])
234+
c.key[7] = binary.little_endian_u32(keys[28..32])
235+
236+
// setup ChaCha20 cipher nonce
237+
c.nonce[0] = binary.little_endian_u32(nonces[0..4])
238+
c.nonce[1] = binary.little_endian_u32(nonces[4..8])
239+
c.nonce[2] = binary.little_endian_u32(nonces[8..12])
240+
}
241+
242+
// chacha20_block transforms a ChaCha20 state by running
243+
// multiple quarter rounds.
244+
// see https://datatracker.ietf.org/doc/html/rfc8439#section-2.3
245+
fn (mut c Cipher) chacha20_block() {
246+
// initializes ChaCha20 state
247+
// 0:cccccccc 1:cccccccc 2:cccccccc 3:cccccccc
248+
// 4:kkkkkkkk 5:kkkkkkkk 6:kkkkkkkk 7:kkkkkkkk
249+
// 8:kkkkkkkk 9:kkkkkkkk 10:kkkkkkkk 11:kkkkkkkk
250+
// 12:bbbbbbbb 13:nnnnnnnn 14:nnnnnnnn 15:nnnnnnnn
251+
//
252+
// where c=constant k=key b=blockcounter n=nonce
253+
c0, c1, c2, c3 := chacha20.cc0, chacha20.cc1, chacha20.cc2, chacha20.cc3
254+
c4 := c.key[0]
255+
c5 := c.key[1]
256+
c6 := c.key[2]
257+
c7 := c.key[3]
258+
c8 := c.key[4]
259+
c9 := c.key[5]
260+
c10 := c.key[6]
261+
c11 := c.key[7]
262+
263+
_ := c.counter
264+
c13 := c.nonce[0]
265+
c14 := c.nonce[1]
266+
c15 := c.nonce[2]
267+
268+
// precomputes three first column rounds that do not depend on counter
269+
if !c.precomp {
270+
c.p1, c.p5, c.p9, c.p13 = quarter_round(c1, c5, c9, c13)
271+
c.p2, c.p6, c.p10, c.p14 = quarter_round(c2, c6, c10, c14)
272+
c.p3, c.p7, c.p11, c.p15 = quarter_round(c3, c7, c11, c15)
273+
c.precomp = true
274+
}
275+
// remaining first column round
276+
fcr0, fcr4, fcr8, fcr12 := quarter_round(c0, c4, c8, c.counter)
277+
278+
// The second diagonal round.
279+
mut x0, mut x5, mut x10, mut x15 := quarter_round(fcr0, c.p5, c.p10, c.p15)
280+
mut x1, mut x6, mut x11, mut x12 := quarter_round(c.p1, c.p6, c.p11, fcr12)
281+
mut x2, mut x7, mut x8, mut x13 := quarter_round(c.p2, c.p7, fcr8, c.p13)
282+
mut x3, mut x4, mut x9, mut x14 := quarter_round(c.p3, fcr4, c.p9, c.p14)
283+
284+
// The remaining 18 rounds.
285+
for i := 0; i < 9; i++ {
286+
// Column round.
287+
x0, x4, x8, x12 = quarter_round(x0, x4, x8, x12)
288+
x1, x5, x9, x13 = quarter_round(x1, x5, x9, x13)
289+
x2, x6, x10, x14 = quarter_round(x2, x6, x10, x14)
290+
x3, x7, x11, x15 = quarter_round(x3, x7, x11, x15)
291+
292+
// Diagonal round.
293+
x0, x5, x10, x15 = quarter_round(x0, x5, x10, x15)
294+
x1, x6, x11, x12 = quarter_round(x1, x6, x11, x12)
295+
x2, x7, x8, x13 = quarter_round(x2, x7, x8, x13)
296+
x3, x4, x9, x14 = quarter_round(x3, x4, x9, x14)
297+
}
298+
299+
// add back to initial state and stores to dst
300+
x0 += c0
301+
x1 += c1
302+
x2 += c2
303+
x3 += c3
304+
x4 += c4
305+
x5 += c5
306+
x6 += c6
307+
x7 += c7
308+
x8 += c8
309+
x9 += c9
310+
x10 += c10
311+
x11 += c11
312+
// x12 is Cipher.counter
313+
x12 += c.counter
314+
x13 += c13
315+
x14 += c14
316+
x15 += c15
317+
318+
binary.little_endian_put_u32(mut c.block[0..4], x0)
319+
binary.little_endian_put_u32(mut c.block[4..8], x1)
320+
binary.little_endian_put_u32(mut c.block[8..12], x2)
321+
binary.little_endian_put_u32(mut c.block[12..16], x3)
322+
binary.little_endian_put_u32(mut c.block[16..20], x4)
323+
binary.little_endian_put_u32(mut c.block[20..24], x5)
324+
binary.little_endian_put_u32(mut c.block[24..28], x6)
325+
binary.little_endian_put_u32(mut c.block[28..32], x7)
326+
binary.little_endian_put_u32(mut c.block[32..36], x8)
327+
binary.little_endian_put_u32(mut c.block[36..40], x9)
328+
binary.little_endian_put_u32(mut c.block[40..44], x10)
329+
binary.little_endian_put_u32(mut c.block[44..48], x11)
330+
binary.little_endian_put_u32(mut c.block[48..52], x12)
331+
binary.little_endian_put_u32(mut c.block[52..56], x13)
332+
binary.little_endian_put_u32(mut c.block[56..60], x14)
333+
binary.little_endian_put_u32(mut c.block[60..64], x15)
334+
}
335+
336+
// generic_key_stream creates generic ChaCha20 keystream block and stores the result in Cipher.block
337+
fn (mut c Cipher) generic_key_stream() {
338+
// creates ChaCha20 block stream
339+
c.chacha20_block()
340+
// updates counter and checks for overflow
341+
ctr := u64(c.counter) + u64(1)
342+
if ctr == math.max_u32 {
343+
c.overflow = true
344+
}
345+
if c.overflow || ctr > math.max_u32 {
346+
panic('counter overflow')
347+
}
348+
c.counter += 1
349+
}

0 commit comments

Comments
 (0)