#!/usr/bin/env python3
#
# OTP generator for RFC 2289 / OPIE
#

import sys
import os
import os.path
import stat
import random
import argparse
import hashlib
import subprocess
import platform
import re

try:
  import pyotp
except ImportError:
  pyotp = None

OTP_PWD_FILE = '.otp'
DEFAULT_ALGO = 'otp-md5'  # if the PWD file doesn't specify
PASSWORD_LEN = 20

ALGO_TOTP = 'totp'
TOTP_QR_GEN = 'https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=otpauth://totp/ASF%%20Infra?secret=%s&issuer=ASF'


def otp_path():
  "Compute the full path to the OTP password storage file."
  return os.path.expanduser(os.path.join('~', OTP_PWD_FILE))


def load_passwords():
  "Load password from the storage file."
  path = otp_path()
  try:
    s = os.lstat(path)
  except OSError:
    return { }
  if not stat.S_ISREG(s.st_mode):
    print('ERROR: not a regular file:', path)
    sys.exit(1)
  # Only user read/write. Nobody else.
  if (s.st_mode & (stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)) != 0o600:
    print('ERROR: should be mode 0600:', path)
    sys.exit(1)

  pwds = { }
  for l in open(path).readlines():
    m = RE_STORAGE.match(l)
    if m:
      #print('MATCH:', m.groups())
      if m.group(2):
        algo = m.group(2).lower()
      else:
        algo = DEFAULT_ALGO
      pwds[m.group(4).encode()] = (m.group(5).encode(), algo)

  return pwds

RE_STORAGE = re.compile(r'(((otp-\w+)|totp)\s+)?(\w+)\s+(.*)')


def add_password(seed, algo):
  "Construct a new password for a seed, and store/append it."
  pwd = new_password(algo)
  path = otp_path()
  fd = os.open(path, os.O_RDWR | os.O_APPEND | os.O_CREAT, mode=0o600)
  os.write(fd, algo.encode() + b' ' + seed + b' ' + pwd + b'\n')
  os.close(fd)
  return pwd


def new_password(algo):
  "Generate a password."
  if algo == ALGO_TOTP:
    # Generate a proper/compatible password for most TOTP apps
    return pyotp.random_base32().encode()
  # Stick to printable characters.
  return ''.join(chr(random.randint(33, 126)) for _ in range(PASSWORD_LEN)).encode()


def otp_md5(begin, seq):
  "Compute a 64-bit value through repeated hashing/folding."
  value = md5_iter_fold(begin)
  for _ in range(seq):
    value = md5_iter_fold(value)
  return value


def md5_iter_fold(value):
  "Iterate the hash, then fold into 64 bits."
  value = hashlib.md5(value).digest()  # 128 bit result
  return bytes(value[i] ^ value[i+8] for i in range(8))


def to_words(value):
  "Convert a 64-bit value into words, with a parity value."
  assert len(value) == 8  # 64 bits, in an 8-byte VALUE

  # Parity is the summation of each 2-bit pair. We will mask
  # out the lowest 2 bits, so no worries about higher bits.
  parity = sum((b >> 6) + (b >> 4) + (b >> 2) + b for b in value)

  # collapse the bytes into a single 64-bit integer; fold in parity bits
  i64 = int.from_bytes(value, byteorder='big')
  i64 = (i64 << 2) | (parity & 0x03)
  
  # and map that integer into words, using the spec's WORDS
  return [WORDS[(i64 >> (i*11)) & 0x7FF] for i in (5, 4, 3, 2, 1, 0)]


class RunTests(argparse.Action):
  "Run some basic tests on the code."

  def __call__(self, parser, namespace, values, option_string=None):
    # Test the to_words() function.
    value = b'\x9E\x87\x61\x34\xD9\x04\x99\xDD'
    words = ' '.join(to_words(value))
    assert words == MD5_TESTS[0][3]
    print('CORRECT:')
    print('  ', ' '.join('%02x%02x' % (value[i*2], value[i*2+1]) for i in range(4)), '--', words)

    # Run the tests from RFC 2289.
    print('COLUMNS:','Password', 'Seed', 'Count')
    for pwd, seed, seq, result in MD5_TESTS:
      value = otp_md5(seed.lower().encode() + pwd.encode(), seq)
      print('P:'+pwd, 'S:'+seed, 'C:'+str(seq))
      words = ' '.join(to_words(value))
      print('  ', ' '.join('%02x%02x' % (value[i*2], value[i*2+1]) for i in range(4)), '--', words)
      assert words == result

    sys.exit(0)


