Add ChaCha20 stream cipher
This commit is contained in:
parent
597dcb39aa
commit
c2eb42b769
|
@ -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
|
||||
|
||||
# Ciphers
|
||||
# ----------------------------------------------------------
|
||||
("tests/t_cipher_chacha20.nim", false),
|
||||
|
||||
# Protocols
|
||||
# ----------------------------------------------------------
|
||||
("tests/t_ethereum_evm_precompiles.nim", false),
|
||||
|
|
|
@ -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
|
|
@ -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)
|
Loading…
Reference in New Issue