kryptools 1.4__tar.gz → 1.5__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.4 → kryptools-1.5}/PKG-INFO +2 -2
  2. {kryptools-1.4 → kryptools-1.5}/README.md +1 -1
  3. {kryptools-1.4 → kryptools-1.5}/kryptools/GF2.py +29 -17
  4. {kryptools-1.4 → kryptools-1.5}/kryptools/Zmod.py +22 -3
  5. {kryptools-1.4 → kryptools-1.5}/kryptools/__init__.py +1 -1
  6. {kryptools-1.4 → kryptools-1.5}/kryptools/code.py +7 -5
  7. {kryptools-1.4 → kryptools-1.5}/kryptools/poly.py +189 -65
  8. {kryptools-1.4 → kryptools-1.5}/kryptools.egg-info/PKG-INFO +2 -2
  9. {kryptools-1.4 → kryptools-1.5}/pyproject.toml +1 -1
  10. {kryptools-1.4 → kryptools-1.5}/tests/test_GF2.py +1 -1
  11. {kryptools-1.4 → kryptools-1.5}/tests/test_poly.py +16 -2
  12. {kryptools-1.4 → kryptools-1.5}/LICENSE +0 -0
  13. {kryptools-1.4 → kryptools-1.5}/kryptools/aes.py +0 -0
  14. {kryptools-1.4 → kryptools-1.5}/kryptools/blockcipher.py +0 -0
  15. {kryptools-1.4 → kryptools-1.5}/kryptools/conway_polynomials.py +0 -0
  16. {kryptools-1.4 → kryptools-1.5}/kryptools/des.py +0 -0
  17. {kryptools-1.4 → kryptools-1.5}/kryptools/dlp.py +0 -0
  18. {kryptools-1.4 → kryptools-1.5}/kryptools/dlp_bsgs.py +0 -0
  19. {kryptools-1.4 → kryptools-1.5}/kryptools/dlp_ic.py +0 -0
  20. {kryptools-1.4 → kryptools-1.5}/kryptools/dlp_qs.py +0 -0
  21. {kryptools-1.4 → kryptools-1.5}/kryptools/dlp_rho.py +0 -0
  22. {kryptools-1.4 → kryptools-1.5}/kryptools/ec.py +0 -0
  23. {kryptools-1.4 → kryptools-1.5}/kryptools/factor.py +0 -0
  24. {kryptools-1.4 → kryptools-1.5}/kryptools/factor_dix.py +0 -0
  25. {kryptools-1.4 → kryptools-1.5}/kryptools/factor_ecm.py +0 -0
  26. {kryptools-1.4 → kryptools-1.5}/kryptools/factor_fmt.py +0 -0
  27. {kryptools-1.4 → kryptools-1.5}/kryptools/factor_pm1.py +0 -0
  28. {kryptools-1.4 → kryptools-1.5}/kryptools/factor_qs.py +0 -0
  29. {kryptools-1.4 → kryptools-1.5}/kryptools/factor_rho.py +0 -0
  30. {kryptools-1.4 → kryptools-1.5}/kryptools/intfuncs.py +0 -0
  31. {kryptools-1.4 → kryptools-1.5}/kryptools/keccak.py +0 -0
  32. {kryptools-1.4 → kryptools-1.5}/kryptools/la.py +0 -0
  33. {kryptools-1.4 → kryptools-1.5}/kryptools/lat.py +0 -0
  34. {kryptools-1.4 → kryptools-1.5}/kryptools/nt.py +0 -0
  35. {kryptools-1.4 → kryptools-1.5}/kryptools/primes.py +0 -0
  36. {kryptools-1.4 → kryptools-1.5}/kryptools/sha1.py +0 -0
  37. {kryptools-1.4 → kryptools-1.5}/kryptools.egg-info/SOURCES.txt +0 -0
  38. {kryptools-1.4 → kryptools-1.5}/kryptools.egg-info/dependency_links.txt +0 -0
  39. {kryptools-1.4 → kryptools-1.5}/kryptools.egg-info/top_level.txt +0 -0
  40. {kryptools-1.4 → kryptools-1.5}/setup.cfg +0 -0
  41. {kryptools-1.4 → kryptools-1.5}/tests/test_Zmod.py +0 -0
  42. {kryptools-1.4 → kryptools-1.5}/tests/test_aes.py +0 -0
  43. {kryptools-1.4 → kryptools-1.5}/tests/test_code.py +0 -0
  44. {kryptools-1.4 → kryptools-1.5}/tests/test_des.py +0 -0
  45. {kryptools-1.4 → kryptools-1.5}/tests/test_dlog.py +0 -0
  46. {kryptools-1.4 → kryptools-1.5}/tests/test_ec.py +0 -0
  47. {kryptools-1.4 → kryptools-1.5}/tests/test_factor.py +0 -0
  48. {kryptools-1.4 → kryptools-1.5}/tests/test_factor_fmt.py +0 -0
  49. {kryptools-1.4 → kryptools-1.5}/tests/test_factor_pm1.py +0 -0
  50. {kryptools-1.4 → kryptools-1.5}/tests/test_factor_qs.py +0 -0
  51. {kryptools-1.4 → kryptools-1.5}/tests/test_intfuncs.py +0 -0
  52. {kryptools-1.4 → kryptools-1.5}/tests/test_keccak.py +0 -0
  53. {kryptools-1.4 → kryptools-1.5}/tests/test_la.py +0 -0
  54. {kryptools-1.4 → kryptools-1.5}/tests/test_lat.py +0 -0
  55. {kryptools-1.4 → kryptools-1.5}/tests/test_nt.py +0 -0
  56. {kryptools-1.4 → kryptools-1.5}/tests/test_primes.py +0 -0
  57. {kryptools-1.4 → kryptools-1.5}/tests/test_sha1.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kryptools
