zora-cli 0.1.2__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.
- zora/__init__.py +1 -0
- zora/cli.py +226 -0
- zora_cli-0.1.2.dist-info/METADATA +1021 -0
- zora_cli-0.1.2.dist-info/RECORD +8 -0
- zora_cli-0.1.2.dist-info/WHEEL +5 -0
- zora_cli-0.1.2.dist-info/entry_points.txt +2 -0
- zora_cli-0.1.2.dist-info/licenses/LICENSE +216 -0
- zora_cli-0.1.2.dist-info/top_level.txt +1 -0
zora/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.2"
|
zora/cli.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import time
|
|
3
|
+
import string
|
|
4
|
+
import argparse
|
|
5
|
+
import random, secrets
|
|
6
|
+
from colorama import Fore, init; init(autoreset=True);
|
|
7
|
+
|
|
8
|
+
def positive_int(value):
|
|
9
|
+
value = int(value)
|
|
10
|
+
if value <= 0:
|
|
11
|
+
raise argparse.ArgumentTypeError("must be greater than 0")
|
|
12
|
+
return value
|
|
13
|
+
|
|
14
|
+
CHARSETS = {
|
|
15
|
+
"digits": string.digits,
|
|
16
|
+
"letters": string.ascii_letters,
|
|
17
|
+
"lower": string.ascii_lowercase,
|
|
18
|
+
"upper": string.ascii_uppercase,
|
|
19
|
+
"hex": string.hexdigits,
|
|
20
|
+
"oct": string.octdigits,
|
|
21
|
+
"bin": "01",
|
|
22
|
+
"special": string.punctuation,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class Zora:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.lines = []
|
|
28
|
+
|
|
29
|
+
def run(self):
|
|
30
|
+
self.start_timer()
|
|
31
|
+
self.parse_args()
|
|
32
|
+
self.build_charset()
|
|
33
|
+
self.generate_keys()
|
|
34
|
+
self.output()
|
|
35
|
+
self.show_timer()
|
|
36
|
+
self.show_entropy()
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def build_charset(self):
|
|
40
|
+
value = self.args.charset
|
|
41
|
+
charset = ""
|
|
42
|
+
|
|
43
|
+
while value:
|
|
44
|
+
if value.startswith("@"):
|
|
45
|
+
value = value[1:]
|
|
46
|
+
|
|
47
|
+
matches = [
|
|
48
|
+
name for name in CHARSETS
|
|
49
|
+
if value.startswith(name)
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
if not matches:
|
|
53
|
+
self.parser.error(f"unknown charset preset near: @{value}")
|
|
54
|
+
|
|
55
|
+
name = max(matches, key=len)
|
|
56
|
+
charset += CHARSETS[name]
|
|
57
|
+
value = value[len(name):]
|
|
58
|
+
|
|
59
|
+
else:
|
|
60
|
+
charset += value[0]
|
|
61
|
+
value = value[1:]
|
|
62
|
+
|
|
63
|
+
self.charset = "".join(dict.fromkeys(charset))
|
|
64
|
+
|
|
65
|
+
def parse_args(self):
|
|
66
|
+
"""
|
|
67
|
+
Parses all the arguments
|
|
68
|
+
"""
|
|
69
|
+
self.parser = argparse.ArgumentParser(
|
|
70
|
+
description="Generate random AUTH keys."
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
sc = self.parser.add_argument
|
|
74
|
+
|
|
75
|
+
#
|
|
76
|
+
# ARGUMENTS
|
|
77
|
+
#
|
|
78
|
+
sc("--charset-list", action="store_true", help="Shows available charset lists")
|
|
79
|
+
|
|
80
|
+
sc("length", type=positive_int, help="Length of the key", nargs="?")
|
|
81
|
+
sc("--seed", type=str, help="Seed for generation")
|
|
82
|
+
sc("--prefix", type=str, default="", help="Prefix the generated keys")
|
|
83
|
+
sc("--suffix", type=str, default="", help="Suffix the generated keys")
|
|
84
|
+
sc("--group", type=positive_int, help="Add separator each X chars (use --sep [str] to set separator)")
|
|
85
|
+
sc("--sep", type=str, default="-", help="Grouping separator string")
|
|
86
|
+
|
|
87
|
+
sc("-n", "--count", type=positive_int, default=1, help="Number of keys to generate")
|
|
88
|
+
sc("-o", "--output", type=str, metavar="FILE", help="Write output to file")
|
|
89
|
+
|
|
90
|
+
sc("--unsafe", action="store_true", help="Use PRNG instead of CSPRNG for generation")
|
|
91
|
+
sc("-q", "--quiet", action="store_true", help="Suppress non-essential output")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
sc("--charset", default="@letters", help="Character set to use (see --charset-list)")
|
|
95
|
+
|
|
96
|
+
self.args = self.parser.parse_args()
|
|
97
|
+
|
|
98
|
+
if self.args.length is None and not self.args.charset_list:
|
|
99
|
+
self.parser.error("length is required")
|
|
100
|
+
|
|
101
|
+
# RANDOM MODULE DISALLOWED + SEED SET
|
|
102
|
+
if self.args.seed is not None and not self.args.unsafe:
|
|
103
|
+
self.parser.error("--seed requires --unsafe (seeding is insecure)")
|
|
104
|
+
|
|
105
|
+
if self.args.charset_list:
|
|
106
|
+
self.parser.exit(0, ("\nAvailable charsets to use:\n @"+("\n @".join(CHARSETS.keys())) + f"\n\n Use as:\n zora --charset @digits = For digits only charset\n zora --charset @letters@digits = For alphanumeric charset\n zora --charset @hex\"XYZ\" = For hexadecimal charset extended with letters X, Y and Z\n"))
|
|
107
|
+
|
|
108
|
+
if self.args.unsafe and not self.args.quiet:
|
|
109
|
+
print(f"{Fore.RED}Program will output cryptographically insecure keys.\n")
|
|
110
|
+
|
|
111
|
+
def generate_keys(self):
|
|
112
|
+
"""
|
|
113
|
+
Generates all the keys
|
|
114
|
+
"""
|
|
115
|
+
def gen():
|
|
116
|
+
if self.args.unsafe:
|
|
117
|
+
return "".join(random.choice(self.charset) for _ in range(self.args.length))
|
|
118
|
+
else:
|
|
119
|
+
return "".join(secrets.choice(self.charset) for _ in range(self.args.length))
|
|
120
|
+
|
|
121
|
+
if self.args.seed is not None:
|
|
122
|
+
random.seed(self.args.seed)
|
|
123
|
+
|
|
124
|
+
for _ in range(self.args.count):
|
|
125
|
+
key = gen()
|
|
126
|
+
if self.args.group is not None:
|
|
127
|
+
key = self.args.sep.join(
|
|
128
|
+
key[i:i + self.args.group]
|
|
129
|
+
for i in range(0, len(key), self.args.group)
|
|
130
|
+
)
|
|
131
|
+
self.lines.append(
|
|
132
|
+
f"{self.args.prefix}{key}{self.args.suffix}"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
def output(self):
|
|
136
|
+
"""
|
|
137
|
+
Prints out / Saves the keys (lines)
|
|
138
|
+
"""
|
|
139
|
+
if self.args.output:
|
|
140
|
+
with open(self.args.output, "w") as f:
|
|
141
|
+
f.write("\n".join(self.lines))
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
print("\n".join(_ for _ in self.lines))
|
|
145
|
+
|
|
146
|
+
def start_timer(self):
|
|
147
|
+
"""
|
|
148
|
+
Starts the timer
|
|
149
|
+
"""
|
|
150
|
+
self.time_start = time.perf_counter()
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def show_timer(self, ms_threshold: int = 1000):
|
|
154
|
+
"""
|
|
155
|
+
Stops the timer and prints elapsed time
|
|
156
|
+
"""
|
|
157
|
+
if self.args.quiet:
|
|
158
|
+
return
|
|
159
|
+
|
|
160
|
+
if not hasattr(self, "time_start"):
|
|
161
|
+
print("Timer has not been started properly.")
|
|
162
|
+
return
|
|
163
|
+
|
|
164
|
+
elapsed = time.perf_counter() - self.time_start
|
|
165
|
+
|
|
166
|
+
total_ms = int(elapsed * 1000)
|
|
167
|
+
total_seconds = total_ms // 1000
|
|
168
|
+
|
|
169
|
+
if total_ms < ms_threshold:
|
|
170
|
+
print(f"\n{Fore.GREEN}Timer: {total_ms}ms elapsed")
|
|
171
|
+
else:
|
|
172
|
+
hours = total_seconds // 3600
|
|
173
|
+
minutes = (total_seconds % 3600) // 60
|
|
174
|
+
seconds = total_seconds % 60
|
|
175
|
+
|
|
176
|
+
parts = []
|
|
177
|
+
|
|
178
|
+
if hours > 0:
|
|
179
|
+
parts.append(f"{hours}h")
|
|
180
|
+
|
|
181
|
+
if minutes > 0:
|
|
182
|
+
parts.append(f"{minutes}m")
|
|
183
|
+
|
|
184
|
+
if seconds > 0 or not parts:
|
|
185
|
+
parts.append(f"{seconds}s")
|
|
186
|
+
|
|
187
|
+
print(f"\n{Fore.GREEN}Timer: {' '.join(parts)} elapsed")
|
|
188
|
+
|
|
189
|
+
def show_entropy(self):
|
|
190
|
+
"""
|
|
191
|
+
Shows final charset, entropy, strength and used generator."""
|
|
192
|
+
# MARK PRNG's STRIKETHROUGH
|
|
193
|
+
STRIKETHROUGH = "\033[9m" if self.args.unsafe else ""
|
|
194
|
+
if not self.args.quiet:
|
|
195
|
+
entropy = self.entropy()
|
|
196
|
+
print(f"{Fore.LIGHTBLUE_EX}Charset: {len(self.charset)}")
|
|
197
|
+
print(f"{STRIKETHROUGH}{Fore.CYAN}Entropy: {entropy:.2f} bits")
|
|
198
|
+
print(f"{STRIKETHROUGH}{Fore.CYAN}Strength: {self.strength(entropy)}")
|
|
199
|
+
print(f"{Fore.LIGHTCYAN_EX}Generator: {Fore.RED + 'PRNG' if self.args.unsafe else Fore.GREEN + 'CSPRNG'}")
|
|
200
|
+
|
|
201
|
+
def entropy(self):
|
|
202
|
+
return self.args.length * math.log2(len(self.charset))
|
|
203
|
+
|
|
204
|
+
def strength(self, entropy):
|
|
205
|
+
"""
|
|
206
|
+
Finds out the strength according to the entropy
|
|
207
|
+
"""
|
|
208
|
+
if entropy < 40:
|
|
209
|
+
return Fore.RED + "Very weak"
|
|
210
|
+
elif entropy < 60:
|
|
211
|
+
return Fore.LIGHTRED_EX + "Weak"
|
|
212
|
+
elif entropy < 80:
|
|
213
|
+
return Fore.YELLOW + "Moderate"
|
|
214
|
+
elif entropy < 100:
|
|
215
|
+
return Fore.LIGHTGREEN_EX + "Strong"
|
|
216
|
+
else:
|
|
217
|
+
return Fore.GREEN + "Very strong"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def main():
|
|
221
|
+
zora = Zora()
|
|
222
|
+
zora.run()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
main()
|