Skip to content

Commit

Permalink
x.crypto.chacha20: speed up the core functionality of the ChaCha20 st…
Browse files Browse the repository at this point in the history
…ream cipher (#20470)
  • Loading branch information
blackshirt committed Jan 10, 2024
1 parent edd07bf commit 0713e39
Show file tree
Hide file tree
Showing 3 changed files with 34 additions and 30 deletions.
44 changes: 14 additions & 30 deletions vlib/x/crypto/chacha20/chacha.v
Expand Up @@ -76,6 +76,7 @@ pub fn decrypt(key []u8, nonce []u8, ciphertext []u8) ![]u8 {
// in src and stores the ciphertext result in dst in a single run of encryption.
// You must never use the same (key, nonce) pair more than once for encryption.
// This would void any confidentiality guarantees for the messages encrypted with the same nonce and key.
@[direct_array_access]
pub fn (mut c Cipher) xor_key_stream(mut dst []u8, src []u8) {
if src.len == 0 {
return
Expand All @@ -86,14 +87,13 @@ pub fn (mut c Cipher) xor_key_stream(mut dst []u8, src []u8) {
if subtle.inexact_overlap(dst, src) {
panic('chacha20: invalid buffer overlap')
}
mut ciphertext := []u8{}

// ChaCha20's encryption mechanism is a relatively simple operation.
// for every block_sized block from src bytes, build ChaCha20 keystream block,
// then xor each byte in the block with keystresm block and then append xor-ed bytes
// then xor each byte in the block with keystresm block and then stores xor-ed bytes
// to the output buffer. If there are remaining (trailing) partial bytes,
// generate one more keystream block, xors keystream block with partial bytes
// and append to the result.
// and stores the result.
//
// Let's process for multiple blocks
// number of blocks the src bytes should be split into
Expand All @@ -104,33 +104,20 @@ pub fn (mut c Cipher) xor_key_stream(mut dst []u8, src []u8) {
// get current src block to be xor-ed
block := unsafe { src[i * chacha20.block_size..(i + 1) * chacha20.block_size] }

// xor current block of plaintext with keystream in c.block and store result in out
mut out := []u8{len: block.len}
n := cipher.xor_bytes(mut out, block, c.block)
// instead allocating output buffer for every block, we use dst buffer directly.
// xor current block of plaintext with keystream in c.block
n := cipher.xor_bytes(mut dst[i * chacha20.block_size..(i + 1) * chacha20.block_size],
block, c.block)
assert n == c.block.len

// append current output to the ciphertext buffer
ciphertext << out
}
// process for partial block
if src.len % chacha20.block_size != 0 {
c.generic_key_stream()
// get the remaining partial block
// get the remaining last partial block
block := unsafe { src[nr_blocks * chacha20.block_size..] }
// xor block with keystream
mut out := []u8{len: block.len}
n := cipher.xor_bytes(mut out, block, c.block)
assert n == block.len

// make sure to take only remaining bytes
out = unsafe { out[0..src.len % chacha20.block_size] }

// append last output to the ciphertext buffer
ciphertext << out
_ := cipher.xor_bytes(mut dst[nr_blocks * chacha20.block_size..], block, c.block)
}
// copy ciphertext message results to the dst buffer
n := copy(mut dst, ciphertext)
assert n == src.len
}

// free the resources taken by the Cipher `c`. Dont use cipher after .free call
Expand All @@ -147,16 +134,11 @@ pub fn (mut c Cipher) free() {
// reset quickly sets all Cipher's fields to default value
@[unsafe]
pub fn (mut c Cipher) reset() {
for i, _ in c.key {
c.key[i] = u32(0)
}
for j, _ in c.nonce {
c.nonce[j] = u32(0)
}
unsafe {
_ := vmemset(&c.key, 0, 32)
_ := vmemset(&c.nonce, 0, 12)
c.block.reset()
}

c.counter = u32(0)
c.overflow = false
c.precomp = false
Expand Down Expand Up @@ -189,13 +171,13 @@ pub fn (mut c Cipher) set_counter(ctr u32) {
}

// rekey resets internal Cipher's state and reinitializes state with the provided key and nonce
@[unsafe]
pub fn (mut c Cipher) rekey(key []u8, nonce []u8) ! {
unsafe { c.reset() }
c.do_rekey(key, nonce)!
}

// do_rekey reinitializes ChaCha20 instance with the provided key and nonce.
@[direct_array_access]
fn (mut c Cipher) do_rekey(key []u8, nonce []u8) ! {
// check for correctness of key and nonce length
if key.len != chacha20.key_size {
Expand Down Expand Up @@ -242,6 +224,7 @@ fn (mut c Cipher) do_rekey(key []u8, nonce []u8) ! {
// chacha20_block transforms a ChaCha20 state by running
// multiple quarter rounds.
// see https://datatracker.ietf.org/doc/html/rfc8439#section-2.3
@[direct_array_access]
fn (mut c Cipher) chacha20_block() {
// initializes ChaCha20 state
// 0:cccccccc 1:cccccccc 2:cccccccc 3:cccccccc
Expand Down Expand Up @@ -334,6 +317,7 @@ fn (mut c Cipher) chacha20_block() {
}

// generic_key_stream creates generic ChaCha20 keystream block and stores the result in Cipher.block
@[direct_array_access]
fn (mut c Cipher) generic_key_stream() {
// creates ChaCha20 block stream
c.chacha20_block()
Expand Down
19 changes: 19 additions & 0 deletions vlib/x/crypto/chacha20/chacha_test.v
@@ -1,7 +1,26 @@
module chacha20

import rand
import encoding.hex

fn test_chacha20_cipher_reset() ! {
mut key := []u8{len: 32}
mut nonce := []u8{len: 12}
rand.read(mut key)
rand.read(mut nonce)

mut c := new_cipher(key, nonce)!
unsafe { c.reset() }

for i, _ in c.key {
assert c.key[i] == u32(0)
}

for i, _ in c.nonce {
assert c.nonce[i] == u32(0)
}
}

struct BlockCase {
key string
nonce string
Expand Down
1 change: 1 addition & 0 deletions vlib/x/crypto/chacha20/xchacha.v
Expand Up @@ -12,6 +12,7 @@ const h_nonce_size = 16
// xchacha20 are intermediary step to build xchacha20 and initialized the same way as the ChaCha20 cipher,
// except xchacha20 use a 128-bit (16 byte) nonce and has no counter to derive subkey
// see https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-xchacha-03#section-2.2
@[direct_array_access]
fn xchacha20(key []u8, nonce []u8) ![]u8 {
// early bound check
if key.len != key_size {
Expand Down

0 comments on commit 0713e39

Please sign in to comment.