Fork of the official github repository of the framework Leaky-LWE-Estimator, a Sage Toolkit to attack and estimate the hardness of LWE with Side Information. https://github.com/lducas/leaky-LWE-Estimator
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

DBDD.sage 14KB

  1. from fpylll import *
  2. from fpylll.algorithms.bkz2 import BKZReduction
  3. load("../framework/load_strategies.sage")
  4. load("../framework/DBDD_generic.sage")
  5. load("../framework/proba_utils.sage")
  6. # Restoration Type
  7. SUBSTITUTION = 0
  8. QMODULUS_SUBSTITUTION = 1
  9. class DBDD(DBDD_generic):
  10. """
  11. This class defines all the elements defining a DBDD instance with all
  12. the basis computations
  13. """
  14. def __init__(self, B, S, mu, u=None, verbosity=1, float_type="ld", **kwargs):
  15. """constructor that builds a DBDD instance from a lattice, mean, sigma
  16. and a target
  17. ;min_dim: Number of coordinates to find to consider the problem solved
  18. :B: Basis of the lattice
  19. :S: The Covariance matrix (Sigma) of the uSVP solution
  20. :mu: The expectation of the uSVP solution
  21. :u: The unique vector to be found (optinal, for verification purposes)
  22. :fp_type: Floating point type to use in FPLLL ("d, ld, dd, qd")
  23. """
  24. self.verbosity = verbosity
  25. self.B = B # The lattice Basis
  26. self.D = kwargs.get('D', None) # The dual Basis (only B or D is active)
  27. assert self.D.T * self.B == identity_matrix(B.nrows())
  28. self._dim = B.nrows()
  29. self._maintains_basis = True
  30. self.S = S
  31. self.PP = 0 * S # Span of the projections so far (orthonormal)
  32. self.mu = mu
  33. self.u = u
  34. self.u_original = u
  35. self.expected_length = RR(sqrt(self.S.trace()) + 1)
  36. self.projections = 0
  37. self.save = {"save": None}
  38. self.float_type = float_type
  39. self.estimate_attack(silent=True)
  40. self.Pi = identity_matrix(self._dim) # Reduction matrix
  41. self.Gamma = identity_matrix(self._dim) # Substitution matrix
  42. self._restoration_instructions = []
  43. def dim(self):
  44. return self._dim
  45. def S_diag(self):
  46. return [self.S[i, i] for i in range(self.S.nrows())]
  47. @without_dependencies
  48. def volumes(self):
  49. if self.B is not None:
  50. Bvol = logdet(self.B * self.B.T) / 2
  51. else:
  52. Bvol = -logdet(self.D * self.D.T) / 2
  53. S = self.S + self.mu.T * self.mu
  54. Svol = degen_logdet(S)
  55. dvol = Bvol - Svol / 2.
  56. return (Bvol, Svol, dvol)
  57. @without_dependencies
  58. def test_primitive_dual(self, V, action):
  59. if self.B is None:
  60. self.B = dual_basis(self.D)
  61. W = V * self.B.T
  62. den = lcm([x.denominator() for x in W[0]])
  63. num = gcd([x for x in W[0] * den])
  64. assert den == 1
  65. if num == 1:
  66. return True
  67. if action == "warn":
  68. logging("non-primitive (factor %d)." %
  69. num, style="WARNING", newline=False)
  70. return True
  71. elif action == "reject":
  72. raise RejectedHint("non-primitive (factor %d)." % num)
  73. raise InvalidHint("non-primitive (factor %d)." % num)
  74. def get_reduced_hint_vector(self, V):
  75. V = V * self.Gamma
  76. if V == 0:
  77. raise RejectedHint("Redundant hint")
  78. return V
  79. def add_restoration_instruction(self, type, Gamma, **data):
  80. self.Gamma *= Gamma
  81. last_instruction = self._restoration_instructions[-1] if len(self._restoration_instructions)>0 else (None, None, None)
  82. if type==SUBSTITUTION and last_instruction is not None and last_instruction[0]==SUBSTITUTION:
  83. self._restoration_instructions[-1] = (SUBSTITUTION, last_instruction[1]*Gamma, {})
  84. else:
  85. self._restoration_instructions.append((type, Gamma, data))
  86. @not_after_projections
  87. @hint_integration_wrapper(force=True, requires=["dual"],
  88. invalidates=["primal"])
  89. def integrate_perfect_hint(self, v, l, reduction=False):
  90. V = concatenate(v, -l)
  91. V = self.get_reduced_hint_vector(V)
  92. VS = V * self.S
  93. den = scal(VS * V.T)
  94. self.D = lattice_orthogonal_section(self.D, V, self._maintains_basis)
  95. self._dim -= 1
  96. num = self.mu * V.T
  97. self.mu -= (num / den) * VS
  98. num = VS.T * VS
  99. self.S -= num / den
  100. ## Dimension Reduction
  101. Gamma, data = build_standard_substitution_matrix(V, output_data=True)
  102. normalization_matrix = data['normalization_matrix']
  103. normalized_Gamma = Gamma*normalization_matrix
  104. self.D = self.D * Gamma
  105. self.mu = self.mu * normalized_Gamma
  106. self.S = normalized_Gamma.T * self.S * normalized_Gamma
  107. self.PP = 0 * self.S
  108. self.Pi = normalized_Gamma.T * self.Pi
  109. self.add_restoration_instruction(SUBSTITUTION, Gamma)
  110. @not_after_projections
  111. @hint_integration_wrapper(force=True, requires=["dual"], invalidates=["primal"])
  112. def integrate_modular_hint(self, v, l, k, smooth=True):
  113. V = concatenate(v, -l)
  114. V = self.get_reduced_hint_vector(V)
  115. VS = V * self.S
  116. den = scal(VS * V.T)
  117. if den == 0:
  118. raise NotImplementedError('Normally, useless condition')
  119. #raise RejectedHint("Redundant hint")
  120. if not smooth:
  121. raise NotImplementedError()
  122. self.D = lattice_modular_intersection(self.D, V, k, self._maintains_basis)
  123. @not_after_projections
  124. @hint_integration_wrapper(force=True, requires=["dual"], invalidates=["primal"])
  125. def integrate_q_modular_hint(self, v, l, q):
  126. V = concatenate(v, -l)
  127. V = self.get_reduced_hint_vector(V)
  128. V = V % q
  129. if V == 0:
  130. raise RejectedHint("Redundant hint")
  131. _, pivot = V.nonzero_positions()[0] # Warning, it is non-zero for F_q! It is why there is "V = V%q" before.
  132. V = V * int(mod(V[0,pivot],q)**(-1)) % q
  133. V = V.apply_map(lambda x: recenter(x,q))
  134. W = q * canonical_vec(self._dim, pivot)
  135. Gamma = build_standard_substitution_matrix(V, pivot=pivot)
  136. assert scal(V * W.T)/q == 1, f'<V, W> = {scal(V * W.T)/q} != 1'
  137. self.D = lattice_modular_intersection(self.D, V, q, self._maintains_basis)
  138. # So, V/q is a dual vector, and we hope it is a primitive one
  139. ## Let build the reduction matrix \Pi
  140. #B_ = block_matrix(QQ, [[Gamma.T], [W]])
  141. #Pi = dual_basis(B_)[:self._dim-1]
  142. dim = self._dim
  143. Pi = block_matrix(QQ, [
  144. [identity_matrix(pivot), zero_matrix(pivot,1), zero_matrix(pivot, dim-pivot-1)],
  145. [zero_matrix(dim-pivot-1, pivot), zero_matrix(dim-pivot-1,1), identity_matrix(dim-pivot-1)],
  146. ])
  147. assert Pi.dimensions() == (dim-1, dim), 'Error in the dimension of \Pi'
  148. assert Pi * Gamma == identity_matrix(dim-1), 'Error in the calculation of \Pi: \Pi\Gamma != I'
  149. assert Pi * W.T == 0, 'Error in the calculation of \Pi: \Pi W != 0'
  150. self._dim -= 1
  151. # Even if we have a formulae for the primal basis, it is incorrect because we don't apply "lattice_modular_intersection" on self.B
  152. if self.D is not None:
  153. self.D = self.D * Gamma
  154. self.mu = self.mu * Pi.T
  155. self.S = Pi * self.S * Pi.T
  156. self.PP = 0 * self.S
  157. self.Pi = Pi * self.Pi
  158. self.add_restoration_instruction(QMODULUS_SUBSTITUTION, Gamma, w=W, v=V, q=q)
  159. @not_after_projections
  160. @hint_integration_wrapper(force=True)
  161. def integrate_approx_hint(self, v, l, variance, aposteriori=False):
  162. if variance < 0:
  163. raise InvalidHint("variance must be non-negative !")
  164. if variance == 0:
  165. raise InvalidHint("variance=0 : must use perfect hint !")
  166. if not aposteriori:
  167. V = concatenate(v, -l)
  168. V = self.get_reduced_hint_vector(V)
  169. VS = V * self.S
  170. d = scal(VS * V.T)
  171. center = scal(self.mu * V.T)
  172. coeff = (- center / (variance + d))
  173. self.mu += coeff * VS
  174. self.S -= (1 / (variance + d) * VS.T) * VS
  175. else:
  176. V = concatenate(v, 0)
  177. V = self.get_reduced_hint_vector(V)
  178. VS = V * self.S
  179. # test if eigenvector
  180. #if not scal(VS * V.T)**2 == scal(VS * VS.T) * scal(V * V.T):
  181. # raise RejectedHint("Not an eigenvector of Σ,")
  182. if not scal(VS * VS.T):
  183. raise RejectedHint("0-Eigenvector of Σ forbidden,")
  184. # New formulae
  185. den = scal(VS * V.T)
  186. self.mu += ((l - scal(self.mu * V.T)) / den) * VS
  187. self.S += (((variance - den) / den**2) * VS.T ) * VS
  188. @not_after_projections
  189. @hint_integration_wrapper()
  190. def integrate_approx_hint_fulldim(self, center,
  191. covariance, aposteriori=False):
  192. # Using http://www.cs.columbia.edu/~liulp/pdf/linear_normal_dist.pdf
  193. # with A = Id
  194. if not aposteriori:
  195. d = self.S.nrows() - 1
  196. if self.S.rank() != d or covariance.rank() != d:
  197. raise InvalidHint("Covariances not full dimensional")
  198. zero = vec(d * [0])
  199. F = (self.S + block4(covariance, zero.T, zero, vec([1]))).inverse()
  200. F[-1, -1] = 0
  201. C = concatenate(center, 1)
  202. self.mu += ((C - self.mu) * F) * self.S
  203. self.S -= self.S * F * self.S
  204. else:
  205. raise NotImplementedError()
  206. @hint_integration_wrapper(force=False,
  207. requires=["primal"],
  208. invalidates=["dual"])
  209. def integrate_short_vector_hint(self, v):
  210. V = concatenate(v, 0)
  211. if V.dimensions()[1] > self.Pi.nrows(): # Cannot use self._dim here
  212. V = V * self.Pi.T # Reduction
  213. assert V.dimensions()[1] == self.Pi.nrows()
  214. V -= V * self.PP
  215. if scal((V * self.S) * V.T) == 0:
  216. raise InvalidHint("Projects to 0")
  217. self.projections += 1
  218. PV = identity_matrix(V.ncols()) - projection_matrix(V)
  219. try:
  220. self.B = lattice_project_against(self.B, V, self._maintains_basis)
  221. self._dim -= 1
  222. except ValueError:
  223. raise InvalidHint("Not in Λ")
  224. self.mu = self.mu * PV
  225. self.u = self.u * (self.Pi.T * PV * self.Gamma.T)
  226. self.S = PV.T * self.S * PV
  227. self.PP += V.T * (V / scal(V * V.T))
  228. @without_dependencies
  229. def attack(self, beta_max=None, beta_pre=None, randomize=False, tours=1):
  230. """
  231. Run the lattice reduction to solve the DBDD instance.
  232. Return the (blocksize, solution) of a succesful attack,
  233. or (None, None) on failure
  234. """
  235. self.logging(" Running the Attack ", style="HEADER")
  236. if self.B is None:
  237. self.B = dual_basis(self.D)
  238. # Apply adequate distortion
  239. denom = lcm([x.denominator() for x in self.B.list()])
  240. B = self.B
  241. d = B.nrows()
  242. S = self.S + self.mu.T * self.mu
  243. L, Linv = square_root_inverse_degen(S, self.B)
  244. M = B * Linv
  245. # Make the matrix Integral
  246. denom = lcm([x.denominator() for x in M.list()])
  247. M = matrix(ZZ, M * denom)
  248. # Build the BKZ object
  249. G = None
  250. try:
  251. G = GSO.Mat(IntegerMatrix.from_matrix(M), float_type=self.float_type)
  252. except ValueError as e:
  253. G = GSO.Mat(IntegerMatrix.from_matrix(M))
  254. bkz = BKZReduction(G)
  255. if randomize:
  256. bkz.lll_obj()
  257. bkz.randomize_block(0, d, density=d / 4)
  258. bkz.lll_obj()
  259. u_den = lcm([x.denominator() for x in self.u.list()])
  260. if beta_pre is not None:
  261. self.logging("\rRunning BKZ-%d (until convergence)" %
  262. beta_pre, newline=False)
  263. bkz.lll_obj()
  264. par = BKZ.Param(block_size=beta_pre, strategies=strategies)
  265. bkz(par)
  266. bkz.lll_obj()
  267. else:
  268. beta_pre = 2
  269. # Run BKZ tours with progressively increasing blocksizes
  270. for beta in range(beta_pre, B.nrows() + 1):
  271. self.logging("\rRunning BKZ-%d" % beta, newline=False)
  272. if beta_max is not None:
  273. if beta > beta_max:
  274. self.logging("Failure ... (reached beta_max)",
  275. style="SUCCESS")
  276. self.logging("")
  277. return None, None
  278. if beta == 2:
  279. bkz.lll_obj()
  280. else:
  281. par = BKZ.Param(block_size=beta,
  282. strategies=strategies, max_loops=tours)
  283. bkz(par)
  284. bkz.lll_obj()
  285. # Recover the tentative solution,
  286. # undo distorition, scaling, and test it
  287. v = vec(bkz.A[0])
  288. v = u_den * v * L / denom
  289. solution = matrix(ZZ, v.apply_map(round)) / u_den
  290. self.reduced_solution = solution
  291. for instruc_type, Gamma, data in self._restoration_instructions[::-1]:
  292. solution = solution * Gamma.T
  293. if instruc_type == SUBSTITUTION:
  294. pass
  295. elif instruc_type == QMODULUS_SUBSTITUTION:
  296. w, v, q = data['w'], data['v'], data['q']
  297. w_norm2 = w[0].inner_product(w[0])
  298. projection_of_s = solution - w * (solution[0].inner_product(w[0]) / w_norm2)
  299. solution += w * round(projection_of_s[0].inner_product(v[0])/q)
  300. else:
  301. w, func = data['w'], data['func']
  302. w_norm2 = w[0].inner_product(w[0])
  303. projection_of_s = solution - w * (solution[0].inner_product(w[0]) / w_norm2)
  304. solution += w * func(projection_of_s)
  305. if not self.check_solution(solution):
  306. continue
  307. self.logging("Success !", style="SUCCESS")
  308. self.logging("")
  309. return beta, solution
  310. self.logging("Failure ...", style="FAILURE")
  311. self.logging("")
  312. return None, None