Skip to content

Instantly share code, notes, and snippets.

@Gumnos
Last active June 6, 2021 00:49
Show Gist options
  • Save Gumnos/fceaf405b0ffd8bfd325 to your computer and use it in GitHub Desktop.
Save Gumnos/fceaf405b0ffd8bfd325 to your computer and use it in GitHub Desktop.
Generate a pass-phrase like "correct horse battery staple" from a dictionary of words
#!/usr/bin/env python
import locale
import random
import re
import sys
from codecs import encode
from copy import copy
from optparse import OptionParser
EXIT_SUCCESS = 0
EXIT_ERROR = 1
punct_re = re.compile(r"(?!\w)(?!\s).")
DEFAULT_PASSWORD_COUNT = 1
DEFAULT_NUMBER_OF_WORDS = 4
DEFAULT_DICT_PATH = "/usr/share/dict/words"
DEFAULT_MIN = 4
DEFAULT_MAX = 6
EXCLUSIONS = [
# (name, test)
("upper", lambda s: any(c for c in s if c.isupper())),
("numeric", lambda s: any(c for c in s if c.isdigit())),
("punct", lambda s: punct_re.search(s)),
]
UNITS = [ # (multiplier, name_singular, name_plural)
(1, "second", "seconds"),
(60, "minute", "minutes"),
(60, "hour", "hours"),
(24, "day", "days"),
(365.24, "year", "years"),
(10, "decade", "decades"),
(10, "century", "centuries"),
(10, "millenium", "millenia"),
]
rot13 = lambda s: encode(s, "rot-13")
def build_parser():
parser = OptionParser()
parser.add_option("-d", "--dict", "--dictionary",
help="The default dictionary to use (default: %s)" % DEFAULT_DICT_PATH,
action="store",
dest="dictionary",
metavar="FILE",
default=DEFAULT_DICT_PATH,
)
parser.add_option("-x", "--exclude",
help="A file containing (optionally ROT13'ed) words to exclude such as profanity",
action="store",
dest="exclude",
metavar="FILE",
default=None,
)
parser.add_option("-c", "--count",
help="The number of passwords to output (default: %i)" % DEFAULT_PASSWORD_COUNT,
action="store",
type="int",
dest="count",
default=DEFAULT_PASSWORD_COUNT,
)
parser.add_option("-i", "-n",
help="The number of words to output (default: %i)" % DEFAULT_NUMBER_OF_WORDS,
action="store",
type="int",
dest="length",
default=DEFAULT_NUMBER_OF_WORDS,
)
parser.add_option("--min", "--min-len", "--min-length",
help="The minimum length of word to consider (default: %i)" % DEFAULT_MIN,
action="store",
type="int",
dest="min",
default=DEFAULT_MIN,
)
parser.add_option("--max", "--max-len", "--max-length",
help="The maximum length of word to consider (default: %i)" % DEFAULT_MAX,
action="store",
type="int",
dest="max",
default=DEFAULT_MAX,
)
for name, test in EXCLUSIONS:
parser.add_option("--include-%s" % name,
help="Include words containing %s chars" % name,
action="store_true",
default=False,
dest=name,
)
parser.add_option("-v", "--verbose",
help="Report additional statistics",
action="store_true",
dest="verbose",
default=False,
)
parser.add_option("-g", "--guesses-per-second", "--guesses", "--gps",
help="Guesses per second (for additional statistics)",
action="store",
type="int",
dest="guesses_per_second",
default=1000,
)
return parser
def main():
parser = build_parser()
options, args = parser.parse_args()
for test, desc in [
(options.count < 1, "Count must be positive"),
(options.length < 1, "Number of words must be positive"),
(options.min < 1, "Minimum length must be positive"),
(options.max < options.min, "Maximum length must not be less than the minimum"),
]:
if test:
sys.stderr.write("%s\n" % desc)
parser.print_help()
return EXIT_ERROR
exclusions = set()
if options.exclude:
try:
f = open(options.exclude)
except IOError:
sys.stderr.write("Unable to open exclusion file %s\n" % options.exclude)
else:
try:
for word in f:
word = word.strip().lower()
exclusions.add(word)
exclusions.add(rot13(word))
finally:
f.close()
try:
f = open(options.dictionary)
except IOError:
sys.stderr.write("Unable to open %s\n" % options.dictionary)
return EXIT_ERROR
else:
try:
word_list = []
for word in f:
word = word.rstrip() # get rid of the newline
if word in exclusions: continue
if options.min <= len(word) <= options.max:
# it's of the right length
for attr, exclusion_test in EXCLUSIONS:
if not getattr(options, attr):
if exclusion_test(word):
break
else:
word_list.append(word)
finally:
f.close()
for i in range(options.count):
result = " ".join(random.sample(word_list, options.length))
print(result)
if options.verbose:
possibilities = len(word_list) ** options.length
print("%i word dictionary, %i word(s) = %i possible" % (
len(word_list),
options.length,
possibilities,
))
seconds = possibilities / float(options.guesses_per_second)
last_multiplier, last_name1, last_name_pl = UNITS[0]
current_multiplier = last_multiplier
for multiplier, name1, name_pl in UNITS:
current_multiplier *= multiplier
if current_multiplier > seconds:
break
last_multiplier = current_multiplier
last_name1 = name1
last_name_pl = name_pl
if options.guesses_per_second == 1:
guess = "guess"
else:
guess = "guesses"
dur = seconds / last_multiplier
if abs(dur - 1.0) < 0.01: # arbitrary EPSILON
last_name = last_name1
else:
last_name = last_name_pl
print("%0.1f %s at %i %s per second" % (
dur,
last_name,
options.guesses_per_second,
guess,
))
return EXIT_SUCCESS
if __name__ == "__main__":
sys.exit(main())
@Gumnos
Copy link
Author

