summaryrefslogtreecommitdiff
path: root/otp2289/server.py
diff options
context:
space:
mode:
Diffstat (limited to 'otp2289/server.py')
-rw-r--r--otp2289/server.py240
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
39class OTPStoreException(Exception):
40 """OTPStoreException class"""
41
42
39class OTPInvalidResponse(Exception): 43class 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
264class 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))