diff options
| author | Simeon Simeonov | 2020-03-30 21:57:53 +0200 |
|---|---|---|
| committer | Simeon Simeonov | 2020-03-30 21:57:53 +0200 |
| commit | 2a6299c32e0baf3df06f8e6d6bc8695b951d630d (patch) | |
| tree | b4bd18092822db0fd10411e49dc6e05d1a60e697 /otp2289/generator.py | |
| parent | a4c050e34ce462f477bc3671e272e244e2792e00 (diff) | |
Implement OTPState in the server module
Diffstat (limited to 'otp2289/generator.py')
| -rw-r--r-- | otp2289/generator.py | 118 |
1 files changed, 95 insertions, 23 deletions
diff --git a/otp2289/generator.py b/otp2289/generator.py index 7001014..ac3bb3c 100644 --- a/otp2289/generator.py +++ b/otp2289/generator.py | |||
| @@ -320,19 +320,19 @@ class OTPGenerator: | |||
| 320 | :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5 | 320 | :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5 |
| 321 | :type hash_algo: int or str | 321 | :type hash_algo: int or str |
| 322 | 322 | ||
| 323 | :raises OTPGeneratorException: In case input does not validate | 323 | :raises OTPGeneratorException: In case the input does not validate |
| 324 | """ | 324 | """ |
| 325 | # enforce the rfc2289 constraints | 325 | # enforce the rfc2289 constraints |
| 326 | self._seed = seed | ||
| 327 | if self._seed: # the seed was set here. Validate it | ||
| 328 | self._seed = self.validate_seed(self._seed) | ||
| 329 | self._hash_algo = self.validate_hash_algo(hash_algo) | ||
| 326 | if not isinstance(password, bytes): | 330 | if not isinstance(password, bytes): |
| 327 | raise OTPGeneratorException('Password must be a byte-string') | 331 | raise OTPGeneratorException('Password must be a byte-string') |
| 328 | if len(password) < 10: | 332 | if len(password) < 10: |
| 329 | raise OTPGeneratorException( | 333 | raise OTPGeneratorException( |
| 330 | 'Password must be longer than 10 bytes') | 334 | 'Password must be longer than 10 bytes') |
| 331 | self._password = password | 335 | self._password = password |
| 332 | self._seed = seed | ||
| 333 | if self._seed: # the seed was set here. Validate it | ||
| 334 | self._seed = self.validate_seed(self._seed) | ||
| 335 | self._hash_algo = self.validate_hash_algo(hash_algo) | ||
| 336 | 336 | ||
| 337 | @staticmethod | 337 | @staticmethod |
| 338 | def bit_pair_sum(bit_stream): | 338 | def bit_pair_sum(bit_stream): |
| @@ -355,6 +355,31 @@ class OTPGenerator: | |||
| 355 | return value | 355 | return value |
| 356 | 356 | ||
| 357 | @staticmethod | 357 | @staticmethod |
| 358 | def bytes_to_tokens(hash_bytes): | ||
| 359 | """ | ||
| 360 | Returns a 6 words token from bytes as specified by RFC-2289. | ||
| 361 | |||
| 362 | :param hash_bytes: The input bytes | ||
| 363 | :type hash_bytes: bytes | ||
| 364 | |||
| 365 | :return: 6 words tokens | ||
| 366 | :rtype: str | ||
| 367 | """ | ||
| 368 | bit_stream = ''.join( | ||
| 369 | ['{0:0>8b}'.format(byte) for byte in hash_bytes]) | ||
| 370 | bit_pair_sum = OTPGenerator.bit_pair_sum(bit_stream) | ||
| 371 | tokens = [] | ||
| 372 | tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)]) | ||
| 373 | tokens.append(RFC1760_TOKENS[int(bit_stream[11:22], 2)]) | ||
| 374 | tokens.append(RFC1760_TOKENS[int(bit_stream[22:33], 2)]) | ||
| 375 | tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)]) | ||
| 376 | tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)]) | ||
| 377 | tokens.append( | ||
| 378 | RFC1760_TOKENS[int( | ||
| 379 | bit_stream[55:64] + '{0:0>8b}'.format(bit_pair_sum)[-2:], 2)]) | ||
| 380 | return ' '.join(tokens) | ||
| 381 | |||
| 382 | @staticmethod | ||
| 358 | def get_tokens_from_challenge(challenge): | 383 | def get_tokens_from_challenge(challenge): |
| 359 | """ | 384 | """ |
| 360 | Returns tokens (seed, hash_algo and step) from a challenge string. | 385 | Returns tokens (seed, hash_algo and step) from a challenge string. |
| @@ -453,6 +478,50 @@ class OTPGenerator: | |||
| 453 | ) | 478 | ) |
| 454 | 479 | ||
| 455 | @staticmethod | 480 | @staticmethod |
| 481 | def tokens_to_bytes(tokens_str): | ||
| 482 | """ | ||
| 483 | Returns bytes from a 6 words token as specified by RFC-2289. | ||
| 484 | |||
| 485 | :param tokens_str: String representing 6 words tokens | ||
| 486 | :type tokens_str: str | ||
| 487 | |||
| 488 | :raises OTPGeneratorException: When the tokens_str is invalid | ||
| 489 | |||
| 490 | :return: 6 words tokens | ||
| 491 | :rtype: bytes | ||
| 492 | """ | ||
| 493 | if not isinstance(tokens_str, str): | ||
| 494 | raise OTPGeneratorException('tokens must be a str') | ||
| 495 | tokens = tokens_str.split() | ||
| 496 | if len(tokens) != 6: | ||
| 497 | raise OTPGeneratorException( | ||
| 498 | 'Tokens-string does not contain 6 tokens') | ||
| 499 | token_ints = [] | ||
| 500 | try: | ||
| 501 | token_ints = [RFC1760_TOKENS.index(token.upper()) for token in | ||
| 502 | tokens] | ||
| 503 | except ValueError: | ||
| 504 | raise OTPGeneratorException( | ||
| 505 | 'One or more words not present in RFC1760') | ||
| 506 | # now we build a string of bits | ||
| 507 | bit_stream = format(token_ints[0], '011b') | ||
| 508 | bit_stream += format(token_ints[1], '011b') | ||
| 509 | bit_stream += format(token_ints[2], '011b') | ||
| 510 | bit_stream += format(token_ints[3], '011b') | ||
| 511 | bit_stream += format(token_ints[4], '011b') | ||
| 512 | bit_stream += format(token_ints[5], '011b') | ||
| 513 | # we have 66 bits: 64 digest + 2 bit pair sum (control number) | ||
| 514 | # RFC-2289: All OTP generators MUST calculate this checksum and all | ||
| 515 | # OTP servers MUST verify this checksum explicitly as part of the | ||
| 516 | # operation of decoding this representation of the one-time password. | ||
| 517 | if ( | ||
| 518 | '{0:0>8b}'.format(OTPGenerator.bit_pair_sum( | ||
| 519 | bit_stream[:64]))[-2:] != bit_stream[-2:] | ||
| 520 | ): | ||
| 521 | raise OTPGeneratorException('Invalid bit checksum') | ||
| 522 | return int(bit_stream[:64], 2).to_bytes(8, 'big') | ||
| 523 | |||
| 524 | @staticmethod | ||
| 456 | def validate_hash_algo(hash_algo): | 525 | def validate_hash_algo(hash_algo): |
| 457 | """ | 526 | """ |
| 458 | Validates the provided hash-algorithm. | 527 | Validates the provided hash-algorithm. |
| @@ -503,6 +572,25 @@ class OTPGenerator: | |||
| 503 | 'The seed MUST consist of purely alphanumeric characters') | 572 | 'The seed MUST consist of purely alphanumeric characters') |
| 504 | return seed | 573 | return seed |
| 505 | 574 | ||
| 575 | @staticmethod | ||
| 576 | def validate_step(step): | ||
| 577 | """ | ||
| 578 | Validates the provided step as defined by RFC-2289. | ||
| 579 | |||
| 580 | :param seed: The step received from the challenge | ||
| 581 | :type seed: int | ||
| 582 | |||
| 583 | :raises OTPGeneratorException: In case step does not validate | ||
| 584 | |||
| 585 | :return: The validated (and very same) step | ||
| 586 | :rtype: int | ||
| 587 | """ | ||
| 588 | if not isinstance(step, int): | ||
| 589 | raise OTPGeneratorException('Step value MUST be an int') | ||
| 590 | if step < 0: | ||
| 591 | raise OTPGeneratorException('Step value MUST be >= 0') | ||
| 592 | return step | ||
| 593 | |||
| 506 | def generate_otp_hexdigest(self, step): | 594 | def generate_otp_hexdigest(self, step): |
| 507 | """ | 595 | """ |
| 508 | Generates the OTP hexdigest for the given step. | 596 | Generates the OTP hexdigest for the given step. |
| @@ -549,20 +637,7 @@ class OTPGenerator: | |||
| 549 | :return: Six words (separated by single space) token for the given step | 637 | :return: Six words (separated by single space) token for the given step |
| 550 | :rtype: str | 638 | :rtype: str |
| 551 | """ | 639 | """ |
| 552 | digest = self._generate_otp_bytes(step) | 640 | return self.bytes_to_tokens(self._generate_otp_bytes(step)) |
| 553 | bit_stream = ''.join( | ||
| 554 | ['{0:0>8b}'.format(byte) for byte in digest]) | ||
| 555 | bit_pair_sum = self.bit_pair_sum(bit_stream) | ||
| 556 | tokens = list() | ||
| 557 | tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)]) | ||
| 558 | tokens.append(RFC1760_TOKENS[int(bit_stream[11:22], 2)]) | ||
| 559 | tokens.append(RFC1760_TOKENS[int(bit_stream[22:33], 2)]) | ||
| 560 | tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)]) | ||
| 561 | tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)]) | ||
| 562 | tokens.append( | ||
| 563 | RFC1760_TOKENS[int( | ||
| 564 | bit_stream[55:64] + '{0:0>8b}'.format(bit_pair_sum)[-2:], 2)]) | ||
| 565 | return ' '.join(tokens) | ||
| 566 | 641 | ||
| 567 | def generate_otp_words_from_challenge(self, challenge): | 642 | def generate_otp_words_from_challenge(self, challenge): |
| 568 | """ | 643 | """ |
| @@ -641,10 +716,7 @@ class OTPGenerator: | |||
| 641 | :return: The digest bytes for the given step | 716 | :return: The digest bytes for the given step |
| 642 | :rtype: bytes | 717 | :rtype: bytes |
| 643 | """ | 718 | """ |
| 644 | if not isinstance(step, int): | 719 | step = self.validate_step(step) |
| 645 | raise OTPGeneratorException('Step value MUST be an int') | ||
| 646 | if step < 0: | ||
| 647 | raise OTPGeneratorException('Step value MUST be >= 0') | ||
| 648 | digest = b'' | 720 | digest = b'' |
| 649 | for _ in range(step + 1): | 721 | for _ in range(step + 1): |
| 650 | hash_obj = hashlib.new(self._hash_algo) | 722 | hash_obj = hashlib.new(self._hash_algo) |
