-
Notifications
You must be signed in to change notification settings - Fork 0
/
kpwgen
executable file
·121 lines (109 loc) · 4.09 KB
/
kpwgen
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
#! /usr/bin/python3
# Copyright (C) 2019 Karl Otness
#
# This program 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, either version 3 of the License, or
# (at your option) any later version.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
from random import SystemRandom
from math import log, ceil
import itertools as it
import sys
import argparse
RANDOM = SystemRandom()
WORD_DICT = "/usr/share/kpwgen/words.txt"
SUBST = {
'a': ['@'],
'e': ['3'],
'i': ['!'],
's': ['$'],
't': ['7'],
}
def compute_word_entropy(dict_words, expand_p):
"""Computes the per-word entropy of our dictionary.
Parameters
----------
dict_words: A sequence of dictionary words.
expand_p: The "expansion" probability. Makes use of SUBST to
compute resulting entropy."""
entropy = 0
word_prob = 1 / len(dict_words)
for word in dict_words:
options = []
for char in word:
if char in SUBST:
opts = [1-expand_p]
for _ in SUBST[char]:
opts.append(expand_p/len(SUBST[char]))
options.append(opts)
else:
options.append([1])
possibilities = it.product(*options)
for poss in possibilities:
prod = 1
for psv in poss:
prod *= psv
prob = word_prob * prod
if prob != 0:
entropy += prob * log(prob, 2)
return -entropy
def expand_word(word, expand_p):
"""Randomly choose an expansion of given word."""
chars = []
for char in word:
if char in SUBST and RANDOM.random() < expand_p:
chars.append(RANDOM.choice(SUBST[char]))
else:
chars.append(char)
return ''.join(chars)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description="""Word-based password generator.
The passwords are based on the XKCD password scheme with
words sourced from the system dictionary. See:
https://xkcd.com/936/
""")
parser.add_argument("--verbose", "-v", action='store_true',
help="print extra information about the password")
parser.add_argument("--words", type=int, default=6,
help="the number of words in the password")
parser.add_argument("--entropy", type=float, default=None,
help="the minimum entropy in the password")
parser.add_argument("--expand", type=float, default=0.25,
help="probability of character replacement")
ARGS = parser.parse_args()
EXPAND = ARGS.expand
with open(WORD_DICT) as dict_file:
# Load dictionary words from file. Respect length bounds
WORD_SET = (w.strip().lower() for w in dict_file)
WORDS = list(WORD_SET)
if not WORDS:
print("No words loaded", file=sys.stderr)
exit(1)
WORD_ENTROPY = compute_word_entropy(WORDS, EXPAND)
NUM_GEN_WORDS = ARGS.words
if ARGS.entropy:
NUM_GEN_WORDS = ceil(ARGS.entropy / WORD_ENTROPY)
SEL_WORDS = [RANDOM.choice(WORDS) for i in range(NUM_GEN_WORDS)]
PLAIN = ''.join(w.capitalize() for w in SEL_WORDS)
EXPANDED = PLAIN
if EXPAND > 0:
EXPANDED = ''.join(expand_word(w, EXPAND).capitalize()
for w in SEL_WORDS)
if not ARGS.verbose:
print(EXPANDED)
else:
print("Entropy: {:.4f} bits".format(WORD_ENTROPY * NUM_GEN_WORDS))
print("Plain: {}".format(PLAIN))
if EXPAND > 0:
print("Expanded: {}".format(EXPANDED))
DIFF_STR = "".join("^" if a != b else " "
for (a, b) in zip(PLAIN, EXPANDED))
print("Diff: " + DIFF_STR)