Skip to content

Commit ca5f2da

Browse files
authored
x.crypto.chacha20poly1305: enable support for (x)chacha20 with 64-bit counter construct (#25421)
1 parent 1fb6fcc commit ca5f2da

2 files changed

Lines changed: 3666 additions & 102 deletions

File tree

‎vlib/x/crypto/chacha20poly1305/chacha20poly1305.v‎

Lines changed: 131 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
// AEAD_CHACHA20_POLY1305 is an authenticated encryption with additional data algorithm.
66
// The inputs to AEAD_CHACHA20_POLY1305 are:
77
// A 256-bit key
8-
// A 96-bit nonce (or bigger 192 bit nonce) -- different for each invocation with the same key
8+
// A 64-bit nonce, 96-bit (or bigger 192 bit nonce) -- different for each invocation with the same key
99
// An arbitrary length plaintext
1010
// Arbitrary length additional authenticated data (AAD)
1111
module chacha20poly1305
@@ -41,55 +41,61 @@ pub interface AEAD {
4141

4242
// key_size is the size of key (in bytes) which the Chacha20Poly1305 AEAD accepts.
4343
pub const key_size = 32
44+
45+
// orig_nonce_size is the size (in bytes) of nonce of the original (DJ Bernstein) variant
46+
// which the Chacha20Poly1305 AEAD accepts.
47+
pub const orig_nonce_size = 8
4448
// nonce_size is the size of the standard nonce (in bytes) which the Chacha20Poly1305 AEAD accepts.
4549
pub const nonce_size = 12
4650
// nonce_size is the size of the extended nonce (in bytes) which the Chacha20Poly1305 AEAD accepts.
4751
pub const x_nonce_size = 24
52+
4853
// tag_size is the size of the message authenticated code (in bytes) produced by Chacha20Poly1305 AEAD.
4954
pub const tag_size = 16
5055

5156
// encrypt does one-shot encryption of given plaintext with associated key, nonce and additional data.
5257
// It return ciphertext output and authenticated tag appended into it.
53-
pub fn encrypt(plaintext []u8, key []u8, nonce []u8, ad []u8) ![]u8 {
54-
mut c := new(key, nonce.len)!
55-
out := c.encrypt(plaintext, nonce, ad)!
56-
return out
58+
pub fn encrypt(plaintext []u8, key []u8, nonce []u8, ad []u8, opt chacha20.Options) ![]u8 {
59+
mut c := new(key, nonce.len, opt)!
60+
return c.encrypt(plaintext, nonce, ad)!
5761
}
5862

5963
// decrypt does one-shot decryption of given ciphertext with associated key, nonce and additional data.
6064
// It return plaintext output and verify if resulting tag is a valid message authenticated code (mac)
6165
// for given message, key and additional data.
62-
pub fn decrypt(ciphertext []u8, key []u8, nonce []u8, ad []u8) ![]u8 {
63-
mut c := new(key, nonce.len)!
64-
out := c.decrypt(ciphertext, nonce, ad)!
65-
return out
66+
pub fn decrypt(ciphertext []u8, key []u8, nonce []u8, ad []u8, opt chacha20.Options) ![]u8 {
67+
mut c := new(key, nonce.len, opt)!
68+
return c.decrypt(ciphertext, nonce, ad)!
6669
}
6770

6871
// Chacha20Poly1305 represents AEAD algorithm backed by `x.crypto.chacha20` and `x.crypto.poly1305`.
72+
@[noinit]
6973
struct Chacha20Poly1305 {
70-
key []u8 = []u8{len: key_size}
71-
ncsize int = nonce_size
74+
mut:
75+
key []u8 = []u8{len: key_size}
76+
nsize int
77+
opt chacha20.Options // for XChaCha20 construct
7278
}
7379

7480
// new creates a new Chacha20Poly1305 AEAD instance with given 32 bytes of key
75-
// and the nonce size in ncsize. The ncsize should be 12 or 24 length, otherwise it would return error.
76-
pub fn new(key []u8, ncsize int) !&AEAD {
81+
// and the nonce size in nsize. The nsize should be 8, 12 or 24 length, otherwise it would return error.
82+
pub fn new(key []u8, nsize int, opt chacha20.Options) !&AEAD {
7783
if key.len != key_size {
7884
return error('chacha20poly1305: bad key size')
7985
}
80-
if ncsize != nonce_size && ncsize != x_nonce_size {
81-
return error('chacha20poly1305: bad nonce size supplied, its should 12 or 24')
86+
if nsize != orig_nonce_size && nsize != nonce_size && nsize != x_nonce_size {
87+
return error('chacha20poly1305: bad nonce size supplied, its should 8, 12 or 24')
8288
}
83-
c := &Chacha20Poly1305{
84-
key: key
85-
ncsize: ncsize
89+
return &Chacha20Poly1305{
90+
key: key
91+
nsize: nsize
92+
opt: opt
8693
}
87-
return c
8894
}
8995

9096
// nonce_size returns the size of underlying nonce (in bytes) of AEAD algorithm.
9197
pub fn (c Chacha20Poly1305) nonce_size() int {
92-
return c.ncsize
98+
return c.nsize
9399
}
94100

95101
// overhead returns maximum difference between the lengths of a plaintext to be encrypted and
@@ -116,42 +122,7 @@ pub fn (c Chacha20Poly1305) encrypt(plaintext []u8, nonce []u8, ad []u8) ![]u8 {
116122
if ad.len > max_u64 {
117123
return error('chacha20poly1305: something bad in your additional data')
118124
}
119-
return c.encrypt_generic(plaintext, nonce, ad)
120-
}
121-
122-
// encrypt_generic encrypts plaintext along with nonce and additional data
123-
fn (c Chacha20Poly1305) encrypt_generic(plaintext []u8, nonce []u8, ad []u8) ![]u8 {
124-
// First, generates a Poly1305 one-time key from the 256-bit key
125-
// and given nonce. Actually its generates by performing ChaCha20 key stream function,
126-
// and take the first 32 bytes as a one-time key for Poly1305 from 64 bytes results.
127-
// see https://datatracker.ietf.org/doc/html/rfc8439#section-2.6
128-
mut polykey := []u8{len: key_size}
129-
mut s := chacha20.new_cipher(c.key, nonce)!
130-
s.encrypt(mut polykey, polykey)!
131-
132-
// Next, the ChaCha20 encryption function is called to encrypt the plaintext,
133-
// using the same key and nonce, and with the initial ChaCha20 counter set to 1.
134-
mut ciphertext := []u8{len: plaintext.len}
135-
s.set_counter(1)
136-
s.encrypt(mut ciphertext, plaintext)!
137-
138-
// Finally, the Poly1305 function is called with the generated Poly1305 one-time key
139-
// calculated above, and a message constructed as described in
140-
// https://datatracker.ietf.org/doc/html/rfc8439#section-2.8
141-
mut constructed_msg := []u8{}
142-
poly1305_construct_msg(mut constructed_msg, ad, ciphertext)
143-
144-
// Lets creates Poly1305 instance with one-time key generates in above step,
145-
// updates Poly1305 state with this constructed_msg and finally generates tag.
146-
mut tag := []u8{len: tag_size}
147-
mut po := poly1305.new(polykey)!
148-
po.update(constructed_msg)
149-
po.finish(mut tag)
150-
151-
// add this tag to ciphertext output
152-
ciphertext << tag
153-
154-
return ciphertext
125+
return c.generic_crypt(plaintext, nonce, ad, .encrypt)!
155126
}
156127

157128
// decrypt decrypts ciphertext along with provided nonce and additional data.
@@ -162,6 +133,10 @@ fn (c Chacha20Poly1305) encrypt_generic(plaintext []u8, nonce []u8, ad []u8) ![]
162133
// The calculated mac is bitwise compared to the received mac.
163134
// The message is authenticated if and only if the tags match, return error if failed to verify.
164135
pub fn (c Chacha20Poly1305) decrypt(ciphertext []u8, nonce []u8, ad []u8) ![]u8 {
136+
// Preliminary check
137+
if ciphertext.len < tag_size {
138+
return error('chacha20poly1305: ciphertext length does not meet minimum required length')
139+
}
165140
if nonce.len != c.nonce_size() {
166141
return error('chacha20poly1305: unmatching nonce size')
167142
}
@@ -170,74 +145,128 @@ pub fn (c Chacha20Poly1305) decrypt(ciphertext []u8, nonce []u8, ad []u8) ![]u8
170145
if u64(ciphertext.len) > (u64(1) << 38) - 48 {
171146
return error('chacha20poly1305: ciphertext too large')
172147
}
173-
return c.decrypt_generic(ciphertext, nonce, ad)
148+
return c.generic_crypt(ciphertext, nonce, ad, .decrypt)!
174149
}
175150

176-
fn (c Chacha20Poly1305) decrypt_generic(ciphertext []u8, nonce []u8, ad []u8) ![]u8 {
177-
// generates poly1305 one-time key for later calculation
178-
mut polykey := []u8{len: key_size}
179-
mut s := chacha20.new_cipher(c.key, nonce)!
180-
s.encrypt(mut polykey, polykey)!
151+
// Helpers
181152

182-
// Remember, ciphertext is concatenation of associated cipher output plus tag (mac) bytes
183-
encrypted := ciphertext[0..ciphertext.len - c.overhead()]
184-
mac := ciphertext[ciphertext.len - c.overhead()..]
153+
// generic_crypt direction
154+
enum Mode {
155+
encrypt = 0
156+
decrypt = 1
157+
}
185158

186-
mut plaintext := []u8{len: encrypted.len}
187-
s.set_counter(1)
188-
// doing reverse encrypt on cipher output part produces plaintext
189-
s.encrypt(mut plaintext, encrypted)!
159+
// generic_crypt does generic encryption or decryption based on the mode flag was passed.
160+
// See AEAD Construction at https://datatracker.ietf.org/doc/html/rfc8439#section-2.8
161+
@[direct_array_access; inline]
162+
fn (c Chacha20Poly1305) generic_crypt(msg []u8, nonce []u8, ad []u8, mode Mode) ![]u8 {
163+
// Setup some values
164+
mut src := unsafe { msg }
165+
mut mac := []u8{} // used in decryption
166+
if mode == .decrypt {
167+
src = unsafe { msg[0..msg.len - c.overhead()] }
168+
mac = unsafe { msg[msg.len - c.overhead()..] }
169+
}
170+
171+
// generates 32-bytes of one-time key for later poly1305 operation
172+
mut otkey := []u8{len: key_size}
173+
mut s := chacha20.new_cipher(c.key, nonce, c.opt)!
174+
s.encrypt(mut otkey, otkey)!
175+
176+
// destination buffer, with overhead spaces for generated tag without reallocating
177+
mut dst := []u8{len: src.len, cap: src.len + c.overhead()}
190178

191-
// authenticated messages part
192-
mut constructed_msg := []u8{}
193-
poly1305_construct_msg(mut constructed_msg, ad, encrypted)
179+
// Next, the ChaCha20 encryption function is called to encrypt (decrypt) message input,
180+
// using the same key and nonce, and with the initial counter set to 1.
181+
s.set_counter(1)
182+
s.encrypt(mut dst, src)!
194183

184+
// Finally, the Poly1305 function is called with the Poly1305 key calculated above
185+
// to build message authentication code (tag).
186+
// length of constructed message
187+
cm_length := if mode == .encrypt {
188+
length_constructed_msg(ad, dst)
189+
} else {
190+
length_constructed_msg(ad, src)
191+
}
192+
mut constructed_msg := []u8{len: cm_length}
193+
if mode == .encrypt {
194+
construct_msg(mut constructed_msg, ad, dst)
195+
} else {
196+
construct_msg(mut constructed_msg, ad, src)
197+
}
195198
mut tag := []u8{len: tag_size}
196-
mut po := poly1305.new(polykey)!
199+
mut po := poly1305.new(otkey)!
197200
po.update(constructed_msg)
198201
po.finish(mut tag)
199202

200-
// lets verify if received mac is matching with calculated tag,
201-
// return error on fail and free allocated resource.
202-
if subtle.constant_time_compare(mac, tag) != 1 {
203-
// free allocated resource
204-
unsafe {
205-
s.free()
206-
tag.free()
207-
polykey.free()
208-
plaintext.free()
203+
// If this a decryption mode, lets verify whether this calculated tag was matching
204+
// with the supplied mac, otherwise return error on fails and free allocated resources.
205+
if mode == .decrypt {
206+
if subtle.constant_time_compare(mac, tag) != 1 {
207+
// free allocated resource
208+
unsafe {
209+
s.free()
210+
tag.free()
211+
otkey.free()
212+
dst.free()
213+
constructed_msg.free()
214+
}
215+
return error('chacha20poly1305: unmatching tag')
216+
} else {
217+
// return the decrypted message (plaintext) when the tag was matching
218+
return dst
209219
}
210-
return error('chacha20poly1305: unmatching tag')
211220
}
212-
213-
return plaintext
221+
// In the encryption mode, appends the tag into end of destination buffer
222+
dst << tag
223+
return dst
214224
}
215225

216-
// Helper function
217-
218226
// pad x to 16 bytes block
227+
@[direct_array_access; inline]
219228
fn pad_to_16(x []u8) []u8 {
220-
mut buf := x.clone()
221-
if buf.len % 16 == 0 {
222-
return buf
229+
if x.len % 16 == 0 {
230+
return x
223231
}
224-
pad_bytes := []u8{len: 16 - buf.len % 16, init: 0}
225-
buf << pad_bytes
226-
return buf
232+
mut out := []u8{len: x.len + (16 - x.len % 16)}
233+
_ := copy(mut out, x)
234+
return out
235+
}
236+
237+
// The length of padded x
238+
@[inline]
239+
fn length_pad_to_16(x []u8) int {
240+
if x.len % 16 == 0 {
241+
return x.len
242+
}
243+
//
244+
return x.len + (16 - x.len % 16)
245+
}
246+
247+
// The length of constructed message
248+
@[inline]
249+
fn length_constructed_msg(ad []u8, bytes []u8) int {
250+
mut n := 0
251+
n += length_pad_to_16(ad)
252+
n += length_pad_to_16(bytes)
253+
n += 16 // 2 * 8 bytes
254+
return n
227255
}
228256

229-
// poly1305_construct_msg constructs poly1305 message for later usage and stores to out.
257+
// construct_msg builds a message for later usage and stored into out.
258+
// The last step on the AEAD Construction on the how the message was constructed.
259+
// See the details on the [2.8](https://datatracker.ietf.org/doc/html/rfc8439#section-2.8)
230260
// The message constructed as a concatenation of the following:
231261
// * padded to multiple of 16 bytes block of the additional data bytes
232262
// * padded to multiple of 16 bytes block of the ciphertext (or plaintext) bytes
233263
// * The length of the additional data in octets (as a 64-bit little-endian integer).
234264
// * The length of the ciphertext (or plaintext) in octets (as a 64-bit little-endian integer).
235-
fn poly1305_construct_msg(mut out []u8, ad []u8, bytes []u8) {
236-
mut b8 := []u8{len: 8}
237-
out << pad_to_16(ad)
238-
out << pad_to_16(bytes)
239-
binary.little_endian_put_u64(mut b8, u64(ad.len))
240-
out << b8
241-
binary.little_endian_put_u64(mut b8, u64(bytes.len))
242-
out << b8
265+
// Assumed the output buffer length was correctly initialized.
266+
@[direct_array_access; inline]
267+
fn construct_msg(mut out []u8, ad []u8, bytes []u8) {
268+
n0 := copy(mut out, pad_to_16(ad))
269+
n1 := copy(mut out[n0..], pad_to_16(bytes))
270+
binary.little_endian_put_u64(mut out[n0 + n1..], u64(ad.len))
271+
binary.little_endian_put_u64(mut out[n0 + n1 + 8..], u64(bytes.len))
243272
}

0 commit comments

Comments
 (0)