3
- Version: 1.4
3
+ Version: 1.5
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
@@ -42,7 +42,7 @@ The tools contained are:
42
42
  * linear codes: Hamming distance, left standard form, parity check matrix
43
43
 
44
44
  * Matrix: a class for Matrices (inverse, det, reduced echelon form, etc.)
45
- * Poly: a class for polynomials (division, modulo, irreducibility test, Lagrange interpolation)
45
+ * Poly: a class for polynomials (division, modulo, factoring and irreducibility test over finite fields, Lagrange interpolation)
46
46
  * Zmod: a class for the ring of integers modulo an integer
47
47
  * GF2: a class for Galois fields GF(2^n)
48
48
  * Goppa: a class for encoding and decoding Goppa codes
@@ -24,7 +24,7 @@ The tools contained are:
24
24
  * linear codes: Hamming distance, left standard form, parity check matrix
25
25
 
26
26
  * Matrix: a class for Matrices (inverse, det, reduced echelon form, etc.)
27
- * Poly: a class for polynomials (division, modulo, irreducibility test, Lagrange interpolation)
27
+ * Poly: a class for polynomials (division, modulo, factoring and irreducibility test over finite fields, Lagrange interpolation)
28
28
  * Zmod: a class for the ring of integers modulo an integer
29
29
  * GF2: a class for Galois fields GF(2^n)
30
30
  * Goppa: a class for encoding and decoding Goppa codes
