12 Copyright (c) 2011 - 2015, Steven Tobin and Contributors.
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.
48 # random.SystemRandom() should be cryptographically secure
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 "
55 "Continuing with less-secure generator.\n")
58 # Python 3 compatibility
59 if sys
.version
[0] == "3":
63 def validate_options(parser
, options
, args
):
65 Given a set of command line options, performs various validation checks
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")
75 parser
.error("Too many arguments.")
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]:
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")
91 options
.wordfile
= locate_wordfile()
93 if not options
.wordfile
:
94 sys
.stderr
.write("Could not find a word file, or word file does "
98 def locate_wordfile():
99 static_default
= os
.path
.join(
100 os
.path
.dirname(os
.path
.abspath(__file__
)),
103 common_word_files
= ["/usr/share/cracklib/cracklib-small",
106 "/usr/share/dict/words"]
108 for wfile
in common_word_files
:
109 if os
.path
.exists(wfile
):
113 def generate_wordlist(wordfile
=None,
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)
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
131 thisword
= line
.strip()
132 if regexp
.match(thisword
) is not None:
133 words
.append(thisword
)
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
148 # Maybe should be a defaultdict, but this reduces dependencies
149 for word
in wordlist
:
151 worddict
[word
[0]].append(word
)
153 worddict
[word
[0]] = [word
, ]
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."
172 print("Your word list contains %i words, or 2^%0.2f words."
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)
190 for letter
in acrostic
:
192 words
.append(rng().choice(worddict
[letter
]))
194 sys
.stderr
.write("No words found starting with " + letter
+ "\n")
199 def generate_xkcdpassword(wordlist
,
205 Generate an XKCD-style password from the words in wordlist.
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")
216 # generate the worddict if we are looking for acrostics
218 worddict
= wordlist_to_worddict(wordlist
)
220 # useful if driving the logic from other code
223 passwd
= delim
.join(rng().sample(wordlist
, n_words
))
225 passwd
= delim
.join(find_acrostic(acrostic
, worddict
))
229 # else, interactive session
231 custom_n_words
= raw_input("Enter number of words (default 6): ")
234 n_words
= int(custom_n_words
)
236 n_words
= len(acrostic
)
240 while accepted
.lower() not in ["y", "yes"]:
242 passwd
= delim
.join(rng().sample(wordlist
, n_words
))
244 passwd
= delim
.join(find_acrostic(acrostic
, worddict
))
245 print("Generated: ", passwd
)
246 accepted
= raw_input("Accept? [yN] ")
253 usage
= "usage: %prog [options]"
254 parser
= optparse
.OptionParser(usage
)
258 dest
="wordfile", default
=None, metavar
="WORDFILE",
260 "Specify that the file WORDFILE contains"
261 " the list of valid words from which to generate passphrases."))
264 dest
="min_length", type="int", default
=5, metavar
="MIN_LENGTH",
265 help="Generate passphrases containing at least MIN_LENGTH words.")
268 dest
="max_length", type="int", default
=9, metavar
="MAX_LENGTH",
269 help="Generate passphrases containing at most MAX_LENGTH words.")
272 dest
="numwords", type="int", default
=6, metavar
="NUM_WORDS",
273 help="Generate passphrases containing exactly NUM_WORDS words.")
275 "-i", "--interactive",
276 action
="store_true", dest
="interactive", default
=False,
278 "Generate and output a passphrase,"
279 " query the user to accept it,"
280 " and loop until one is accepted."))
282 "-v", "--valid_chars",
283 dest
="valid_chars", default
=".", metavar
="VALID_CHARS",
285 "Limit passphrases to only include words matching"
286 " the regex pattern VALID_CHARS (e.g. '[a-z]')."))
289 action
="store_true", dest
="verbose", default
=False,
290 help="Report various metrics for given options.")
293 dest
="acrostic", default
=False,
294 help="Generate passphrases with an acrostic matching ACROSTIC.")
297 dest
="count", type="int", default
=1, metavar
="COUNT",
298 help="Generate COUNT passphrases.")
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
)
313 verbose_reports(len(my_wordlist
),
317 count
= options
.count
319 print(generate_xkcdpassword(my_wordlist
,
320 interactive
=options
.interactive
,
321 n_words
=options
.numwords
,
322 acrostic
=options
.acrostic
,
323 delim
=options
.delim
))
327 if __name__
== '__main__':