# -*- coding: utf-8 -*-
# SPDX-License-Identifier: BSD-2-Clause-FreeBSD
#
# Copyright (c) 2020-2023 Simeon Simeonov
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
#    this list of conditions and the following disclaimer in the documentation
#    and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
"""A pure Python implementation of the RFC-2289 OTP generator"""
import binascii
import hashlib
import string

OTP_ALGO_MD5 = 1
OTP_ALGO_SHA1 = 2

# the tokens are defined in https://tools.ietf.org/html/rfc2289 #
RFC1760_TOKENS = [
    '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']

_ALGO_DICT = {OTP_ALGO_MD5: 'md5', OTP_ALGO_SHA1: 'sha1'}


class OTPGeneratorException(Exception):
    """OTPGeneratorException class"""


class OTPChallengeException(Exception):
    """OTPChallengeException class"""


class OTPGenerator:
    """OTPGenerator class"""

    def __init__(
        self,
        password: bytes,
        seed: str = '',
        hash_algo=OTP_ALGO_MD5,
    ):
        """
        Constructs an OTPGenerator object with a given password and seed.

        :param password: The password string
        :type password: bytes

        :param seed: The seed received from the challenge, defaults to ''
        :type seed: str

        :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5
        :type hash_algo: int or str

        :raises otp2289.OTPGeneratorException: If the input does not validate
        """
        # enforce the rfc2289 constraints
        self._seed = seed
        if self._seed:  # the seed was set here. Validate it
            self._seed = self.validate_seed(self._seed)
        self._hash_algo = self.validate_hash_algo(hash_algo)
        if not isinstance(password, bytes):
            raise OTPGeneratorException('Password must be a byte-string')
        if len(password) < 10:
            raise OTPGeneratorException(
                'Password must be longer than 10 bytes'
            )
        self._password = password

    def __repr__(self):
        """repr implementation"""
        return (
            f'{self.__class__} at {id(self)} (seed={self._seed}, '
            f'hash_algo={self._hash_algo})'
        )

    @staticmethod
    def bit_pair_sum(bit_stream: str) -> int:
        """
        Split bit_stream in bit-pairs and sum them all together.

        :param bit_stream: The bit-stream object
        :type bit_stream: str

        :return: The sum of all bit-pairs in bit_stream
        :rtype: int
        """
        if not isinstance(bit_stream, str):
            raise OTPGeneratorException('bit_stream must be of type str')
        if len(bit_stream) != 64:
            raise OTPGeneratorException('bit_stream must be of size 64')
        value = 0
        for pair in zip(bit_stream[::2], bit_stream[1::2]):
            value += int(''.join(pair), 2)
        return value

    @staticmethod
    def bytes_to_tokens(hash_bytes: bytes) -> str:
        """
        Returns a 6 words token from bytes as specified by RFC-2289.

        :param hash_bytes: The input bytes
        :type hash_bytes: bytes

        :return: 6 words tokens
        :rtype: str
        """
        bit_stream = ''.join([f'{byte:0>8b}' for byte in hash_bytes])
        bit_pair_sum = OTPGenerator.bit_pair_sum(bit_stream)
        tokens = []
        tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)])
        tokens.append(RFC1760_TOKENS[int(bit_stream[11:22], 2)])
        tokens.append(RFC1760_TOKENS[int(bit_stream[22:33], 2)])
        tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)])
        tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)])
        tokens.append(
            RFC1760_TOKENS[
                int(
                    bit_stream[55:64] + f'{bit_pair_sum:0>8b}'[-2:],
                    2,
                )
            ]
        )
        return ' '.join(tokens)

    @staticmethod
    def get_tokens_from_challenge(challenge: str) -> tuple:
        """
        Returns tokens (seed, hash_algo and step) from a challenge string.

        N.B. The tokens are not validated here.

        :param challenge: The challenge string described in RFC-2289
        :type challenge: str

        :raises otp2289.OTPChallengeException: If the challenge is invalid

        :return: (seed, hash_algo, step) tuple.
        :rtype: tuple
        """
        if not isinstance(challenge, str):
            raise OTPChallengeException('Challenge must be str')
        challenge = challenge.strip()
        if not challenge.startswith('otp-'):
            raise OTPChallengeException('Invalid challenge')
        try:
            hash_algo, step, seed = challenge[4:].split()
            return (seed, hash_algo, int(step))
        except ValueError:
            raise OTPChallengeException('Invalid challenge') from None

    @staticmethod
    def sha1_digest_folding(sha1_digest: bytes) -> bytes:
        """
        Implementation of the 160bit -> 64bit folding algorithm
        for sha1 digest.

        :param sha1_digest: The SHA1 digest
        :type sha1_digest: bytes

        :return: The byte-string representing the folded sha1-digest
        :rtype: bytes
        """
        if not (isinstance(sha1_digest, bytes)):
            raise OTPGeneratorException('sha1_digest must be of type bytes')
        if len(sha1_digest) != 20:
            raise OTPGeneratorException(
                'sha1_digest must be 160 bits (20 bytes) long'
            )
        digested = list(5 * b'i')  # 5 bytes (40 bits)
        result = list(8 * b'x')  # 8 bytes (64 bits)
        for i in range(5):
            digested[i] = (
                ((sha1_digest[i * 4 + 0] & 0xFF) << 24)
                | ((sha1_digest[i * 4 + 1] & 0xFF) << 16)
                | ((sha1_digest[i * 4 + 2] & 0xFF) << 8)
                | (sha1_digest[i * 4 + 3] & 0xFF)
            )
        # sha.digest[0] ^= sha.digest[2];
        # sha.digest[1] ^= sha.digest[3];
        # sha.digest[0] ^= sha.digest[4];
        digested[0] ^= digested[2]
        digested[1] ^= digested[3]
        digested[0] ^= digested[4]
        # for (i = 0, j = 0; j < 8; i++, j += 4) {
        # result[j] = (unsigned char)(sha.digest[i] & 0xff);
        # result[j+1] = (unsigned char)((sha.digest[i] >> 8) & 0xff);
        # result[j+2] = (unsigned char)((sha.digest[i] >> 16) & 0xff);
        # result[j+3] = (unsigned char)((sha.digest[i] >> 24) & 0xff);
        # }
        # just hardcoding the two iterations for better efficiency
        result[0] = digested[0] & 0xFF
        result[1] = (digested[0] >> 8) & 0xFF
        result[2] = (digested[0] >> 16) & 0xFF
        result[3] = (digested[0] >> 24) & 0xFF
        result[4] = digested[1] & 0xFF
        result[5] = (digested[1] >> 8) & 0xFF
        result[6] = (digested[1] >> 16) & 0xFF
        result[7] = (digested[1] >> 24) & 0xFF
        return bytes(result)

    @staticmethod
    def strxor(byte_str1: bytes, byte_str2: bytes) -> bytes:
        """
        Implementation of strxor similar to the one provided by pycrypto.

        :param byte_str1: Byte-string 1
        :type byte_str1: bytes

        :param byte_str2: Byte-string 2
        :type byte_str2: bytes

        :return: The byte-string representing the result of byte_str1^byte_str2
        :rtype: bytes
        """
        if not (isinstance(byte_str1, bytes) and isinstance(byte_str2, bytes)):
            raise OTPGeneratorException(
                'byte_str1 and byte_str2 must be of type bytes'
            )
        length = len(byte_str1)
        if length != len(byte_str2) or length < 1:
            raise OTPGeneratorException(
                'byte_str1 and byte_str2 must be of the same length > 0'
            )
        return bytes([byte_str1[i] ^ byte_str2[i] for i in range(length)])

    @staticmethod
    def tokens_to_bytes(tokens_str: str) -> bytes:
        """
        Returns bytes from a 6 words token as specified by RFC-2289.

        :param tokens_str: String representing 6 words tokens
        :type tokens_str: str

        :raises otp2289.OTPGeneratorException: When the tokens_str is invalid

        :return: 6 words tokens
        :rtype: bytes
        """
        if not isinstance(tokens_str, str):
            raise OTPGeneratorException('tokens must be a str')
        tokens = tokens_str.split()
        if len(tokens) != 6:
            raise OTPGeneratorException(
                'Tokens-string does not contain 6 tokens'
            )
        token_ints = []
        try:
            token_ints = [
                RFC1760_TOKENS.index(token.upper()) for token in tokens
            ]
        except ValueError:
            raise OTPGeneratorException(
                'One or more words not present in RFC1760'
            ) from None
        # now we build a string of bits
        bit_stream = format(token_ints[0], '011b')
        bit_stream += format(token_ints[1], '011b')
        bit_stream += format(token_ints[2], '011b')
        bit_stream += format(token_ints[3], '011b')
        bit_stream += format(token_ints[4], '011b')
        bit_stream += format(token_ints[5], '011b')
        # we have 66 bits: 64 digest + 2 bit pair sum (control number)
        # RFC-2289: All OTP generators MUST calculate this checksum and all
        # OTP servers MUST verify this checksum explicitly as part of the
        # operation of decoding this representation of the one-time password.
        if (
            f'{OTPGenerator.bit_pair_sum(bit_stream[:64]):0>8b}'[-2:]
            != bit_stream[-2:]
        ):
            raise OTPGeneratorException('Invalid bit checksum')
        return int(bit_stream[:64], 2).to_bytes(8, 'big')

    @staticmethod
    def validate_hash_algo(hash_algo) -> str:
        """
        Validates the provided hash-algorithm.

        :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5
        :type hash_algo: int or str

        :raises otp2289.OTPGeneratorException: If hash_algo does not validate

        :return: The validated hash_algo in str-form
        :rtype: str
        """
        if isinstance(hash_algo, int):
            if hash_algo not in _ALGO_DICT:
                raise OTPGeneratorException(
                    'hash_algo is not among the known algorithms'
                )
            hash_algo = _ALGO_DICT.get(hash_algo)
        if not isinstance(hash_algo, str):
            raise OTPGeneratorException('hash_algo must be an int or a str')
        if hash_algo not in hashlib.algorithms_available:
            raise OTPGeneratorException(
                f'{hash_algo} is not supported by this version of the '
                'hashlib module'
            )
        return hash_algo

    @staticmethod
    def validate_seed(seed: str) -> str:
        """
        Validates the provided seed as defined by RFC-2289.

        :param seed: The seed received from the challenge, defaults to ''
        :type seed: str

        :raises otp2289.OTPGeneratorException: If seed does not validate

        :return: The validated (and very same) seed
        :rtype: str
        """
        if not isinstance(seed, str):
            raise OTPGeneratorException('Seed must be a string')
        if not seed or len(seed) > 16:
            raise OTPGeneratorException(
                'The seed MUST be of 1 to 16 characters in length'
            )
        for char in seed:
            if char not in string.ascii_letters + string.digits:
                raise OTPGeneratorException(
                    'The seed MUST consist of purely alphanumeric characters'
                )
        return seed

    @staticmethod
    def validate_step(step: int) -> int:
        """
        Validates the provided step as defined by RFC-2289.

        :param seed: The step received from the challenge
        :type seed: int

        :raises otp2289.OTPGeneratorException: If step does not validate

        :return: The validated (and very same) step
        :rtype: int
        """
        if not isinstance(step, int):
            raise OTPGeneratorException('Step value MUST be an int')
        if step < 0:
            raise OTPGeneratorException('Step value MUST be >= 0')
        return step

    def generate_otp_hexdigest(self, step: int) -> str:
        """
        Generates the OTP hexdigest for the given step.

        :param step: The step to generate OTP for
        :type step: int

        :return: Hexdigest for the given step
        :rtype: str
        """
        return '0x' + binascii.hexlify(self._generate_otp_bytes(step)).decode()

    def generate_otp_hexdigest_from_challenge(self, challenge: str) -> str:
        """
        Same as generate_otp_hexdigest, but it generates hex. from a challenge.

        RFC-2289 states:
        The challenge MUST be in a standard syntax so
        that automated generators can recognize the challenge in context and
        extract these parameters. The syntax of the challenge is:
        otp-<algorithm identifier> <sequence integer> <seed>

        :param challenge: The challenge string
        :type challenge: str

        :return: Hexdigest for the given challenge
        :rtype: str
        """
        seed, hash_algo, step = self.get_tokens_from_challenge(challenge)
        self._seed = self.validate_seed(seed)
        self._hash_algo = self.validate_hash_algo(hash_algo)
        return self.generate_otp_hexdigest(step)

    def generate_otp_words(self, step: int) -> str:
        """
        Generates the OTP six words token for the given step.

        :param step: The step to generate OTP for
        :type step: int

        :return: Six words (separated by single space) token for the given step
        :rtype: str
        """
        return self.bytes_to_tokens(self._generate_otp_bytes(step))

    def generate_otp_words_from_challenge(self, challenge: str) -> str:
        """
        Same as generate_otp_words, but it generates words from a challenge.

        RFC-2289 states:
        The challenge MUST be in a standard syntax so
        that automated generators can recognize the challenge in context and
        extract these parameters. The syntax of the challenge is:
        otp-<algorithm identifier> <sequence integer> <seed>

        :param challenge: The challenge string
        :type challenge: str

        :return: Six words token for the given challenge
        :rtype: str
        """
        seed, hash_algo, step = self.get_tokens_from_challenge(challenge)
        self._seed = self.validate_seed(seed)
        self._hash_algo = self.validate_hash_algo(hash_algo)
        return self.generate_otp_words(step)

    def hexdigest_range(self, start: int = 499, stop: int = 0):
        """
        Returns an iterator that providing hexdigests corresponding to steps
        from `start` to and including `stop`.

        :param start: The start of the range (default: 499)
        :type start: int

        :param stop: The last step (default: 0)
        :type stop: int

        :return: Iterator
        :rtype: generator
        """
        if not isinstance(start, int) and isinstance(stop, int):
            raise OTPGeneratorException('Step value MUST be an int')
        if start < stop:
            raise OTPGeneratorException(
                'Start value can not be lower than stop'
            )
        for step in range(start, stop - 1, -1):
            yield self.generate_otp_hexdigest(step)

    def words_range(self, start: int = 499, stop: int = 0):
        """
        Returns an iterator that providing the words corresponding to steps
        from `start` to and including `stop`.

        :param start: The start of the range (default: 499)
        :type start: int

        :param stop: The last step (default: 0)
        :type stop: int

        :return: Iterator
        :rtype: generator
        """
        if not isinstance(start, int) and isinstance(stop, int):
            raise OTPGeneratorException('Step value MUST be an int')
        if start < stop:
            raise OTPGeneratorException(
                'Start value can not be lower than stop'
            )
        for step in range(start, stop - 1, -1):
            yield self.generate_otp_words(step)

    def _generate_otp_bytes(self, step: int) -> bytes:
        """
        Generates the OTP bytes for the given step.

        :param step: The step to generate OTP for
        :type step: int

        :return: The digest bytes for the given step
        :rtype: bytes
        """
        step = self.validate_step(step)
        digest = b''
        for _ in range(step + 1):
            hash_obj = hashlib.new(self._hash_algo)
            if not digest:
                # 0 step
                hash_obj.update(self._seed.lower().encode() + self._password)
            else:
                hash_obj.update(digest)
            large_digest = hash_obj.digest()
            if self._hash_algo == 'md5':
                # md4 and md5 128bit -> 64bit folding
                digest = self.strxor(large_digest[0:8], large_digest[8:])
            elif self._hash_algo == 'sha1':
                # sha1 160bit -> 64bit folding
                digest = self.sha1_digest_folding(large_digest)
            else:
                raise OTPGeneratorException(
                    f'{self._hash_algo} is not supported by this module'
                )
        return digest