@@ -3,6 +3,7 @@ Galois field GF(2^n).
3
3
  """
4
4
 
5
5
  from math import gcd
6
+ from random import randint
6
7
  from .poly import Poly
7
8
  from .Zmod import Zmod
8
9
  from .factor import factorint
@@ -41,13 +42,14 @@ class GF2:
41
42
  if n in conway_polynomials2:
42
43
  modulus = conway_polynomials2[n]
43
44
  else:
44
- raise ValueError("Unknown power. Please specify a modulus.")
45
+ raise ValueError("Unknown degree. Please specify a modulus.")
45
46
  self.modulus = modulus # integer whose biniary digits are the coefficients of the irreducible modulus polynomial
46
47
  if modulus & 2**n == 2**n:
47
48
  self.bitreversed = False
48
49
  else:
49
50
  self.bitreversed = True
50
- self.power = n # n
51
+ self.degree = n # n
52
+ self.characteristic = 2
51
53
  self.order = 2**n # 2**n
52
54
  self.print_hex: bool = True
53
55
  self.byteorder: str = "big" # big|little
@@ -55,7 +57,7 @@ class GF2:
55
57
  self.factors = {} # factoring of the group order
56
58
 
57
59
  def __repr__(self):
58
- return f"GF(2^{self.power})"
60
+ return f"GF(2^{self.degree})"
59
61
 
60
62
  def __call__(self, x: int | list | tuple | range | map):
61
63
  if isinstance(x, list|tuple|range|map):
@@ -74,7 +76,7 @@ class GF2:
74
76
  return self.order == other.order and self.modulus == other.modulus
75
77
  return False
76
78
 
77
- def __contains__(self, other: "ZmodPoint") -> bool:
79
+ def __contains__(self, other: "GF2nPoint") -> bool:
78
80
  return isinstance(other, GF2nPoint) and self == other.field
79
81
 
80
82
  def _factors(self) -> dict:
@@ -83,12 +85,22 @@ class GF2:
83
85
  self.factors = factorint(self.mult_order)
84
86
  return self.factors
85
87
 
88
+ def zero(self) -> bool:
89
+ "Return zero."
90
+ return self(0)
91
+
86
92
  def one(self) -> bool:
87
93
  "Return one."
88
94
  if self.bitreversed:
89
95
  return self(self.order - 1)
90
96
  return self(1)
91
97
 
98
+ def random(self, num: int = 0) -> "GF2nPoint":
99
+ "Return a single random point or a list of random points."
100
+ if num:
101
+ return([self(randint(0,self.order-1)) for _ in range(num)])
102
+ return self(randint(0,self.order-1))
103
+
92
104
  def is_cyclic(self) -> bool:
93
105
  "GF(2^n)^* is cyclic."
94
106
  return True
@@ -99,7 +111,7 @@ class GF2:
99
111
 
100
112
  def generator(self) -> int | None:
101
113
  "Return a generator of the group GF(2^n)^*."
102
- if self.power == 1:
114
+ if self.degree == 1:
103
115
  return self(1)
104
116
  for a in range(2, self.order):
105
117
  a = self(a)
@@ -131,7 +143,7 @@ class GF2nPoint:
131
143
 
132
144
  def __repr__(self):
133
145
  if self.field.print_hex:
134
- return format(self.x, "0"+str(self.field.power // 4)+"x")
146
+ return format(self.x, "0"+str(self.field.degree // 4)+"x")
135
147
  return self.x
136
148
 
137
149
  def __eq__(self, other):
@@ -146,16 +158,16 @@ class GF2nPoint:
146
158
  return self.x
147
159
 
148
160
  def __bytes__(self):
149
- return self.x.to_bytes(self.field.power // 8, byteorder=self.field.byteorder)
161
+ return self.x.to_bytes(self.field.degree // 8, byteorder=self.field.byteorder)
150
162
 
151
163
  def bits(self) -> list:
152
164
  "Convert to a list of bits."
153
165
  if self.field.bitreversed:
154
166
  coeff = [int(d) for d in str(
155
- format(self.x, "0" + str(self.field.power) + "b"))]
167
+ format(self.x, "0" + str(self.field.degree) + "b"))]
156
168
  else:
157
169
  coeff = reversed([int(d) for d in str(
158
- format(self.x, "0" + str(self.field.power) + "b"))])
170
+ format(self.x, "0" + str(self.field.degree) + "b"))])
159
171
  return list(coeff)
160
172
 
161
173
  def poly(self) -> "Poly":
@@ -163,14 +175,14 @@ class GF2nPoint:
163
175
  Z_2 = Zmod(2)
164
176
  if self.field.bitreversed:
165
177
  coeff = [Z_2(int(d)) for d in str(
166
- format(self.x, "0" + str(self.field.power) + "b"))]
178
+ format(self.x, "0" + str(self.field.degree) + "b"))]
167
179
  coeff_modulus = [Z_2(int(d)) for d in str(
168
- format((self.field.modulus << 1) + 1, "0" + str(self.field.power) + "b"))]
180
+ format((self.field.modulus << 1) + 1, "0" + str(self.field.degree) + "b"))]
169
181
  else:
170
182
  coeff = reversed([Z_2(int(d)) for d in str(
171
- format(self.x, "0" + str(self.field.power) + "b"))])
183
+ format(self.x, "0" + str(self.field.degree) + "b"))])
172
184
  coeff_modulus = reversed([Z_2(int(d)) for d in str(
173
- format(self.field.modulus, "0" + str(self.field.power) + "b"))])
185
+ format(self.field.modulus, "0" + str(self.field.degree) + "b"))])
174
186
  return Poly(list(coeff), modulus=list(coeff_modulus))
175
187
 
176
188
  def __hash__(self):
@@ -208,7 +220,7 @@ class GF2nPoint:
208
220
  z = 0
209
221
  y = other.x
210
222
  if self.field.bitreversed:
211
- for d in bin(self.x)[2:].zfill(self.field.power):
223
+ for d in bin(self.x)[2:].zfill(self.field.degree):
212
224
  if int(d):
213
225
  z ^= y
214
226
  if y % 2:
@@ -219,7 +231,7 @@ class GF2nPoint:
219
231
  else:
220
232
  bitmap1 = self.field.order
221
233
  bitmap2 = 1
222
- for _ in range(self.field.power):
234
+ for _ in range(self.field.degree):
223
235
  if self.x & bitmap2:
224
236
  z ^= y
225
237
  bitmap2 <<= 1
@@ -300,11 +312,11 @@ class GF2nPoint:
300
312
 
301
313
  def sbox(self, inv: bool = False) -> "GF2nPoint":
302
314
  "Apply the AES sbox."
303
- if self.field.power == 8 and self.field.modulus == 0b100011011: # AES
315
+ if self.field.degree == 8 and self.field.modulus == 0b100011011: # AES
304
316
  if inv:
305
317
  return aes_sbox_inv[self.x]
306
318
  return aes_sbox[self.x]
307
- elif self.field.power == 4 and self.field.modulus == 0b10011: # MiniAES
319
+ elif self.field.degree == 4 and self.field.modulus == 0b10011: # MiniAES
308
320
  if inv:
309
321
  return miniaes_sbox_inv[self.x]
310
322
  return miniaes_sbox[self.x]
@@ -3,6 +3,7 @@ Ring of intergers modulo `n`.
3
3
  """
