diff options
Diffstat (limited to 'otp2289')
| -rw-r--r-- | otp2289/__init__.py | 2 | ||||
| -rw-r--r-- | otp2289/generator.py | 192 | ||||
| -rw-r--r-- | otp2289/server.py | 4 |
3 files changed, 156 insertions, 42 deletions
diff --git a/otp2289/__init__.py b/otp2289/__init__.py index d2f6386..82c1413 100644 --- a/otp2289/__init__.py +++ b/otp2289/__init__.py | |||
| @@ -3,6 +3,7 @@ | |||
| 3 | 3 | ||
| 4 | from .generator import (OTP_ALGO_MD5, | 4 | from .generator import (OTP_ALGO_MD5, |
| 5 | OTP_ALGO_SHA1, | 5 | OTP_ALGO_SHA1, |
| 6 | OTPChallengeException, | ||
| 6 | OTPGenerator, | 7 | OTPGenerator, |
| 7 | OTPGeneratorException) | 8 | OTPGeneratorException) |
| 8 | 9 | ||
| @@ -24,5 +25,6 @@ VERSION = tuple(map(int_or_str, __version__.split('.'))) | |||
| 24 | 25 | ||
| 25 | __all__ = ['OTP_ALGO_MD5', | 26 | __all__ = ['OTP_ALGO_MD5', |
| 26 | 'OTP_ALGO_SHA1', | 27 | 'OTP_ALGO_SHA1', |
| 28 | 'OTPChallengeException', | ||
| 27 | 'OTPGenerator', | 29 | 'OTPGenerator', |
| 28 | 'OTPGeneratorException'] | 30 | 'OTPGeneratorException'] |
diff --git a/otp2289/generator.py b/otp2289/generator.py index b007d87..020d154 100644 --- a/otp2289/generator.py +++ b/otp2289/generator.py | |||
| @@ -1,5 +1,4 @@ | |||
| 1 | # -*- coding: utf-8 -*- | 1 | # -*- coding: utf-8 -*- |
| 2 | |||
| 3 | """ | 2 | """ |
| 4 | A pure Python implementation of the RFC-2289 OTP generator | 3 | A pure Python implementation of the RFC-2289 OTP generator |
| 5 | """ | 4 | """ |
| @@ -279,23 +278,28 @@ class OTPGeneratorException(Exception): | |||
| 279 | """OTPGeneratorException class""" | 278 | """OTPGeneratorException class""" |
| 280 | 279 | ||
| 281 | 280 | ||
| 281 | class OTPChallengeException(Exception): | ||
| 282 | """OTPChallengeException class""" | ||
| 283 | |||
| 284 | |||
| 282 | class OTPGenerator: | 285 | class OTPGenerator: |
| 283 | """OTPGenerator class""" | 286 | """OTPGenerator class""" |
| 284 | 287 | ||
| 285 | def __init__(self, password, seed, hash_algo=OTP_ALGO_MD5): | 288 | def __init__(self, password, seed='', hash_algo=OTP_ALGO_MD5): |
| 286 | """ | 289 | """ |
| 287 | Constructs an OTPGenerator object with a given password and seed | 290 | Constructs an OTPGenerator object with a given password and seed. |
| 288 | 291 | ||
| 289 | Keyword Arguments: | 292 | Keyword Arguments: |
| 290 | :param password: the password string | 293 | :param password: The password string |
| 291 | :type password: bytes | 294 | :type password: bytes |
| 292 | 295 | ||
| 293 | :param seed: the seed received from the server-challenge | 296 | :param seed: The seed received from the challenge, defaults to '' |
| 294 | :type seed: str | 297 | :type seed: str |
| 295 | 298 | ||
| 296 | :param hash_algo: the hash algo. | 299 | :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5 |
| 297 | :type hash_algo: int or str | 300 | :type hash_algo: int or str |
| 298 | (default OTP_ALGO_MD5) | 301 | |
| 302 | :raises OTPGeneratorException: In case input does not validate | ||
| 299 | """ | 303 | """ |
| 300 | # enforce the rfc2289 constraints | 304 | # enforce the rfc2289 constraints |
| 301 | if not isinstance(password, bytes): | 305 | if not isinstance(password, bytes): |
| @@ -304,37 +308,20 @@ class OTPGenerator: | |||
| 304 | raise OTPGeneratorException( | 308 | raise OTPGeneratorException( |
| 305 | 'Password must be longer than 10 bytes') | 309 | 'Password must be longer than 10 bytes') |
| 306 | self._password = password | 310 | self._password = password |
| 307 | if not isinstance(seed, str): | ||
| 308 | raise OTPGeneratorException('Seed must be a string') | ||
| 309 | if not seed or len(seed) > 16: | ||
| 310 | raise OTPGeneratorException( | ||
| 311 | 'The seed MUST be of 1 to 16 characters in length') | ||
| 312 | for char in seed: | ||
| 313 | if char not in string.ascii_letters + string.digits: | ||
| 314 | raise OTPGeneratorException( | ||
| 315 | 'The seed MUST consist of purely alphanumeric characters') | ||
| 316 | self._seed = seed | 311 | self._seed = seed |
| 317 | if isinstance(hash_algo, int): | 312 | if self._seed: # the seed was set here. Validate it |
| 318 | self._hash_algo = _ALGO_DICT.get(hash_algo, 'md5') | 313 | self._seed = self.validate_seed(self._seed) |
| 319 | elif isinstance(hash_algo, str): | 314 | self._hash_algo = self.validate_hash_algo(hash_algo) |
| 320 | self._hash_algo = hash_algo | ||
| 321 | else: | ||
| 322 | raise OTPGeneratorException( | ||
| 323 | 'hash_algo must be an int or a str') | ||
| 324 | if self._hash_algo not in hashlib.algorithms_available: | ||
| 325 | raise OTPGeneratorException( | ||
| 326 | '{hash_algo} is not supported by this version of the ' | ||
| 327 | 'hashlib module'.format(hash_algo=self._hash_algo)) | ||
| 328 | 315 | ||
| 329 | @staticmethod | 316 | @staticmethod |
| 330 | def bit_pair_sum(bit_stream): | 317 | def bit_pair_sum(bit_stream): |
| 331 | """ | 318 | """ |
| 332 | Split bit_stream in bit-pairs and sum them all together | 319 | Split bit_stream in bit-pairs and sum them all together. |
| 333 | 320 | ||
| 334 | :param bit_stream: The bit-stream object | 321 | :param bit_stream: The bit-stream object |
| 335 | :type bit_stream: str | 322 | :type bit_stream: str |
| 336 | 323 | ||
| 337 | :return: the sum of all bit-pairs in bit_stream | 324 | :return: The sum of all bit-pairs in bit_stream |
| 338 | :rtype: int | 325 | :rtype: int |
| 339 | """ | 326 | """ |
| 340 | if not isinstance(bit_stream, str): | 327 | if not isinstance(bit_stream, str): |
| @@ -347,15 +334,41 @@ class OTPGenerator: | |||
| 347 | return value | 334 | return value |
| 348 | 335 | ||
| 349 | @staticmethod | 336 | @staticmethod |
| 337 | def get_tokens_from_challenge(challenge): | ||
| 338 | """ | ||
| 339 | Returns tokens (seed, hash_algo and step) from a challenge string. | ||
| 340 | |||
| 341 | N.B. The tokens are not validated here. | ||
| 342 | |||
| 343 | :param challenge: The challenge string described in RFC-2289 | ||
| 344 | :type challenge: str | ||
| 345 | |||
| 346 | :raises OTPChallengeException: When the challenge string is invalid | ||
| 347 | |||
| 348 | :return: (seed, hash_algo, step) tuple. | ||
| 349 | :rtype: tuple | ||
| 350 | """ | ||
| 351 | if not isinstance(challenge, str): | ||
| 352 | raise OTPChallengeException('Challenge must be str') | ||
| 353 | challenge = challenge.strip() | ||
| 354 | if not challenge.startswith('otp-'): | ||
| 355 | raise OTPChallengeException('Invalid challenge') | ||
| 356 | try: | ||
| 357 | hash_algo, step, seed = challenge[4:].split() | ||
| 358 | return (seed, hash_algo, int(step)) | ||
| 359 | except ValueError: | ||
| 360 | raise OTPChallengeException('Invalid challenge') | ||
| 361 | |||
| 362 | @staticmethod | ||
| 350 | def sha1_digest_folding(sha1_digest): | 363 | def sha1_digest_folding(sha1_digest): |
| 351 | """ | 364 | """ |
| 352 | Implementation of the 160bit -> 64bit folding algorithm | 365 | Implementation of the 160bit -> 64bit folding algorithm |
| 353 | for sha1 digest | 366 | for sha1 digest. |
| 354 | 367 | ||
| 355 | :param sha1_digest: The SHA1 digest | 368 | :param sha1_digest: The SHA1 digest |
| 356 | :type sha1_digest: bytes | 369 | :type sha1_digest: bytes |
| 357 | 370 | ||
| 358 | :return: the byte-string representing the folded sha1-digest | 371 | :return: The byte-string representing the folded sha1-digest |
| 359 | :rtype: bytes | 372 | :rtype: bytes |
| 360 | """ | 373 | """ |
| 361 | if not (isinstance(sha1_digest, bytes)): | 374 | if not (isinstance(sha1_digest, bytes)): |
| @@ -396,7 +409,7 @@ class OTPGenerator: | |||
| 396 | @staticmethod | 409 | @staticmethod |
| 397 | def strxor(byte_str1, byte_str2): | 410 | def strxor(byte_str1, byte_str2): |
| 398 | """ | 411 | """ |
| 399 | Implementation of strxor similar to the one provided by pycrypto | 412 | Implementation of strxor similar to the one provided by pycrypto. |
| 400 | 413 | ||
| 401 | :param byte_str1: Byte-string 1 | 414 | :param byte_str1: Byte-string 1 |
| 402 | :type byte_str1: bytes | 415 | :type byte_str1: bytes |
| @@ -404,7 +417,7 @@ class OTPGenerator: | |||
| 404 | :param byte_str2: Byte-string 2 | 417 | :param byte_str2: Byte-string 2 |
| 405 | :type byte_str2: bytes | 418 | :type byte_str2: bytes |
| 406 | 419 | ||
| 407 | :return: the byte-string representing the result of byte_str1^byte_str2 | 420 | :return: The byte-string representing the result of byte_str1^byte_str2 |
| 408 | :rtype: bytes | 421 | :rtype: bytes |
| 409 | """ | 422 | """ |
| 410 | if not (isinstance(byte_str1, bytes) and isinstance(byte_str2, bytes)): | 423 | if not (isinstance(byte_str1, bytes) and isinstance(byte_str2, bytes)): |
| @@ -418,28 +431,101 @@ class OTPGenerator: | |||
| 418 | [byte_str1[i] ^ byte_str2[i] for i in range(length)] | 431 | [byte_str1[i] ^ byte_str2[i] for i in range(length)] |
| 419 | ) | 432 | ) |
| 420 | 433 | ||
| 434 | @staticmethod | ||
| 435 | def validate_hash_algo(hash_algo): | ||
| 436 | """ | ||
| 437 | Validates the provided hash-algorithm. | ||
| 438 | |||
| 439 | :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5 | ||
| 440 | :type hash_algo: int or str | ||
| 441 | |||
| 442 | :raises OTPGeneratorException: In case hash_algo does not validate | ||
| 443 | |||
| 444 | :return: The validated hash_algo in str-form | ||
| 445 | :rtype: str | ||
| 446 | """ | ||
| 447 | if isinstance(hash_algo, int): | ||
| 448 | if hash_algo not in _ALGO_DICT: | ||
| 449 | raise OTPGeneratorException( | ||
| 450 | 'hash_algo is not among the known algorithms') | ||
| 451 | hash_algo = _ALGO_DICT.get(hash_algo) | ||
| 452 | if not isinstance(hash_algo, str): | ||
| 453 | raise OTPGeneratorException( | ||
| 454 | 'hash_algo must be an int or a str') | ||
| 455 | if hash_algo not in hashlib.algorithms_available: | ||
| 456 | raise OTPGeneratorException( | ||
| 457 | f'{hash_algo} is not supported by this version of the ' | ||
| 458 | 'hashlib module') | ||
| 459 | return hash_algo | ||
| 460 | |||
| 461 | @staticmethod | ||
| 462 | def validate_seed(seed): | ||
| 463 | """ | ||
| 464 | Validates the provided seed as defined by RFC-2289. | ||
| 465 | |||
| 466 | :param seed: The seed received from the challenge, defaults to '' | ||
| 467 | :type seed: str | ||
| 468 | |||
| 469 | :raises OTPGeneratorException: In case seed does not validate | ||
| 470 | |||
| 471 | :return: The validated (and very same) seed | ||
| 472 | :rtype: str | ||
| 473 | """ | ||
| 474 | if not isinstance(seed, str): | ||
| 475 | raise OTPGeneratorException('Seed must be a string') | ||
| 476 | if not seed or len(seed) > 16: | ||
| 477 | raise OTPGeneratorException( | ||
| 478 | 'The seed MUST be of 1 to 16 characters in length') | ||
| 479 | for char in seed: | ||
| 480 | if char not in string.ascii_letters + string.digits: | ||
| 481 | raise OTPGeneratorException( | ||
| 482 | 'The seed MUST consist of purely alphanumeric characters') | ||
| 483 | return seed | ||
| 484 | |||
| 421 | def generate_otp_hexdigest(self, step): | 485 | def generate_otp_hexdigest(self, step): |
| 422 | """ | 486 | """ |
| 423 | Generates the OTP hexdigest for the given step | 487 | Generates the OTP hexdigest for the given step. |
| 424 | 488 | ||
| 425 | Keyword Arguments: | 489 | Keyword Arguments: |
| 426 | :param step: the step to generate OTP for | 490 | :param step: The step to generate OTP for |
| 427 | :type step: int | 491 | :type step: int |
| 428 | 492 | ||
| 429 | :return: hexdigest for the given step | 493 | :return: Hexdigest for the given step |
| 430 | :rtype: str | 494 | :rtype: str |
| 431 | """ | 495 | """ |
| 432 | return '0x' + binascii.hexlify(self._generate_otp_bytes(step)).decode() | 496 | return '0x' + binascii.hexlify(self._generate_otp_bytes(step)).decode() |
| 433 | 497 | ||
| 498 | def generate_otp_hexdigest_from_challenge(self, challenge): | ||
| 499 | """ | ||
| 500 | Same as generate_otp_hexdigest, but it generates hex. from a challenge. | ||
| 501 | |||
| 502 | RFC-2289 states: | ||
| 503 | The challenge MUST be in a standard syntax so | ||
| 504 | that automated generators can recognize the challenge in context and | ||
| 505 | extract these parameters. The syntax of the challenge is: | ||
| 506 | otp-<algorithm identifier> <sequence integer> <seed> | ||
| 507 | |||
| 508 | Keyword Arguments: | ||
| 509 | :param challenge: The challenge string | ||
| 510 | :type challenge: str | ||
| 511 | |||
| 512 | :return: Hexdigest for the given challenge | ||
| 513 | :rtype: str | ||
| 514 | """ | ||
| 515 | seed, hash_algo, step = self.get_tokens_from_challenge(challenge) | ||
| 516 | self._seed = self.validate_seed(seed) | ||
| 517 | self._hash_algo = self.validate_hash_algo(hash_algo) | ||
| 518 | return self.generate_otp_hexdigest(step) | ||
| 519 | |||
| 434 | def generate_otp_words(self, step): | 520 | def generate_otp_words(self, step): |
| 435 | """ | 521 | """ |
| 436 | Generates the OTP six words token for the given step | 522 | Generates the OTP six words token for the given step. |
| 437 | 523 | ||
| 438 | Keyword Arguments: | 524 | Keyword Arguments: |
| 439 | :param step: the step to generate OTP for | 525 | :param step: The step to generate OTP for |
| 440 | :type step: int | 526 | :type step: int |
| 441 | 527 | ||
| 442 | :return: six words (separated by single space) token for the given step | 528 | :return: Six words (separated by single space) token for the given step |
| 443 | :rtype: str | 529 | :rtype: str |
| 444 | """ | 530 | """ |
| 445 | digest = self._generate_otp_bytes(step) | 531 | digest = self._generate_otp_bytes(step) |
| @@ -457,6 +543,28 @@ class OTPGenerator: | |||
| 457 | bit_stream[55:64] + '{0:0>8b}'.format(bit_pair_sum)[-2:], 2)]) | 543 | bit_stream[55:64] + '{0:0>8b}'.format(bit_pair_sum)[-2:], 2)]) |
| 458 | return ' '.join(tokens) | 544 | return ' '.join(tokens) |
| 459 | 545 | ||
| 546 | def generate_otp_words_from_challenge(self, challenge): | ||
| 547 | """ | ||
| 548 | Same as generate_otp_words, but it generates words from a challenge. | ||
| 549 | |||
| 550 | RFC-2289 states: | ||
| 551 | The challenge MUST be in a standard syntax so | ||
| 552 | that automated generators can recognize the challenge in context and | ||
| 553 | extract these parameters. The syntax of the challenge is: | ||
| 554 | otp-<algorithm identifier> <sequence integer> <seed> | ||
| 555 | |||
| 556 | Keyword Arguments: | ||
| 557 | :param challenge: The challenge string | ||
| 558 | :type challenge: str | ||
| 559 | |||
| 560 | :return: Six words token for the given challenge | ||
| 561 | :rtype: str | ||
| 562 | """ | ||
| 563 | seed, hash_algo, step = self.get_tokens_from_challenge(challenge) | ||
| 564 | self._seed = self.validate_seed(seed) | ||
| 565 | self._hash_algo = self.validate_hash_algo(hash_algo) | ||
| 566 | return self.generate_otp_words(step) | ||
| 567 | |||
| 460 | def hexdigest_range(self, start=499, stop=0): | 568 | def hexdigest_range(self, start=499, stop=0): |
| 461 | """ | 569 | """ |
| 462 | Returns an iterator that providing hexdigests corresponding to steps | 570 | Returns an iterator that providing hexdigests corresponding to steps |
| @@ -503,13 +611,13 @@ class OTPGenerator: | |||
| 503 | 611 | ||
| 504 | def _generate_otp_bytes(self, step): | 612 | def _generate_otp_bytes(self, step): |
| 505 | """ | 613 | """ |
| 506 | Generates the OTP bytes for the given step | 614 | Generates the OTP bytes for the given step. |
| 507 | 615 | ||
| 508 | Keyword Arguments: | 616 | Keyword Arguments: |
| 509 | :param step: the step to generate OTP for | 617 | :param step: The step to generate OTP for |
| 510 | :type step: int | 618 | :type step: int |
| 511 | 619 | ||
| 512 | :return: the digest bytes for the given step | 620 | :return: The digest bytes for the given step |
| 513 | :rtype: bytes | 621 | :rtype: bytes |
| 514 | """ | 622 | """ |
| 515 | if not isinstance(step, int): | 623 | if not isinstance(step, int): |
diff --git a/otp2289/server.py b/otp2289/server.py new file mode 100644 index 0000000..86c4fa5 --- /dev/null +++ b/otp2289/server.py | |||
| @@ -0,0 +1,4 @@ | |||
| 1 | # -*- coding: utf-8 -*- | ||
| 2 | """ | ||
| 3 | A pure Python implementation of the RFC-2289 OTP server | ||
| 4 | """ | ||
