summaryrefslogtreecommitdiff
path: root/otp2289/generator.py
diff options
context:
space:
mode:
Diffstat (limited to 'otp2289/generator.py')
-rw-r--r--otp2289/generator.py142
1 files changed, 82 insertions, 60 deletions
diff --git a/otp2289/generator.py b/otp2289/generator.py
index 5ea738b..7b86e1b 100644
--- a/otp2289/generator.py
+++ b/otp2289/generator.py
@@ -1,7 +1,7 @@
1# -*- coding: utf-8 -*- 1# -*- coding: utf-8 -*-
2# SPDX-License-Identifier: BSD-2-Clause-FreeBSD 2# SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3# 3#
4# Copyright (c) 2020, Simeon Simeonov 4# Copyright (c) 2020-2022 Simeon Simeonov
5# All rights reserved. 5# All rights reserved.
6# 6#
7# Redistribution and use in source and binary forms, with or without 7# Redistribution and use in source and binary forms, with or without
@@ -28,7 +28,6 @@ import binascii
28import hashlib 28import hashlib
29import string 29import string
30 30
31
32OTP_ALGO_MD5 = 1 31OTP_ALGO_MD5 = 1
33OTP_ALGO_SHA1 = 2 32OTP_ALGO_SHA1 = 2
34 33
@@ -306,7 +305,12 @@ class OTPChallengeException(Exception):
306class OTPGenerator: 305class OTPGenerator:
307 """OTPGenerator class""" 306 """OTPGenerator class"""
308 307
309 def __init__(self, password, seed='', hash_algo=OTP_ALGO_MD5): 308 def __init__(
309 self,
310 password: bytes,
311 seed: str = '',
312 hash_algo=OTP_ALGO_MD5,
313 ):
310 """ 314 """
311 Constructs an OTPGenerator object with a given password and seed. 315 Constructs an OTPGenerator object with a given password and seed.
312 316
@@ -330,16 +334,19 @@ class OTPGenerator:
330 raise OTPGeneratorException('Password must be a byte-string') 334 raise OTPGeneratorException('Password must be a byte-string')
331 if len(password) < 10: 335 if len(password) < 10:
332 raise OTPGeneratorException( 336 raise OTPGeneratorException(
333 'Password must be longer than 10 bytes') 337 'Password must be longer than 10 bytes'
338 )
334 self._password = password 339 self._password = password
335 340
336 def __repr__(self): 341 def __repr__(self):
337 """repr implementation""" 342 """repr implementation"""
338 return (f'{self.__class__} at {id(self)} (seed={self._seed}, ' 343 return (
339 f'hash_algo={self._hash_algo})') 344 f'{self.__class__} at {id(self)} (seed={self._seed}, '
345 f'hash_algo={self._hash_algo})'
346 )
340 347
341 @staticmethod 348 @staticmethod
342 def bit_pair_sum(bit_stream): 349 def bit_pair_sum(bit_stream: str) -> int:
343 """ 350 """
344 Split bit_stream in bit-pairs and sum them all together. 351 Split bit_stream in bit-pairs and sum them all together.
345 352
@@ -359,7 +366,7 @@ class OTPGenerator:
359 return value 366 return value
360 367
361 @staticmethod 368 @staticmethod
362 def bytes_to_tokens(hash_bytes): 369 def bytes_to_tokens(hash_bytes: bytes) -> str:
363 """ 370 """
364 Returns a 6 words token from bytes as specified by RFC-2289. 371 Returns a 6 words token from bytes as specified by RFC-2289.
365 372
@@ -369,8 +376,7 @@ class OTPGenerator:
369 :return: 6 words tokens 376 :return: 6 words tokens
370 :rtype: str 377 :rtype: str
371 """ 378 """
372 bit_stream = ''.join( 379 bit_stream = ''.join([f'{byte:0>8b}' for byte in hash_bytes])
373 ['{0:0>8b}'.format(byte) for byte in hash_bytes])
374 bit_pair_sum = OTPGenerator.bit_pair_sum(bit_stream) 380 bit_pair_sum = OTPGenerator.bit_pair_sum(bit_stream)
375 tokens = [] 381 tokens = []
376 tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)]) 382 tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)])
@@ -379,12 +385,17 @@ class OTPGenerator:
379 tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)]) 385 tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)])
380 tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)]) 386 tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)])
381 tokens.append( 387 tokens.append(
382 RFC1760_TOKENS[int( 388 RFC1760_TOKENS[
383 bit_stream[55:64] + '{0:0>8b}'.format(bit_pair_sum)[-2:], 2)]) 389 int(
390 bit_stream[55:64] + f'{bit_pair_sum:0>8b}'[-2:],
391 2,
392 )
393 ]
394 )
384 return ' '.join(tokens) 395 return ' '.join(tokens)
385 396
386 @staticmethod 397 @staticmethod
387 def get_tokens_from_challenge(challenge): 398 def get_tokens_from_challenge(challenge: str) -> tuple:
388 """ 399 """
389 Returns tokens (seed, hash_algo and step) from a challenge string. 400 Returns tokens (seed, hash_algo and step) from a challenge string.
390 401
@@ -410,7 +421,7 @@ class OTPGenerator:
410 raise OTPChallengeException('Invalid challenge') from None 421 raise OTPChallengeException('Invalid challenge') from None
411 422
412 @staticmethod 423 @staticmethod
413 def sha1_digest_folding(sha1_digest): 424 def sha1_digest_folding(sha1_digest: bytes) -> bytes:
414 """ 425 """
415 Implementation of the 160bit -> 64bit folding algorithm 426 Implementation of the 160bit -> 64bit folding algorithm
416 for sha1 digest. 427 for sha1 digest.
@@ -425,14 +436,17 @@ class OTPGenerator:
425 raise OTPGeneratorException('sha1_digest must be of type bytes') 436 raise OTPGeneratorException('sha1_digest must be of type bytes')
426 if len(sha1_digest) != 20: 437 if len(sha1_digest) != 20:
427 raise OTPGeneratorException( 438 raise OTPGeneratorException(
428 'sha1_digest must be 160 bits (20 bytes) long') 439 'sha1_digest must be 160 bits (20 bytes) long'
440 )
429 digested = list(5 * b'i') # 5 bytes (40 bits) 441 digested = list(5 * b'i') # 5 bytes (40 bits)
430 result = list(8 * b'x') # 8 bytes (64 bits) 442 result = list(8 * b'x') # 8 bytes (64 bits)
431 for i in range(5): 443 for i in range(5):
432 digested[i] = (((sha1_digest[i * 4 + 0] & 0xff) << 24) | 444 digested[i] = (
433 ((sha1_digest[i * 4 + 1] & 0xff) << 16) | 445 ((sha1_digest[i * 4 + 0] & 0xFF) << 24)
434 ((sha1_digest[i * 4 + 2] & 0xff) << 8) | 446 | ((sha1_digest[i * 4 + 1] & 0xFF) << 16)
435 (sha1_digest[i * 4 + 3] & 0xff)) 447 | ((sha1_digest[i * 4 + 2] & 0xFF) << 8)
448 | (sha1_digest[i * 4 + 3] & 0xFF)
449 )
436 # sha.digest[0] ^= sha.digest[2]; 450 # sha.digest[0] ^= sha.digest[2];
437 # sha.digest[1] ^= sha.digest[3]; 451 # sha.digest[1] ^= sha.digest[3];
438 # sha.digest[0] ^= sha.digest[4]; 452 # sha.digest[0] ^= sha.digest[4];
@@ -446,18 +460,18 @@ class OTPGenerator:
446 # result[j+3] = (unsigned char)((sha.digest[i] >> 24) & 0xff); 460 # result[j+3] = (unsigned char)((sha.digest[i] >> 24) & 0xff);
447 # } 461 # }
448 # just hardcoding the two iterations for better efficiency 462 # just hardcoding the two iterations for better efficiency
449 result[0] = digested[0] & 0xff 463 result[0] = digested[0] & 0xFF
450 result[1] = (digested[0] >> 8) & 0xff 464 result[1] = (digested[0] >> 8) & 0xFF
451 result[2] = (digested[0] >> 16) & 0xff 465 result[2] = (digested[0] >> 16) & 0xFF
452 result[3] = (digested[0] >> 24) & 0xff 466 result[3] = (digested[0] >> 24) & 0xFF
453 result[4] = digested[1] & 0xff 467 result[4] = digested[1] & 0xFF
454 result[5] = (digested[1] >> 8) & 0xff 468 result[5] = (digested[1] >> 8) & 0xFF
455 result[6] = (digested[1] >> 16) & 0xff 469 result[6] = (digested[1] >> 16) & 0xFF
456 result[7] = (digested[1] >> 24) & 0xff 470 result[7] = (digested[1] >> 24) & 0xFF
457 return bytes(result) 471 return bytes(result)
458 472
459 @staticmethod 473 @staticmethod
460 def strxor(byte_str1, byte_str2): 474 def strxor(byte_str1: bytes, byte_str2: bytes) -> bytes:
461 """ 475 """
462 Implementation of strxor similar to the one provided by pycrypto. 476 Implementation of strxor similar to the one provided by pycrypto.
463 477
@@ -472,17 +486,17 @@ class OTPGenerator:
472 """ 486 """
473 if not (isinstance(byte_str1, bytes) and isinstance(byte_str2, bytes)): 487 if not (isinstance(byte_str1, bytes) and isinstance(byte_str2, bytes)):
474 raise OTPGeneratorException( 488 raise OTPGeneratorException(
475 'byte_str1 and byte_str2 must be of type bytes') 489 'byte_str1 and byte_str2 must be of type bytes'
490 )
476 length = len(byte_str1) 491 length = len(byte_str1)
477 if length != len(byte_str2) or length < 1: 492 if length != len(byte_str2) or length < 1:
478 raise OTPGeneratorException( 493 raise OTPGeneratorException(
479 'byte_str1 and byte_str2 must be of the same length > 0') 494 'byte_str1 and byte_str2 must be of the same length > 0'
480 return bytes( 495 )
481 [byte_str1[i] ^ byte_str2[i] for i in range(length)] 496 return bytes([byte_str1[i] ^ byte_str2[i] for i in range(length)])
482 )
483 497
484 @staticmethod 498 @staticmethod
485 def tokens_to_bytes(tokens_str): 499 def tokens_to_bytes(tokens_str: str) -> bytes:
486 """ 500 """
487 Returns bytes from a 6 words token as specified by RFC-2289. 501 Returns bytes from a 6 words token as specified by RFC-2289.
488 502
@@ -499,14 +513,17 @@ class OTPGenerator:
499 tokens = tokens_str.split() 513 tokens = tokens_str.split()
500 if len(tokens) != 6: 514 if len(tokens) != 6:
501 raise OTPGeneratorException( 515 raise OTPGeneratorException(
502 'Tokens-string does not contain 6 tokens') 516 'Tokens-string does not contain 6 tokens'
517 )
503 token_ints = [] 518 token_ints = []
504 try: 519 try:
505 token_ints = [RFC1760_TOKENS.index(token.upper()) for token in 520 token_ints = [
506 tokens] 521 RFC1760_TOKENS.index(token.upper()) for token in tokens
522 ]
507 except ValueError: 523 except ValueError:
508 raise OTPGeneratorException( 524 raise OTPGeneratorException(
509 'One or more words not present in RFC1760') from None 525 'One or more words not present in RFC1760'
526 ) from None
510 # now we build a string of bits 527 # now we build a string of bits
511 bit_stream = format(token_ints[0], '011b') 528 bit_stream = format(token_ints[0], '011b')
512 bit_stream += format(token_ints[1], '011b') 529 bit_stream += format(token_ints[1], '011b')
@@ -519,14 +536,14 @@ class OTPGenerator:
519 # OTP servers MUST verify this checksum explicitly as part of the 536 # OTP servers MUST verify this checksum explicitly as part of the
520 # operation of decoding this representation of the one-time password. 537 # operation of decoding this representation of the one-time password.
521 if ( 538 if (
522 '{0:0>8b}'.format(OTPGenerator.bit_pair_sum( 539 f'{OTPGenerator.bit_pair_sum(bit_stream[:64]):0>8b}'[-2:]
523 bit_stream[:64]))[-2:] != bit_stream[-2:] 540 != bit_stream[-2:]
524 ): 541 ):
525 raise OTPGeneratorException('Invalid bit checksum') 542 raise OTPGeneratorException('Invalid bit checksum')
526 return int(bit_stream[:64], 2).to_bytes(8, 'big') 543 return int(bit_stream[:64], 2).to_bytes(8, 'big')
527 544
528 @staticmethod 545 @staticmethod
529 def validate_hash_algo(hash_algo): 546 def validate_hash_algo(hash_algo) -> str:
530 """ 547 """
531 Validates the provided hash-algorithm. 548 Validates the provided hash-algorithm.
532 549
@@ -541,19 +558,20 @@ class OTPGenerator:
541 if isinstance(hash_algo, int): 558 if isinstance(hash_algo, int):
542 if hash_algo not in _ALGO_DICT: 559 if hash_algo not in _ALGO_DICT:
543 raise OTPGeneratorException( 560 raise OTPGeneratorException(
544 'hash_algo is not among the known algorithms') 561 'hash_algo is not among the known algorithms'
562 )
545 hash_algo = _ALGO_DICT.get(hash_algo) 563 hash_algo = _ALGO_DICT.get(hash_algo)
546 if not isinstance(hash_algo, str): 564 if not isinstance(hash_algo, str):
547 raise OTPGeneratorException( 565 raise OTPGeneratorException('hash_algo must be an int or a str')
548 'hash_algo must be an int or a str')
549 if hash_algo not in hashlib.algorithms_available: 566 if hash_algo not in hashlib.algorithms_available:
550 raise OTPGeneratorException( 567 raise OTPGeneratorException(
551 f'{hash_algo} is not supported by this version of the ' 568 f'{hash_algo} is not supported by this version of the '
552 'hashlib module') 569 'hashlib module'
570 )
553 return hash_algo 571 return hash_algo
554 572
555 @staticmethod 573 @staticmethod
556 def validate_seed(seed): 574 def validate_seed(seed: str) -> str:
557 """ 575 """
558 Validates the provided seed as defined by RFC-2289. 576 Validates the provided seed as defined by RFC-2289.
559 577
@@ -569,15 +587,17 @@ class OTPGenerator:
569 raise OTPGeneratorException('Seed must be a string') 587 raise OTPGeneratorException('Seed must be a string')
570 if not seed or len(seed) > 16: 588 if not seed or len(seed) > 16:
571 raise OTPGeneratorException( 589 raise OTPGeneratorException(
572 'The seed MUST be of 1 to 16 characters in length') 590 'The seed MUST be of 1 to 16 characters in length'
591 )
573 for char in seed: 592 for char in seed:
574 if char not in string.ascii_letters + string.digits: 593 if char not in string.ascii_letters + string.digits:
575 raise OTPGeneratorException( 594 raise OTPGeneratorException(
576 'The seed MUST consist of purely alphanumeric characters') 595 'The seed MUST consist of purely alphanumeric characters'
596 )
577 return seed 597 return seed
578 598
579 @staticmethod 599 @staticmethod
580 def validate_step(step): 600 def validate_step(step: int) -> int:
581 """ 601 """
582 Validates the provided step as defined by RFC-2289. 602 Validates the provided step as defined by RFC-2289.
583 603
@@ -595,7 +615,7 @@ class OTPGenerator:
595 raise OTPGeneratorException('Step value MUST be >= 0') 615 raise OTPGeneratorException('Step value MUST be >= 0')
596 return step 616 return step
597 617
598 def generate_otp_hexdigest(self, step): 618 def generate_otp_hexdigest(self, step: int) -> str:
599 """ 619 """
600 Generates the OTP hexdigest for the given step. 620 Generates the OTP hexdigest for the given step.
601 621
@@ -607,7 +627,7 @@ class OTPGenerator:
607 """ 627 """
608 return '0x' + binascii.hexlify(self._generate_otp_bytes(step)).decode() 628 return '0x' + binascii.hexlify(self._generate_otp_bytes(step)).decode()
609 629
610 def generate_otp_hexdigest_from_challenge(self, challenge): 630 def generate_otp_hexdigest_from_challenge(self, challenge: str) -> str:
611 """ 631 """
612 Same as generate_otp_hexdigest, but it generates hex. from a challenge. 632 Same as generate_otp_hexdigest, but it generates hex. from a challenge.
613 633
@@ -628,7 +648,7 @@ class OTPGenerator:
628 self._hash_algo = self.validate_hash_algo(hash_algo) 648 self._hash_algo = self.validate_hash_algo(hash_algo)
629 return self.generate_otp_hexdigest(step) 649 return self.generate_otp_hexdigest(step)
630 650
631 def generate_otp_words(self, step): 651 def generate_otp_words(self, step: int) -> str:
632 """ 652 """
633 Generates the OTP six words token for the given step. 653 Generates the OTP six words token for the given step.
634 654
@@ -640,7 +660,7 @@ class OTPGenerator:
640 """ 660 """
641 return self.bytes_to_tokens(self._generate_otp_bytes(step)) 661 return self.bytes_to_tokens(self._generate_otp_bytes(step))
642 662
643 def generate_otp_words_from_challenge(self, challenge): 663 def generate_otp_words_from_challenge(self, challenge: str) -> str:
644 """ 664 """
645 Same as generate_otp_words, but it generates words from a challenge. 665 Same as generate_otp_words, but it generates words from a challenge.
646 666
@@ -661,7 +681,7 @@ class OTPGenerator:
661 self._hash_algo = self.validate_hash_algo(hash_algo) 681 self._hash_algo = self.validate_hash_algo(hash_algo)
662 return self.generate_otp_words(step) 682 return self.generate_otp_words(step)
663 683
664 def hexdigest_range(self, start=499, stop=0): 684 def hexdigest_range(self, start: int = 499, stop: int = 0):
665 """ 685 """
666 Returns an iterator that providing hexdigests corresponding to steps 686 Returns an iterator that providing hexdigests corresponding to steps
667 from `start` to and including `stop`. 687 from `start` to and including `stop`.
@@ -679,11 +699,12 @@ class OTPGenerator:
679 raise OTPGeneratorException('Step value MUST be an int') 699 raise OTPGeneratorException('Step value MUST be an int')
680 if start < stop: 700 if start < stop:
681 raise OTPGeneratorException( 701 raise OTPGeneratorException(
682 'Start value can not be lower than stop') 702 'Start value can not be lower than stop'
703 )
683 for step in range(start, stop - 1, -1): 704 for step in range(start, stop - 1, -1):
684 yield self.generate_otp_hexdigest(step) 705 yield self.generate_otp_hexdigest(step)
685 706
686 def words_range(self, start=499, stop=0): 707 def words_range(self, start: int = 499, stop: int = 0):
687 """ 708 """
688 Returns an iterator that providing the words corresponding to steps 709 Returns an iterator that providing the words corresponding to steps
689 from `start` to and including `stop`. 710 from `start` to and including `stop`.
@@ -701,11 +722,12 @@ class OTPGenerator:
701 raise OTPGeneratorException('Step value MUST be an int') 722 raise OTPGeneratorException('Step value MUST be an int')
702 if start < stop: 723 if start < stop:
703 raise OTPGeneratorException( 724 raise OTPGeneratorException(
704 'Start value can not be lower than stop') 725 'Start value can not be lower than stop'
726 )
705 for step in range(start, stop - 1, -1): 727 for step in range(start, stop - 1, -1):
706 yield self.generate_otp_words(step) 728 yield self.generate_otp_words(step)
707 729
708 def _generate_otp_bytes(self, step): 730 def _generate_otp_bytes(self, step: int) -> bytes:
709 """ 731 """
710 Generates the OTP bytes for the given step. 732 Generates the OTP bytes for the given step.
711 733
@@ -733,6 +755,6 @@ class OTPGenerator:
733 digest = self.sha1_digest_folding(large_digest) 755 digest = self.sha1_digest_folding(large_digest)
734 else: 756 else:
735 raise OTPGeneratorException( 757 raise OTPGeneratorException(
736 '{hash_algo} is not supported by this module'.format( 758 f'{self._hash_algo} is not supported by this module'
737 hash_algo=self._hash_algo)) 759 )
738 return digest 760 return digest