Add ChaCha20 stream cipher

This commit is contained in:
Mamy Ratsimbazafy 2022-03-02 01:18:47 +01:00
parent 597dcb39aa
commit c2eb42b769
No known key found for this signature in database
GPG Key ID: 6227262F49BE273A
3 changed files with 174 additions and 0 deletions

View File

@ -188,6 +188,10 @@ const testDesc: seq[tuple[path: string, useGMP: bool]] = @[
# ---------------------------------------------------------- # ----------------------------------------------------------
("tests/t_hash_sha256_vs_openssl.nim", true), # skip OpenSSL tests on Windows ("tests/t_hash_sha256_vs_openssl.nim", true), # skip OpenSSL tests on Windows
# Ciphers
# ----------------------------------------------------------
("tests/t_cipher_chacha20.nim", false),
# Protocols # Protocols
# ---------------------------------------------------------- # ----------------------------------------------------------
("tests/t_ethereum_evm_precompiles.nim", false), ("tests/t_ethereum_evm_precompiles.nim", false),

View File

@ -0,0 +1,127 @@
# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import ../platforms/endians
# ############################################################
#
# ChaCha20 stream cipher
#
# ############################################################
# Implementation of IETF ChaCha20 stream cipher
# https://datatracker.ietf.org/doc/html/rfc8439
# ---------------------------------------------
{.push raises:[].} # No exceptions for crypto
{.push checks:off.} # We want unchecked int and array accesses
template rotl(x, n: uint32): uint32 =
## Rotate left the bits
# We always use it with constants in 0 ..< 32
# so no undefined behaviour.
(x shl n) or (x shr (32 - n))
template `^=`(x: var uint32, y: uint32) =
x = x xor y
template `<<<=`(x: var uint32, n: uint32) =
x = x.rotl(n)
template quarter_round(a, b, c, d: var uint32) =
a += b; d ^= a; d <<<= 16
c += d; b ^= c; b <<<= 12
a += b; d ^= a; d <<<= 8
c += d; b ^= c; b <<<= 7
template qround(state: var array[16, uint32], x, y, z, w: int) =
quarterRound(state[x], state[y], state[z], state[w])
template inner_block(s: var array[16, uint32]) =
# State
# 0 1 2 3
# 4 5 6 7
# 8 9 10 11
# 12 13 14 15
# Column rounds
state.qround(0, 4, 8, 12)
state.qround(1, 5, 9, 13)
state.qround(2, 6, 10, 14)
state.qround(3, 7, 11, 15)
# Diagonal rounds
state.qround(0, 5, 10, 15)
state.qround(1, 6, 11, 12)
state.qround(2, 7, 8, 13)
state.qround(3, 4, 9, 14)
func chacha20_block(
key_stream: var array[64, byte],
key: array[8, uint32],
block_counter: uint32,
nonce: array[3, uint32]) =
const cccc = [uint32 0x61707865, 0x3320646e, 0x79622d32, 0x6b206574]
var state{.noInit.}: array[16, uint32]
for i in 0 ..< 4:
state[i] = cccc[i]
for i in 4 ..< 12:
state[i] = key[i-4]
state[12] = block_counter
for i in 13 ..< 16:
state[i] = nonce[i-13]
for i in 0 ..< 10:
state.inner_block()
# uint32 are 4 bytes so multiply destination by 4
for i in 0'u ..< 4:
key_stream.dumpRawInt(state[i] + cccc[i], i shl 2, littleEndian)
for i in 4'u ..< 12:
key_stream.dumpRawInt(state[i] + key[i-4], i shl 2, littleEndian)
key_stream.dumpRawInt(state[12] + block_counter, 12 shl 2, littleEndian)
for i in 13'u ..< 16:
key_stream.dumpRawInt(state[i] + nonce[i-13], i shl 2, littleEndian)
func chacha20_cipher*[T: byte|char](
key: array[32, byte],
counter: uint32,
nonce: array[12, byte],
data: var openarray[T]): uint32 =
## Encrypt or decrypt `data` using the ChaCha20 cipher
## - `key` is a 256-bit (32 bytes) secret shared encryption/decryption key.
## - `counter`. A monotonically increasing value per encryption.
## The counter can be initially set to any value.
## - `nonce` (Number-used-once), nonce MUST NOT be reused for the same key.
## If multiple senders are using the same key,
## `nonce` MUST be made unique per sender.
##
## Encryption/decryption is done in-place.
## Returns the new counter
var keyU{.noInit.}: array[8, uint32]
var nonceU{.noInit.}: array[3, uint32]
var pos = 0'u
for i in 0 ..< 8:
keyU[i].parseFromBlob(key, pos, littleEndian)
pos = 0'u
for i in 0 ..< 3:
nonceU[i].parseFromBlob(nonce, pos, littleEndian)
var counter = counter
var eaten = 0
while eaten < data.len:
var key_stream{.noInit.}: array[64, byte]
key_stream.chacha20_block(keyU, counter, nonceU)
# Plaintext length can be leaked, it doesn't reveal the content.
for i in eaten ..< min(eaten+64, data.len):
data[i].byte() ^= key_stream[i-eaten]
eaten += 64
counter += 1
return counter

View File

@ -0,0 +1,43 @@
# Constantine
# Copyright (c) 2018-2019 Status Research & Development GmbH
# Copyright (c) 2020-Present Mamy André-Ratsimbazafy
# Licensed and distributed under either of
# * MIT license (license terms in the root directory or at http://opensource.org/licenses/MIT).
# * Apache v2 license (license terms in the root directory or at http://www.apache.org/licenses/LICENSE-2.0).
# at your option. This file may not be copied, modified, or distributed except according to those terms.
import
std/unittest,
../constantine/ciphers/chacha20
suite "[Cipher] Chacha20":
test "Test vector 1 - RFC8439":
let plaintext = "Ladies and Gentlemen of the class of '99: If I could offer you only one tip for the future, sunscreen would be it."
let ciphertext = [
byte 0x6e, 0x2e, 0x35, 0x9a, 0x25, 0x68, 0xf9, 0x80, 0x41, 0xba, 0x07, 0x28, 0xdd, 0x0d, 0x69, 0x81,
0xe9, 0x7e, 0x7a, 0xec, 0x1d, 0x43, 0x60, 0xc2, 0x0a, 0x27, 0xaf, 0xcc, 0xfd, 0x9f, 0xae, 0x0b,
0xf9, 0x1b, 0x65, 0xc5, 0x52, 0x47, 0x33, 0xab, 0x8f, 0x59, 0x3d, 0xab, 0xcd, 0x62, 0xb3, 0x57,
0x16, 0x39, 0xd6, 0x24, 0xe6, 0x51, 0x52, 0xab, 0x8f, 0x53, 0x0c, 0x35, 0x9f, 0x08, 0x61, 0xd8,
0x07, 0xca, 0x0d, 0xbf, 0x50, 0x0d, 0x6a, 0x61, 0x56, 0xa3, 0x8e, 0x08, 0x8a, 0x22, 0xb6, 0x5e,
0x52, 0xbc, 0x51, 0x4d, 0x16, 0xcc, 0xf8, 0x06, 0x81, 0x8c, 0xe9, 0x1a, 0xb7, 0x79, 0x37, 0x36,
0x5a, 0xf9, 0x0b, 0xbf, 0x74, 0xa3, 0x5b, 0xe6, 0xb4, 0x0b, 0x8e, 0xed, 0xf2, 0x78, 0x5e, 0x42,
0x87, 0x4d
]
let key = [
byte 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f,
0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17,
0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f
]
let nonce = [byte 0, 0, 0, 0, 0, 0, 0, 0x4a, 0, 0, 0, 0]
var data = newSeq[byte](plaintext.len)
copyMem(data[0].addr, plaintext[0].unsafeAddr, plaintext.len)
doAssert cast[seq[byte]](plaintext) != ciphertext
discard chacha20_cipher(key, counter = 1, nonce, data)
doAssert data == ciphertext
discard chacha20_cipher(key, counter = 1, nonce, data)
doAssert data == cast[seq[byte]](plaintext)