summaryrefslogtreecommitdiff
path: root/otp2289/server.py
diff options
context:
space:
mode:
authorSimeon Simeonov2020-03-30 21:57:53 +0200
committerSimeon Simeonov2020-03-30 21:57:53 +0200
commit2a6299c32e0baf3df06f8e6d6bc8695b951d630d (patch)
treeb4bd18092822db0fd10411e49dc6e05d1a60e697 /otp2289/server.py
parenta4c050e34ce462f477bc3671e272e244e2792e00 (diff)
Implement OTPState in the server module
Diffstat (limited to 'otp2289/server.py')
-rw-r--r--otp2289/server.py171
1 files changed, 171 insertions, 0 deletions
diff --git a/otp2289/server.py b/otp2289/server.py
index 096ebd7..4151026 100644
--- a/otp2289/server.py
+++ b/otp2289/server.py
@@ -24,3 +24,174 @@
24# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 24# (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. 25# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26"""A pure Python implementation of the RFC-2289 OTP server""" 26"""A pure Python implementation of the RFC-2289 OTP server"""
27import binascii
28import hashlib
29
30from .generator import (OTP_ALGO_MD5,
31 OTPGenerator,
32 OTPGeneratorException)
33
34
35class OTPStateException(Exception):
36 """OTPStateException class"""
37
38
39class OTPInvalidResponse(Exception):
40 """OTPInvalidResponse class"""
41
42
43class OTPState:
44 """
45 OTPState class
46
47 The OTPState class represents a single state on the server side that can:
48 - generate a challenge
49 - validate the corresponding generated response from the generator
50 """
51
52 def __init__(self, ot_hex, current_step, seed, hash_algo=OTP_ALGO_MD5):
53 """
54 Constructs an OTPState object with the given arguments.
55
56 Keyword Arguments:
57 :param ot_hex: The one-time hex from the last successful
58 authentication or the first OTP of a newly
59 initialized sequence
60 :type ot_hex: str
61
62 :param current_step: The current step that is sent with the challenge
63 :type current_step: int
64
65 :param seed: The seed that is sent with the challenge
66 :type seed: str
67
68 :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5
69 :type hash_algo: int or str
70
71 :raises OTPStateException: In case the input does not validate
72 """
73 # enforce the rfc2289 constraints
74 try:
75 self._seed = OTPGenerator.validate_seed(seed)
76 self._hash_algo = OTPGenerator.validate_hash_algo(hash_algo)
77 self._step = OTPGenerator.validate_step(current_step)
78 except OTPGeneratorException as exp:
79 raise OTPStateException(exp.args[0])
80 self._current_digest = self.validate_hex(ot_hex)
81 self._new_digest_hex = None # set upon a successful validation
82
83 def __str__(self):
84 """Duplicate the challenge string"""
85 return f'otp-{self._hash_algo} {self._step} {self._seed} '
86
87 @property
88 def challenge_string(self):
89 """challenge_string-property"""
90 # RFC-2289: "...the entire challenge string MUST be
91 # terminated with either a space or a new line."
92 return f'otp-{self._hash_algo} {self._step} {self._seed} '
93
94 @property
95 def validated(self):
96 """validated-property"""
97 return bool(self._new_digest_hex)
98
99 @staticmethod
100 def response_to_bytes(response):
101 """
102 A wrapper that handles/validates the response as specified by RFC-2289.
103
104 The method first checks if response is a token and tries to convert
105 it to bytes. If that fails, the method assumes that response is a hex.
106 If neither of those attempts succeeds OTPInvalidResponse is raised.
107 It is up to the caller to run another iteration and compare the result
108 to an existing digest in this state.
109
110 :param response: The response to this state (its challenge)
111 :type response: str
112
113 :raises OTPInvalidResponse: If the response is corrupt / illegal,
114 but not if it simply does not validate
115
116 :return: The bytes representation of response (if any)
117 :rtype: bytes
118 """
119 try:
120 return OTPGenerator.tokens_to_bytes(response)
121 except OTPGeneratorException:
122 # now assume hex...
123 try:
124 return OTPState.validate_hex(response)
125 except OTPStateException:
126 raise OTPInvalidResponse(
127 'The response is neither a valid token or hex')
128
129 @staticmethod
130 def validate_hex(ot_hex):
131 """
132 Validates the provided hexidigest.
133
134 :param ot_hex: The one-time hex to validate
135 :type ot_hex: str
136
137 :raises OTPStateException: In case hex does not validate
138
139 :return: The validated hex (without leading 0x) converted to bytes
140 :rtype: bytes
141 """
142 if not isinstance(ot_hex, str):
143 raise OTPStateException('OT-hex must be a str')
144 if ot_hex.startswith('0x'):
145 ot_hex = ot_hex[2:]
146 ot_hex = ot_hex.strip().lower()
147 if len(ot_hex) != 16:
148 raise OTPStateException('The length of the hex should be 16 '
149 '(representing 64 bits digest)')
150 try:
151 return binascii.unhexlify(ot_hex)
152 except binascii.Error:
153 raise OTPStateException('Invalid OT-hex')
154
155 def response_validates(self, response, store_valid_response=True):
156 """
157 Validates the incoming response as specified by RFC-2289.
158
159 :param response: The response to this state (its challenge)
160 :type response: str
161
162 :param store_valid_response: Should a valid response be stored
163 :type store_valid_response: bool
164
165 :raises OTPInvalidResponse: If the response does not match this state
166
167 :return: Returns True if response validates, False otherwise
168 :rtype: bool
169 """
170 # self.response_to_bytes raises OTPInvalidResponse in case response
171 # is corrupt or in a wrong format
172 response_bytes = self.response_to_bytes(response)
173 if self._hash_algo == 'md5':
174 digest = hashlib.md5(response_bytes).digest()
175 if (
176 OTPGenerator.strxor(digest[0:8], digest[8:]) ==
177 self._current_digest
178 ):
179 if store_valid_response:
180 self._new_digest_hex = binascii.hexlify(
181 response_bytes).decode()
182 return True
183 return False
184 if self._hash_algo == 'sha1':
185 digest = hashlib.sha1(response_bytes).digest()
186 if (
187 OTPGenerator.sha1_digest_folding(
188 hashlib.sha1(
189 response_bytes).digest()) == self._current_digest
190 ):
191 if store_valid_response:
192 self._new_digest_hex = binascii.hexlify(
193 response_bytes).decode()
194 return True
195 return False
196 # this should not happen since the hash_algo is validated by the caller
197 raise OTPInvalidResponse(f'Ivalid hash_algo: {self._hash_algo}')