|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# Copyright (c) 2022 The Bitcoin Core developers |
| 3 | +# Distributed under the MIT software license, see the accompanying |
| 4 | +# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
| 5 | + |
| 6 | +"""Test-only implementation of ChaCha20 cipher |
| 7 | +
|
| 8 | +It is designed for ease of understanding, not performance. |
| 9 | +
|
| 10 | +WARNING: This code is slow and trivially vulnerable to side channel attacks. Do not use for |
| 11 | +anything but tests. |
| 12 | +""" |
| 13 | + |
| 14 | +import unittest |
| 15 | + |
| 16 | +CHACHA20_INDICES = ( |
| 17 | + (0, 4, 8, 12), (1, 5, 9, 13), (2, 6, 10, 14), (3, 7, 11, 15), |
| 18 | + (0, 5, 10, 15), (1, 6, 11, 12), (2, 7, 8, 13), (3, 4, 9, 14) |
| 19 | +) |
| 20 | + |
| 21 | +CHACHA20_CONSTANTS = (0x61707865, 0x3320646e, 0x79622d32, 0x6b206574) |
| 22 | + |
| 23 | + |
| 24 | +def rotl32(v, bits): |
| 25 | + """Rotate the 32-bit value v left by bits bits.""" |
| 26 | + bits %= 32 # Make sure the term below does not throw an exception |
| 27 | + return ((v << bits) & 0xffffffff) | (v >> (32 - bits)) |
| 28 | + |
| 29 | + |
| 30 | +def chacha20_doubleround(s): |
| 31 | + """Apply a ChaCha20 double round to 16-element state array s. |
| 32 | + See https://cr.yp.to/chacha/chacha-20080128.pdf and https://tools.ietf.org/html/rfc8439 |
| 33 | + """ |
| 34 | + for a, b, c, d in CHACHA20_INDICES: |
| 35 | + s[a] = (s[a] + s[b]) & 0xffffffff |
| 36 | + s[d] = rotl32(s[d] ^ s[a], 16) |
| 37 | + s[c] = (s[c] + s[d]) & 0xffffffff |
| 38 | + s[b] = rotl32(s[b] ^ s[c], 12) |
| 39 | + s[a] = (s[a] + s[b]) & 0xffffffff |
| 40 | + s[d] = rotl32(s[d] ^ s[a], 8) |
| 41 | + s[c] = (s[c] + s[d]) & 0xffffffff |
| 42 | + s[b] = rotl32(s[b] ^ s[c], 7) |
| 43 | + |
| 44 | + |
| 45 | +def chacha20_block(key, nonce, cnt): |
| 46 | + """Compute the 64-byte output of the ChaCha20 block function. |
| 47 | + Takes as input a 32-byte key, 12-byte nonce, and 32-bit integer counter. |
| 48 | + """ |
| 49 | + # Initial state. |
| 50 | + init = [0] * 16 |
| 51 | + init[:4] = CHACHA20_CONSTANTS[:4] |
| 52 | + init[4:12] = [int.from_bytes(key[i:i+4], 'little') for i in range(0, 32, 4)] |
| 53 | + init[12] = cnt |
| 54 | + init[13:16] = [int.from_bytes(nonce[i:i+4], 'little') for i in range(0, 12, 4)] |
| 55 | + # Perform 20 rounds. |
| 56 | + state = list(init) |
| 57 | + for _ in range(10): |
| 58 | + chacha20_doubleround(state) |
| 59 | + # Add initial values back into state. |
| 60 | + for i in range(16): |
| 61 | + state[i] = (state[i] + init[i]) & 0xffffffff |
| 62 | + # Produce byte output |
| 63 | + return b''.join(state[i].to_bytes(4, 'little') for i in range(16)) |
| 64 | + |
| 65 | + |
| 66 | +# Test vectors from RFC7539/8439 consisting of 32 byte key, 12 byte nonce, block counter |
| 67 | +# and 64 byte output after applying `chacha20_block` function |
| 68 | +CHACHA20_TESTS = [ |
| 69 | + ["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", [0x09000000, 0x4a000000], 1, |
| 70 | + "10f1e7e4d13b5915500fdd1fa32071c4c7d1f4c733c068030422aa9ac3d46c4e" |
| 71 | + "d2826446079faa0914c2d705d98b02a2b5129cd1de164eb9cbd083e8a2503c4e"], |
| 72 | + ["0000000000000000000000000000000000000000000000000000000000000000", [0, 0], 0, |
| 73 | + "76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7" |
| 74 | + "da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586"], |
| 75 | + ["0000000000000000000000000000000000000000000000000000000000000000", [0, 0], 1, |
| 76 | + "9f07e7be5551387a98ba977c732d080dcb0f29a048e3656912c6533e32ee7aed" |
| 77 | + "29b721769ce64e43d57133b074d839d531ed1f28510afb45ace10a1f4b794d6f"], |
| 78 | + ["0000000000000000000000000000000000000000000000000000000000000001", [0, 0], 1, |
| 79 | + "3aeb5224ecf849929b9d828db1ced4dd832025e8018b8160b82284f3c949aa5a" |
| 80 | + "8eca00bbb4a73bdad192b5c42f73f2fd4e273644c8b36125a64addeb006c13a0"], |
| 81 | + ["00ff000000000000000000000000000000000000000000000000000000000000", [0, 0], 2, |
| 82 | + "72d54dfbf12ec44b362692df94137f328fea8da73990265ec1bbbea1ae9af0ca" |
| 83 | + "13b25aa26cb4a648cb9b9d1be65b2c0924a66c54d545ec1b7374f4872e99f096"], |
| 84 | + ["0000000000000000000000000000000000000000000000000000000000000000", [0, 0x200000000000000], 0, |
| 85 | + "c2c64d378cd536374ae204b9ef933fcd1a8b2288b3dfa49672ab765b54ee27c7" |
| 86 | + "8a970e0e955c14f3a88e741b97c286f75f8fc299e8148362fa198a39531bed6d"], |
| 87 | + ["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", [0, 0x4a000000], 1, |
| 88 | + "224f51f3401bd9e12fde276fb8631ded8c131f823d2c06e27e4fcaec9ef3cf78" |
| 89 | + "8a3b0aa372600a92b57974cded2b9334794cba40c63e34cdea212c4cf07d41b7"], |
| 90 | + ["0000000000000000000000000000000000000000000000000000000000000001", [0, 0], 0, |
| 91 | + "4540f05a9f1fb296d7736e7b208e3c96eb4fe1834688d2604f450952ed432d41" |
| 92 | + "bbe2a0b6ea7566d2a5d1e7e20d42af2c53d792b1c43fea817e9ad275ae546963"], |
| 93 | + ["0000000000000000000000000000000000000000000000000000000000000000", [0, 1], 0, |
| 94 | + "ef3fdfd6c61578fbf5cf35bd3dd33b8009631634d21e42ac33960bd138e50d32" |
| 95 | + "111e4caf237ee53ca8ad6426194a88545ddc497a0b466e7d6bbdb0041b2f586b"], |
| 96 | + ["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f", [0, 0x0706050403020100], 0, |
| 97 | + "f798a189f195e66982105ffb640bb7757f579da31602fc93ec01ac56f85ac3c1" |
| 98 | + "34a4547b733b46413042c9440049176905d3be59ea1c53f15916155c2be8241a"], |
| 99 | +] |
| 100 | + |
| 101 | + |
| 102 | +class TestFrameworkChacha(unittest.TestCase): |
| 103 | + def test_chacha20(self): |
| 104 | + """ChaCha20 test vectors.""" |
| 105 | + for test_vector in CHACHA20_TESTS: |
| 106 | + hex_key, nonce, counter, hex_output = test_vector |
| 107 | + key = bytes.fromhex(hex_key) |
| 108 | + nonce_bytes = nonce[0].to_bytes(4, 'little') + nonce[1].to_bytes(8, 'little') |
| 109 | + keystream = chacha20_block(key, nonce_bytes, counter) |
| 110 | + self.assertEqual(hex_output, keystream.hex()) |
0 commit comments