I wanted to write a commandline scipt to generate diceware passwords based on the EFF diceware list.
The script (diceware_password.py
) can be found below. It was originally developed for Python 2, but I want it to be compatible with Python 3 as well (both versions have been tested on my machine).
Running it with no arguments will generate a 6 word password as recommended by the EFF. Additionally, you may also input dice throws made in the "real world".
Note: You need to download the EFF's large wordlist for the script to work. It assumes the default filename, but you can pass other filenames using the --wordlist
flag.
Any feedback is very welcome, especially on the clarity of the code, as well as Python 2/3 compatibility best practices.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
import random
import codecs
import argparse
import six.moves
def large_wordlist_from_file(fname, sep="\t"):
"""Read an EFF compatible large wordlist from file"""
word_dict = {}
with codecs.open(fname, encoding="utf-8", mode="rb") as file_:
while True:
line = file_.readline()
if line:
key, word = line.strip().split(sep)
word_dict[tuple(int(i) for i in key)] = word
else:
break
return word_dict
def roll_dices(nrolls, ndices=5):
"""Rolls a number of dices (default: 5) n times"""
rng = random.SystemRandom()
return tuple(
tuple(rng.randrange(1, 7) for _ in six.moves.range(ndices)) for _ in six.moves.range(nrolls)
)
def validate_dice_rolls(roll_sequences):
"""Validate dice sequences from user input"""
rolls = []
for roll_sequence in roll_sequences:
roll = []
for throw in roll_sequence:
if not (1 <= int(throw) <= 6):
raise ValueError("Each throw must be between 1 and 6.")
roll.append(int(throw))
if len(roll) != 5:
raise ValueError("Each roll sequence has to be of length 5 "
"to work with the EFF wordlist!")
rolls.append(tuple(roll))
return rolls
def get_interactive_dice_rolls():
"""Enter your dice throws on the command line"""
rolls = []
while True:
user_input = six.moves.input("Please enter your dice sequences: ")
roll_sequence = user_input.strip().split(" ")
try:
rolls = validate_dice_rolls(roll_sequence)
break
except ValueError as ex:
print("Failed to parse your input for the following reason: "+ex.message)
user_input = six.moves.input("Do you want to try again? (y/n) ")
user_input = user_input.strip().lower()
if user_input.startswith("n"):
break
return rolls
def main():
"""Commandline script to generate diceware passwords"""
parser = argparse.ArgumentParser(
description="Generate diceware passwords with N words. "
"EFF recommendation is N >= 6."
)
parser.add_argument(
"n", metavar="N", type=int, nargs="?", default=6,
help="number of diceware words (default: 6)",
)
parser.add_argument(
"--real-dice", type=str, nargs="*", metavar="DICE_SEQUENCE",
help="instead of using PRNG, use your given dice throws. "
"Please group them in N DICE_SEQUENCEs of length 5 seperated "
"by a space"
)
parser.add_argument(
"--wordlist", type=str, nargs="?", default="eff_large_wordlist.txt",
help="The diceware wordlist to use. This script assumes the list to "
"contain one word per line where each word is preceeded by its "
"'dice' index. Index and word are separated by a single tab. "
"(default: \"eff_large_wordlist.txt\")"
)
parser.add_argument(
"--interactive", action="store_true", default=False,
help="Interactive mode allows you to enter your dice sequences via "
"command prompt in order to avoid traces in your bash history"
)
args = parser.parse_args()
words = large_wordlist_from_file(args.wordlist, "\t")
rolls = []
if args.real_dice is not None:
rolls = validate_dice_rolls(args.real_dice)
elif args.interactive:
rolls = get_interactive_dice_rolls()
else:
rolls = roll_dices(args.n, 5)
if rolls:
# print the result
try:
print(" ".join(words[roll] for roll in rolls))
except KeyError:
print(list(words.keys()))
if __name__ == "__main__":
main()