diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/otp2289/__init__.py | 4 | ||||
| -rw-r--r-- | src/otp2289/__main__.py | 153 | ||||
| -rw-r--r-- | src/otp2289/generator.py | 314 | ||||
| -rw-r--r-- | src/otp2289/server.py | 104 |
4 files changed, 311 insertions, 264 deletions
diff --git a/src/otp2289/__init__.py b/src/otp2289/__init__.py index 8f8cf24..da65d9e 100644 --- a/src/otp2289/__init__.py +++ b/src/otp2289/__init__.py | |||
| @@ -31,6 +31,7 @@ from .generator import ( | |||
| 31 | OTPGenerator, | 31 | OTPGenerator, |
| 32 | OTPGeneratorError, | 32 | OTPGeneratorError, |
| 33 | OTPResponse, | 33 | OTPResponse, |
| 34 | OTPResponseError, | ||
| 34 | ) | 35 | ) |
| 35 | from .server import ( | 36 | from .server import ( |
| 36 | OTPInvalidResponseError, | 37 | OTPInvalidResponseError, |
| @@ -41,7 +42,7 @@ from .server import ( | |||
| 41 | ) | 42 | ) |
| 42 | 43 | ||
| 43 | __author__ = 'Simeon Simeonov' | 44 | __author__ = 'Simeon Simeonov' |
| 44 | __version__ = '2.0.0a' | 45 | __version__ = '2.0.0' |
| 45 | __license__ = 'BSD 2-Clause' | 46 | __license__ = 'BSD 2-Clause' |
| 46 | 47 | ||
| 47 | 48 | ||
| @@ -63,6 +64,7 @@ __all__ = [ | |||
| 63 | 'OTPGeneratorError', | 64 | 'OTPGeneratorError', |
| 64 | 'OTPInvalidResponseError', | 65 | 'OTPInvalidResponseError', |
| 65 | 'OTPResponse', | 66 | 'OTPResponse', |
| 67 | 'OTPResponseError', | ||
| 66 | 'OTPState', | 68 | 'OTPState', |
| 67 | 'OTPStateError', | 69 | 'OTPStateError', |
| 68 | 'OTPStore', | 70 | 'OTPStore', |
diff --git a/src/otp2289/__main__.py b/src/otp2289/__main__.py index 0fdcc52..c5d28b6 100644 --- a/src/otp2289/__main__.py +++ b/src/otp2289/__main__.py | |||
| @@ -51,6 +51,56 @@ def eprint( | |||
| 51 | print(*value, sep=sep, end=end, file=sys.stderr, flush=True) | 51 | print(*value, sep=sep, end=end, file=sys.stderr, flush=True) |
| 52 | 52 | ||
| 53 | 53 | ||
| 54 | def get_rnd_seed() -> str: | ||
| 55 | """ | ||
| 56 | Returns a random seed in the format: | ||
| 57 | |||
| 58 | 2 random letters (capitalize()) + 5 random digits | ||
| 59 | """ | ||
| 60 | rnd = secrets.SystemRandom() | ||
| 61 | return ''.join( | ||
| 62 | rnd.choices(string.ascii_lowercase, k=2) | ||
| 63 | ).capitalize() + ''.join(rnd.choices(string.digits, k=5)) | ||
| 64 | |||
| 65 | |||
| 66 | def initiate_new_sequence(args: argparse.Namespace) -> str: | ||
| 67 | """ | ||
| 68 | Generates a new sequence based on the parameters sent from the parser. | ||
| 69 | |||
| 70 | :param args: The arguments assigned from argparse | ||
| 71 | :type args: argparse.Namespace | ||
| 72 | |||
| 73 | :raises otp2289.OTPChallengeError: If the challenge is invalid | ||
| 74 | |||
| 75 | :raises otp2289.OTPGeneratorError: If generator parameters are wrong | ||
| 76 | |||
| 77 | :raises otp2289.OTPResponseError: If tokens or hex can not be generated | ||
| 78 | |||
| 79 | :return: The response string | ||
| 80 | :rtype: str | ||
| 81 | """ | ||
| 82 | if not args.seed: | ||
| 83 | args.seed = get_rnd_seed() | ||
| 84 | |||
| 85 | header = '' | ||
| 86 | if not args.quiet: | ||
| 87 | header = ( | ||
| 88 | f'Seed: {args.seed}, Step: {args.step}, ' | ||
| 89 | f'Hash: {args.hash_algo}{os.linesep}' | ||
| 90 | ) | ||
| 91 | generator = otp2289.OTPGenerator( | ||
| 92 | args.password.encode(), args.seed, args.hash_algo | ||
| 93 | ) | ||
| 94 | if args.challenge_string: | ||
| 95 | return ( | ||
| 96 | header | ||
| 97 | + generator.generate_otp_response_from_challenge( | ||
| 98 | args.challenge_string | ||
| 99 | ).hexdigest | ||
| 100 | ) | ||
| 101 | return header + generator.generate_otp_response(args.step).hexdigest | ||
| 102 | |||
| 103 | |||
| 54 | def generate_otp_response(args: argparse.Namespace) -> str: | 104 | def generate_otp_response(args: argparse.Namespace) -> str: |
| 55 | """ | 105 | """ |
| 56 | Generates a response based on the parameters sent from the parser | 106 | Generates a response based on the parameters sent from the parser |
| @@ -62,20 +112,22 @@ def generate_otp_response(args: argparse.Namespace) -> str: | |||
| 62 | 112 | ||
| 63 | :raises otp2289.OTPGeneratorError: If generator parameters are wrong | 113 | :raises otp2289.OTPGeneratorError: If generator parameters are wrong |
| 64 | 114 | ||
| 115 | :raises otp2289.OTPResponseError: If tokens or hex can not be generated | ||
| 116 | |||
| 65 | :return: The response string | 117 | :return: The response string |
| 66 | :rtype: str | 118 | :rtype: str |
| 67 | """ | 119 | """ |
| 68 | generator = otp2289.generator.OTPGenerator( | 120 | generator = otp2289.OTPGenerator( |
| 69 | args.password.encode(), args.seed, args.hash_algo | 121 | args.password.encode(), args.seed, args.hash_algo |
| 70 | ) | 122 | ) |
| 71 | if args.challenge_string: | 123 | if args.challenge_string: |
| 72 | if args.output_format == 'token': | 124 | response = generator.generate_otp_response_from_challenge( |
| 73 | return generator.generate_otp_words_from_challenge( | 125 | args.challange_string |
| 74 | args.challenge_string | ||
| 75 | ) | ||
| 76 | return generator.generate_otp_hexdigest_from_challenge( | ||
| 77 | args.challenge_string | ||
| 78 | ) | 126 | ) |
| 127 | if args.output_format == 'token': | ||
| 128 | return response.words | ||
| 129 | return response.hexdigest | ||
| 130 | |||
| 79 | # regular parameters | 131 | # regular parameters |
| 80 | header = '' | 132 | header = '' |
| 81 | if not args.quiet: | 133 | if not args.quiet: |
| @@ -83,9 +135,12 @@ def generate_otp_response(args: argparse.Namespace) -> str: | |||
| 83 | f'Seed: {args.seed}, Step: {args.step}, ' | 135 | f'Seed: {args.seed}, Step: {args.step}, ' |
| 84 | f'Hash: {args.hash_algo}{os.linesep}' | 136 | f'Hash: {args.hash_algo}{os.linesep}' |
| 85 | ) | 137 | ) |
| 138 | |||
| 139 | response = generator.generate_otp_response(args.step) | ||
| 140 | |||
| 86 | if args.output_format == 'token': | 141 | if args.output_format == 'token': |
| 87 | return header + generator.generate_otp_words(args.step) | 142 | return header + response.words |
| 88 | return header + generator.generate_otp_hexdigest(args.step) | 143 | return header + response.hexdigest |
| 89 | 144 | ||
| 90 | 145 | ||
| 91 | def generate_otp_range(args: argparse.Namespace) -> str: | 146 | def generate_otp_range(args: argparse.Namespace) -> str: |
| @@ -99,20 +154,24 @@ def generate_otp_range(args: argparse.Namespace) -> str: | |||
| 99 | 154 | ||
| 100 | :raises otp2289.OTPGeneratorError: If generator parameters are wrong | 155 | :raises otp2289.OTPGeneratorError: If generator parameters are wrong |
| 101 | 156 | ||
| 157 | :raises otp2289.OTPResponseError: If tokens or hex can not be generated | ||
| 158 | |||
| 102 | :return: The responses string | 159 | :return: The responses string |
| 103 | :rtype: str | 160 | :rtype: str |
| 104 | """ | 161 | """ |
| 105 | generator = otp2289.generator.OTPGenerator( | 162 | generator = otp2289.OTPGenerator( |
| 106 | args.password.encode(), args.seed, args.hash_algo | 163 | args.password.encode(), args.seed, args.hash_algo |
| 107 | ) | 164 | ) |
| 108 | if args.output_format == 'token': | ||
| 109 | method = generator.generate_otp_words | ||
| 110 | else: | ||
| 111 | method = generator.generate_otp_hexdigest | ||
| 112 | 165 | ||
| 113 | # handle most cases explicitly | 166 | # handle most cases explicitly |
| 114 | if args.range == 1: | 167 | if args.range == 1: |
| 115 | return f'{args.step}: ' + method(args.step) | 168 | response = generator.generate_otp_response(args.step) |
| 169 | if args.output_format == 'token': | ||
| 170 | response_str = response.words | ||
| 171 | else: | ||
| 172 | response_str = response.hexdigest | ||
| 173 | return f'{args.step}: ' + response_str | ||
| 174 | |||
| 116 | args.range = min(args.range, args.step + 1) | 175 | args.range = min(args.range, args.step + 1) |
| 117 | 176 | ||
| 118 | # any need for quiet? | 177 | # any need for quiet? |
| @@ -122,9 +181,19 @@ def generate_otp_range(args: argparse.Namespace) -> str: | |||
| 122 | f'Seed: {args.seed}, Step: {args.step}, ' | 181 | f'Seed: {args.seed}, Step: {args.step}, ' |
| 123 | f'Hash: {args.hash_algo}, Range: {args.range}{os.linesep}' | 182 | f'Hash: {args.hash_algo}, Range: {args.range}{os.linesep}' |
| 124 | ) | 183 | ) |
| 184 | |||
| 185 | if args.output_format == 'token': | ||
| 186 | return header + os.linesep.join( | ||
| 187 | [ | ||
| 188 | f'{step}: ' + generator.generate_otp_response(step).words | ||
| 189 | for step in range(args.step, args.step - args.range, -1) | ||
| 190 | ] | ||
| 191 | ) | ||
| 192 | |||
| 193 | # assume hex | ||
| 125 | return header + os.linesep.join( | 194 | return header + os.linesep.join( |
| 126 | [ | 195 | [ |
| 127 | f'{step}: ' + method(step) | 196 | f'{step}: ' + generator.generate_otp_response(step).hexdigest |
| 128 | for step in range(args.step, args.step - args.range, -1) | 197 | for step in range(args.step, args.step - args.range, -1) |
| 129 | ] | 198 | ] |
| 130 | ) | 199 | ) |
| @@ -172,50 +241,6 @@ def get_password(args: argparse.Namespace) -> str: | |||
| 172 | return args.password | 241 | return args.password |
| 173 | 242 | ||
| 174 | 243 | ||
| 175 | def get_rnd_seed() -> str: | ||
| 176 | """ | ||
| 177 | Returns a random seed in the format: | ||
| 178 | |||
| 179 | 2 random letters (capitalize()) + 5 random digits | ||
| 180 | """ | ||
| 181 | rnd = secrets.SystemRandom() | ||
| 182 | return ''.join( | ||
| 183 | rnd.choices(string.ascii_lowercase, k=2) | ||
| 184 | ).capitalize() + ''.join(rnd.choices(string.digits, k=5)) | ||
| 185 | |||
| 186 | |||
| 187 | def initiate_new_sequence(args: argparse.Namespace) -> str: | ||
| 188 | """ | ||
| 189 | Generates a new sequence based on the parameters sent from the parser. | ||
| 190 | |||
| 191 | :param args: The arguments assigned from argparse | ||
| 192 | :type args: argparse.Namespace | ||
| 193 | |||
| 194 | :raises otp2289.OTPChallengeError: If the challenge is invalid | ||
| 195 | |||
| 196 | :raises otp2289.OTPGeneratorError: If generator parameters are wrong | ||
| 197 | |||
| 198 | :return: The response string | ||
| 199 | :rtype: str | ||
| 200 | """ | ||
| 201 | if not args.seed: | ||
| 202 | args.seed = get_rnd_seed() | ||
| 203 | header = '' | ||
| 204 | if not args.quiet: | ||
| 205 | header = ( | ||
| 206 | f'Seed: {args.seed}, Step: {args.step}, ' | ||
| 207 | f'Hash: {args.hash_algo}{os.linesep}' | ||
| 208 | ) | ||
| 209 | generator = otp2289.generator.OTPGenerator( | ||
| 210 | args.password.encode(), args.seed, args.hash_algo | ||
| 211 | ) | ||
| 212 | if args.challenge_string: | ||
| 213 | return header + generator.generate_otp_hexdigest_from_challenge( | ||
| 214 | args.challenge_string | ||
| 215 | ) | ||
| 216 | return header + generator.generate_otp_hexdigest(args.step) | ||
| 217 | |||
| 218 | |||
| 219 | def main(inargs: list[str] | None = None) -> None: | 244 | def main(inargs: list[str] | None = None) -> None: |
| 220 | """the main entry point""" | 245 | """the main entry point""" |
| 221 | parser = argparse.ArgumentParser( | 246 | parser = argparse.ArgumentParser( |
| @@ -278,8 +303,8 @@ def main(inargs: list[str] | None = None) -> None: | |||
| 278 | parser.add_argument( | 303 | parser.add_argument( |
| 279 | '-a', | 304 | '-a', |
| 280 | '--hash-algorithm', | 305 | '--hash-algorithm', |
| 281 | metavar='<md5 | sha1>', | ||
| 282 | type=str, | 306 | type=str, |
| 307 | choices=['md5', 'sha1'], | ||
| 283 | dest='hash_algo', | 308 | dest='hash_algo', |
| 284 | default='md5', | 309 | default='md5', |
| 285 | help='The hash algorithm to use. Possible values: md5 (default), sha1', | 310 | help='The hash algorithm to use. Possible values: md5 (default), sha1', |
| @@ -296,8 +321,8 @@ def main(inargs: list[str] | None = None) -> None: | |||
| 296 | parser.add_argument( | 321 | parser.add_argument( |
| 297 | '-f', | 322 | '-f', |
| 298 | '--output-format', | 323 | '--output-format', |
| 299 | metavar='<hex | token>', | ||
| 300 | type=str, | 324 | type=str, |
| 325 | choices=['hex', 'token'], | ||
| 301 | dest='output_format', | 326 | dest='output_format', |
| 302 | default='hex', | 327 | default='hex', |
| 303 | help='The output format to use. Possible values: hex (default), token', | 328 | help='The output format to use. Possible values: hex (default), token', |
| @@ -368,9 +393,9 @@ def main(inargs: list[str] | None = None) -> None: | |||
| 368 | if args.generate_otp_response: | 393 | if args.generate_otp_response: |
| 369 | print(generate_otp_response(args)) | 394 | print(generate_otp_response(args)) |
| 370 | sys.exit(0) | 395 | sys.exit(0) |
| 371 | except otp2289.generator.OTPGeneratorError as exp: | 396 | except otp2289.OTPGeneratorError as exp: |
| 372 | eprint(f'GeneratorException: {exp}') | 397 | eprint(f'GeneratorException: {exp}') |
| 373 | except otp2289.generator.OTPChallengeError as exp: | 398 | except otp2289.OTPChallengeError as exp: |
| 374 | eprint(f'ChallengeException: {exp}') | 399 | eprint(f'ChallengeException: {exp}') |
| 375 | except Exception as exp: | 400 | except Exception as exp: |
| 376 | eprint(f'Unknown error: {exp}') | 401 | eprint(f'Unknown error: {exp}') |
diff --git a/src/otp2289/generator.py b/src/otp2289/generator.py index 32123b4..17024a2 100644 --- a/src/otp2289/generator.py +++ b/src/otp2289/generator.py | |||
| @@ -24,16 +24,21 @@ | |||
| 24 | # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 24 | # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 25 | """A pure Python implementation of the RFC-2289 OTP generator""" | 25 | """A pure Python implementation of the RFC-2289 OTP generator""" |
| 26 | 26 | ||
| 27 | from __future__ import annotations | ||
| 28 | |||
| 27 | import hashlib | 29 | import hashlib |
| 28 | import string | 30 | import string |
| 29 | import typing | 31 | import typing |
| 30 | from collections.abc import Iterator | 32 | |
| 33 | if typing.TYPE_CHECKING: | ||
| 34 | from collections.abc import Iterator | ||
| 31 | 35 | ||
| 32 | OTP_ALGO_MD5: typing.Final[int] = 1 | 36 | OTP_ALGO_MD5: typing.Final[int] = 1 |
| 33 | OTP_ALGO_SHA1: typing.Final[int] = 2 | 37 | OTP_ALGO_SHA1: typing.Final[int] = 2 |
| 34 | 38 | ||
| 35 | # useful constants | 39 | # useful constants |
| 36 | OTP2289_BITSTREAM_SIZE: typing.Final[int] = 64 | 40 | OTP2289_BITSTREAM_SIZE: typing.Final[int] = 64 |
| 41 | OTP2289_HEX_DIGEST_SIZE: typing.Final[int] = 16 | ||
| 37 | OTP2289_MAX_SEED_LENGTH: typing.Final[int] = 16 | 42 | OTP2289_MAX_SEED_LENGTH: typing.Final[int] = 16 |
| 38 | OTP2289_MIN_PASSWORD_LENGTH: typing.Final[int] = 10 | 43 | OTP2289_MIN_PASSWORD_LENGTH: typing.Final[int] = 10 |
| 39 | OTP2289_SHA1_DIGEST_SIZE: typing.Final[int] = 20 | 44 | OTP2289_SHA1_DIGEST_SIZE: typing.Final[int] = 20 |
| @@ -2094,12 +2099,16 @@ RFC1760_TOKENS = [ | |||
| 2094 | _ALGO_DICT = {OTP_ALGO_MD5: 'md5', OTP_ALGO_SHA1: 'sha1'} | 2099 | _ALGO_DICT = {OTP_ALGO_MD5: 'md5', OTP_ALGO_SHA1: 'sha1'} |
| 2095 | 2100 | ||
| 2096 | 2101 | ||
| 2102 | class OTPChallengeError(Exception): | ||
| 2103 | """OTPChallengeError class""" | ||
| 2104 | |||
| 2105 | |||
| 2097 | class OTPGeneratorError(Exception): | 2106 | class OTPGeneratorError(Exception): |
| 2098 | """OTPGeneratorError class""" | 2107 | """OTPGeneratorError class""" |
| 2099 | 2108 | ||
| 2100 | 2109 | ||
| 2101 | class OTPChallengeError(Exception): | 2110 | class OTPResponseError(Exception): |
| 2102 | """OTPChallengeError class""" | 2111 | """OTPResponseError class""" |
| 2103 | 2112 | ||
| 2104 | 2113 | ||
| 2105 | class OTPResponse: | 2114 | class OTPResponse: |
| @@ -2120,10 +2129,24 @@ class OTPResponse: | |||
| 2120 | """bytes representation of the object""" | 2129 | """bytes representation of the object""" |
| 2121 | return self._response_bytes | 2130 | return self._response_bytes |
| 2122 | 2131 | ||
| 2132 | def __eq__(self, value: object, /) -> bool: | ||
| 2133 | """definition for type equality""" | ||
| 2134 | if not isinstance(value, OTPResponse): | ||
| 2135 | return False | ||
| 2136 | |||
| 2137 | return self._response_bytes == value.response_bytes | ||
| 2138 | |||
| 2123 | def __hash__(self) -> int: | 2139 | def __hash__(self) -> int: |
| 2124 | """Uses the hash value of _response_bytes""" | 2140 | """Uses the hash value of _response_bytes""" |
| 2125 | return hash(self._response_bytes) | 2141 | return hash(self._response_bytes) |
| 2126 | 2142 | ||
| 2143 | def __repr__(self) -> str: | ||
| 2144 | """repr implementation""" | ||
| 2145 | return ( | ||
| 2146 | f'{self.__class__} at {id(self)} ' | ||
| 2147 | f'(response_bytes={self._response_bytes!r})' | ||
| 2148 | ) | ||
| 2149 | |||
| 2127 | @property | 2150 | @property |
| 2128 | def hexdigest(self) -> str: | 2151 | def hexdigest(self) -> str: |
| 2129 | """Hexdigest representation of the OTP response""" | 2152 | """Hexdigest representation of the OTP response""" |
| @@ -2139,6 +2162,59 @@ class OTPResponse: | |||
| 2139 | """Tokens representation of the OTP response""" | 2162 | """Tokens representation of the OTP response""" |
| 2140 | return self._words | 2163 | return self._words |
| 2141 | 2164 | ||
| 2165 | @classmethod | ||
| 2166 | def from_hex(cls, ot_hex: str) -> OTPResponse: | ||
| 2167 | """ | ||
| 2168 | Generates instance from the provided hexidigest with or without | ||
| 2169 | leading 0x as specified by RFC-2289. | ||
| 2170 | |||
| 2171 | :param ot_hex: The one-time hex to validate | ||
| 2172 | :type ot_hex: str | ||
| 2173 | |||
| 2174 | :raises otp2289.OTPResponseError: When the ot_hex is invalid | ||
| 2175 | |||
| 2176 | :return: A new OTPResponse object | ||
| 2177 | :rtype: otp2289.OTPResponse | ||
| 2178 | """ | ||
| 2179 | return cls(OTPResponse.hex_to_bytes(ot_hex)) | ||
| 2180 | |||
| 2181 | @classmethod | ||
| 2182 | def from_tokens(cls, tokens_str: str) -> OTPResponse: | ||
| 2183 | """ | ||
| 2184 | Generates instance from a 6 words token as specified by RFC-2289. | ||
| 2185 | |||
| 2186 | :param tokens_str: String representing 6 words tokens | ||
| 2187 | :type tokens_str: str | ||
| 2188 | |||
| 2189 | :raises otp2289.OTPResponseError: When the tokens_str is invalid | ||
| 2190 | |||
| 2191 | :return: A new OTPResponse object | ||
| 2192 | :rtype: otp2289.OTPResponse | ||
| 2193 | """ | ||
| 2194 | return cls(OTPResponse.tokens_to_bytes(tokens_str)) | ||
| 2195 | |||
| 2196 | @staticmethod | ||
| 2197 | def bit_pair_sum(bit_stream: str) -> int: | ||
| 2198 | """ | ||
| 2199 | Split bit_stream in bit-pairs and sum them all together. | ||
| 2200 | |||
| 2201 | :param bit_stream: The bit-stream object | ||
| 2202 | :type bit_stream: str | ||
| 2203 | |||
| 2204 | :return: The sum of all bit-pairs in bit_stream | ||
| 2205 | :rtype: int | ||
| 2206 | """ | ||
| 2207 | if not isinstance(bit_stream, str): | ||
| 2208 | raise OTPResponseError('bit_stream must be of type str') | ||
| 2209 | if len(bit_stream) != OTP2289_BITSTREAM_SIZE: | ||
| 2210 | raise OTPResponseError( | ||
| 2211 | f'bit_stream must be of size {OTP2289_BITSTREAM_SIZE}' | ||
| 2212 | ) | ||
| 2213 | value = 0 | ||
| 2214 | for pair in zip(bit_stream[::2], bit_stream[1::2], strict=True): | ||
| 2215 | value += int(''.join(pair), 2) | ||
| 2216 | return value | ||
| 2217 | |||
| 2142 | @staticmethod | 2218 | @staticmethod |
| 2143 | def bytes_to_tokens(hash_bytes: bytes) -> str: | 2219 | def bytes_to_tokens(hash_bytes: bytes) -> str: |
| 2144 | """ | 2220 | """ |
| @@ -2151,7 +2227,7 @@ class OTPResponse: | |||
| 2151 | :rtype: str | 2227 | :rtype: str |
| 2152 | """ | 2228 | """ |
| 2153 | bit_stream = ''.join([f'{byte:0>8b}' for byte in hash_bytes]) | 2229 | bit_stream = ''.join([f'{byte:0>8b}' for byte in hash_bytes]) |
| 2154 | bit_pair_sum = OTPGenerator.bit_pair_sum(bit_stream) | 2230 | bit_pair_sum = OTPResponse.bit_pair_sum(bit_stream) |
| 2155 | tokens = [] | 2231 | tokens = [] |
| 2156 | tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)]) | 2232 | tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)]) |
| 2157 | tokens.append(RFC1760_TOKENS[int(bit_stream[11:22], 2)]) | 2233 | tokens.append(RFC1760_TOKENS[int(bit_stream[11:22], 2)]) |
| @@ -2165,6 +2241,81 @@ class OTPResponse: | |||
| 2165 | ) | 2241 | ) |
| 2166 | return ' '.join(tokens) | 2242 | return ' '.join(tokens) |
| 2167 | 2243 | ||
| 2244 | @staticmethod | ||
| 2245 | def hex_to_bytes(ot_hex: str) -> bytes: | ||
| 2246 | """ | ||
| 2247 | Returns bytes from the provided hexidigest. | ||
| 2248 | |||
| 2249 | :param ot_hex: The one-time hex to validate | ||
| 2250 | :type ot_hex: str | ||
| 2251 | |||
| 2252 | :raises otp2289.OTPResponseError: If hex does not validate | ||
| 2253 | |||
| 2254 | :return: The validated hex (without leading 0x) converted to bytes | ||
| 2255 | :rtype: bytes | ||
| 2256 | """ | ||
| 2257 | if not isinstance(ot_hex, str): | ||
| 2258 | raise OTPResponseError('OT-hex must be a str') | ||
| 2259 | if ot_hex.startswith('0x'): | ||
| 2260 | ot_hex = ot_hex[2:] | ||
| 2261 | ot_hex = ot_hex.strip().lower() | ||
| 2262 | if len(ot_hex) != OTP2289_HEX_DIGEST_SIZE: | ||
| 2263 | raise OTPResponseError( | ||
| 2264 | f'The length of the hex should be {OTP2289_HEX_DIGEST_SIZE} ' | ||
| 2265 | '(representing 64 bits digest)' | ||
| 2266 | ) | ||
| 2267 | try: | ||
| 2268 | return bytes.fromhex(ot_hex) | ||
| 2269 | except ValueError: | ||
| 2270 | raise OTPResponseError('Invalid OT-hex') from None | ||
| 2271 | |||
| 2272 | @staticmethod | ||
| 2273 | def tokens_to_bytes(tokens_str: str) -> bytes: | ||
| 2274 | """ | ||
| 2275 | Returns bytes from a 6 words token as specified by RFC-2289. | ||
| 2276 | |||
| 2277 | :param tokens_str: String representing 6 words tokens | ||
| 2278 | :type tokens_str: str | ||
| 2279 | |||
| 2280 | :raises otp2289.OTPResponseError: When the tokens_str is invalid | ||
| 2281 | |||
| 2282 | :return: 6 words tokens | ||
| 2283 | :rtype: bytes | ||
| 2284 | """ | ||
| 2285 | if not isinstance(tokens_str, str): | ||
| 2286 | raise OTPResponseError('tokens must be a str') | ||
| 2287 | tokens = tokens_str.split() | ||
| 2288 | if len(tokens) != OTP2289_TOKENS_COUNT: | ||
| 2289 | raise OTPResponseError( | ||
| 2290 | f'Tokens-string does not contain {OTP2289_TOKENS_COUNT} tokens' | ||
| 2291 | ) | ||
| 2292 | token_ints = [] | ||
| 2293 | try: | ||
| 2294 | token_ints = [ | ||
| 2295 | RFC1760_TOKENS.index(token.upper()) for token in tokens | ||
| 2296 | ] | ||
| 2297 | except ValueError: | ||
| 2298 | raise OTPResponseError( | ||
| 2299 | 'One or more words not present in RFC1760' | ||
| 2300 | ) from None | ||
| 2301 | # now we build a string of bits | ||
| 2302 | bit_stream = format(token_ints[0], '011b') | ||
| 2303 | bit_stream += format(token_ints[1], '011b') | ||
| 2304 | bit_stream += format(token_ints[2], '011b') | ||
| 2305 | bit_stream += format(token_ints[3], '011b') | ||
| 2306 | bit_stream += format(token_ints[4], '011b') | ||
| 2307 | bit_stream += format(token_ints[5], '011b') | ||
| 2308 | # we have 66 bits: 64 digest + 2 bit pair sum (control number) | ||
| 2309 | # RFC-2289: All OTP generators MUST calculate this checksum and all | ||
| 2310 | # OTP servers MUST verify this checksum explicitly as part of the | ||
| 2311 | # operation of decoding this representation of the one-time password. | ||
| 2312 | if ( | ||
| 2313 | f'{OTPResponse.bit_pair_sum(bit_stream[:64]):0>8b}'[-2:] | ||
| 2314 | != bit_stream[-2:] | ||
| 2315 | ): | ||
| 2316 | raise OTPResponseError('Invalid bit checksum') | ||
| 2317 | return int(bit_stream[:64], 2).to_bytes(8, 'big') | ||
| 2318 | |||
| 2168 | 2319 | ||
| 2169 | class OTPGenerator: | 2320 | class OTPGenerator: |
| 2170 | """OTPGenerator class""" | 2321 | """OTPGenerator class""" |
| @@ -2211,28 +2362,6 @@ class OTPGenerator: | |||
| 2211 | ) | 2362 | ) |
| 2212 | 2363 | ||
| 2213 | @staticmethod | 2364 | @staticmethod |
| 2214 | def bit_pair_sum(bit_stream: str) -> int: | ||
| 2215 | """ | ||
| 2216 | Split bit_stream in bit-pairs and sum them all together. | ||
| 2217 | |||
| 2218 | :param bit_stream: The bit-stream object | ||
| 2219 | :type bit_stream: str | ||
| 2220 | |||
| 2221 | :return: The sum of all bit-pairs in bit_stream | ||
| 2222 | :rtype: int | ||
| 2223 | """ | ||
| 2224 | if not isinstance(bit_stream, str): | ||
| 2225 | raise OTPGeneratorError('bit_stream must be of type str') | ||
| 2226 | if len(bit_stream) != OTP2289_BITSTREAM_SIZE: | ||
| 2227 | raise OTPGeneratorError( | ||
| 2228 | f'bit_stream must be of size {OTP2289_BITSTREAM_SIZE}' | ||
| 2229 | ) | ||
| 2230 | value = 0 | ||
| 2231 | for pair in zip(bit_stream[::2], bit_stream[1::2], strict=True): | ||
| 2232 | value += int(''.join(pair), 2) | ||
| 2233 | return value | ||
| 2234 | |||
| 2235 | @staticmethod | ||
| 2236 | def get_tokens_from_challenge(challenge: str) -> tuple[str, str, int]: | 2365 | def get_tokens_from_challenge(challenge: str) -> tuple[str, str, int]: |
| 2237 | """ | 2366 | """ |
| 2238 | Returns tokens (seed, hash_algo and step) from a challenge string. | 2367 | Returns tokens (seed, hash_algo and step) from a challenge string. |
| @@ -2335,54 +2464,6 @@ class OTPGenerator: | |||
| 2335 | return bytes([byte_str1[i] ^ byte_str2[i] for i in range(length)]) | 2464 | return bytes([byte_str1[i] ^ byte_str2[i] for i in range(length)]) |
| 2336 | 2465 | ||
| 2337 | @staticmethod | 2466 | @staticmethod |
| 2338 | def tokens_to_bytes(tokens_str: str) -> bytes: | ||
| 2339 | """ | ||
| 2340 | Returns bytes from a 6 words token as specified by RFC-2289. | ||
| 2341 | |||
| 2342 | :param tokens_str: String representing 6 words tokens | ||
| 2343 | :type tokens_str: str | ||
| 2344 | |||
| 2345 | :raises otp2289.OTPGeneratorError: When the tokens_str is invalid | ||
| 2346 | |||
| 2347 | :return: 6 words tokens | ||
| 2348 | :rtype: bytes | ||
| 2349 | """ | ||
| 2350 | if not isinstance(tokens_str, str): | ||
| 2351 | raise OTPGeneratorError('tokens must be a str') | ||
| 2352 | tokens = tokens_str.split() | ||
| 2353 | if len(tokens) != OTP2289_TOKENS_COUNT: | ||
| 2354 | raise OTPGeneratorError( | ||
| 2355 | f'Tokens-string does not contain {OTP2289_SHA1_DIGEST_SIZE} ' | ||
| 2356 | 'tokens' | ||
| 2357 | ) | ||
| 2358 | token_ints = [] | ||
| 2359 | try: | ||
| 2360 | token_ints = [ | ||
| 2361 | RFC1760_TOKENS.index(token.upper()) for token in tokens | ||
| 2362 | ] | ||
| 2363 | except ValueError: | ||
| 2364 | raise OTPGeneratorError( | ||
| 2365 | 'One or more words not present in RFC1760' | ||
| 2366 | ) from None | ||
| 2367 | # now we build a string of bits | ||
| 2368 | bit_stream = format(token_ints[0], '011b') | ||
| 2369 | bit_stream += format(token_ints[1], '011b') | ||
| 2370 | bit_stream += format(token_ints[2], '011b') | ||
| 2371 | bit_stream += format(token_ints[3], '011b') | ||
| 2372 | bit_stream += format(token_ints[4], '011b') | ||
| 2373 | bit_stream += format(token_ints[5], '011b') | ||
| 2374 | # we have 66 bits: 64 digest + 2 bit pair sum (control number) | ||
| 2375 | # RFC-2289: All OTP generators MUST calculate this checksum and all | ||
| 2376 | # OTP servers MUST verify this checksum explicitly as part of the | ||
| 2377 | # operation of decoding this representation of the one-time password. | ||
| 2378 | if ( | ||
| 2379 | f'{OTPGenerator.bit_pair_sum(bit_stream[:64]):0>8b}'[-2:] | ||
| 2380 | != bit_stream[-2:] | ||
| 2381 | ): | ||
| 2382 | raise OTPGeneratorError('Invalid bit checksum') | ||
| 2383 | return int(bit_stream[:64], 2).to_bytes(8, 'big') | ||
| 2384 | |||
| 2385 | @staticmethod | ||
| 2386 | def validate_hash_algo(hash_algo: int | str) -> str: | 2467 | def validate_hash_algo(hash_algo: int | str) -> str: |
| 2387 | """ | 2468 | """ |
| 2388 | Validates the provided hash-algorithm. | 2469 | Validates the provided hash-algorithm. |
| @@ -2456,56 +2537,24 @@ class OTPGenerator: | |||
| 2456 | raise OTPGeneratorError('Step value MUST be >= 0') | 2537 | raise OTPGeneratorError('Step value MUST be >= 0') |
| 2457 | return step | 2538 | return step |
| 2458 | 2539 | ||
| 2459 | def generate_otp_hexdigest(self, step: int) -> str: | 2540 | def generate_otp_response(self, step: int) -> OTPResponse: |
| 2460 | """ | 2541 | """ |
| 2461 | Generates the OTP hexdigest for the given step. | 2542 | Generates OTPResponse instance for the given step |
| 2462 | 2543 | ||
| 2463 | :param step: The step to generate OTP for | 2544 | :param step: The step to generate OTP for |
| 2464 | :type step: int | 2545 | :type step: int |
| 2465 | 2546 | ||
| 2466 | :return: Hexdigest for the given step | 2547 | :return: OTPResponse instance for the given step |
| 2467 | :rtype: str | 2548 | :rtype: OTPResponse |
| 2468 | """ | 2549 | """ |
| 2469 | response = OTPResponse(self._generate_otp_bytes(step)) | 2550 | return OTPResponse(self._generate_otp_bytes(step)) |
| 2470 | return response.hexdigest | ||
| 2471 | 2551 | ||
| 2472 | def generate_otp_hexdigest_from_challenge(self, challenge: str) -> str: | 2552 | def generate_otp_response_from_challenge( |
| 2553 | self, challenge: str | ||
| 2554 | ) -> OTPResponse: | ||
| 2473 | """ | 2555 | """ |
| 2474 | Same as generate_otp_hexdigest, but it generates hex. from a challenge. | 2556 | Same as generate_otp_response, |
| 2475 | 2557 | but it generates OTPResponse from a challenge. | |
| 2476 | RFC-2289 states: | ||
| 2477 | The challenge MUST be in a standard syntax so | ||
| 2478 | that automated generators can recognize the challenge in context and | ||
| 2479 | extract these parameters. The syntax of the challenge is: | ||
| 2480 | otp-<algorithm identifier> <sequence integer> <seed> | ||
| 2481 | |||
| 2482 | :param challenge: The challenge string | ||
| 2483 | :type challenge: str | ||
| 2484 | |||
| 2485 | :return: Hexdigest for the given challenge | ||
| 2486 | :rtype: str | ||
| 2487 | """ | ||
| 2488 | seed, hash_algo, step = self.get_tokens_from_challenge(challenge) | ||
| 2489 | self._seed = self.validate_seed(seed) | ||
| 2490 | self._hash_algo = self.validate_hash_algo(hash_algo) | ||
| 2491 | return self.generate_otp_hexdigest(step) | ||
| 2492 | |||
| 2493 | def generate_otp_words(self, step: int) -> str: | ||
| 2494 | """ | ||
| 2495 | Generates the OTP six words token for the given step. | ||
| 2496 | |||
| 2497 | :param step: The step to generate OTP for | ||
| 2498 | :type step: int | ||
| 2499 | |||
| 2500 | :return: Six words (separated by single space) token for the given step | ||
| 2501 | :rtype: str | ||
| 2502 | """ | ||
| 2503 | response = OTPResponse(self._generate_otp_bytes(step)) | ||
| 2504 | return response.words | ||
| 2505 | |||
| 2506 | def generate_otp_words_from_challenge(self, challenge: str) -> str: | ||
| 2507 | """ | ||
| 2508 | Same as generate_otp_words, but it generates words from a challenge. | ||
| 2509 | 2558 | ||
| 2510 | RFC-2289 states: | 2559 | RFC-2289 states: |
| 2511 | The challenge MUST be in a standard syntax so | 2560 | The challenge MUST be in a standard syntax so |
| @@ -2522,35 +2571,14 @@ class OTPGenerator: | |||
| 2522 | seed, hash_algo, step = self.get_tokens_from_challenge(challenge) | 2571 | seed, hash_algo, step = self.get_tokens_from_challenge(challenge) |
| 2523 | self._seed = self.validate_seed(seed) | 2572 | self._seed = self.validate_seed(seed) |
| 2524 | self._hash_algo = self.validate_hash_algo(hash_algo) | 2573 | self._hash_algo = self.validate_hash_algo(hash_algo) |
| 2525 | return self.generate_otp_words(step) | 2574 | return self.generate_otp_response(step) |
| 2526 | 2575 | ||
| 2527 | def hexdigest_range( | 2576 | def otp_response_range( |
| 2528 | self, start: int = 499, stop: int = 0 | 2577 | self, start: int = 499, stop: int = 0 |
| 2529 | ) -> Iterator[str]: | 2578 | ) -> Iterator[OTPResponse]: |
| 2530 | """ | ||
| 2531 | Returns an iterator that providing hexdigests corresponding to steps | ||
| 2532 | from `start` to and including `stop`. | ||
| 2533 | |||
| 2534 | :param start: The start of the range (default: 499) | ||
| 2535 | :type start: int | ||
| 2536 | |||
| 2537 | :param stop: The last step (default: 0) | ||
| 2538 | :type stop: int | ||
| 2539 | |||
| 2540 | :return: Iterator | ||
| 2541 | :rtype: generator | ||
| 2542 | """ | ||
| 2543 | if not isinstance(start, int) and isinstance(stop, int): | ||
| 2544 | raise OTPGeneratorError('Step value MUST be an int') | ||
| 2545 | if start < stop: | ||
| 2546 | raise OTPGeneratorError('Start value can not be lower than stop') | ||
| 2547 | for step in range(start, stop - 1, -1): | ||
| 2548 | yield self.generate_otp_hexdigest(step) | ||
| 2549 | |||
| 2550 | def words_range(self, start: int = 499, stop: int = 0) -> Iterator[str]: | ||
| 2551 | """ | 2579 | """ |
| 2552 | Returns an iterator that providing the words corresponding to steps | 2580 | Returns an iterator that is providing OTPResponse instances |
| 2553 | from `start` to and including `stop`. | 2581 | corresponding to steps from `start` to and including `stop` |
| 2554 | 2582 | ||
| 2555 | :param start: The start of the range (default: 499) | 2583 | :param start: The start of the range (default: 499) |
| 2556 | :type start: int | 2584 | :type start: int |
| @@ -2566,7 +2594,7 @@ class OTPGenerator: | |||
| 2566 | if start < stop: | 2594 | if start < stop: |
| 2567 | raise OTPGeneratorError('Start value can not be lower than stop') | 2595 | raise OTPGeneratorError('Start value can not be lower than stop') |
| 2568 | for step in range(start, stop - 1, -1): | 2596 | for step in range(start, stop - 1, -1): |
| 2569 | yield self.generate_otp_words(step) | 2597 | yield self.generate_otp_response(step) |
| 2570 | 2598 | ||
| 2571 | def _generate_otp_bytes(self, step: int) -> bytes: | 2599 | def _generate_otp_bytes(self, step: int) -> bytes: |
| 2572 | """ | 2600 | """ |
diff --git a/src/otp2289/server.py b/src/otp2289/server.py index 99ee460..62e6205 100644 --- a/src/otp2289/server.py +++ b/src/otp2289/server.py | |||
| @@ -32,9 +32,13 @@ import typing | |||
| 32 | if typing.TYPE_CHECKING: | 32 | if typing.TYPE_CHECKING: |
| 33 | from collections.abc import Iterator | 33 | from collections.abc import Iterator |
| 34 | 34 | ||
| 35 | from .generator import OTP_ALGO_MD5, OTPGenerator, OTPGeneratorError | 35 | from .generator import ( |
| 36 | 36 | OTP_ALGO_MD5, | |
| 37 | OTP2289_HEX_DIGEST_SIZE: typing.Final[int] = 16 | 37 | OTPGenerator, |
| 38 | OTPGeneratorError, | ||
| 39 | OTPResponse, | ||
| 40 | OTPResponseError, | ||
| 41 | ) | ||
| 38 | 42 | ||
| 39 | 43 | ||
| 40 | class OTPStateError(Exception): | 44 | class OTPStateError(Exception): |
| @@ -89,12 +93,15 @@ class OTPState: | |||
| 89 | self._seed = OTPGenerator.validate_seed(seed) | 93 | self._seed = OTPGenerator.validate_seed(seed) |
| 90 | self._hash_algo = OTPGenerator.validate_hash_algo(hash_algo) | 94 | self._hash_algo = OTPGenerator.validate_hash_algo(hash_algo) |
| 91 | self._step = OTPGenerator.validate_step(current_step) | 95 | self._step = OTPGenerator.validate_step(current_step) |
| 92 | except OTPGeneratorError as exp: | 96 | except OTPGeneratorError as err: |
| 93 | raise OTPStateError(exp.args[0]) from exp | 97 | raise OTPStateError(err.args[0]) from err |
| 94 | 98 | ||
| 95 | self._current_digest = None | 99 | self._current_digest = None |
| 96 | if ot_hex is not None: | 100 | if ot_hex is not None: |
| 97 | self._current_digest = self.validate_hex(ot_hex) | 101 | try: |
| 102 | self._current_digest = OTPResponse.hex_to_bytes(ot_hex) | ||
| 103 | except OTPResponseError as err: | ||
| 104 | raise OTPStateError(err.args[0]) from err | ||
| 98 | self._new_digest_hex = None # set upon a successful validation | 105 | self._new_digest_hex = None # set upon a successful validation |
| 99 | 106 | ||
| 100 | def __repr__(self) -> str: | 107 | def __repr__(self) -> str: |
| @@ -154,70 +161,43 @@ class OTPState: | |||
| 154 | :type dict_obj: dict | 161 | :type dict_obj: dict |
| 155 | 162 | ||
| 156 | :return: A new OTPState object | 163 | :return: A new OTPState object |
| 157 | :rtype: otp2289.OTPStore | 164 | :rtype: otp2289.OTPState |
| 158 | """ | 165 | """ |
| 159 | return cls(**dict_obj) | 166 | return cls(**dict_obj) |
| 160 | 167 | ||
| 161 | @staticmethod | 168 | @staticmethod |
| 162 | def response_to_bytes(response: str) -> bytes: | 169 | def response_string_to_otp_response(response_str: str) -> OTPResponse: |
| 163 | """ | 170 | """ |
| 164 | A wrapper that handles/validates the response as specified by RFC-2289. | 171 | A wrapper that handles/validates the response as specified by RFC-2289. |
| 165 | 172 | ||
| 166 | The method first checks if response is a token and tries to convert | 173 | The method first checks if the response string is a token and tries to |
| 167 | it to bytes. If that fails, the method assumes that response is a hex. | 174 | convert it to a OTPResponse instance. If that fails, the method assumes |
| 168 | If neither of those attempts succeeds OTPInvalidResponseError is raised | 175 | that response string is a hex. If neither of those attempts succeeds |
| 169 | It is up to the caller to run another iteration and compare the result | 176 | OTPInvalidResponseError is raised. It is up to the caller to run |
| 170 | to an existing digest in this state. | 177 | another iteration and compare the result to an existing digest in |
| 178 | this state. | ||
| 171 | 179 | ||
| 172 | :param response: The response to this state (its challenge) | 180 | :param response_str: The response string to this state (its challenge) |
| 173 | :type response: str | 181 | :type response_str: str |
| 174 | 182 | ||
| 175 | :raises otp2289.OTPInvalidResponseError: If the response is | 183 | :raises otp2289.OTPInvalidResponseError: If the response is |
| 176 | corrupt/illegal, but not if it | 184 | corrupt/illegal, but not if it |
| 177 | simply does not validate | 185 | simply does not validate |
| 178 | 186 | ||
| 179 | :return: The bytes representation of response (if any) | 187 | :return: OTPResponse instance |
| 180 | :rtype: bytes | 188 | :rtype: otp2289.OTPResponse |
| 181 | """ | 189 | """ |
| 182 | try: | 190 | try: |
| 183 | return OTPGenerator.tokens_to_bytes(response) | 191 | return OTPResponse.from_tokens(response_str) |
| 184 | except OTPGeneratorError: | 192 | except OTPResponseError: |
| 185 | # now assume hex... | 193 | # now assume hex... |
| 186 | try: | 194 | try: |
| 187 | return OTPState.validate_hex(response) | 195 | return OTPResponse.from_hex(response_str) |
| 188 | except OTPStateError: | 196 | except OTPResponseError: |
| 189 | raise OTPInvalidResponseError( | 197 | raise OTPInvalidResponseError( |
| 190 | 'The response is neither a valid token or hex' | 198 | 'The response is neither a valid token or hex' |
| 191 | ) from None | 199 | ) from None |
| 192 | 200 | ||
| 193 | @staticmethod | ||
| 194 | def validate_hex(ot_hex: str) -> bytes: | ||
| 195 | """ | ||
| 196 | Validates the provided hexidigest. | ||
| 197 | |||
| 198 | :param ot_hex: The one-time hex to validate | ||
| 199 | :type ot_hex: str | ||
| 200 | |||
| 201 | :raises otp2289.OTPStateError: If hex does not validate | ||
| 202 | |||
| 203 | :return: The validated hex (without leading 0x) converted to bytes | ||
| 204 | :rtype: bytes | ||
| 205 | """ | ||
| 206 | if not isinstance(ot_hex, str): | ||
| 207 | raise OTPStateError('OT-hex must be a str') | ||
| 208 | if ot_hex.startswith('0x'): | ||
| 209 | ot_hex = ot_hex[2:] | ||
| 210 | ot_hex = ot_hex.strip().lower() | ||
| 211 | if len(ot_hex) != OTP2289_HEX_DIGEST_SIZE: | ||
| 212 | raise OTPStateError( | ||
| 213 | f'The length of the hex should be {OTP2289_HEX_DIGEST_SIZE} ' | ||
| 214 | '(representing 64 bits digest)' | ||
| 215 | ) | ||
| 216 | try: | ||
| 217 | return bytes.fromhex(ot_hex) | ||
| 218 | except ValueError: | ||
| 219 | raise OTPStateError('Invalid OT-hex') from None | ||
| 220 | |||
| 221 | def get_next_state(self) -> OTPState | None: | 201 | def get_next_state(self) -> OTPState | None: |
| 222 | """ | 202 | """ |
| 223 | Returns the next state for a validated OTPState. | 203 | Returns the next state for a validated OTPState. |
| @@ -235,26 +215,38 @@ class OTPState: | |||
| 235 | ) | 215 | ) |
| 236 | 216 | ||
| 237 | def response_validates( | 217 | def response_validates( |
| 238 | self, response: str, *, store_valid_response: bool = True | 218 | self, response: str | OTPResponse, *, store_valid_response: bool = True |
| 239 | ) -> bool: | 219 | ) -> bool: |
| 240 | """ | 220 | """ |
| 241 | Validates the incoming response as specified by RFC-2289. | 221 | Validates the incoming response as specified by RFC-2289. |
| 242 | 222 | ||
| 243 | :param response: The response to this state (its challenge) | 223 | :param response: The response to this state (its challenge) |
| 244 | :type response: str | 224 | :type response: str or OTPResponse |
| 245 | 225 | ||
| 246 | :param store_valid_response: Should a valid response be stored | 226 | :param store_valid_response: Should a valid response be stored |
| 247 | :type store_valid_response: bool | 227 | :type store_valid_response: bool |
| 248 | 228 | ||
| 249 | :raises otp2289.OTPInvalidResponseError: If the response does not match | 229 | :raises otp2289.OTPInvalidResponseError: If the response is corrupt |
| 250 | this state | 230 | or invalid |
| 251 | 231 | ||
| 252 | :return: Returns True if response validates, False otherwise | 232 | :return: Returns True if response validates, False otherwise |
| 253 | :rtype: bool | 233 | :rtype: bool |
| 254 | """ | 234 | """ |
| 255 | # self.response_to_bytes raises OTPInvalidResponseError in case | 235 | # self.response_string_to_otp_response raises OTPInvalidResponseError |
| 256 | # response is corrupt or in a wrong format | 236 | # in case response is corrupt or in a wrong format |
| 257 | response_bytes = self.response_to_bytes(response) | 237 | if not isinstance(response, (str, OTPResponse)): |
| 238 | raise OTPInvalidResponseError( | ||
| 239 | 'response must be of type str or OTPResponse' | ||
| 240 | ) | ||
| 241 | |||
| 242 | if isinstance(response, str): | ||
| 243 | response_bytes = self.response_string_to_otp_response( | ||
| 244 | response | ||
| 245 | ).response_bytes | ||
| 246 | else: | ||
| 247 | # assume OTPResponse | ||
| 248 | response_bytes = response.response_bytes | ||
| 249 | |||
| 258 | if self._hash_algo == 'md5': | 250 | if self._hash_algo == 'md5': |
| 259 | digest = hashlib.md5(response_bytes).digest() | 251 | digest = hashlib.md5(response_bytes).digest() |
| 260 | if ( | 252 | if ( |
