13 Copyright (c) 2011 - 2016, Steven Tobin and Contributors.
16 Redistribution and use in source and binary forms, with or without
17 modification, are permitted provided that the following conditions are met:
18 * Redistributions of source code must retain the above copyright
19 notice, this list of conditions and the following disclaimer.
20 * Redistributions in binary form must reproduce the above copyright
21 notice, this list of conditions and the following disclaimer in the
22 documentation and/or other materials provided with the distribution.
23 * Neither the name of the <organization> nor the
24 names of its contributors may be used to endorse or promote products
25 derived from this software without specific prior written permission.
27 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
28 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
29 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
30 DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
31 DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
32 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
33 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
34 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
35 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
36 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
39 # random.SystemRandom() should be cryptographically secure
41 rng
= random
.SystemRandom
42 except AttributeError as ex
:
43 sys
.stderr
.write("WARNING: System does not support cryptographically "
44 "secure random number generator or you are using Python "
46 if "XKCDPASS_ALLOW_WEAKRNG" in os
.environ
or \
47 "--allow-weak-rng" in sys
.argv
:
48 sys
.stderr
.write("Continuing with less-secure generator.\n")
54 # Python 3 compatibility
55 if sys
.version_info
[0] >= 3:
60 def validate_options(parser
, options
):
62 Given a parsed collection of options, performs various validation checks.
65 if options
.max_length
< options
.min_length
:
66 sys
.stderr
.write("The maximum length of a word can not be "
67 "less than the minimum length.\n"
68 "Check the specified settings.\n")
71 if options
.wordfile
is not None:
72 if not os
.path
.isfile(os
.path
.abspath(options
.wordfile
)):
73 sys
.stderr
.write("Could not open the specified word file.\n")
76 options
.wordfile
= locate_wordfile()
78 if not options
.wordfile
:
79 sys
.stderr
.write("Could not find a word file, or word file does "
84 def locate_wordfile():
85 static_default
= os
.path
.join(
86 os
.path
.dirname(os
.path
.abspath(__file__
)),
89 common_word_files
= ["/usr/share/cracklib/cracklib-small",
92 "/usr/share/dict/words"]
94 for wfile
in common_word_files
:
95 if os
.path
.isfile(wfile
):
99 def generate_wordlist(wordfile
=None,
104 Generate a word list from either a kwarg wordfile, or a system default
105 valid_chars is a regular expression match condition (default - all chars)
109 wordfile
= locate_wordfile()
113 regexp
= re
.compile("^{0}{{{1},{2}}}$".format(valid_chars
,
117 # At this point wordfile is set
118 wordfile
= os
.path
.expanduser(wordfile
) # just to be sure
120 # read words from file into wordlist
121 with
open(wordfile
) as wlf
:
123 thisword
= line
.strip()
124 if regexp
.match(thisword
) is not None:
125 words
.append(thisword
)
127 return list(set(words
)) # deduplicate, just in case
130 def wordlist_to_worddict(wordlist
):
132 Takes a wordlist and returns a dictionary keyed by the first letter of
133 the words. Used for acrostic pass phrase generation
138 # Maybe should be a defaultdict, but this reduces dependencies
139 for word
in wordlist
:
141 worddict
[word
[0]].append(word
)
143 worddict
[word
[0]] = [word
, ]
148 def verbose_reports(length
, numwords
, wordfile
):
150 Report entropy metrics based on word list and requested password size"
153 bits
= math
.log(length
, 2)
155 print("The supplied word list is located at"
156 " {0}.".format(os
.path
.abspath(wordfile
)))
158 if int(bits
) == bits
:
159 print("Your word list contains {0} words, or 2^{1} words."
160 "".format(length
, bits
))
162 print("Your word list contains {0} words, or 2^{1:.2f} words."
163 "".format(length
, bits
))
165 print("A {0} word password from this list will have roughly "
166 "{1} ({2:.2f} * {3}) bits of entropy,"
167 "".format(numwords
, int(bits
* numwords
), bits
, numwords
)),
168 print("assuming truly random word selection.")
171 def find_acrostic(acrostic
, worddict
):
173 Constrain choice of words to those beginning with the letters of the
174 given word (acrostic).
175 Second argument is a dictionary (output of wordlist_to_worddict)
180 for letter
in acrostic
:
182 words
.append(rng().choice(worddict
[letter
]))
184 sys
.stderr
.write("No words found starting with " + letter
+ "\n")
189 def choose_words(wordlist
, numwords
):
191 Choose numwords randomly from wordlist
194 return [rng().choice(wordlist
) for i
in xrange(numwords
)]
197 def try_input(prompt
, validate
):
199 Suppress stack trace on user cancel and validate input with supplied
204 answer
= raw_input(prompt
)
205 except (KeyboardInterrupt, EOFError):
211 return validate(answer
)
214 def generate_xkcdpassword(wordlist
,
220 Generate an XKCD-style password from the words in wordlist.
225 # generate the worddict if we are looking for acrostics
227 worddict
= wordlist_to_worddict(wordlist
)
229 # useful if driving the logic from other code
232 passwd
= delimiter
.join(choose_words(wordlist
, numwords
))
234 passwd
= delimiter
.join(find_acrostic(acrostic
, worddict
))
238 # else, interactive session
239 # define input validators
240 def n_words_validator(answer
):
242 Validate custom number of words input
245 if isinstance(answer
, str) and len(answer
) == 0:
253 sys
.stderr
.write("Please enter a positive integer\n")
256 def accepted_validator(answer
):
257 return answer
.lower().strip() in ["y", "yes"]
260 n_words_prompt
= ("Enter number of words (default {0}):"
261 " ".format(numwords
))
263 numwords
= try_input(n_words_prompt
, n_words_validator
)
265 numwords
= len(acrostic
)
267 # generate passwords until the user accepts
272 passwd
= delimiter
.join(choose_words(wordlist
, numwords
))
274 passwd
= delimiter
.join(find_acrostic(acrostic
, worddict
))
275 print("Generated: " + passwd
)
276 accepted
= try_input("Accept? [yN] ", accepted_validator
)
281 def emit_passwords(wordlist
, options
):
282 """ Generate the specified number of passwords and output them. """
283 count
= options
.count
285 print(generate_xkcdpassword(
287 interactive
=options
.interactive
,
288 numwords
=options
.numwords
,
289 acrostic
=options
.acrostic
,
290 delimiter
=options
.delimiter
))
294 class XkcdPassArgumentParser(argparse
.ArgumentParser
):
295 """ Command-line argument parser for this program. """
297 def __init__(self
, *args
, **kwargs
):
298 super(XkcdPassArgumentParser
, self
).__init
__(*args
, **kwargs
)
300 self
._add
_arguments
()
302 def _add_arguments(self
):
303 """ Add the arguments needed for this program. """
306 dest
="wordfile", default
=None, metavar
="WORDFILE",
308 "Specify that the file WORDFILE contains the list"
309 " of valid words from which to generate passphrases."))
312 dest
="min_length", type=int, default
=5, metavar
="MIN_LENGTH",
313 help="Generate passphrases containing at least MIN_LENGTH words.")
316 dest
="max_length", type=int, default
=9, metavar
="MAX_LENGTH",
317 help="Generate passphrases containing at most MAX_LENGTH words.")
320 dest
="numwords", type=int, default
=6, metavar
="NUM_WORDS",
321 help="Generate passphrases containing exactly NUM_WORDS words.")
323 "-i", "--interactive",
324 action
="store_true", dest
="interactive", default
=False,
326 "Generate and output a passphrase, query the user to"
327 " accept it, and loop until one is accepted."))
329 "-v", "--valid-chars",
330 dest
="valid_chars", default
=".", metavar
="VALID_CHARS",
332 "Limit passphrases to only include words matching the regex"
333 " pattern VALID_CHARS (e.g. '[a-z]')."))
336 action
="store_true", dest
="verbose", default
=False,
337 help="Report various metrics for given options.")
340 dest
="acrostic", default
=False,
341 help="Generate passphrases with an acrostic matching ACROSTIC.")
344 dest
="count", type=int, default
=1, metavar
="COUNT",
345 help="Generate COUNT passphrases.")
348 dest
="delimiter", default
=" ", metavar
="DELIM",
349 help="Separate words within a passphrase with DELIM.")
352 action
="store_true", dest
="allow_weak_rng", default
=False,
354 "Allow fallback to weak RNG if the "
355 "system does not support cryptographically secure RNG. "
356 "Only use this if you know what you are doing."))
360 """ Mainline code for this program. """
368 program_name
= os
.path
.basename(argv
[0])
369 parser
= XkcdPassArgumentParser(prog
=program_name
)
371 options
= parser
.parse_args(argv
[1:])
372 validate_options(parser
, options
)
374 my_wordlist
= generate_wordlist(
375 wordfile
=options
.wordfile
,
376 min_length
=options
.min_length
,
377 max_length
=options
.max_length
,
378 valid_chars
=options
.valid_chars
)
386 emit_passwords(my_wordlist
, options
)
388 except SystemExit as exc
:
389 exit_status
= exc
.code
394 if __name__
== '__main__':
395 exit_status
= main(sys
.argv
)
396 sys
.exit(exit_status
)