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.py88
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
27import binascii 27import binascii
28import hashlib 28import hashlib
29 29
30from .generator import OTP_ALGO_MD5, OTPGenerator, OTPGeneratorException 30from .generator import OTP_ALGO_MD5, OTPGenerator, OTPGeneratorError
31 31
32 32
33class OTPStateException(Exception): 33class OTPStateError(Exception):
34 """OTPStateException class""" 34 """OTPStateError class"""
35 35
36 36
37class OTPStoreException(Exception): 37class OTPStoreError(Exception):
38 """OTPStoreException class""" 38 """OTPStoreError class"""
39 39
40 40
41class OTPInvalidResponse(Exception): 41class OTPInvalidResponseError(Exception):
42 """OTPInvalidResponse class""" 42 """OTPInvalidResponseError class"""
43 43
44 44
45class OTPState: 45class 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