diff options
Diffstat (limited to 'src/otp2289/server.py')
| -rw-r--r-- | src/otp2289/server.py | 88 |
1 files changed, 38 insertions, 50 deletions
diff --git a/src/otp2289/server.py b/src/otp2289/server.py index 4305f75..e5ee0f2 100644 --- a/src/otp2289/server.py +++ b/src/otp2289/server.py | |||
| @@ -1,7 +1,6 @@ | |||
| 1 | # -*- coding: utf-8 -*- | ||
| 2 | # SPDX-License-Identifier: BSD-2-Clause-FreeBSD | 1 | # SPDX-License-Identifier: BSD-2-Clause-FreeBSD |
| 3 | # | 2 | # |
| 4 | # Copyright (c) 2020-2023 Simeon Simeonov | 3 | # Copyright (c) 2020-2025 Simeon Simeonov |
| 5 | # All rights reserved. | 4 | # All rights reserved. |
| 6 | # | 5 | # |
| 7 | # Redistribution and use in source and binary forms, with or without | 6 | # Redistribution and use in source and binary forms, with or without |
| @@ -24,22 +23,23 @@ | |||
| 24 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF | 23 | # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| 25 | # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | 24 | # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| 26 | """A pure Python implementation of the RFC-2289 OTP server""" | 25 | """A pure Python implementation of the RFC-2289 OTP server""" |
| 26 | |||
| 27 | import binascii | 27 | import binascii |
| 28 | import hashlib | 28 | import hashlib |
| 29 | 29 | ||
| 30 | from .generator import OTP_ALGO_MD5, OTPGenerator, OTPGeneratorException | 30 | from .generator import OTP_ALGO_MD5, OTPGenerator, OTPGeneratorError |
| 31 | 31 | ||
| 32 | 32 | ||
| 33 | class OTPStateException(Exception): | 33 | class OTPStateError(Exception): |
| 34 | """OTPStateException class""" | 34 | """OTPStateError class""" |
| 35 | 35 | ||
| 36 | 36 | ||
| 37 | class OTPStoreException(Exception): | 37 | class OTPStoreError(Exception): |
| 38 | """OTPStoreException class""" | 38 | """OTPStoreError class""" |
| 39 | 39 | ||
| 40 | 40 | ||
| 41 | class OTPInvalidResponse(Exception): | 41 | class OTPInvalidResponseError(Exception): |
| 42 | """OTPInvalidResponse class""" | 42 | """OTPInvalidResponseError class""" |
| 43 | 43 | ||
| 44 | 44 | ||
| 45 | class OTPState: | 45 | class OTPState: |
| @@ -52,11 +52,7 @@ class OTPState: | |||
| 52 | """ | 52 | """ |
| 53 | 53 | ||
| 54 | def __init__( | 54 | def __init__( |
| 55 | self, | 55 | self, ot_hex: str, current_step: int, seed: str, hash_algo=OTP_ALGO_MD5 |
| 56 | ot_hex: str, | ||
| 57 | current_step: int, | ||
| 58 | seed: str, | ||
| 59 | hash_algo=OTP_ALGO_MD5, | ||
| 60 | ): | 56 | ): |
| 61 | """ | 57 | """ |
| 62 | Constructs an OTPState object with the given arguments. | 58 | Constructs an OTPState object with the given arguments. |
| @@ -75,15 +71,15 @@ class OTPState: | |||
| 75 | :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5 | 71 | :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5 |
| 76 | :type hash_algo: int or str | 72 | :type hash_algo: int or str |
| 77 | 73 | ||
| 78 | :raises otp2289.OTPStateException: If the input does not validate | 74 | :raises otp2289.OTPStateError: If the input does not validate |
| 79 | """ | 75 | """ |
| 80 | # enforce the rfc2289 constraints | 76 | # enforce the rfc2289 constraints |
| 81 | try: | 77 | try: |
| 82 | self._seed = OTPGenerator.validate_seed(seed) | 78 | self._seed = OTPGenerator.validate_seed(seed) |
| 83 | self._hash_algo = OTPGenerator.validate_hash_algo(hash_algo) | 79 | self._hash_algo = OTPGenerator.validate_hash_algo(hash_algo) |
| 84 | self._step = OTPGenerator.validate_step(current_step) | 80 | self._step = OTPGenerator.validate_step(current_step) |
| 85 | except OTPGeneratorException as exp: | 81 | except OTPGeneratorError as exp: |
| 86 | raise OTPStateException(exp.args[0]) from None | 82 | raise OTPStateError(exp.args[0]) from None |
| 87 | self._current_digest = None | 83 | self._current_digest = None |
| 88 | if ot_hex is not None: | 84 | if ot_hex is not None: |
| 89 | self._current_digest = self.validate_hex(ot_hex) | 85 | self._current_digest = self.validate_hex(ot_hex) |
| @@ -157,28 +153,28 @@ class OTPState: | |||
| 157 | 153 | ||
| 158 | The method first checks if response is a token and tries to convert | 154 | The method first checks if response is a token and tries to convert |
| 159 | it to bytes. If that fails, the method assumes that response is a hex. | 155 | it to bytes. If that fails, the method assumes that response is a hex. |
| 160 | If neither of those attempts succeeds OTPInvalidResponse is raised. | 156 | If neither of those attempts succeeds OTPInvalidResponseError is raised |
| 161 | It is up to the caller to run another iteration and compare the result | 157 | It is up to the caller to run another iteration and compare the result |
| 162 | to an existing digest in this state. | 158 | to an existing digest in this state. |
| 163 | 159 | ||
| 164 | :param response: The response to this state (its challenge) | 160 | :param response: The response to this state (its challenge) |
| 165 | :type response: str | 161 | :type response: str |
| 166 | 162 | ||
| 167 | :raises otp2289.OTPInvalidResponse: If the response is corrupt/illegal, | 163 | :raises otp2289.OTPInvalidResponseError: If the response is |
| 168 | but not if it simply does not | 164 | corrupt/illegal, but not if it |
| 169 | validate | 165 | simply does not validate |
| 170 | 166 | ||
| 171 | :return: The bytes representation of response (if any) | 167 | :return: The bytes representation of response (if any) |
| 172 | :rtype: bytes | 168 | :rtype: bytes |
| 173 | """ | 169 | """ |
| 174 | try: | 170 | try: |
| 175 | return OTPGenerator.tokens_to_bytes(response) | 171 | return OTPGenerator.tokens_to_bytes(response) |
| 176 | except OTPGeneratorException: | 172 | except OTPGeneratorError: |
| 177 | # now assume hex... | 173 | # now assume hex... |
| 178 | try: | 174 | try: |
| 179 | return OTPState.validate_hex(response) | 175 | return OTPState.validate_hex(response) |
| 180 | except OTPStateException: | 176 | except OTPStateError: |
| 181 | raise OTPInvalidResponse( | 177 | raise OTPInvalidResponseError( |
| 182 | 'The response is neither a valid token or hex' | 178 | 'The response is neither a valid token or hex' |
| 183 | ) from None | 179 | ) from None |
| 184 | 180 | ||
| @@ -190,25 +186,25 @@ class OTPState: | |||
| 190 | :param ot_hex: The one-time hex to validate | 186 | :param ot_hex: The one-time hex to validate |
| 191 | :type ot_hex: str | 187 | :type ot_hex: str |
| 192 | 188 | ||
| 193 | :raises otp2289.OTPStateException: If hex does not validate | 189 | :raises otp2289.OTPStateError: If hex does not validate |
| 194 | 190 | ||
| 195 | :return: The validated hex (without leading 0x) converted to bytes | 191 | :return: The validated hex (without leading 0x) converted to bytes |
| 196 | :rtype: bytes | 192 | :rtype: bytes |
| 197 | """ | 193 | """ |
| 198 | if not isinstance(ot_hex, str): | 194 | if not isinstance(ot_hex, str): |
| 199 | raise OTPStateException('OT-hex must be a str') | 195 | raise OTPStateError('OT-hex must be a str') |
| 200 | if ot_hex.startswith('0x'): | 196 | if ot_hex.startswith('0x'): |
| 201 | ot_hex = ot_hex[2:] | 197 | ot_hex = ot_hex[2:] |
| 202 | ot_hex = ot_hex.strip().lower() | 198 | ot_hex = ot_hex.strip().lower() |
| 203 | if len(ot_hex) != 16: | 199 | if len(ot_hex) != 16: |
| 204 | raise OTPStateException( | 200 | raise OTPStateError( |
| 205 | 'The length of the hex should be 16 ' | 201 | 'The length of the hex should be 16 ' |
| 206 | '(representing 64 bits digest)' | 202 | '(representing 64 bits digest)' |
| 207 | ) | 203 | ) |
| 208 | try: | 204 | try: |
| 209 | return binascii.unhexlify(ot_hex) | 205 | return binascii.unhexlify(ot_hex) |
| 210 | except binascii.Error: | 206 | except binascii.Error: |
| 211 | raise OTPStateException('Invalid OT-hex') from None | 207 | raise OTPStateError('Invalid OT-hex') from None |
| 212 | 208 | ||
| 213 | def get_next_state(self): | 209 | def get_next_state(self): |
| 214 | """ | 210 | """ |
| @@ -223,16 +219,11 @@ class OTPState: | |||
| 223 | if self._new_digest_hex is None: | 219 | if self._new_digest_hex is None: |
| 224 | return None | 220 | return None |
| 225 | return OTPState( | 221 | return OTPState( |
| 226 | self._new_digest_hex, | 222 | self._new_digest_hex, self._step - 1, self._seed, self._hash_algo |
| 227 | self._step - 1, | ||
| 228 | self._seed, | ||
| 229 | self._hash_algo, | ||
| 230 | ) | 223 | ) |
| 231 | 224 | ||
| 232 | def response_validates( | 225 | def response_validates( |
| 233 | self, | 226 | self, response: str, store_valid_response: str = True |
| 234 | response: str, | ||
| 235 | store_valid_response: str = True, | ||
| 236 | ) -> bool: | 227 | ) -> bool: |
| 237 | """ | 228 | """ |
| 238 | Validates the incoming response as specified by RFC-2289. | 229 | Validates the incoming response as specified by RFC-2289. |
| @@ -243,14 +234,14 @@ class OTPState: | |||
| 243 | :param store_valid_response: Should a valid response be stored | 234 | :param store_valid_response: Should a valid response be stored |
| 244 | :type store_valid_response: bool | 235 | :type store_valid_response: bool |
| 245 | 236 | ||
| 246 | :raises otp2289.OTPInvalidResponse: If the response does not match | 237 | :raises otp2289.OTPInvalidResponseError: If the response does not match |
| 247 | this state | 238 | this state |
| 248 | 239 | ||
| 249 | :return: Returns True if response validates, False otherwise | 240 | :return: Returns True if response validates, False otherwise |
| 250 | :rtype: bool | 241 | :rtype: bool |
| 251 | """ | 242 | """ |
| 252 | # self.response_to_bytes raises OTPInvalidResponse in case response | 243 | # self.response_to_bytes raises OTPInvalidResponseError in case |
| 253 | # is corrupt or in a wrong format | 244 | # response is corrupt or in a wrong format |
| 254 | response_bytes = self.response_to_bytes(response) | 245 | response_bytes = self.response_to_bytes(response) |
| 255 | if self._hash_algo == 'md5': | 246 | if self._hash_algo == 'md5': |
| 256 | digest = hashlib.md5(response_bytes).digest() | 247 | digest = hashlib.md5(response_bytes).digest() |
| @@ -281,7 +272,7 @@ class OTPState: | |||
| 281 | return True | 272 | return True |
| 282 | return False | 273 | return False |
| 283 | # this should not happen since the hash_algo is validated by the caller | 274 | # this should not happen since the hash_algo is validated by the caller |
| 284 | raise OTPInvalidResponse(f'Ivalid hash_algo: {self._hash_algo}') | 275 | raise OTPInvalidResponseError(f'Ivalid hash_algo: {self._hash_algo}') |
| 285 | 276 | ||
| 286 | def to_dict(self) -> dict: | 277 | def to_dict(self) -> dict: |
| 287 | """ | 278 | """ |
| @@ -367,12 +358,12 @@ class OTPStore: | |||
| 367 | :param state: The OTPState object | 358 | :param state: The OTPState object |
| 368 | :type state: otp2289.OTPState | 359 | :type state: otp2289.OTPState |
| 369 | 360 | ||
| 370 | :raises otp2289.OTPStoreException: On failure | 361 | :raises otp2289.OTPStoreError: On failure |
| 371 | """ | 362 | """ |
| 372 | if not isinstance(key, str): | 363 | if not isinstance(key, str): |
| 373 | raise OTPStoreException('key must be a str') | 364 | raise OTPStoreError('key must be a str') |
| 374 | if not isinstance(state, OTPState): | 365 | if not isinstance(state, OTPState): |
| 375 | raise OTPStoreException('state must be an OTPState-object') | 366 | raise OTPStoreError('state must be an OTPState-object') |
| 376 | self._data[key] = state | 367 | self._data[key] = state |
| 377 | self._states[state] = key | 368 | self._states[state] = key |
| 378 | 369 | ||
| @@ -393,22 +384,19 @@ class OTPStore: | |||
| 393 | 384 | ||
| 394 | :raises KeyError: If key does not exist | 385 | :raises KeyError: If key does not exist |
| 395 | 386 | ||
| 396 | :raises otp2289.OTPStoreException: On failure | 387 | :raises otp2289.OTPStoreError: On failure |
| 397 | 388 | ||
| 398 | :return: The state corresponding to the key | 389 | :return: The state corresponding to the key |
| 399 | :rtype: otp2289.OTPState | 390 | :rtype: otp2289.OTPState |
| 400 | """ | 391 | """ |
| 401 | if not isinstance(key, str): | 392 | if not isinstance(key, str): |
| 402 | raise OTPStoreException('key must be a str') | 393 | raise OTPStoreError('key must be a str') |
| 403 | state = self._data.pop(key) | 394 | state = self._data.pop(key) |
| 404 | self._states.pop(state) | 395 | self._states.pop(state) |
| 405 | return state | 396 | return state |
| 406 | 397 | ||
| 407 | def response_validates( | 398 | def response_validates( |
| 408 | self, | 399 | self, key: str, response: str, store_valid_response: bool = True |
| 409 | key: str, | ||
| 410 | response: str, | ||
| 411 | store_valid_response: bool = True, | ||
| 412 | ) -> bool: | 400 | ) -> bool: |
| 413 | """ | 401 | """ |
| 414 | A method that wraps around OTPState.response_validates and | 402 | A method that wraps around OTPState.response_validates and |
| @@ -429,8 +417,8 @@ class OTPStore: | |||
| 429 | 417 | ||
| 430 | :raises KeyError: If the key is not present | 418 | :raises KeyError: If the key is not present |
| 431 | 419 | ||
| 432 | :raises otp2289.OTPInvalidResponse: If the response does not match | 420 | :raises otp2289.OTPInvalidResponseError: If the response does not match |
| 433 | this state | 421 | this state |
| 434 | 422 | ||
| 435 | :return: Returns True if response validates, False otherwise | 423 | :return: Returns True if response validates, False otherwise |
| 436 | :rtype: bool | 424 | :rtype: bool |
