Switch to upstream source from tarball ‘xkcdpass_1.2.3.orig.tar.gz’.
[debian_xkcdpass.git] / xkcdpass / xkcd_password.py
blob5194a19c10a6933fbf869ce43547c90ec0312953
1 #!/usr/bin/env python
2 # encoding: utf-8
4 import random
5 import os
6 import optparse
7 import re
8 import math
9 import sys
11 __LICENSE__ = """
12 Copyright (c) 2011 - 2014, Steven Tobin and Contributors.
13 All rights reserved.
15 Contributors: Steven Tobin,
16 Rob Lanphier <robla@robla.net>,
17 Mithrandir <mithrandiragain@lavabit.com>,
18 Daniel Beecham <daniel@lunix.se>,
19 Kim Slawson <kimslawson@gmail.com>,
20 Stanislav Bytsko <zbstof@gmail.com>,
21 Lowe Thiderman <lowe.thiderman@gmail.com>,
22 Daniil Baturin <daniil@baturin.org>
24 Redistribution and use in source and binary forms, with or without
25 modification, are permitted provided that the following conditions are met:
26 * Redistributions of source code must retain the above copyright
27 notice, this list of conditions and the following disclaimer.
28 * Redistributions in binary form must reproduce the above copyright
29 notice, this list of conditions and the following disclaimer in the
30 documentation and/or other materials provided with the distribution.
31 * Neither the name of the <organization> nor the
32 names of its contributors may be used to endorse or promote products
33 derived from this software without specific prior written permission.
35 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
36 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
37 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
38 DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
39 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
40 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
41 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
42 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
43 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
44 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
45 """
47 # random.SystemRandom() should be cryptographically secure
48 try:
49 rng = random.SystemRandom
50 except AttributeError:
51 sys.stderr.write("WARNING: System does not support cryptographically "
52 "secure random number generator or you are using Python "
53 "version < 2.4.\n"
54 "Continuing with less-secure generator.\n")
55 rng = random.Random
57 # Python 3 compatibility
58 if sys.version[0] == "3":
59 raw_input = input
62 def validate_options(parser, options, args):
63 """
64 Given a set of command line options, performs various validation checks
65 """
67 if options.max_length < options.min_length:
68 sys.stderr.write("The maximum length of a word can not be "
69 "lesser then minimum length.\n"
70 "Check the specified settings.\n")
71 sys.exit(1)
73 if len(args) > 1:
74 parser.error("Too many arguments.")
76 if len(args) == 1:
77 # supporting either -w or args[0] for wordlist, but not both
78 if options.wordfile is None:
79 options.wordfile = args[0]
80 elif options.wordfile == args[0]:
81 pass
82 else:
83 parser.error("Conflicting values for wordlist: " + args[0] +
84 " and " + options.wordfile)
85 if options.wordfile is not None:
86 if not os.path.exists(os.path.abspath(options.wordfile)):
87 sys.stderr.write("Could not open the specified word file.\n")
88 sys.exit(1)
89 else:
90 options.wordfile = locate_wordfile()
92 if not options.wordfile:
93 sys.stderr.write("Could not find a word file, or word file does "
94 "not exist.\n")
95 sys.exit(1)
97 def locate_wordfile():
98 static_default = os.path.join(
99 os.path.dirname(os.path.abspath(__file__)),
100 'static',
101 'default.txt')
102 common_word_files = ["/usr/share/cracklib/cracklib-small",
103 static_default,
104 "/usr/dict/words",
105 "/usr/share/dict/words"]
107 for wfile in common_word_files:
108 if os.path.exists(wfile):
109 return wfile
112 def generate_wordlist(wordfile=None,
113 min_length=5,
114 max_length=9,
115 valid_chars='.'):
117 Generate a word list from either a kwarg wordfile, or a system default
118 valid_chars is a regular expression match condition (default - all chars)
121 words = []
123 regexp = re.compile("^%s{%i,%i}$" % (valid_chars, min_length, max_length))
125 # At this point wordfile is set
126 wordfile = os.path.expanduser(wordfile) # just to be sure
127 wlf = open(wordfile)
129 for line in wlf:
130 thisword = line.strip()
131 if regexp.match(thisword) is not None:
132 words.append(thisword)
134 wlf.close()
136 return words
139 def wordlist_to_worddict(wordlist):
141 Takes a wordlist and returns a dictionary keyed by the first letter of
142 the words. Used for acrostic pass phrase generation
145 worddict = {}
147 # Maybe should be a defaultdict, but this reduces dependencies
148 for word in wordlist:
149 try:
150 worddict[word[0]].append(word)
151 except KeyError:
152 worddict[word[0]] = [word, ]
154 return worddict
157 def verbose_reports(length, numwords, wordfile):
159 Report entropy metrics based on word list and requested password size"
162 bits = math.log(length, 2)
164 print("The supplied word list is located at %s."
165 % os.path.abspath(wordfile))
167 if int(bits) == bits:
168 print("Your word list contains %i words, or 2^%i words."
169 % (length, bits))
170 else:
171 print("Your word list contains %i words, or 2^%0.2f words."
172 % (length, bits))
174 print("A %i word password from this list will have roughly "
175 "%i (%0.2f * %i) bits of entropy," %
176 (numwords, int(bits * numwords), bits, numwords)),
177 print("assuming truly random word selection.")
180 def find_acrostic(acrostic, worddict):
182 Constrain choice of words to those beginning with the letters of the
183 given word (acrostic).
184 Second argument is a dictionary (output of wordlist_to_worddict)
187 words = []
189 for letter in acrostic:
190 try:
191 words.append(rng().choice(worddict[letter]))
192 except KeyError:
193 sys.stderr.write("No words found starting with " + letter + "\n")
194 sys.exit(1)
195 return words
198 def generate_xkcdpassword(wordlist,
199 n_words=6,
200 interactive=False,
201 acrostic=False,
202 delim=" "):
204 Generate an XKCD-style password from the words in wordlist.
207 passwd = False
209 if len(wordlist) < n_words:
210 sys.stderr.write("Could not get enough words!\n"
211 "This could be a result of either your wordfile\n"
212 "being too small, or your settings too strict.\n")
213 sys.exit(1)
215 # generate the worddict if we are looking for acrostics
216 if acrostic:
217 worddict = wordlist_to_worddict(wordlist)
219 # useful if driving the logic from other code
220 if not interactive:
221 if not acrostic:
222 passwd = delim.join(rng().sample(wordlist, n_words))
223 else:
224 passwd = delim.join(find_acrostic(acrostic, worddict))
226 return passwd
228 # else, interactive session
229 if not acrostic:
230 custom_n_words = raw_input("Enter number of words (default 6): ")
232 if custom_n_words:
233 n_words = int(custom_n_words)
234 else:
235 n_words = len(acrostic)
237 accepted = "n"
239 while accepted.lower() not in ["y", "yes"]:
240 if not acrostic:
241 passwd = delim.join(rng().sample(wordlist, n_words))
242 else:
243 passwd = delim.join(find_acrostic(acrostic, worddict))
244 print("Generated: ", passwd)
245 accepted = raw_input("Accept? [yN] ")
247 return passwd
250 def main():
251 count = 1
252 usage = "usage: %prog [options]"
253 parser = optparse.OptionParser(usage)
255 parser.add_option("-w", "--wordfile", dest="wordfile",
256 default=None,
257 help="List of valid words for password")
258 parser.add_option("--min", dest="min_length",
259 default=5, type="int",
260 help="Minimum length of words to make password")
261 parser.add_option("--max", dest="max_length",
262 default=9, type="int",
263 help="Maximum length of words to make password")
264 parser.add_option("-n", "--numwords", dest="numwords",
265 default=6, type="int",
266 help="Number of words to make password")
267 parser.add_option("-i", "--interactive", dest="interactive",
268 default=False, action="store_true",
269 help="Interactively select a password")
270 parser.add_option("-v", "--valid_chars", dest="valid_chars",
271 default='.',
272 help="Valid chars, using regexp style (e.g. '[a-z]')")
273 parser.add_option("-V", "--verbose", dest="verbose",
274 default=False, action="store_true",
275 help="Report various metrics for given options")
276 parser.add_option("-a", "--acrostic", dest="acrostic",
277 default=False,
278 help="Acrostic to constrain word choices")
279 parser.add_option("-c", "--count", dest="count",
280 default=1, type="int",
281 help="number of passwords to generate")
282 parser.add_option("-d", "--delimiter", dest="delim",
283 default=" ",
284 help="separator character between words")
286 (options, args) = parser.parse_args()
287 validate_options(parser, options, args)
289 my_wordlist = generate_wordlist(wordfile=options.wordfile,
290 min_length=options.min_length,
291 max_length=options.max_length,
292 valid_chars=options.valid_chars)
294 if options.verbose:
295 verbose_reports(len(my_wordlist),
296 options.numwords,
297 options.wordfile)
299 count = options.count
300 while count > 0:
301 print(generate_xkcdpassword(my_wordlist,
302 interactive=options.interactive,
303 n_words=options.numwords,
304 acrostic=options.acrostic,
305 delim=options.delim))
306 count -= 1
309 if __name__ == '__main__':
310 main()