summaryrefslogtreecommitdiff
path: root/src/otp2289/server.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/otp2289/server.py')
-rw-r--r--src/otp2289/server.py104
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
32if typing.TYPE_CHECKING: 32if typing.TYPE_CHECKING:
33 from collections.abc import Iterator 33 from collections.abc import Iterator
34 34
35from .generator import OTP_ALGO_MD5, OTPGenerator, OTPGeneratorError 35from .generator import (
36 36 OTP_ALGO_MD5,
37OTP2289_HEX_DIGEST_SIZE: typing.Final[int] = 16 37 OTPGenerator,
38 OTPGeneratorError,
39 OTPResponse,
40 OTPResponseError,
41)
38 42
39 43
40class OTPStateError(Exception): 44class 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 (