Last active
December 20, 2022 03:28
-
-
Save philpennock/cf2021e35baa5c8edb92d07af904da23 to your computer and use it in GitHub Desktop.
makepassword: make a decentish password
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
""" | |
makepassword: make a decentish password | |
The -D/--dice option makes a diceware passphrase wordlist; the -B/--bitcoin | |
option makes a random passphrase wordlist using the Bitcoin BIP39 dictionaries. | |
The --skey option uses the RFC1751 word-list (but not that algorithm). | |
Without any of these options, a more traditional password is generated. | |
The password will start and end with some alphabetic characters, to make it | |
easier to copy/paste. In the middle will be a broader selection of characters. | |
With --open, the distribution is very random, with a strong bias towards | |
alphabetic characters. | |
Otherwise, there will be N digits and M punctuation characters. | |
The digits and punctuation can be specified as a ratio (0, 1) or an absolute | |
count; for a ratio, the count will be the ceiling of this ratio times the length | |
of the password. | |
If -e/--entropy is used then the specified "amount of entropy" will be used and | |
the length will be ignored. Eg, a diceware-style password of 96+ bits entropy | |
can be obtained with: | |
makepassword -De 96 | |
Note that for a given amount of entropy, a "conforming" password is longer than | |
an "open" password because the constraints on counts limit the actual entropy | |
provided per character. Note that our entropy calculation is too crude and is | |
log2(possible_values), not accounting for weighting bias. | |
""" | |
__author__ = '[email protected] (Phil Pennock)' | |
import argparse | |
import math | |
import pathlib | |
from random import shuffle as random_shuffle # avoid temptation of random. in namespace | |
import secrets | |
import sys | |
import typing | |
class WordList(typing.NamedTuple): | |
"""A diceware-style wordlist with N dice-rolls per word.""" | |
file: str | |
rolls: int = 5 | |
_DEFAULT_LENGTH = 20 | |
_MIN_LENGTH = 8 | |
_DEF_DIGITS_FRAC = 0.2 | |
_DEF_PUNCTUATION_FRAC = 0.25 | |
_DEF_BUFFER_ALPHA = 2 | |
_MIN_ENTROPY = 64 | |
_DEF_WORDLISTS_DIR = pathlib.Path('~/notes/WordLists').expanduser() | |
_DEF_BITCOIN_DIR = pathlib.Path('~/notes/WordLists/bitcoin').expanduser() | |
_DEF_WORDLIST = 'eff-large' | |
_DEF_BITCOIN = 'english.txt' | |
_BITCOIN_BIP39_WORDCOUNT = 2048 # defined by spec | |
# TODO: figure out if I want a config file to contain mappings in the dir, and | |
# source from there, and have a means to indicate the default. | |
KNOWN_WORDLISTS: typing.Mapping[str, WordList] = { | |
'beale': WordList('beale.wordlist.asc'), | |
'jp-romaji': WordList('diceware_jp_romaji.txt'), | |
'diceware-latin': WordList('diceware_latin.txt.asc'), | |
'diceware': WordList('diceware.wordlist.asc'), | |
'eff-large': WordList('eff_large_wordlist.txt'), | |
'eff-short1': WordList('eff_short_wordlist_1.txt', 4), | |
'eff-short2': WordList('eff_short_wordlist_2_0.txt', 4), | |
} | |
_CACHED_WORDLISTS: typing.MutableMapping[str, typing.List[str]] = {} | |
_CACHED_BITCOIN_WORDLISTS: typing.MutableMapping[str, typing.List[str]] = {} | |
class ExitError(Exception): | |
"""Exception causing clean process exit without stack-trace.""" | |
pass | |
class CountOrRatio(object): | |
"""Used for options parsing for digit and punctuation amounts. | |
If we ask for a password of length 60 and don't otherwise specify the | |
number of digits, then the count should scale up from a default suitable | |
for length 20 instead of becoming an ever smaller slice of the total. | |
""" | |
def __init__(self, param: typing.Union[float, str], *, base: int = 0): | |
self.count = 0 | |
self.fraction = 0.0 | |
self.base = 0 | |
if isinstance(param, str): | |
param = float(param) | |
if param < 0.0 or base < 0: | |
raise ValueError('all parameters must be non-negative') | |
if param.is_integer() and param >= 0: | |
self.count = int(param) | |
elif 0.0 < param and param < 1.0: | |
self.fraction = param | |
else: | |
raise ValueError('bad parameter for a CountOrRatio') | |
self.base = base | |
def __int__(self) -> int: | |
if self.count: | |
return self.count | |
if self.fraction and self.base: | |
return math.ceil(self.fraction * self.base) | |
return 0 | |
def __lt__(self, other: int) -> bool: | |
return int(self) < other | |
def __add__(self, other: int) -> int: | |
return int(self) + other | |
def __radd__(self, other: int) -> int: | |
return other + int(self) | |
def __index__(self) -> int: | |
return int(self) | |
def __str__(self) -> str: | |
if self.count: | |
return str(self.count) | |
if self.fraction and self.base: | |
return f'({self.fraction}·{self.base})={int(self)}' | |
return 'err' | |
def chars_in_range(start: str, end: str) -> typing.List[str]: | |
"""All ASCII characters in range between start and end inclusive.""" | |
return [chr(x) for x in range(ord(start), ord(end) + 1)] | |
class EntropyList(object): | |
def __init__(self, entropy_per_char: float, items: typing.List[str]): | |
self.entropy = entropy_per_char | |
self.items = items | |
def __len__(self) -> int: | |
return len(self.items) | |
def __getitem__(self, key) -> str: | |
return self.items[key] | |
def make_full_dictionary() -> EntropyList: | |
"""Should return a list 256 entries long.""" | |
encoded = [chr(x) for x in range(0x21, 0x7F)] | |
for _ in range(2): | |
encoded.extend(chars_in_range('A', 'Z')) | |
for _ in range(4): | |
encoded.extend(chars_in_range('a', 'z')) | |
encoded.extend(['2', '3', '4', '5', '6', '8']) | |
random_shuffle(encoded) # gratuitous | |
# 1 of the values in the range [0x21..0x7E] ... 95 values, log2(95) = 6.56986 | |
e_per_char = 6.569855608330948 | |
return EntropyList(e_per_char, encoded) | |
def make_x_dictionary(x: typing.List[str]) -> EntropyList: | |
"""Make a list 256 entries long from an input list.""" | |
# This will have a bias in the results, since are unlikely to have an input which exactly divides 256. | |
encoded: typing.List[str] = [] | |
while len(encoded) < 256: | |
encoded.extend(x) | |
# This shuffle means the nature of the bias is unpredictable, so matters less. | |
# This safety depends upon this dictionary not being persisted to be used across creating multiple passwords, | |
# as otherwise the bias will exist and be correlatable across them. | |
random_shuffle(encoded) | |
return EntropyList(math.log2(len(x)), encoded[:256]) | |
def make_alpha_dictionary() -> EntropyList: | |
"""Some dictionary of alphabetic chars with unpredictable bias.""" | |
xs = [] | |
xs.extend(chars_in_range('A', 'Z')) | |
xs.extend(chars_in_range('a', 'z')) | |
return make_x_dictionary(xs) | |
def make_digit_dictionary() -> EntropyList: | |
"""Some dictionary of digits with unpredictable bias.""" | |
return make_x_dictionary(chars_in_range('0', '9')) | |
def secret_bytes_list(length: int, options: argparse.Namespace): | |
if options.fixed_nonrandom_hex is not None: | |
print('WARNING: using fixed "random" number supplied on command-line, this is INSECURE', file=sys.stderr) | |
return list(bytes.fromhex(options.fixed_nonrandom_hex)[:length]) # ignores whitespace | |
else: | |
return list(secrets.token_bytes(length)) | |
class Random_IntBelow_Generator: | |
"""Implement an iterator of random numbers in a range [0,max_at_init). | |
If the program was invoked with --fixed-nonrandom-hex then the random numbers won't be random. | |
This is for testing. Otherwise, the numbers should be high quality randomness. | |
""" | |
def __init__(self, max_value: int, options: argparse.Namespace): | |
self.above_max_value = max_value | |
self.static = None | |
if options.fixed_nonrandom_hex is None: | |
return | |
print('WARNING: using fixed "random" number supplied on command-line, this is INSECURE', file=sys.stderr) | |
# FIXME: we are just using a whole number of bytes per word here, instead of some number of bits. | |
# We should know how many bits are needed and work through them, as per RFC1751 (which is horrid C I don't feel like decoding right now). | |
# This will do for now. But it means that the whole reason I added --fixed-nonrandom-hex is voided, since I can't double-check against the RFC. | |
self.static = bytes.fromhex(options.fixed_nonrandom_hex) | |
self.static_offset = 0 | |
self.static_step = 1 | |
t = max_value | |
while t > 256: | |
t //= 256 | |
self.static_step += 1 | |
def next(self) -> int: | |
if self.static is None: | |
return secrets.randbelow(self.above_max_value) | |
if self.static_offset + self.static_step > len(self.static): | |
raise ExitError('exceeded entropy from static source') | |
raw_bytes = self.static[self.static_offset:self.static_offset + self.static_step] | |
self.static_offset += self.static_step | |
v = int.from_bytes(raw_bytes, byteorder='big') | |
return v % self.above_max_value | |
def password_fairly_open(min_entropy: int, length: int, buffer: int, options: argparse.Namespace) -> str: | |
"""A full password with no minimum constraints except padding alphas.""" | |
full_characters = make_full_dictionary() | |
alpha_characters = make_alpha_dictionary() | |
if min_entropy > 0: | |
have_entropy = 2 * buffer * alpha_characters.entropy | |
length = max(1, math.ceil((min_entropy - have_entropy) / full_characters.entropy)) + 2 * buffer | |
raw_binary_offsets = secret_bytes_list(length, options) | |
password_chars = [] | |
password_chars.extend([alpha_characters[n] for n in raw_binary_offsets[:buffer]]) | |
password_chars.extend([full_characters[n] for n in raw_binary_offsets[buffer:-buffer]]) | |
password_chars.extend([alpha_characters[n] for n in raw_binary_offsets[-buffer:]]) | |
return ''.join(password_chars) | |
def password_conforming(options: argparse.Namespace) -> str: | |
"""A password conforming to rules expressed through program options.""" | |
al = make_alpha_dictionary() | |
dg = make_digit_dictionary() | |
# We can add options to constrain the punctuation character set, for | |
# varying levels of portability. | |
punct_chars = [] | |
if options.punctuation_characters: | |
punct_chars.extend([c for c in options.punctuation_characters]) | |
else: | |
punct_chars.extend(chars_in_range('!', '/')) | |
punct_chars.extend(chars_in_range(':', '@')) | |
punct_chars.extend(chars_in_range('[', '`')) | |
punct_chars.extend(chars_in_range('{', '~')) | |
pt = make_x_dictionary(punct_chars) | |
if options.entropy > 0: | |
min_entropy = options.entropy | |
have_entropy = options.digits * dg.entropy + options.punctuation * pt.entropy + options.buffer * 2 * al.entropy | |
have_len = options.digits + options.punctuation + 2 * options.buffer | |
length = max(1, math.ceil((min_entropy - have_entropy) / al.entropy)) + have_len | |
else: | |
length = options.length | |
indices = secret_bytes_list(length, options) | |
password_chars = [] | |
password_chars.extend([dg[n] for n in indices[:options.digits]]) | |
password_chars.extend([pt[n] for n in indices[options.digits:options.digits + options.punctuation]]) | |
# Beware that n:-0 is an empty list, which is why we constrain padding to be positive | |
password_chars.extend([al[n] for n in indices[options.digits + options.punctuation: -(options.buffer * 2)]]) | |
buffer_chars = [al[n] for n in indices[-(options.buffer * 2):]] | |
random_shuffle(password_chars) | |
res = buffer_chars[:options.buffer] | |
res.extend(password_chars) | |
res.extend(buffer_chars[options.buffer:]) | |
return ''.join(res) | |
def load_word_list(name: str, path: pathlib.Path) -> typing.List[str]: | |
"""Return a diceware style wordlist, loading it if needed.""" | |
global _CACHED_WORDLISTS | |
if name not in _CACHED_WORDLISTS: | |
_CACHED_WORDLISTS[name] = load_word_list_raw(path) | |
return _CACHED_WORDLISTS[name] | |
def load_word_list_raw(path: pathlib.Path) -> typing.List[str]: | |
"""Load a diceware style wordlist.""" | |
ll: typing.List[str] = [] | |
for line in path.open(): | |
# FIXME: better robustness checks here | |
if not line: | |
continue | |
if line[0] not in ('1', '2', '3', '4', '5', '6'): # how should this handle non-six-sided dice? | |
continue | |
try: | |
ll.append(line.strip().split(None, 1)[1]) | |
except Exception as e: | |
raise ExitError(f'FAILED: {e} in line {line} of {path!r}') from e | |
return ll | |
def load_bitcoin_list(path: pathlib.Path) -> typing.List[str]: | |
"""Return a bitcoin BIP39 style wordlist, loading it if needed.""" | |
global _CACHED_BITCOIN_WORDLISTS | |
key = str(path) | |
if key not in _CACHED_BITCOIN_WORDLISTS: | |
_CACHED_BITCOIN_WORDLISTS[key] = load_bitcoin_list_raw(path) | |
return _CACHED_BITCOIN_WORDLISTS[key] | |
def load_bitcoin_list_raw(path: pathlib.Path) -> typing.List[str]: | |
"""Load a bitcoin BIP39 style wordlist.""" | |
ll: typing.List[str] = ['' for _ in range(_BITCOIN_BIP39_WORDCOUNT)] | |
seen = 0 | |
for line in path.open(): | |
if not line: | |
continue | |
ll[seen] = line.strip() | |
seen += 1 | |
if seen != _BITCOIN_BIP39_WORDCOUNT: | |
raise ExitError(f'FAILED: not expected {_BITCOIN_BIP39_WORDCOUNT} entries in {path!r}, found {seen}') | |
return ll | |
def password_dice(options: argparse.Namespace) -> str: | |
"""Return a diceware style password per program options.""" | |
wl_name = options.word_list | |
DIE_SIDES = 6 | |
wl = KNOWN_WORDLISTS[wl_name] | |
wl_file = options.word_list_dir / wl.file | |
if not wl_file.exists(): | |
raise ExitError(f'missing diceware file {wl_file}') | |
word_list = load_word_list(wl_name, wl_file) | |
return password_words_from_list(word_list=word_list, | |
max_int=DIE_SIDES ** wl.rolls - 1, | |
word_values_count=DIE_SIDES ** wl.rolls, | |
options=options) | |
def password_words_from_list(word_list: typing.List[str], | |
max_int: int, | |
word_values_count: int, | |
options: argparse.Namespace) -> str: | |
if options.entropy: | |
# For bitcoin and skey, word_values_count should be 2048, so per_word is 11 | |
per_word = math.log2(word_values_count) | |
word_count = math.ceil(options.entropy / per_word) | |
else: | |
word_count = options.length | |
words: typing.List[str] = [] | |
rng = Random_IntBelow_Generator(max_int, options) | |
for _ in range(word_count): | |
index_int = rng.next() | |
try: | |
words.append(word_list[index_int]) | |
except Exception as e: | |
raise ExitError(f'bad index {index_int}, max {len(word_list)}') from e | |
return options.word_separator.join(words) | |
def password_skey(options: argparse.Namespace) -> str: | |
"""Returns a diceware style password using RFC1751 S/Key words.""" | |
word_list = _RFC1751_SKEY_WORDS | |
word_count = len(word_list) | |
if word_count != 2048: | |
raise Exception(f'invalid _RFC1751_SKEY_WORDS, should be 2048 entries long, is {word_count}') | |
return password_words_from_list(word_list=word_list, | |
max_int=word_count, | |
word_values_count=word_count, | |
options=options) | |
def password_bitcoin(options: argparse.Namespace) -> str: | |
"""Return a Bitcoin BIP39 style password per program options.""" | |
wl_file = options.bitcoin_list_dir / options.bitcoin_list | |
if not wl_file.exists(): | |
raise ExitError(f'missing bitcoin file {wl_file}') | |
word_list = load_bitcoin_list(wl_file) | |
return password_words_from_list(word_list=word_list, | |
max_int=_BITCOIN_BIP39_WORDCOUNT, # should be 2048 | |
word_values_count=_BITCOIN_BIP39_WORDCOUNT, | |
options=options) | |
def main() -> int: | |
"""Program main entry-point, excluding exit exception handling.""" | |
parser = argparse.ArgumentParser( | |
description=__doc__, | |
formatter_class=argparse.RawDescriptionHelpFormatter) | |
parser.add_argument('-d', '--digits', | |
type=CountOrRatio, metavar='N', | |
default=CountOrRatio(_DEF_DIGITS_FRAC, base=_DEFAULT_LENGTH), | |
help='How many digits to include [%(default)s]') | |
parser.add_argument('-p', '--punctuation', | |
type=CountOrRatio, metavar='M', | |
default=CountOrRatio(_DEF_PUNCTUATION_FRAC, base=_DEFAULT_LENGTH), | |
help='How many punctuation characters to include [%(default)s]') | |
parser.add_argument('-P', '--punctuation-characters', | |
type=str, default='', metavar='CHARS', | |
help='Constrain the punctuation characters to these') | |
parser.add_argument('-b', '--buffer', | |
type=int, default=_DEF_BUFFER_ALPHA, metavar='PAD', | |
help='How many leading and trailing characters to be pure alphabetic [%(default)s]') | |
parser.add_argument('--open', | |
action='store_true', default=False, | |
help='Use a more open approach') | |
worder = parser.add_mutually_exclusive_group() | |
worder.add_argument('-D', '--dice', | |
action='store_true', default=False, | |
help='Use a diceware passphrase approach') | |
parser.add_argument('--word-list', | |
choices=KNOWN_WORDLISTS.keys(), default=None, | |
help='Use diceware and change default wordlist [' + _DEF_WORDLIST + ']') | |
parser.add_argument('--word-list-dir', | |
default=_DEF_WORDLISTS_DIR, type=pathlib.Path, metavar='D', | |
help='Directory holding diceware wordlist files [%(default)s]') | |
worder.add_argument('-B', '--bitcoin', '--bip39', | |
action='store_true', default=False, | |
help='Make a passphrase using the Bitcoin BIP39 wordlists for dictionaries') | |
parser.add_argument('--bitcoin-list-dir', | |
default=_DEF_BITCOIN_DIR, type=pathlib.Path, metavar='D', | |
help='Directory holding bitcoin BIP39 wordlist files [%(default)s]') | |
parser.add_argument('--bitcoin-list', | |
default=_DEF_BITCOIN, | |
help='File within --bitcoin-list-dir to use for BIP39 passphrases') | |
worder.add_argument('--skey', '--rfc1751', | |
action='store_true', default=False, | |
help='Use RFC 1751 (S/Key) words, but NOT the S/Key algorithm') # FIXME: should this change | |
parser.add_argument('-S', '--word-separator', | |
default=' ', metavar='SEP', | |
help='Separator between words in wordlist passphrases') | |
parser.add_argument('-e', '--entropy', | |
type=int, default=0, | |
help=f'Target minimal length password with this many bits of entropy [default: unused; minimum: {_MIN_ENTROPY}]') | |
parser.add_argument('--allow-insecure-low-entropy', | |
action='store_true', default=False, | |
help='Allow dangerously weak passwords') | |
parser.add_argument('--fixed-nonrandom-hex', | |
type=str, default=None, metavar='H', | |
help='Force the random number to not be random, but parse this hex instead') | |
parser.add_argument('length', | |
type=int, default=_DEFAULT_LENGTH, nargs='?', | |
help='How long a password to make [%(default)s]') | |
options = parser.parse_args() | |
if options.entropy: | |
if options.entropy < 0: | |
parser.error(f'entropy needs to be positive, not [{options.entropy}]') | |
if options.entropy < _MIN_ENTROPY and not options.allow_insecure_low_entropy: | |
parser.error(f'entropy of {options.entropy} is less than minimum of {_MIN_ENTROPY}') | |
options.digits.base = options.length | |
options.punctuation.base = options.length | |
# This is replacing default=_DEF_WORDLIST and letting us track that the | |
# option was used, so that one option can imply another. | |
if options.word_list is not None: | |
options.dice = True | |
else: | |
options.word_list = _DEF_WORDLIST | |
if options.dice: | |
if not options.word_list_dir.is_dir(): | |
raise ExitError(f'not a directory: {options.word_list_dir}') | |
# NB: for the entropy-based approach, the entropy depends upon the | |
# word-list; the number of rolls per word can vary. | |
# FIXME: auto-adapt the default min_length so that the argparse does this check for us. | |
min_length = 2 | |
if options.length < min_length and not options.entropy: | |
raise ExitError(f'length needs to be at least {min_length}; \'{options.length}\' too small') | |
print(password_dice(options)) | |
return 0 | |
if options.bitcoin: | |
if not options.bitcoin_list: | |
raise parser.error('value of --bitcoin-list must not be empty') | |
if not options.bitcoin_list_dir.is_dir(): | |
raise ExitError(f'not a directory: {options.bitcoin_list_dir}') | |
# This is very similar in spirit to diceware, but the list of words is supposed to be 2048 each | |
min_length = 2 | |
if options.length < min_length and not options.entropy: | |
raise ExitError(f'length needs to be at least {min_length}; \'{options.length}\' too small') | |
print(password_bitcoin(options)) | |
return 0 | |
if options.skey: | |
min_length = 2 | |
if options.length < min_length and not options.entropy: | |
raise ExitError(f'length needs to be at least {min_length}; \'{options.length}\' too small') | |
print(password_skey(options)) | |
return 0 | |
for numfield in ('digits', 'punctuation'): | |
v = getattr(options, numfield) | |
if v < 0: | |
parser.error(f'field {numfield} should be a non-negative integer, not \'{v}\'') | |
for numfield in ('length', 'buffer'): | |
# nb: buffer needs to be non-zero to avoid more complexity in offset indicing | |
v = getattr(options, numfield) | |
if v <= 0: | |
parser.error(f'field {numfield} should be a positive integer, not \'{v}\'') | |
if options.open: | |
min_length = 1 + 2 * options.buffer | |
else: | |
min_length = 1 + options.digits + options.punctuation + 2 * options.buffer | |
if options.length < min_length: | |
raise ExitError(f'length needs to be at least {min_length}; \'{options.length}\' too small') | |
if options.open: | |
print(password_fairly_open(options.entropy, options.length, options.buffer, options)) | |
return 0 | |
print(password_conforming(options)) | |
return 0 | |
# RFC1751 Static Data {{{ | |
_RFC1751_SKEY_WORDS = [ | |
"A", "ABE", "ACE", "ACT", "AD", "ADA", "ADD", | |
"AGO", "AID", "AIM", "AIR", "ALL", "ALP", "AM", "AMY", "AN", "ANA", | |
"AND", "ANN", "ANT", "ANY", "APE", "APS", "APT", "ARC", "ARE", "ARK", | |
"ARM", "ART", "AS", "ASH", "ASK", "AT", "ATE", "AUG", "AUK", "AVE", | |
"AWE", "AWK", "AWL", "AWN", "AX", "AYE", "BAD", "BAG", "BAH", "BAM", | |
"BAN", "BAR", "BAT", "BAY", "BE", "BED", "BEE", "BEG", "BEN", "BET", | |
"BEY", "BIB", "BID", "BIG", "BIN", "BIT", "BOB", "BOG", "BON", "BOO", | |
"BOP", "BOW", "BOY", "BUB", "BUD", "BUG", "BUM", "BUN", "BUS", "BUT", | |
"BUY", "BY", "BYE", "CAB", "CAL", "CAM", "CAN", "CAP", "CAR", "CAT", | |
"CAW", "COD", "COG", "COL", "CON", "COO", "COP", "COT", "COW", "COY", | |
"CRY", "CUB", "CUE", "CUP", "CUR", "CUT", "DAB", "DAD", "DAM", "DAN", | |
"DAR", "DAY", "DEE", "DEL", "DEN", "DES", "DEW", "DID", "DIE", "DIG", | |
"DIN", "DIP", "DO", "DOE", "DOG", "DON", "DOT", "DOW", "DRY", "DUB", | |
"DUD", "DUE", "DUG", "DUN", "EAR", "EAT", "ED", "EEL", "EGG", "EGO", | |
"ELI", "ELK", "ELM", "ELY", "EM", "END", "EST", "ETC", "EVA", "EVE", | |
"EWE", "EYE", "FAD", "FAN", "FAR", "FAT", "FAY", "FED", "FEE", "FEW", | |
"FIB", "FIG", "FIN", "FIR", "FIT", "FLO", "FLY", "FOE", "FOG", "FOR", | |
"FRY", "FUM", "FUN", "FUR", "GAB", "GAD", "GAG", "GAL", "GAM", "GAP", | |
"GAS", "GAY", "GEE", "GEL", "GEM", "GET", "GIG", "GIL", "GIN", "GO", | |
"GOT", "GUM", "GUN", "GUS", "GUT", "GUY", "GYM", "GYP", "HA", "HAD", | |
"HAL", "HAM", "HAN", "HAP", "HAS", "HAT", "HAW", "HAY", "HE", "HEM", | |
"HEN", "HER", "HEW", "HEY", "HI", "HID", "HIM", "HIP", "HIS", "HIT", | |
"HO", "HOB", "HOC", "HOE", "HOG", "HOP", "HOT", "HOW", "HUB", "HUE", | |
"HUG", "HUH", "HUM", "HUT", "I", "ICY", "IDA", "IF", "IKE", "ILL", | |
"INK", "INN", "IO", "ION", "IQ", "IRA", "IRE", "IRK", "IS", "IT", "ITS", | |
"IVY", "JAB", "JAG", "JAM", "JAN", "JAR", "JAW", "JAY", "JET", "JIG", | |
"JIM", "JO", "JOB", "JOE", "JOG", "JOT", "JOY", "JUG", "JUT", "KAY", | |
"KEG", "KEN", "KEY", "KID", "KIM", "KIN", "KIT", "LA", "LAB", "LAC", | |
"LAD", "LAG", "LAM", "LAP", "LAW", "LAY", "LEA", "LED", "LEE", "LEG", | |
"LEN", "LEO", "LET", "LEW", "LID", "LIE", "LIN", "LIP", "LIT", "LO", | |
"LOB", "LOG", "LOP", "LOS", "LOT", "LOU", "LOW", "LOY", "LUG", "LYE", | |
"MA", "MAC", "MAD", "MAE", "MAN", "MAO", "MAP", "MAT", "MAW", "MAY", | |
"ME", "MEG", "MEL", "MEN", "MET", "MEW", "MID", "MIN", "MIT", "MOB", | |
"MOD", "MOE", "MOO", "MOP", "MOS", "MOT", "MOW", "MUD", "MUG", "MUM", | |
"MY", "NAB", "NAG", "NAN", "NAP", "NAT", "NAY", "NE", "NED", "NEE", | |
"NET", "NEW", "NIB", "NIL", "NIP", "NIT", "NO", "NOB", "NOD", "NON", | |
"NOR", "NOT", "NOV", "NOW", "NU", "NUN", "NUT", "O", "OAF", "OAK", | |
"OAR", "OAT", "ODD", "ODE", "OF", "OFF", "OFT", "OH", "OIL", "OK", | |
"OLD", "ON", "ONE", "OR", "ORB", "ORE", "ORR", "OS", "OTT", "OUR", | |
"OUT", "OVA", "OW", "OWE", "OWL", "OWN", "OX", "PA", "PAD", "PAL", | |
"PAM", "PAN", "PAP", "PAR", "PAT", "PAW", "PAY", "PEA", "PEG", "PEN", | |
"PEP", "PER", "PET", "PEW", "PHI", "PI", "PIE", "PIN", "PIT", "PLY", | |
"PO", "POD", "POE", "POP", "POT", "POW", "PRO", "PRY", "PUB", "PUG", | |
"PUN", "PUP", "PUT", "QUO", "RAG", "RAM", "RAN", "RAP", "RAT", "RAW", | |
"RAY", "REB", "RED", "REP", "RET", "RIB", "RID", "RIG", "RIM", "RIO", | |
"RIP", "ROB", "ROD", "ROE", "RON", "ROT", "ROW", "ROY", "RUB", "RUE", | |
"RUG", "RUM", "RUN", "RYE", "SAC", "SAD", "SAG", "SAL", "SAM", "SAN", | |
"SAP", "SAT", "SAW", "SAY", "SEA", "SEC", "SEE", "SEN", "SET", "SEW", | |
"SHE", "SHY", "SIN", "SIP", "SIR", "SIS", "SIT", "SKI", "SKY", "SLY", | |
"SO", "SOB", "SOD", "SON", "SOP", "SOW", "SOY", "SPA", "SPY", "SUB", | |
"SUD", "SUE", "SUM", "SUN", "SUP", "TAB", "TAD", "TAG", "TAN", "TAP", | |
"TAR", "TEA", "TED", "TEE", "TEN", "THE", "THY", "TIC", "TIE", "TIM", | |
"TIN", "TIP", "TO", "TOE", "TOG", "TOM", "TON", "TOO", "TOP", "TOW", | |
"TOY", "TRY", "TUB", "TUG", "TUM", "TUN", "TWO", "UN", "UP", "US", | |
"USE", "VAN", "VAT", "VET", "VIE", "WAD", "WAG", "WAR", "WAS", "WAY", | |
"WE", "WEB", "WED", "WEE", "WET", "WHO", "WHY", "WIN", "WIT", "WOK", | |
"WON", "WOO", "WOW", "WRY", "WU", "YAM", "YAP", "YAW", "YE", "YEA", | |
"YES", "YET", "YOU", "ABED", "ABEL", "ABET", "ABLE", "ABUT", "ACHE", | |
"ACID", "ACME", "ACRE", "ACTA", "ACTS", "ADAM", "ADDS", "ADEN", "AFAR", | |
"AFRO", "AGEE", "AHEM", "AHOY", "AIDA", "AIDE", "AIDS", "AIRY", "AJAR", | |
"AKIN", "ALAN", "ALEC", "ALGA", "ALIA", "ALLY", "ALMA", "ALOE", "ALSO", | |
"ALTO", "ALUM", "ALVA", "AMEN", "AMES", "AMID", "AMMO", "AMOK", "AMOS", | |
"AMRA", "ANDY", "ANEW", "ANNA", "ANNE", "ANTE", "ANTI", "AQUA", "ARAB", | |
"ARCH", "AREA", "ARGO", "ARID", "ARMY", "ARTS", "ARTY", "ASIA", "ASKS", | |
"ATOM", "AUNT", "AURA", "AUTO", "AVER", "AVID", "AVIS", "AVON", "AVOW", | |
"AWAY", "AWRY", "BABE", "BABY", "BACH", "BACK", "BADE", "BAIL", "BAIT", | |
"BAKE", "BALD", "BALE", "BALI", "BALK", "BALL", "BALM", "BAND", "BANE", | |
"BANG", "BANK", "BARB", "BARD", "BARE", "BARK", "BARN", "BARR", "BASE", | |
"BASH", "BASK", "BASS", "BATE", "BATH", "BAWD", "BAWL", "BEAD", "BEAK", | |
"BEAM", "BEAN", "BEAR", "BEAT", "BEAU", "BECK", "BEEF", "BEEN", "BEER", | |
"BEET", "BELA", "BELL", "BELT", "BEND", "BENT", "BERG", "BERN", "BERT", | |
"BESS", "BEST", "BETA", "BETH", "BHOY", "BIAS", "BIDE", "BIEN", "BILE", | |
"BILK", "BILL", "BIND", "BING", "BIRD", "BITE", "BITS", "BLAB", "BLAT", | |
"BLED", "BLEW", "BLOB", "BLOC", "BLOT", "BLOW", "BLUE", "BLUM", "BLUR", | |
"BOAR", "BOAT", "BOCA", "BOCK", "BODE", "BODY", "BOGY", "BOHR", "BOIL", | |
"BOLD", "BOLO", "BOLT", "BOMB", "BONA", "BOND", "BONE", "BONG", "BONN", | |
"BONY", "BOOK", "BOOM", "BOON", "BOOT", "BORE", "BORG", "BORN", "BOSE", | |
"BOSS", "BOTH", "BOUT", "BOWL", "BOYD", "BRAD", "BRAE", "BRAG", "BRAN", | |
"BRAY", "BRED", "BREW", "BRIG", "BRIM", "BROW", "BUCK", "BUDD", "BUFF", | |
"BULB", "BULK", "BULL", "BUNK", "BUNT", "BUOY", "BURG", "BURL", "BURN", | |
"BURR", "BURT", "BURY", "BUSH", "BUSS", "BUST", "BUSY", "BYTE", "CADY", | |
"CAFE", "CAGE", "CAIN", "CAKE", "CALF", "CALL", "CALM", "CAME", "CANE", | |
"CANT", "CARD", "CARE", "CARL", "CARR", "CART", "CASE", "CASH", "CASK", | |
"CAST", "CAVE", "CEIL", "CELL", "CENT", "CERN", "CHAD", "CHAR", "CHAT", | |
"CHAW", "CHEF", "CHEN", "CHEW", "CHIC", "CHIN", "CHOU", "CHOW", "CHUB", | |
"CHUG", "CHUM", "CITE", "CITY", "CLAD", "CLAM", "CLAN", "CLAW", "CLAY", | |
"CLOD", "CLOG", "CLOT", "CLUB", "CLUE", "COAL", "COAT", "COCA", "COCK", | |
"COCO", "CODA", "CODE", "CODY", "COED", "COIL", "COIN", "COKE", "COLA", | |
"COLD", "COLT", "COMA", "COMB", "COME", "COOK", "COOL", "COON", "COOT", | |
"CORD", "CORE", "CORK", "CORN", "COST", "COVE", "COWL", "CRAB", "CRAG", | |
"CRAM", "CRAY", "CREW", "CRIB", "CROW", "CRUD", "CUBA", "CUBE", "CUFF", | |
"CULL", "CULT", "CUNY", "CURB", "CURD", "CURE", "CURL", "CURT", "CUTS", | |
"DADE", "DALE", "DAME", "DANA", "DANE", "DANG", "DANK", "DARE", "DARK", | |
"DARN", "DART", "DASH", "DATA", "DATE", "DAVE", "DAVY", "DAWN", "DAYS", | |
"DEAD", "DEAF", "DEAL", "DEAN", "DEAR", "DEBT", "DECK", "DEED", "DEEM", | |
"DEER", "DEFT", "DEFY", "DELL", "DENT", "DENY", "DESK", "DIAL", "DICE", | |
"DIED", "DIET", "DIME", "DINE", "DING", "DINT", "DIRE", "DIRT", "DISC", | |
"DISH", "DISK", "DIVE", "DOCK", "DOES", "DOLE", "DOLL", "DOLT", "DOME", | |
"DONE", "DOOM", "DOOR", "DORA", "DOSE", "DOTE", "DOUG", "DOUR", "DOVE", | |
"DOWN", "DRAB", "DRAG", "DRAM", "DRAW", "DREW", "DRUB", "DRUG", "DRUM", | |
"DUAL", "DUCK", "DUCT", "DUEL", "DUET", "DUKE", "DULL", "DUMB", "DUNE", | |
"DUNK", "DUSK", "DUST", "DUTY", "EACH", "EARL", "EARN", "EASE", "EAST", | |
"EASY", "EBEN", "ECHO", "EDDY", "EDEN", "EDGE", "EDGY", "EDIT", "EDNA", | |
"EGAN", "ELAN", "ELBA", "ELLA", "ELSE", "EMIL", "EMIT", "EMMA", "ENDS", | |
"ERIC", "EROS", "EVEN", "EVER", "EVIL", "EYED", "FACE", "FACT", "FADE", | |
"FAIL", "FAIN", "FAIR", "FAKE", "FALL", "FAME", "FANG", "FARM", "FAST", | |
"FATE", "FAWN", "FEAR", "FEAT", "FEED", "FEEL", "FEET", "FELL", "FELT", | |
"FEND", "FERN", "FEST", "FEUD", "FIEF", "FIGS", "FILE", "FILL", "FILM", | |
"FIND", "FINE", "FINK", "FIRE", "FIRM", "FISH", "FISK", "FIST", "FITS", | |
"FIVE", "FLAG", "FLAK", "FLAM", "FLAT", "FLAW", "FLEA", "FLED", "FLEW", | |
"FLIT", "FLOC", "FLOG", "FLOW", "FLUB", "FLUE", "FOAL", "FOAM", "FOGY", | |
"FOIL", "FOLD", "FOLK", "FOND", "FONT", "FOOD", "FOOL", "FOOT", "FORD", | |
"FORE", "FORK", "FORM", "FORT", "FOSS", "FOUL", "FOUR", "FOWL", "FRAU", | |
"FRAY", "FRED", "FREE", "FRET", "FREY", "FROG", "FROM", "FUEL", "FULL", | |
"FUME", "FUND", "FUNK", "FURY", "FUSE", "FUSS", "GAFF", "GAGE", "GAIL", | |
"GAIN", "GAIT", "GALA", "GALE", "GALL", "GALT", "GAME", "GANG", "GARB", | |
"GARY", "GASH", "GATE", "GAUL", "GAUR", "GAVE", "GAWK", "GEAR", "GELD", | |
"GENE", "GENT", "GERM", "GETS", "GIBE", "GIFT", "GILD", "GILL", "GILT", | |
"GINA", "GIRD", "GIRL", "GIST", "GIVE", "GLAD", "GLEE", "GLEN", "GLIB", | |
"GLOB", "GLOM", "GLOW", "GLUE", "GLUM", "GLUT", "GOAD", "GOAL", "GOAT", | |
"GOER", "GOES", "GOLD", "GOLF", "GONE", "GONG", "GOOD", "GOOF", "GORE", | |
"GORY", "GOSH", "GOUT", "GOWN", "GRAB", "GRAD", "GRAY", "GREG", "GREW", | |
"GREY", "GRID", "GRIM", "GRIN", "GRIT", "GROW", "GRUB", "GULF", "GULL", | |
"GUNK", "GURU", "GUSH", "GUST", "GWEN", "GWYN", "HAAG", "HAAS", "HACK", | |
"HAIL", "HAIR", "HALE", "HALF", "HALL", "HALO", "HALT", "HAND", "HANG", | |
"HANK", "HANS", "HARD", "HARK", "HARM", "HART", "HASH", "HAST", "HATE", | |
"HATH", "HAUL", "HAVE", "HAWK", "HAYS", "HEAD", "HEAL", "HEAR", "HEAT", | |
"HEBE", "HECK", "HEED", "HEEL", "HEFT", "HELD", "HELL", "HELM", "HERB", | |
"HERD", "HERE", "HERO", "HERS", "HESS", "HEWN", "HICK", "HIDE", "HIGH", | |
"HIKE", "HILL", "HILT", "HIND", "HINT", "HIRE", "HISS", "HIVE", "HOBO", | |
"HOCK", "HOFF", "HOLD", "HOLE", "HOLM", "HOLT", "HOME", "HONE", "HONK", | |
"HOOD", "HOOF", "HOOK", "HOOT", "HORN", "HOSE", "HOST", "HOUR", "HOVE", | |
"HOWE", "HOWL", "HOYT", "HUCK", "HUED", "HUFF", "HUGE", "HUGH", "HUGO", | |
"HULK", "HULL", "HUNK", "HUNT", "HURD", "HURL", "HURT", "HUSH", "HYDE", | |
"HYMN", "IBIS", "ICON", "IDEA", "IDLE", "IFFY", "INCA", "INCH", "INTO", | |
"IONS", "IOTA", "IOWA", "IRIS", "IRMA", "IRON", "ISLE", "ITCH", "ITEM", | |
"IVAN", "JACK", "JADE", "JAIL", "JAKE", "JANE", "JAVA", "JEAN", "JEFF", | |
"JERK", "JESS", "JEST", "JIBE", "JILL", "JILT", "JIVE", "JOAN", "JOBS", | |
"JOCK", "JOEL", "JOEY", "JOHN", "JOIN", "JOKE", "JOLT", "JOVE", "JUDD", | |
"JUDE", "JUDO", "JUDY", "JUJU", "JUKE", "JULY", "JUNE", "JUNK", "JUNO", | |
"JURY", "JUST", "JUTE", "KAHN", "KALE", "KANE", "KANT", "KARL", "KATE", | |
"KEEL", "KEEN", "KENO", "KENT", "KERN", "KERR", "KEYS", "KICK", "KILL", | |
"KIND", "KING", "KIRK", "KISS", "KITE", "KLAN", "KNEE", "KNEW", "KNIT", | |
"KNOB", "KNOT", "KNOW", "KOCH", "KONG", "KUDO", "KURD", "KURT", "KYLE", | |
"LACE", "LACK", "LACY", "LADY", "LAID", "LAIN", "LAIR", "LAKE", "LAMB", | |
"LAME", "LAND", "LANE", "LANG", "LARD", "LARK", "LASS", "LAST", "LATE", | |
"LAUD", "LAVA", "LAWN", "LAWS", "LAYS", "LEAD", "LEAF", "LEAK", "LEAN", | |
"LEAR", "LEEK", "LEER", "LEFT", "LEND", "LENS", "LENT", "LEON", "LESK", | |
"LESS", "LEST", "LETS", "LIAR", "LICE", "LICK", "LIED", "LIEN", "LIES", | |
"LIEU", "LIFE", "LIFT", "LIKE", "LILA", "LILT", "LILY", "LIMA", "LIMB", | |
"LIME", "LIND", "LINE", "LINK", "LINT", "LION", "LISA", "LIST", "LIVE", | |
"LOAD", "LOAF", "LOAM", "LOAN", "LOCK", "LOFT", "LOGE", "LOIS", "LOLA", | |
"LONE", "LONG", "LOOK", "LOON", "LOOT", "LORD", "LORE", "LOSE", "LOSS", | |
"LOST", "LOUD", "LOVE", "LOWE", "LUCK", "LUCY", "LUGE", "LUKE", "LULU", | |
"LUND", "LUNG", "LURA", "LURE", "LURK", "LUSH", "LUST", "LYLE", "LYNN", | |
"LYON", "LYRA", "MACE", "MADE", "MAGI", "MAID", "MAIL", "MAIN", "MAKE", | |
"MALE", "MALI", "MALL", "MALT", "MANA", "MANN", "MANY", "MARC", "MARE", | |
"MARK", "MARS", "MART", "MARY", "MASH", "MASK", "MASS", "MAST", "MATE", | |
"MATH", "MAUL", "MAYO", "MEAD", "MEAL", "MEAN", "MEAT", "MEEK", "MEET", | |
"MELD", "MELT", "MEMO", "MEND", "MENU", "MERT", "MESH", "MESS", "MICE", | |
"MIKE", "MILD", "MILE", "MILK", "MILL", "MILT", "MIMI", "MIND", "MINE", | |
"MINI", "MINK", "MINT", "MIRE", "MISS", "MIST", "MITE", "MITT", "MOAN", | |
"MOAT", "MOCK", "MODE", "MOLD", "MOLE", "MOLL", "MOLT", "MONA", "MONK", | |
"MONT", "MOOD", "MOON", "MOOR", "MOOT", "MORE", "MORN", "MORT", "MOSS", | |
"MOST", "MOTH", "MOVE", "MUCH", "MUCK", "MUDD", "MUFF", "MULE", "MULL", | |
"MURK", "MUSH", "MUST", "MUTE", "MUTT", "MYRA", "MYTH", "NAGY", "NAIL", | |
"NAIR", "NAME", "NARY", "NASH", "NAVE", "NAVY", "NEAL", "NEAR", "NEAT", | |
"NECK", "NEED", "NEIL", "NELL", "NEON", "NERO", "NESS", "NEST", "NEWS", | |
"NEWT", "NIBS", "NICE", "NICK", "NILE", "NINA", "NINE", "NOAH", "NODE", | |
"NOEL", "NOLL", "NONE", "NOOK", "NOON", "NORM", "NOSE", "NOTE", "NOUN", | |
"NOVA", "NUDE", "NULL", "NUMB", "OATH", "OBEY", "OBOE", "ODIN", "OHIO", | |
"OILY", "OINT", "OKAY", "OLAF", "OLDY", "OLGA", "OLIN", "OMAN", "OMEN", | |
"OMIT", "ONCE", "ONES", "ONLY", "ONTO", "ONUS", "ORAL", "ORGY", "OSLO", | |
"OTIS", "OTTO", "OUCH", "OUST", "OUTS", "OVAL", "OVEN", "OVER", "OWLY", | |
"OWNS", "QUAD", "QUIT", "QUOD", "RACE", "RACK", "RACY", "RAFT", "RAGE", | |
"RAID", "RAIL", "RAIN", "RAKE", "RANK", "RANT", "RARE", "RASH", "RATE", | |
"RAVE", "RAYS", "READ", "REAL", "REAM", "REAR", "RECK", "REED", "REEF", | |
"REEK", "REEL", "REID", "REIN", "RENA", "REND", "RENT", "REST", "RICE", | |
"RICH", "RICK", "RIDE", "RIFT", "RILL", "RIME", "RING", "RINK", "RISE", | |
"RISK", "RITE", "ROAD", "ROAM", "ROAR", "ROBE", "ROCK", "RODE", "ROIL", | |
"ROLL", "ROME", "ROOD", "ROOF", "ROOK", "ROOM", "ROOT", "ROSA", "ROSE", | |
"ROSS", "ROSY", "ROTH", "ROUT", "ROVE", "ROWE", "ROWS", "RUBE", "RUBY", | |
"RUDE", "RUDY", "RUIN", "RULE", "RUNG", "RUNS", "RUNT", "RUSE", "RUSH", | |
"RUSK", "RUSS", "RUST", "RUTH", "SACK", "SAFE", "SAGE", "SAID", "SAIL", | |
"SALE", "SALK", "SALT", "SAME", "SAND", "SANE", "SANG", "SANK", "SARA", | |
"SAUL", "SAVE", "SAYS", "SCAN", "SCAR", "SCAT", "SCOT", "SEAL", "SEAM", | |
"SEAR", "SEAT", "SEED", "SEEK", "SEEM", "SEEN", "SEES", "SELF", "SELL", | |
"SEND", "SENT", "SETS", "SEWN", "SHAG", "SHAM", "SHAW", "SHAY", "SHED", | |
"SHIM", "SHIN", "SHOD", "SHOE", "SHOT", "SHOW", "SHUN", "SHUT", "SICK", | |
"SIDE", "SIFT", "SIGH", "SIGN", "SILK", "SILL", "SILO", "SILT", "SINE", | |
"SING", "SINK", "SIRE", "SITE", "SITS", "SITU", "SKAT", "SKEW", "SKID", | |
"SKIM", "SKIN", "SKIT", "SLAB", "SLAM", "SLAT", "SLAY", "SLED", "SLEW", | |
"SLID", "SLIM", "SLIT", "SLOB", "SLOG", "SLOT", "SLOW", "SLUG", "SLUM", | |
"SLUR", "SMOG", "SMUG", "SNAG", "SNOB", "SNOW", "SNUB", "SNUG", "SOAK", | |
"SOAR", "SOCK", "SODA", "SOFA", "SOFT", "SOIL", "SOLD", "SOME", "SONG", | |
"SOON", "SOOT", "SORE", "SORT", "SOUL", "SOUR", "SOWN", "STAB", "STAG", | |
"STAN", "STAR", "STAY", "STEM", "STEW", "STIR", "STOW", "STUB", "STUN", | |
"SUCH", "SUDS", "SUIT", "SULK", "SUMS", "SUNG", "SUNK", "SURE", "SURF", | |
"SWAB", "SWAG", "SWAM", "SWAN", "SWAT", "SWAY", "SWIM", "SWUM", "TACK", | |
"TACT", "TAIL", "TAKE", "TALE", "TALK", "TALL", "TANK", "TASK", "TATE", | |
"TAUT", "TEAL", "TEAM", "TEAR", "TECH", "TEEM", "TEEN", "TEET", "TELL", | |
"TEND", "TENT", "TERM", "TERN", "TESS", "TEST", "THAN", "THAT", "THEE", | |
"THEM", "THEN", "THEY", "THIN", "THIS", "THUD", "THUG", "TICK", "TIDE", | |
"TIDY", "TIED", "TIER", "TILE", "TILL", "TILT", "TIME", "TINA", "TINE", | |
"TINT", "TINY", "TIRE", "TOAD", "TOGO", "TOIL", "TOLD", "TOLL", "TONE", | |
"TONG", "TONY", "TOOK", "TOOL", "TOOT", "TORE", "TORN", "TOTE", "TOUR", | |
"TOUT", "TOWN", "TRAG", "TRAM", "TRAY", "TREE", "TREK", "TRIG", "TRIM", | |
"TRIO", "TROD", "TROT", "TROY", "TRUE", "TUBA", "TUBE", "TUCK", "TUFT", | |
"TUNA", "TUNE", "TUNG", "TURF", "TURN", "TUSK", "TWIG", "TWIN", "TWIT", | |
"ULAN", "UNIT", "URGE", "USED", "USER", "USES", "UTAH", "VAIL", "VAIN", | |
"VALE", "VARY", "VASE", "VAST", "VEAL", "VEDA", "VEIL", "VEIN", "VEND", | |
"VENT", "VERB", "VERY", "VETO", "VICE", "VIEW", "VINE", "VISE", "VOID", | |
"VOLT", "VOTE", "WACK", "WADE", "WAGE", "WAIL", "WAIT", "WAKE", "WALE", | |
"WALK", "WALL", "WALT", "WAND", "WANE", "WANG", "WANT", "WARD", "WARM", | |
"WARN", "WART", "WASH", "WAST", "WATS", "WATT", "WAVE", "WAVY", "WAYS", | |
"WEAK", "WEAL", "WEAN", "WEAR", "WEED", "WEEK", "WEIR", "WELD", "WELL", | |
"WELT", "WENT", "WERE", "WERT", "WEST", "WHAM", "WHAT", "WHEE", "WHEN", | |
"WHET", "WHOA", "WHOM", "WICK", "WIFE", "WILD", "WILL", "WIND", "WINE", | |
"WING", "WINK", "WINO", "WIRE", "WISE", "WISH", "WITH", "WOLF", "WONT", | |
"WOOD", "WOOL", "WORD", "WORE", "WORK", "WORM", "WORN", "WOVE", "WRIT", | |
"WYNN", "YALE", "YANG", "YANK", "YARD", "YARN", "YAWL", "YAWN", "YEAH", | |
"YEAR", "YELL", "YOGA", "YOKE" | |
] | |
# RFC1751 Static Data }}} | |
if __name__ == '__main__': | |
try: | |
rv = main() | |
sys.exit(rv) | |
except ExitError as e: | |
argv0 = pathlib.Path(sys.argv[0]).name | |
if argv0.endswith('.py'): | |
argv0 = argv0[:-3] | |
print('{}: {}'.format(argv0, e), file=sys.stderr) | |
sys.exit(1) | |
# vim: set ft=python sw=4 expandtab foldmethod=marker : |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
WARNING: requires external word-lists be available
For the bitcoin BIP39 word-lists, I use https://github.com/trezor/python-mnemonic/tree/master/src/mnemonic/wordlist as a source.
For the others, the source shows the aliases and file-names, and I just haven't bothered making that drivable from a config file yet.