1
1
import os
2
2
from ..utilities .utils import xor_bytes
3
3
from Crypto .Cipher import AES
4
+ from typing import Optional
4
5
5
6
6
7
class AES256_CTR_DRBG :
7
- def __init__ (self , seed = None , personalization = b"" ):
8
+ def __init__ (
9
+ self , seed : Optional [bytes ] = None , personalization : bytes = b""
10
+ ):
11
+ """
12
+ DRBG implementation based on AES-256 CTR following the document NIST SP
13
+ 800-90A Section 10.2.1
14
+
15
+ https://csrc.nist.gov/pubs/sp/800/90/a/r1/final
16
+
17
+ Used for deterministic randomness, particularly used for comparing the
18
+ output of Kyber/ML-KEM against known answer tests.
19
+
20
+ :param bytes seed: 48 byte seed, if none is supplied a seed is generated
21
+ using ``os.urandom(48)``.
22
+ :param bytes personalization: optional bytes, of length at most 48 used
23
+ during instantiation of the DRBG
24
+ """
8
25
self .seed_length = 48
9
26
self .reseed_interval = 2 ** 48
10
27
self .key = bytes ([0 ]) * 32
11
28
self .V = bytes ([0 ]) * 16
12
29
self .entropy_input = self .__check_entropy_input (seed )
13
30
14
31
seed_material = self .__instantiate (personalization = personalization )
15
- self .ctr_drbg_update (seed_material )
32
+ self .__ctr_drbg_update (seed_material )
16
33
self .reseed_ctr = 1
17
34
18
- def __check_entropy_input (self , entropy_input ) :
35
+ def __check_entropy_input (self , entropy_input : bytes ) -> bytes :
19
36
"""
20
37
If no entropy given, us os.urandom, else
21
38
check that the input is of the right length.
@@ -29,32 +46,44 @@ def __check_entropy_input(self, entropy_input):
29
46
)
30
47
return entropy_input
31
48
32
- def __instantiate (self , personalization = b"" ):
49
+ def __instantiate (self , personalization : bytes = b"" ) -> bytes :
33
50
"""
34
51
Combine the input seed and optional personalisation
35
52
string into the seed material for the DRBG
53
+
54
+ Section 10.2.1.3.1, Page 52 (CTR_DRBG_Instantiate_algorithm)
36
55
"""
37
56
if len (personalization ) > self .seed_length :
38
57
raise ValueError (
39
58
f"The Personalization String must be at most length: "
40
59
f"{ self .seed_length } . Input has length { len (personalization )} "
41
60
)
42
- elif len ( personalization ) < self . seed_length :
43
- personalization += bytes ([0 ]) * (
44
- self .seed_length - len (personalization )
45
- )
61
+ # Ensure personalization has exactly seed_length bytes
62
+ personalization += bytes ([0 ]) * (
63
+ self .seed_length - len (personalization )
64
+ )
46
65
# debugging
47
66
assert len (personalization ) == self .seed_length
48
67
return xor_bytes (self .entropy_input , personalization )
49
68
50
- def __increment_counter (self ):
69
+ def __increment_counter (self ) -> None :
70
+ """
71
+ Increment the internal counter of the DRBG
72
+ """
51
73
int_V = int .from_bytes (self .V , "big" )
52
- new_V = (int_V + 1 ) % 2 ** ( 8 * 16 )
74
+ new_V = (int_V + 1 ) % 2 ** 128
53
75
self .V = new_V .to_bytes (16 , byteorder = "big" )
54
76
55
- def ctr_drbg_update (self , provided_data ):
77
+ def __ctr_drbg_update (self , provided_data : bytes ) -> None :
78
+ """
79
+ Updates the internal state of the CTR_DRBG using the
80
+ provided_data
81
+
82
+ Section 10.2.1.2, Page 51 (CTR_DRBG_Update)
83
+ """
56
84
tmp = b""
57
85
cipher = AES .new (self .key , AES .MODE_ECB )
86
+
58
87
# Collect bytes from AES ECB
59
88
while len (tmp ) != self .seed_length :
60
89
self .__increment_counter ()
@@ -68,7 +97,19 @@ def ctr_drbg_update(self, provided_data):
68
97
self .key = tmp [:32 ]
69
98
self .V = tmp [32 :]
70
99
71
- def random_bytes (self , num_bytes , additional = None ):
100
+ def random_bytes (
101
+ self , num_bytes : int , additional : Optional [bytes ] = None
102
+ ) -> bytes :
103
+ """
104
+ Generate pseudorandom bytes without a generating function
105
+
106
+ Section 10.2.1.5.1, Page 56 (CTR_DRBG_Generate_algorithm)
107
+
108
+ :param int num_bytes: the number of random bytes requested
109
+ :param bytes additional: optional bytes to be mixed into the generation
110
+ :return: pseudorandom bytes extracted from the DRBG of length ``num_bytes``.
111
+ :rtype: bytes
112
+ """
72
113
# We don't cover this in coverage as we would need to run the counter 2^48 times
73
114
if self .reseed_ctr >= self .reseed_interval : # pragma: no cover
74
115
raise Warning ("The DRBG has been exhausted! Reseed!" )
@@ -82,9 +123,8 @@ def random_bytes(self, num_bytes, additional=None):
82
123
f"The additional input must be of length at most: "
83
124
f"{ self .seed_length } . Input has length { len (additional )} "
84
125
)
85
- elif len (additional ) < self .seed_length :
86
- additional += bytes ([0 ]) * (self .seed_length - len (additional ))
87
- self .ctr_drbg_update (additional )
126
+ additional += bytes ([0 ]) * (self .seed_length - len (additional ))
127
+ self .__ctr_drbg_update (additional )
88
128
89
129
# Collect bytes!
90
130
tmp = b""
@@ -95,6 +135,6 @@ def random_bytes(self, num_bytes, additional=None):
95
135
96
136
# Collect only the requested number of bits
97
137
output_bytes = tmp [:num_bytes ]
98
- self .ctr_drbg_update (additional )
138
+ self .__ctr_drbg_update (additional )
99
139
self .reseed_ctr += 1
100
140
return output_bytes
0 commit comments