diff options
| author | Simeon Simeonov | 2020-04-03 00:00:52 +0200 |
|---|---|---|
| committer | Simeon Simeonov | 2020-04-03 00:00:52 +0200 |
| commit | 097aaa49fa99cffec9db74432d7025457c34199e (patch) | |
| tree | 1af3ab2ec494b71610d299468c818f7ac144ce59 /otp2289/server.py | |
| parent | 2a6299c32e0baf3df06f8e6d6bc8695b951d630d (diff) | |
Implement OTPStore container class in the server module
Diffstat (limited to 'otp2289/server.py')
| -rw-r--r-- | otp2289/server.py | 240 |
1 files changed, 236 insertions, 4 deletions
diff --git a/otp2289/server.py b/otp2289/server.py index 4151026..b765af6 100644 --- a/otp2289/server.py +++ b/otp2289/server.py | |||
| @@ -36,6 +36,10 @@ class OTPStateException(Exception): | |||
| 36 | """OTPStateException class""" | 36 | """OTPStateException class""" |
| 37 | 37 | ||
| 38 | 38 | ||
| 39 | class OTPStoreException(Exception): | ||
| 40 | """OTPStoreException class""" | ||
| 41 | |||
| 42 | |||
| 39 | class OTPInvalidResponse(Exception): | 43 | class OTPInvalidResponse(Exception): |
| 40 | """OTPInvalidResponse class""" | 44 | """OTPInvalidResponse class""" |
| 41 | 45 | ||
| @@ -80,10 +84,6 @@ class OTPState: | |||
| 80 | self._current_digest = self.validate_hex(ot_hex) | 84 | self._current_digest = self.validate_hex(ot_hex) |
| 81 | self._new_digest_hex = None # set upon a successful validation | 85 | self._new_digest_hex = None # set upon a successful validation |
| 82 | 86 | ||
| 83 | def __str__(self): | ||
| 84 | """Duplicate the challenge string""" | ||
| 85 | return f'otp-{self._hash_algo} {self._step} {self._seed} ' | ||
| 86 | |||
| 87 | @property | 87 | @property |
| 88 | def challenge_string(self): | 88 | def challenge_string(self): |
| 89 | """challenge_string-property""" | 89 | """challenge_string-property""" |
| @@ -92,10 +92,43 @@ class OTPState: | |||
| 92 | return f'otp-{self._hash_algo} {self._step} {self._seed} ' | 92 | return f'otp-{self._hash_algo} {self._step} {self._seed} ' |
| 93 | 93 | ||
| 94 | @property | 94 | @property |
| 95 | def current_digest(self): | ||
| 96 | """current_digest-property""" | ||
| 97 | return self._current_digest | ||
| 98 | |||
| 99 | @property | ||
| 100 | def hash_algo(self): | ||
| 101 | """hash_algo-property""" | ||
| 102 | return self._hash_algo | ||
| 103 | |||
| 104 | @property | ||
| 105 | def seed(self): | ||
| 106 | """seed-property""" | ||
| 107 | return self._seed | ||
| 108 | |||
| 109 | @property | ||
| 110 | def step(self): | ||
| 111 | """step-property""" | ||
| 112 | return self._step | ||
| 113 | |||
| 114 | @property | ||
| 95 | def validated(self): | 115 | def validated(self): |
| 96 | """validated-property""" | 116 | """validated-property""" |
| 97 | return bool(self._new_digest_hex) | 117 | return bool(self._new_digest_hex) |
| 98 | 118 | ||
| 119 | @classmethod | ||
| 120 | def from_dict(cls, dict_obj): | ||
| 121 | """ | ||
| 122 | Returns an OTPState object from the dict-object | ||
| 123 | |||
| 124 | :param dict_obj: The dict object | ||
| 125 | :type dict_obj: dict | ||
| 126 | |||
| 127 | :return: A new OTPState object | ||
| 128 | :rtype: OTPStore | ||
| 129 | """ | ||
| 130 | return cls(**dict_obj) | ||
| 131 | |||
| 99 | @staticmethod | 132 | @staticmethod |
| 100 | def response_to_bytes(response): | 133 | def response_to_bytes(response): |
| 101 | """ | 134 | """ |
| @@ -152,6 +185,23 @@ class OTPState: | |||
| 152 | except binascii.Error: | 185 | except binascii.Error: |
| 153 | raise OTPStateException('Invalid OT-hex') | 186 | raise OTPStateException('Invalid OT-hex') |
| 154 | 187 | ||
| 188 | def get_next_state(self): | ||
| 189 | """ | ||
| 190 | Returns the next state for a validated OTPState. | ||
| 191 | |||
| 192 | This is a brand new OTPState object with the same hash_algo and seed | ||
| 193 | where step -= 1 and ot_hex = self._new_digest_hex | ||
| 194 | |||
| 195 | :return: The next OTPState if validated, None otherwise | ||
| 196 | :rtype: OTPState or None | ||
| 197 | """ | ||
| 198 | if self._new_digest_hex is None: | ||
| 199 | return None | ||
| 200 | return OTPState(self._new_digest_hex, | ||
| 201 | self._step - 1, | ||
| 202 | self._seed, | ||
| 203 | self._hash_algo) | ||
| 204 | |||
| 155 | def response_validates(self, response, store_valid_response=True): | 205 | def response_validates(self, response, store_valid_response=True): |
| 156 | """ | 206 | """ |
| 157 | Validates the incoming response as specified by RFC-2289. | 207 | Validates the incoming response as specified by RFC-2289. |
| @@ -195,3 +245,185 @@ class OTPState: | |||
| 195 | return False | 245 | return False |
| 196 | # this should not happen since the hash_algo is validated by the caller | 246 | # this should not happen since the hash_algo is validated by the caller |
| 197 | raise OTPInvalidResponse(f'Ivalid hash_algo: {self._hash_algo}') | 247 | raise OTPInvalidResponse(f'Ivalid hash_algo: {self._hash_algo}') |
| 248 | |||
| 249 | def to_dict(self): | ||
| 250 | """ | ||
| 251 | Returns a dict representation of the object. | ||
| 252 | |||
| 253 | This could be the base for a JSON serialization. | ||
| 254 | |||
| 255 | :return: The dict representation of the object | ||
| 256 | :rtype: dict | ||
| 257 | """ | ||
| 258 | return {'ot_hex': binascii.hexlify(self._current_digest).decode(), | ||
| 259 | 'current_step': self._step, | ||
| 260 | 'seed': self._seed, | ||
| 261 | 'hash_algo': self._hash_algo} | ||
| 262 | |||
| 263 | |||
| 264 | class OTPStore: | ||
| 265 | """ | ||
| 266 | OTPStore class | ||
| 267 | |||
| 268 | A helper / container class that stores OTPState objects in a 2 layered | ||
| 269 | dict structure represented by [domain][key]. | ||
| 270 | |||
| 271 | The class could serve as a base class when implementing store backends. | ||
| 272 | """ | ||
| 273 | def __init__(self, data=None): | ||
| 274 | """ | ||
| 275 | Constructs an OTPStore object from data | ||
| 276 | |||
| 277 | :param data: The data object, defaults to None | ||
| 278 | :type data: object or None | ||
| 279 | """ | ||
| 280 | self._data = {} # {key1: {state1-data...}, key2: {state2-data...}} | ||
| 281 | self._states = {} # OTPState: (domain, key) - dict | ||
| 282 | if data is not None: | ||
| 283 | self._add_data(data) | ||
| 284 | |||
| 285 | def __contains__(self, state): | ||
| 286 | """membership test""" | ||
| 287 | return state in self._states | ||
| 288 | |||
| 289 | def __iter__(self): | ||
| 290 | """iterator for OTPStore""" | ||
| 291 | return iter(self._data) | ||
| 292 | |||
| 293 | def __len__(self): | ||
| 294 | """len() implementation""" | ||
| 295 | return len(self._data) | ||
| 296 | |||
| 297 | @property | ||
| 298 | def data(self): | ||
| 299 | """ | ||
| 300 | data-property | ||
| 301 | |||
| 302 | Exposes the entire raw-data structure (dict). | ||
| 303 | Use the high level methods when possible! | ||
| 304 | """ | ||
| 305 | return self._data | ||
| 306 | |||
| 307 | @property | ||
| 308 | def states(self): | ||
| 309 | """ | ||
| 310 | states-property | ||
| 311 | |||
| 312 | Exposes the entire states structure (dict). | ||
| 313 | Use the high level methods when possible! | ||
| 314 | """ | ||
| 315 | return self._states | ||
| 316 | |||
| 317 | def add_state(self, key, state): | ||
| 318 | """ | ||
| 319 | Adds an OTPState object with a given key. | ||
| 320 | |||
| 321 | :param key: The key under which to add the state | ||
| 322 | :type key: str | ||
| 323 | |||
| 324 | :param state: The OTPState object | ||
| 325 | :type state: OTPState | ||
| 326 | |||
| 327 | :raises OTPStoreException: On failure | ||
| 328 | """ | ||
| 329 | if not isinstance(key, str): | ||
| 330 | raise OTPStoreException('key must be a str') | ||
| 331 | if not isinstance(state, OTPState): | ||
| 332 | raise OTPStoreException('state must be an OTPState-object') | ||
| 333 | self._data[key] = state | ||
| 334 | self._states[state] = key | ||
| 335 | |||
| 336 | def get(self, key, default=None): | ||
| 337 | """A wrapper for dict.get""" | ||
| 338 | return self._data.get(key, default) | ||
| 339 | |||
| 340 | def items(self): | ||
| 341 | """A wrapper for dict.items""" | ||
| 342 | return self._data.items() | ||
| 343 | |||
| 344 | def pop_state(self, key): | ||
| 345 | """ | ||
| 346 | Removes specified key and returns the corresponding OTPState-object. | ||
| 347 | |||
| 348 | :param key: The key | ||
| 349 | :type key: str | ||
| 350 | |||
| 351 | :raises KeyError: If key does not exist | ||
| 352 | |||
| 353 | :raises OTPStoreException: On failure | ||
| 354 | |||
| 355 | :return: The state corresponding to the key | ||
| 356 | :rtype: OTPState | ||
| 357 | """ | ||
| 358 | if not isinstance(key, str): | ||
| 359 | raise OTPStoreException('key must be a str') | ||
| 360 | state = self._data.pop(key) | ||
| 361 | self._states.pop(state) | ||
| 362 | return state | ||
| 363 | |||
| 364 | def response_validates(self, key, response, store_valid_response=True): | ||
| 365 | """ | ||
| 366 | A method that wraps around OTPState.response_validates and | ||
| 367 | OTPState.get_next_state. | ||
| 368 | |||
| 369 | The response is validated against the OTPState object that corresponds | ||
| 370 | to key (if any). If store_valid_response is True, the state is replaced | ||
| 371 | by the next state on successful validation. | ||
| 372 | |||
| 373 | :param key: The key | ||
| 374 | :type key: str | ||
| 375 | |||
| 376 | :param response: The response to this state (its challenge) | ||
| 377 | :type response: str | ||
| 378 | |||
| 379 | :param store_valid_response: Should a valid response be stored | ||
| 380 | :type store_valid_response: bool | ||
| 381 | |||
| 382 | :raises KeyError: If the key is not present | ||
| 383 | |||
| 384 | :raises OTPInvalidResponse: If the response does not match this state | ||
| 385 | |||
| 386 | :return: Returns True if response validates, False otherwise | ||
| 387 | :rtype: bool | ||
| 388 | """ | ||
| 389 | state = self._data[key] | ||
| 390 | rvalue = state.response_validates(response, store_valid_response) | ||
| 391 | if rvalue and store_valid_response: | ||
| 392 | next_state = state.get_next_state() | ||
| 393 | self._data[key] = next_state | ||
| 394 | self._states[next_state] = key | ||
| 395 | self._states.pop(state) | ||
| 396 | return rvalue | ||
| 397 | |||
| 398 | def to_dict(self): | ||
| 399 | """ | ||
| 400 | Returns a dict representation of the object. | ||
| 401 | |||
| 402 | This could be the base for a JSON serialization. | ||
| 403 | |||
| 404 | :return: The dict representation of the object | ||
| 405 | :rtype: dict | ||
| 406 | """ | ||
| 407 | return {key: state.to_dict() for key, state in self._data.items()} | ||
| 408 | |||
| 409 | def _add_data(self, dict_obj): | ||
| 410 | """ | ||
| 411 | Adds data from a dict object (dict_obj). | ||
| 412 | |||
| 413 | This method should probably be either overloaded or wrapped | ||
| 414 | in a child class. | ||
| 415 | |||
| 416 | dict_obj has the following format: | ||
| 417 | {'key': {'ot_hex': val1, | ||
| 418 | 'current_step': val2, | ||
| 419 | 'seed': val3, | ||
| 420 | 'hash_algo': val4}, | ||
| 421 | ...., ....} | ||
| 422 | |||
| 423 | :param dict_obj: The dict-object | ||
| 424 | :type dict_obj: dict | ||
| 425 | """ | ||
| 426 | if not dict_obj: | ||
| 427 | return | ||
| 428 | for key, state_dict in dict_obj.items(): | ||
| 429 | self.add_state(key, OTPState(**state_dict)) | ||
