The protocol consist 2 large primes;
Since
This means that
is a subset of
Bob and Alice pick
(Each one of them got one element of
Then, they compute the shared secret:
and they both agree on another element from the group
We factorize
Each divisor
Eve, as the attacker, sends Bob
Bob in his turn computes:
where
As we noted,
We find the correct value using brute-force search (that's why we picked small
Once
We build the following Oracle to represent Bob's side of Diffie-Hellman key agreement:
class BobOracle:
"""
Alice computes u=g^a, and sends u to Bob.
Bob computes v=g^b and sends v to Alice.
The secret shared by Alice and Bob is w = g^(a*b).
"""
# class consts
shared_secret_len = math.ceil(math.log2(p) / 8)
def __init__(self):
# initiate random private key
self._b = random.randint(1, q-1)
@classmethod
def generate_shared_secret(cls, u: int, b: int) -> bytes:
shared_secret = pow(u, b, p)
shared_secret = shared_secret.to_bytes(cls.shared_secret_len, byteorder='big')
return shared_secret
def get_response(self, u: int) -> tuple[bytes, bytes]:
shared_secret = self.generate_shared_secret(u, self._b)
m = b"crazy flamboyant for the rap enjoyment"
digest = hmac.digest(key=shared_secret, msg=m, digest='sha256')
return m, digest
For the factorization part, we use the simple Trial division Algorithm:
def trial_division(n: int) -> Iterator[int]:
"""
Basic Integer Factorization Algorithm.
https://en.wikipedia.org/wiki/Trial_division
"""
while n % 2 == 0:
yield 2
n //= 2
f = 3
while f * f <= n:
if n % f == 0:
yield f
n //= f
else:
f += 2
if n != 1:
yield n
Using Bob's response, we use brute-force searching to find the value of a (mod r)
def brute_force_search_private_key(response, h, r) -> int:
for i in range(r):
shared_secret = BobOracle.generate_shared_secret(h, i)
digest = hmac.digest(key=shared_secret, msg=response[0], digest='sha256')
if digest == response[1]:
return i
raise Exception('should not reach here')
The CRT (Chinese Remainder Theorem) algorithm to retrieve Bob's secret key:
def chinese_remainder(n_list: list[int], a_list: list[int]) -> int:
"""
Solution of the system:
x = a1 (mod n1)
x = a2 (mod n2)
...
x = ak (mod k)
Such that 0 <= x < N,
where N = n1 * n2 * ... * nk
https://en.wikipedia.org/wiki/Chinese_remainder_theorem#Existence_(direct_construction)
"""
x = 0
N = reduce(lambda a, b: a * b, n_list)
for ni, ai in zip(n_list, a_list):
Ni = N // ni
_, (Mi, _) = extended_gcd(Ni, ni)
x += ai*Mi*Ni
return x % N
def extended_gcd(a: int, b: int) -> tuple[int, tuple[int, int]]:
"""
Extended Euclidean algorithm
:return: ( 'gcd' - the resulting gcd,
'coeffs' - Bézout coefficients )
"""
old_r, r = a, b
old_s, s = 1, 0
old_t, t = 0, 1
while r != 0:
quotient = old_r // r
old_r, r = r, old_r - quotient * r
old_s, s = s, old_s - quotient * s
old_t, t = t, old_t - quotient * t
return old_r, (old_s, old_t)
Finally, the whole attack:
def attack(oracle: BobOracle) -> int:
# find xi = a (mod ri), i=1,...,n
# until r1*...*rn > q
r_list = []
x_list = []
total_size = 1
factor_gen = trial_division(j)
while total_size <= q:
# find next factor of j
r = next(factor_gen)
if r in r_list:
continue
# update max number counting
total_size *= r
# find element h of order r
h = find_element_of_order(r, p)
# acquire Bob response
response = oracle.get_response(h)
# search for the correct private key (mod r)
a_r = brute_force_search_private_key(response, h, r)
# store results
r_list.append(r)
x_list.append(a_r)
# use CRT to reconstruct a (Bob private key)
a = chinese_remainder(r_list, x_list)
return a
And we can verify it finds the correct key:
bob = BobOracle()
bob_secret_key = attack(bob)
print(f'{bob_secret_key=}') # bob_secret_key=179569166138842856879519552572563281908
print(f'{bob._b=}') # bob._b=179569166138842856879519552572563281908
assert bob_secret_key == bob._b