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
.exists(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
.exists(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("^%s{%i,%i}$" % (valid_chars
, min_length
, max_length
))
115 # At this point wordfile is set
116 wordfile
= os
.path
.expanduser(wordfile
) # just to be sure
120 thisword
= line
.strip()
121 if regexp
.match(thisword
) is not None:
122 words
.append(thisword
)
126 return list(set(words
)) # deduplicate, just in case
129 def wordlist_to_worddict(wordlist
):
131 Takes a wordlist and returns a dictionary keyed by the first letter of
132 the words. Used for acrostic pass phrase generation
137 # Maybe should be a defaultdict, but this reduces dependencies
138 for word
in wordlist
:
140 worddict
[word
[0]].append(word
)
142 worddict
[word
[0]] = [word
, ]
147 def verbose_reports(length
, numwords
, wordfile
):
149 Report entropy metrics based on word list and requested password size"
152 bits
= math
.log(length
, 2)
154 print("The supplied word list is located at %s."
155 % os
.path
.abspath(wordfile
))
157 if int(bits
) == bits
:
158 print("Your word list contains %i words, or 2^%i words."
161 print("Your word list contains %i words, or 2^%0.2f words."
164 print("A %i word password from this list will have roughly "
165 "%i (%0.2f * %i) bits of entropy," %
166 (numwords
, int(bits
* numwords
), bits
, numwords
)),
167 print("assuming truly random word selection.")
170 def find_acrostic(acrostic
, worddict
):
172 Constrain choice of words to those beginning with the letters of the
173 given word (acrostic).
174 Second argument is a dictionary (output of wordlist_to_worddict)
179 for letter
in acrostic
:
181 words
.append(rng().choice(worddict
[letter
]))
183 sys
.stderr
.write("No words found starting with " + letter
+ "\n")
188 def choose_words(wordlist
, numwords
):
190 for i
in xrange(numwords
):
191 s
.append(rng().choice(wordlist
))
195 def generate_xkcdpassword(wordlist
,
201 Generate an XKCD-style password from the words in wordlist.
206 # generate the worddict if we are looking for acrostics
208 worddict
= wordlist_to_worddict(wordlist
)
210 # useful if driving the logic from other code
213 passwd
= delimiter
.join(choose_words(wordlist
, numwords
))
215 passwd
= delimiter
.join(find_acrostic(acrostic
, worddict
))
219 # else, interactive session
221 custom_n_words
= raw_input("Enter number of words (default 6): ")
224 numwords
= int(custom_n_words
)
226 numwords
= len(acrostic
)
230 while accepted
.lower() not in ["y", "yes"]:
232 passwd
= delimiter
.join(choose_words(wordlist
, numwords
))
234 passwd
= delimiter
.join(find_acrostic(acrostic
, worddict
))
235 print("Generated: ", passwd
)
236 accepted
= raw_input("Accept? [yN] ")
241 def emit_passwords(wordlist
, options
):
242 """ Generate the specified number of passwords and output them. """
243 count
= options
.count
245 print(generate_xkcdpassword(
247 interactive
=options
.interactive
,
248 numwords
=options
.numwords
,
249 acrostic
=options
.acrostic
,
250 delimiter
=options
.delimiter
))
254 class XkcdPassArgumentParser(argparse
.ArgumentParser
):
255 """ Command-line argument parser for this program. """
257 def __init__(self
, *args
, **kwargs
):
258 super(XkcdPassArgumentParser
, self
).__init
__(*args
, **kwargs
)
260 self
._add
_arguments
()
262 def _add_arguments(self
):
263 """ Add the arguments needed for this program. """
266 dest
="wordfile", default
=None, metavar
="WORDFILE",
268 "Specify that the file WORDFILE contains the list"
269 " of valid words from which to generate passphrases."))
272 dest
="min_length", type=int, default
=5, metavar
="MIN_LENGTH",
273 help="Generate passphrases containing at least MIN_LENGTH words.")
276 dest
="max_length", type=int, default
=9, metavar
="MAX_LENGTH",
277 help="Generate passphrases containing at most MAX_LENGTH words.")
280 dest
="numwords", type=int, default
=6, metavar
="NUM_WORDS",
281 help="Generate passphrases containing exactly NUM_WORDS words.")
283 "-i", "--interactive",
284 action
="store_true", dest
="interactive", default
=False,
286 "Generate and output a passphrase, query the user to"
287 " accept it, and loop until one is accepted."))
289 "-v", "--valid-chars",
290 dest
="valid_chars", default
=".", metavar
="VALID_CHARS",
292 "Limit passphrases to only include words matching the regex"
293 " pattern VALID_CHARS (e.g. '[a-z]')."))
296 action
="store_true", dest
="verbose", default
=False,
297 help="Report various metrics for given options.")
300 dest
="acrostic", default
=False,
301 help="Generate passphrases with an acrostic matching ACROSTIC.")
304 dest
="count", type=int, default
=1, metavar
="COUNT",
305 help="Generate COUNT passphrases.")
308 dest
="delimiter", default
=" ", metavar
="DELIM",
309 help="Separate words within a passphrase with DELIM.")
312 action
="store_true", dest
="allow_weak_rng", default
=False,
314 "Allow fallback to weak RNG if the "
315 "system does not support cryptographically secure RNG. "
316 "Only use this if you know what you are doing."))
320 """ Mainline code for this program. """
328 program_name
= os
.path
.basename(argv
[0])
329 parser
= XkcdPassArgumentParser(prog
=program_name
)
331 options
= parser
.parse_args(argv
[1:])
332 validate_options(parser
, options
)
334 my_wordlist
= generate_wordlist(
335 wordfile
=options
.wordfile
,
336 min_length
=options
.min_length
,
337 max_length
=options
.max_length
,
338 valid_chars
=options
.valid_chars
)
346 emit_passwords(my_wordlist
, options
)
348 except SystemExit as exc
:
349 exit_status
= exc
.code
354 if __name__
== '__main__':
355 exit_status
= main(sys
.argv
)
356 sys
.exit(exit_status
)