4
4
 
5
5
  from math import gcd
6
+ from random import randint
6
7
  from .factor import factorint
7
8
  from .primes import is_prime
8
9
  from .nt import sqrt_mod, crt
@@ -31,9 +32,10 @@ class Zmod:
31
32
  if not isinstance(n, int) or n < 1:
32
33
  raise ValueError(f"{n} is not a positive integer.")
33
34
  self.n = n
35
+ self.isfield = None
34
36
  self.n_factors = {} # factoring of n
35
37
  self.short = short
36
- self.group_order = 0
38
+ self.group_order = 0 # order of the multiplicative group
37
39
  self.factors = {} # factoring of the multiplicative group order
38
40
 
39
41
  def __repr__(self):
@@ -56,6 +58,12 @@ class Zmod:
56
58
  def __contains__(self, other: "ZmodPoint") -> bool:
57
59
  return isinstance(other, ZmodPoint) and self.n == other.ring.n
58
60
 
61
+ def random(self, num: int = 0) -> "ZmodPoint":
62
+ "Return a single random point or a list of random points."
63
+ if num:
64
+ return([self(randint(0,self.n-1)) for _ in range(num)])
65
+ return self(randint(0,self.n-1))
66
+
59
67
  def order(self) -> int:
60
68
  "Compute the order of the group Z_n^*."
61
69
  if self.group_order:
@@ -63,7 +71,7 @@ class Zmod:
63
71
  # We compute euler_phi(n) and its factorization in one pass
64
72
  if not self.n_factors:
65
73
  self.n_factors = factorint(self.n)
66
- for p, k in self.n_factors.items(): # first factorize n
74
+ for p, k in self.n_factors.items(): # factorization of n
67
75
  # factor p-1 and add the factors
68
76
  for pm, km in factorint(p - 1).items():
69
77
  if pm in self.factors:
@@ -96,7 +104,18 @@ class Zmod:
96
104
 
97
105
  def is_field(self) -> bool:
98
106
  "Test if Z_n is a field."
99
- return is_prime(self.n)
107
+ if self.isfield is None:
108
+ if self.n_factors:
109
+ if len(self.n_factors) == 1:
110
+ self.isfield = True
111
+ else:
112
+ self.isfield = False
113
+ elif is_prime(self.n):
114
+ self.isfield = True
115
+ self.n_factors = { self.n: 1}
116
+ else:
117
+ self.isfield = False
118
+ return self.isfield
100
119
 
101
120
  def generator(self) -> int | None:
102
121
  "Return a generator of the group Z_n^*."
@@ -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.4"
8
+ __version__ = "1.5"
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
@@ -114,22 +114,24 @@ class Goppa():
114
114
  def decode(self, y: list[int]):
115
115
  "Decode a given list of bits."
116
116
  y = Matrix(y, ring = self.Z_2)
117
+ zero = self.gf(0)
118
+ one = zero**0
117
119
  if self.H * y: # no code word
118
- s = self.gf(0) # syndrom polynomial
120
+ s = zero # syndrom polynomial
119
121
  for i, x in enumerate(self.alpha):
120
- s += int(y[i]) * Poly([-x, 1], ring = self.gf, modulus = self.g).inv()
121
- v = (s.inv() - Poly([0, 1], ring = self.gf, modulus = self.g))**(self.gf.order ** self.g.degree() // 2)
122
+ s += int(y[i]) * Poly([-x, one], modulus = self.g).inv()
123
+ v = (s.inv() - Poly([zero, one], modulus = self.g))**(self.gf.order ** self.g.degree() // 2)
122
124
  # determine error locator polynomial sigma(x) = a(x^2) + b(x^2) * x
123
125
  # find a and b by running EEA until b has the desired degree
124
126
  v.modulus = None
125
127
  r0, r1 = self.g, v
126
- y0, y1 = Poly([self.gf(0)]), Poly([self.gf(1)])
128
+ y0, y1 = Poly([zero]), Poly([one])
127
129
  while 2 * r1.degree() > self.g.degree():
128
130
  q, r = r0.divmod(r1)
129
131
  r0, r1 = r1, r
130
132
  y0, y1 = y1, y0 - q * y1
131
133
  a, b = r1, y1
132
- sigma = a**2 + b**2 * Poly([0,1], ring = self.gf)
134
+ sigma = a**2 + b**2 * Poly([zero, one])
133
135
  # correct the error
134
136
  for x in self.alpha:
135
137
  if not sigma(x):
@@ -4,6 +4,7 @@ Polynomials
4
4
 
5
5
  from math import prod
6
6
  from numbers import Number
7
+ from random import randint
7
8
  from .primes import is_prime
8
9
  #from .factor import factorint
9
10
 
@@ -24,7 +25,7 @@ class Poly:
24
25
 
25
26
  def __init__(self, coeff: list, ring=None, modulus: list = None):
26
27
  self.coeff = list(coeff)
27
- for i in range(len(self.coeff) - 1, 0, -1):
28
+ for i in range(len(self.coeff) - 1, 0, -1): # strip leading zeros
28
29
  if self.coeff[i]:
29
30
  break
30
31
  self.coeff.pop(i)
@@ -49,6 +50,9 @@ class Poly:
49
50
  def __len__(self):
50
51
  return len(self.coeff)
51
52
 
53
+ def __hash__(self):
54
+ return hash(tuple(self.coeff))
55
+
52
56
  def __repr__(self, latex = False):
53
57
  def prx(i: int) -> str:
54
58
  if i == 0:
@@ -142,27 +146,34 @@ class Poly:
142
146
  ring = None
143
147
  return zero, one, ring
144
148
 
149
+ def _guess_field(self):
150
+ zero, one, field = self._guess_ring()
151
+ if not field:
152
+ raise ValueError("The polynomial does not seem to be over a known finite field.")
153
+ if hasattr(field, 'n'): # Zmod
154
+ p = field.n
155
+ q = field.n
156
+ if not field.is_field():
157
+ raise ValueError("The polynomial does not seem to be over a finite field.")
158
+ else: # G2 or galois
159
+ p = field.characteristic
160
+ q = field.order
161
+ return field, zero, one, q, p
162
+
145
163
  def __add__(self, other: "Poly") -> "Poly":
146
- zero = 0 * self.coeff[0]
147
164
  if not isinstance(other, self.__class__):
148
165
  if self._check_type(other):
149
166
  tmp = self.coeff[:]
150
167
  tmp[0] += other
151
168
  return self.__class__(tmp, modulus=self.modulus)
152
169
  return NotImplemented
153
- ls, lo = len(self.coeff), len(other.coeff)
154
- if ls < lo:
155
- scoeff = self.coeff + (lo - ls) * [zero]
156
- else:
157
- scoeff = self.coeff
158
- if ls > lo:
159
- ocoeff = other.coeff + (ls - lo) * [zero]
160
- else:
161
- ocoeff = other.coeff
162
170
  modulus = self.modulus
163
171
  if not modulus and other.modulus:
164
172
  modulus = other.modulus
165
- return self.__class__([s + o for s, o in zip(scoeff, ocoeff)], modulus=modulus)
173
+ ls, lo = len(self.coeff), len(other.coeff)
174
+ if ls < lo:
175
+ return self.__class__([ s + o for s, o in zip(self.coeff, other.coeff) ] + other.coeff[ls:], modulus=modulus)
176
+ return self.__class__([ s + o for s, o in zip(self.coeff, other.coeff) ] + self.coeff[lo:], modulus=modulus)
166
177
 
167
178
  def __radd__(self, other: "Poly") -> "Poly":
168
179
  return self + other
@@ -186,9 +197,7 @@ class Poly:
186
197
  return NotImplemented
187
198
  zero = 0 * self.coeff[0]
188
199
  ls, lo = len(self.coeff), len(other.coeff)
189
- coeff = [None] * (ls + lo - 1)
190
- for k in range(ls + lo - 1):
191
- coeff[k] = sum((self.coeff[j] * other.coeff[k - j] for j in range(max(0, k - lo + 1), min(ls, k + 1))), start=zero)
200
+ coeff = [ sum((self.coeff[j] * other.coeff[k - j] for j in range(max(0, k - lo + 1), min(ls, k + 1))), start=zero) for k in range(ls + lo - 1) ]
192
201
  modulus = self.modulus
193
202
  if not modulus and other.modulus:
194
203
  modulus = other.modulus
@@ -308,7 +317,7 @@ class Poly:
308
317
  tmp = self.coeff[sd - i] * one # "* 1" is here to make sure we get a copy
309
318
  for j in range(od + 1):
310
319
  self.coeff[sd - i - j] -= tmp * oth[od - j]
311
- for i in range(len(self.coeff) - 1, 0, -1):
320
+ for i in range(len(self.coeff) - 1, 0, -1): # strip leading zeros
312
321
  if self.coeff[i]:
313
322
  break
314
323
  self.coeff.pop(i)
@@ -318,53 +327,60 @@ class Poly:
318
327
  if not other:
319
328
  other = self.modulus
320
329
  zero, one, ring = self._guess_ring()
321
- if isinstance(other, list):
322
- other = self.__class__(other, ring=ring)
323
- elif not isinstance(other, self.__class__):
324
- raise NotImplementedError(f"Cannot invert {self} modulo {other}.")
330
+ if isinstance(other, self.__class__):
331
+ other = other.coeff
332
+ elif not isinstance(other, list):
333
+ raise NotImplementedError(f"{other} must be a list of coefficients or a polynomial.")
334
+ other = Poly(other, ring = ring)
325
335
  if not other:
326
336
  raise NotImplementedError(f"{other} must be nonzero.")
327
337
  r0, r1 = other, self
328
- y0, y1 = self.__class__([zero], modulus=self.modulus), self.__class__([one], modulus=self.modulus)
338
+ y0, y1 = self.__class__([zero]), self.__class__([one])
329
339
  while r1:
330
340
  q, r = r0.divmod(r1)
331
341
  r0, r1 = r1, r
332
342
  y0, y1 = y1, y0 - q * y1
333
343
  if r0.degree() != 0:
334
344
  raise ValueError(f"{self} is not invertible mod {other}.")
335
- tmp = one / r0[0]
336
- for i in range(len(y0)):
337
- y0.coeff[i] *= tmp
338
- return y0
345
+ y0.modulus = self.modulus
346
+ return y0 / r0.coeff[0]
339
347
 
340
348
  def gcd(self, other: "Poly") -> "Poly":
341
349
  "Greates common divisor with a given polynomial."
342
350
  ring = self._guess_ring()[-1]
343
- if isinstance(other, list):
344
- other = self.__class__(other, ring=ring)
345
- elif not isinstance(other, self.__class__):
351
+ if not isinstance(other, self.__class__):
346
352
  raise NotImplementedError(f"Cannot compute gcd: {other} must be a polynomial.")
353
+ if not self:
354
+ if not other:
355
+ return Poly([self.coeff[0]])
356
+ tmp = other / other.coeff[-1]
357
+ tmp.modulus = None
358
+ return tmp
347
359
  r0, r1 = other, self
348
360
  while r1:
349
361
  r0, r1 = r1, r0.divmod(r1)[1]
350
- return r0
362
+ r0.modulus = None
363
+ return r0 / r0.coeff[-1]
351
364
 
352
365
  def egcd(self, other: "Poly") -> ("Poly", "Poly", "Poly"):
353
366
  """Perform the extended Euclidean agorithm for polynomials. Returns gcd, x, y such that other * x + self * y = gcd."""
354
367
  zero, one, ring = self._guess_ring()
355
- if isinstance(other, list):
356
- other = self.__class__(other, ring=ring)
357
- elif not isinstance(other, self.__class__):
368
+ if not isinstance(other, self.__class__):
358
369
  raise NotImplementedError(f"Cannot perform egcd: {other} must be a polynomial.")
370
+ if not self:
371
+ if not other:
372
+ return Poly([zero]), Poly([zero]), Poly([zero])
373
+ return other / other.coeff[-1], Poly([zero]), one / other.coeff[-1]
359
374
  r0, r1 = other, self
360
- x0, x1 = self.__class__([one], modulus=self.modulus), self.__class__([zero], modulus=self.modulus)
361
- y0, y1 = self.__class__([zero], modulus=self.modulus), self.__class__([one], modulus=self.modulus)
375
+ x0, x1 = self.__class__([one]), self.__class__([zero])
376
+ y0, y1 = self.__class__([zero]), self.__class__([one])
362
377
  while r1:
363
378
  q, r = r0.divmod(r1)
364
379
  r0, r1 = r1, r
365
- x0, x1 = x1, x0 - q * x1
366
- y0, y1 = y1, y0 - q * y1
367
- return r0, x0, y0
380
+ x0, x1 = x1, x0 - x1 * q
381
+ y0, y1 = y1, y0 - y1 * q
382
+ lcr0 = r0.coeff[-1]
383
+ return r0 / lcr0, x0 / lcr0, y0 / lcr0
368
384
 
369
385
  def derivative(self) -> "Poly":
370
386
  "Returns the formal derivative."
@@ -375,39 +391,147 @@ class Poly:
375
391
  tmp = [ j * self.coeff[j] for j in range(1, l) ]
376
392
  return self.__class__(tmp, modulus=self.modulus)
377
393
 
378
- def rabin_test(self) -> bool:
379
- """Determine if a polynomial is irreducible over a Galois field using a Rabin test."""
380
- ring = self._guess_ring()[-1]
381
- if not ring:
382
- raise ValueError("The polynomial does not seem to be over a known finite field.")
383
- try:
384
- q = ring.n # Zmod
385
- isfield = is_prime(q)
386
- except:
387
- q = ring.order # GF2
388
- isfield = True
389
- if not isfield:
390
- raise ValueError("The polynomial does not seem to be over a finite field.")
391
- #n = self.degree()
392
- #x = self.__class__([0, 1], ring=ring, modulus = self.coeff) # x
393
- #for p in factorint(n).keys():
394
- # g = self.gcd(x**(q**(n//p)) - x) # gcd with x^{q/n} - x
395
- # if g.degree():
396
- # return False
397
- #g = x**(q**n) - x
398
- #return not(g)
399
- x = Poly([0, 1], ring = ring) # x
400
- xq = Poly(q * [0] + [1], ring = ring, modulus = self.coeff) # x^q modulo self to keep the polynomials small
401
- for k in range(1, self.degree() // 2 + 1):
394
+ def square_free_factors(self) -> dict:
395
+ "Determine the square free factors of a polynomial over a Galois field."
396
+ _, zero, _, q, p = self._guess_field()
397
+ pr = q // p # x^pr will be the p'th root of x
398
+ if self.degree() < 1:
399
+ return {}
400
+ if self.degree() == 1:
401
+ return { self / self.coeff[-1]: 1}
402
+ factors = {}
403
+ w = self / self.coeff[-1]
404
+ c = w.gcd(w.derivative())
405
+ w = w // c
406
+
407
+ # first we find all factors which are not a power of p
408
+ i = 1
409
+ while w.degree() > 0:
410
+ y = w.gcd(c)
411
+ fac = w // y
412
+ if fac.degree() > 0:
413
+ factors[fac] = i
414
+ w = y
415
+ c //= y
416
+ i += 1
417
+ # c is now a power of p
418
+ if c.degree() > 0:
419
+ # compute the p'th root of c
420
+ d = c.degree() // p
421
+ root = [ zero ] * (d + 1)
422
+ while d >= 0:
423
+ root[d] = c.coeff[d * p]**pr
424
+ d -= 1
425
+ #assert Poly(root)**p == c, root
426
+ for fac, mult in Poly(root).square_free_factors().items():
427
+ factors[fac] = p * mult
428
+ return factors
429
+
430
+ def is_square_free(self) -> bool:
431
+ "Test if the polynomial over a Galois field is square free."
432
+ multiplicities = self.square_free_factors().values()
433
+ return len(multiplicities) == 1 and multiplicities[0] == 1
434
+
435
+ def distinct_degree_factors(self, test:bool = False) -> bool|dict:
436
+ "Factors the polynomial over a Galois field into distinct products of irredicible factors of the same degree."
437
+ _, zero, one, q, _ = self._guess_field()
438
+ if self.degree() < 1:
439
+ raise ValueError("The polynomial must be non-constant.")
440
+ factors = {}
441
+ w = self
442
+ x = Poly([zero, one]) # x
443
+ xq = Poly(q * [zero] + [one], modulus = self.coeff) # x^q modulo self to keep the polynomials small
444
+ k = 1
445
+ while k <= w.degree()//2:
402
446
  if k == 1:
403
447
  b = xq
404
448
  else:
405
449
  b **= q # we compute x^{q^k} mod a recursively
406
- g = self.gcd(b - x) # gcd with x^{q^k} - x
450
+ g = w.gcd(b - x) # gcd with x^{q^k} - x
407
451
  if g.degree():
408
- return False
409
- return True
452
+ if test:
453
+ return False
454
+ factors[k] = g
455
+ w = w // factors[k]
456
+ k += 1
457
+ if test:
458
+ return True
459
+ if w.degree() > 0:
460
+ factors[w.degree()] = w
461
+ return factors
462
+
463
+ def rabin_test(self) -> bool:
464
+ "Determine if a polynomial is irreducible over a Galois field using a Rabin test."
465
+ return self.distinct_degree_factors(test = True)
466
+
467
+ def is_irreducible(self) -> bool:
468
+ "Test if the polynomial over a Galois field is irreducibel."
469
+ if self.degree() == 0:
470
+ return False
471
+ return self.distinct_degree_factors(test = True)
410
472
 
473
+ def equal_degree_factors(self ,k:int) -> list:
474
+ "Factors a square free product of irreducibel polynomials of degree `k` over a Galois field."
475
+ field, zero, one, q, p = self._guess_field()
476
+ if hasattr(field, "degree"):
477
+ degree= field.degree
478
+ else:
479
+ degree = 1
480
+ d = self.degree()
481
+ if not isinstance(k, int) or k < 1 :
482
+ raise ValueError(f"The degree {k} must be a positive integer.")
483
+ if d < k or d % k:
484
+ raise ValueError(f"The polynomial is no product of irreducibel polynomials of degree {k}.")
485
+ r = d // k
486
+ qk = (q**k - 1) // 2
487
+ poly = [0] * k + [1]
488
+
489
+ factors = [ self ]
490
+ while len(factors) < r:
491
+ g = Poly([randint(0,q-1) for _ in range(d+1)], ring = field, modulus = self)
492
+ if p == 2: # Gathen-Shoup
493
+ h = g
494
+ for i in range(1,k):
495
+ g += h**q
496
+ if degree > 1:
497
+ h = g
498
+ for i in range(1,degree):
499
+ g += g**2
500
+ else: # Cantor–Zassenhaus
501
+ g **= qk
502
+ factors2 = [] # list for the next round (to avoid the the new factors are tested a second time with the same g)
503
+ for fac in factors:
504
+ if fac.degree() == k: # this one is already irreducible
505
+ factors2.append(fac)
506
+ continue
507
+ gg = fac.gcd(g)
508
+ if gg.degree() == 0 or gg.degree() == fac.degree(): # trivial factor
509
+ factors2.append(fac)
510
+ continue
511
+ gg.modulus = None
512
+ factors2.append(gg)
513
+ factors2.append(fac // gg)
514
+ factors = factors2
515
+ return factors
516
+
517
+ def factor(self) -> dict:
518
+ "Factors a polynomials over a Galois field."
519
+ if self.degree() < 1:
520
+ return {self: 1}
521
+ c = Poly([self.coeff[-1]])
522
+ one = c**0
523
+ if c != one:
524
+ factors = {c: 1}
525
+ else:
526
+ factors = {}
527
+ for fac1, m in self.square_free_factors().items():
528
+ for k, fac2 in fac1.distinct_degree_factors().items():
529
+ if k == fac2.degree(): # irreducible
530
+ factors[fac2] = m
531
+ else:
532
+ for fac3 in fac2.equal_degree_factors(k):
533
+ factors[fac3] = m
534
+ return dict(sorted(factors.items(), key=lambda item: item[0].degree()))
411
535
 
412
536
  def lagrange_interpolation(x_coordinates:list, y_coordinates:list) -> "Poly":
413
537
  "Compute the Lagrange interpolation polynomial from a given list of x and y values."
@@ -416,7 +540,7 @@ def lagrange_interpolation(x_coordinates:list, y_coordinates:list) -> "Poly":
416
540
  raise ValueError('List of x and y values must be nonempty of equal length.')
417
541
  if l != len(set(x_coordinates)):
418
542
  raise ValueError('List of x values must not contain duplicate values.')
419
- # Guess zero and one
543
+ # guess zero and one
420
544
  zero = 0 * x_coordinates[0]
421
545
  one = zero**0
422
546
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kryptools
3
- Version: 1.4
3
+ Version: 1.5
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
@@ -42,7 +42,7 @@ The tools contained are:
42
42
  * linear codes: Hamming distance, left standard form, parity check matrix
43
43
 
44
44
  * Matrix: a class for Matrices (inverse, det, reduced echelon form, etc.)
45
- * Poly: a class for polynomials (division, modulo, irreducibility test, Lagrange interpolation)
45
+ * Poly: a class for polynomials (division, modulo, factoring and irreducibility test over finite fields, Lagrange interpolation)
46
46
  * Zmod: a class for the ring of integers modulo an integer
47
47
  * GF2: a class for Galois fields GF(2^n)
48
48
  * Goppa: a class for encoding and decoding Goppa codes
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "kryptools"
3
- version = "1.4"
3
+ version = "1.5"
4
4
  authors = [
5
5
  { name="Gerald Teschl", email="gerald.teschl@univie.ac.at" },
6
6
  ]
@@ -13,7 +13,7 @@ def test_GF2_ops():
13
13
  assert gf(1) == gf(1)
14
14
  assert not gf(1) == gf(0)
15
15
  assert not gf(0) == 0
16
- if gf.power < 17:
16
+ if gf.degree < 17:
17
17
  assert len(list(gf)) == gf.order
18
18
  assert len(list(gf.star())) == gf.order - 1
19
19
  assert len(list(gf.generators())) == euler_phi(gf.order - 1)
@@ -1,8 +1,9 @@
1
1
  import pytest
2
- from random import sample, choices
2
+ from math import prod
3
+ from random import sample, choices, randint, seed
3
4
  from kryptools import Poly, Zmod, GF2, lagrange_interpolation
4
5
  from kryptools import divisors, moebius_mu
5
-
6
+ seed(0)
6
7
 
7
8
  def test_Poly():
8
9
  p = Poly([2, 0, 2])
@@ -36,6 +37,19 @@ def test_rabin():
36
37
  count += 1
37
38
  assert count == sum([moebius_mu(d) * order ** (t // d) for d in divisors(t)]) // t
38
39
 
40
+ def test_factor():
41
+ for gf,t in [ [Zmod(7), 14], [GF2(4), 11]]:
42
+ order = len(list(gf))
43
+ one = Poly([gf(1)])
44
+ for _ in range(20):
45
+ p = one
46
+ while p.degree() < 1:
47
+ p = Poly([randint(0,order-1) for _ in range(t)], ring =gf)
48
+ factors = p.factor()
49
+ assert prod([f**k for f,k in factors.items()], start = one) == p, p
50
+ for fac in factors.keys():
51
+ assert fac.degree() == 0 or fac.rabin_test(), p
52
+
39
53
  def test_lagrange():
40
54
  for gf in [ Zmod(2), Zmod(3), Zmod(5), Zmod(7), Zmod(23), GF2(4), GF2(6)]:
41
55
  all = list(gf)
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