231 lines
7.2 KiB
V
231 lines
7.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.
|
|
//
|
|
// Chacha20 symmetric key stream cipher encryption based on RFC 8439
|
|
module chacha20
|
|
|
|
import crypto.internal.subtle
|
|
|
|
// The size of ChaCha20 key, ie 256 bits size, in bytes
|
|
pub const key_size = 32
|
|
// The size of standard IETF ChaCha20 nonce, ie 96 bits size, in bytes
|
|
pub const nonce_size = 12
|
|
// The size of extended variant of standard ChaCha20 (XChaCha20) nonce, 192 bits
|
|
pub const x_nonce_size = 24
|
|
// The size of original ChaCha20 nonce, 64 bits
|
|
pub const orig_nonce_size = 8
|
|
// internal block size ChaCha20 operates on, in bytes
|
|
const block_size = 64
|
|
|
|
// four constants of ChaCha20 state.
|
|
const cc0 = u32(0x61707865) // expa
|
|
const cc1 = u32(0x3320646e) // nd 3
|
|
const cc2 = u32(0x79622d32) // 2-by
|
|
const cc3 = u32(0x6b206574) // te k
|
|
|
|
// CipherMode was enumeration of ChaCha20 supported variant.
|
|
enum CipherMode {
|
|
// The standard IETF ChaCha20 (and XChaCha20), with 32-bit internal counter.
|
|
standard
|
|
// The original ChaCha20 with 64-bit internal counter.
|
|
original
|
|
}
|
|
|
|
// Configuration options
|
|
@[params]
|
|
pub struct Options {
|
|
pub mut:
|
|
// currently, used for XChaCha20 construct
|
|
use_64bit_counter bool
|
|
}
|
|
|
|
// encrypt encrypts plaintext bytes with ChaCha20 cipher instance with provided key and nonce.
|
|
// It was a thin wrapper around two supported nonce size, ChaCha20 with 96 bits
|
|
// and XChaCha20 with 192 bits nonce. Internally, encrypt start with 0's counter value.
|
|
// If you want more control, use Cipher instance and setup the counter by your self.
|
|
pub fn encrypt(key []u8, nonce []u8, plaintext []u8, opt Options) ![]u8 {
|
|
mut stream := new_stream_with_options(key, nonce, opt)!
|
|
mut dst := []u8{len: plaintext.len}
|
|
stream.keystream_full(mut dst, plaintext)!
|
|
unsafe { stream.reset() }
|
|
return dst
|
|
}
|
|
|
|
// decrypt does reverse of encrypt operation by decrypting ciphertext with ChaCha20 cipher
|
|
// instance with provided key and nonce.
|
|
pub fn decrypt(key []u8, nonce []u8, ciphertext []u8, opt Options) ![]u8 {
|
|
mut stream := new_stream_with_options(key, nonce)!
|
|
mut dst := []u8{len: ciphertext.len}
|
|
stream.keystream_full(mut dst, ciphertext)!
|
|
unsafe { stream.reset() }
|
|
return dst
|
|
}
|
|
|
|
// Cipher represents ChaCha20 stream cipher instances.
|
|
@[noinit]
|
|
pub struct Cipher {
|
|
Stream
|
|
mut:
|
|
// internal buffer for storing key stream results
|
|
block []u8 = []u8{len: block_size}
|
|
// The last length of leftover unprocessed keystream from internal buffer
|
|
length int
|
|
}
|
|
|
|
// new_cipher creates a new ChaCha20 stream cipher with the given 32 bytes key
|
|
// and bytes of nonce with supported size, ie, 8, 12 or 24 bytes nonce.
|
|
// Standard IETF variant use 12 bytes nonce's, if you want create original ChaCha20 cipher
|
|
// with support for 64-bit counter, use 8 bytes length nonce's instead
|
|
// If 24 bytes of nonce was provided, the XChaCha20 construction will be used.
|
|
// It returns new ChaCha20 cipher instance or an error if key or nonce have any other length.
|
|
pub fn new_cipher(key []u8, nonce []u8, opt Options) !&Cipher {
|
|
stream := new_stream_with_options(key, nonce, opt)!
|
|
return &Cipher{
|
|
Stream: stream
|
|
}
|
|
}
|
|
|
|
// xor_key_stream xors each byte in the given slice in the src with a byte from the
|
|
// cipher's key stream. It fulfills `cipher.Stream` interface. It encrypts the plaintext message
|
|
// in src and stores the ciphertext result in dst in a key stream fashion.
|
|
// 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
|
|
}
|
|
if dst.len < src.len {
|
|
panic('chacha20/chacha: dst buffer is to small')
|
|
}
|
|
dst = unsafe { dst[..src.len] }
|
|
if subtle.inexact_overlap(dst, src) {
|
|
panic('chacha20: invalid buffer overlap')
|
|
}
|
|
// index of position within src bytes
|
|
mut idx := 0
|
|
|
|
// First, try to drain any remaining key stream from internal buffer
|
|
if c.length != 0 {
|
|
// remaining keystream on internal buffer
|
|
mut kstream := c.block[block_size - c.length..]
|
|
if src.len < kstream.len {
|
|
kstream = unsafe { kstream[..src.len] }
|
|
}
|
|
// xors every bytes in src with bytes from key stream and stored into dst
|
|
for i, b in kstream {
|
|
dst[idx + i] = src[idx + i] ^ b
|
|
}
|
|
// updates position and internal buffer length.
|
|
// when c.length reaches the block_size, we reset it for future use.
|
|
c.length -= kstream.len
|
|
idx += kstream.len
|
|
if c.length == block_size {
|
|
unsafe { c.block.reset() }
|
|
c.length = 0
|
|
}
|
|
}
|
|
// process for remaining unprocessed src bytes
|
|
mut remains := unsafe { src[idx..] }
|
|
nr_blocks := remains.len / block_size
|
|
|
|
// process for full block_size-d message
|
|
for i := 0; i < nr_blocks; i++ {
|
|
// for every block_sized message, we generates 64-bytes block key stream
|
|
// and then xor-ing this block with generated key stream
|
|
block := unsafe { remains[i * block_size..(i + 1) * block_size] }
|
|
ks := c.keystream() or { panic(err) }
|
|
for j, b in ks {
|
|
dst[idx + j] = block[j] ^ b
|
|
}
|
|
// updates position
|
|
idx += block_size
|
|
}
|
|
|
|
// process for remaining partial block
|
|
if remains.len % block_size != 0 {
|
|
last_block := unsafe { remains[nr_blocks * block_size..] }
|
|
// generates one 64-bytes keystream block
|
|
c.block = c.keystream() or { panic(err) }
|
|
for i, b in last_block {
|
|
dst[idx + i] = b ^ c.block[i]
|
|
}
|
|
c.length = block_size - last_block.len
|
|
idx += last_block.len
|
|
}
|
|
}
|
|
|
|
// encrypt encrypts src and stores into dst buffer. It works like `xor_key_stream` except
|
|
// its ignore key streaming process by ignoring remaining key stream in the internal buffer,
|
|
// so, its works in one shot of fashion.
|
|
// Its added to allow `chacha20poly1305` modules to work without key stream fashion.
|
|
// TODO: integrates it with the rest
|
|
@[direct_array_access]
|
|
pub fn (mut c Cipher) encrypt(mut dst []u8, src []u8) ! {
|
|
if src.len == 0 {
|
|
return
|
|
}
|
|
if dst.len < src.len {
|
|
return error('chacha20: dst buffer is to small')
|
|
}
|
|
if subtle.inexact_overlap(dst, src) {
|
|
return error('chacha20: invalid buffer overlap')
|
|
}
|
|
|
|
c.Stream.keystream_full(mut dst, src)!
|
|
}
|
|
|
|
// free the resources taken by the Cipher `c`. Dont use cipher after .free call
|
|
@[unsafe]
|
|
pub fn (mut c Cipher) free() {
|
|
$if prealloc {
|
|
return
|
|
}
|
|
unsafe {
|
|
c.block.free()
|
|
}
|
|
}
|
|
|
|
// reset quickly sets all Cipher's fields to default value.
|
|
// This method will be deprecated.
|
|
@[deprecated: 'do not use .reset() at all, create a new Cipher instead']
|
|
@[deprecated_after: '2025-11-30']
|
|
@[unsafe]
|
|
pub fn (mut c Cipher) reset() {
|
|
c.Stream.reset()
|
|
unsafe {
|
|
c.block.reset()
|
|
}
|
|
c.length = 0
|
|
}
|
|
|
|
// set_counter sets Cipher's counter
|
|
pub fn (mut c Cipher) set_counter(ctr u64) {
|
|
c.Stream.set_ctr(ctr)
|
|
}
|
|
|
|
// counter returns a current underlying counter value, as u64.
|
|
pub fn (c Cipher) counter() u64 {
|
|
return c.Stream.ctr()
|
|
}
|
|
|
|
// rekey resets internal Cipher's state and reinitializes state with the provided key and nonce
|
|
pub fn (mut c Cipher) rekey(key []u8, nonce []u8) ! {
|
|
// start of .reset() inline
|
|
unsafe {
|
|
c.Stream.reset()
|
|
c.block.reset()
|
|
}
|
|
c.length = 0
|
|
// end of .reset() inline
|
|
|
|
// we use c.Stream.mode info to get 64-bit counter capability
|
|
w64 := if c.mode == .original { true } else { false }
|
|
opt := Options{
|
|
use_64bit_counter: w64
|
|
}
|
|
stream := new_stream_with_options(key, nonce, opt)!
|
|
c.Stream = stream
|
|
}
|