250 lines
9.2 KiB
V
250 lines
9.2 KiB
V
// Copyright (c) 2024 blackshirt.
|
|
// Use of this source code is governed by an MIT license
|
|
// that can be found in the LICENSE file.
|
|
//
|
|
// AEAD_CHACHA20_POLY1305 is an authenticated encryption with additional data algorithm.
|
|
// The inputs to AEAD_CHACHA20_POLY1305 are:
|
|
// A 256-bit key
|
|
// A 64-bit nonce, 96-bit (or bigger 192 bit nonce) -- different for each invocation with the same key
|
|
// An arbitrary length plaintext
|
|
// Arbitrary length additional authenticated data (AAD)
|
|
module chacha20poly1305
|
|
|
|
import crypto.cipher
|
|
import encoding.binary
|
|
import crypto.internal.subtle
|
|
import x.crypto.chacha20
|
|
import x.crypto.poly1305
|
|
|
|
// key_size is the size of key (in bytes) which the Chacha20Poly1305 AEAD accepts.
|
|
pub const key_size = 32
|
|
|
|
// orig_nonce_size is the size (in bytes) of nonce of the original (DJ Bernstein) variant
|
|
// which the Chacha20Poly1305 AEAD accepts.
|
|
pub const orig_nonce_size = 8
|
|
// nonce_size is the size of the standard nonce (in bytes) which the Chacha20Poly1305 AEAD accepts.
|
|
pub const nonce_size = 12
|
|
// nonce_size is the size of the extended nonce (in bytes) which the Chacha20Poly1305 AEAD accepts.
|
|
pub const x_nonce_size = 24
|
|
|
|
// tag_size is the size of the message authenticated code (in bytes) produced by Chacha20Poly1305 AEAD.
|
|
pub const tag_size = 16
|
|
|
|
// encrypt does one-shot encryption of given plaintext with associated key, nonce and additional data.
|
|
// It return ciphertext output and authenticated tag appended into it.
|
|
pub fn encrypt(plaintext []u8, key []u8, nonce []u8, ad []u8, opt chacha20.Options) ![]u8 {
|
|
mut c := new(key, nonce.len, opt)!
|
|
return c.encrypt(plaintext, nonce, ad)!
|
|
}
|
|
|
|
// decrypt does one-shot decryption of given ciphertext with associated key, nonce and additional data.
|
|
// It return plaintext output and verify if resulting tag is a valid message authenticated code (mac)
|
|
// for given message, key and additional data.
|
|
pub fn decrypt(ciphertext []u8, key []u8, nonce []u8, ad []u8, opt chacha20.Options) ![]u8 {
|
|
mut c := new(key, nonce.len, opt)!
|
|
return c.decrypt(ciphertext, nonce, ad)!
|
|
}
|
|
|
|
// Chacha20Poly1305 represents AEAD algorithm backed by `x.crypto.chacha20` and `x.crypto.poly1305`.
|
|
@[noinit]
|
|
struct Chacha20Poly1305 {
|
|
mut:
|
|
key []u8 = []u8{len: key_size}
|
|
nsize int
|
|
opt chacha20.Options // for XChaCha20 construct
|
|
}
|
|
|
|
// new creates a new Chacha20Poly1305 AEAD instance with given 32 bytes of key
|
|
// and the nonce size in nsize. The nsize should be 8, 12 or 24 length, otherwise it would return error.
|
|
pub fn new(key []u8, nsize int, opt chacha20.Options) !&cipher.AEAD {
|
|
if key.len != key_size {
|
|
return error('chacha20poly1305: bad key size')
|
|
}
|
|
if nsize != orig_nonce_size && nsize != nonce_size && nsize != x_nonce_size {
|
|
return error('chacha20poly1305: bad nonce size supplied, its should 8, 12 or 24')
|
|
}
|
|
return &Chacha20Poly1305{
|
|
key: key
|
|
nsize: nsize
|
|
opt: opt
|
|
}
|
|
}
|
|
|
|
// nonce_size returns the size of underlying nonce (in bytes) of AEAD algorithm.
|
|
pub fn (c Chacha20Poly1305) nonce_size() int {
|
|
return c.nsize
|
|
}
|
|
|
|
// overhead returns maximum difference between the lengths of a plaintext to be encrypted and
|
|
// ciphertext's output. In the context of Chacha20Poly1305, `.overhead() == .tag_size`.
|
|
pub fn (c Chacha20Poly1305) overhead() int {
|
|
return tag_size
|
|
}
|
|
|
|
// encrypt encrypts plaintext, along with nonce and additional data and generates
|
|
// authenticated tag appended into ciphertext's output.
|
|
pub fn (c Chacha20Poly1305) encrypt(plaintext []u8, nonce []u8, ad []u8) ![]u8 {
|
|
// makes sure if the nonce length is matching with internal nonce size
|
|
if nonce.len != c.nonce_size() {
|
|
return error('chacha20poly1305: unmatching nonce size')
|
|
}
|
|
// check if the plaintext length doesn't exceed the amount of limit.
|
|
// its comes from the internal of chacha20 mechanism, where the counter are u32
|
|
// with the facts of chacha20 operates on 64 bytes block, we can measure the amount
|
|
// of encrypted data possible in a single invocation, ie.,
|
|
// amount = (2^32-1)*64 = 274,877,906,880 bytes, or nearly 256 GB
|
|
if u64(plaintext.len) > (u64(1) << 38) - 64 {
|
|
panic('chacha20poly1305: plaintext too large')
|
|
}
|
|
if ad.len > max_u64 {
|
|
return error('chacha20poly1305: something bad in your additional data')
|
|
}
|
|
return c.generic_crypt(plaintext, nonce, ad, .encrypt)!
|
|
}
|
|
|
|
// decrypt decrypts ciphertext along with provided nonce and additional data.
|
|
// Decryption is similar with the encryption process with slight differences in:
|
|
// The roles of ciphertext and plaintext are reversed, so the ChaCha20 encryption
|
|
// function is applied to the ciphertext, producing the plaintext.
|
|
// The Poly1305 function is still run on the AAD and the ciphertext, not the plaintext.
|
|
// The calculated mac is bitwise compared to the received mac.
|
|
// The message is authenticated if and only if the tags match, return error if failed to verify.
|
|
pub fn (c Chacha20Poly1305) decrypt(ciphertext []u8, nonce []u8, ad []u8) ![]u8 {
|
|
// Preliminary check
|
|
if ciphertext.len < tag_size {
|
|
return error('chacha20poly1305: ciphertext length does not meet minimum required length')
|
|
}
|
|
if nonce.len != c.nonce_size() {
|
|
return error('chacha20poly1305: unmatching nonce size')
|
|
}
|
|
// ciphertext max = plaintext max length + tag length
|
|
// ie, (2^32-1)*64 + overhead = (u64(1) << 38) - 64 + 16 = 274,877,906,896 octets.
|
|
if u64(ciphertext.len) > (u64(1) << 38) - 48 {
|
|
return error('chacha20poly1305: ciphertext too large')
|
|
}
|
|
return c.generic_crypt(ciphertext, nonce, ad, .decrypt)!
|
|
}
|
|
|
|
// Helpers
|
|
|
|
// generic_crypt direction
|
|
enum Mode {
|
|
encrypt = 0
|
|
decrypt = 1
|
|
}
|
|
|
|
// generic_crypt does generic encryption or decryption based on the mode flag was passed.
|
|
// See AEAD Construction at https://datatracker.ietf.org/doc/html/rfc8439#section-2.8
|
|
@[direct_array_access; inline]
|
|
fn (c Chacha20Poly1305) generic_crypt(msg []u8, nonce []u8, ad []u8, mode Mode) ![]u8 {
|
|
// Setup some values
|
|
mut src := unsafe { msg }
|
|
mut mac := []u8{} // used in decryption
|
|
if mode == .decrypt {
|
|
src = unsafe { msg[0..msg.len - c.overhead()] }
|
|
mac = unsafe { msg[msg.len - c.overhead()..] }
|
|
}
|
|
|
|
// generates 32-bytes of one-time key for later poly1305 operation
|
|
mut otkey := []u8{len: key_size}
|
|
mut s := chacha20.new_cipher(c.key, nonce, c.opt)!
|
|
s.encrypt(mut otkey, otkey)!
|
|
|
|
// destination buffer, with overhead spaces for generated tag without reallocating
|
|
mut dst := []u8{len: src.len, cap: src.len + c.overhead()}
|
|
|
|
// Next, the ChaCha20 encryption function is called to encrypt (decrypt) message input,
|
|
// using the same key and nonce, and with the initial counter set to 1.
|
|
s.set_counter(1)
|
|
s.encrypt(mut dst, src)!
|
|
|
|
// Finally, the Poly1305 function is called with the Poly1305 key calculated above
|
|
// to build message authentication code (tag).
|
|
// length of constructed message
|
|
cm_length := if mode == .encrypt {
|
|
length_constructed_msg(ad, dst)
|
|
} else {
|
|
length_constructed_msg(ad, src)
|
|
}
|
|
mut constructed_msg := []u8{len: cm_length}
|
|
if mode == .encrypt {
|
|
construct_msg(mut constructed_msg, ad, dst)
|
|
} else {
|
|
construct_msg(mut constructed_msg, ad, src)
|
|
}
|
|
mut tag := []u8{len: tag_size}
|
|
mut po := poly1305.new(otkey)!
|
|
po.update(constructed_msg)
|
|
po.finish(mut tag)
|
|
|
|
// If this a decryption mode, lets verify whether this calculated tag was matching
|
|
// with the supplied mac, otherwise return error on fails and free allocated resources.
|
|
if mode == .decrypt {
|
|
if subtle.constant_time_compare(mac, tag) != 1 {
|
|
// free allocated resource
|
|
unsafe {
|
|
s.free()
|
|
tag.free()
|
|
otkey.free()
|
|
dst.free()
|
|
constructed_msg.free()
|
|
}
|
|
return error('chacha20poly1305: unmatching tag')
|
|
} else {
|
|
// return the decrypted message (plaintext) when the tag was matching
|
|
return dst
|
|
}
|
|
}
|
|
// In the encryption mode, appends the tag into end of destination buffer
|
|
dst << tag
|
|
return dst
|
|
}
|
|
|
|
// pad x to 16 bytes block
|
|
@[direct_array_access; inline]
|
|
fn pad_to_16(x []u8) []u8 {
|
|
if x.len % 16 == 0 {
|
|
return x
|
|
}
|
|
mut out := []u8{len: x.len + (16 - x.len % 16)}
|
|
_ := copy(mut out, x)
|
|
return out
|
|
}
|
|
|
|
// The length of padded x
|
|
@[inline]
|
|
fn length_pad_to_16(x []u8) int {
|
|
if x.len % 16 == 0 {
|
|
return x.len
|
|
}
|
|
//
|
|
return x.len + (16 - x.len % 16)
|
|
}
|
|
|
|
// The length of constructed message
|
|
@[inline]
|
|
fn length_constructed_msg(ad []u8, bytes []u8) int {
|
|
mut n := 0
|
|
n += length_pad_to_16(ad)
|
|
n += length_pad_to_16(bytes)
|
|
n += 16 // 2 * 8 bytes
|
|
return n
|
|
}
|
|
|
|
// construct_msg builds a message for later usage and stored into out.
|
|
// The last step on the AEAD Construction on the how the message was constructed.
|
|
// See the details on the [2.8](https://datatracker.ietf.org/doc/html/rfc8439#section-2.8)
|
|
// The message constructed as a concatenation of the following:
|
|
// * padded to multiple of 16 bytes block of the additional data bytes
|
|
// * padded to multiple of 16 bytes block of the ciphertext (or plaintext) bytes
|
|
// * The length of the additional data in octets (as a 64-bit little-endian integer).
|
|
// * The length of the ciphertext (or plaintext) in octets (as a 64-bit little-endian integer).
|
|
// Assumed the output buffer length was correctly initialized.
|
|
@[direct_array_access; inline]
|
|
fn construct_msg(mut out []u8, ad []u8, bytes []u8) {
|
|
n0 := copy(mut out, pad_to_16(ad))
|
|
n1 := copy(mut out[n0..], pad_to_16(bytes))
|
|
binary.little_endian_put_u64(mut out[n0 + n1..], u64(ad.len))
|
|
binary.little_endian_put_u64(mut out[n0 + n1 + 8..], u64(bytes.len))
|
|
}
|