3
3
# Distributed under the MIT software license, see the accompanying
4
4
# file COPYING or http://www.opensource.org/licenses/mit-license.php.
5
5
6
- """Test-only implementation of ChaCha20 cipher
6
+ """Test-only implementation of ChaCha20 cipher and FSChaCha20 for BIP 324
7
7
8
8
It is designed for ease of understanding, not performance.
9
9
19
19
)
20
20
21
21
CHACHA20_CONSTANTS = (0x61707865 , 0x3320646e , 0x79622d32 , 0x6b206574 )
22
+ REKEY_INTERVAL = 224 # packets
22
23
23
24
24
25
def rotl32 (v , bits ):
@@ -62,6 +63,34 @@ def chacha20_block(key, nonce, cnt):
62
63
# Produce byte output
63
64
return b'' .join (state [i ].to_bytes (4 , 'little' ) for i in range (16 ))
64
65
66
+ class FSChaCha20 :
67
+ """Rekeying wrapper stream cipher around ChaCha20."""
68
+ def __init__ (self , initial_key , rekey_interval = REKEY_INTERVAL ):
69
+ self ._key = initial_key
70
+ self ._rekey_interval = rekey_interval
71
+ self ._block_counter = 0
72
+ self ._chunk_counter = 0
73
+ self ._keystream = b''
74
+
75
+ def _get_keystream_bytes (self , nbytes ):
76
+ while len (self ._keystream ) < nbytes :
77
+ nonce = ((0 ).to_bytes (4 , 'little' ) + (self ._chunk_counter // self ._rekey_interval ).to_bytes (8 , 'little' ))
78
+ self ._keystream += chacha20_block (self ._key , nonce , self ._block_counter )
79
+ self ._block_counter += 1
80
+ ret = self ._keystream [:nbytes ]
81
+ self ._keystream = self ._keystream [nbytes :]
82
+ return ret
83
+
84
+ def crypt (self , chunk ):
85
+ ks = self ._get_keystream_bytes (len (chunk ))
86
+ ret = bytes ([ks [i ] ^ chunk [i ] for i in range (len (chunk ))])
87
+ if ((self ._chunk_counter + 1 ) % self ._rekey_interval ) == 0 :
88
+ self ._key = self ._get_keystream_bytes (32 )
89
+ self ._block_counter = 0
90
+ self ._keystream = b''
91
+ self ._chunk_counter += 1
92
+ return ret
93
+
65
94
66
95
# Test vectors from RFC7539/8439 consisting of 32 byte key, 12 byte nonce, block counter
67
96
# and 64 byte output after applying `chacha20_block` function
@@ -98,6 +127,16 @@ def chacha20_block(key, nonce, cnt):
98
127
"34a4547b733b46413042c9440049176905d3be59ea1c53f15916155c2be8241a" ],
99
128
]
100
129
130
+ FSCHACHA20_TESTS = [
131
+ ["000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" ,
132
+ "0000000000000000000000000000000000000000000000000000000000000000" , 256 ,
133
+ "a93df4ef03011f3db95f60d996e1785df5de38fc39bfcb663a47bb5561928349" ],
134
+ ["01" , "000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f" , 5 , "ea" ],
135
+ ["e93fdb5c762804b9a706816aca31e35b11d2aa3080108ef46a5b1f1508819c0a" ,
136
+ "8ec4c3ccdaea336bdeb245636970be01266509b33f3d2642504eaf412206207a" , 4096 ,
137
+ "8bfaa4eacff308fdb4a94a5ff25bd9d0c1f84b77f81239f67ff39d6e1ac280c9" ],
138
+ ]
139
+
101
140
102
141
class TestFrameworkChacha (unittest .TestCase ):
103
142
def test_chacha20 (self ):
@@ -108,3 +147,16 @@ def test_chacha20(self):
108
147
nonce_bytes = nonce [0 ].to_bytes (4 , 'little' ) + nonce [1 ].to_bytes (8 , 'little' )
109
148
keystream = chacha20_block (key , nonce_bytes , counter )
110
149
self .assertEqual (hex_output , keystream .hex ())
150
+
151
+ def test_fschacha20 (self ):
152
+ """FSChaCha20 test vectors."""
153
+ for test_vector in FSCHACHA20_TESTS :
154
+ hex_plaintext , hex_key , rekey_interval , hex_ciphertext_after_rotation = test_vector
155
+ plaintext = bytes .fromhex (hex_plaintext )
156
+ key = bytes .fromhex (hex_key )
157
+ fsc20 = FSChaCha20 (key , rekey_interval )
158
+ for _ in range (rekey_interval ):
159
+ fsc20 .crypt (plaintext )
160
+
161
+ ciphertext = fsc20 .crypt (plaintext )
162
+ self .assertEqual (hex_ciphertext_after_rotation , ciphertext .hex ())
0 commit comments