summaryrefslogtreecommitdiff
path: root/otp2289
diff options
context:
space:
mode:
Diffstat (limited to 'otp2289')
-rw-r--r--otp2289/__init__.py51
-rw-r--r--otp2289/__main__.py155
-rw-r--r--otp2289/generator.py142
-rw-r--r--otp2289/server.py120
4 files changed, 286 insertions, 182 deletions
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