12 Copyright (c) 2011 - 2014, 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>
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.
47 # random.SystemRandom() should be cryptographically secure
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 "
54 "Continuing with less-secure generator.\n")
57 # Python 3 compatibility
58 if sys
.version
[0] == "3":
62 def validate_options(parser
, options
, args
):
64 Given a set of command line options, performs various validation checks
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")
74 parser
.error("Too many arguments.")
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]:
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")
90 options
.wordfile
= locate_wordfile()
92 if not options
.wordfile
:
93 sys
.stderr
.write("Could not find a word file, or word file does "
97 def locate_wordfile():
98 static_default
= os
.path
.join(
99 os
.path
.dirname(os
.path
.abspath(__file__
)),
102 common_word_files
= ["/usr/share/cracklib/cracklib-small",
105 "/usr/share/dict/words"]
107 for wfile
in common_word_files
:
108 if os
.path
.exists(wfile
):
112 def generate_wordlist(wordfile
=None,
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)
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
130 thisword
= line
.strip()
131 if regexp
.match(thisword
) is not None:
132 words
.append(thisword
)
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
147 # Maybe should be a defaultdict, but this reduces dependencies
148 for word
in wordlist
:
150 worddict
[word
[0]].append(word
)
152 worddict
[word
[0]] = [word
, ]
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."
171 print("Your word list contains %i words, or 2^%0.2f words."
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)
189 for letter
in acrostic
:
191 words
.append(rng().choice(worddict
[letter
]))
193 sys
.stderr
.write("No words found starting with " + letter
+ "\n")
198 def generate_xkcdpassword(wordlist
,
204 Generate an XKCD-style password from the words in wordlist.
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")
215 # generate the worddict if we are looking for acrostics
217 worddict
= wordlist_to_worddict(wordlist
)
219 # useful if driving the logic from other code
222 passwd
= delim
.join(rng().sample(wordlist
, n_words
))
224 passwd
= delim
.join(find_acrostic(acrostic
, worddict
))
228 # else, interactive session
230 custom_n_words
= raw_input("Enter number of words (default 6): ")
233 n_words
= int(custom_n_words
)
235 n_words
= len(acrostic
)
239 while accepted
.lower() not in ["y", "yes"]:
241 passwd
= delim
.join(rng().sample(wordlist
, n_words
))
243 passwd
= delim
.join(find_acrostic(acrostic
, worddict
))
244 print("Generated: ", passwd
)
245 accepted
= raw_input("Accept? [yN] ")
252 usage
= "usage: %prog [options]"
253 parser
= optparse
.OptionParser(usage
)
255 parser
.add_option("-w", "--wordfile", dest
="wordfile",
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",
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",
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",
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
)
295 verbose_reports(len(my_wordlist
),
299 count
= options
.count
301 print(generate_xkcdpassword(my_wordlist
,
302 interactive
=options
.interactive
,
303 n_words
=options
.numwords
,
304 acrostic
=options
.acrostic
,
305 delim
=options
.delim
))
309 if __name__
== '__main__':