ALGOS = {
  'otp-md4': None,
  'otp-md5': otp_md5,
  'otp-sha1': None,
  }


# If the pyotp module is present, then add an algorithm for its use
if pyotp:
  def totp(password):
    t = pyotp.TOTP(password)
    return t.now()
  ALGOS[ALGO_TOTP] = totp


def main():
  cmd = os.path.basename(sys.argv[0])
  if cmd in ALGOS:
    # switch to USAGE:
    #   $ ALGO SEQUENCE SEED
    # omit the prompt, and use argv
    algo = cmd
    seq = int(sys.argv[1])
    seed = sys.argv[2].lower().encode()
    # ignore any other argv
  else:
    parser = argparse.ArgumentParser(description='Compute OTP strings.')
    parser.add_argument('--test', help='Run the test suite.', nargs=0,
                        action=RunTests)
    parser.add_argument('--qr', help='Print a URL for a QR code',
                        action='store_true')
    parser.add_argument('args', nargs=argparse.REMAINDER)
  
    # Note: this may exit, if tests are run.
    parsed = parser.parse_args()

    parts = parsed.args

    # We have two possible forms:
    #   $ CMD [ALGO] SEQUENCE SEED
    #   $ CMD totp SEED

    # If the [ALGO] SEQUENCE SEED is not on the cmdline, then ask.
    if len(parts) < 2:
      line = input('Challenge? ')
      parts = line.split()

    if len(parts) == 2:
      if parts[0] == ALGO_TOTP:
        # form: totp SEED
        algo = ALGO_TOTP
        seq = None  # unused
      else:
        # form: SEQUENCE SEED
        algo = None
        seq = int(parts[0])
      seed = parts[1].lower().encode()
    elif len(parts) >= 3:
      algo = parts[0]
      seq = int(parts[1])
      seed = parts[2].lower().encode()
      # ignore anything else on line (eg. "ext")
    else:
      print('ERROR: challenge must have: [ALGO] SEQUENCE SEED ...')
      sys.exit(1)

  # Load a dictionary mapping seeds to passwords.
  pwds = load_passwords()

  if seed not in pwds:
    print('Creating new password for:', seed.decode())
    if not algo:
      algo = DEFAULT_ALGO
      print('NOTE: using "%s" algorithm' % (algo,))
    pwd = add_password(seed, algo)
  else:
    old_algo = algo
    pwd, algo = pwds[seed]
    if old_algo and algo != old_algo:
      print('NOTE: switched to "%s" algorithm' % (algo,))

  processor = ALGOS.get(algo)
  assert processor, 'Unknown/unsupported algorithm: "%s"' % (algo,)

  if algo == ALGO_TOTP:
    if parsed.qr:
      response = TOTP_QR_GEN % (pwd.decode(),)
    else:
      response = processor(pwd)
  else:
    value = processor(seed + pwd, seq)
    response = ' '.join(to_words(value))
  print('Response:', response)

  osname = platform.system()

  # Attempt to push this to the clipboard
  if osname == 'Darwin':
    result = subprocess.run(['pbcopy', '-pboard', 'general'], input=response.encode())
  else:
    result = subprocess.run(['xclip', '-selection', 'clipboard'], input=response.encode())
  if result.returncode == 0:
    print('NOTE: copied to clipboard')


# From RFC 2289, Appendix D.
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",
)


# From RFC 2289, Appendix C.
MD5_TESTS = (
  ("This is a test.", "TeSt", 0, "INCH SEA ANNE LONG AHEM TOUR"),
  ("This is a test.", "TeSt", 1, "EASE OIL FUM CURE AWRY AVIS"),
  ("This is a test.", "TeSt", 99, "BAIL TUFT BITS GANG CHEF THY"),
  ("AbCdEfGhIjK", "alpha1", 0, "FULL PEW DOWN ONCE MORT ARC"),
  ("AbCdEfGhIjK", "alpha1", 1, "FACT HOOF AT FIST SITE KENT"),
  ("AbCdEfGhIjK", "alpha1", 99, "BODE HOP JAKE STOW JUT RAP"),
  ("OTP's are good", "correct", 0, "ULAN NEW ARMY FUSE SUIT EYED"),
  ("OTP's are good", "correct", 1, "SKIM CULT LOB SLAM POE HOWL"),
  ("OTP's are good", "correct", 99, "LONG IVY JULY AJAR BOND LEE"),
)


if __name__ == '__main__':
  main()
