v / vlib / x / crypto / chacha20poly1305 / psiv.v
498 lines · 437 sloc · 14.43 KB · e2e5cf8db56f3562c7baa735061690be936bdf3e
Raw
1// Copyright (c) 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 an experimental port of a Rust reference implementation of nonce-misuse
6// resistant and key-committing authenticated encryption scheme called ChaCha20-Poly1305-PSIV,
7// It backed by `chacha20` stream cipher and `poly1305` message authentication code module.
8// Its originally described by Michiel Verbauwhede and the teams on his papers.
9// See the detail on the [A Robust Variant of ChaCha20-Poly1305](https://eprint.iacr.org/2025/222).
10module chacha20poly1305
11
12import crypto.cipher
13import encoding.binary
14import crypto.internal.subtle
15import x.crypto.chacha20
16import x.crypto.poly1305
17
18// new_psiv creates a new Chacha20Poly1305RE with PSIV construct to operate on.
19@[direct_array_access]
20pub fn new_psiv(key []u8) !&Chacha20Poly1305RE {
21 if key.len != key_size {
22 return error('new_psiv: bad key size')
23 }
24 // derives and initializes the new key for later purposes
25 pol_key := fk_k(key)
26 mac_key := fm_k(key)
27 enc_key := fe_k(key)
28
29 mut s := chacha20.State{}
30 mut x64 := [64]u8{}
31 unsafe { vmemcpy(x64, pol_key[0], 36) }
32 unpack_into_state(mut s, x64)
33 ws := chacha20_core(s)
34
35 // For poly1305 mac, we only take a first 32-bytes of the state as a key
36 mut poly1305_key := []u8{len: 32}
37 pack32_from_state(mut poly1305_key, ws)
38 po := poly1305.new(poly1305_key)!
39
40 // set the values
41 c := &Chacha20Poly1305RE{
42 mac_key: mac_key
43 enc_key: enc_key
44 po: po
45 }
46 return c
47}
48
49// psiv_encrypt encrypts plaintext with provided key, nonce and additional data ad.
50// It returns a ciphertext plus message authentication code (mac) contained
51// within the end of ciphertext
52@[direct_array_access]
53pub fn psiv_encrypt(plaintext []u8, key []u8, nonce []u8, ad []u8) ![]u8 {
54 mut c := new_psiv(key)!
55 out := c.encrypt(plaintext, nonce, ad)!
56 unsafe { c.free() }
57 return out
58}
59
60// psiv_decrypt decrypts the ciphertext with provided key, nonce and additional data in ad.
61// It also tries to validate message authentication code within ciphertext compared with
62// calculated tag. It returns successfully decrypted message or error on fails.
63@[direct_array_access]
64pub fn psiv_decrypt(ciphertext []u8, key []u8, nonce []u8, ad []u8) ![]u8 {
65 mut c := new_psiv(key)!
66 out := c.decrypt(ciphertext, nonce, ad)!
67 unsafe { c.free() }
68 return out
69}
70
71// Chacha20Poly1305RE is a Chacha20Poly1305 opaque with nonce-misuse resistent
72// and key-commiting AEAD scheme with PSIV construct.
73@[noinit]
74pub struct Chacha20Poly1305RE implements cipher.AEAD {
75mut:
76 // flag that marked this instance should not be used again, set on .free call
77 done bool
78 // underlying derived keys, set on instance creation with new_psiv.
79 mac_key [36]u8
80 enc_key [36]u8
81 po &poly1305.Poly1305 = unsafe { nil }
82}
83
84// free releases resources taken by c. Dont use c after `.free` call.
85@[unsafe]
86pub fn (mut c Chacha20Poly1305RE) free() {
87 // if it already marked as done, just return
88 if c.done {
89 return
90 }
91 unsafe {
92 // we reset derived keys
93 vmemset(c.mac_key, 0, c.mac_key.len)
94 vmemset(c.enc_key, 0, c.enc_key.len)
95 c.po = nil
96 }
97 // mark this instance as done, no longer usable
98 c.done = true
99}
100
101// nonce_size return the size of the nonce of underlying c.
102// Currently, it only support for standard 12-bytes nonce.
103pub fn (c &Chacha20Poly1305RE) nonce_size() int {
104 return nonce_size
105}
106
107// overhead returns difference between the lengths of a plaintext and its ciphertext.
108// Its normally returns a tag size produced by this scheme.
109pub fn (c &Chacha20Poly1305RE) overhead() int {
110 return tag_size
111}
112
113// encrypt encrypts and authenticates the provided plaintext along with a nonce, and
114// to be authenticated additional data in `ad`. It returns a ciphertext with message authenticated
115// code stored within the end of ciphertext.
116@[direct_array_access]
117pub fn (c Chacha20Poly1305RE) encrypt(plaintext []u8, nonce []u8, ad []u8) ![]u8 {
118 if c.done {
119 return error('Chacha20Poly1305RE.encrypt: instance marked as done, no longer usable')
120 }
121 if nonce.len != nonce_size {
122 return error('Chacha20Poly1305RE.encrypt: bad nonce length, only support 12-bytes nonce')
123 }
124
125 // clone the initial poly1305 state and updates it with additional data ad
126 mut po_ad := c.po.clone()
127 update_with_padding(mut po_ad, ad)
128 // make a clone of updated poly1305
129 mut po_ad_clone := po_ad.clone()
130
131 // setup output buffer
132 mut out := []u8{len: plaintext.len + tag_size}
133 // write out an authentication tag into the last tag_size bytes of output
134 psiv_gen_tag(mut out[plaintext.len..], mut po_ad_clone, plaintext, ad.len, c.mac_key, nonce)
135 // write out authenticated encrypted plaintext into the first plaintext.len bytes of output
136 psiv_encrypt_internal(mut out[0..plaintext.len], plaintext, c.enc_key, out[plaintext.len..],
137 nonce)!
138
139 // return the result
140 return out
141}
142
143// decrypt decrypts the ciphertext with provided key, nonce and additional data in ad.
144// It also tries to validate message authenticated code within ciphertext compared with
145// calculated tag. It returns successfully decrypted message or error on fails.
146@[direct_array_access]
147pub fn (c Chacha20Poly1305RE) decrypt(ciphertext []u8, nonce []u8, ad []u8) ![]u8 {
148 if c.done {
149 return error('Chacha20Poly1305RE.decrypt: instance marked as done, no longer usable')
150 }
151 if ciphertext.len < tag_size {
152 return error('Chacha20Poly1305RE.decrypt: insufficient ciphertext length')
153 }
154 if nonce.len != nonce_size {
155 return error('Chacha20Poly1305RE.decrypt: invalid nonce length provided')
156 }
157 enc := ciphertext[0..ciphertext.len - c.overhead()]
158 tag := ciphertext[ciphertext.len - c.overhead()..]
159
160 // updates a clone of poly1305 with additional data
161 mut po_with_ad := c.po.clone()
162 update_with_padding(mut po_with_ad, ad)
163 mut poad_clone := po_with_ad.clone()
164
165 // generates authenticated encrypted plaintext with associated mac
166 mut out := []u8{len: enc.len}
167 psiv_encrypt_internal(mut out, enc, c.enc_key, tag, nonce)!
168
169 mut mac := []u8{len: tag_size}
170 psiv_gen_tag(mut mac, mut poad_clone, out, ad.len, c.mac_key, nonce)
171
172 // check if authentication tag was matching or error on fails.
173 if subtle.constant_time_compare(mac, tag) != 1 {
174 unsafe {
175 out.free()
176 mac.free()
177 }
178 return error('unmatching tag')
179 }
180 // return the decrypted ciphertext
181 return out
182}
183
184// The AEAD_CHACHA20_POLY1305 PSIV construct helpers
185//
186
187// psiv_encrypt_internal is an internal encryption routine used by the core of psiv construct
188// for encrypting (or decrypting) message.
189@[direct_array_access]
190fn psiv_encrypt_internal(mut dst []u8, plaintext []u8, dkey [36]u8, tag []u8, nonce []u8) ! {
191 // loads the counter from the first 8-bytes of the tag input
192 mut ctr := binary.little_endian_u64(tag[0..8])
193
194 // setup some temporary vars
195 mut tc := []u8{len: 8} // counter buffer
196 mut s := chacha20.State{}
197 mut b64 := [64]u8{} // state buffer
198 mut tt := merge_drv_key(dkey, nonce, tag[0..8], tag[8..16])
199
200 mut j := 0
201 mut n := 0
202
203 // process for every bytes on plaintext input
204 for plaintext[n..].len > 0 {
205 // how many block of bytes available to process on
206 want_len := if plaintext[n..].len < 64 { plaintext[n..].len } else { 64 }
207 // loads current counter
208 binary.little_endian_put_u64(mut tc, ctr)
209
210 // updates derived keys with current counter, scrambled with chacha20_core and
211 // puts state into b64 buffer
212 unsafe { vmemcpy(tt[48], tc.data, tc.len) }
213 unpack_into_state(mut s, tt)
214 ws := chacha20_core(s)
215 pack64_from_state(mut b64, ws)
216
217 // xor every bytes of plaintext with bytes on b64, stores result in dst
218 for i in 0 .. want_len {
219 dst[j] = plaintext[j] ^ b64[i]
220 j++
221 }
222 // updates current counter and returns error on overflow.
223 ctr += 1
224 if ctr == 0 {
225 return error('counter overflowing')
226 }
227 n += want_len
228 }
229 // explicitly reset (release) temporary allocated resources and return the result.
230 unsafe {
231 tc.free()
232 s.reset()
233 vmemset(b64, 0, b64.len)
234 vmemset(tt, 0, tt.len)
235 }
236}
237
238// psiv_gen_tag computes a tag from the key, nonce, and Poly1305 tag of the associated data
239// and plaintext using the ChaCha20 permutation with the feed-forward, truncating the output.
240@[direct_array_access]
241fn psiv_gen_tag(mut out []u8, mut po poly1305.Poly1305, input []u8, ad_len int, mac_key [36]u8, nonce []u8) {
242 // updates poly1305 mac by input message, associated data length and input length.
243 update_with_padding(mut po, input)
244 po.update(length_to_block(ad_len, input.len))
245
246 // produces 16-bytes of mac from current poly1305 state.
247 po.finish(mut out)
248
249 // The tag was produced from derived key scrambled with chacha20 quarter round routine,
250 // and then truncating the output into 16-bytes tag.
251 drv_key := merge_drv_key(mac_key, nonce, out[0..8], out[8..16])
252 mut x := chacha20.State{}
253 unpack_into_state(mut x, drv_key)
254 ws := chacha20_core(x)
255
256 // truncating state output into tag_sized bytes. As a note, we reuse buffer previously allocated
257 // to store the result.
258 pack16_from_state(mut out, ws)
259
260 // explicitly releases (reset) temporary allocated resources
261 unsafe {
262 vmemset(drv_key, 0, drv_key.len)
263 ws.reset()
264 x.reset()
265 }
266}
267
268// update_with_padding updates poly1305 mac with data, padding the tail block if necessary.
269@[direct_array_access; inline]
270fn update_with_padding(mut po poly1305.Poly1305, data []u8) {
271 po.update(data)
272 rem := data.len % tag_size
273 if rem != 0 {
274 block := []u8{len: tag_size}
275 po.update(block[..tag_size - rem])
276 }
277}
278
279// merge_drv_key merges provided bytes into 64-bytes key
280@[direct_array_access; inline]
281fn merge_drv_key(dkey [36]u8, nonce []u8, tag_ctr []u8, tag_rest []u8) [64]u8 {
282 mut x64 := [64]u8{}
283
284 // 0..36
285 for i := 0; i < dkey.len; i++ {
286 x64[i] = dkey[i]
287 }
288 // 36..48
289 for i := 0; i < nonce.len; i++ {
290 x64[36 + i] = nonce[i]
291 }
292 // 48..56
293 for i := 0; i < tag_ctr.len; i++ {
294 x64[i + 48] = tag_ctr[i]
295 }
296 // 56..64
297 for i := 0; i < tag_rest.len; i++ {
298 x64[i + 56] = tag_rest[i]
299 }
300
301 return x64
302}
303
304// fk_k maps and transforms 32-bytes of key into 36-bytes of new key used to
305// derive a poly1305 construction.
306// See the papers doc on the 3.3 Additional Details part, on page 12-13
307@[direct_array_access; inline]
308fn fk_k(key []u8) [36]u8 {
309 // fk(K) = K1 ∥ K2 ∥ K3 ∥ 03 ∥ K5 ∥ K6 ∥ K7 ∥ 0c ∥ K9 ∥ K10 ∥ K11 ∥ 30
310 // ∥ K4 ∥ K8 ∥ K12 ∥ c0 ∥ K13 ∥ K14 ∥ · · · ∥ K32
311 // with 0-based index
312 // K0 ∥ K1 ∥ K2 ∥ 03 ∥ K4 ∥ K5 ∥ K6 ∥ 0c ∥ K8 ∥ K9 ∥ K10 ∥ 30
313 // ∥ K3 ∥ K7 ∥ K11 ∥ c0 ∥ K12 ∥ K13 ∥ · · · ∥ K31
314 mut x := [36]u8{}
315 // 0 .. 4
316 for i := 0; i < 3; i++ {
317 x[i] = key[i]
318 }
319 x[3] = u8(0x03)
320
321 // 4 .. 8
322 for i := 4; i < 7; i++ {
323 x[i] = key[i]
324 }
325 x[7] = 0x0c
326
327 // 8 .. 12
328 for i := 8; i < 11; i++ {
329 x[i] = key[i]
330 }
331 x[11] = 0x30
332
333 // 12 .. 16
334 x[12] = key[3]
335 x[13] = key[7]
336 x[14] = key[11]
337 x[15] = 0xc0
338
339 // 16 .. 36
340 for i := 16; i < 36; i++ {
341 x[i] = key[i - 4]
342 }
343
344 return x
345}
346
347// fm_k maps and transforms 32-bytes of key into 36-bytes of message authentication key.
348// It later used for psiv tag generation.
349@[direct_array_access; inline]
350fn fm_k(key []u8) [36]u8 {
351 // fm(K) = K1 ∥ K2 ∥ K3 ∥ 05 ∥ K5 ∥ K6 ∥ K7 ∥ 0a ∥ K9 ∥ K10 ∥ K11 ∥ 50 ∥
352 // K4 ∥ K8 ∥ K12 ∥ a0 ∥ K13 ∥ K14 ∥ · · · ∥ K32 ,
353 // Or, with 0-based index
354 // fm(K) = K0 ∥ K1 ∥ K2 ∥ 05 ∥ K4 ∥ K5 ∥ K6 ∥ 0a ∥ K8 ∥ K9 ∥ K10 ∥ 50 ∥
355 // K3 ∥ K7 ∥ K11 ∥ a0 ∥ K12 ∥ K13 ∥ · · · ∥ K31
356 mut x := [36]u8{}
357 // 0 .. 4
358 for i := 0; i < 3; i++ {
359 x[i] = key[i]
360 }
361 x[3] = u8(0x05)
362
363 // 4 .. 8
364 for i := 4; i < 7; i++ {
365 x[i] = key[i]
366 }
367 x[7] = 0x0a
368
369 // 8 .. 12
370 for i := 8; i < 11; i++ {
371 x[i] = key[i]
372 }
373 x[11] = 0x50
374
375 // 12 .. 16
376 x[12] = key[3]
377 x[13] = key[7]
378 x[14] = key[11]
379 x[15] = 0xa0
380
381 // 16 .. 36
382 for i := 16; i < 36; i++ {
383 x[i] = key[i - 4]
384 }
385
386 return x
387}
388
389// fe_k maps and transforms 32-bytes of key into 36-bytes of new encryption key
390@[direct_array_access; inline]
391fn fe_k(key []u8) [36]u8 {
392 // fe(K) = K1 ∥ K2 ∥ K3 ∥ 06 ∥ K5 ∥ K6 ∥ K7 ∥ 09 ∥ K9 ∥ K10 ∥ K11 ∥ 60 ∥
393 // K4 ∥ K8 ∥ K12 ∥ 90 ∥ K13 ∥ K14 ∥ · · · ∥ K32
394 // Or, with 0-based index
395 // fe(K) = K0 ∥ K1 ∥ K2 ∥ 06 ∥ K4 ∥ K5 ∥ K6 ∥ 09 ∥ K8 ∥ K9 ∥ K10 ∥ 60 ∥
396 // K3 ∥ K7 ∥ K11 ∥ 90 ∥ K12 ∥ K13 ∥ · · · ∥ K31
397 mut x := [36]u8{}
398 // 0 .. 4
399 for i := 0; i < 3; i++ {
400 x[i] = key[i]
401 }
402 x[3] = u8(0x06)
403
404 // 4 .. 8
405 for i := 4; i < 7; i++ {
406 x[i] = key[i]
407 }
408 x[7] = 0x09
409
410 // 8 .. 12
411 for i := 8; i < 11; i++ {
412 x[i] = key[i]
413 }
414 x[11] = 0x60
415
416 // 12 .. 16
417 x[12] = key[3]
418 x[13] = key[7]
419 x[14] = key[11]
420 x[15] = 0x90
421
422 // 16 .. 36
423 for i := 16; i < 36; i++ {
424 x[i] = key[i - 4]
425 }
426
427 return x
428}
429
430// unpack_into_state deserializes (in little-endian form) 64-bytes of data in x into state s.
431@[direct_array_access; inline]
432fn unpack_into_state(mut s chacha20.State, x [64]u8) {
433 for i := 0; i < 16; i++ {
434 s[i] = u32(x[i * 4]) | (u32(x[i * 4 + 1]) << u32(8)) | (u32(x[i * 4 + 2]) << u32(16)) | (u32(x[
435 i * 4 + 3]) << u32(24))
436 }
437}
438
439// pack64_from_state serializes state s into 64-bytes output in little-endian form.
440@[direct_array_access; inline]
441fn pack64_from_state(mut out [64]u8, s chacha20.State) {
442 mut j := 0
443 for v in s {
444 out[j] = u8(v)
445 out[j + 1] = u8(v >> u32(8))
446 out[j + 2] = u8(v >> u32(16))
447 out[j + 3] = u8(v >> u32(24))
448 j += 4
449 }
450}
451
452// pack32_from_state serializes only a half of state s into 32-bytes output in little-endian form.
453@[direct_array_access; inline]
454fn pack32_from_state(mut out []u8, s chacha20.State) {
455 mut j := 0
456 for i in 0 .. 8 {
457 out[j] = u8(s[i])
458 out[j + 1] = u8(s[i] >> u32(8))
459 out[j + 2] = u8(s[i] >> u32(16))
460 out[j + 3] = u8(s[i] >> u32(24))
461 j += 4
462 }
463}
464
465// pack16_from_state serializes the first quartet of state s into 16-bytes output in little-endian form.
466@[direct_array_access; inline]
467fn pack16_from_state(mut out []u8, s chacha20.State) {
468 mut j := 0
469 for i in 0 .. 4 {
470 out[j] = u8(s[i])
471 out[j + 1] = u8(s[i] >> u32(8))
472 out[j + 2] = u8(s[i] >> u32(16))
473 out[j + 3] = u8(s[i] >> u32(24))
474 j += 4
475 }
476}
477
478// chacha20_core performs chacha20 quarter round on the state s.
479// It returns a copy of updated state after quarter round.
480@[direct_array_access; inline]
481fn chacha20_core(s chacha20.State) chacha20.State {
482 mut ws := s.clone()
483 ws.qround(10)
484 for i := 0; i < 16; i++ {
485 ws[i] += s[i]
486 }
487 return ws
488}
489
490// length_to_block transforms two's length in len1 and len2 into 16-bytes block
491@[inline]
492fn length_to_block(len1 int, len2 int) []u8 {
493 mut block := []u8{len: 16}
494 binary.little_endian_put_u64(mut block[0..8], u64(len1))
495 binary.little_endian_put_u64(mut block[8..16], u64(len2))
496
497 return block
498}
499