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