diff options
Diffstat (limited to 'src/otp2289/server.py')
| -rw-r--r-- | src/otp2289/server.py | 104 |
1 files changed, 48 insertions, 56 deletions
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 ( |
