Source code for otp2289.generator

# 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