kryptools 1.7__tar.gz → 1.8__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. {kryptools-1.7 → kryptools-1.8}/PKG-INFO +2 -2
  2. {kryptools-1.7 → kryptools-1.8}/README.md +1 -1
  3. {kryptools-1.7 → kryptools-1.8}/kryptools/Zmod.py +25 -5
  4. {kryptools-1.7 → kryptools-1.8}/kryptools/__init__.py +3 -2
  5. {kryptools-1.7 → kryptools-1.8}/kryptools/la.py +188 -57
  6. {kryptools-1.7 → kryptools-1.8}/kryptools/lat.py +123 -24
  7. {kryptools-1.7 → kryptools-1.8}/kryptools.egg-info/PKG-INFO +2 -2
  8. {kryptools-1.7 → kryptools-1.8}/pyproject.toml +1 -1
  9. {kryptools-1.7 → kryptools-1.8}/tests/test_Zmod.py +5 -2
  10. {kryptools-1.7 → kryptools-1.8}/tests/test_la.py +2 -2
  11. {kryptools-1.7 → kryptools-1.8}/LICENSE +0 -0
  12. {kryptools-1.7 → kryptools-1.8}/kryptools/GF2.py +0 -0
  13. {kryptools-1.7 → kryptools-1.8}/kryptools/aes.py +0 -0
  14. {kryptools-1.7 → kryptools-1.8}/kryptools/blockcipher.py +0 -0
  15. {kryptools-1.7 → kryptools-1.8}/kryptools/code.py +0 -0
  16. {kryptools-1.7 → kryptools-1.8}/kryptools/conway_polynomials.py +0 -0
  17. {kryptools-1.7 → kryptools-1.8}/kryptools/des.py +0 -0
  18. {kryptools-1.7 → kryptools-1.8}/kryptools/dlp.py +0 -0
  19. {kryptools-1.7 → kryptools-1.8}/kryptools/dlp_bsgs.py +0 -0
  20. {kryptools-1.7 → kryptools-1.8}/kryptools/dlp_ic.py +0 -0
  21. {kryptools-1.7 → kryptools-1.8}/kryptools/dlp_qs.py +0 -0
  22. {kryptools-1.7 → kryptools-1.8}/kryptools/dlp_rho.py +0 -0
  23. {kryptools-1.7 → kryptools-1.8}/kryptools/ec.py +0 -0
  24. {kryptools-1.7 → kryptools-1.8}/kryptools/factor.py +0 -0
  25. {kryptools-1.7 → kryptools-1.8}/kryptools/factor_dix.py +0 -0
  26. {kryptools-1.7 → kryptools-1.8}/kryptools/factor_ecm.py +0 -0
  27. {kryptools-1.7 → kryptools-1.8}/kryptools/factor_fmt.py +0 -0
  28. {kryptools-1.7 → kryptools-1.8}/kryptools/factor_pm1.py +0 -0
  29. {kryptools-1.7 → kryptools-1.8}/kryptools/factor_qs.py +0 -0
  30. {kryptools-1.7 → kryptools-1.8}/kryptools/factor_rho.py +0 -0
  31. {kryptools-1.7 → kryptools-1.8}/kryptools/intfuncs.py +0 -0
  32. {kryptools-1.7 → kryptools-1.8}/kryptools/keccak.py +0 -0
  33. {kryptools-1.7 → kryptools-1.8}/kryptools/nt.py +0 -0
  34. {kryptools-1.7 → kryptools-1.8}/kryptools/poly.py +0 -0
  35. {kryptools-1.7 → kryptools-1.8}/kryptools/primes.py +0 -0
  36. {kryptools-1.7 → kryptools-1.8}/kryptools/sha1.py +0 -0
  37. {kryptools-1.7 → kryptools-1.8}/kryptools.egg-info/SOURCES.txt +0 -0
  38. {kryptools-1.7 → kryptools-1.8}/kryptools.egg-info/dependency_links.txt +0 -0
  39. {kryptools-1.7 → kryptools-1.8}/kryptools.egg-info/top_level.txt +0 -0
  40. {kryptools-1.7 → kryptools-1.8}/setup.cfg +0 -0
  41. {kryptools-1.7 → kryptools-1.8}/tests/test_GF2.py +0 -0
  42. {kryptools-1.7 → kryptools-1.8}/tests/test_aes.py +0 -0
  43. {kryptools-1.7 → kryptools-1.8}/tests/test_code.py +0 -0
  44. {kryptools-1.7 → kryptools-1.8}/tests/test_des.py +0 -0
  45. {kryptools-1.7 → kryptools-1.8}/tests/test_dlog.py +0 -0
  46. {kryptools-1.7 → kryptools-1.8}/tests/test_ec.py +0 -0
  47. {kryptools-1.7 → kryptools-1.8}/tests/test_factor.py +0 -0
  48. {kryptools-1.7 → kryptools-1.8}/tests/test_factor_fmt.py +0 -0
  49. {kryptools-1.7 → kryptools-1.8}/tests/test_factor_pm1.py +0 -0
  50. {kryptools-1.7 → kryptools-1.8}/tests/test_factor_qs.py +0 -0
  51. {kryptools-1.7 → kryptools-1.8}/tests/test_intfuncs.py +0 -0
  52. {kryptools-1.7 → kryptools-1.8}/tests/test_keccak.py +0 -0
  53. {kryptools-1.7 → kryptools-1.8}/tests/test_lat.py +0 -0
  54. {kryptools-1.7 → kryptools-1.8}/tests/test_nt.py +0 -0
  55. {kryptools-1.7 → kryptools-1.8}/tests/test_poly.py +0 -0
  56. {kryptools-1.7 → kryptools-1.8}/tests/test_primes.py +0 -0
  57. {kryptools-1.7 → kryptools-1.8}/tests/test_sha1.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kryptools
