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.
Files changed (49) hide show
  1. cryptoexp/__init__.py +397 -0
  2. cryptoexp/cli.py +229 -0
  3. cryptoexp/core/__init__.py +0 -0
  4. cryptoexp/core/analysis/__init__.py +147 -0
  5. cryptoexp/core/analysis/classical.py +115 -0
  6. cryptoexp/core/analysis/crc_scan.py +155 -0
  7. cryptoexp/core/analysis/encoding_scan.py +123 -0
  8. cryptoexp/core/analysis/findings.py +106 -0
  9. cryptoexp/core/analysis/lattice_analysis.py +143 -0
  10. cryptoexp/core/analysis/lfsr_scan.py +130 -0
  11. cryptoexp/core/analysis/numbertheory.py +119 -0
  12. cryptoexp/core/analysis/rsa.py +272 -0
  13. cryptoexp/core/analysis/symmetric.py +105 -0
  14. cryptoexp/core/context.py +192 -0
  15. cryptoexp/core/report.py +290 -0
  16. cryptoexp/core/solve.py +1204 -0
  17. cryptoexp/core/verify.py +157 -0
  18. cryptoexp/hypothesis.py +544 -0
  19. cryptoexp/lab.py +145 -0
  20. cryptoexp/oracle.py +242 -0
  21. cryptoexp/utils/__init__.py +0 -0
  22. cryptoexp/utils/aes.py +234 -0
  23. cryptoexp/utils/algebra.py +508 -0
  24. cryptoexp/utils/classic_extra.py +713 -0
  25. cryptoexp/utils/dlp.py +124 -0
  26. cryptoexp/utils/encoding.py +949 -0
  27. cryptoexp/utils/forensics.py +1224 -0
  28. cryptoexp/utils/gf.py +176 -0
  29. cryptoexp/utils/gf2.py +1052 -0
  30. cryptoexp/utils/hashes.py +452 -0
  31. cryptoexp/utils/keys.py +608 -0
  32. cryptoexp/utils/lattice.py +494 -0
  33. cryptoexp/utils/modular.py +829 -0
  34. cryptoexp/utils/output.py +65 -0
  35. cryptoexp/utils/pad.py +73 -0
  36. cryptoexp/utils/polytools.py +772 -0
  37. cryptoexp/utils/prng.py +128 -0
  38. cryptoexp/utils/prng_extra.py +599 -0
  39. cryptoexp/utils/rsa_attacks.py +696 -0
  40. cryptoexp/utils/rsa_ops.py +372 -0
  41. cryptoexp/utils/signatures.py +244 -0
  42. cryptoexp/utils/stream.py +316 -0
  43. cryptoexp/utils/symtools.py +108 -0
  44. cryptoexp-0.1.0a1.dist-info/METADATA +326 -0
  45. cryptoexp-0.1.0a1.dist-info/RECORD +49 -0
  46. cryptoexp-0.1.0a1.dist-info/WHEEL +5 -0
  47. cryptoexp-0.1.0a1.dist-info/entry_points.txt +2 -0
  48. cryptoexp-0.1.0a1.dist-info/licenses/LICENSE +21 -0
  49. 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