summaryrefslogtreecommitdiff
path: root/otp2289/generator.py
diff options
context:
space:
mode:
Diffstat (limited to 'otp2289/generator.py')
-rw-r--r--otp2289/generator.py118
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)