From c2eb42b769ce9c15843855fa01f86305798f4a5d Mon Sep 17 00:00:00 2001 From: Mamy Ratsimbazafy Date: Wed, 2 Mar 2022 01:18:47 +0100 Subject: [PATCH] Add ChaCha20 stream cipher --- constantine.nimble | 4 + constantine/ciphers/chacha20.nim | 127 +++++++++++++++++++++++++++++++ tests/t_cipher_chacha20.nim | 43 +++++++++++ 3 files changed, 174 insertions(+) create mode 100644 constantine/ciphers/chacha20.nim create mode 100644 tests/t_cipher_chacha20.nim diff --git a/constantine.nimble b/constantine.nimble index 786b3eb..7216308 100644 --- a/constantine.nimble +++ b/constantine.nimble @@ -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), diff --git a/constantine/ciphers/chacha20.nim b/constantine/ciphers/chacha20.nim new file mode 100644 index 0000000..8065ba7 --- /dev/null +++ b/constantine/ciphers/chacha20.nim @@ -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 \ No newline at end of file diff --git a/tests/t_cipher_chacha20.nim b/tests/t_cipher_chacha20.nim new file mode 100644 index 0000000..cf06588 --- /dev/null +++ b/tests/t_cipher_chacha20.nim @@ -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) \ No newline at end of file