summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimeon Simeonov2022-03-21 13:25:26 +0100
committerSimeon Simeonov2022-03-21 13:25:26 +0100
commit12c5e38691b24d12a8fc9605e3b93fac86d461ae (patch)
treee27a66b696bf42b61fe2c27e864e9a73c9b57d4a
parent9179abf6c4b3e3eb2c991cbbd74738e160b910cc (diff)
Reformat the code with annotations and new code style
-rw-r--r--README.md28
-rw-r--r--otp2289/__init__.py51
-rw-r--r--otp2289/__main__.py155
-rw-r--r--otp2289/generator.py142
-rw-r--r--otp2289/server.py120
5 files changed, 296 insertions, 200 deletions
diff --git a/README.md b/README.md
index 7ba6487..a3b00c5 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
1# pyotp2289 1# pyotp2289
2 2
3pyotp2289 is a pure Python 3 implementation of "A One-Time Password System" - 3*pyotp2289* is a pure Python 3 implementation of "A One-Time Password System" -
4RFC-2289. 4RFC-2289.
5 5
6It requires no additional libraries. 6It requires no additional libraries.
@@ -30,7 +30,7 @@ I hope that somebody will find it useful.
30 30
31### FreeBSD 31### FreeBSD
32 32
33`pyotp2289` is included in the official ports-tree. 33*pyotp2289* is included in the official ports-tree.
34 34
35 ```bash 35 ```bash
36 cd /usr/ports/security/py-pyotp2289 36 cd /usr/ports/security/py-pyotp2289
@@ -41,21 +41,13 @@ I hope that somebody will find it useful.
41### Gentoo 41### Gentoo
42 42
43 ```bash 43 ```bash
44 layman -a sgs 44 # add sgs' custom repository using app-eselect/eselect-repository
45 emerge dev-python/pyotp2289 45 eselect repository add sgs
46 ```
47
48
49### Fedora ( >= 31)
50 46
51Set up a custom repo as described: https://pkg.pichove.org/Fedora/README.txt 47 # ... or using app-portage/layman (obsolete)
52 48 layman -a sgs
53Key fingerprint: A664 5797 661E 2F47 3DD3 FF06 BCE7 0555 C3BB 08F7
54
55Install the package:
56 49
57 ```bash 50 emerge dev-python/pyotp2289
58 sudo dnf install python3-pyotp2289
59 ``` 51 ```
60 52
61 53
@@ -189,7 +181,7 @@ a complete reference.
189 181
190If you don't care about developing applications in Python and only care about 182If you don't care about developing applications in Python and only care about
191generating one-time passwords (tokens / hex digests) and authenticating with 183generating one-time passwords (tokens / hex digests) and authenticating with
192existing solutions (f.i. FreeBSD servers), pyotp2289 comes with a simple CLI: 184existing solutions (f.i. FreeBSD servers), *pyotp2289* comes with a simple CLI:
193 185
194 ```bash 186 ```bash
195 python -m otp2289 --generate-otp-response -f token -i 498 -s TesT 187 python -m otp2289 --generate-otp-response -f token -i 498 -s TesT
@@ -207,7 +199,7 @@ starting from (and including) 498.
207 199
208## Support and contributing 200## Support and contributing
209 201
210pyotp2289 is hosted on GitHub: https://github.com/blackm0re/pyotp2289 202*pyotp2289* is hosted on GitHub: https://github.com/blackm0re/pyotp2289
211 203
212 204
213## Author 205## Author
@@ -217,7 +209,7 @@ Simeon Simeonov - sgs @ LiberaChat
217 209
218## [License](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE) 210## [License](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE)
219 211
220Copyright (c) 2020, Simeon Simeonov 212Copyright (c) 2020-2022 Simeon Simeonov
221All rights reserved. 213All rights reserved.
222 214
223[Licensed](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE) under the BSD 2-clause. 215[Licensed](https://github.com/blackm0re/pyotp2289/blob/master/LICENSE) under the BSD 2-clause.
diff --git a/otp2289/__init__.py b/otp2289/__init__.py
index 36d1659..608595b 100644
--- a/otp2289/__init__.py
+++ b/otp2289/__init__.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
@@ -24,20 +24,23 @@
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 RFC-2289""" 26"""A pure Python implementation of RFC-2289"""
27from .generator import (OTP_ALGO_MD5, 27from .generator import (
28 OTP_ALGO_SHA1, 28 OTP_ALGO_MD5,
29 OTPChallengeException, 29 OTP_ALGO_SHA1,
30 OTPGenerator, 30 OTPChallengeException,
31 OTPGeneratorException) 31 OTPGenerator,
32from .server import (OTPInvalidResponse, 32 OTPGeneratorException,
33 OTPState, 33)
34 OTPStateException, 34from .server import (
35 OTPStore, 35 OTPInvalidResponse,
36 OTPStoreException) 36 OTPState,
37 37 OTPStateException,
38 OTPStore,
39 OTPStoreException,
40)
38 41
39__author__ = 'Simeon Simeonov' 42__author__ = 'Simeon Simeonov'
40__version__ = '1.0.0' 43__version__ = '1.1.0-beta1'
41__license__ = 'BSD 2-Clause' 44__license__ = 'BSD 2-Clause'
42 45
43 46
@@ -51,13 +54,15 @@ def int_or_str(value):
51 54
52VERSION = tuple(map(int_or_str, __version__.split('.'))) 55VERSION = tuple(map(int_or_str, __version__.split('.')))
53 56
54__all__ = ['OTP_ALGO_MD5', 57__all__ = [
55 'OTP_ALGO_SHA1', 58 'OTP_ALGO_MD5',
56 'OTPChallengeException', 59 'OTP_ALGO_SHA1',
57 'OTPGenerator', 60 'OTPChallengeException',
58 'OTPGeneratorException', 61 'OTPGenerator',
59 'OTPInvalidResponse', 62 'OTPGeneratorException',
60 'OTPState', 63 'OTPInvalidResponse',
61 'OTPStateException', 64 'OTPState',
62 'OTPStore', 65 'OTPStateException',
63 'OTPStoreException'] 66 'OTPStore',
67 'OTPStoreException',
68]
diff --git a/otp2289/__main__.py b/otp2289/__main__.py
index 7bb0673..471ae24 100644
--- a/otp2289/__main__.py
+++ b/otp2289/__main__.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
@@ -35,6 +35,7 @@ python -m otp2289 --generate-otp-response -s TesT -i 499 -f token
35import argparse 35import argparse
36import errno 36import errno
37import getpass 37import getpass
38import io
38import os 39import os
39import secrets 40import secrets
40import string 41import string
@@ -48,7 +49,7 @@ def eprint(*arg, **kwargs):
48 print(*arg, file=sys.stderr, flush=True, **kwargs) 49 print(*arg, file=sys.stderr, flush=True, **kwargs)
49 50
50 51
51def generate_otp_response(args): 52def generate_otp_response(args: argparse.Namespace) -> str:
52 """ 53 """
53 Generates a response based on the parameters sent from the parser 54 Generates a response based on the parameters sent from the parser
54 55
@@ -65,24 +66,29 @@ def generate_otp_response(args):
65 generator = otp2289.generator.OTPGenerator( 66 generator = otp2289.generator.OTPGenerator(
66 args.password.encode(), 67 args.password.encode(),
67 args.seed, 68 args.seed,
68 args.hash_algo) 69 args.hash_algo,
70 )
69 if args.challenge_string: 71 if args.challenge_string:
70 if args.output_format == 'token': 72 if args.output_format == 'token':
71 return generator.generate_otp_words_from_challenge( 73 return generator.generate_otp_words_from_challenge(
72 args.challenge_string) 74 args.challenge_string
75 )
73 return generator.generate_otp_hexdigest_from_challenge( 76 return generator.generate_otp_hexdigest_from_challenge(
74 args.challenge_string) 77 args.challenge_string
78 )
75 # regular parameters 79 # regular parameters
76 header = '' 80 header = ''
77 if not args.quiet: 81 if not args.quiet:
78 header = (f'Seed: {args.seed}, Step: {args.step}, ' 82 header = (
79 f'Hash: {args.hash_algo}{os.linesep}') 83 f'Seed: {args.seed}, Step: {args.step}, '
84 f'Hash: {args.hash_algo}{os.linesep}'
85 )
80 if args.output_format == 'token': 86 if args.output_format == 'token':
81 return header + generator.generate_otp_words(args.step) 87 return header + generator.generate_otp_words(args.step)
82 return header + generator.generate_otp_hexdigest(args.step) 88 return header + generator.generate_otp_hexdigest(args.step)
83 89
84 90
85def generate_otp_range(args): 91def generate_otp_range(args: argparse.Namespace) -> str:
86 """ 92 """
87 Generates range of responses based on the parameters sent from the parser 93 Generates range of responses based on the parameters sent from the parser
88 94
@@ -99,7 +105,8 @@ def generate_otp_range(args):
99 generator = otp2289.generator.OTPGenerator( 105 generator = otp2289.generator.OTPGenerator(
100 args.password.encode(), 106 args.password.encode(),
101 args.seed, 107 args.seed,
102 args.hash_algo) 108 args.hash_algo,
109 )
103 if args.output_format == 'token': 110 if args.output_format == 'token':
104 method = generator.generate_otp_words 111 method = generator.generate_otp_words
105 else: 112 else:
@@ -112,25 +119,35 @@ def generate_otp_range(args):
112 # any need for quiet? 119 # any need for quiet?
113 header = '' 120 header = ''
114 if not args.quiet: 121 if not args.quiet:
115 header = (f'Seed: {args.seed}, Step: {args.step}, ' 122 header = (
116 f'Hash: {args.hash_algo}, Range: {args.range}{os.linesep}') 123 f'Seed: {args.seed}, Step: {args.step}, '
124 f'Hash: {args.hash_algo}, Range: {args.range}{os.linesep}'
125 )
117 return header + os.linesep.join( 126 return header + os.linesep.join(
118 [f'{step}: ' + method(step) for step in range( 127 [
119 args.step, args.step - args.range, -1)]) 128 f'{step}: ' + method(step)
129 for step in range(
130 args.step,
131 args.step - args.range,
132 -1,
133 )
134 ]
135 )
120 136
121 137
122def get_rnd_seed(): 138def get_rnd_seed() -> str:
123 """ 139 """
124 Returns a random seed in the format: 140 Returns a random seed in the format:
125 141
126 2 random letters (capitalize()) + 5 random digits 142 2 random letters (capitalize()) + 5 random digits
127 """ 143 """
128 rnd = secrets.SystemRandom() 144 rnd = secrets.SystemRandom()
129 return (''.join(rnd.choices(string.ascii_lowercase, k=2)).capitalize() + 145 return ''.join(
130 ''.join(rnd.choices(string.digits, k=5))) 146 rnd.choices(string.ascii_lowercase, k=2)
147 ).capitalize() + ''.join(rnd.choices(string.digits, k=5))
131 148
132 149
133def initiate_new_sequence(args): 150def initiate_new_sequence(args: argparse.Namespace) -> str:
134 """ 151 """
135 Generates a new sequence based on the parameters sent from the parser. 152 Generates a new sequence based on the parameters sent from the parser.
136 153
@@ -148,15 +165,19 @@ def initiate_new_sequence(args):
148 args.seed = get_rnd_seed() 165 args.seed = get_rnd_seed()
149 header = '' 166 header = ''
150 if not args.quiet: 167 if not args.quiet:
151 header = (f'Seed: {args.seed}, Step: {args.step}, ' 168 header = (
152 f'Hash: {args.hash_algo}{os.linesep}') 169 f'Seed: {args.seed}, Step: {args.step}, '
170 f'Hash: {args.hash_algo}{os.linesep}'
171 )
153 generator = otp2289.generator.OTPGenerator( 172 generator = otp2289.generator.OTPGenerator(
154 args.password.encode(), 173 args.password.encode(),
155 args.seed, 174 args.seed,
156 args.hash_algo) 175 args.hash_algo,
176 )
157 if args.challenge_string: 177 if args.challenge_string:
158 return header + generator.generate_otp_hexdigest_from_challenge( 178 return header + generator.generate_otp_hexdigest_from_challenge(
159 args.challenge_string) 179 args.challenge_string
180 )
160 return header + generator.generate_otp_hexdigest(args.step) 181 return header + generator.generate_otp_hexdigest(args.step)
161 182
162 183
@@ -164,92 +185,122 @@ def main(args=None):
164 """the main entry point""" 185 """the main entry point"""
165 parser = argparse.ArgumentParser( 186 parser = argparse.ArgumentParser(
166 prog=__package__, 187 prog=__package__,
167 epilog=(f'%(prog)s {otp2289.__version__} by Simeon Simeonov ' 188 epilog=(
168 '(sgs @ LiberaChat)'), 189 f'%(prog)s {otp2289.__version__} by Simeon Simeonov '
169 description='The following options are available') 190 '(sgs @ LiberaChat)'
191 ),
192 description='The following options are available',
193 )
170 group = parser.add_mutually_exclusive_group(required=True) 194 group = parser.add_mutually_exclusive_group(required=True)
171 group.add_argument( 195 group.add_argument(
172 '--generate-otp-range', 196 '--generate-otp-range',
173 action='store_true', 197 action='store_true',
174 dest='generate_otp_range', 198 dest='generate_otp_range',
175 default=False, 199 default=False,
176 help='Generates a range of OTP responses') 200 help='Generates a range of OTP responses',
201 )
177 group.add_argument( 202 group.add_argument(
178 '--generate-otp-response', 203 '--generate-otp-response',
179 action='store_true', 204 action='store_true',
180 dest='generate_otp_response', 205 dest='generate_otp_response',
181 default=False, 206 default=False,
182 help='Generates a new OTP response') 207 help='Generates a new OTP response',
208 )
183 group.add_argument( 209 group.add_argument(
184 '--initiate-new-sequence', 210 '--initiate-new-sequence',
185 action='store_true', 211 action='store_true',
186 dest='initiate_new_sequence', 212 dest='initiate_new_sequence',
187 default=False, 213 default=False,
188 help=('Initiates a new OTP sequence. Essentially the same as ' 214 help=(
189 '--generate-otp-response only it prompts twice for password ' 215 'Initiates a new OTP sequence. Essentially the same as '
190 'and always outputs hex (ignores -f).')) 216 '--generate-otp-response only it prompts twice for password '
217 'and always outputs hex (ignores -f).'
218 ),
219 )
191 parser.add_argument( 220 parser.add_argument(
192 '-a', '--hash-algorithm', 221 '-a',
222 '--hash-algorithm',
193 metavar='<md5 | sha1>', 223 metavar='<md5 | sha1>',
194 type=str, 224 type=str,
195 dest='hash_algo', 225 dest='hash_algo',
196 default='md5', 226 default='md5',
197 help='The hash algorithm to use. Possible values: md5 (default), sha1') 227 help='The hash algorithm to use. Possible values: md5 (default), sha1',
228 )
198 parser.add_argument( 229 parser.add_argument(
199 '-c', '--challenge-string', 230 '-c',
231 '--challenge-string',
200 metavar='<challenge string>', 232 metavar='<challenge string>',
201 type=str, 233 type=str,
202 dest='challenge_string', 234 dest='challenge_string',
203 default='', 235 default='',
204 help='Use challenge string when generating response') 236 help='Use challenge string when generating response',
237 )
205 parser.add_argument( 238 parser.add_argument(
206 '-f', '--output-format', 239 '-f',
240 '--output-format',
207 metavar='<hex | token>', 241 metavar='<hex | token>',
208 type=str, 242 type=str,
209 dest='output_format', 243 dest='output_format',
210 default='hex', 244 default='hex',
211 help='The output format to use. Possible values: hex (default), token') 245 help='The output format to use. Possible values: hex (default), token',
246 )
212 parser.add_argument( 247 parser.add_argument(
213 '-i', '--step', 248 '-i',
249 '--step',
214 metavar='<step>', 250 metavar='<step>',
215 type=int, 251 type=int,
216 dest='step', 252 dest='step',
217 default=500, 253 default=500,
218 help='The step. Default for initiating a new sequence is: 500') 254 help='The step. Default for initiating a new sequence is: 500',
255 )
219 parser.add_argument( 256 parser.add_argument(
220 '-p', '--password', 257 '-p',
258 '--password',
221 metavar='<PASSWORD[FILE]>', 259 metavar='<PASSWORD[FILE]>',
222 type=str, 260 type=str,
223 dest='password', 261 dest='password',
224 default='', 262 default='',
225 help=('The password or path to password file ' 263 help=(
226 '(default & recommended: prompt for passwd)')) 264 'The password or path to password file '
265 '(default & recommended: prompt for passwd)'
266 ),
267 )
227 parser.add_argument( 268 parser.add_argument(
228 '-q', '--quiet', 269 '-q',
270 '--quiet',
229 action='store_true', 271 action='store_true',
230 dest='quiet', 272 dest='quiet',
231 default=False, 273 default=False,
232 help='Dot not show headers. Only hex / tokens') 274 help='Dot not show headers. Only hex / tokens',
275 )
233 parser.add_argument( 276 parser.add_argument(
234 '-r', '--range', 277 '-r',
278 '--range',
235 metavar='<range>', 279 metavar='<range>',
236 type=int, 280 type=int,
237 dest='range', 281 dest='range',
238 default=1, 282 default=1,
239 help='Amount of consecutive OTP hex/tokens to generate. default: 1') 283 help='Amount of consecutive OTP hex/tokens to generate. default: 1',
284 )
240 parser.add_argument( 285 parser.add_argument(
241 '-s', '--seed', 286 '-s',
287 '--seed',
242 metavar='[seed]', 288 metavar='[seed]',
243 type=str, 289 type=str,
244 dest='seed', 290 dest='seed',
245 default='', 291 default='',
246 help=('The seed to use (1 to 16 alphanumeric characters) ' 292 help=(
247 '(default & recommended: random seed)')) 293 'The seed to use (1 to 16 alphanumeric characters) '
294 '(default & recommended: random seed)'
295 ),
296 )
248 parser.add_argument( 297 parser.add_argument(
249 '-v', '--version', 298 '-v',
299 '--version',
250 action='version', 300 action='version',
251 version=f'%(prog)s {otp2289.__version__}', 301 version=f'%(prog)s {otp2289.__version__}',
252 help='display program-version and exit') 302 help='display program-version and exit',
303 )
253 args = parser.parse_args(args) 304 args = parser.parse_args(args)
254 # handle the password before everything else 305 # handle the password before everything else
255 if not args.password: 306 if not args.password:
@@ -257,8 +308,8 @@ def main(args=None):
257 while True: 308 while True:
258 args.password = getpass.getpass() 309 args.password = getpass.getpass()
259 if ( 310 if (
260 not args.initiate_new_sequence or 311 not args.initiate_new_sequence
261 args.password == getpass.getpass('Repeat password: ') 312 or args.password == getpass.getpass('Repeat password: ')
262 ): 313 ):
263 break 314 break
264 eprint('The passwords do not match') 315 eprint('The passwords do not match')
@@ -267,7 +318,7 @@ def main(args=None):
267 sys.exit(errno.EACCES) 318 sys.exit(errno.EACCES)
268 elif os.path.isfile(args.password): 319 elif os.path.isfile(args.password):
269 try: 320 try:
270 with open(args.password, 'r') as fp: 321 with io.open(args.password, 'r', encoding='utf-8') as fp:
271 args.password = fp.readline().strip() 322 args.password = fp.readline().strip()
272 except Exception as exp: 323 except Exception as exp:
273 eprint(f'Unable to open password file: {exp}') 324 eprint(f'Unable to open password file: {exp}')
diff --git a/otp2289/generator.py b/otp2289/generator.py
index 5ea738b..7b86e1b 100644
--- a/otp2289/generator.py
+++ b/otp2289/generator.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
@@ -28,7 +28,6 @@ import binascii
28import hashlib 28import hashlib
29import string 29import string
30 30
31
32OTP_ALGO_MD5 = 1 31OTP_ALGO_MD5 = 1
33OTP_ALGO_SHA1 = 2 32OTP_ALGO_SHA1 = 2
34 33
@@ -306,7 +305,12 @@ class OTPChallengeException(Exception):
306class OTPGenerator: 305class OTPGenerator:
307 """OTPGenerator class""" 306 """OTPGenerator class"""
308 307
309 def __init__(self, password, seed='', hash_algo=OTP_ALGO_MD5): 308 def __init__(
309 self,
310 password: bytes,
311 seed: str = '',
312 hash_algo=OTP_ALGO_MD5,
313 ):
310 """ 314 """
311 Constructs an OTPGenerator object with a given password and seed. 315 Constructs an OTPGenerator object with a given password and seed.
312 316
@@ -330,16 +334,19 @@ class OTPGenerator:
330 raise OTPGeneratorException('Password must be a byte-string') 334 raise OTPGeneratorException('Password must be a byte-string')
331 if len(password) < 10: 335 if len(password) < 10:
332 raise OTPGeneratorException( 336 raise OTPGeneratorException(
333 'Password must be longer than 10 bytes') 337 'Password must be longer than 10 bytes'
338 )
334 self._password = password 339 self._password = password
335 340
336 def __repr__(self): 341 def __repr__(self):
337 """repr implementation""" 342 """repr implementation"""
338 return (f'{self.__class__} at {id(self)} (seed={self._seed}, ' 343 return (
339 f'hash_algo={self._hash_algo})') 344 f'{self.__class__} at {id(self)} (seed={self._seed}, '
345 f'hash_algo={self._hash_algo})'
346 )
340 347
341 @staticmethod 348 @staticmethod
342 def bit_pair_sum(bit_stream): 349 def bit_pair_sum(bit_stream: str) -> int:
343 """ 350 """
344 Split bit_stream in bit-pairs and sum them all together. 351 Split bit_stream in bit-pairs and sum them all together.
345 352
@@ -359,7 +366,7 @@ class OTPGenerator:
359 return value 366 return value
360 367
361 @staticmethod 368 @staticmethod
362 def bytes_to_tokens(hash_bytes): 369 def bytes_to_tokens(hash_bytes: bytes) -> str:
363 """ 370 """
364 Returns a 6 words token from bytes as specified by RFC-2289. 371 Returns a 6 words token from bytes as specified by RFC-2289.
365 372
@@ -369,8 +376,7 @@ class OTPGenerator:
369 :return: 6 words tokens 376 :return: 6 words tokens
370 :rtype: str 377 :rtype: str
371 """ 378 """
372 bit_stream = ''.join( 379 bit_stream = ''.join([f'{byte:0>8b}' for byte in hash_bytes])
373 ['{0:0>8b}'.format(byte) for byte in hash_bytes])
374 bit_pair_sum = OTPGenerator.bit_pair_sum(bit_stream) 380 bit_pair_sum = OTPGenerator.bit_pair_sum(bit_stream)
375 tokens = [] 381 tokens = []
376 tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)]) 382 tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)])
@@ -379,12 +385,17 @@ class OTPGenerator:
379 tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)]) 385 tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)])
380 tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)]) 386 tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)])
381 tokens.append( 387 tokens.append(
382 RFC1760_TOKENS[int( 388 RFC1760_TOKENS[
383 bit_stream[55:64] + '{0:0>8b}'.format(bit_pair_sum)[-2:], 2)]) 389 int(
390 bit_stream[55:64] + f'{bit_pair_sum:0>8b}'[-2:],
391 2,
392 )
393 ]
394 )
384 return ' '.join(tokens) 395 return ' '.join(tokens)
385 396
386 @staticmethod 397 @staticmethod
387 def get_tokens_from_challenge(challenge): 398 def get_tokens_from_challenge(challenge: str) -> tuple:
388 """ 399 """
389 Returns tokens (seed, hash_algo and step) from a challenge string. 400 Returns tokens (seed, hash_algo and step) from a challenge string.
390 401
@@ -410,7 +421,7 @@ class OTPGenerator:
410 raise OTPChallengeException('Invalid challenge') from None 421 raise OTPChallengeException('Invalid challenge') from None
411 422
412 @staticmethod 423 @staticmethod
413 def sha1_digest_folding(sha1_digest): 424 def sha1_digest_folding(sha1_digest: bytes) -> bytes:
414 """ 425 """
415 Implementation of the 160bit -> 64bit folding algorithm 426 Implementation of the 160bit -> 64bit folding algorithm
416 for sha1 digest. 427 for sha1 digest.
@@ -425,14 +436,17 @@ class OTPGenerator:
425 raise OTPGeneratorException('sha1_digest must be of type bytes') 436 raise OTPGeneratorException('sha1_digest must be of type bytes')
426 if len(sha1_digest) != 20: 437 if len(sha1_digest) != 20:
427 raise OTPGeneratorException( 438 raise OTPGeneratorException(
428 'sha1_digest must be 160 bits (20 bytes) long') 439 'sha1_digest must be 160 bits (20 bytes) long'
440 )
429 digested = list(5 * b'i') # 5 bytes (40 bits) 441 digested = list(5 * b'i') # 5 bytes (40 bits)
430 result = list(8 * b'x') # 8 bytes (64 bits) 442 result = list(8 * b'x') # 8 bytes (64 bits)
431 for i in range(5): 443 for i in range(5):
432 digested[i] = (((sha1_digest[i * 4 + 0] & 0xff) << 24) | 444 digested[i] = (
433 ((sha1_digest[i * 4 + 1] & 0xff) << 16) | 445 ((sha1_digest[i * 4 + 0] & 0xFF) << 24)
434 ((sha1_digest[i * 4 + 2] & 0xff) << 8) | 446 | ((sha1_digest[i * 4 + 1] & 0xFF) << 16)
435 (sha1_digest[i * 4 + 3] & 0xff)) 447 | ((sha1_digest[i * 4 + 2] & 0xFF) << 8)
448 | (sha1_digest[i * 4 + 3] & 0xFF)
449 )
436 # sha.digest[0] ^= sha.digest[2]; 450 # sha.digest[0] ^= sha.digest[2];
437 # sha.digest[1] ^= sha.digest[3]; 451 # sha.digest[1] ^= sha.digest[3];
438 # sha.digest[0] ^= sha.digest[4]; 452 # sha.digest[0] ^= sha.digest[4];
@@ -446,18 +460,18 @@ class OTPGenerator:
446 # result[j+3] = (unsigned char)((sha.digest[i] >> 24) & 0xff); 460 # result[j+3] = (unsigned char)((sha.digest[i] >> 24) & 0xff);
447 # } 461 # }
448 # just hardcoding the two iterations for better efficiency 462 # just hardcoding the two iterations for better efficiency
449 result[0] = digested[0] & 0xff 463 result[0] = digested[0] & 0xFF
450 result[1] = (digested[0] >> 8) & 0xff 464 result[1] = (digested[0] >> 8) & 0xFF
451 result[2] = (digested[0] >> 16) & 0xff 465 result[2] = (digested[0] >> 16) & 0xFF
452 result[3] = (digested[0] >> 24) & 0xff 466 result[3] = (digested[0] >> 24) & 0xFF
453 result[4] = digested[1] & 0xff 467 result[4] = digested[1] & 0xFF
454 result[5] = (digested[1] >> 8) & 0xff 468 result[5] = (digested[1] >> 8) & 0xFF
455 result[6] = (digested[1] >> 16) & 0xff 469 result[6] = (digested[1] >> 16) & 0xFF
456 result[7] = (digested[1] >> 24) & 0xff 470 result[7] = (digested[1] >> 24) & 0xFF
457 return bytes(result) 471 return bytes(result)
458 472
459 @staticmethod 473 @staticmethod
460 def strxor(byte_str1, byte_str2): 474 def strxor(byte_str1: bytes, byte_str2: bytes) -> bytes:
461 """ 475 """
462 Implementation of strxor similar to the one provided by pycrypto. 476 Implementation of strxor similar to the one provided by pycrypto.
463 477
@@ -472,17 +486,17 @@ class OTPGenerator:
472 """ 486 """
473 if not (isinstance(byte_str1, bytes) and isinstance(byte_str2, bytes)): 487 if not (isinstance(byte_str1, bytes) and isinstance(byte_str2, bytes)):
474 raise OTPGeneratorException( 488 raise OTPGeneratorException(
475 'byte_str1 and byte_str2 must be of type bytes') 489 'byte_str1 and byte_str2 must be of type bytes'
490 )
476 length = len(byte_str1) 491 length = len(byte_str1)
477 if length != len(byte_str2) or length < 1: 492 if length != len(byte_str2) or length < 1:
478 raise OTPGeneratorException( 493 raise OTPGeneratorException(
479 'byte_str1 and byte_str2 must be of the same length > 0') 494 'byte_str1 and byte_str2 must be of the same length > 0'
480 return bytes( 495 )
481 [byte_str1[i] ^ byte_str2[i] for i in range(length)] 496 return bytes([byte_str1[i] ^ byte_str2[i] for i in range(length)])
482 )
483 497
484 @staticmethod 498 @staticmethod
485 def tokens_to_bytes(tokens_str): 499 def tokens_to_bytes(tokens_str: str) -> bytes:
486 """ 500 """
487 Returns bytes from a 6 words token as specified by RFC-2289. 501 Returns bytes from a 6 words token as specified by RFC-2289.
488 502
@@ -499,14 +513,17 @@ class OTPGenerator:
499 tokens = tokens_str.split() 513 tokens = tokens_str.split()
500 if len(tokens) != 6: 514 if len(tokens) != 6:
501 raise OTPGeneratorException( 515 raise OTPGeneratorException(
502 'Tokens-string does not contain 6 tokens') 516 'Tokens-string does not contain 6 tokens'
517 )
503 token_ints = [] 518 token_ints = []
504 try: 519 try:
505 token_ints = [RFC1760_TOKENS.index(token.upper()) for token in 520 token_ints = [
506 tokens] 521 RFC1760_TOKENS.index(token.upper()) for token in tokens
522 ]
507 except ValueError: 523 except ValueError:
508 raise OTPGeneratorException( 524 raise OTPGeneratorException(
509 'One or more words not present in RFC1760') from None 525 'One or more words not present in RFC1760'
526 ) from None
510 # now we build a string of bits 527 # now we build a string of bits
511 bit_stream = format(token_ints[0], '011b') 528 bit_stream = format(token_ints[0], '011b')
512 bit_stream += format(token_ints[1], '011b') 529 bit_stream += format(token_ints[1], '011b')
@@ -519,14 +536,14 @@ class OTPGenerator:
519 # OTP servers MUST verify this checksum explicitly as part of the 536 # OTP servers MUST verify this checksum explicitly as part of the
520 # operation of decoding this representation of the one-time password. 537 # operation of decoding this representation of the one-time password.
521 if ( 538 if (
522 '{0:0>8b}'.format(OTPGenerator.bit_pair_sum( 539 f'{OTPGenerator.bit_pair_sum(bit_stream[:64]):0>8b}'[-2:]
523 bit_stream[:64]))[-2:] != bit_stream[-2:] 540 != bit_stream[-2:]
524 ): 541 ):
525 raise OTPGeneratorException('Invalid bit checksum') 542 raise OTPGeneratorException('Invalid bit checksum')
526 return int(bit_stream[:64], 2).to_bytes(8, 'big') 543 return int(bit_stream[:64], 2).to_bytes(8, 'big')
527 544
528 @staticmethod 545 @staticmethod
529 def validate_hash_algo(hash_algo): 546 def validate_hash_algo(hash_algo) -> str:
530 """ 547 """
531 Validates the provided hash-algorithm. 548 Validates the provided hash-algorithm.
532 549
@@ -541,19 +558,20 @@ class OTPGenerator:
541 if isinstance(hash_algo, int): 558 if isinstance(hash_algo, int):
542 if hash_algo not in _ALGO_DICT: 559 if hash_algo not in _ALGO_DICT:
543 raise OTPGeneratorException( 560 raise OTPGeneratorException(
544 'hash_algo is not among the known algorithms') 561 'hash_algo is not among the known algorithms'
562 )
545 hash_algo = _ALGO_DICT.get(hash_algo) 563 hash_algo = _ALGO_DICT.get(hash_algo)
546 if not isinstance(hash_algo, str): 564 if not isinstance(hash_algo, str):
547 raise OTPGeneratorException( 565 raise OTPGeneratorException('hash_algo must be an int or a str')
548 'hash_algo must be an int or a str')
549 if hash_algo not in hashlib.algorithms_available: 566 if hash_algo not in hashlib.algorithms_available:
550 raise OTPGeneratorException( 567 raise OTPGeneratorException(
551 f'{hash_algo} is not supported by this version of the ' 568 f'{hash_algo} is not supported by this version of the '
552 'hashlib module') 569 'hashlib module'
570 )
553 return hash_algo 571 return hash_algo
554 572
555 @staticmethod 573 @staticmethod
556 def validate_seed(seed): 574 def validate_seed(seed: str) -> str:
557 """ 575 """
558 Validates the provided seed as defined by RFC-2289. 576 Validates the provided seed as defined by RFC-2289.
559 577
@@ -569,15 +587,17 @@ class OTPGenerator:
569 raise OTPGeneratorException('Seed must be a string') 587 raise OTPGeneratorException('Seed must be a string')
570 if not seed or len(seed) > 16: 588 if not seed or len(seed) > 16:
571 raise OTPGeneratorException( 589 raise OTPGeneratorException(
572 'The seed MUST be of 1 to 16 characters in length') 590 'The seed MUST be of 1 to 16 characters in length'
591 )
573 for char in seed: 592 for char in seed:
574 if char not in string.ascii_letters + string.digits: 593 if char not in string.ascii_letters + string.digits:
575 raise OTPGeneratorException( 594 raise OTPGeneratorException(
576 'The seed MUST consist of purely alphanumeric characters') 595 'The seed MUST consist of purely alphanumeric characters'
596 )
577 return seed 597 return seed
578 598
579 @staticmethod 599 @staticmethod
580 def validate_step(step): 600 def validate_step(step: int) -> int:
581 """ 601 """
582 Validates the provided step as defined by RFC-2289. 602 Validates the provided step as defined by RFC-2289.
583 603
@@ -595,7 +615,7 @@ class OTPGenerator:
595 raise OTPGeneratorException('Step value MUST be >= 0') 615 raise OTPGeneratorException('Step value MUST be >= 0')
596 return step 616 return step
597 617
598 def generate_otp_hexdigest(self, step): 618 def generate_otp_hexdigest(self, step: int) -> str:
599 """ 619 """
600 Generates the OTP hexdigest for the given step. 620 Generates the OTP hexdigest for the given step.
601 621
@@ -607,7 +627,7 @@ class OTPGenerator:
607 """ 627 """
608 return '0x' + binascii.hexlify(self._generate_otp_bytes(step)).decode() 628 return '0x' + binascii.hexlify(self._generate_otp_bytes(step)).decode()
609 629
610 def generate_otp_hexdigest_from_challenge(self, challenge): 630 def generate_otp_hexdigest_from_challenge(self, challenge: str) -> str:
611 """ 631 """
612 Same as generate_otp_hexdigest, but it generates hex. from a challenge. 632 Same as generate_otp_hexdigest, but it generates hex. from a challenge.
613 633
@@ -628,7 +648,7 @@ class OTPGenerator:
628 self._hash_algo = self.validate_hash_algo(hash_algo) 648 self._hash_algo = self.validate_hash_algo(hash_algo)
629 return self.generate_otp_hexdigest(step) 649 return self.generate_otp_hexdigest(step)
630 650
631 def generate_otp_words(self, step): 651 def generate_otp_words(self, step: int) -> str:
632 """ 652 """
633 Generates the OTP six words token for the given step. 653 Generates the OTP six words token for the given step.
634 654
@@ -640,7 +660,7 @@ class OTPGenerator:
640 """ 660 """
641 return self.bytes_to_tokens(self._generate_otp_bytes(step)) 661 return self.bytes_to_tokens(self._generate_otp_bytes(step))
642 662
643 def generate_otp_words_from_challenge(self, challenge): 663 def generate_otp_words_from_challenge(self, challenge: str) -> str:
644 """ 664 """
645 Same as generate_otp_words, but it generates words from a challenge. 665 Same as generate_otp_words, but it generates words from a challenge.
646 666
@@ -661,7 +681,7 @@ class OTPGenerator:
661 self._hash_algo = self.validate_hash_algo(hash_algo) 681 self._hash_algo = self.validate_hash_algo(hash_algo)
662 return self.generate_otp_words(step) 682 return self.generate_otp_words(step)
663 683
664 def hexdigest_range(self, start=499, stop=0): 684 def hexdigest_range(self, start: int = 499, stop: int = 0):
665 """ 685 """
666 Returns an iterator that providing hexdigests corresponding to steps 686 Returns an iterator that providing hexdigests corresponding to steps
667 from `start` to and including `stop`. 687 from `start` to and including `stop`.
@@ -679,11 +699,12 @@ class OTPGenerator:
679 raise OTPGeneratorException('Step value MUST be an int') 699 raise OTPGeneratorException('Step value MUST be an int')
680 if start < stop: 700 if start < stop:
681 raise OTPGeneratorException( 701 raise OTPGeneratorException(
682 'Start value can not be lower than stop') 702 'Start value can not be lower than stop'
703 )
683 for step in range(start, stop - 1, -1): 704 for step in range(start, stop - 1, -1):
684 yield self.generate_otp_hexdigest(step) 705 yield self.generate_otp_hexdigest(step)
685 706
686 def words_range(self, start=499, stop=0): 707 def words_range(self, start: int = 499, stop: int = 0):
687 """ 708 """
688 Returns an iterator that providing the words corresponding to steps 709 Returns an iterator that providing the words corresponding to steps
689 from `start` to and including `stop`. 710 from `start` to and including `stop`.
@@ -701,11 +722,12 @@ class OTPGenerator:
701 raise OTPGeneratorException('Step value MUST be an int') 722 raise OTPGeneratorException('Step value MUST be an int')
702 if start < stop: 723 if start < stop:
703 raise OTPGeneratorException( 724 raise OTPGeneratorException(
704 'Start value can not be lower than stop') 725 'Start value can not be lower than stop'
726 )
705 for step in range(start, stop - 1, -1): 727 for step in range(start, stop - 1, -1):
706 yield self.generate_otp_words(step) 728 yield self.generate_otp_words(step)
707 729
708 def _generate_otp_bytes(self, step): 730 def _generate_otp_bytes(self, step: int) -> bytes:
709 """ 731 """
710 Generates the OTP bytes for the given step. 732 Generates the OTP bytes for the given step.
711 733
@@ -733,6 +755,6 @@ class OTPGenerator:
733 digest = self.sha1_digest_folding(large_digest) 755 digest = self.sha1_digest_folding(large_digest)
734 else: 756 else:
735 raise OTPGeneratorException( 757 raise OTPGeneratorException(
736 '{hash_algo} is not supported by this module'.format( 758 f'{self._hash_algo} is not supported by this module'
737 hash_algo=self._hash_algo)) 759 )
738 return digest 760 return digest
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