Switch to upstream source from tarball ‘xkcdpass_1.2.4.orig.tar.gz’.
[debian_xkcdpass.git] / xkcdpass / xkcd_password.py
blob4f40158e0d1f621478a8f73af78b7c636a8d109a
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 - 2015, 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>,
23 Ben Finney <ben@benfinney.id.au>
25 Redistribution and use in source and binary forms, with or without
26 modification, are permitted provided that the following conditions are met:
27 * Redistributions of source code must retain the above copyright
28 notice, this list of conditions and the following disclaimer.
29 * Redistributions in binary form must reproduce the above copyright
30 notice, this list of conditions and the following disclaimer in the
31 documentation and/or other materials provided with the distribution.
32 * Neither the name of the <organization> nor the
33 names of its contributors may be used to endorse or promote products
34 derived from this software without specific prior written permission.
36 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
37 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
38 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
39 DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
40 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
41 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
42 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
43 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
44 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
45 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
46 """
48 # random.SystemRandom() should be cryptographically secure
49 try:
50 rng = random.SystemRandom
51 except AttributeError:
52 sys.stderr.write("WARNING: System does not support cryptographically "
53 "secure random number generator or you are using Python "
54 "version < 2.4.\n"
55 "Continuing with less-secure generator.\n")
56 rng = random.Random
58 # Python 3 compatibility
59 if sys.version[0] == "3":
60 raw_input = input
63 def validate_options(parser, options, args):
64 """
65 Given a set of command line options, performs various validation checks
66 """
68 if options.max_length < options.min_length:
69 sys.stderr.write("The maximum length of a word can not be "
70 "lesser then minimum length.\n"
71 "Check the specified settings.\n")
72 sys.exit(1)
74 if len(args) > 1:
75 parser.error("Too many arguments.")
77 if len(args) == 1:
78 # supporting either -w or args[0] for wordlist, but not both
79 if options.wordfile is None:
80 options.wordfile = args[0]
81 elif options.wordfile == args[0]:
82 pass
83 else:
84 parser.error("Conflicting values for wordlist: " + args[0] +
85 " and " + options.wordfile)
86 if options.wordfile is not None:
87 if not os.path.exists(os.path.abspath(options.wordfile)):
88 sys.stderr.write("Could not open the specified word file.\n")
89 sys.exit(1)
90 else:
91 options.wordfile = locate_wordfile()
93 if not options.wordfile:
94 sys.stderr.write("Could not find a word file, or word file does "
95 "not exist.\n")
96 sys.exit(1)
98 def locate_wordfile():
99 static_default = os.path.join(
100 os.path.dirname(os.path.abspath(__file__)),
101 'static',
102 'default.txt')
103 common_word_files = ["/usr/share/cracklib/cracklib-small",
104 static_default,
105 "/usr/dict/words",
106 "/usr/share/dict/words"]
108 for wfile in common_word_files:
109 if os.path.exists(wfile):
110 return wfile
113 def generate_wordlist(wordfile=None,
114 min_length=5,
115 max_length=9,
116 valid_chars='.'):
118 Generate a word list from either a kwarg wordfile, or a system default
119 valid_chars is a regular expression match condition (default - all chars)
122 words = []
124 regexp = re.compile("^%s{%i,%i}$" % (valid_chars, min_length, max_length))
126 # At this point wordfile is set
127 wordfile = os.path.expanduser(wordfile) # just to be sure
128 wlf = open(wordfile)
130 for line in wlf:
131 thisword = line.strip()
132 if regexp.match(thisword) is not None:
133 words.append(thisword)
135 wlf.close()
137 return words
140 def wordlist_to_worddict(wordlist):
142 Takes a wordlist and returns a dictionary keyed by the first letter of
143 the words. Used for acrostic pass phrase generation
146 worddict = {}
148 # Maybe should be a defaultdict, but this reduces dependencies
149 for word in wordlist:
150 try:
151 worddict[word[0]].append(word)
152 except KeyError:
153 worddict[word[0]] = [word, ]
155 return worddict
158 def verbose_reports(length, numwords, wordfile):
160 Report entropy metrics based on word list and requested password size"
163 bits = math.log(length, 2)
165 print("The supplied word list is located at %s."
166 % os.path.abspath(wordfile))
168 if int(bits) == bits:
169 print("Your word list contains %i words, or 2^%i words."
170 % (length, bits))
171 else:
172 print("Your word list contains %i words, or 2^%0.2f words."
173 % (length, bits))
175 print("A %i word password from this list will have roughly "
176 "%i (%0.2f * %i) bits of entropy," %
177 (numwords, int(bits * numwords), bits, numwords)),
178 print("assuming truly random word selection.")
181 def find_acrostic(acrostic, worddict):
183 Constrain choice of words to those beginning with the letters of the
184 given word (acrostic).
185 Second argument is a dictionary (output of wordlist_to_worddict)
188 words = []
190 for letter in acrostic:
191 try:
192 words.append(rng().choice(worddict[letter]))
193 except KeyError:
194 sys.stderr.write("No words found starting with " + letter + "\n")
195 sys.exit(1)
196 return words
199 def generate_xkcdpassword(wordlist,
200 n_words=6,
201 interactive=False,
202 acrostic=False,
203 delim=" "):
205 Generate an XKCD-style password from the words in wordlist.
208 passwd = False
210 if len(wordlist) < n_words:
211 sys.stderr.write("Could not get enough words!\n"
212 "This could be a result of either your wordfile\n"
213 "being too small, or your settings too strict.\n")
214 sys.exit(1)
216 # generate the worddict if we are looking for acrostics
217 if acrostic:
218 worddict = wordlist_to_worddict(wordlist)
220 # useful if driving the logic from other code
221 if not interactive:
222 if not acrostic:
223 passwd = delim.join(rng().sample(wordlist, n_words))
224 else:
225 passwd = delim.join(find_acrostic(acrostic, worddict))
227 return passwd
229 # else, interactive session
230 if not acrostic:
231 custom_n_words = raw_input("Enter number of words (default 6): ")
233 if custom_n_words:
234 n_words = int(custom_n_words)
235 else:
236 n_words = len(acrostic)
238 accepted = "n"
240 while accepted.lower() not in ["y", "yes"]:
241 if not acrostic:
242 passwd = delim.join(rng().sample(wordlist, n_words))
243 else:
244 passwd = delim.join(find_acrostic(acrostic, worddict))
245 print("Generated: ", passwd)
246 accepted = raw_input("Accept? [yN] ")
248 return passwd
251 def main():
252 count = 1
253 usage = "usage: %prog [options]"
254 parser = optparse.OptionParser(usage)
256 parser.add_option(
257 "-w", "--wordfile",
258 dest="wordfile", default=None, metavar="WORDFILE",
259 help=(
260 "Specify that the file WORDFILE contains"
261 " the list of valid words from which to generate passphrases."))
262 parser.add_option(
263 "--min",
264 dest="min_length", type="int", default=5, metavar="MIN_LENGTH",
265 help="Generate passphrases containing at least MIN_LENGTH words.")
266 parser.add_option(
267 "--max",
268 dest="max_length", type="int", default=9, metavar="MAX_LENGTH",
269 help="Generate passphrases containing at most MAX_LENGTH words.")
270 parser.add_option(
271 "-n", "--numwords",
272 dest="numwords", type="int", default=6, metavar="NUM_WORDS",
273 help="Generate passphrases containing exactly NUM_WORDS words.")
274 parser.add_option(
275 "-i", "--interactive",
276 action="store_true", dest="interactive", default=False,
277 help=(
278 "Generate and output a passphrase,"
279 " query the user to accept it,"
280 " and loop until one is accepted."))
281 parser.add_option(
282 "-v", "--valid_chars",
283 dest="valid_chars", default=".", metavar="VALID_CHARS",
284 help=(
285 "Limit passphrases to only include words matching"
286 " the regex pattern VALID_CHARS (e.g. '[a-z]')."))
287 parser.add_option(
288 "-V", "--verbose",
289 action="store_true", dest="verbose", default=False,
290 help="Report various metrics for given options.")
291 parser.add_option(
292 "-a", "--acrostic",
293 dest="acrostic", default=False,
294 help="Generate passphrases with an acrostic matching ACROSTIC.")
295 parser.add_option(
296 "-c", "--count",
297 dest="count", type="int", default=1, metavar="COUNT",
298 help="Generate COUNT passphrases.")
299 parser.add_option(
300 "-d", "--delimiter",
301 dest="delim", default=" ", metavar="DELIM",
302 help="Separate words within a passphrase with DELIM.")
304 (options, args) = parser.parse_args()
305 validate_options(parser, options, args)
307 my_wordlist = generate_wordlist(wordfile=options.wordfile,
308 min_length=options.min_length,
309 max_length=options.max_length,
310 valid_chars=options.valid_chars)
312 if options.verbose:
313 verbose_reports(len(my_wordlist),
314 options.numwords,
315 options.wordfile)
317 count = options.count
318 while count > 0:
319 print(generate_xkcdpassword(my_wordlist,
320 interactive=options.interactive,
321 n_words=options.numwords,
322 acrostic=options.acrostic,
323 delim=options.delim))
324 count -= 1
327 if __name__ == '__main__':
328 main()