/
sanepass
executable file
·135 lines (123 loc) · 5.4 KB
/
sanepass
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
#!/usr/bin/python3
# jbin - Joe's miscellaneous scripts, tools and configs
# sanepass: Create fixed-entropy passwords with a nice alphabet
# Copyright (C) 2018-2020 Johannes Bauer
#
# This file is part of jbin.
#
# jbin is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; this program is ONLY licensed under
# version 3 of the License, later versions are explicitly excluded.
#
# jbin is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with jbin. If not, see <http://www.gnu.org/licenses/>.
#
# Johannes Bauer <JohannesBauer@gmx.de>
import os
import sys
import string
import math
from FriendlyArgumentParser import FriendlyArgumentParser
filtered_alphabet = set("".join([
"1liI",
"B8",
"O0o",
"UV",
"uv",
"2Z",
"6G",
]))
filtered_us_alphabet = set("zZyY")
source_alphabet = {
"all": string.ascii_uppercase + string.ascii_lowercase + string.digits,
"chars": string.ascii_uppercase + string.ascii_lowercase,
"phone": string.ascii_lowercase,
}
parser = FriendlyArgumentParser()
parser.add_argument("-u", "--filter-us-keyboard", action = "store_true", help = "Filter characters that are on different places on a QWERTZ vs. QWERTY keyboard.")
parser.add_argument("-n", "--no-filter-ambiguous", action = "store_true", help = "Do not filter characters that could be ambiguous, such as 1/l/I or O/0, from the passwords.")
parser.add_argument("-a", "--alphabet", choices = [ "all", "chars", "phone" ], default = "all", help = "Specifies the input alphabet to use. Defaults to %(default)s.")
parser.add_argument("-c", "--consecutive-filter", choices = [ "lalala", "qwertz" ], help = "Character alphabets can vary according to the given rules, for example passphrases can be generated that generate only consonant/vovel combinations ('lalala') or avoid thick finger combinations (for typing on a smart phone).")
parser.add_argument("--thick-finger-distance", metavar = "keycnt", type = float, default = 2, help = "Euclidian distance between two keys that has to minimally given to pass the 'thick finger test'. Defaults to %(default).1f")
parser.add_argument("-v", "--verbose", action = "store_true", help = "Increase output verbosity.")
#parser.add_argument("-l", "--min-length", type = int, help = "Minimum length of characters the password must have.")
parser.add_argument("entropy_bits", metavar = "bits", type = int, nargs = "?", default = 128, help = "Bits of entropy the password should contain. Defaults to %(default)d bit.")
args = parser.parse_args(sys.argv[1:])
bytecnt = (args.entropy_bits + 7) // 8
passphrase = os.urandom(bytecnt)
intphrase = int.from_bytes(passphrase, "little")
alphabet = source_alphabet[args.alphabet]
alphabet = set(alphabet)
if not args.no_filter_ambiguous:
alphabet -= filtered_alphabet
if args.filter_us_keyboard:
alphabet -= filtered_us_alphabet
lalphabet = list(alphabet)
if args.consecutive_filter is None:
next_alphabet = { char: lalphabet for char in alphabet }
elif args.consecutive_filter == "lalala":
consonants = [ char for char in alphabet if char.lower() in "bcdfghjklmnpqrstvwxyz" ]
vovels = [ char for char in alphabet if char.lower() in "aeiou" ]
rest = [ char for char in alphabet if ((char not in consonants) and (char not in vovels)) ]
vovels_and_rest = vovels + rest
next_alphabet = { }
for char in alphabet:
if char in consonants:
next_alphabet[char] = vovels_and_rest
else:
next_alphabet[char] = consonants
elif args.consecutive_filter == "qwertz":
keyboard_rows = [
(0, "1234567890"),
(0.5, "qwertzuiop"),
(0.8, "asdfghjkl"),
(1.1, "yxcvbnm"),
]
ignored_chars = set(alphabet)
coordinates = { }
for (y, (x_offset, row)) in enumerate(keyboard_rows):
for (charno, char) in enumerate(row):
x = x_offset + charno
if char in alphabet:
coordinates[char] = (x, y)
ignored_chars -= set([ char ])
if char.upper() in alphabet:
coordinates[char.upper()] = (x, y)
ignored_chars -= set([ char.upper() ])
ignored_chars = list(ignored_chars)
next_alphabet = { }
for char in alphabet:
if char in coordinates:
(x, y) = coordinates[char]
next_alphabet[char] = list(ignored_chars)
for (other_char, (other_x, other_y)) in coordinates.items():
dist = math.sqrt((x - other_x) ** 2 + (y - other_y) ** 2)
if dist >= args.thick_finger_distance:
next_alphabet[char].append(other_char)
else:
next_alphabet[char] = lalphabet
else:
raise NotImplementedError(args.consecutive_filter)
alphabet = lalphabet
current_alphabet = alphabet
if args.verbose:
print("Used alphabet: %s" % ("".join(sorted(lalphabet))))
for (char, follow_alphabet) in sorted(next_alphabet.items()):
print("%s: %s" % (char, "".join(sorted(follow_alphabet))))
for (char, follow_alphabet) in next_alphabet.items():
if len(follow_alphabet) < 2:
print("There are only %d characters (\"%s\") that can follow '%s', this leads to uncreatable passphrases. Aborting." % (len(follow_alphabet), "".join(sorted(follow_alphabet)), char))
sys.exit(1)
decoded = ""
while intphrase > 0:
(intphrase, nextchar) = divmod(intphrase, len(current_alphabet))
nextchar = current_alphabet[nextchar]
current_alphabet = next_alphabet[nextchar]
decoded += nextchar
print(decoded)