Gumnos commented Sep 8, 2015

On multiple occasions I've had to generate reasonably strong passwords that still needed to be conveyed easily over the phone. I whipped this up to use the XKCD algorithm to generate a bunch of possible passwords so I don't have to hand-crank the code every time. Example invocations:

$ passphrasegen.py # generate one passphrase
pedant tanker bails creaky
$ passphrasegen.py -c 4 # generate 4 passphrases
coupes sticks boll fewer
foul skips phased grader
mottle mask dipper idols
horsed cadets digit ruse
$ passphrasegen.py --min=6 --max=8 # words must be 6 <= len(word) <= 8
clearing mushier rivetted planners
$ passphrasegen.py --help
Usage: passphrasegen.py [options]

Options:
  -h, --help            show this help message and exit
  -d PATH, --dict=PATH, --dictionary=PATH
                        The default dictionary to use (default:
                        /usr/share/dict/words)
  -c COUNT, --count=COUNT
                        The number of passwords to output (default: 1)
  -i LENGTH             The number of words to output (default: 4)
  --min=MIN, --min-len=MIN, --min-length=MIN
                        The minimum length of word to consider (default: 4)
  --max=MAX, --max-len=MAX, --max-length=MAX
                        The maximum length of word to consider (default: 6)
  --include-upper       Include words containing upper chars
  --include-numeric     Include words containing numeric chars
  --include-punct       Include words containing punct chars
  -v, --verbose         Report additional statistics

It assumes the dictionary is located at /usr/share/dict/words which it is on most Linux/BSD/Mac boxes. However, you can use the --dict option to specify an alternate dictionary if you want (or need to on a Windows box)

Throwing this out there in case other folks need a fast way to generate such passwords.

@Gumnos
Copy link
Author

Gumnos commented Sep 20, 2015

Updated to include time-to-crack in the --verbose output based on the --guesses-per-second, parameter (defaults to 1000g/s based on the XKCD comic)

@Gumnos
Copy link
Author

Gumnos commented Sep 20, 2015

Should work with Python2 and Python3 based on my testing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment