| 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) |
| 11 | module chacha20poly1305 |
| 12 | |
| 13 | import crypto.cipher |
| 14 | import encoding.binary |
| 15 | import crypto.internal.subtle |
| 16 | import x.crypto.chacha20 |
| 17 | import x.crypto.poly1305 |
| 18 | |
| 19 | // key_size is the size of key (in bytes) which the Chacha20Poly1305 AEAD accepts. |
| 20 | pub 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. |
| 24 | pub const orig_nonce_size = 8 |
| 25 | // nonce_size is the size of the standard nonce (in bytes) which the Chacha20Poly1305 AEAD accepts. |
| 26 | pub const nonce_size = 12 |
| 27 | // nonce_size is the size of the extended nonce (in bytes) which the Chacha20Poly1305 AEAD accepts. |
| 28 | pub const x_nonce_size = 24 |
| 29 | |
| 30 | // tag_size is the size of the message authenticated code (in bytes) produced by Chacha20Poly1305 AEAD. |
| 31 | pub 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. |
| 35 | pub 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. |
| 43 | pub 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] |
| 50 | struct Chacha20Poly1305 { |
| 51 | mut: |
| 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. |
| 59 | pub 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. |
| 74 | pub 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`. |
| 80 | pub 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. |
| 86 | pub 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. |
| 112 | pub 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 |
| 131 | enum 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] |
| 139 | fn (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] |
| 205 | fn 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] |
| 216 | fn 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] |
| 226 | fn 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] |
| 244 | fn 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 | |