3
- Version: 1.7
3
+ Version: 1.8
4
4
  Summary: Implemenation of same basic algorithms used in cryptography.
5
5
  Author-email: Gerald Teschl <gerald.teschl@univie.ac.at>
6
6
  License: MIT
@@ -44,7 +44,7 @@ The tools contained are:
44
44
  * integer factorization (Fermat, Pollard p-1, Pollard rho, Lenstra's ECM, Dixon, basic quadratic sieve)
45
45
  * elliptic curves (Weierstrass form), group operations, order, discrete logarithms
46
46
  * linear algebra: Hermite normal form, Gram-Schmidt
47
- * lattices: Hadamard ratio, Babai rounding/nearest plane algorithm, lattice reduction (Lenstra-Lenstra-Lovaz)
47
+ * lattices: Hadamard ratio, Babai rounding/nearest plane algorithm, lattice reduction (Lenstra-Lenstra-Lovaz), SIS, ISIS, LWE
48
48
  * linear codes: Hamming distance, left standard form, parity check matrix
49
49
 
50
50
  * Matrix: a class for Matrices (inverse, det, reduced echelon form, kernel, etc.)
@@ -26,7 +26,7 @@ The tools contained are:
26
26
  * integer factorization (Fermat, Pollard p-1, Pollard rho, Lenstra's ECM, Dixon, basic quadratic sieve)
27
27
  * elliptic curves (Weierstrass form), group operations, order, discrete logarithms
28
28
  * linear algebra: Hermite normal form, Gram-Schmidt
29
- * lattices: Hadamard ratio, Babai rounding/nearest plane algorithm, lattice reduction (Lenstra-Lenstra-Lovaz)
29
+ * lattices: Hadamard ratio, Babai rounding/nearest plane algorithm, lattice reduction (Lenstra-Lenstra-Lovaz), SIS, ISIS, LWE
30
30
  * linear codes: Hamming distance, left standard form, parity check matrix
31
31
 
32
32
  * Matrix: a class for Matrices (inverse, det, reduced echelon form, kernel, etc.)
@@ -28,13 +28,15 @@ class Zmod:
28
28
  0
29
29
  """
30
30
 
31
- def __init__(self, n: int, short: bool = True):
31
+ def __init__(self, n: int, short: bool = True, sharp: bool = False):
32
32
  if not isinstance(n, int) or n < 1:
33
33
  raise ValueError(f"{n} is not a positive integer.")
34
34
  self.n = n
35
+ self.n2 = n // 2
35
36
  self.isfield = None
36
37
  self.n_factors = {} # factoring of n
37
38
  self.short = short
39
+ self.sharp = sharp
38
40
  self.group_order = 0 # order of the multiplicative group
39
41
  self.factors = {} # factoring of the multiplicative group order
40
42
 
@@ -161,9 +163,13 @@ class ZmodPoint:
161
163
  self.ring = ring
162
164
 
163
165
  def __repr__(self):
166
+ if self.ring.sharp:
167
+ x = self.sharp()
168
+ else:
169
+ x = self.x
164
170
  if self.ring.short:
165
- return str(self.x)
166
- return f"{self.x} (mod {self.ring.n})"
171
+ return str(x)
172
+ return f"{x} (mod {self.ring.n})"
167
173
 
168
174
  def __eq__(self, other):
169
175
  if not isinstance(other, self.__class__) or self.ring != other.ring:
@@ -239,7 +245,9 @@ class ZmodPoint:
239
245
  return NotImplemented
240
246
 
241
247
  def __abs__(self) -> int:
242
- return abs(self.sharp())
248
+ if self.x <= self.ring.n2:
249
+ return self.x
250
+ return self.ring.n - self.x
243
251
 
244
252
  def bits(self) -> list:
245
253
  "Convert to a list of bits."
@@ -247,7 +255,7 @@ class ZmodPoint:
247
255
 
248
256
  def sharp(self):
249
257
  "Returns a symmetric (w.r.t. 0) representative."
250
- if self.x <= self.ring.n // 2:
258
+ if self.x <= self.ring.n2:
251
259
  return self.x
252
260
  return self.x - self.ring.n
253
261
 
@@ -320,6 +328,18 @@ class ZmodPoint:
320
328
  roots.append((p**(j//2) * x) % pk)
321
329
  return self.ring(crt(roots, powers))
322
330
 
331
+ def solve(self, b):
332
+ "Find a solution `x` of the linear equation `self * x == b` in Z_n."
333
+ b = int(b)
334
+ if not self:
335
+ if b:
336
+ return None
337
+ return self.ring(0)
338
+ g = gcd(self.x, self.ring.n)
339
+ if b % g:
340
+ return None
341
+ return self.ring(pow(self.x // g, -1, self.ring.n) * (b // g))
342
+
323
343
  def is_generator(self):
324
344
  "Test if the point is a generator of the group Z_n^*."
325
345
  return self.ring.order() == self.order()
@@ -5,7 +5,7 @@ Implemenation of same basic algorithms used in cryptography.
5
5
  __author__ = "Gerald Teschl"
6
6
  __copyright__ = "Copyright 2024-2025, Gerald Teschl"
7
7
  __license__ = "MIT License"
8
- __version__ = "1.7"
8
+ __version__ = "1.8"
9
9
  __email__ = "Gerald.Teschl@univie.ac.at"
10
10
 
11
11
  from .nt import egcd, cf, convergents, legendre_symbol, jacobi_symbol, sqrt_mod, euler_phi, carmichael_lambda, moebius_mu, is_carmichael_number, order, crt
@@ -15,7 +15,8 @@ from .factor import factorint, divisors
15
15
  from .dlp import dlog
16
16
  from .ec import EC_Weierstrass
17
17
  from .la import Matrix, zeros, eye, circulant, BinaryMatrix
18
- from .lat import gram_det, hadamard_ratio, hermite_nf, gram_schmidt, babai_round_cvp, babai_round_bnd, babai_plane_cvp, babai_plane_bnd, lagrange_lr, lll, random_unimodular_matrix
18
+ from .lat import gram_det, hadamard_ratio, hermite_nf, gram_schmidt, babai_round_cvp, babai_round_bnd, babai_plane_cvp, babai_plane_bnd, lagrange_lr, lll, random_unimodular_matrix, q_ary_lattice
19
+ from .lat import svp_lll, cvp_lll, sis_lll, isis_lll, svp_search, cvp_search, sis_search, isis_search
19
20
  from .poly import Poly, PolyBinMult, lagrange_interpolation
20
21
  from .Zmod import Zmod
21
22
  from .GF2 import GF2, GF2_aes, GF2_miniaes, GF2_ghash
@@ -2,7 +2,8 @@
2
2
  Linear algebra
3
3
  """
4
4
 
5
- from math import inf, sqrt
5
+ # pragma pylint: disable=C0302
6
+ from math import gcd, inf, sqrt
6
7
  from numbers import Number
7
8
  from fractions import Fraction
8
9
  from .Zmod import Zmod
@@ -69,7 +70,7 @@ class Matrix:
69
70
  res = "\\begin{pmatrix}\n"
70
71
  for row in self.matrix:
71
72
  res += " & ".join(map(str, row)) + '\\\\\n'
72
- res += '\\end{pmatrix}'
73
+ res = res[:-3] + '\n\\end{pmatrix}'
73
74
  return res
74
75
 
75
76
  def __len__(self):
@@ -177,26 +178,42 @@ class Matrix:
177
178
 
178
179
  def append_row(self, row: list|tuple, ring = None) -> None:
179
180
  "Append a row."
180
- if not isinstance(row, list|tuple):
181
+ if isinstance(row, self.__class__):
182
+ row = row.matrix
183
+ elif not isinstance(row, list|tuple):
181
184
  row = list(row)
182
- if len(row) != self.cols:
185
+ if not isinstance(row[0], list|tuple):
186
+ row = [ row ]
187
+ if len(row[0]) != self.cols:
183
188
  raise ValueError("Length does not match the number of columns.")
184
189
  if ring:
185
- row =list(map(ring, row))
186
- self.matrix.append(row)
187
- self.rows += 1
190
+ for i, r in enumerate(row):
191
+ row[i] =list(map(ring, r))
192
+ self.matrix += row
193
+ self.rows += len(row)
188
194
 
189
195
  def append_column(self, col: list|tuple, ring = None) -> None:
190
196
  "Append a column."
191
- if not isinstance(col, list|tuple):
197
+ if isinstance(col, self.__class__):
198
+ col = col.matrix
199
+ elif not isinstance(col, list|tuple):
192
200
  col = list(col)
193
201
  if len(col) != self.rows:
194
202
  raise ValueError("Length does not match the number of rows.")
195
- if ring:
196
- col =list(map(ring, col))
197
- for i in range(self.rows):
198
- self.matrix[i].append(col[i])
199
- self.cols += 1
203
+ if isinstance(col[0], list|tuple):
204
+ for i, c in enumerate(col):
205
+ if ring:
206
+ self.matrix[i] += list(map(ring, c))
207
+ else:
208
+ self.matrix[i] += c
209
+ self.cols += len(col[0])
210
+ else:
211
+ for i, c in enumerate(col):
212
+ if ring:
213
+ self.matrix[i].append(ring(c))
214
+ else:
215
+ self.matrix[i].append(c)
216
+ self.cols += 1
200
217
 
201
218
  def permute_columns(self, permutation) -> None:
202
219
  "Permute columns according to a list of new positions."
@@ -240,7 +257,7 @@ class Matrix:
240
257
 
241
258
  def norm2(self) -> float:
242
259
  "Squared Frobenius/Euclidean norm."
243
- return sum(sum(x*x for x in row) for row in self.matrix)
260
+ return sum(sum(abs(x)**2 for x in row) for row in self.matrix)
244
261
 
245
262
  def norm(self, p: int = 2) -> float:
246
263
  "p-norm of the matrix regarded as a vector."
@@ -312,37 +329,45 @@ class Matrix:
312
329
  return self.__class__([[item * other for item in row] for row in self.matrix])
313
330
  return NotImplemented
314
331
 
315
- def rref(self, start = 0, drop_zero_rows = False) -> "Matrix":
332
+ def rref(self, start: int = 0, drop_zero_rows: bool = False) -> "Matrix":
316
333
  "Compute the reduced echelon form."
334
+ if hasattr(self.matrix[0][0], "ring") and not self.matrix[0][0].ring.is_field():
335
+ # the matrix is over a ring (not a field)
336
+ return self._rref_ring(start = start, drop_zero_rows = drop_zero_rows)
317
337
  one = self._guess_zero()[1]
318
338
  n, m = self.cols, self.rows
319
- R = self[:, :]
320
- R.pivotcols = []
321
- R.nonpivotcols = []
339
+ R = [ self.matrix[i][:] for i in range(m) ]
340
+ pivotcols = []
341
+ nonpivotcols = []
322
342
  i = 0
323
343
  if start >= n:
324
344
  raise ValueError("Start value cannot be beyond the last column.")
325
345
  for j in range(start, n):
326
- if not R[i, j]: # search for a nonzero entry in the present column
327
- for ii in range(i+1, m):
328
- if R[ii, j]:
329
- R[i, :], R[ii, :] = R[ii, :], R[i, :] # swap rows
346
+ if not R[i][j]: # search for a nonzero entry in the present column
347
+ for ii in range(i + 1, m):
348
+ if R[ii][j]:
349
+ R[i], R[ii] = R[ii], R[i] # swap rows
330
350
  break
331
351
  else:
332
- R.nonpivotcols.append(j)
352
+ nonpivotcols.append(j)
333
353
  continue # all entries are zero
334
- R.pivotcols.append(j)
335
- if R[i, j] != one:
336
- R[i, :] = R[i, j]**-1 * R[i, :] # make the pivot one
354
+ pivotcols.append(j)
355
+ if R[i][j] != one:
356
+ tmp = R[i][j]**-1
357
+ for k in range(n):
358
+ R[i][k] *= tmp # make the pivot one
337
359
  for ii in range(m): # remove the column entries above/below the pivot
338
360
  if i == ii:
339
361
  continue
340
- tmp = R[ii, j]
341
- R[ii, ::] -= tmp * R[i, :]
362
+ tmp = R[ii][j]
363
+ for k in range(n):
364
+ R[ii][k] -= tmp * R[i][k]
342
365
  i += 1
343
366
  if i == m:
344
367
  break
345
- R.nonpivotcols += list(range(j+1, R.cols))
368
+ R = Matrix(R)
369
+ R.pivotcols = pivotcols
370
+ R.nonpivotcols = nonpivotcols + list(range(j+1, R.cols))
346
371
  # purge zero rows
347
372
  if drop_zero_rows:
348
373
  l = len(R.pivotcols)
@@ -352,6 +377,85 @@ class Matrix:
352
377
  R.rows = l
353
378
  return R
354
379
 
380
+ def _rref_ring(self, start: int = 0, drop_zero_rows: bool = False) -> "Matrix":
381
+ "Compute the reduced echelon form if the base ring is Z_n and no field."
382
+ ring = self.matrix[0][0].ring
383
+ M = self.applyfunc(int)
384
+ done = False
385
+ while not done:
386
+ M = M.hermite_rnf(start = start, drop_zero_rows = drop_zero_rows)
387
+ done = True
388
+ for i, j in enumerate(M.pivotcols):
389
+ pivot = M.matrix[i][j]
390
+ if pivot >= ring.n:
391
+ done = False
392
+ pivot %= ring.n
393
+ if pivot!=1 and gcd(pivot, ring.n) == 1: # we can make the pivot one
394
+ done = False
395
+ M.matrix[i][j] = 1
396
+ tmp = pow(pivot, -1, ring.n)
397
+ for k in range(j + 1, M.cols):
398
+ M.matrix[i][k] *= tmp
399
+ M.map(lambda x: x % ring.n)
400
+ M.map(ring)
401
+ return M
402
+
403
+ def hermite_rnf(self, start = 0, drop_zero_rows: bool = False) -> "Matrix":
404
+ "Compute the Hermite row normal form."
405
+ n, m = self.cols, self.rows
406
+ if not isinstance(self.matrix[0][0], int):
407
+ raise ValueError("Hermite normal form requires integer entries!")
408
+ H = [ self.matrix[i][:] for i in range(m) ]
409
+ pivotcols = []
410
+ nonpivotcols = []
411
+ i = 0
412
+ if start >= n:
413
+ raise ValueError("Start value cannot be beyond the last column.")
414
+ for j in range(start, n):
415
+ i0 = i
416
+ minimum = abs(H[i][j]) # search for the pivot in the present column
417
+ for ii in range(i + 1, m):
418
+ tmp = abs(H[ii][j])
419
+ if tmp > 0 and (tmp < minimum or minimum == 0):
420
+ minimum = tmp
421
+ i0 = ii
422
+ if minimum == 0:
423
+ nonpivotcols.append(j)
424
+ continue # all entrjes are zero
425
+ pivotcols.append(j)
426
+ if i0 > i:
427
+ H[i], H[i0] = H[i0], H[i] # swap rows, to move the pivot jn place
428
+ if H[i][j] < 0:
429
+ for k in range(n):
430
+ H[i][k] *= -1 # make the pivot positive
431
+ ii = i + 1
432
+ while ii < m: # make the column entries below to the pivot zero
433
+ tmp = H[ii][j] // H[i][j]
434
+ for k in range(n):
435
+ H[ii][k] -= tmp * H[i][k]
436
+ if H[ii][j]:
437
+ H[i], H[ii] = H[ii], H[i] # swap rows
438
+ else:
439
+ ii += 1
440
+ for ii in range(i): # reduce the column entries above to the pivot
441
+ tmp = H[ii][j] // H[i][j]
442
+ for k in range(n):
443
+ H[ii][k] -= tmp * H[i][k]
444
+ i += 1
445
+ if i >= m:
446
+ break
447
+ H = Matrix(H)
448
+ H.pivotcols = pivotcols
449
+ H.nonpivotcols = nonpivotcols + list(range(j+1, H.cols))
450
+ # purge zero rows
451
+ if drop_zero_rows:
452
+ l = len(H.pivotcols)
453
+ if not l:
454
+ l = 1 # do not delete all rows
455
+ del H.matrix[l:]
456
+ H.rows = l
457
+ return H
458
+
355
459
  def left_standard_form(self) -> "Matrix":
356
460
  "Compute the left standard form."
357
461
  # reduced row echelon form
@@ -363,6 +467,8 @@ class Matrix:
363
467
 
364
468
  def kernel(self) -> "Matrix":
365
469
  "Compute a basis for the kernel."
470
+ if hasattr(self.matrix[0][0], "ring") and not self.matrix[0][0].ring.is_field():
471
+ raise NotImplementedError("The matrix must be over a field, not a ring.")
366
472
  _, one = self._guess_zero()
367
473
  M = self.rref(drop_zero_rows = True)
368
474
  K = M.zeros(M.cols, max(1,len(M.nonpivotcols)))
@@ -374,31 +480,43 @@ class Matrix:
374
480
 
375
481
  def det(self) -> int:
376
482
  "Compute the determinant."
377
- zero, one = self._guess_zero()
378
- if self.rows != self.cols:
379
- raise ValueError("Matrix must be square!")
380
483
  n, m = self.cols, self.rows
381
- R = self[:, :]
484
+ if n != m:
485
+ raise ValueError("Matrix must be square!")
486
+ if hasattr(self.matrix[0][0], "ring") and not self.matrix[0][0].ring.is_field():
487
+ # the matrix is over a ring (not a field)
488
+ ring = self.matrix[0][0].ring
489
+ R = [ list(map(lambda x: Fraction(int(x)), self.matrix[i])) for i in range(m) ]
490
+ zero, one = ring(0), 1
491
+ else:
492
+ ring = None
493
+ R = [ self.matrix[i][:] for i in range(m) ]
494
+ zero, one = self._guess_zero()
382
495
  D = one
383
496
  i = 0
384
497
  for j in range(n):
385
- if not R[i, j]: # search for a nonzero entry in the present column
498
+ if not R[i][j]: # search for a nonzero entry in the present column
386
499
  for ii in range(i+1, m):
387
- if R[ii, j]:
500
+ if R[ii][j]:
388
501
  D *= -one
389
- R[i, :], R[ii, :] = R[ii, :], R[i, :] # swap rows
502
+ R[i], R[ii] = R[ii], R[i] # swap rows
390
503
  break
391
- else:
392
- return zero # all entries are zero
393
- if R[i, j] != one:
394
- D *= R[i, j]
395
- R[i, :] = R[i, j]**-1 * R[i, :] # make the pivot one
396
- for ii in range(i+1, n): # remove the column entries below the pivot
504
+ else: # all entries are zero
505
+ return zero
506
+ if R[i][j] != one:
507
+ D *= R[i][j]
508
+ tmp = R[i][j]**-1
509
+ for k in range(n):
510
+ R[i][k] *= tmp # make the pivot one
511
+ for ii in range(i + 1, n): # remove the column entries below the pivot
397
512
  if i == ii:
398
513
  continue
399
- tmp = R[ii, j]
400
- R[ii, ::] -= tmp * R[i, :]
514
+ tmp = R[ii][j]
515
+ for k in range(n):
516
+ R[ii][k] -= tmp * R[i][k]
401
517
  i += 1
518
+ if ring:
519
+ return ring(D)
402
520
  return D
403
521
 
404
522
  def rank(self) -> int:
@@ -425,12 +543,17 @@ class Matrix:
425
543
  M = self.__class__(M).rref()
426
544
  if not left and M.pivotcols[self.cols - 1] != self.cols -1:
427
545
  raise ValueError("Matrix is not invertible!")
546
+ if hasattr(self.matrix[0][0], "ring") and not self.matrix[0][0].ring.is_field():
547
+ # the matrix is over a ring (not a field)
548
+ for i, j in enumerate(M.pivotcols):
549
+ if M.matrix[i][j] != one:
550
+ raise ValueError("Matrix is not invertible!")
428
551
  return M[:, n:]
429
552
 
430
- def solve(self, b: list|tuple, ring = None) -> "Matrix":
553
+ def solve(self, b: list|tuple) -> "Matrix":
431
554
  "Solve the linear system with given inhomogenous vector."
432
555
  if isinstance(b, list|tuple):
433
- b = Matrix(b, ring = ring)
556
+ b = Matrix(b)
434
557
  if self.rows != b.rows or b.cols != 1:
435
558
  raise ValueError("Matrix dimensions do not match.")
436
559
  A = self.zeros(self.rows, self.cols + 1) # extended coefficient matrix
@@ -438,15 +561,24 @@ class Matrix:
438
561
  A[:, self.cols] = b
439
562
  A = A.rref(drop_zero_rows = True)
440
563
  solution = self.zeros(self.cols, 1)
564
+ if not any(A.matrix[-1][:-1]):
565
+ if A.matrix[-1][-1]:
566
+ return None # Not solvable
567
+ if hasattr(self.matrix[0][0], "ring") and not self.matrix[0][0].ring.is_field():
568
+ # the matrix is over a ring (not a field)
569
+ ring = self.matrix[0][0].ring
570
+ else:
571
+ ring = None
441
572
  for i in range(A.rows-1, -1, -1):
442
- if not any(A.matrix[i][:-1]):
443
- if A.matrix[i][-1]:
444
- return None # Not solvable
445
- else:
446
- for j in range(A.cols - 1):
447
- if A.matrix[i][j]:
448
- break # leading nonzero coefficient
449
- solution[j]= A.matrix[i][-1]
573
+ j = A.pivotcols[i]
574
+ b = A.matrix[i][-1]
575
+ if ring:
576
+ for k in range(j + 1, self.cols):
577
+ b -= A.matrix[i][k] * solution[k]
578
+ b = A.matrix[i][j].solve(b)
579
+ if b is None:
580
+ return None
581
+ solution[j] = b
450
582
  return solution
451
583
 
452
584
  def is_unimodular(self) -> bool:
@@ -520,7 +652,6 @@ def circulant(vector: list|tuple, m: int = None, ring=None) -> "Matrix":
520
652
  vector = list(reversed(vector))
521
653
  return Matrix([rotate(vector, -n-1) for n in range(m)], ring=ring)
522
654
 
523
-
524
655
  class BinaryMatrix:
525
656
  """
526
657
  Binary Matrix class.
@@ -528,7 +659,7 @@ class BinaryMatrix:
528
659
  Example:
529
660
 
530
661
  To define a binary matrix use
531
- >>> Matrix([[0, 1], [1, 0]])
662
+ >>> BinaryMatrix([[0, 1], [1, 0]])
532
663
  [01]
533
664
  [10]
534
665
  """
@@ -793,7 +924,7 @@ class BinaryMatrix:
793
924
  return res
794
925
  return self.from_bits(res)
795
926
 
796
- def rref(self, start = 0, reduce = True, drop_zero_rows = False) -> "Matrix":
927
+ def rref(self, start: int = 0, reduce: bool = True, drop_zero_rows: bool = False) -> "Matrix":
797
928
  "Row reduced echelon form."
798
929
  rref = [ None ] * self.cols # store rows accoring to leading bit
799
930
  rows = self.matrix[:] # copy
@@ -2,10 +2,11 @@
2
2
  Lattice tools
3
3
  """
4
4
 
5
- from math import prod, floor
5
+ from math import prod, floor, inf
6
+ from itertools import product
6
7
  from fractions import Fraction
7
8
  from random import choice, sample
8
- from .la import Matrix, zeros
9
+ from .la import Matrix, eye, zeros
9
10
 
10
11
 
11
12
  def hermite_nf(M: Matrix) -> Matrix:
@@ -13,7 +14,7 @@ def hermite_nf(M: Matrix) -> Matrix:
13
14
  n, m = M.cols, M.rows
14
15
  H = M[:, :]
15
16
  j = n - 1
16
- for i in reversed(range(m)):
17
+ for i in range(m-1,-1,-1):
17
18
  j0 = j
18
19
  minimum = abs(H[i, j]) # search for the pivot in the present row
19
20
  for jj in range(j):
@@ -24,10 +25,7 @@ def hermite_nf(M: Matrix) -> Matrix:
24
25
  if minimum == 0:
25
26
  continue # all entries are zero
26
27
  if j0 < j:
27
- H[:, j], H[:, j0] = (
28
- H[:, j0],
29
- H[:, j],
30
- ) # swap columns, to move the pivot in place
28
+ H[:, j], H[:, j0] = H[:, j0], H[:, j] # swap columns, to move the pivot in place
31
29
  if H[i, j] < 0:
32
30
  H[:, j] *= -1 # make the pivot positive
33
31
  jj = j - 1
@@ -41,7 +39,6 @@ def hermite_nf(M: Matrix) -> Matrix:
41
39
  for jj in range(j + 1, n): # reduce the row entries right to the pivot
42
40
  tmp = H[i, jj] // H[i, j]
43
41
  H[:, jj] -= tmp * H[:, j]
44
- #print(H)
45
42
  j -= 1
46
43
  if j < 0:
47
44
  break
@@ -55,22 +52,32 @@ def norm2(v: Matrix) -> float:
55
52
  return sum(map(lambda x: x * x, v))
56
53
 
57
54
 
58
- def gram_schmidt(U: Matrix) -> (Matrix, Matrix):
55
+ def gram_schmidt(U: Matrix, drop_dependent: bool = True) -> (Matrix, Matrix):
59
56
  "Compute the Gram-Schmidt orthogonalization of the column vectors of a matrix M."
60
- M = U.eye()
61
- Us = U[:, :]
62
- for j in range(1, U.rows):
57
+ M = U.eye(U.cols)
58
+ Us = U.zeros()
59
+ Us[:, 0] = U[:, 0]
60
+ jj = 0 # offset taking removed vectors into account
61
+ for j in range(1, U.cols):
63
62
  tmp = U[:, j]
64
- for i in range(j):
65
- M[i, j] = U[:, j].dot(Us[:, i]) / norm2(Us[:, i])
66
- tmp -= M[i, j] * Us[:, i]
67
- Us[:, j] = tmp
63
+ for i in range(j - jj):
64
+ M[i, j - jj] = U[:, j].dot(Us[:, i]) / Us[:, i].norm2()
65
+ tmp -= M[i, j - jj] * Us[:, i]
66
+ if not tmp:
67
+ if not drop_dependent:
68
+ raise ValueError("Vectors are linearly dependent.")
69
+ jj += 1
70
+ else:
71
+ Us[:, j - jj] = tmp
72
+ if jj:
73
+ Us = Us[:,:-jj]
74
+ M = M[:-jj,:-jj]
68
75
  return Us, M
69
76
 
70
77
 
71
78
  def gram_det(U: Matrix) -> float:
72
79
  "Compute the Gram determinant of a matrix."
73
- Us = gram_schmidt(U)[0]
80
+ Us = gram_schmidt(U, drop_dependent = False)[0]
74
81
  return prod([Us[:, i].norm() for i in range(U.rows)])
75
82
 
76
83
 
@@ -109,7 +116,7 @@ def babai_plane_bnd(U: Matrix, p=2) -> float:
109
116
 
110
117
  def lagrange_lr(V: Matrix) -> Matrix:
111
118
  "Lagrange lattice reduction."
112
- if (V.rows, V.cols) != (2, 2):
119
+ if V.cols != 2:
113
120
  raise ValueError("Lagrange lattice reduction requires dimension two.")
114
121
  v1, v2 = V[:, 0], V[:, 1]
115
122
  if norm2(v1) > norm2(v2):
@@ -130,10 +137,10 @@ def lll(V: Matrix, delta: float = 0.75, sort: bool = True) -> Matrix:
130
137
  U.map(int)
131
138
  Us = U[:, :]
132
139
  Us.map(Fraction)
133
- M = U.zeros()
140
+ M = U.zeros(U.cols)
134
141
  M.map(Fraction)
135
142
  M[0, 0] = norm2(Us[:, 0]) # we store the squared norms on the diagonal
136
- for l in range(1, U.rows): # Gram-Schmidt decomposition
143
+ for l in range(1, U.cols): # Gram-Schmidt decomposition
137
144
  tmp = U[:, l]
138
145
  for i in range(l):
139
146
  M[i, l] = U[:, l].dot(Us[:, i]) / M[i, i]
@@ -141,7 +148,7 @@ def lll(V: Matrix, delta: float = 0.75, sort: bool = True) -> Matrix:
141
148
  Us[:, l] = tmp
142
149
  M[l, l] = norm2(Us[:, l])
143
150
 
144
- while j < U.rows:
151
+ while j < U.cols:
145
152
  for i in range(j - 1, -1, -1): # reduce the weights of the basis vectors
146
153
  r = round(M[i, j])
147
154
  if r:
@@ -172,7 +179,7 @@ def lll(V: Matrix, delta: float = 0.75, sort: bool = True) -> Matrix:
172
179
  M[l, j], M[l, j - 1] = M[l, j - 1], M[l, j]
173
180
  tmp1 = oldM00 / M[j - 1, j - 1]
174
181
  tmp2 = oldM10 * oldM11 / M[j - 1, j - 1]
175
- for l in range(j + 1, U.rows):
182
+ for l in range(j + 1, U.cols):
176
183
  M[j - 1, l], M[j, l] = (
177
184
  tmp1 * M[j, l] + tmp2 * M[j - 1, l],
178
185
  M[j - 1, l] - oldM10 * M[j, l],
@@ -180,9 +187,9 @@ def lll(V: Matrix, delta: float = 0.75, sort: bool = True) -> Matrix:
180
187
  j = max(j - 1, 1) # redo the last step
181
188
 
182
189
  if sort: # sort the vectors according to their norm
183
- tmp = [U[:, j] for j in range(U.rows)]
190
+ tmp = [U[:, j] for j in range(U.cols)]
184
191
  tmp.sort(key=norm2)
185
- for j in range(U.rows):
192
+ for j in range(U.cols):
186
193
  U[:, j] = tmp[j]
187
194
  return U
188
195
 
@@ -204,3 +211,95 @@ def random_unimodular_matrix(n: int, iterations: int = 50, max_val: int = None)
204
211
  if not max_val or max(abs(x) for x in tmp) <= max_val:
205
212
  W[:, i] = tmp
206
213
  return W
214
+
215
+ def q_ary_lattice(U: Matrix, lll: bool = False) -> Matrix: # pylint: disable=W0621
216
+ "Create a q-ary lattice and (optinally) LLL reduce the basis."
217
+ if isinstance(U, Matrix) and hasattr(U.matrix[0][0], "ring"):
218
+ q = U.matrix[0][0].ring.n
219
+ else:
220
+ raise ValueError("The matrix does not seem to be over Zmod.")
221
+ V = eye(U.rows, one = q)
222
+ V.append_column(U, ring = int)
223
+ V = hermite_nf(V)
224
+ if lll:
225
+ return globals()['lll'](V)
226
+ return V
227
+
228
+ def svp_lll(U: Matrix) -> Matrix:
229
+ "Solve the shortest vector problem in a q-ary lattice associated with a matrix over a finite ring Z_q."
230
+ gf = U[0].ring
231
+ V = q_ary_lattice(U, lll = True)
232
+ x = V[:, 0]
233
+ x.map(gf)
234
+ return x
235
+
236
+ def cvp_lll(U: Matrix, x: Matrix) -> Matrix:
237
+ "Solve the closest vector problem in a q-ary lattice associated with a matrix over a finite ring Z_q (LWE)."
238
+ gf = U[0].ring
239
+ V = q_ary_lattice(U, lll = True)
240
+ x.map(int)
241
+ y = babai_plane_cvp(x, V)
242
+ x.map(gf)
243
+ return y
244
+
245
+ def sis_lll(A: Matrix) -> Matrix:
246
+ "Solve the short integer problem over a finite ring Z_q (SIS)."
247
+ return svp_lll(A.kernel())
248
+
249
+ def isis_lll(A: Matrix, b: Matrix) -> Matrix:
250
+ "Solve the inhomogenous short integer problem over a finite ring Z_q (ISIS)."
251
+ y = A.solve(b)
252
+ return y - cvp_lll(A.kernel(), y)
253
+
254
+ def svp_search(U: Matrix, m: int = 0, p: int = 2) -> Matrix:
255
+ "Solve the shortest vector problem in a q-ary lattice associated with a matrix over a finite ring Z_q."
256
+ gf = U[0].ring
257
+ if m:
258
+ ml, mu = -m, m + 1
259
+ else:
260
+ ml, mu = -((gf.n-1)//2), gf.n//2 + 1
261
+ U.map(int)
262
+ norm_min = inf
263
+ c_min = None
264
+ for j in range(U.cols):
265
+ for c in product(range(ml, mu), repeat = U.cols-j-1):
266
+ for cc in range(1, mu):
267
+ cc = U * Matrix([ 0 ] * j + [ cc ] + list(c))
268
+ cc.map(gf)
269
+ norm = cc.norm(p)
270
+ if norm < norm_min:
271
+ norm_min = norm
272
+ c_min = cc
273
+ U.map(gf)
274
+ return c_min
275
+
276
+ def cvp_search(U: Matrix, b: Matrix, p: int = 2) -> Matrix:
277
+ "Solve the closest vector problem in a q-ary lattice associated with a matrix over a finite ring Z_q (LWE)."
278
+ gf = U[0].ring
279
+ U.map(int)
280
+ b.map(int)
281
+ norm_min = inf
282
+ c_min = None
283
+ for c in product(range(-((gf.n-1)//2), gf.n//2 + 1), repeat = U.cols):
284
+ c = U * Matrix(c)
285
+ d = c - b
286
+ d.map(gf)
287
+ norm = d.norm(p)
288
+ if norm < norm_min:
289
+ norm_min = norm
290
+ c.map(gf)
291
+ c_min = c
292
+ U.map(gf)
293
+ b.map(gf)
294
+ return c_min
295
+
296
+ def sis_search(A: Matrix, p: int = 2) -> Matrix:
297
+ "Solve the short integer problem over a finite ring Z_q (SIS)."
298
+ return svp_search(A.kernel(), p = p)
299
+
300
+ def isis_search(A: Matrix, b: Matrix, p: int = 2) -> Matrix:
301
+ "Solve the inhomogenous short integer problem over a finite ring Z_q (ISIS)."
302
+ y = A.solve(b)
303
+ if y is None:
304
+ return None
305
+ return y - cvp_search(A.kernel(), y, p = p)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kryptools
3
- Version: 1.7
3
+ Version: 1.8
4
4
  Summary: Implemenation of same basic algorithms used in cryptography.
5
5
  Author-email: Gerald Teschl <gerald.teschl@univie.ac.at>
6
6
  License: MIT
@@ -44,7 +44,7 @@ The tools contained are:
44
44
  * integer factorization (Fermat, Pollard p-1, Pollard rho, Lenstra's ECM, Dixon, basic quadratic sieve)
45
45
  * elliptic curves (Weierstrass form), group operations, order, discrete logarithms
46
46
  * linear algebra: Hermite normal form, Gram-Schmidt
47
- * lattices: Hadamard ratio, Babai rounding/nearest plane algorithm, lattice reduction (Lenstra-Lenstra-Lovaz)
47
+ * lattices: Hadamard ratio, Babai rounding/nearest plane algorithm, lattice reduction (Lenstra-Lenstra-Lovaz), SIS, ISIS, LWE
48
48
  * linear codes: Hamming distance, left standard form, parity check matrix
49
49
 
50
50
  * Matrix: a class for Matrices (inverse, det, reduced echelon form, kernel, etc.)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryptools"
3
- version = "1.7"
3
+ version = "1.8"
4
4
  authors = [
5
5
  { name="Gerald Teschl", email="gerald.teschl@univie.ac.at" },
6
6
  ]
@@ -45,10 +45,11 @@ def test_Zmod_methods():
45
45
  assert [abs(i) for i in Z_5] == [0, 1, 2, 2, 1]
46
46
  assert not Z_5(0)
47
47
  assert Z_5(1)
48
- assert Z_5(2) == Z_5(2)
48
+ assert Z_5(2) == Z_5(7)
49
49
  assert Z_5(2) != Z_5(3)
50
50
  assert Z_5(2) != 2
51
51
  assert str(Z_5(3)) == "3"
52
+ assert Z_5(2).solve(3) == 3 * Z_5(2)**-1
52
53
  Z_5.short = False
53
54
  assert str(Z_5(6)) == "1 (mod 5)"
54
55
 
@@ -67,10 +68,12 @@ def test_Zmod_methods():
67
68
  assert [abs(i) for i in Z_6] == [0, 1, 2, 3, 2, 1]
68
69
  assert not Z_6(0)
69
70
  assert Z_6(1)
70
- assert Z_6(2) == Z_6(2)
71
+ assert Z_6(2) == Z_6(8)
71
72
  assert Z_6(2) != Z_6(3)
72
73
  assert Z_5(2) != Z_6(2)
73
74
  assert str(Z_6(3)) == "3"
75
+ assert Z_6(2).solve(4) == Z_6(2)
76
+ assert Z_6(2).solve(3) is None
74
77
 
75
78
 
76
79
  def test_Zmod_order():
@@ -1,7 +1,7 @@
1
1
  # pragma pylint: disable=C0114,C0116
2
2
  import pytest # pylint: disable=W0611
3
- from fractions import Fraction # pylint: disable=C0411
4
3
  from random import randint, seed # pylint: disable=C0411
4
+ from fractions import Fraction # pylint: disable=C0411
5
5
  from kryptools import Matrix, Zmod, GF2, eye, circulant, BinaryMatrix
6
6
 
7
7
 
@@ -84,7 +84,7 @@ def test_BinaryMatrix():
84
84
  assert BinaryMatrix(Mat.inv()) == mat.inv()
85
85
  assert Mat.rank() == mat.rank()
86
86
  sol = mat.solve(b)
87
- Sol = Mat.solve(b, ring = Z_2)
87
+ Sol = Mat.solve(Z_2(b))
88
88
  assert (sol is None and Sol is None) or list(Sol.applyfunc(int)) == sol
89
89
  if mat.rows == 1:
90
90
  assert int(Mat * X) == mat.apply(x)[0]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes