laicrypto 0.1.0

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 542a40d2e82129821225c13236509b3432870e6a32fd12b079800377c5a76a42
4
+ data.tar.gz: f0240e59391207efcad435482237b2af8be2acb8c5d07f35037838d8ad4c6545
5
+ SHA512:
6
+ metadata.gz: 55d0e02c98ad5824b0f2c89b808fcf5ed51476f6a058b891324773aa92ef2553da1ddd7536735e5fc872c7b4eae66026035681d3e11b7ac1bd81ff16eb1880a0
7
+ data.tar.gz: ad07b4cb86dcca560db066527eaef153a2de18ae67acd4c7709a69644db395b407c41d2a7bdd65d66247455b9a78d3181819c5535c9222ac8de62d1e2808f534
data/README.md ADDED
@@ -0,0 +1,338 @@
1
+ # pqcrypto
2
+
3
+ <img src="https://github.com/4211421036/pqcrypto/blob/main/logo.png" />
4
+
5
+ **Post-Quantum Lemniscate-AGM Isogeny (LAI) Encryption**
6
+
7
+ A Python package providing a reference implementation of the Lemniscate-AGM Isogeny (LAI) encryption scheme. LAI is a promising post-quantum cryptosystem based on isogenies of elliptic curves over lemniscate lattices, offering resistance against quantum-capable adversaries.
8
+
9
+ ---
10
+
11
+ ## Project Overview
12
+
13
+ This library implements the core mathematical primitives and high-level API of the LAI scheme, including:
14
+
15
+ * **Key Generation**: Derivation of a private scalar and corresponding public point via binary exponentiation of the LAI transformation.
16
+ * **Encryption**: Secure encryption of integer messages modulo a prime.
17
+ * **Decryption**: Accurate recovery of plaintext via inverse transform.
18
+
19
+ The code is annotated with direct correspondence to the mathematical definitions and pseudocode, making it suitable for research, educational use, and further development.
20
+
21
+ ---
22
+
23
+ ## Mathematical Formulation
24
+
25
+ ### 1. Hash-Based Seed Function
26
+
27
+ Define:
28
+
29
+ $$
30
+ H(x, y, s) \;=\; \mathrm{SHA256}\bigl(x \,\|\, y \,\|\, s\bigr) \bmod p
31
+ $$
32
+
33
+ where \$x,y,s \in \mathbb{Z}\_p\$ and \$|\$ denotes byte-string concatenation.
34
+
35
+ ### 2. Modular Square Root (Tonelli–Shanks)
36
+
37
+ Compute \$z = \sqrt{a} \bmod p\$ for prime \$p\$:
38
+
39
+ * If \$p \equiv 3 \pmod{4}\$:
40
+ $z \;=\; a^{\frac{p+1}{4}} \bmod p$
41
+ * Otherwise, use the full Tonelli–Shanks algorithm for general primes.
42
+
43
+ ### 3. LAI Transformation \$T\$
44
+
45
+ Given a point \$(x,y) \in \mathbb{F}\_p^2\$, parameter \$a\$, and seed index \$s\$, define:
46
+
47
+ $$
48
+ \begin{aligned}
49
+ h &= H(x,y,s), \[4pt]
50
+ x' &= \frac{x + a + h}{2} \bmod p, \[4pt]
51
+ y' &= \sqrt{x \, y + h} \bmod p.
52
+ \end{aligned}
53
+ $$
54
+
55
+ Thus,
56
+
57
+ $T\bigl((x,y), s; a, p\bigr) = (\,x', y').$
58
+
59
+ ### 4. Binary Exponentiation of \$T\$
60
+
61
+ To compute \$T^k(P\_0)\$ efficiently, use exponentiation by squaring:
62
+
63
+ ```text
64
+ function pow_T(P, k):
65
+ result ← P
66
+ base ← P
67
+ s ← 1
68
+ while k > 0:
69
+ if (k mod 2) == 1:
70
+ result ← T(result, s)
71
+ base ← T(base, s)
72
+ k ← k >> 1
73
+ s ← s + 1
74
+ return result
75
+ ```
76
+
77
+ ### 5. API Algorithms
78
+
79
+ **Key Generation**
80
+
81
+ ```text
82
+ function keygen(p, a, P0):
83
+ k ← random integer in [1, p−1]
84
+ Q ← pow_T(P0, k)
85
+ return (k, Q)
86
+ ```
87
+
88
+ **Encryption**
89
+
90
+ ```text
91
+ function encrypt(m, Q, p, a, P0):
92
+ r ← random integer in [1, p−1]
93
+ C1 ← pow_T(P0, r)
94
+ Sr ← pow_T(Q, r)
95
+ M ← (m mod p, 0)
96
+ C2 ← ( (M.x + Sr.x) mod p,
97
+ (M.y + Sr.y) mod p )
98
+ return (C1, C2)
99
+ ```
100
+
101
+ **Decryption**
102
+
103
+ ```text
104
+ function decrypt(C1, C2, k, a, p):
105
+ S ← pow_T(C1, k)
106
+ M.x ← (C2.x − S.x) mod p
107
+ return M.x
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Features
113
+
114
+ 1. **Pure Python** implementation: no external dependencies for core routines (uses `hashlib` & `secrets`).
115
+ 2. **Mathematically Annotated**: formulas and pseudocode directly reference the original scheme.
116
+ 3. **Modular Design**: separation of primitives (`H`, `sqrt_mod`, `T`) and high-level API (`keygen`, `encrypt`, `decrypt`).
117
+ 4. **General & Optimized**: Tonelli–Shanks for any prime, plus branch for \$p\equiv3\pmod4\$.
118
+ 5. **Automated Testing**: `pytest` suite for end-to-end verification.
119
+ 6. **CI/CD Ready**: PyPI publication via GitHub Actions.
120
+
121
+ ---
122
+
123
+ ## Installation
124
+
125
+ ### From PyPI
126
+
127
+ ```bash
128
+ pip install pqcrypto
129
+ ```
130
+
131
+ ### From NPM
132
+ ```bash
133
+ npm install pqlaicrypto
134
+ ```
135
+
136
+ ### From Ruby GEMFILE
137
+ ```bash
138
+ gem build laicrypto.gemspec
139
+ gem install ./laicrypto-0.1.0.gem
140
+ ```
141
+
142
+ ### From Source
143
+
144
+ ```bash
145
+ git clone https://github.com/4211421036/pqcrypto.git
146
+ cd pqcrypto
147
+ pip install .
148
+ ```
149
+
150
+ ---
151
+
152
+ ## Usage Example
153
+
154
+ Python
155
+ ```python
156
+ import math
157
+
158
+ from pqcrypto import keygen, encrypt, decrypt
159
+
160
+ p = 10007
161
+ a = 5
162
+ P0 = (1, 0)
163
+
164
+ def max_block_size(p: int) -> int:
165
+ bit_len = p.bit_length()
166
+ return (bit_len - 1) // 8
167
+
168
+ def text_to_int_blocks(text: str, p: int) -> list[int]:
169
+ raw_bytes = text.encode("utf-8")
170
+ B = max_block_size(p)
171
+ if B < 1:
172
+ raise ValueError("Prime p terlalu kecil untuk menyimpan satu byte pun.")
173
+
174
+ blocks = []
175
+ # Hitung jumlah blok
176
+ n_blocks = math.ceil(len(raw_bytes) / B)
177
+ for i in range(n_blocks):
178
+ start = i * B
179
+ end = start + B
180
+ chunk = raw_bytes[start:end]
181
+ m_int = int.from_bytes(chunk, byteorder="big")
182
+ if m_int >= p:
183
+ raise ValueError("Blok integer melebihi modulus p.")
184
+ blocks.append(m_int)
185
+
186
+ return blocks
187
+
188
+
189
+ def int_blocks_to_text(blocks: list[int], p: int) -> str:
190
+ all_bytes = bytearray()
191
+ for m_int in blocks:
192
+ if not (0 <= m_int < p):
193
+ raise ValueError(f"Integer block {m_int} di luar range [0, p).")
194
+ if m_int == 0:
195
+ chunk_bytes = b"\x00"
196
+ else:
197
+ byte_len = math.ceil(m_int.bit_length() / 8)
198
+ chunk_bytes = m_int.to_bytes(byte_len, byteorder="big")
199
+ all_bytes.extend(chunk_bytes)
200
+
201
+ return all_bytes.decode("utf-8", errors="strict")
202
+
203
+ def encrypt_text(
204
+ text: str,
205
+ k: int,
206
+ public_Q: tuple[int, int],
207
+ p: int,
208
+ a: int,
209
+ P0: tuple[int, int],
210
+ ) -> list[dict]:
211
+ int_blocks = text_to_int_blocks(text, p)
212
+ ciphertext = []
213
+
214
+ for m_int in int_blocks:
215
+ # encrypt() sudah otomatis retry jika T^r gagal
216
+ C1, C2, r = encrypt(m_int, public_Q, k, p, a, P0)
217
+ ciphertext.append({
218
+ "C1": (C1[0], C1[1]),
219
+ "C2": (C2[0], C2[1]),
220
+ "r": r,
221
+ })
222
+
223
+ return ciphertext
224
+
225
+ def decrypt_text(
226
+ ciphertext: list[dict],
227
+ k: int,
228
+ p: int,
229
+ a: int,
230
+ ) -> str:
231
+ int_blocks = []
232
+ for block in ciphertext:
233
+ x1, y1 = block["C1"]
234
+ x2, y2 = block["C2"]
235
+ r = block["r"]
236
+ m_int = decrypt((x1, y1), (x2, y2), k, r, a, p)
237
+ int_blocks.append(m_int)
238
+
239
+ return int_blocks_to_text(int_blocks, p)
240
+
241
+ if __name__ == "__main__":
242
+ # 6.1. Generate keypair
243
+ private_k, public_Q = keygen(p, a, P0)
244
+ print("=== Key Generation ===")
245
+ print("Private k :", private_k)
246
+ print("Public Q :", public_Q)
247
+ print()
248
+
249
+ original_text = """
250
+ function hello(name) {
251
+ console.log("Hello, " + name + "!");
252
+ }
253
+ hello("LAI User");
254
+ """.strip()
255
+
256
+ print("=== Original Text ===")
257
+ print(original_text)
258
+ print()
259
+
260
+ ciphertext = encrypt_text(original_text, private_k, public_Q, p, a, P0)
261
+ print("=== Ciphertext (serialized) ===")
262
+ for i, blk in enumerate(ciphertext):
263
+ print(f"Block {i+1}: C1={blk['C1']}, C2={blk['C2']}, r={blk['r']}")
264
+
265
+ print()
266
+ recovered_text = decrypt_text(ciphertext, private_k, p, a)
267
+ print("=== Decrypted Text ===")
268
+ print(recovered_text)
269
+ print()
270
+
271
+ assert recovered_text == original_text, "Decryption mismatch!"
272
+ print("Round-trip successful! Teks tepat sama dengan semula.")
273
+
274
+ ```
275
+
276
+ JS
277
+ ```js
278
+ const {
279
+ keygen,
280
+ encrypt,
281
+ decrypt
282
+ } = require('pqlaicrypto');
283
+
284
+ const p = 23n;
285
+ const a = 5n;
286
+ const P0 = [3n, 10n];
287
+
288
+ const { k, Q } = keygen(p, a, P0);
289
+
290
+ const m = 7n;
291
+ const { C1, C2, r } = encrypt(m, Q, k, p, a, P0);
292
+
293
+ const decrypted = decrypt(C1, C2, k, r, a, p);
294
+ console.log('Pesan asli:', decrypted.toString());
295
+ ```
296
+
297
+ Ruby
298
+
299
+ ```rb
300
+ irb
301
+ > require "laicrypto"
302
+ > laicrypto.keygen(23, 5, [3,10])
303
+ ```
304
+
305
+ ---
306
+
307
+ ## API Reference
308
+
309
+ | Function | Description |
310
+ | ------------------------------------ | --------------------------------------- |
311
+ | `H(x, y, s, p) -> int` | Hash-based seed modulo \$p\$. |
312
+ | `sqrt_mod(a, p) -> int` | Modular square root via Tonelli–Shanks. |
313
+ | `T(point, s, a, p) -> (int, int)` | One LAI transform step. |
314
+ | `keygen(p, a, P0) -> (k, Q)` | Generate private key and public point. |
315
+ | `encrypt(m, Q, p, a, P0) -> (C1,C2)` | Encrypt integer message. |
316
+ | `decrypt(C1, C2, k, a, p) -> int` | Decrypt ciphertext to integer. |
317
+
318
+ ---
319
+
320
+ ## Testing
321
+
322
+ ```bash
323
+ pytest --disable-warnings -q
324
+ ```
325
+
326
+ ---
327
+
328
+ ## Contributing & Development
329
+
330
+ 1. Fork the repo
331
+ 2. Create branch: `git checkout -b feature/xyz`
332
+ 3. Implement changes with corresponding tests
333
+ 4. Run tests: `pytest`
334
+ 5. Submit Pull Request
335
+
336
+ Please follow PEP 8 and include unit tests for new functionality.
337
+
338
+ ---
@@ -0,0 +1,3 @@
1
+ module Laicrypto
2
+ VERSION = "0.1.0"
3
+ end
data/lib/laicrypto.rb ADDED
@@ -0,0 +1,163 @@
1
+ require "digest"
2
+ require_relative "laicrypto/version"
3
+
4
+ module Laicrypto
5
+ class Error < StandardError; end
6
+
7
+ # H(x, y, s, p) = SHA256(x || y || s) mod p
8
+ def self.H(x, y, s, p)
9
+ data = "#{x}|#{y}|#{s}"
10
+ digest = Digest::SHA256.digest(data)
11
+ digest_bn = digest.unpack1("H*").to_i(16)
12
+ digest_bn % p
13
+ end
14
+
15
+ # mod_pow(base, exp, mod): base^exp mod mod (exponentiation by squaring)
16
+ def self.mod_pow(base, exp, mod)
17
+ result = 1
18
+ b = base % mod
19
+ e = exp
20
+
21
+ while e > 0
22
+ result = (result * b) % mod if (e & 1) != 0
23
+ b = (b * b) % mod
24
+ e >>= 1
25
+ end
26
+
27
+ result
28
+ end
29
+
30
+ # sqrt_mod(a, p): Tonelli–Shanks to find sqrt modulo p (p prime). Return nil if no root.
31
+ def self.sqrt_mod(a, p)
32
+ a = a % p
33
+ return 0 if a == 0
34
+
35
+ # Legendre symbol: a^((p-1)//2) mod p
36
+ ls = mod_pow(a, (p - 1) / 2, p)
37
+ return nil if ls == p - 1
38
+
39
+ # Shortcut if p % 4 == 3
40
+ if p % 4 == 3
41
+ return mod_pow(a, (p + 1) / 4, p)
42
+ end
43
+
44
+ # Tonelli–Shanks for p ≡ 1 (mod 4)
45
+ q = p - 1
46
+ s = 0
47
+ while (q & 1) == 0
48
+ q >>= 1
49
+ s += 1
50
+ end
51
+
52
+ # Find z: a quadratic non-residue
53
+ z = 2
54
+ while mod_pow(z, (p - 1) / 2, p) != p - 1
55
+ z += 1
56
+ end
57
+
58
+ m = s
59
+ c = mod_pow(z, q, p)
60
+ t = mod_pow(a, q, p)
61
+ r = mod_pow(a, (q + 1) / 2, p)
62
+
63
+ loop do
64
+ return r if t == 1
65
+ # Find smallest i: t^(2^i) ≡ 1 (mod p)
66
+ t2i = t
67
+ i = 0
68
+ (1...m).each do |j|
69
+ t2i = (t2i * t2i) % p
70
+ if t2i == 1
71
+ i = j
72
+ break
73
+ end
74
+ end
75
+ # Compute next values
76
+ b = mod_pow(c, 1 << (m - i - 1), p)
77
+ m = i
78
+ c = (b * b) % p
79
+ t = (t * c) % p
80
+ r = (r * b) % p
81
+ end
82
+ end
83
+
84
+ # T(point, s, a, p): transformation
85
+ # x' = (x + a + H(x,y,s)) * inv2 mod p
86
+ # y' = sqrt_mod(x*y + H(x,y,s), p)
87
+ # If sqrt_mod returns nil, increment s (up to 10 tries)
88
+ def self.T(point, s, a, p)
89
+ x, y = point
90
+ inv2 = mod_pow(2, p - 2, p) # modular inverse of 2 mod p
91
+ trials = 0
92
+ s_current = s
93
+
94
+ while trials < 10
95
+ h = H(x, y, s_current, p)
96
+ x_candidate = ((x + a + h) * inv2) % p
97
+ y_sq = (x * y + h) % p
98
+ y_candidate = sqrt_mod(y_sq, p)
99
+ return [x_candidate, y_candidate] unless y_candidate.nil?
100
+
101
+ s_current += 1
102
+ trials += 1
103
+ end
104
+
105
+ raise Error, "T: Gagal menemukan sqrt untuk y^2=#{y_sq} mod #{p} setelah #{trials} percobaan."
106
+ end
107
+
108
+ # _pow_T_range(P, start_s, exp, a, p): apply T iteratively exp times, starting seed = start_s
109
+ def self._pow_T_range(P, start_s, exp, a, p)
110
+ result = P.dup
111
+ curr_s = start_s
112
+ exp.times do
113
+ result = T(result, curr_s, a, p)
114
+ curr_s += 1
115
+ end
116
+ result
117
+ end
118
+
119
+ # keygen(p, a, P0) → { k: Integer, Q: [x,y] }
120
+ def self.keygen(p, a, P0)
121
+ loop do
122
+ k_candidate = rand(1...p)
123
+ begin
124
+ Q = _pow_T_range(P0, 1, k_candidate, a, p)
125
+ return { k: k_candidate, Q: Q }
126
+ rescue Error
127
+ next
128
+ end
129
+ end
130
+ end
131
+
132
+ # encrypt(m, public_Q, k, p, a, P0) → { C1: [x,y], C2: [x,y], r: Integer }
133
+ def self.encrypt(m, public_Q, k, p, a, P0)
134
+ loop do
135
+ r = rand(1...p)
136
+ # Compute C1 = T^r(P0) with seeds 1..r
137
+ begin
138
+ C1 = _pow_T_range(P0, 1, r, a, p)
139
+ rescue Error
140
+ next
141
+ end
142
+
143
+ # Compute Sr = T^r(public_Q) with seeds (k+1)..(k+r)
144
+ begin
145
+ Sr = _pow_T_range(public_Q, k + 1, r, a, p)
146
+ rescue Error
147
+ next
148
+ end
149
+
150
+ M0 = m % p
151
+ M = [ M0 < 0 ? M0 + p : M0, 0 ]
152
+ C2 = [ (M[0] + Sr[0]) % p, (M[1] + Sr[1]) % p ]
153
+ return { C1: C1, C2: C2, r: r }
154
+ end
155
+ end
156
+
157
+ # decrypt(C1, C2, k, r, a, p) → Integer (original message)
158
+ def self.decrypt(C1, C2, k, r, a, p)
159
+ S = _pow_T_range(C1, r + 1, k, a, p)
160
+ M0 = (C2[0] - S[0]) % p
161
+ M0 < 0 ? M0 + p : M0
162
+ end
163
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: laicrypto
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - GALIH RIDHO UTOMO
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-05-31 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: 'LAI is a promising post-quantum cryptosystem based on isogenies of elliptic
14
+ curves over lemniscate lattices, offering resistance against quantum-capable adversaries.
15
+
16
+ '
17
+ email:
18
+ - g4lihru@students.unnes.ac.id
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - README.md
24
+ - lib/laicrypto.rb
25
+ - lib/laicrypto/version.rb
26
+ homepage: https://github.com/4211421036/pqcrypto
27
+ licenses:
28
+ - MIT
29
+ metadata: {}
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: '0'
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.3.27
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: LAI is a promising post-quantum cryptosystem based on isogenies of elliptic
49
+ curves.
50
+ test_files: []