summaryrefslogtreecommitdiff
path: root/otp2289/server.py
diff options
context:
space:
mode:
Diffstat (limited to 'otp2289/server.py')
-rw-r--r--otp2289/server.py120
1 files changed, 73 insertions, 47 deletions
diff --git a/otp2289/server.py b/otp2289/server.py
index d0c3e01..7f742cd 100644
--- a/otp2289/server.py
+++ b/otp2289/server.py
@@ -1,7 +1,7 @@
1# -*- coding: utf-8 -*- 1# -*- coding: utf-8 -*-
2# SPDX-License-Identifier: BSD-2-Clause-FreeBSD 2# SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3# 3#
4# Copyright (c) 2020, Simeon Simeonov 4# Copyright (c) 2020-2022 Simeon Simeonov
5# All rights reserved. 5# All rights reserved.
6# 6#
7# Redistribution and use in source and binary forms, with or without 7# Redistribution and use in source and binary forms, with or without
@@ -27,9 +27,7 @@
27import binascii 27import binascii
28import hashlib 28import hashlib
29 29
30from .generator import (OTP_ALGO_MD5, 30from .generator import OTP_ALGO_MD5, OTPGenerator, OTPGeneratorException
31 OTPGenerator,
32 OTPGeneratorException)
33 31
34 32
35class OTPStateException(Exception): 33class OTPStateException(Exception):
@@ -53,7 +51,13 @@ class OTPState:
53 - validate the corresponding generated response from the generator 51 - validate the corresponding generated response from the generator
54 """ 52 """
55 53
56 def __init__(self, ot_hex, current_step, seed, hash_algo=OTP_ALGO_MD5): 54 def __init__(
55 self,
56 ot_hex: str,
57 current_step: int,
58 seed: str,
59 hash_algo=OTP_ALGO_MD5,
60 ):
57 """ 61 """
58 Constructs an OTPState object with the given arguments. 62 Constructs an OTPState object with the given arguments.
59 63
@@ -87,45 +91,47 @@ class OTPState:
87 91
88 def __repr__(self): 92 def __repr__(self):
89 """repr implementation""" 93 """repr implementation"""
90 return (f'{self.__class__} at {id(self)} ' 94 return (
91 f'(ot_hex={self._current_digest}, current_step={self._step}, ' 95 f'{self.__class__} at {id(self)} '
92 f'seed={self._seed}, ' 96 f'(ot_hex={self._current_digest}, current_step={self._step}, '
93 f'hash_algo={self._hash_algo})') 97 f'seed={self._seed}, '
98 f'hash_algo={self._hash_algo})'
99 )
94 100
95 @property 101 @property
96 def challenge_string(self): 102 def challenge_string(self) -> str:
97 """challenge_string-property""" 103 """challenge_string-property"""
98 # RFC-2289: "...the entire challenge string MUST be 104 # RFC-2289: "...the entire challenge string MUST be
99 # terminated with either a space or a new line." 105 # terminated with either a space or a new line."
100 return f'otp-{self._hash_algo} {self._step} {self._seed} ' 106 return f'otp-{self._hash_algo} {self._step} {self._seed} '
101 107
102 @property 108 @property
103 def current_digest(self): 109 def current_digest(self) -> bytes:
104 """current_digest-property""" 110 """current_digest-property"""
105 return self._current_digest 111 return self._current_digest
106 112
107 @property 113 @property
108 def hash_algo(self): 114 def hash_algo(self) -> str:
109 """hash_algo-property""" 115 """hash_algo-property"""
110 return self._hash_algo 116 return self._hash_algo
111 117
112 @property 118 @property
113 def seed(self): 119 def seed(self) -> str:
114 """seed-property""" 120 """seed-property"""
115 return self._seed 121 return self._seed
116 122
117 @property 123 @property
118 def step(self): 124 def step(self) -> int:
119 """step-property""" 125 """step-property"""
120 return self._step 126 return self._step
121 127
122 @property 128 @property
123 def validated(self): 129 def validated(self) -> bool:
124 """validated-property""" 130 """validated-property"""
125 return bool(self._new_digest_hex) 131 return bool(self._new_digest_hex)
126 132
127 @classmethod 133 @classmethod
128 def from_dict(cls, dict_obj): 134 def from_dict(cls, dict_obj: dict):
129 """ 135 """
130 Returns an OTPState object from the dict-object 136 Returns an OTPState object from the dict-object
131 137
@@ -138,7 +144,7 @@ class OTPState:
138 return cls(**dict_obj) 144 return cls(**dict_obj)
139 145
140 @staticmethod 146 @staticmethod
141 def response_to_bytes(response): 147 def response_to_bytes(response: str) -> bytes:
142 """ 148 """
143 A wrapper that handles/validates the response as specified by RFC-2289. 149 A wrapper that handles/validates the response as specified by RFC-2289.
144 150
@@ -166,10 +172,11 @@ class OTPState:
166 return OTPState.validate_hex(response) 172 return OTPState.validate_hex(response)
167 except OTPStateException: 173 except OTPStateException:
168 raise OTPInvalidResponse( 174 raise OTPInvalidResponse(
169 'The response is neither a valid token or hex') from None 175 'The response is neither a valid token or hex'
176 ) from None
170 177
171 @staticmethod 178 @staticmethod
172 def validate_hex(ot_hex): 179 def validate_hex(ot_hex: str) -> bytes:
173 """ 180 """
174 Validates the provided hexidigest. 181 Validates the provided hexidigest.
175 182
@@ -187,8 +194,10 @@ class OTPState:
187 ot_hex = ot_hex[2:] 194 ot_hex = ot_hex[2:]
188 ot_hex = ot_hex.strip().lower() 195 ot_hex = ot_hex.strip().lower()
189 if len(ot_hex) != 16: 196 if len(ot_hex) != 16:
190 raise OTPStateException('The length of the hex should be 16 ' 197 raise OTPStateException(
191 '(representing 64 bits digest)') 198 'The length of the hex should be 16 '
199 '(representing 64 bits digest)'
200 )
192 try: 201 try:
193 return binascii.unhexlify(ot_hex) 202 return binascii.unhexlify(ot_hex)
194 except binascii.Error: 203 except binascii.Error:
@@ -206,12 +215,18 @@ class OTPState:
206 """ 215 """
207 if self._new_digest_hex is None: 216 if self._new_digest_hex is None:
208 return None 217 return None
209 return OTPState(self._new_digest_hex, 218 return OTPState(
210 self._step - 1, 219 self._new_digest_hex,
211 self._seed, 220 self._step - 1,
212 self._hash_algo) 221 self._seed,
222 self._hash_algo,
223 )
213 224
214 def response_validates(self, response, store_valid_response=True): 225 def response_validates(
226 self,
227 response: str,
228 store_valid_response: str = True,
229 ) -> bool:
215 """ 230 """
216 Validates the incoming response as specified by RFC-2289. 231 Validates the incoming response as specified by RFC-2289.
217 232
@@ -233,32 +248,35 @@ class OTPState:
233 if self._hash_algo == 'md5': 248 if self._hash_algo == 'md5':
234 digest = hashlib.md5(response_bytes).digest() 249 digest = hashlib.md5(response_bytes).digest()
235 if ( 250 if (
236 self._current_digest is None or 251 self._current_digest is None
237 OTPGenerator.strxor(digest[0:8], digest[8:]) == 252 or OTPGenerator.strxor(digest[0:8], digest[8:])
238 self._current_digest 253 == self._current_digest
239 ): 254 ):
240 if store_valid_response: 255 if store_valid_response:
241 self._new_digest_hex = binascii.hexlify( 256 self._new_digest_hex = binascii.hexlify(
242 response_bytes).decode() 257 response_bytes
258 ).decode()
243 return True 259 return True
244 return False 260 return False
245 if self._hash_algo == 'sha1': 261 if self._hash_algo == 'sha1':
246 digest = hashlib.sha1(response_bytes).digest() 262 digest = hashlib.sha1(response_bytes).digest()
247 if ( 263 if (
248 self._current_digest is None or 264 self._current_digest is None
249 OTPGenerator.sha1_digest_folding( 265 or OTPGenerator.sha1_digest_folding(
250 hashlib.sha1( 266 hashlib.sha1(response_bytes).digest()
251 response_bytes).digest()) == self._current_digest 267 )
268 == self._current_digest
252 ): 269 ):
253 if store_valid_response: 270 if store_valid_response:
254 self._new_digest_hex = binascii.hexlify( 271 self._new_digest_hex = binascii.hexlify(
255 response_bytes).decode() 272 response_bytes
273 ).decode()
256 return True 274 return True
257 return False 275 return False
258 # this should not happen since the hash_algo is validated by the caller 276 # this should not happen since the hash_algo is validated by the caller
259 raise OTPInvalidResponse(f'Ivalid hash_algo: {self._hash_algo}') 277 raise OTPInvalidResponse(f'Ivalid hash_algo: {self._hash_algo}')
260 278
261 def to_dict(self): 279 def to_dict(self) -> dict:
262 """ 280 """
263 Returns a dict representation of the object. 281 Returns a dict representation of the object.
264 282
@@ -270,10 +288,12 @@ class OTPState:
270 ot_hex = self._current_digest 288 ot_hex = self._current_digest
271 if ot_hex is not None: 289 if ot_hex is not None:
272 ot_hex = binascii.hexlify(self._current_digest).decode() 290 ot_hex = binascii.hexlify(self._current_digest).decode()
273 return {'ot_hex': ot_hex, 291 return {
274 'current_step': self._step, 292 'ot_hex': ot_hex,
275 'seed': self._seed, 293 'current_step': self._step,
276 'hash_algo': self._hash_algo} 294 'seed': self._seed,
295 'hash_algo': self._hash_algo,
296 }
277 297
278 298
279class OTPStore: 299class OTPStore:
@@ -285,6 +305,7 @@ class OTPStore:
285 305
286 The class could serve as a base class when implementing store backends. 306 The class could serve as a base class when implementing store backends.
287 """ 307 """
308
288 def __init__(self, data=None): 309 def __init__(self, data=None):
289 """ 310 """
290 Constructs an OTPStore object from data 311 Constructs an OTPStore object from data
@@ -310,7 +331,7 @@ class OTPStore:
310 return len(self._data) 331 return len(self._data)
311 332
312 @property 333 @property
313 def data(self): 334 def data(self) -> dict:
314 """ 335 """
315 data-property 336 data-property
316 337
@@ -320,7 +341,7 @@ class OTPStore:
320 return self._data 341 return self._data
321 342
322 @property 343 @property
323 def states(self): 344 def states(self) -> dict:
324 """ 345 """
325 states-property 346 states-property
326 347
@@ -329,7 +350,7 @@ class OTPStore:
329 """ 350 """
330 return self._states 351 return self._states
331 352
332 def add_state(self, key, state): 353 def add_state(self, key: str, state: OTPState):
333 """ 354 """
334 Adds an OTPState object with a given key. 355 Adds an OTPState object with a given key.
335 356
@@ -356,7 +377,7 @@ class OTPStore:
356 """A wrapper for dict.items""" 377 """A wrapper for dict.items"""
357 return self._data.items() 378 return self._data.items()
358 379
359 def pop_state(self, key): 380 def pop_state(self, key: str) -> OTPState:
360 """ 381 """
361 Removes specified key and returns the corresponding OTPState-object. 382 Removes specified key and returns the corresponding OTPState-object.
362 383
@@ -376,7 +397,12 @@ class OTPStore:
376 self._states.pop(state) 397 self._states.pop(state)
377 return state 398 return state
378 399
379 def response_validates(self, key, response, store_valid_response=True): 400 def response_validates(
401 self,
402 key: str,
403 response: str,
404 store_valid_response: bool = True,
405 ) -> bool:
380 """ 406 """
381 A method that wraps around OTPState.response_validates and 407 A method that wraps around OTPState.response_validates and
382 OTPState.get_next_state. 408 OTPState.get_next_state.
@@ -411,7 +437,7 @@ class OTPStore:
411 self._states.pop(state) 437 self._states.pop(state)
412 return rvalue 438 return rvalue
413 439
414 def to_dict(self): 440 def to_dict(self) -> dict:
415 """ 441 """
416 Returns a dict representation of the object. 442 Returns a dict representation of the object.
417 443
@@ -422,7 +448,7 @@ class OTPStore:
422 """ 448 """
423 return {key: state.to_dict() for key, state in self._data.items()} 449 return {key: state.to_dict() for key, state in self._data.items()}
424 450
425 def _add_data(self, dict_obj): 451 def _add_data(self, dict_obj: dict) -> dict:
426 """ 452 """
427 Adds data from a dict object (dict_obj). 453 Adds data from a dict object (dict_obj).
428 454