4.13 TAKING THE TIME
EXERCISE 4.13: TAKING THE TIME
How long does the attack take? Instrument your code with timing checks and a count of how many times the oracle function is called. Run the attack on a suite of inputs and determine the average amount of time to break keys of size $512, 1024, $ and \(2048\).
The code:
# ex4.13.py
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives.asymmetric.rsa import RSAPrivateKey, RSAPublicKey
from cryptography.hazmat.primitives import hashes,serialization
from cryptography.hazmat.backends import default_backend
import gmpy2
from collections import namedtuple
import timeit
= namedtuple('Interval', ['a', 'b'])
Interval
def simple_rsa_encrypt(m, public_key):
= public_key.public_numbers()
numbers return gmpy2.powmod(m, numbers.e, numbers.n)
def simple_rsa_decrypt(c: int, private_key):
= private_key.private_numbers()
numbers return gmpy2.powmod(c, numbers.d, numbers.public_numbers.n)
def int_to_bytes(i, min_size=None):
# i might be a gmpy2 big integer; convert back to a Python int.
= int(i)
i = i.to_bytes((i.bit_length()+7)//8, byteorder='big')
b if min_size != None and len(b) < min_size:
= b'\x00'*(min_size - len(b)) + b
b return b
def bytes_to_int(b):
return int.from_bytes(b, byteorder='big')
class Oracle:
pass
class FakeOracle(Oracle):
def __init__(self, private_key):
self.private_key = private_key
self.counter = 0
def __call__(self,cipher_text:int)-> bool :
self.counter += 1
= simple_rsa_decrypt(cipher_text, private_key=self.private_key)
recovered_as_int = int_to_bytes(recovered_as_int, min_size=self.private_key.key_size//8)
recovered return recovered[:2] == bytes([0, 2])
class RSAOracleAttacker:
def __init__(self, public_key: RSAPublicKey, oracle: Oracle):
self.public_key = public_key
self.oracle = oracle
def _step1_blinding(self, c):
self.c0 = c
self.B = 2 ** (self.public_key.key_size - 16)
self.s = [1]
self.M = [
2 * self.B, 3 * self.B - 1)],
[Interval(
]
self.i = 1
self.n = self.public_key.public_numbers().n
def _find_s(self, start_s, s_max = None):
= start_s
si = simple_rsa_encrypt(si, self.public_key)
ci
while not self.oracle((self.c0 * ci) % self.n):
+= 1
si
if s_max and (si > s_max):
return None
= simple_rsa_encrypt(si, self.public_key)
ci
return si
def _step2a_start_the_searching(self):
= self._find_s(start_s=gmpy2.c_div(self.n, 3 * self.B))
si return si
def _step2b_searching_with_more_than_one_interval(self):
= self._find_s(start_s=self.s[-1] + 1)
si return si
def _step2c_searching_with_one_interval_left(self):
= self.M[-1][0]
a, b = gmpy2.c_div(2*(b*self.s[-1] - 2 * self.B), self.n)
ri = None
si
while si == None:
= gmpy2.c_div((2*self.B + ri * self.n), b)
si = gmpy2.c_div((3 * self.B + ri * self.n), a)
s_max = self._find_s(start_s = si, s_max = s_max)
si += 1
ri
return si
def _step3_narrowing_set_of_solutions(self, si):
= set()
new_intervals for a, b in self.M[-1]:
= gmpy2.c_div((a*si - 3 * self.B + 1), self.n)
r_min = gmpy2.f_div((b*si - 2 * self.B), self.n)
r_max
for r in range(r_min, r_max + 1):
= gmpy2.c_div((2 * self.B + r * self.n), si)
a_candidate = gmpy2.f_div((3 * self.B-1 + r * self.n), si)
b_candidate
= Interval(max(a, a_candidate), min(b, b_candidate))
new_interval
new_intervals.add(new_interval)
= list(new_intervals)
new_intervals self.M.append(new_intervals)
self.s.append(si)
if len(new_intervals) == 1 and new_intervals[0].a == new_intervals[0].b:
return True
return False
def _step4_computing_the_solution(self):
= self.M[-1][0]
interval return interval.a
def attack(self, c):
self._step1_blinding(c)
# do this until there is one interval left
= False
finished while not finished:
if self.i == 1:
= self._step2a_start_the_searching()
si elif len(self.M[-1]) > 1:
= self._step2b_searching_with_more_than_one_interval()
si elif len(self.M[-1]) == 1:
= self.M[-1][0]
interval = self._step2c_searching_with_one_interval_left()
si
= self._step3_narrowing_set_of_solutions(si)
finished self.i += 1
= self._step4_computing_the_solution()
m return m
if __name__ == '__main__':
= [512, 1024, 2048]
key_sizes_to_try
list[RSAPrivateKey] = [
private_keys:
rsa.generate_private_key(=65537,
public_exponent=key_size,
key_size=default_backend(),
backend
)for key_size in key_sizes_to_try
]print("[+] Three private keys generated successfully.")
list[RSAPublicKey] = [
public_keys:
single_private_key.public_key()for single_private_key in private_keys
]print("[+] Corresponding public keys generated successfully.")
list[FakeOracle] = [
oracles: =single_private_key)
FakeOracle(private_keyfor single_private_key in private_keys
]print("[+] Corresponding Oracles generated successfully.")
= b'Hello Bob? How are you? Max varies. Love Alice'
msg
= [
ciphertexts
public_key.encrypt(= msg,
plaintext = padding.PKCS1v15(),
padding
)for public_key in public_keys
]
list[RSAOracleAttacker] = [
rsa_oracle_attackers: =public_key, oracle=oracle)
RSAOracleAttacker(public_keyfor public_key, oracle in zip(public_keys, oracles)
]print("[+] Corresponding Oracle attackers generated successfully.")
print("Setup code done. Attack phase starting...")
for key_size, attacker, c in zip(key_sizes_to_try, rsa_oracle_attackers, ciphertexts):
print(f"Attacking the ciphertext encrypted with key size: {key_size}")
int = bytes_to_int(c)
c_in_int:
= timeit.timeit(lambda: attacker.attack(c_in_int), number=1)
time_it_took print(f"\t * Time it took: {time_it_took} seconds")
print(f"\t * Number of times oracle is called: {attacker.oracle.counter}")
Note that this code does not answer the question fully. The question asks to give the code a “suite of inputs” and to take the average. I will leave that up to you. 😉