v / vlib / x / crypto / chacha20poly1305 / chacha20poly1305.v
249 lines · 225 sloc · 9.17 KB · a9423b51df5a1d4f136cffe6326c9a84beba5902
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// AEAD_CHACHA20_POLY1305 is an authenticated encryption with additional data algorithm.
6// The inputs to AEAD_CHACHA20_POLY1305 are:
7// A 256-bit key
8// A 64-bit nonce, 96-bit (or bigger 192 bit nonce) -- different for each invocation with the same key
9// An arbitrary length plaintext
10// Arbitrary length additional authenticated data (AAD)
11module chacha20poly1305
12
13import crypto.cipher
14import encoding.binary
15import crypto.internal.subtle
16import x.crypto.chacha20
17import x.crypto.poly1305
18
19// key_size is the size of key (in bytes) which the Chacha20Poly1305 AEAD accepts.
20pub const key_size = 32
21
22// orig_nonce_size is the size (in bytes) of nonce of the original (DJ Bernstein) variant
23// which the Chacha20Poly1305 AEAD accepts.
24pub const orig_nonce_size = 8
25// nonce_size is the size of the standard nonce (in bytes) which the Chacha20Poly1305 AEAD accepts.
26pub const nonce_size = 12
27// nonce_size is the size of the extended nonce (in bytes) which the Chacha20Poly1305 AEAD accepts.
28pub const x_nonce_size = 24
29
30// tag_size is the size of the message authenticated code (in bytes) produced by Chacha20Poly1305 AEAD.
31pub const tag_size = 16
32
33// encrypt does one-shot encryption of given plaintext with associated key, nonce and additional data.
34// It return ciphertext output and authenticated tag appended into it.
35pub fn encrypt(plaintext []u8, key []u8, nonce []u8, ad []u8, opt chacha20.Options) ![]u8 {
36 mut c := new(key, nonce.len, opt)!
37 return c.encrypt(plaintext, nonce, ad)!
38}
39
40// decrypt does one-shot decryption of given ciphertext with associated key, nonce and additional data.
41// It return plaintext output and verify if resulting tag is a valid message authenticated code (mac)
42// for given message, key and additional data.
43pub fn decrypt(ciphertext []u8, key []u8, nonce []u8, ad []u8, opt chacha20.Options) ![]u8 {
44 mut c := new(key, nonce.len, opt)!
45 return c.decrypt(ciphertext, nonce, ad)!
46}
47
48// Chacha20Poly1305 represents AEAD algorithm backed by `x.crypto.chacha20` and `x.crypto.poly1305`.
49@[noinit]
50struct Chacha20Poly1305 {
51mut:
52 key []u8 = []u8{len: key_size}
53 nsize int
54 opt chacha20.Options // for XChaCha20 construct
55}
56
57// new creates a new Chacha20Poly1305 AEAD instance with given 32 bytes of key
58// and the nonce size in nsize. The nsize should be 8, 12 or 24 length, otherwise it would return error.
59pub fn new(key []u8, nsize int, opt chacha20.Options) !&cipher.AEAD {
60 if key.len != key_size {
61 return error('chacha20poly1305: bad key size')
62 }
63 if nsize != orig_nonce_size && nsize != nonce_size && nsize != x_nonce_size {
64 return error('chacha20poly1305: bad nonce size supplied, its should 8, 12 or 24')
65 }
66 return &Chacha20Poly1305{
67 key: key
68 nsize: nsize
69 opt: opt
70 }
71}
72
73// nonce_size returns the size of underlying nonce (in bytes) of AEAD algorithm.
74pub fn (c Chacha20Poly1305) nonce_size() int {
75 return c.nsize
76}
77
78// overhead returns maximum difference between the lengths of a plaintext to be encrypted and
79// ciphertext's output. In the context of Chacha20Poly1305, `.overhead() == .tag_size`.
80pub fn (c Chacha20Poly1305) overhead() int {
81 return tag_size
82}
83
84// encrypt encrypts plaintext, along with nonce and additional data and generates
85// authenticated tag appended into ciphertext's output.
86pub fn (c Chacha20Poly1305) encrypt(plaintext []u8, nonce []u8, ad []u8) ![]u8 {
87 // makes sure if the nonce length is matching with internal nonce size
88 if nonce.len != c.nonce_size() {
89 return error('chacha20poly1305: unmatching nonce size')
90 }
91 // check if the plaintext length doesn't exceed the amount of limit.
92 // its comes from the internal of chacha20 mechanism, where the counter are u32
93 // with the facts of chacha20 operates on 64 bytes block, we can measure the amount
94 // of encrypted data possible in a single invocation, ie.,
95 // amount = (2^32-1)*64 = 274,877,906,880 bytes, or nearly 256 GB
96 if u64(plaintext.len) > (u64(1) << 38) - 64 {
97 panic('chacha20poly1305: plaintext too large')
98 }
99 if ad.len > max_u64 {
100 return error('chacha20poly1305: something bad in your additional data')
101 }
102 return c.generic_crypt(plaintext, nonce, ad, .encrypt)!
103}
104
105// decrypt decrypts ciphertext along with provided nonce and additional data.
106// Decryption is similar with the encryption process with slight differences in:
107// The roles of ciphertext and plaintext are reversed, so the ChaCha20 encryption
108// function is applied to the ciphertext, producing the plaintext.
109// The Poly1305 function is still run on the AAD and the ciphertext, not the plaintext.
110// The calculated mac is bitwise compared to the received mac.
111// The message is authenticated if and only if the tags match, return error if failed to verify.
112pub fn (c Chacha20Poly1305) decrypt(ciphertext []u8, nonce []u8, ad []u8) ![]u8 {
113 // Preliminary check
114 if ciphertext.len < tag_size {
115 return error('chacha20poly1305: ciphertext length does not meet minimum required length')
116 }
117 if nonce.len != c.nonce_size() {
118 return error('chacha20poly1305: unmatching nonce size')
119 }
120 // ciphertext max = plaintext max length + tag length
121 // ie, (2^32-1)*64 + overhead = (u64(1) << 38) - 64 + 16 = 274,877,906,896 octets.
122 if u64(ciphertext.len) > (u64(1) << 38) - 48 {
123 return error('chacha20poly1305: ciphertext too large')
124 }
125 return c.generic_crypt(ciphertext, nonce, ad, .decrypt)!
126}
127
128// Helpers
129
130// generic_crypt direction
131enum Mode {
132 encrypt = 0
133 decrypt = 1
134}
135
136// generic_crypt does generic encryption or decryption based on the mode flag was passed.
137// See AEAD Construction at https://datatracker.ietf.org/doc/html/rfc8439#section-2.8
138@[direct_array_access; inline]
139fn (c Chacha20Poly1305) generic_crypt(msg []u8, nonce []u8, ad []u8, mode Mode) ![]u8 {
140 // Setup some values
141 mut src := unsafe { msg }
142 mut mac := []u8{} // used in decryption
143 if mode == .decrypt {
144 src = unsafe { msg[0..msg.len - c.overhead()] }
145 mac = unsafe { msg[msg.len - c.overhead()..] }
146 }
147
148 // generates 32-bytes of one-time key for later poly1305 operation
149 mut otkey := []u8{len: key_size}
150 mut s := chacha20.new_cipher(c.key, nonce, c.opt)!
151 s.encrypt(mut otkey, otkey)!
152
153 // destination buffer, with overhead spaces for generated tag without reallocating
154 mut dst := []u8{len: src.len, cap: src.len + c.overhead()}
155
156 // Next, the ChaCha20 encryption function is called to encrypt (decrypt) message input,
157 // using the same key and nonce, and with the initial counter set to 1.
158 s.set_counter(1)
159 s.encrypt(mut dst, src)!
160
161 // Finally, the Poly1305 function is called with the Poly1305 key calculated above
162 // to build message authentication code (tag).
163 // length of constructed message
164 cm_length := if mode == .encrypt {
165 length_constructed_msg(ad, dst)
166 } else {
167 length_constructed_msg(ad, src)
168 }
169 mut constructed_msg := []u8{len: cm_length}
170 if mode == .encrypt {
171 construct_msg(mut constructed_msg, ad, dst)
172 } else {
173 construct_msg(mut constructed_msg, ad, src)
174 }
175 mut tag := []u8{len: tag_size}
176 mut po := poly1305.new(otkey)!
177 po.update(constructed_msg)
178 po.finish(mut tag)
179
180 // If this a decryption mode, lets verify whether this calculated tag was matching
181 // with the supplied mac, otherwise return error on fails and free allocated resources.
182 if mode == .decrypt {
183 if subtle.constant_time_compare(mac, tag) != 1 {
184 // free allocated resource
185 unsafe {
186 s.free()
187 tag.free()
188 otkey.free()
189 dst.free()
190 constructed_msg.free()
191 }
192 return error('chacha20poly1305: unmatching tag')
193 } else {
194 // return the decrypted message (plaintext) when the tag was matching
195 return dst
196 }
197 }
198 // In the encryption mode, appends the tag into end of destination buffer
199 dst << tag
200 return dst
201}
202
203// pad x to 16 bytes block
204@[direct_array_access; inline]
205fn pad_to_16(x []u8) []u8 {
206 if x.len % 16 == 0 {
207 return x
208 }
209 mut out := []u8{len: x.len + (16 - x.len % 16)}
210 _ := copy(mut out, x)
211 return out
212}
213
214// The length of padded x
215@[inline]
216fn length_pad_to_16(x []u8) int {
217 if x.len % 16 == 0 {
218 return x.len
219 }
220 //
221 return x.len + (16 - x.len % 16)
222}
223
224// The length of constructed message
225@[inline]
226fn length_constructed_msg(ad []u8, bytes []u8) int {
227 mut n := 0
228 n += length_pad_to_16(ad)
229 n += length_pad_to_16(bytes)
230 n += 16 // 2 * 8 bytes
231 return n
232}
233
234// construct_msg builds a message for later usage and stored into out.
235// The last step on the AEAD Construction on the how the message was constructed.
236// See the details on the [2.8](https://datatracker.ietf.org/doc/html/rfc8439#section-2.8)
237// The message constructed as a concatenation of the following:
238// * padded to multiple of 16 bytes block of the additional data bytes
239// * padded to multiple of 16 bytes block of the ciphertext (or plaintext) bytes
240// * The length of the additional data in octets (as a 64-bit little-endian integer).
241// * The length of the ciphertext (or plaintext) in octets (as a 64-bit little-endian integer).
242// Assumed the output buffer length was correctly initialized.
243@[direct_array_access; inline]
244fn construct_msg(mut out []u8, ad []u8, bytes []u8) {
245 n0 := copy(mut out, pad_to_16(ad))
246 n1 := copy(mut out[n0..], pad_to_16(bytes))
247 binary.little_endian_put_u64(mut out[n0 + n1..], u64(ad.len))
248 binary.little_endian_put_u64(mut out[n0 + n1 + 8..], u64(bytes.len))
249}
250