Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
jvdsn
GitHub Repository: jvdsn/crypto-attacks
Path: blob/master/attacks/lcg/truncated_parameter_recovery.py
2589 views
1
import logging
2
import os
3
import sys
4
from itertools import combinations
5
from math import ceil
6
from math import gcd
7
from math import sqrt
8
9
from sage.all import ZZ
10
from sage.all import Zmod
11
from sage.all import factor
12
from sage.all import matrix
13
14
path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(os.path.abspath(__file__)))))
15
if sys.path[1] != path:
16
sys.path.insert(1, path)
17
18
from attacks.hnp import lattice_attack
19
from shared.lattice import shortest_vectors
20
from shared.polynomial import polynomial_gcd_crt
21
22
23
# Section 2.1 in "On Stern's Attack Against Secret Truncated Linear Congruential Generators".
24
def _generate_polynomials(y, n, t):
25
B = matrix(ZZ, n, n + t)
26
for i in range(n):
27
for j in range(t):
28
B[i, j] = y[i + j + 1] - y[i + j]
29
30
B[i, t + i] = 1
31
32
x = ZZ["x"].gen()
33
for v in shortest_vectors(B):
34
P = 0
35
for i, l in enumerate(v[t:]):
36
P += l * x ** i
37
yield P
38
39
40
# Section 4 in "On Stern's Attack Against Secret Truncated Linear Congruential Generators".
41
def _recover_modulus_and_multiplier(polynomials, m=None, a=None, check_modulus=None):
42
for comb in combinations(polynomials, 3):
43
P0 = comb[0]
44
P1 = comb[1]
45
P2 = comb[2]
46
m_ = gcd(P0.resultant(P1), P1.resultant(P2), P0.resultant(P2))
47
if (m is None and check_modulus(m_)) or m_ == m:
48
if a is None:
49
factors = factor(m_)
50
g = polynomial_gcd_crt(P0, polynomial_gcd_crt(P1, P2, factors), factors)
51
for a_ in g.change_ring(Zmod(m_)).roots(multiplicities=False):
52
yield int(m_), int(a_)
53
else:
54
yield int(m_), a
55
56
57
# Generates possible values for the modulus, multiplier, increment, and seed.
58
# This is similar to the Hidden Number Problem, but with two 'global' unknowns.
59
def _recover_increment_and_seed(y, k, s, m, a):
60
a_ = []
61
b_ = []
62
X = 2 ** (k - s)
63
mult1 = a
64
mult2 = 1
65
for i in range(len(y)):
66
a_.append([mult1, mult2])
67
b_.append(-X * y[i])
68
mult1 = (a * mult1) % m
69
mult2 = (a * mult2 + 1) % m
70
71
for _, (x0_, c_) in lattice_attack.attack(a_, b_, m, X):
72
yield m, a, c_, x0_
73
74
75
def attack(y, k, s, m=None, a=None, check_modulus=None):
76
"""
77
Recovers possible parameters and states from a truncated linear congruential generator.
78
More information: Contini S., Shparlinski I. E., "On Stern's Attack Against Secret Truncated Linear Congruential Generators"
79
If no modulus is provided, attempts to recover a modulus from the outputs.
80
If no multiplier is provided, attempts to recover a multiplier from the outputs.
81
Also recovers an increment from the outputs.
82
The resulting parameters may not match the original parameters, but the generated sequence should be the same up to some small error.
83
:param y: the sequential output values obtained from the truncated LCG (the states truncated to s most significant bits)
84
:param k: the bit length of the states
85
:param s: the bit length of the outputs
86
:param m: the modulus of the LCG (can be None)
87
:param a: the multiplier of the LCG (can be None)
88
:param check_modulus: a function which checks if a possible value can be the modulus (default: compare the bit length with k)
89
:return: a generator generating possible parameters (tuples of modulus, multiplier, increment, and seed) of the truncated LCG
90
"""
91
if m is None or a is None:
92
alpha = s / k
93
t = int(1 / alpha)
94
n = ceil(sqrt(2 * alpha * t * k))
95
96
# We start at the minimum useful chunk size.
97
chunk_size = n + t
98
while chunk_size <= len(y):
99
logging.info(f"Trying chunk size {chunk_size}...")
100
polynomials = []
101
for i in range(len(y) // chunk_size):
102
logging.info(f"Generating polynomials for {n = }, {t = }...")
103
for P in _generate_polynomials(y[chunk_size * i:chunk_size * (i + 1)], n, t):
104
polynomials.append(P)
105
106
logging.info("Recovering modulus and multiplier...")
107
for m_, a_ in _recover_modulus_and_multiplier(polynomials, m, a, check_modulus or (lambda m_: m_.bit_length() == k)):
108
logging.info("Recovering increment and seed...")
109
yield from _recover_increment_and_seed(y, k, s, m_, a_)
110
111
t += 1
112
n = ceil(sqrt(2 * alpha * t * k))
113
chunk_size = n + t
114
else:
115
logging.info("Recovering increment and seed...")
116
yield from _recover_increment_and_seed(y, k, s, m, a)
117
118