# SPDX-License-Identifier: BSD-2-Clause
#
# Copyright (c) 2020-2026 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"""
from __future__ import annotations
import hashlib
import string
import typing
if typing.TYPE_CHECKING:
from collections.abc import Iterator
OTP_ALGO_MD5: typing.Final[int] = 1
OTP_ALGO_SHA1: typing.Final[int] = 2
# useful constants
OTP2289_BITSTREAM_SIZE: typing.Final[int] = 64
OTP2289_HEX_DIGEST_SIZE: typing.Final[int] = 16
OTP2289_MAX_SEED_LENGTH: typing.Final[int] = 16
OTP2289_MIN_PASSWORD_LENGTH: typing.Final[int] = 10
OTP2289_SHA1_DIGEST_SIZE: typing.Final[int] = 20
OTP2289_TOKENS_COUNT: typing.Final[int] = 6
# 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'}
[docs]
class OTPChallengeError(Exception):
"""OTPChallengeError class"""
[docs]
class OTPGeneratorError(Exception):
"""OTPGeneratorError class"""
[docs]
class OTPResponseError(Exception):
"""OTPResponseError class"""
[docs]
class OTPResponse:
"""Encapsulates the functionality for a single OTP response"""
def __init__(self, response_bytes: bytes) -> None:
"""
Constructs a single OTP response
:param response_bytes: The response state
:type response_bytes: bytes
"""
self._response_bytes = response_bytes
self._hexdigest = '0x' + response_bytes.hex()
self._words = self.bytes_to_tokens(response_bytes)
def __bytes__(self) -> bytes:
"""bytes representation of the object"""
return self._response_bytes
def __eq__(self, value: object, /) -> bool:
"""definition for type equality"""
if not isinstance(value, OTPResponse):
return False
return self._response_bytes == value.response_bytes
def __hash__(self) -> int:
"""Uses the hash value of _response_bytes"""
return hash(self._response_bytes)
def __repr__(self) -> str:
"""repr implementation"""
return (
f'{self.__class__} at {id(self)} '
f'(response_bytes={self._response_bytes!r})'
)
@property
def hexdigest(self) -> str:
"""Hexdigest representation of the OTP response"""
return self._hexdigest
@property
def response_bytes(self) -> bytes:
"""response_bytes read-only property"""
return self._response_bytes
@property
def words(self) -> str:
"""Tokens representation of the OTP response"""
return self._words
[docs]
@classmethod
def from_hex(cls, ot_hex: str) -> OTPResponse:
"""
Generates instance from the provided hexidigest with or without
leading 0x as specified by RFC-2289.
:param ot_hex: The one-time hex to validate
:type ot_hex: str
:raises otp2289.OTPResponseError: When the ot_hex is invalid
:return: A new OTPResponse object
:rtype: otp2289.OTPResponse
"""
return cls(OTPResponse.hex_to_bytes(ot_hex))
[docs]
@classmethod
def from_tokens(cls, tokens_str: str) -> OTPResponse:
"""
Generates instance from a 6 words token as specified by RFC-2289.
:param tokens_str: String representing 6 words tokens
:type tokens_str: str
:raises otp2289.OTPResponseError: When the tokens_str is invalid
:return: A new OTPResponse object
:rtype: otp2289.OTPResponse
"""
return cls(OTPResponse.tokens_to_bytes(tokens_str))
[docs]
@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 OTPResponseError('bit_stream must be of type str')
if len(bit_stream) != OTP2289_BITSTREAM_SIZE:
raise OTPResponseError(
f'bit_stream must be of size {OTP2289_BITSTREAM_SIZE}'
)
value = 0
for pair in zip(bit_stream[::2], bit_stream[1::2], strict=True):
value += int(''.join(pair), 2)
return value
[docs]
@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 = OTPResponse.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)
[docs]
@staticmethod
def hex_to_bytes(ot_hex: str) -> bytes:
"""
Returns bytes from the provided hexidigest.
:param ot_hex: The one-time hex to validate
:type ot_hex: str
:raises otp2289.OTPResponseError: If hex does not validate
:return: The validated hex (without leading 0x) converted to bytes
:rtype: bytes
"""
if not isinstance(ot_hex, str):
raise OTPResponseError('OT-hex must be a str')
if ot_hex.startswith('0x'):
ot_hex = ot_hex[2:]
ot_hex = ot_hex.strip().lower()
if len(ot_hex) != OTP2289_HEX_DIGEST_SIZE:
raise OTPResponseError(
f'The length of the hex should be {OTP2289_HEX_DIGEST_SIZE} '
'(representing 64 bits digest)'
)
try:
return bytes.fromhex(ot_hex)
except ValueError:
raise OTPResponseError('Invalid OT-hex') from None
[docs]
@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.OTPResponseError: When the tokens_str is invalid
:return: 6 words tokens
:rtype: bytes
"""
if not isinstance(tokens_str, str):
raise OTPResponseError('tokens must be a str')
tokens = tokens_str.split()
if len(tokens) != OTP2289_TOKENS_COUNT:
raise OTPResponseError(
f'Tokens-string does not contain {OTP2289_TOKENS_COUNT} tokens'
)
token_ints = []
try:
token_ints = [
RFC1760_TOKENS.index(token.upper()) for token in tokens
]
except ValueError:
raise OTPResponseError(
'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'{OTPResponse.bit_pair_sum(bit_stream[:64]):0>8b}'[-2:]
!= bit_stream[-2:]
):
raise OTPResponseError('Invalid bit checksum')
return int(bit_stream[:64], 2).to_bytes(8, 'big')
[docs]
class OTPGenerator:
"""OTPGenerator class"""
def __init__(
self,
password: bytes,
seed: str = '',
hash_algo: int | str = OTP_ALGO_MD5,
) -> None:
"""
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.OTPGeneratorError: 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 OTPGeneratorError('Password must be a byte-string')
if len(password) < OTP2289_MIN_PASSWORD_LENGTH:
raise OTPGeneratorError(
f'Password must be longer than {OTP2289_MIN_PASSWORD_LENGTH} '
'bytes'
)
self._password = password
def __repr__(self) -> str:
"""repr implementation"""
return (
f'{self.__class__} at {id(self)} (seed={self._seed}, '
f'hash_algo={self._hash_algo})'
)
[docs]
@staticmethod
def get_tokens_from_challenge(challenge: str) -> tuple[str, str, int]:
"""
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.OTPChallengeError: If the challenge is invalid
:return: (seed, hash_algo, step) tuple.
:rtype: tuple
"""
if not isinstance(challenge, str):
raise OTPChallengeError('Challenge must be str')
challenge = challenge.strip()
if not challenge.startswith('otp-'):
raise OTPChallengeError('Invalid challenge')
try:
hash_algo, step, seed = challenge[4:].split()
return (seed, hash_algo, int(step))
except ValueError:
raise OTPChallengeError('Invalid challenge') from None
[docs]
@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 OTPGeneratorError('sha1_digest must be of type bytes')
if len(sha1_digest) != OTP2289_SHA1_DIGEST_SIZE:
raise OTPGeneratorError(
f'sha1_digest must be {OTP2289_SHA1_DIGEST_SIZE * 2} bits '
f'({OTP2289_SHA1_DIGEST_SIZE} 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)
[docs]
@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 OTPGeneratorError(
'byte_str1 and byte_str2 must be of type bytes'
)
length = len(byte_str1)
if length != len(byte_str2) or length < 1:
raise OTPGeneratorError(
'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)])
[docs]
@staticmethod
def validate_hash_algo(hash_algo: int | str) -> 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.OTPGeneratorError: 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 OTPGeneratorError(
'hash_algo is not among the known algorithms'
)
hash_algo = _ALGO_DICT[hash_algo]
if not isinstance(hash_algo, str):
raise OTPGeneratorError('hash_algo must be an int or a str')
if hash_algo not in hashlib.algorithms_available:
raise OTPGeneratorError(
f'{hash_algo} is not supported by this version of the '
'hashlib module'
)
return hash_algo
[docs]
@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.OTPGeneratorError: If seed does not validate
:return: The validated (and very same) seed
:rtype: str
"""
if not isinstance(seed, str):
raise OTPGeneratorError('Seed must be a string')
if not seed or len(seed) > OTP2289_MAX_SEED_LENGTH:
raise OTPGeneratorError(
f'The seed MUST be of 1 to {OTP2289_MAX_SEED_LENGTH} '
'characters in length'
)
for char in seed:
if char not in string.ascii_letters + string.digits:
raise OTPGeneratorError(
'The seed MUST consist of purely alphanumeric characters'
)
return seed
[docs]
@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.OTPGeneratorError: If step does not validate
:return: The validated (and very same) step
:rtype: int
"""
if not isinstance(step, int):
raise OTPGeneratorError('Step value MUST be an int')
if step < 0:
raise OTPGeneratorError('Step value MUST be >= 0')
return step
[docs]
def generate_otp_response(self, step: int) -> OTPResponse:
"""
Generates OTPResponse instance for the given step
:param step: The step to generate OTP for
:type step: int
:return: OTPResponse instance for the given step
:rtype: OTPResponse
"""
return OTPResponse(self._generate_otp_bytes(step))
[docs]
def generate_otp_response_from_challenge(
self, challenge: str
) -> OTPResponse:
"""
Same as generate_otp_response,
but it generates OTPResponse 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_response(step)
[docs]
def otp_response_range(
self, start: int = 499, stop: int = 0
) -> Iterator[OTPResponse]:
"""
Returns an iterator that is providing OTPResponse instances
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 OTPGeneratorError('Step value MUST be an int')
if start < stop:
raise OTPGeneratorError('Start value can not be lower than stop')
for step in range(start, stop - 1, -1):
yield self.generate_otp_response(step)
[docs]
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 OTPGeneratorError(
f'{self._hash_algo} is not supported by this module'
)
return digest