Skip to content

Commit

Permalink
x.crypto: add AEAD ChaCha20Poly1305 algorithm in pure v (#20817)
Browse files Browse the repository at this point in the history
  • Loading branch information
blackshirt committed Feb 14, 2024
1 parent ca45311 commit 9476aed
Show file tree
Hide file tree
Showing 4 changed files with 647 additions and 0 deletions.
38 changes: 38 additions & 0 deletions vlib/x/crypto/chacha20poly1305/README.md
@@ -0,0 +1,38 @@
# chacha20poly1305

Chacha20Poly1305 Authenticated Encryption with Additional Data (AEAD) module for V Language

This module provides authenticated encryption with additional data (AEAD) algorithm in V Language.
Its backed by experimental `x.crypto.chacha20` symetric key stream cipher encryption
module and `x.crypto.poly1305` message authentication code (MAC) module.

> [!Warning]
> This is an absolutely experimental module, which is subject to change.
> Please use it carefully, thoroughly and wisely.
Examples
--------
```v
import encoding.hex
import x.crypto.chacha20poly1305
fn main() {
// plaintext message to be encrypted and authenticated
message := "Ladies and Gentlemen of the class of '99: If I could offer you only \
one tip for the future, sunscreen would be it.".bytes()
// sets your secure random key
key := hex.decode('808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f')!
// give yours nonce
nonce := hex.decode('070000004041424344454647')!
// yours additional data
aad := hex.decode('50515253c0c1c2c3c4c5c6c7')!
// lets doing authenticated encryption
ciphertext := chacha20poly1305.encrypt(message, key, nonce, aad)!
// lets perform decryption back
plaintext := chacha20poly1305.decrypt(ciphertext, key, nonce, aad)!
assert plaintext == message
}
243 changes: 243 additions & 0 deletions vlib/x/crypto/chacha20poly1305/chacha20poly1305.v
@@ -0,0 +1,243 @@
// 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 96-bit nonce (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 encoding.binary
import crypto.internal.subtle
import x.crypto.chacha20
import x.crypto.poly1305

// This interface was a proposed draft for Authenticated Encryption with Additional Data (AEAD)
// interface `AEAD` likes discussion at discord channel.
// see https://discord.com/channels/592103645835821068/592320321995014154/1206029352412778577
// But its little modified to be more v-idiomatic.
// Note: This interface should be more appropriately located in `crypto.cipher`, we can move
// it into `crypto.cipher` later.
// Authenticated Encryption with Additional Data (AEAD) interface
pub interface AEAD {
// nonce_size return the nonce size (in bytes) used by this AEAD algorithm that should be
// passed to `.encrypt` or `.decrypt`.
nonce_size() int
// overhead returns the maximum difference between the lengths of a plaintext and its ciphertext.
overhead() int
// encrypt encrypts and authenticates the provided plaintext along with a nonce, and
// to be authenticated additional data in `ad`.
// It returns ciphertext bytes where its encoded form is up to implementation and
// not dictated by the interfaces.
// Usually its contains encrypted text plus some authentication tag, and maybe some other bytes.
encrypt(plaintext []u8, nonce []u8, ad []u8) ![]u8
// decrypt decrypts and authenticates (verifies) the provided ciphertext along with a nonce, and
// additional data. If verified successfully, it returns the plaintext and error otherwise.
decrypt(ciphertext []u8, nonce []u8, ad []u8) ![]u8
}

// key_size is the size of key (in bytes) which the Chacha20Poly1305 AEAD accepts.
pub const key_size = 32
// nonce_size is the size of the standar 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) ![]u8 {
mut c := new(key, nonce.len)!
out := c.encrypt(plaintext, nonce, ad)!
return out
}

// 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) ![]u8 {
mut c := new(key, nonce.len)!
out := c.decrypt(ciphertext, nonce, ad)!
return out
}

// Chacha20Poly1305 represents AEAD algorithm backed by `x.crypto.chacha20` and `x.crypto.poly1305`.
struct Chacha20Poly1305 {
key []u8 = []u8{len: chacha20poly1305.key_size}
ncsize int = chacha20poly1305.nonce_size
}

// new creates a new Chacha20Poly1305 AEAD instance with given 32 bytes of key
// and the nonce size in ncsize. The ncsize should be 12 or 24 length, otherwise it would return error.
pub fn new(key []u8, ncsize int) !&AEAD {
if key.len != chacha20poly1305.key_size {
return error('chacha20poly1305: bad key size')
}
if ncsize != chacha20poly1305.nonce_size && ncsize != chacha20poly1305.x_nonce_size {
return error('chacha20poly1305: bad nonce size supplied, its should 12 or 24')
}
c := &Chacha20Poly1305{
key: key
ncsize: ncsize
}
return c
}

// nonce_size returns the size of underlying nonce (in bytes) of AEAD algorithm.
pub fn (c Chacha20Poly1305) nonce_size() int {
return c.ncsize
}

// 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 chacha20poly1305.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.encrypt_generic(plaintext, nonce, ad)
}

// encrypt_generic encrypts plaintext along with nonce and additional data
fn (c Chacha20Poly1305) encrypt_generic(plaintext []u8, nonce []u8, ad []u8) ![]u8 {
// First, generates a Poly1305 one-time key from the 256-bit key
// and given nonce. Actually its generates by performing ChaCha20 key stream function,
// and take the first 32 bytes as a one-time key for Poly1305 from 64 bytes results.
// see https://datatracker.ietf.org/doc/html/rfc8439#section-2.6
mut polykey := []u8{len: chacha20poly1305.key_size}
mut s := chacha20.new_cipher(c.key, nonce)!
s.xor_key_stream(mut polykey, polykey)

// Next, the ChaCha20 encryption function is called to encrypt the plaintext,
// using the same key and nonce, and with the initial ChaCha20 counter set to 1.
mut ciphertext := []u8{len: plaintext.len}
s.set_counter(1)
s.xor_key_stream(mut ciphertext, plaintext)

// Finally, the Poly1305 function is called with the generated Poly1305 one-time key
// calculated above, and a message constructed as descibed in
// https://datatracker.ietf.org/doc/html/rfc8439#section-2.8
mut constructed_msg := []u8{}
poly1305_construct_msg(mut constructed_msg, ad, ciphertext)

// Lets creates Poly1305 instance with one-time key generates in above step,
// updates Poly1305 state with this constructed_msg and finally generates tag.
mut tag := []u8{len: chacha20poly1305.tag_size}
mut po := poly1305.new(polykey)!
po.update(constructed_msg)
po.finish(mut tag)

// add this tag to ciphertext output
ciphertext << tag

return ciphertext
}

// decrypt decrypts ciphertext along with provided nonce and additional data.
// Decryption is similar with the encryption processs 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 {
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.decrypt_generic(ciphertext, nonce, ad)
}

fn (c Chacha20Poly1305) decrypt_generic(ciphertext []u8, nonce []u8, ad []u8) ![]u8 {
// generates poly1305 one-time key for later calculation
mut polykey := []u8{len: chacha20poly1305.key_size}
mut s := chacha20.new_cipher(c.key, nonce)!
s.xor_key_stream(mut polykey, polykey)

// Remember, ciphertext is concatenation of associated cipher output plus tag (mac) bytes
encrypted := ciphertext[0..ciphertext.len - c.overhead()]
mac := ciphertext[ciphertext.len - c.overhead()..]

mut plaintext := []u8{len: encrypted.len}
s.set_counter(1)
// doing reverse encrypt on cipher output part produces plaintext
s.xor_key_stream(mut plaintext, encrypted)

// authenticated messages part
mut constructed_msg := []u8{}
poly1305_construct_msg(mut constructed_msg, ad, encrypted)

mut tag := []u8{len: chacha20poly1305.tag_size}
mut po := poly1305.new(polykey)!
po.update(constructed_msg)
po.finish(mut tag)

// lets verify if received mac is matching with calculated tag,
// return error on fail and free allocated resource.
if subtle.constant_time_compare(mac, tag) != 1 {
// free allocated resource
unsafe {
s.free()
tag.free()
polykey.free()
plaintext.free()
}
return error('chacha20poly1305: unmatching tag')
}

return plaintext
}

// Helper function

// pad x to 16 bytes block
fn pad_to_16(x []u8) []u8 {
mut buf := x.clone()
if buf.len % 16 == 0 {
return buf
}
pad_bytes := []u8{len: 16 - buf.len % 16, init: 0}
buf << pad_bytes
return buf
}

// poly1305_construct_msg constructs poly1305 message for later usage and stores to out.
// 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).
fn poly1305_construct_msg(mut out []u8, ad []u8, bytes []u8) {
mut b8 := []u8{len: 8}
out << pad_to_16(ad)
out << pad_to_16(bytes)
binary.little_endian_put_u64(mut b8, u64(ad.len))
out << b8
binary.little_endian_put_u64(mut b8, u64(bytes.len))
out << b8
}

0 comments on commit 9476aed

Please sign in to comment.