cryptoexp 0.1.0a1__py3-none-any.whl
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.
- cryptoexp/__init__.py +397 -0
- cryptoexp/cli.py +229 -0
- cryptoexp/core/__init__.py +0 -0
- cryptoexp/core/analysis/__init__.py +147 -0
- cryptoexp/core/analysis/classical.py +115 -0
- cryptoexp/core/analysis/crc_scan.py +155 -0
- cryptoexp/core/analysis/encoding_scan.py +123 -0
- cryptoexp/core/analysis/findings.py +106 -0
- cryptoexp/core/analysis/lattice_analysis.py +143 -0
- cryptoexp/core/analysis/lfsr_scan.py +130 -0
- cryptoexp/core/analysis/numbertheory.py +119 -0
- cryptoexp/core/analysis/rsa.py +272 -0
- cryptoexp/core/analysis/symmetric.py +105 -0
- cryptoexp/core/context.py +192 -0
- cryptoexp/core/report.py +290 -0
- cryptoexp/core/solve.py +1204 -0
- cryptoexp/core/verify.py +157 -0
- cryptoexp/hypothesis.py +544 -0
- cryptoexp/lab.py +145 -0
- cryptoexp/oracle.py +242 -0
- cryptoexp/utils/__init__.py +0 -0
- cryptoexp/utils/aes.py +234 -0
- cryptoexp/utils/algebra.py +508 -0
- cryptoexp/utils/classic_extra.py +713 -0
- cryptoexp/utils/dlp.py +124 -0
- cryptoexp/utils/encoding.py +949 -0
- cryptoexp/utils/forensics.py +1224 -0
- cryptoexp/utils/gf.py +176 -0
- cryptoexp/utils/gf2.py +1052 -0
- cryptoexp/utils/hashes.py +452 -0
- cryptoexp/utils/keys.py +608 -0
- cryptoexp/utils/lattice.py +494 -0
- cryptoexp/utils/modular.py +829 -0
- cryptoexp/utils/output.py +65 -0
- cryptoexp/utils/pad.py +73 -0
- cryptoexp/utils/polytools.py +772 -0
- cryptoexp/utils/prng.py +128 -0
- cryptoexp/utils/prng_extra.py +599 -0
- cryptoexp/utils/rsa_attacks.py +696 -0
- cryptoexp/utils/rsa_ops.py +372 -0
- cryptoexp/utils/signatures.py +244 -0
- cryptoexp/utils/stream.py +316 -0
- cryptoexp/utils/symtools.py +108 -0
- cryptoexp-0.1.0a1.dist-info/METADATA +326 -0
- cryptoexp-0.1.0a1.dist-info/RECORD +49 -0
- cryptoexp-0.1.0a1.dist-info/WHEEL +5 -0
- cryptoexp-0.1.0a1.dist-info/entry_points.txt +2 -0
- cryptoexp-0.1.0a1.dist-info/licenses/LICENSE +21 -0
- cryptoexp-0.1.0a1.dist-info/top_level.txt +1 -0
cryptoexp/__init__.py
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
"""cryptoexp — dependency-free crypto toolkit for CTF, security work and research
|
|
2
|
+
|
|
3
|
+
Three usage tiers, in order of how much hidden state they involve:
|
|
4
|
+
|
|
5
|
+
1) The recommended way (no namespace surprises):
|
|
6
|
+
import cryptoexp as cx
|
|
7
|
+
cx.algebra.gcd(a, b)
|
|
8
|
+
cx.long_to_bytes(0x4142)
|
|
9
|
+
cx.rsa_ops.wiener_attack(e, n)
|
|
10
|
+
cx.crc_forge_append(prefix, target, 32, 0x04C11DB7)
|
|
11
|
+
|
|
12
|
+
2) Namespaced imports when a script wants shorter lines:
|
|
13
|
+
from cryptoexp import algebra, encoding, forensics
|
|
14
|
+
algebra.gcd(a, b)
|
|
15
|
+
|
|
16
|
+
3) `from cryptoexp import *` is meant for **throwaway solve scripts**, and it only
|
|
17
|
+
brings names that are unmistakably ours (attack and helper names such as
|
|
18
|
+
`wiener_attack`, `audit_rsa_key`, `crc_forge_append`). One-word primitives that
|
|
19
|
+
user code, `math`, `hashlib` or pycryptodome also define (`gcd`, `sha256`, `AES`,
|
|
20
|
+
`xor`, ...) are deliberately **not** star-exported - they would silently shadow the
|
|
21
|
+
caller's own names. They remain available as `cx.gcd` or `from cryptoexp import gcd`.
|
|
22
|
+
|
|
23
|
+
As a tool:
|
|
24
|
+
python3 cryptoexp_cli.py analyze challenge.txt --solve
|
|
25
|
+
|
|
26
|
+
Zero dependencies: the core uses only the standard library (AES/LLL/Coppersmith/MT19937
|
|
27
|
+
are all implemented here); optional extras (gmpy2/sympy/pycryptodome/z3) are auto-detected,
|
|
28
|
+
and missing ones never affect correctness.
|
|
29
|
+
|
|
30
|
+
Naming convention: attack functions return a unified structure
|
|
31
|
+
{"ok", "plaintext", "detail", "factors", "d", "note"}
|
|
32
|
+
|
|
33
|
+
Authors: coolmoon & guaidao2 (MIT)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from .utils import algebra, encoding, lattice, pad, prng, rsa_ops, aes, gf, dlp
|
|
37
|
+
from .utils.algebra import (
|
|
38
|
+
egcd, gcd, lcm, modinv, crt, isqrt, iroot, exact_root, perfect_square,
|
|
39
|
+
is_prime, next_prime, prime_sieve, pollard_rho, factor, factor_limited,
|
|
40
|
+
divisors, continued_fraction, convergents, wiener_recover_d, fermat_factor,
|
|
41
|
+
close_factor_probe, bsgs, tonelli_shanks, lcg_recover,
|
|
42
|
+
)
|
|
43
|
+
from .utils.encoding import (
|
|
44
|
+
score_text, printable_ratio, confidence_of, flag_candidates,
|
|
45
|
+
loose_flag_candidates, find_flags, build_flag_pattern, set_flag_prefixes,
|
|
46
|
+
get_flag_prefixes, reset_flag_prefixes, DEFAULT_FLAG_PREFIXES,
|
|
47
|
+
decode_chain, guess_kinds, single_byte_xor, repeating_key_xor,
|
|
48
|
+
caesar, rot13, affine_decrypt, morse_decode, morse_encode,
|
|
49
|
+
vigenere_recover, fence_decrypt, index_of_coincidence,
|
|
50
|
+
to_hex, to_base64, to_base32, to_base58,
|
|
51
|
+
enhex, unhex, b64e, b64d, b32e, b32d, b58e, b58d,
|
|
52
|
+
)
|
|
53
|
+
from .utils.pad import (
|
|
54
|
+
pkcs7_pad, pkcs7_unpad, pkcs7_valid, zero_pad, zero_unpad, xor, xor_repeat,
|
|
55
|
+
blocks, bitflip_cbc, BLOCK,
|
|
56
|
+
)
|
|
57
|
+
from .utils.lattice import (
|
|
58
|
+
lll, poly_trim, poly_mul, poly_pow, poly_eval, coppersmith_univariate,
|
|
59
|
+
known_high_bits_factor, subset_sum_lll,
|
|
60
|
+
)
|
|
61
|
+
from .utils.rsa_ops import (
|
|
62
|
+
itob, btoi, long_to_bytes, bytes_to_long, keygen, encrypt as rsa_encrypt,
|
|
63
|
+
decrypt as rsa_decrypt, crt_decrypt, reencrypt_check, factor_from_phi,
|
|
64
|
+
factor_from_d, decrypt_with_factors, small_e_attack, broadcast_attack,
|
|
65
|
+
common_modulus_attack, shared_prime_attack, wiener_attack, fermat_attack,
|
|
66
|
+
pollard_attack, dp_leak_attack, phi_leak_attack, known_high_bits_attack,
|
|
67
|
+
auto_attack,
|
|
68
|
+
)
|
|
69
|
+
from .utils.prng import (
|
|
70
|
+
MT19937, untemper, clone_from_outputs, predict_next, clone_python_random,
|
|
71
|
+
crack_seed,
|
|
72
|
+
)
|
|
73
|
+
from . import oracle
|
|
74
|
+
from .oracle import (
|
|
75
|
+
detect_block_size, detect_mode, find_prefix_len, ecb_byte_at_a_time,
|
|
76
|
+
padding_oracle_attack, make_ecb_oracle, make_cbc_oracle, make_padding_oracle,
|
|
77
|
+
)
|
|
78
|
+
from .utils.gf import (
|
|
79
|
+
mat_rref, solve_linear, nullspace, mat_mul, mat_inv, solve_lcg_params,
|
|
80
|
+
recover_linear_map,
|
|
81
|
+
)
|
|
82
|
+
from .utils.dlp import discrete_log, pohlig_hellman, dlog_feasibility
|
|
83
|
+
from .utils.lattice import mitm_subset_sum
|
|
84
|
+
from .utils import modular, polytools, gf2, hashes, stream, rsa_attacks, keys, prng_extra
|
|
85
|
+
from .utils import signatures, symtools, classic_extra
|
|
86
|
+
from .utils.modular import (
|
|
87
|
+
legendre_symbol, jacobi_symbol, kronecker_symbol, is_quadratic_residue,
|
|
88
|
+
sqrt_mod, totient, carmichael, mobius, is_squarefree, sum_of_divisors,
|
|
89
|
+
divisor_count, order_mod, primitive_root, is_primitive_root, crt_general,
|
|
90
|
+
crt_list, gcd_list, lcm_list, pollard_pm1, williams_pp1, is_smooth,
|
|
91
|
+
smooth_part, integer_log, binomial_mod,
|
|
92
|
+
)
|
|
93
|
+
from .utils.polytools import (
|
|
94
|
+
poly_divmod, poly_mod, poly_gcd, poly_derivative, poly_roots_mod_p,
|
|
95
|
+
poly_from_roots, poly_compose, poly_powmod, poly_eval_mod,
|
|
96
|
+
poly_is_irreducible, poly_resultant, gf2_poly_mul, gf2_poly_mod,
|
|
97
|
+
gf2_poly_gcd, gf2_poly_roots,
|
|
98
|
+
)
|
|
99
|
+
from .utils.gf2 import (
|
|
100
|
+
gf2_rref, gf2_rank, gf2_solve, gf2_nullspace, gf2_inv, LFSR,
|
|
101
|
+
lfsr_from_bits, lfsr_recover, lfsr_next, berlekamp_massey, CRC,
|
|
102
|
+
crc_compute, crc_known, crc_reverse_params, crc_forge_append,
|
|
103
|
+
crc_solve_unknown, bits_to_int, int_to_bits, bytes_to_bits, bits_to_bytes,
|
|
104
|
+
)
|
|
105
|
+
from .utils.hashes import (
|
|
106
|
+
sha1, sha256, sha1_hex, sha256_hex, md5, md5_hex, sha1_state, sha256_state,
|
|
107
|
+
md5_state, HashState, length_extension, hmac_sha256,
|
|
108
|
+
)
|
|
109
|
+
from .utils.stream import (
|
|
110
|
+
rc4, rc4_keystream, ChaCha20, chacha20, chacha20_block, aes_ctr,
|
|
111
|
+
aes_ctr_keystream, crib_drag, keystream_xor, fixed_nonce_reuse_recover,
|
|
112
|
+
)
|
|
113
|
+
from .utils.rsa_attacks import (
|
|
114
|
+
franklin_reiter, hastad_padded, stereotyped_message, parity_oracle_attack,
|
|
115
|
+
lsb_oracle_attack, rsa_recover_d_from_factors, bleichenbacher_note,
|
|
116
|
+
)
|
|
117
|
+
from .utils.keys import (
|
|
118
|
+
parse_pem, parse_der_rsa_public, parse_der_rsa_private, parse_ssh_public_key,
|
|
119
|
+
parse_openssh_private, rsa_public_der, key_fingerprint_sha256, pem_wrap,
|
|
120
|
+
)
|
|
121
|
+
from .utils.prng_extra import (
|
|
122
|
+
java_random_next, java_random_ints, java_random_recover, java_random_predict,
|
|
123
|
+
glibc_rand_values, glibc_rand_recover, glibc_rand_predict,
|
|
124
|
+
xorshift32_next, xorshift32_stream, xorshift_recover, xorshift_predict,
|
|
125
|
+
lcg_recover_truncated,
|
|
126
|
+
)
|
|
127
|
+
from .utils.classic_extra import (
|
|
128
|
+
atbash, rot47, rot_n, bacon_encode, bacon_decode, playfair_encrypt,
|
|
129
|
+
playfair_decrypt, hill_encrypt, hill_decrypt, columnar_encrypt,
|
|
130
|
+
columnar_decrypt, rail_fence_encrypt, rail_fence_decrypt, autokey_encrypt,
|
|
131
|
+
autokey_decrypt, substitution_decrypt, to_base62, from_base62, to_base91,
|
|
132
|
+
from_base91, url_encode, url_decode, html_entity_decode, detect_classic,
|
|
133
|
+
)
|
|
134
|
+
from .utils.signatures import (
|
|
135
|
+
hash_to_int, ecdsa_nonce_reuse, dsa_nonce_reuse, ecdsa_recover_k,
|
|
136
|
+
ecdsa_verify, ecdsa_sign, dsa_sign, rsa_e3_signature_forge, pkcs1_v15_pad,
|
|
137
|
+
pkcs1_v15_unpad,
|
|
138
|
+
)
|
|
139
|
+
from .utils.symtools import (
|
|
140
|
+
ecb_repeated_blocks, is_ecb, infer_block_size, block_index,
|
|
141
|
+
cbc_flip_plaintext, cbc_decrypt_blocks, strip_pkcs7, pad_pkcs7,
|
|
142
|
+
)
|
|
143
|
+
from .utils.forensics import (
|
|
144
|
+
batch_gcd, common_factor_pairs, parse_der_signature, parse_jwt,
|
|
145
|
+
audit_rsa_key, detect_weak_prng,
|
|
146
|
+
)
|
|
147
|
+
from . import hypothesis
|
|
148
|
+
from .hypothesis import (
|
|
149
|
+
evaluate as evaluate_hypotheses, list_hypotheses, register_hypothesis,
|
|
150
|
+
params_from_ctx,
|
|
151
|
+
)
|
|
152
|
+
from . import lab
|
|
153
|
+
from .lab import build_workbench, write_workbench
|
|
154
|
+
from .core.context import build_context
|
|
155
|
+
from .core.analysis import (
|
|
156
|
+
analyze_all, register_analyzer, list_analyzers, probe_optional_deps,
|
|
157
|
+
)
|
|
158
|
+
from .core.verify import verify_candidates
|
|
159
|
+
from .core.solve import (
|
|
160
|
+
register_solver, list_solvers, generate as solve_script,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
__version__ = "0.1.0a1"
|
|
164
|
+
__author__ = "coolmoon & guaidao2"
|
|
165
|
+
__license__ = "MIT"
|
|
166
|
+
|
|
167
|
+
_CANDIDATE_EXPORTS = [
|
|
168
|
+
# submodules (for finer-grained entry points use cryptoexp.rsa_ops.xxx)
|
|
169
|
+
"algebra", "encoding", "lattice", "pad", "prng", "rsa_ops", "aes", "oracle",
|
|
170
|
+
"gf", "dlp", "hypothesis", "lab", "modular", "polytools", "gf2", "hashes",
|
|
171
|
+
"stream", "rsa_attacks", "keys", "prng_extra", "signatures", "symtools",
|
|
172
|
+
# hypothesis engine / workbench
|
|
173
|
+
"evaluate_hypotheses", "list_hypotheses", "register_hypothesis",
|
|
174
|
+
"params_from_ctx", "build_workbench", "write_workbench",
|
|
175
|
+
# number theory
|
|
176
|
+
"egcd", "gcd", "lcm", "modinv", "crt", "isqrt", "iroot", "exact_root",
|
|
177
|
+
"perfect_square", "is_prime", "next_prime", "prime_sieve", "pollard_rho",
|
|
178
|
+
"factor", "factor_limited", "divisors", "continued_fraction", "convergents",
|
|
179
|
+
"wiener_recover_d", "fermat_factor", "close_factor_probe", "bsgs",
|
|
180
|
+
"tonelli_shanks", "lcg_recover",
|
|
181
|
+
# encoding / classical
|
|
182
|
+
"score_text", "printable_ratio", "confidence_of", "flag_candidates",
|
|
183
|
+
"loose_flag_candidates", "find_flags", "build_flag_pattern",
|
|
184
|
+
"set_flag_prefixes", "get_flag_prefixes", "reset_flag_prefixes",
|
|
185
|
+
"DEFAULT_FLAG_PREFIXES",
|
|
186
|
+
"decode_chain", "guess_kinds", "single_byte_xor", "repeating_key_xor",
|
|
187
|
+
"caesar", "rot13", "affine_decrypt", "morse_decode", "morse_encode",
|
|
188
|
+
"vigenere_recover", "fence_decrypt", "index_of_coincidence",
|
|
189
|
+
"to_hex", "to_base64", "to_base32", "to_base58",
|
|
190
|
+
"enhex", "unhex", "b64e", "b64d", "b32e", "b32d", "b58e", "b58d",
|
|
191
|
+
# bytes / padding
|
|
192
|
+
"pkcs7_pad", "pkcs7_unpad", "pkcs7_valid", "zero_pad", "zero_unpad",
|
|
193
|
+
"xor", "xor_repeat", "blocks", "bitflip_cbc", "BLOCK",
|
|
194
|
+
# lattice
|
|
195
|
+
"lll", "poly_trim", "poly_mul", "poly_pow", "poly_eval",
|
|
196
|
+
"coppersmith_univariate", "known_high_bits_factor", "subset_sum_lll",
|
|
197
|
+
"mitm_subset_sum",
|
|
198
|
+
# GF(p) linear algebra / discrete log
|
|
199
|
+
"mat_rref", "solve_linear", "nullspace", "mat_mul", "mat_inv",
|
|
200
|
+
"solve_lcg_params", "recover_linear_map", "discrete_log", "pohlig_hellman",
|
|
201
|
+
"dlog_feasibility",
|
|
202
|
+
# RSA
|
|
203
|
+
"itob", "btoi", "long_to_bytes", "bytes_to_long", "keygen", "rsa_encrypt",
|
|
204
|
+
"rsa_decrypt", "crt_decrypt", "reencrypt_check", "factor_from_phi",
|
|
205
|
+
"factor_from_d", "decrypt_with_factors", "small_e_attack",
|
|
206
|
+
"broadcast_attack", "common_modulus_attack", "shared_prime_attack",
|
|
207
|
+
"wiener_attack", "fermat_attack", "pollard_attack", "dp_leak_attack",
|
|
208
|
+
"phi_leak_attack", "known_high_bits_attack", "auto_attack",
|
|
209
|
+
# PRNG
|
|
210
|
+
"MT19937", "untemper", "clone_from_outputs", "predict_next",
|
|
211
|
+
"clone_python_random", "crack_seed",
|
|
212
|
+
# oracle
|
|
213
|
+
"detect_block_size", "detect_mode", "find_prefix_len",
|
|
214
|
+
"ecb_byte_at_a_time", "padding_oracle_attack", "make_ecb_oracle",
|
|
215
|
+
"make_cbc_oracle", "make_padding_oracle",
|
|
216
|
+
# analysis / generation / verification
|
|
217
|
+
"build_context", "analyze_all", "verify_candidates", "solve_script",
|
|
218
|
+
"register_analyzer", "register_solver", "list_analyzers", "list_solvers",
|
|
219
|
+
"probe_optional_deps",
|
|
220
|
+
# modular extras
|
|
221
|
+
"legendre_symbol", "jacobi_symbol", "kronecker_symbol", "is_quadratic_residue",
|
|
222
|
+
"sqrt_mod", "totient", "carmichael", "mobius", "is_squarefree",
|
|
223
|
+
"sum_of_divisors", "divisor_count", "order_mod", "primitive_root",
|
|
224
|
+
"is_primitive_root", "crt_general", "crt_list", "gcd_list", "lcm_list",
|
|
225
|
+
"pollard_pm1", "williams_pp1", "is_smooth", "smooth_part", "integer_log",
|
|
226
|
+
"binomial_mod",
|
|
227
|
+
# polynomials
|
|
228
|
+
"poly_divmod", "poly_mod", "poly_gcd", "poly_derivative", "poly_roots_mod_p",
|
|
229
|
+
"poly_from_roots", "poly_compose", "poly_powmod", "poly_eval_mod",
|
|
230
|
+
"poly_is_irreducible", "poly_resultant", "gf2_poly_mul", "gf2_poly_mod",
|
|
231
|
+
"gf2_poly_gcd", "gf2_poly_roots",
|
|
232
|
+
# GF(2) / LFSR / CRC / bits
|
|
233
|
+
"gf2_rref", "gf2_rank", "gf2_solve", "gf2_nullspace", "gf2_inv", "LFSR",
|
|
234
|
+
"lfsr_from_bits", "lfsr_recover", "lfsr_next", "berlekamp_massey", "CRC",
|
|
235
|
+
"crc_compute", "crc_known", "crc_reverse_params", "crc_forge_append",
|
|
236
|
+
"crc_solve_unknown", "bits_to_int", "int_to_bits", "bytes_to_bits", "bits_to_bytes",
|
|
237
|
+
# hashes
|
|
238
|
+
"sha1", "sha256", "sha1_hex", "sha256_hex", "md5", "md5_hex", "sha1_state",
|
|
239
|
+
"sha256_state", "md5_state", "HashState", "length_extension", "hmac_sha256",
|
|
240
|
+
# stream ciphers
|
|
241
|
+
"rc4", "rc4_keystream", "ChaCha20", "chacha20", "chacha20_block", "aes_ctr",
|
|
242
|
+
"aes_ctr_keystream", "crib_drag", "keystream_xor", "fixed_nonce_reuse_recover",
|
|
243
|
+
# symmetric helpers
|
|
244
|
+
"ecb_repeated_blocks", "is_ecb", "infer_block_size", "block_index",
|
|
245
|
+
"cbc_flip_plaintext", "cbc_decrypt_blocks", "strip_pkcs7", "pad_pkcs7",
|
|
246
|
+
# real-world forensics (beyond CTF)
|
|
247
|
+
"batch_gcd", "common_factor_pairs", "parse_der_signature", "parse_jwt",
|
|
248
|
+
"audit_rsa_key", "detect_weak_prng",
|
|
249
|
+
# RSA attacks (extra) / keys
|
|
250
|
+
"franklin_reiter", "hastad_padded", "stereotyped_message",
|
|
251
|
+
"parity_oracle_attack", "lsb_oracle_attack", "rsa_recover_d_from_factors",
|
|
252
|
+
"bleichenbacher_note", "parse_pem", "parse_der_rsa_public",
|
|
253
|
+
"parse_der_rsa_private", "parse_ssh_public_key", "parse_openssh_private",
|
|
254
|
+
"rsa_public_der", "key_fingerprint_sha256", "pem_wrap",
|
|
255
|
+
# signatures
|
|
256
|
+
"hash_to_int", "ecdsa_nonce_reuse", "dsa_nonce_reuse", "ecdsa_recover_k",
|
|
257
|
+
"ecdsa_verify", "ecdsa_sign", "dsa_sign", "rsa_e3_signature_forge",
|
|
258
|
+
"pkcs1_v15_pad", "pkcs1_v15_unpad",
|
|
259
|
+
# PRNG (extra)
|
|
260
|
+
"java_random_next", "java_random_ints", "java_random_recover",
|
|
261
|
+
"java_random_predict", "glibc_rand_values", "glibc_rand_recover",
|
|
262
|
+
"glibc_rand_predict", "xorshift32_next", "xorshift32_stream",
|
|
263
|
+
"xorshift_recover", "xorshift_predict", "lcg_recover_truncated",
|
|
264
|
+
# classic ciphers (extra)
|
|
265
|
+
"atbash", "rot47", "rot_n", "bacon_encode", "bacon_decode",
|
|
266
|
+
"playfair_encrypt", "playfair_decrypt", "hill_encrypt", "hill_decrypt",
|
|
267
|
+
"columnar_encrypt", "columnar_decrypt", "rail_fence_encrypt",
|
|
268
|
+
"rail_fence_decrypt", "autokey_encrypt", "autokey_decrypt",
|
|
269
|
+
"substitution_decrypt", "to_base62", "from_base62", "to_base91",
|
|
270
|
+
"from_base91", "url_encode", "url_decode", "html_entity_decode",
|
|
271
|
+
"detect_classic",
|
|
272
|
+
]
|
|
273
|
+
|
|
274
|
+
# ── What a star import brings, and what it deliberately does not ──────────────
|
|
275
|
+
# `from cryptoexp import *` must not shadow the caller's own names. These are the
|
|
276
|
+
# one-word primitives that user code, `math`, `hashlib` and pycryptodome also define;
|
|
277
|
+
# a star import that grabbed `gcd` or `AES` would silently break the calling module.
|
|
278
|
+
# They stay *fully available* - as attributes (`cx.gcd`) and by explicit import
|
|
279
|
+
# (`from cryptoexp import gcd`) - they are just not pushed into the caller's
|
|
280
|
+
# namespace behind their back.
|
|
281
|
+
_COLLISION_PRONE = {
|
|
282
|
+
"gcd", "egcd", "lcm", "modinv", "crt", "isqrt", "iroot", "exact_root",
|
|
283
|
+
"perfect_square", "is_prime", "next_prime", "prime_sieve", "pollard_rho",
|
|
284
|
+
"factor", "factor_limited", "divisors", "totient", "carmichael", "mobius",
|
|
285
|
+
"is_squarefree", "sum_of_divisors", "divisor_count", "order_mod",
|
|
286
|
+
"primitive_root", "is_primitive_root", "is_smooth", "smooth_part",
|
|
287
|
+
"integer_log", "binomial_mod", "legendre_symbol", "jacobi_symbol",
|
|
288
|
+
"kronecker_symbol", "sqrt_mod", "is_quadratic_residue",
|
|
289
|
+
"xor", "blocks", "BLOCK", "itob", "btoi", "long_to_bytes", "bytes_to_long",
|
|
290
|
+
"sha1", "sha256", "md5", "sha1_hex", "sha256_hex", "md5_hex", "hmac_sha256",
|
|
291
|
+
"keygen", "to_hex", "to_base64", "to_base32", "to_base58",
|
|
292
|
+
"score_text", "printable_ratio", "confidence_of",
|
|
293
|
+
"int_to_bits", "bits_to_int", "bytes_to_bits", "bits_to_bytes",
|
|
294
|
+
"detect_block_size", "is_ecb", "nullspace", "mat_inv", "mat_mul",
|
|
295
|
+
"poly_trim", "poly_mul", "poly_pow", "poly_eval", "gcd_list", "lcm_list",
|
|
296
|
+
"factor_from_phi", "factor_from_d", "crt_decrypt", "rsa_encrypt", "rsa_decrypt",
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
# Submodules are reached through the package (`cx.algebra.gcd`) or imported by name
|
|
300
|
+
# (`from cryptoexp import algebra`); they are not star-exported either, because
|
|
301
|
+
# `encoding`, `keys`, `stream` and `aes` are exactly the kind of names a caller
|
|
302
|
+
# already has.
|
|
303
|
+
_SUBMODULES = {
|
|
304
|
+
"algebra", "encoding", "lattice", "pad", "prng", "rsa_ops", "aes", "gf", "dlp",
|
|
305
|
+
"modular", "polytools", "gf2", "hashes", "stream", "rsa_attacks", "keys",
|
|
306
|
+
"prng_extra", "signatures", "symtools", "classical_extra", "forensics",
|
|
307
|
+
"oracle", "hypothesis", "lab",
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
__all__ = [name for name in _CANDIDATE_EXPORTS
|
|
311
|
+
if name not in _COLLISION_PRONE and name not in _SUBMODULES]
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def api(verbose: bool = False) -> str:
|
|
315
|
+
"""Print/return the API map (the `from pwn import help` equivalent)"""
|
|
316
|
+
groups = { "number theory": ["gcd", "egcd", "modinv", "crt", "iroot", "isqrt", "is_prime",
|
|
317
|
+
"next_prime", "factor", "factor_limited", "divisors",
|
|
318
|
+
"pollard_rho", "continued_fraction", "convergents",
|
|
319
|
+
"fermat_factor", "bsgs", "tonelli_shanks", "lcg_recover"],
|
|
320
|
+
"encoding/classical": ["enhex", "unhex", "b64e", "b64d", "b32e", "b32d", "b58d",
|
|
321
|
+
"score_text", "flag_candidates", "loose_flag_candidates",
|
|
322
|
+
"find_flags", "set_flag_prefixes", "get_flag_prefixes",
|
|
323
|
+
"build_flag_pattern",
|
|
324
|
+
"decode_chain",
|
|
325
|
+
"single_byte_xor", "repeating_key_xor", "caesar",
|
|
326
|
+
"affine_decrypt", "vigenere_recover", "morse_decode",
|
|
327
|
+
"morse_encode", "fence_decrypt"],
|
|
328
|
+
"bytes/padding": ["pkcs7_pad", "pkcs7_unpad", "pkcs7_valid", "xor",
|
|
329
|
+
"xor_repeat", "blocks", "bitflip_cbc"],
|
|
330
|
+
"AES": ["aes.new", "aes.encrypt_ecb", "aes.decrypt_cbc", "aes.MODE_ECB"],
|
|
331
|
+
"lattice": ["lll", "coppersmith_univariate", "known_high_bits_factor",
|
|
332
|
+
"subset_sum_lll"],
|
|
333
|
+
"RSA": ["keygen", "rsa_encrypt", "rsa_decrypt", "long_to_bytes",
|
|
334
|
+
"small_e_attack", "broadcast_attack", "common_modulus_attack",
|
|
335
|
+
"shared_prime_attack", "wiener_attack", "fermat_attack",
|
|
336
|
+
"pollard_attack", "dp_leak_attack", "phi_leak_attack",
|
|
337
|
+
"known_high_bits_attack", "auto_attack", "reencrypt_check"],
|
|
338
|
+
"PRNG": ["MT19937", "untemper", "clone_from_outputs", "predict_next",
|
|
339
|
+
"clone_python_random", "crack_seed"],
|
|
340
|
+
"oracle attacks": ["detect_block_size", "detect_mode", "find_prefix_len",
|
|
341
|
+
"ecb_byte_at_a_time", "padding_oracle_attack",
|
|
342
|
+
"make_ecb_oracle", "make_cbc_oracle", "make_padding_oracle"],
|
|
343
|
+
"analysis/solve": ["analyze_all", "verify_candidates", "solve_script",
|
|
344
|
+
"register_analyzer", "register_solver"],
|
|
345
|
+
"hypothesis engine / workbench": ["evaluate_hypotheses", "list_hypotheses",
|
|
346
|
+
"register_hypothesis", "write_workbench"],
|
|
347
|
+
"linear algebra / discrete log": ["solve_linear", "mat_inv", "recover_linear_map",
|
|
348
|
+
"solve_lcg_params", "discrete_log", "pohlig_hellman",
|
|
349
|
+
"dlog_feasibility", "mitm_subset_sum"],
|
|
350
|
+
"modular extras": ["legendre_symbol", "jacobi_symbol", "kronecker_symbol",
|
|
351
|
+
"sqrt_mod", "totient", "carmichael", "mobius",
|
|
352
|
+
"order_mod", "primitive_root", "crt_general", "pollard_pm1",
|
|
353
|
+
"williams_pp1", "is_smooth", "binomial_mod"],
|
|
354
|
+
"polynomials": ["poly_divmod", "poly_gcd", "poly_roots_mod_p", "poly_from_roots",
|
|
355
|
+
"poly_powmod", "poly_is_irreducible", "poly_resultant",
|
|
356
|
+
"gf2_poly_gcd", "gf2_poly_roots"],
|
|
357
|
+
"GF(2) / LFSR / CRC": ["gf2_rref", "gf2_solve", "gf2_nullspace", "gf2_inv",
|
|
358
|
+
"LFSR", "lfsr_recover", "lfsr_next", "berlekamp_massey",
|
|
359
|
+
"CRC", "crc_compute", "crc_known", "crc_reverse_params",
|
|
360
|
+
"crc_forge_append", "crc_solve_unknown",
|
|
361
|
+
"bits_to_bytes", "bytes_to_bits"],
|
|
362
|
+
"hashes": ["sha1", "sha256", "md5", "sha1_hex", "sha256_hex",
|
|
363
|
+
"length_extension", "HashState", "hmac_sha256"],
|
|
364
|
+
"stream ciphers": ["rc4", "ChaCha20", "chacha20", "aes_ctr", "aes_ctr_keystream",
|
|
365
|
+
"crib_drag", "keystream_xor", "fixed_nonce_reuse_recover"],
|
|
366
|
+
"symmetric tools": ["is_ecb", "ecb_repeated_blocks", "infer_block_size",
|
|
367
|
+
"cbc_flip_plaintext", "cbc_decrypt_blocks", "pad_pkcs7"],
|
|
368
|
+
"RSA attacks (extra)": ["franklin_reiter", "hastad_padded", "stereotyped_message",
|
|
369
|
+
"parity_oracle_attack", "lsb_oracle_attack",
|
|
370
|
+
"bleichenbacher_note"],
|
|
371
|
+
"keys": ["parse_pem", "parse_der_rsa_public", "parse_der_rsa_private",
|
|
372
|
+
"parse_ssh_public_key", "parse_openssh_private", "rsa_public_der",
|
|
373
|
+
"key_fingerprint_sha256", "pem_wrap"],
|
|
374
|
+
"signatures": ["ecdsa_nonce_reuse", "dsa_nonce_reuse", "ecdsa_verify",
|
|
375
|
+
"ecdsa_sign", "dsa_sign", "rsa_e3_signature_forge",
|
|
376
|
+
"pkcs1_v15_pad", "pkcs1_v15_unpad"],
|
|
377
|
+
"PRNG (extra)": ["java_random_recover", "java_random_predict",
|
|
378
|
+
"glibc_rand_recover", "glibc_rand_predict",
|
|
379
|
+
"xorshift_recover", "xorshift_predict",
|
|
380
|
+
"lcg_recover_truncated"],
|
|
381
|
+
"forensics / assessment": ["batch_gcd", "common_factor_pairs",
|
|
382
|
+
"parse_der_signature", "parse_jwt",
|
|
383
|
+
"audit_rsa_key", "detect_weak_prng",
|
|
384
|
+
"find_flags", "set_flag_prefixes"],
|
|
385
|
+
}
|
|
386
|
+
lines = [f"cryptoexp {__version__} — dependency-free crypto toolkit", ""]
|
|
387
|
+
for group, names in groups.items():
|
|
388
|
+
lines.append(f"[{group}]")
|
|
389
|
+
lines.append(" " + ", ".join(names))
|
|
390
|
+
lines.append("")
|
|
391
|
+
lines.append("usage: import cryptoexp as cx; cx.algebra.gcd(24, 36); "
|
|
392
|
+
"cx.crc_forge_append(...)")
|
|
393
|
+
lines.append(" from cryptoexp import * brings only unmistakably-ours names "
|
|
394
|
+
"(star import is for throwaway scripts)")
|
|
395
|
+
text = "\n".join(lines)
|
|
396
|
+
print(text)
|
|
397
|
+
return text
|
cryptoexp/cli.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""cryptoexp command line interface.
|
|
2
|
+
|
|
3
|
+
Installed console script (`cryptoexp`) and the repository-level `cryptoexp_cli.py`
|
|
4
|
+
shim both call `main()` here. English-first output.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import contextlib
|
|
9
|
+
import io
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from .utils.output import (print_banner, print_error, print_info,
|
|
15
|
+
print_section_header, print_success, print_warning)
|
|
16
|
+
from .core.analysis import analyze_all, list_analyzers
|
|
17
|
+
from .core.report import print_results, print_json_summary
|
|
18
|
+
from .core.verify import verify_candidates
|
|
19
|
+
from .core.solve import generate as gen_solve, list_solvers
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _run_one(target: str, args) -> dict:
|
|
23
|
+
results = analyze_all(target, skip_encoding=args.no_encoding,
|
|
24
|
+
effort=getattr(args, "effort", "normal"),
|
|
25
|
+
flag_prefixes=getattr(args, "flag_prefix", None),
|
|
26
|
+
flag_pattern=getattr(args, "flag_regex", None))
|
|
27
|
+
verification = verify_candidates(results)
|
|
28
|
+
results["verification"] = verification
|
|
29
|
+
|
|
30
|
+
if args.json:
|
|
31
|
+
print(print_json_summary(results, verification))
|
|
32
|
+
return results
|
|
33
|
+
|
|
34
|
+
print_section_header(f"Analyzing target: {target}")
|
|
35
|
+
print_results(results, verification)
|
|
36
|
+
|
|
37
|
+
if args.solve or args.run:
|
|
38
|
+
path = gen_solve(results, out_dir=args.out, only=args.only)
|
|
39
|
+
print_success(f"solve script: {path}")
|
|
40
|
+
if args.run:
|
|
41
|
+
print_info(f"running {path} ...")
|
|
42
|
+
import subprocess
|
|
43
|
+
proc = subprocess.run([sys.executable, path], capture_output=True,
|
|
44
|
+
text=True, timeout=120)
|
|
45
|
+
print(proc.stdout.rstrip() or "(no output)")
|
|
46
|
+
if proc.returncode != 0:
|
|
47
|
+
print_warning(f"script exit code {proc.returncode} "
|
|
48
|
+
f"(may be a [skeleton], see the notes above)")
|
|
49
|
+
if proc.stderr.strip():
|
|
50
|
+
print(proc.stderr.strip()[-500:])
|
|
51
|
+
return results
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def cmd_analyze(args):
|
|
55
|
+
if args.dir:
|
|
56
|
+
if not os.path.isdir(args.dir):
|
|
57
|
+
print_error(f"directory does not exist: {args.dir}")
|
|
58
|
+
return 1
|
|
59
|
+
entries = [os.path.join(args.dir, n) for n in sorted(os.listdir(args.dir))
|
|
60
|
+
if os.path.isfile(os.path.join(args.dir, n))]
|
|
61
|
+
if not entries:
|
|
62
|
+
print_error("no files in the directory")
|
|
63
|
+
return 1
|
|
64
|
+
all_res = []
|
|
65
|
+
for i, p in enumerate(entries, 1):
|
|
66
|
+
if args.json:
|
|
67
|
+
buf = io.StringIO()
|
|
68
|
+
with contextlib.redirect_stdout(buf):
|
|
69
|
+
r = _run_one(p, args)
|
|
70
|
+
all_res.append(json.loads(print_json_summary(r, r.get("verification"))))
|
|
71
|
+
else:
|
|
72
|
+
print(f"\n=== [{i}/{len(entries)}] {os.path.basename(p)} ===")
|
|
73
|
+
_run_one(p, args)
|
|
74
|
+
if args.json:
|
|
75
|
+
print(json.dumps(all_res, ensure_ascii=False, indent=2, default=str))
|
|
76
|
+
return 0
|
|
77
|
+
|
|
78
|
+
if not args.target:
|
|
79
|
+
print_error('analyze needs <target> or --dir; '
|
|
80
|
+
'the target may be a file, a directory or challenge text')
|
|
81
|
+
return 1
|
|
82
|
+
_run_one(args.target, args)
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def cmd_hypotheses(args):
|
|
87
|
+
"""Run the hypothesis engine end to end (every entry is really attempted)"""
|
|
88
|
+
from .hypothesis import params_from_ctx, evaluate
|
|
89
|
+
from .core.context import build_context
|
|
90
|
+
|
|
91
|
+
ctx = build_context(args.target)
|
|
92
|
+
params = params_from_ctx(ctx)
|
|
93
|
+
result = evaluate(params, budget="full")
|
|
94
|
+
|
|
95
|
+
if args.json:
|
|
96
|
+
print(json.dumps({
|
|
97
|
+
"target": args.target,
|
|
98
|
+
"applicable": result["applicable"],
|
|
99
|
+
"gaps": result["gaps"],
|
|
100
|
+
"results": [{k: v for k, v in r.items() if k != "plaintext"}
|
|
101
|
+
for r in result["results"]],
|
|
102
|
+
}, ensure_ascii=False, indent=2, default=str))
|
|
103
|
+
return 0
|
|
104
|
+
|
|
105
|
+
print_section_header(f"Hypothesis engine: {args.target}")
|
|
106
|
+
if not result["results"]:
|
|
107
|
+
print_warning("no hypothesis applies — see the gap list below for the missing piece")
|
|
108
|
+
for r in result["results"]:
|
|
109
|
+
tag = {"confirmed": "[confirmed]", "candidate": "[candidate]",
|
|
110
|
+
"not_reproduced": "[not_reproduced]"}.get(r["state"], "[?]")
|
|
111
|
+
print(f" {tag} {r['name']} ({r['domain']}, {r['cost']}) - {r['idea']}")
|
|
112
|
+
if r.get("detail"):
|
|
113
|
+
print(f" {r['detail']}")
|
|
114
|
+
if r.get("note"):
|
|
115
|
+
print(f" {r['note']}")
|
|
116
|
+
if r.get("plaintext"):
|
|
117
|
+
print(f" plaintext: {r['plaintext'][:80]!r}")
|
|
118
|
+
print_section_header(f"Gap list ({len(result['gaps'])} entries)")
|
|
119
|
+
for g in result["gaps"]:
|
|
120
|
+
print(f" - {g['name']} - {g['idea']}")
|
|
121
|
+
print(f" missing: {'; '.join(g.get('missing', [])[:3])}")
|
|
122
|
+
return 0
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def cmd_lab(args):
|
|
126
|
+
"""Generate the workbench scaffold (start here for challenges no template covers)"""
|
|
127
|
+
from .lab import write_workbench
|
|
128
|
+
from .hypothesis import params_from_ctx, evaluate
|
|
129
|
+
|
|
130
|
+
results = analyze_all(args.target, skip_encoding=args.no_encoding)
|
|
131
|
+
params = params_from_ctx(results.get("_ctx") or {})
|
|
132
|
+
hypotheses = evaluate(params, budget="full" if args.deep else "list")
|
|
133
|
+
path = write_workbench(results, out_dir=args.out, params=params,
|
|
134
|
+
hypotheses=hypotheses)
|
|
135
|
+
print_success(f"workbench script: {path}")
|
|
136
|
+
print_info("open it: parameters are already extracted, applicable hypotheses and gaps "
|
|
137
|
+
"are written in comments, and the starting moves are ready to call")
|
|
138
|
+
if args.run:
|
|
139
|
+
import subprocess
|
|
140
|
+
proc = subprocess.run([sys.executable, path], capture_output=True,
|
|
141
|
+
text=True, timeout=180)
|
|
142
|
+
print(proc.stdout.rstrip() or "(no output)")
|
|
143
|
+
if proc.returncode != 0 and proc.stderr.strip():
|
|
144
|
+
print_warning(proc.stderr.strip()[-400:])
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def cmd_list(args):
|
|
149
|
+
from .core.analysis import probe_optional_deps
|
|
150
|
+
print_section_header("Analyzers (built-in + plugins)")
|
|
151
|
+
for name in ("encoding", "classical", "rsa", "symmetric", "numbertheory", "lattice"):
|
|
152
|
+
print(f" {name}")
|
|
153
|
+
for name in list_analyzers():
|
|
154
|
+
print(f" {name} (plugin)")
|
|
155
|
+
print_section_header("solve templates (lower priority runs first)")
|
|
156
|
+
for name, prio in list_solvers() or [("(not registered yet, loaded on first generation)", "-")]:
|
|
157
|
+
print(f" {prio:>4} {name}")
|
|
158
|
+
print_section_header("optional extras (core is dependency-free; missing ones are harmless)")
|
|
159
|
+
for mod, info in probe_optional_deps().items():
|
|
160
|
+
mark = "installed" if info["available"] else "missing"
|
|
161
|
+
print(f" {mod:<8} {mark} {info['use']}")
|
|
162
|
+
return 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
166
|
+
parser = argparse.ArgumentParser(description="cryptoexp - cryptography CTF helper")
|
|
167
|
+
sub = parser.add_subparsers(dest="command")
|
|
168
|
+
|
|
169
|
+
p = sub.add_parser("analyze", help="analyze a challenge (file/directory/challenge text)")
|
|
170
|
+
p.add_argument("target", nargs="?", help="challenge file, directory, or raw challenge text")
|
|
171
|
+
p.add_argument("--dir", help="batch-analyze every file in a directory")
|
|
172
|
+
p.add_argument("--json", action="store_true", help="JSON output (schema v1.0)")
|
|
173
|
+
p.add_argument("--solve", action="store_true", help="generate a solve script")
|
|
174
|
+
p.add_argument("--run", action="store_true", help="generate and run the solve script")
|
|
175
|
+
p.add_argument("--out", default="solves",
|
|
176
|
+
help="output directory for solve scripts (default solves/)")
|
|
177
|
+
p.add_argument("--only", help="only allow the solve template with this exact name")
|
|
178
|
+
p.add_argument("--no-encoding", action="store_true",
|
|
179
|
+
help="skip encoding/XOR analysis (faster)")
|
|
180
|
+
p.add_argument("--effort", choices=["fast", "normal", "max"], default="normal",
|
|
181
|
+
help="effort level: fast = cheap checks only / max = attempt everything")
|
|
182
|
+
p.add_argument("--flag-prefix", action="append", metavar="PREFIX",
|
|
183
|
+
help="expected flag prefix, repeatable (e.g. --flag-prefix DH). "
|
|
184
|
+
"Defaults to CRYPTOEXP_FLAG_PREFIXES, else the built-in "
|
|
185
|
+
"CTF event list")
|
|
186
|
+
p.add_argument("--flag-regex", metavar="REGEX",
|
|
187
|
+
help="a regex that replaces the prefix list entirely, for marker "
|
|
188
|
+
"formats that are not PREFIX{...}")
|
|
189
|
+
|
|
190
|
+
sub.add_parser("list", help="list analyzers and solve templates")
|
|
191
|
+
|
|
192
|
+
p = sub.add_parser("hypotheses",
|
|
193
|
+
help="run the hypothesis engine end to end (attack surface + gaps)")
|
|
194
|
+
p.add_argument("target", help="challenge file/directory/challenge text")
|
|
195
|
+
p.add_argument("--json", action="store_true", help="JSON output")
|
|
196
|
+
|
|
197
|
+
p = sub.add_parser("lab",
|
|
198
|
+
help="generate a workbench scaffold (start here for long-tail challenges)")
|
|
199
|
+
p.add_argument("target", help="challenge file/directory/challenge text")
|
|
200
|
+
p.add_argument("--out", default="lab", help="output directory (default lab/)")
|
|
201
|
+
p.add_argument("--deep", action="store_true",
|
|
202
|
+
help="try every hypothesis while generating (slow)")
|
|
203
|
+
p.add_argument("--run", action="store_true", help="run it once right after generating")
|
|
204
|
+
p.add_argument("--no-encoding", action="store_true", help="skip encoding/XOR analysis")
|
|
205
|
+
return parser
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def main(argv=None):
|
|
209
|
+
parser = build_parser()
|
|
210
|
+
args = parser.parse_args(argv)
|
|
211
|
+
if not args.command:
|
|
212
|
+
parser.print_help()
|
|
213
|
+
return 1
|
|
214
|
+
if not getattr(args, "json", False):
|
|
215
|
+
print_banner()
|
|
216
|
+
match args.command:
|
|
217
|
+
case "analyze":
|
|
218
|
+
return cmd_analyze(args)
|
|
219
|
+
case "hypotheses":
|
|
220
|
+
return cmd_hypotheses(args)
|
|
221
|
+
case "lab":
|
|
222
|
+
return cmd_lab(args)
|
|
223
|
+
case "list":
|
|
224
|
+
return cmd_list(args)
|
|
225
|
+
return 0
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
if __name__ == "__main__":
|
|
229
|
+
sys.exit(main())
|
|
File without changes
|