vicrypt 0.1.0__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.
- vicrypt-0.1.0/.gitignore +1 -0
- vicrypt-0.1.0/LICENSE +21 -0
- vicrypt-0.1.0/PKG-INFO +77 -0
- vicrypt-0.1.0/README.md +55 -0
- vicrypt-0.1.0/main.py +388 -0
- vicrypt-0.1.0/pyproject.toml +48 -0
vicrypt-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.wakatime-project
|
vicrypt-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Vincent Chen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
vicrypt-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vicrypt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python cryptography toolset for CTF work
|
|
5
|
+
Project-URL: Homepage, https://github.com/vincentchen18/vicrypt
|
|
6
|
+
Project-URL: Repository, https://github.com/vincentchen18/vicrypt
|
|
7
|
+
Project-URL: Issues, https://github.com/vincentchen18/vicrypt/issues
|
|
8
|
+
Author: Vincent Chen
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: cipher,cryptography,ctf,encoding,security
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Security :: Cryptography
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
20
|
+
Requires-Dist: ruff>=0.4; extra == 'dev'
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# ViCrypt
|
|
24
|
+
|
|
25
|
+
## Overview
|
|
26
|
+
|
|
27
|
+
ViCrypt is a command line cryptography tool (currently a WIP) with basic encoding/decoding functionality! Featuring hex encode/decode, base64 encode/decode, base32 encode/decode, base85 encode/decode, URL encode/decode, binary encode/decode, atbash, ROT13, ROT47, ROT brute-force (all rotations), custom-offset rotation, affine encode/decode, affine brute-force (function exists but not wired to CLI), repeating-key XOR, single-byte XOR brute-force (in magic only), text and decimal codepoints, and the magic-wand auto-analyser (multi-layer beam search that auto-detects and decodes, scoring by printable ratio + common words + flag regex)
|
|
28
|
+
|
|
29
|
+
This project was inspired by [gchq](https://github.com/gchq/)'s [Cyberchef](https://github.com/gchq/CyberChef), although no code was taken from it.
|
|
30
|
+
|
|
31
|
+
## Installation
|
|
32
|
+
|
|
33
|
+
### Requirements:
|
|
34
|
+
Python >= 3.9
|
|
35
|
+
|
|
36
|
+
In your terminal, run
|
|
37
|
+
|
|
38
|
+
`pip install vicrypt`
|
|
39
|
+
|
|
40
|
+
and you're good to go!
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
All commands take the form `vicrypt <command> <text> [options]`.
|
|
46
|
+
|
|
47
|
+
| Command | Description | Example |
|
|
48
|
+
|---------|-------------|---------|
|
|
49
|
+
| `hex` | Hex encode / decode | `vicrypt hex <text>` |
|
|
50
|
+
| `b64` | Base64 encode / decode | `vicrypt b64 <text>` |
|
|
51
|
+
| `b32` | Base32 encode / decode | `vicrypt b32 <text>` |
|
|
52
|
+
| `b85` | Base85 encode / decode | `vicrypt b85 <text>` |
|
|
53
|
+
| `url` | URL encode / decode | `vicrypt url <text>` |
|
|
54
|
+
| `binary` | Binary encode / decode | `vicrypt binary <text>` |
|
|
55
|
+
| `atbash` | Atbash cipher (self-inverse) | `vicrypt atbash <text>` |
|
|
56
|
+
| `rot` | ROT13 / ROT47, with brute-force | `vicrypt rot <text>` |
|
|
57
|
+
| `affine` | Affine cipher | `vicrypt affine <text> -a 5 -b 8` |
|
|
58
|
+
| `xor` | Repeating-key XOR | `vicrypt xor <text> --key <key>` |
|
|
59
|
+
| `decimal` | Text to/from decimal codepoints | `vicrypt decimal <text>` |
|
|
60
|
+
| `analyse` | Auto-detect and decode (magic wand) | `vicrypt analyse <text>` |
|
|
61
|
+
|
|
62
|
+
### Flags
|
|
63
|
+
|
|
64
|
+
| Flag | Applies to | Meaning |
|
|
65
|
+
|------|-------------|---------|
|
|
66
|
+
| `-d`, `--decode` | codecs, `affine`, `decimal` | decode instead of encode |
|
|
67
|
+
| `--variant` | `rot` | rot13 (default) or rot47 |
|
|
68
|
+
| `-n` | `rot` | rotation offset |
|
|
69
|
+
| `--brute` | `rot` | print all rotations |
|
|
70
|
+
| `-a`, `-b` | `affine` | cipher coefficients (required) |
|
|
71
|
+
| `--key` | `xor` | XOR key (required) |
|
|
72
|
+
| `-d`, `--depth` | `analyse` | max decode layers (default 3) |
|
|
73
|
+
| `-b`, `--beam` | `analyse` | branches kept per layer (default 6) |
|
|
74
|
+
|
|
75
|
+
## AI Use
|
|
76
|
+
|
|
77
|
+
AI was used for help on the pyproject.toml as well as the argparse module, however all encodings and encryptions were researched and coded and implemented by me.
|
vicrypt-0.1.0/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# ViCrypt
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
ViCrypt is a command line cryptography tool (currently a WIP) with basic encoding/decoding functionality! Featuring hex encode/decode, base64 encode/decode, base32 encode/decode, base85 encode/decode, URL encode/decode, binary encode/decode, atbash, ROT13, ROT47, ROT brute-force (all rotations), custom-offset rotation, affine encode/decode, affine brute-force (function exists but not wired to CLI), repeating-key XOR, single-byte XOR brute-force (in magic only), text and decimal codepoints, and the magic-wand auto-analyser (multi-layer beam search that auto-detects and decodes, scoring by printable ratio + common words + flag regex)
|
|
6
|
+
|
|
7
|
+
This project was inspired by [gchq](https://github.com/gchq/)'s [Cyberchef](https://github.com/gchq/CyberChef), although no code was taken from it.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
### Requirements:
|
|
12
|
+
Python >= 3.9
|
|
13
|
+
|
|
14
|
+
In your terminal, run
|
|
15
|
+
|
|
16
|
+
`pip install vicrypt`
|
|
17
|
+
|
|
18
|
+
and you're good to go!
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Usage
|
|
22
|
+
|
|
23
|
+
All commands take the form `vicrypt <command> <text> [options]`.
|
|
24
|
+
|
|
25
|
+
| Command | Description | Example |
|
|
26
|
+
|---------|-------------|---------|
|
|
27
|
+
| `hex` | Hex encode / decode | `vicrypt hex <text>` |
|
|
28
|
+
| `b64` | Base64 encode / decode | `vicrypt b64 <text>` |
|
|
29
|
+
| `b32` | Base32 encode / decode | `vicrypt b32 <text>` |
|
|
30
|
+
| `b85` | Base85 encode / decode | `vicrypt b85 <text>` |
|
|
31
|
+
| `url` | URL encode / decode | `vicrypt url <text>` |
|
|
32
|
+
| `binary` | Binary encode / decode | `vicrypt binary <text>` |
|
|
33
|
+
| `atbash` | Atbash cipher (self-inverse) | `vicrypt atbash <text>` |
|
|
34
|
+
| `rot` | ROT13 / ROT47, with brute-force | `vicrypt rot <text>` |
|
|
35
|
+
| `affine` | Affine cipher | `vicrypt affine <text> -a 5 -b 8` |
|
|
36
|
+
| `xor` | Repeating-key XOR | `vicrypt xor <text> --key <key>` |
|
|
37
|
+
| `decimal` | Text to/from decimal codepoints | `vicrypt decimal <text>` |
|
|
38
|
+
| `analyse` | Auto-detect and decode (magic wand) | `vicrypt analyse <text>` |
|
|
39
|
+
|
|
40
|
+
### Flags
|
|
41
|
+
|
|
42
|
+
| Flag | Applies to | Meaning |
|
|
43
|
+
|------|-------------|---------|
|
|
44
|
+
| `-d`, `--decode` | codecs, `affine`, `decimal` | decode instead of encode |
|
|
45
|
+
| `--variant` | `rot` | rot13 (default) or rot47 |
|
|
46
|
+
| `-n` | `rot` | rotation offset |
|
|
47
|
+
| `--brute` | `rot` | print all rotations |
|
|
48
|
+
| `-a`, `-b` | `affine` | cipher coefficients (required) |
|
|
49
|
+
| `--key` | `xor` | XOR key (required) |
|
|
50
|
+
| `-d`, `--depth` | `analyse` | max decode layers (default 3) |
|
|
51
|
+
| `-b`, `--beam` | `analyse` | branches kept per layer (default 6) |
|
|
52
|
+
|
|
53
|
+
## AI Use
|
|
54
|
+
|
|
55
|
+
AI was used for help on the pyproject.toml as well as the argparse module, however all encodings and encryptions were researched and coded and implemented by me.
|
vicrypt-0.1.0/main.py
ADDED
|
@@ -0,0 +1,388 @@
|
|
|
1
|
+
import re
|
|
2
|
+
def hex_to_text(text, delimiter=""):
|
|
3
|
+
try:
|
|
4
|
+
if delimiter:
|
|
5
|
+
text = text.replace(delimiter, "")
|
|
6
|
+
text = re.sub(r"(?i)0x|\\x|[^0-9a-f]", "", text)
|
|
7
|
+
if "0x" == text[:2]:
|
|
8
|
+
return bytes.fromhex(text[2:]).decode("utf-8")
|
|
9
|
+
else:
|
|
10
|
+
return bytes.fromhex(text).decode("utf-8")
|
|
11
|
+
except Exception:
|
|
12
|
+
return False
|
|
13
|
+
|
|
14
|
+
def text_to_hex(text):
|
|
15
|
+
try:
|
|
16
|
+
return text.encode("utf-8").hex()
|
|
17
|
+
except Exception:
|
|
18
|
+
return False
|
|
19
|
+
|
|
20
|
+
import base64
|
|
21
|
+
def text_to_base64(text):
|
|
22
|
+
try:
|
|
23
|
+
return base64.b64encode(text.encode("utf-8")).decode("utf-8")
|
|
24
|
+
except Exception:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
def base64_to_text(text):
|
|
28
|
+
try:
|
|
29
|
+
return base64.b64decode(text.encode("utf-8")).decode("utf-8")
|
|
30
|
+
except Exception:
|
|
31
|
+
return False
|
|
32
|
+
|
|
33
|
+
def text_to_base32(text):
|
|
34
|
+
try:
|
|
35
|
+
return base64.b32encode(text.encode("utf-8")).decode("utf-8")
|
|
36
|
+
except Exception:
|
|
37
|
+
return False
|
|
38
|
+
|
|
39
|
+
def base32_to_text(text):
|
|
40
|
+
try:
|
|
41
|
+
return base64.b32decode(text.encode("utf-8")).decode("utf-8")
|
|
42
|
+
except Exception:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
def text_to_base85(text):
|
|
46
|
+
try:
|
|
47
|
+
return base64.b85encode(text.encode("utf-8")).decode("utf-8")
|
|
48
|
+
except Exception:
|
|
49
|
+
return False
|
|
50
|
+
|
|
51
|
+
def base85_to_text(text):
|
|
52
|
+
try:
|
|
53
|
+
return base64.b85decode(text.encode("utf-8")).decode("utf-8")
|
|
54
|
+
except Exception:
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
from urllib.parse import quote, unquote
|
|
58
|
+
def text_to_url(text):
|
|
59
|
+
try:
|
|
60
|
+
return quote(text.encode("utf-8"))
|
|
61
|
+
except Exception:
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
def url_to_text(url):
|
|
65
|
+
try:
|
|
66
|
+
return unquote(url)
|
|
67
|
+
except Exception:
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
letters = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"]
|
|
71
|
+
def rot13(text, rot=13, bruteforce=False, rotate_numbers=False):
|
|
72
|
+
if bruteforce:
|
|
73
|
+
result = []
|
|
74
|
+
for i in range(26):
|
|
75
|
+
try:
|
|
76
|
+
result.append(rot13(text, rot=i+1, rotate_numbers=rotate_numbers))
|
|
77
|
+
except Exception:
|
|
78
|
+
return False
|
|
79
|
+
return result
|
|
80
|
+
else:
|
|
81
|
+
try:
|
|
82
|
+
global letters
|
|
83
|
+
ans = ""
|
|
84
|
+
for char in text:
|
|
85
|
+
if char in letters:
|
|
86
|
+
ind = letters[(ord(char.upper())+13+rot)%26]
|
|
87
|
+
if char.isupper():
|
|
88
|
+
ans += ind.upper()
|
|
89
|
+
else:
|
|
90
|
+
ans += ind.lower()
|
|
91
|
+
elif rotate_numbers and char.isdigit():
|
|
92
|
+
ans += str((int(char) + rot)%10)
|
|
93
|
+
else:
|
|
94
|
+
ans += char
|
|
95
|
+
return ans
|
|
96
|
+
except Exception:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def xor(text, key):
|
|
100
|
+
ct = ""
|
|
101
|
+
for i in range(len(text)):
|
|
102
|
+
ct += chr(ord(text[i]) ^ ord(key[i % len(key)]))
|
|
103
|
+
return ct
|
|
104
|
+
|
|
105
|
+
def rot47(text, rot=47, bruteforce=False):
|
|
106
|
+
if bruteforce:
|
|
107
|
+
result = []
|
|
108
|
+
for i in range(94):
|
|
109
|
+
try:
|
|
110
|
+
result.append(rot47(text, rot=i))
|
|
111
|
+
except Exception:
|
|
112
|
+
return False
|
|
113
|
+
return result
|
|
114
|
+
try:
|
|
115
|
+
ans = ""
|
|
116
|
+
for char in text:
|
|
117
|
+
if 33 <= ord(char) <= 126:
|
|
118
|
+
ans += chr((ord(char) - 33 + rot) % 94 + 33)
|
|
119
|
+
else:
|
|
120
|
+
ans += char
|
|
121
|
+
return ans
|
|
122
|
+
except Exception:
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
def affine_encode(text, a, b):
|
|
126
|
+
global letters
|
|
127
|
+
ret = ""
|
|
128
|
+
for char in text:
|
|
129
|
+
if char.lower() in letters:
|
|
130
|
+
if char.isupper():
|
|
131
|
+
ret += letters[((ord(char.upper()) - 65) * a + b)%26].upper()
|
|
132
|
+
else:
|
|
133
|
+
ret += letters[((ord(char.upper()) - 65) * a + b)%26].lower()
|
|
134
|
+
else:
|
|
135
|
+
ret += char
|
|
136
|
+
return ret
|
|
137
|
+
|
|
138
|
+
def atbash(text):
|
|
139
|
+
return affine_encode(text, a=25, b=25)
|
|
140
|
+
|
|
141
|
+
def affine_decode(text, a, b):
|
|
142
|
+
global letters
|
|
143
|
+
if a not in [1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25]:
|
|
144
|
+
return False
|
|
145
|
+
invmod = pow(a, -1, 26)
|
|
146
|
+
ret = ""
|
|
147
|
+
for char in text:
|
|
148
|
+
if char.lower() in letters:
|
|
149
|
+
if char.isupper():
|
|
150
|
+
ret += letters[((ord(char.upper())-65-b)%26 * invmod)%26].upper()
|
|
151
|
+
else:
|
|
152
|
+
ret += letters[((ord(char.upper())-65-b)%26 * invmod)%26].lower()
|
|
153
|
+
else:
|
|
154
|
+
ret += char
|
|
155
|
+
return ret
|
|
156
|
+
|
|
157
|
+
def affine_bruteforce(text, ciphertext):
|
|
158
|
+
for a in [1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25]:
|
|
159
|
+
for b in range(26):
|
|
160
|
+
if affine_encode(text, a, b) == ciphertext:
|
|
161
|
+
return a, b
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
def decimal_to_text(decimallist):
|
|
165
|
+
ret = "".join([chr(decimallist[i]) for i in range(len(decimallist))])
|
|
166
|
+
return ret
|
|
167
|
+
|
|
168
|
+
def text_to_decimal(text):
|
|
169
|
+
ret = [ord(char) for char in text]
|
|
170
|
+
return ret
|
|
171
|
+
|
|
172
|
+
def binary_to_text(binarylist):
|
|
173
|
+
try:
|
|
174
|
+
ret = ""
|
|
175
|
+
for binary in list(binarylist.split()):
|
|
176
|
+
decimal = int(binary, 2)
|
|
177
|
+
ret += chr(decimal)
|
|
178
|
+
return ret
|
|
179
|
+
except Exception:
|
|
180
|
+
return False
|
|
181
|
+
|
|
182
|
+
def text_to_binary(text):
|
|
183
|
+
try:
|
|
184
|
+
ret = []
|
|
185
|
+
for char in text:
|
|
186
|
+
decimal = ord(char)
|
|
187
|
+
if decimal > 255:
|
|
188
|
+
return False
|
|
189
|
+
binary = format(ord(char), "08b")
|
|
190
|
+
ret.append(binary)
|
|
191
|
+
return " ".join(ret)
|
|
192
|
+
except Exception:
|
|
193
|
+
return False
|
|
194
|
+
|
|
195
|
+
import argparse
|
|
196
|
+
import sys
|
|
197
|
+
|
|
198
|
+
codecs = {
|
|
199
|
+
"hex": (text_to_hex, hex_to_text),
|
|
200
|
+
"b64": (text_to_base64, base64_to_text),
|
|
201
|
+
"b32": (text_to_base32, base32_to_text),
|
|
202
|
+
"b85": (text_to_base85, base85_to_text),
|
|
203
|
+
"url": (text_to_url, url_to_text),
|
|
204
|
+
"binary": (text_to_binary, binary_to_text),
|
|
205
|
+
"atbash": (atbash, atbash),
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
common_words = {"the", "and", "flag", "for", "you", "that", "this",
|
|
209
|
+
"with", "ctf", "key", "are", "was", "have", "not"}
|
|
210
|
+
|
|
211
|
+
flag_regex = re.compile(r"[a-zA-Z0-9_]{2,}\{[^}]{2,}\}")
|
|
212
|
+
|
|
213
|
+
def score(s):
|
|
214
|
+
global flag_regex, common_words
|
|
215
|
+
if not s or not isinstance(s, str):
|
|
216
|
+
return 0.0
|
|
217
|
+
printable = sum(1 for c in s if 32 <= ord(c) < 127 or c in "\n\t\r")
|
|
218
|
+
scoar = printable / len(s)
|
|
219
|
+
low = s.lower()
|
|
220
|
+
scoar += 0.2 * sum(w in low for w in common_words)
|
|
221
|
+
if flag_regex.search(s):
|
|
222
|
+
scoar += 3.0
|
|
223
|
+
return scoar
|
|
224
|
+
|
|
225
|
+
def one(fn, label):
|
|
226
|
+
def run(t):
|
|
227
|
+
out = fn(t)
|
|
228
|
+
return [(label, out)] if out and isinstance(out, str) else []
|
|
229
|
+
return run
|
|
230
|
+
|
|
231
|
+
def rot13_all(t):
|
|
232
|
+
res = rot13(t, bruteforce=True)
|
|
233
|
+
return [(f"rot13#{i+1}", v) for i, v in enumerate(res)] if res else []
|
|
234
|
+
|
|
235
|
+
def rot47_all(t):
|
|
236
|
+
res = rot47(t, bruteforce=True)
|
|
237
|
+
return [(f"rot47#{i}", v) for i, v in enumerate(res)] if res else []
|
|
238
|
+
|
|
239
|
+
def affine_all(t):
|
|
240
|
+
out = []
|
|
241
|
+
for a in [1, 3, 5, 7, 9, 11, 15, 17, 19, 21, 23, 25]:
|
|
242
|
+
for b in range(26):
|
|
243
|
+
v = affine_decode(t, a, b)
|
|
244
|
+
if v:
|
|
245
|
+
out.append((f"affine:a{a}b{b}", v))
|
|
246
|
+
return out
|
|
247
|
+
|
|
248
|
+
def xor1(t):
|
|
249
|
+
out = []
|
|
250
|
+
for k in range(1, 256):
|
|
251
|
+
try:
|
|
252
|
+
out.append((f"xor:0x{k:02x}", xor(t, chr(k))))
|
|
253
|
+
except Exception:
|
|
254
|
+
pass
|
|
255
|
+
return out
|
|
256
|
+
|
|
257
|
+
cheap_search = [
|
|
258
|
+
one(hex_to_text, "hex"),
|
|
259
|
+
one(base64_to_text, "b64"),
|
|
260
|
+
one(base32_to_text, "b32"),
|
|
261
|
+
one(base85_to_text, "b85"),
|
|
262
|
+
one(binary_to_text, "binary"),
|
|
263
|
+
one(atbash, "atbash"),
|
|
264
|
+
rot13_all,
|
|
265
|
+
]
|
|
266
|
+
expensive_search = [affine_all, rot47_all, xor1]
|
|
267
|
+
|
|
268
|
+
def magic(data, max_depth=3, beam=6, top=10):
|
|
269
|
+
results = []
|
|
270
|
+
frontier = [(data, [])]
|
|
271
|
+
seen = {data}
|
|
272
|
+
for depth in range(max_depth):
|
|
273
|
+
decoders = cheap_search + (expensive_search if depth == 0 else [])
|
|
274
|
+
scored = []
|
|
275
|
+
for text, recipe in frontier:
|
|
276
|
+
for dec in decoders:
|
|
277
|
+
for label, out in dec(text):
|
|
278
|
+
if not out or not isinstance(out, str) or out in seen:
|
|
279
|
+
continue
|
|
280
|
+
seen.add(out)
|
|
281
|
+
sc = score(out)
|
|
282
|
+
step = recipe + [label]
|
|
283
|
+
results.append((sc, step, out))
|
|
284
|
+
scored.append((sc, out, step))
|
|
285
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
286
|
+
frontier = [(out, step) for _, out, step in scored[:beam]]
|
|
287
|
+
if not frontier:
|
|
288
|
+
break
|
|
289
|
+
results.sort(key=lambda x: x[0], reverse=True)
|
|
290
|
+
return results[:top]
|
|
291
|
+
|
|
292
|
+
def do_codec(args):
|
|
293
|
+
enc, dec = codecs[args.op]
|
|
294
|
+
fn = dec if args.decode else enc
|
|
295
|
+
out = fn(args.text)
|
|
296
|
+
if out is False:
|
|
297
|
+
mode = "decode" if args.decode else "encode"
|
|
298
|
+
print(f"error: {args.op} {mode} failed", file=sys.stderr)
|
|
299
|
+
sys.exit(1)
|
|
300
|
+
print(out)
|
|
301
|
+
|
|
302
|
+
def do_rot(args):
|
|
303
|
+
fn = rot47 if args.variant == "rot47" else rot13
|
|
304
|
+
if args.brute:
|
|
305
|
+
res = fn(args.text, bruteforce=True)
|
|
306
|
+
if not res:
|
|
307
|
+
sys.exit(1)
|
|
308
|
+
for i, v in enumerate(res):
|
|
309
|
+
print(f"{i}: {v}")
|
|
310
|
+
else:
|
|
311
|
+
kwargs = {"rot": args.n} if args.n is not None else {}
|
|
312
|
+
print(fn(args.text, **kwargs))
|
|
313
|
+
|
|
314
|
+
def do_affine(args):
|
|
315
|
+
fn = affine_decode if args.decode else affine_encode
|
|
316
|
+
out = fn(args.text, args.a, args.b)
|
|
317
|
+
if out is False:
|
|
318
|
+
print("error: invalid a (must be coprime with 26)", file=sys.stderr)
|
|
319
|
+
sys.exit(1)
|
|
320
|
+
print(out)
|
|
321
|
+
|
|
322
|
+
def do_xor(args):
|
|
323
|
+
print(xor(args.text, args.key))
|
|
324
|
+
|
|
325
|
+
def do_decimal(args):
|
|
326
|
+
if args.decode:
|
|
327
|
+
nums = [int(x) for x in args.text.split()]
|
|
328
|
+
print(decimal_to_text(nums))
|
|
329
|
+
else:
|
|
330
|
+
print(" ".join(str(n) for n in text_to_decimal(args.text)))
|
|
331
|
+
|
|
332
|
+
def do_analyse(args):
|
|
333
|
+
hits = magic(args.text, max_depth=args.depth, beam=args.beam)
|
|
334
|
+
if not hits:
|
|
335
|
+
print("no candidates found", file=sys.stderr)
|
|
336
|
+
sys.exit(1)
|
|
337
|
+
for sc, recipe, out in hits:
|
|
338
|
+
preview = out if len(out) <= 80 else out[:77] + "..."
|
|
339
|
+
print(f"[{sc:5.2f}] {' -> '.join(recipe):<28} {preview!r}")
|
|
340
|
+
|
|
341
|
+
def build_parser():
|
|
342
|
+
p = argparse.ArgumentParser(prog="vicrypt", description="Crypto/encoding toolset for CTFs")
|
|
343
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
344
|
+
|
|
345
|
+
for name in codecs:
|
|
346
|
+
c = sub.add_parser(name, help=f"{name} encode/decode")
|
|
347
|
+
c.add_argument("text")
|
|
348
|
+
c.add_argument("-d", "--decode", action="store_true")
|
|
349
|
+
c.set_defaults(func=do_codec, op=name)
|
|
350
|
+
|
|
351
|
+
ro = sub.add_parser("rot", help="rot13/rot47")
|
|
352
|
+
ro.add_argument("text")
|
|
353
|
+
ro.add_argument("--variant", choices=["rot13", "rot47"], default="rot13")
|
|
354
|
+
ro.add_argument("-n", type=int, default=None)
|
|
355
|
+
ro.add_argument("--brute", action="store_true")
|
|
356
|
+
ro.set_defaults(func=do_rot)
|
|
357
|
+
|
|
358
|
+
af = sub.add_parser("affine", help="affine cipher")
|
|
359
|
+
af.add_argument("text")
|
|
360
|
+
af.add_argument("-a", type=int, required=True)
|
|
361
|
+
af.add_argument("-b", type=int, required=True)
|
|
362
|
+
af.add_argument("-d", "--decode", action="store_true")
|
|
363
|
+
af.set_defaults(func=do_affine)
|
|
364
|
+
|
|
365
|
+
x = sub.add_parser("xor", help="repeating-key xor")
|
|
366
|
+
x.add_argument("text")
|
|
367
|
+
x.add_argument("--key", required=True)
|
|
368
|
+
x.set_defaults(func=do_xor)
|
|
369
|
+
|
|
370
|
+
de = sub.add_parser("decimal", help="text <-> decimal codepoints")
|
|
371
|
+
de.add_argument("text")
|
|
372
|
+
de.add_argument("-d", "--decode", action="store_true")
|
|
373
|
+
de.set_defaults(func=do_decimal)
|
|
374
|
+
|
|
375
|
+
an = sub.add_parser("analyse", aliases=["magic"], help="auto-detect & decode")
|
|
376
|
+
an.add_argument("text")
|
|
377
|
+
an.add_argument("-d", "--depth", type=int, default=3)
|
|
378
|
+
an.add_argument("-b", "--beam", type=int, default=6)
|
|
379
|
+
an.set_defaults(func=do_analyse)
|
|
380
|
+
|
|
381
|
+
return p
|
|
382
|
+
|
|
383
|
+
def main():
|
|
384
|
+
args = build_parser().parse_args()
|
|
385
|
+
args.func(args)
|
|
386
|
+
|
|
387
|
+
if __name__ == "__main__":
|
|
388
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vicrypt"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A Python cryptography toolset for CTF work"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Vincent Chen" }]
|
|
13
|
+
keywords = ["cryptography", "ctf", "cipher", "encoding", "security"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Security :: Cryptography",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.optional-dependencies]
|
|
24
|
+
dev = ["pytest>=7.0", "ruff>=0.4"]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://github.com/vincentchen18/vicrypt"
|
|
28
|
+
Repository = "https://github.com/vincentchen18/vicrypt"
|
|
29
|
+
Issues = "https://github.com/vincentchen18/vicrypt/issues"
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
vicrypt = "main:main"
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.wheel]
|
|
35
|
+
only-include = ["main.py"]
|
|
36
|
+
|
|
37
|
+
[tool.hatch.build.targets.wheel.sources]
|
|
38
|
+
"main.py" = "vicrypt.py"
|
|
39
|
+
|
|
40
|
+
[tool.ruff]
|
|
41
|
+
line-length = 100
|
|
42
|
+
target-version = "py39"
|
|
43
|
+
|
|
44
|
+
[tool.ruff.lint]
|
|
45
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|