summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSimeon Simeonov2020-03-30 21:57:53 +0200
committerSimeon Simeonov2020-03-30 21:57:53 +0200
commit2a6299c32e0baf3df06f8e6d6bc8695b951d630d (patch)
treeb4bd18092822db0fd10411e49dc6e05d1a60e697
parenta4c050e34ce462f477bc3671e272e244e2792e00 (diff)
Implement OTPState in the server module
-rw-r--r--README.md24
-rw-r--r--otp2289/__init__.py6
-rw-r--r--otp2289/generator.py118
-rw-r--r--otp2289/server.py171
-rw-r--r--tests/test_generator.py20
-rw-r--r--tests/test_server.py84
-rw-r--r--tests/test_static.py79
7 files changed, 467 insertions, 35 deletions
diff --git a/README.md b/README.md
index fef620a..ac3bca9 100644
--- a/README.md
+++ b/README.md
@@ -57,10 +57,32 @@ one-time password it received, and must store the corresponding one-
57time password sequence number. The server must also facilitate the 57time password sequence number. The server must also facilitate the
58changing of the user's secret pass-phrase in a secure manner." 58changing of the user's secret pass-phrase in a secure manner."
59 59
60"The OTP system generator passes the user's secret pass-phrase, along
61with a seed received from the server as part of the challenge,
62through multiple iterations of a secure hash function to produce a
63one-time password. After each successful authentication, the number
64of secure hash function iterations is reduced by one. Thus, a unique
65sequence of passwords is generated. The server verifies the one-time
66password received from the generator by computing the secure hash
67function once and comparing the result with the previously accepted
68one-time password."
69
60 70
61## Examples 71## Examples
62 72
63TODO 73 ```python
74 import getpass
75
76 import otp2289
77
78 # create a generator object
79 passwd_bytes = getpass.getpass().encode() # Type: This is a test.
80 generator = otp2289.generator.OTPGenerator(passwd_bytes,
81 'TesT',
82 otp2289.OTP_ALGO_MD5)
83 generator.generate_otp_hexdigest(0)
84 generator.gen.generate_otp_words(0)
85 ```
64 86
65 87
66## Author 88## Author
diff --git a/otp2289/__init__.py b/otp2289/__init__.py
index 44c39bb..2fba4ea 100644
--- a/otp2289/__init__.py
+++ b/otp2289/__init__.py
@@ -29,6 +29,7 @@ from .generator import (OTP_ALGO_MD5,
29 OTPChallengeException, 29 OTPChallengeException,
30 OTPGenerator, 30 OTPGenerator,
31 OTPGeneratorException) 31 OTPGeneratorException)
32from .server import OTPInvalidResponse, OTPState, OTPStateException
32 33
33 34
34__author__ = 'Simeon Simeonov' 35__author__ = 'Simeon Simeonov'
@@ -50,4 +51,7 @@ __all__ = ['OTP_ALGO_MD5',
50 'OTP_ALGO_SHA1', 51 'OTP_ALGO_SHA1',
51 'OTPChallengeException', 52 'OTPChallengeException',
52 'OTPGenerator', 53 'OTPGenerator',
53 'OTPGeneratorException'] 54 'OTPGeneratorException',
55 'OTPInvalidResponse',
56 'OTPState',
57 'OTPStateException']
diff --git a/otp2289/generator.py b/otp2289/generator.py
index 7001014..ac3bb3c 100644
--- a/otp2289/generator.py
+++ b/otp2289/generator.py
@@ -320,19 +320,19 @@ class OTPGenerator:
320 :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5 320 :param hash_algo: The hash algo, defaults to OTP_ALGO_MD5
321 :type hash_algo: int or str 321 :type hash_algo: int or str
322 322
323 :raises OTPGeneratorException: In case input does not validate 323 :raises OTPGeneratorException: In case the input does not validate
324 """ 324 """
325 # enforce the rfc2289 constraints 325 # enforce the rfc2289 constraints
326 self._seed = seed
327 if self._seed: # the seed was set here. Validate it
328 self._seed = self.validate_seed(self._seed)
329 self._hash_algo = self.validate_hash_algo(hash_algo)
326 if not isinstance(password, bytes): 330 if not isinstance(password, bytes):
327 raise OTPGeneratorException('Password must be a byte-string') 331 raise OTPGeneratorException('Password must be a byte-string')
328 if len(password) < 10: 332 if len(password) < 10:
329 raise OTPGeneratorException( 333 raise OTPGeneratorException(
330 'Password must be longer than 10 bytes') 334 'Password must be longer than 10 bytes')
331 self._password = password 335 self._password = password
332 self._seed = seed
333 if self._seed: # the seed was set here. Validate it
334 self._seed = self.validate_seed(self._seed)
335 self._hash_algo = self.validate_hash_algo(hash_algo)
336 336
337 @staticmethod 337 @staticmethod
338 def bit_pair_sum(bit_stream): 338 def bit_pair_sum(bit_stream):
@@ -355,6 +355,31 @@ class OTPGenerator:
355 return value 355 return value
356 356
357 @staticmethod 357 @staticmethod
358 def bytes_to_tokens(hash_bytes):
359 """
360 Returns a 6 words token from bytes as specified by RFC-2289.
361
362 :param hash_bytes: The input bytes
363 :type hash_bytes: bytes
364
365 :return: 6 words tokens
366 :rtype: str
367 """
368 bit_stream = ''.join(
369 ['{0:0>8b}'.format(byte) for byte in hash_bytes])
370 bit_pair_sum = OTPGenerator.bit_pair_sum(bit_stream)
371 tokens = []
372 tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)])
373 tokens.append(RFC1760_TOKENS[int(bit_stream[11:22], 2)])
374 tokens.append(RFC1760_TOKENS[int(bit_stream[22:33], 2)])
375 tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)])
376 tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)])
377 tokens.append(
378 RFC1760_TOKENS[int(
379 bit_stream[55:64] + '{0:0>8b}'.format(bit_pair_sum)[-2:], 2)])
380 return ' '.join(tokens)
381
382 @staticmethod
358 def get_tokens_from_challenge(challenge): 383 def get_tokens_from_challenge(challenge):
359 """ 384 """
360 Returns tokens (seed, hash_algo and step) from a challenge string. 385 Returns tokens (seed, hash_algo and step) from a challenge string.
@@ -453,6 +478,50 @@ class OTPGenerator:
453 ) 478 )
454 479
455 @staticmethod 480 @staticmethod
481 def tokens_to_bytes(tokens_str):
482 """
483 Returns bytes from a 6 words token as specified by RFC-2289.
484
485 :param tokens_str: String representing 6 words tokens
486 :type tokens_str: str
487
488 :raises OTPGeneratorException: When the tokens_str is invalid
489
490 :return: 6 words tokens
491 :rtype: bytes
492 """
493 if not isinstance(tokens_str, str):
494 raise OTPGeneratorException('tokens must be a str')
495 tokens = tokens_str.split()
496 if len(tokens) != 6:
497 raise OTPGeneratorException(
498 'Tokens-string does not contain 6 tokens')
499 token_ints = []
500 try:
501 token_ints = [RFC1760_TOKENS.index(token.upper()) for token in
502 tokens]
503 except ValueError:
504 raise OTPGeneratorException(
505 'One or more words not present in RFC1760')
506 # now we build a string of bits
507 bit_stream = format(token_ints[0], '011b')
508 bit_stream += format(token_ints[1], '011b')
509 bit_stream += format(token_ints[2], '011b')
510 bit_stream += format(token_ints[3], '011b')
511 bit_stream += format(token_ints[4], '011b')
512 bit_stream += format(token_ints[5], '011b')
513 # we have 66 bits: 64 digest + 2 bit pair sum (control number)
514 # RFC-2289: All OTP generators MUST calculate this checksum and all
515 # OTP servers MUST verify this checksum explicitly as part of the
516 # operation of decoding this representation of the one-time password.
517 if (
518 '{0:0>8b}'.format(OTPGenerator.bit_pair_sum(
519 bit_stream[:64]))[-2:] != bit_stream[-2:]
520 ):
521 raise OTPGeneratorException('Invalid bit checksum')
522 return int(bit_stream[:64], 2).to_bytes(8, 'big')
523
524 @staticmethod
456 def validate_hash_algo(hash_algo): 525 def validate_hash_algo(hash_algo):
457 """ 526 """
458 Validates the provided hash-algorithm. 527 Validates the provided hash-algorithm.
@@ -503,6 +572,25 @@ class OTPGenerator:
503 'The seed MUST consist of purely alphanumeric characters') 572 'The seed MUST consist of purely alphanumeric characters')
504 return seed 573 return seed
505 574
575 @staticmethod
576 def validate_step(step):
577 """
578 Validates the provided step as defined by RFC-2289.
579
580 :param seed: The step received from the challenge
581 :type seed: int
582
583 :raises OTPGeneratorException: In case step does not validate
584
585 :return: The validated (and very same) step
586 :rtype: int
587 """
588 if not isinstance(step, int):
589 raise OTPGeneratorException('Step value MUST be an int')
590 if step < 0:
591 raise OTPGeneratorException('Step value MUST be >= 0')
592 return step
593
506 def generate_otp_hexdigest(self, step): 594 def generate_otp_hexdigest(self, step):
507 """ 595 """
508 Generates the OTP hexdigest for the given step. 596 Generates the OTP hexdigest for the given step.
@@ -549,20 +637,7 @@ class OTPGenerator:
549 :return: Six words (separated by single space) token for the given step 637 :return: Six words (separated by single space) token for the given step
550 :rtype: str 638 :rtype: str
551 """ 639 """
552 digest = self._generate_otp_bytes(step) 640 return self.bytes_to_tokens(self._generate_otp_bytes(step))
553 bit_stream = ''.join(
554 ['{0:0>8b}'.format(byte) for byte in digest])
555 bit_pair_sum = self.bit_pair_sum(bit_stream)
556 tokens = list()
557 tokens.append(RFC1760_TOKENS[int(bit_stream[:11], 2)])
558 tokens.append(RFC1760_TOKENS[int(bit_stream[11:22], 2)])
559 tokens.append(RFC1760_TOKENS[int(bit_stream[22:33], 2)])
560 tokens.append(RFC1760_TOKENS[int(bit_stream[33:44], 2)])
561 tokens.append(RFC1760_TOKENS[int(bit_stream[44:55], 2)])
562 tokens.append(
563 RFC1760_TOKENS[int(
564 bit_stream[55:64] + '{0:0>8b}'.format(bit_pair_sum)[-2:], 2)])
565 return ' '.join(tokens)
566 641
567 def generate_otp_words_from_challenge(self, challenge): 642 def generate_otp_words_from_challenge(self, challenge):
568 """ 643 """
@@ -641,10 +716,7 @@ class OTPGenerator:
641 :return: The digest bytes for the given step 716 :return: The digest bytes for the given step
642 :rtype: bytes 717 :rtype: bytes
643 """ 718 """
644 if not isinstance(step, int): 719 step = self.validate_step(step)
645 raise OTPGeneratorException('Step value MUST be an int')
646 if step < 0:
647 raise OTPGeneratorException('Step value MUST be >= 0')
648 digest = b'' 720 digest = b''
649 for _ in range(step + 1): 721 for _ in range(step + 1):
650 hash_obj = hashlib.new(self._hash_algo) 722 hash_obj = hashlib.new(self._hash_algo)
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}')
diff --git a/tests/test_generator.py b/tests/test_generator.py
index 74fc3a4..50274e2 100644
--- a/tests/test_generator.py
+++ b/tests/test_generator.py
@@ -62,16 +62,6 @@ def test_constructor_exceptions():
62 """ 62 """
63 # test the otp2289.OTPGenerator __init__ and validators 63 # test the otp2289.OTPGenerator __init__ and validators
64 with pytest.raises(otp2289.OTPGeneratorException) as exc_info: 64 with pytest.raises(otp2289.OTPGeneratorException) as exc_info:
65 otp2289.OTPGenerator('1234567', 'TeStø'.encode(), otp2289.OTP_ALGO_MD5)
66 assert exc_info.type is otp2289.OTPGeneratorException
67 assert exc_info.value.args[0] == 'Password must be a byte-string'
68 with pytest.raises(otp2289.OTPGeneratorException) as exc_info:
69 otp2289.OTPGenerator('1234567'.encode(),
70 'TeStø'.encode(),
71 otp2289.OTP_ALGO_MD5)
72 assert exc_info.type is otp2289.OTPGeneratorException
73 assert exc_info.value.args[0] == 'Password must be longer than 10 bytes'
74 with pytest.raises(otp2289.OTPGeneratorException) as exc_info:
75 otp2289.OTPGenerator('This is a test.'.encode(), 65 otp2289.OTPGenerator('This is a test.'.encode(),
76 'TeStø'.encode(), 66 'TeStø'.encode(),
77 otp2289.OTP_ALGO_MD5) 67 otp2289.OTP_ALGO_MD5)
@@ -108,6 +98,16 @@ def test_constructor_exceptions():
108 assert exc_info.type is otp2289.generator.OTPGeneratorException 98 assert exc_info.type is otp2289.generator.OTPGeneratorException
109 assert exc_info.value.args[0] == ('foo is not supported by this version ' 99 assert exc_info.value.args[0] == ('foo is not supported by this version '
110 'of the hashlib module') 100 'of the hashlib module')
101 with pytest.raises(otp2289.OTPGeneratorException) as exc_info:
102 otp2289.OTPGenerator('1234567', 'TeSt', otp2289.OTP_ALGO_MD5)
103 assert exc_info.type is otp2289.OTPGeneratorException
104 assert exc_info.value.args[0] == 'Password must be a byte-string'
105 with pytest.raises(otp2289.OTPGeneratorException) as exc_info:
106 otp2289.OTPGenerator('1234567'.encode(),
107 'TeSt',
108 otp2289.OTP_ALGO_MD5)
109 assert exc_info.type is otp2289.OTPGeneratorException
110 assert exc_info.value.args[0] == 'Password must be longer than 10 bytes'
111 111
112 112
113def test_md5(): 113def test_md5():
diff --git a/tests/test_server.py b/tests/test_server.py
new file mode 100644
index 0000000..c7cdb12
--- /dev/null
+++ b/tests/test_server.py
@@ -0,0 +1,84 @@
1# -*- coding: utf-8 -*-
2# SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3#
4# Copyright (c) 2020, Simeon Simeonov
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10# 1. Redistributions of source code must retain the above copyright
11# notice, this list of conditions and the following disclaimer.
12# 2. Redistributions in binary form must reproduce the above copyright notice,
13# this list of conditions and the following disclaimer in the documentation
14# and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
17# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
20# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
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.
26"""Tests for otp2289.server"""
27import pytest
28
29import otp2289
30
31
32def test_state_caller_exceptions():
33 """Tests the exceptions when calling the OTPState objects"""
34 state = otp2289.OTPState('0x7965e05436f5029f',
35 1,
36 'TeSt',
37 otp2289.OTP_ALGO_MD5)
38 with pytest.raises(otp2289.OTPInvalidResponse) as exc_info:
39 state.response_validates('bla')
40 assert exc_info.type is otp2289.OTPInvalidResponse
41 assert exc_info.value.args[0] == (
42 'The response is neither a valid token or hex')
43
44
45def test_state_constructor_exceptions():
46 """Tests the exceptions when initializing new OTPState objects"""
47 with pytest.raises(otp2289.OTPStateException) as exc_info:
48 otp2289.OTPState('0x7965e05436f5029t',
49 1,
50 'TeStø'.encode(),
51 otp2289.OTP_ALGO_MD5)
52 assert exc_info.type is otp2289.OTPStateException
53 assert exc_info.value.args[0] == 'Seed must be a string'
54 with pytest.raises(otp2289.OTPStateException) as exc_info:
55 otp2289.OTPState('0x7965e05436f5029t',
56 '1',
57 'TeSt',
58 otp2289.OTP_ALGO_MD5)
59 assert exc_info.type is otp2289.OTPStateException
60 assert exc_info.value.args[0] == 'Step value MUST be an int'
61
62
63def test_state_validation_md5():
64 """Tests the OTPState validation functionality for MD5"""
65 state = otp2289.OTPState('0x7965e05436f5029f',
66 1,
67 'TeSt',
68 otp2289.OTP_ALGO_MD5)
69 assert state.validated is False
70 assert state.response_validates('0x9e876134d90499dd') is True
71 assert state.response_validates('INCH SEA ANNE LONG AHEM TOUR') is True
72 assert state.validated is True
73
74
75def test_state_validation_sha1():
76 """Tests the OTPState validation functionality for SHA1"""
77 state = otp2289.OTPState('0x63d936639734385b',
78 1,
79 'TeSt',
80 otp2289.OTP_ALGO_SHA1)
81 assert state.validated is False
82 assert state.response_validates('0xbb9e6ae1979d8ff4') is True
83 assert state.response_validates('MILT VARY MAST OK SEES WENT') is True
84 assert state.validated is True
diff --git a/tests/test_static.py b/tests/test_static.py
new file mode 100644
index 0000000..dc6e2d7
--- /dev/null
+++ b/tests/test_static.py
@@ -0,0 +1,79 @@
1# -*- coding: utf-8 -*-
2# SPDX-License-Identifier: BSD-2-Clause-FreeBSD
3#
4# Copyright (c) 2020, Simeon Simeonov
5# All rights reserved.
6#
7# Redistribution and use in source and binary forms, with or without
8# modification, are permitted provided that the following conditions
9# are met:
10# 1. Redistributions of source code must retain the above copyright
11# notice, this list of conditions and the following disclaimer.
12# 2. Redistributions in binary form must reproduce the above copyright notice,
13# this list of conditions and the following disclaimer in the documentation
14# and/or other materials provided with the distribution.
15#
16# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
17# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
18# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
19# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
20# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
21# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
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.
26"""Tests for the static methods and basic bit, byte, token functionality"""
27import binascii
28import os
29
30import otp2289
31
32
33def test_bytes_and_tokens():
34 """Tests the official hex and tokens defined in RFC2289"""
35 assert binascii.unhexlify('9e876134d90499dd') == (
36 otp2289.OTPGenerator.tokens_to_bytes('INCH SEA ANNE LONG AHEM TOUR'))
37 assert binascii.unhexlify('7965e05436f5029f') == (
38 otp2289.OTPGenerator.tokens_to_bytes('EASE OIL FUM CURE AWRY AVIS'))
39 assert binascii.unhexlify('50fe1962c4965880') == (
40 otp2289.OTPGenerator.tokens_to_bytes('BAIL TUFT BITS GANG CHEF THY'))
41 assert binascii.unhexlify('87066dd9644bf206') == (
42 otp2289.OTPGenerator.tokens_to_bytes('FULL PEW DOWN ONCE MORT ARC'))
43 assert binascii.unhexlify('7cd34c1040add14b') == (
44 otp2289.OTPGenerator.tokens_to_bytes('FACT HOOF AT FIST SITE KENT'))
45 assert binascii.unhexlify('5aa37a81f212146c') == (
46 otp2289.OTPGenerator.tokens_to_bytes('BODE HOP JAKE STOW JUT RAP'))
47 assert binascii.unhexlify('f205753943de4cf9') == (
48 otp2289.OTPGenerator.tokens_to_bytes('ULAN NEW ARMY FUSE SUIT EYED'))
49 assert binascii.unhexlify('ddcdac956f234937') == (
50 otp2289.OTPGenerator.tokens_to_bytes('SKIM CULT LOB SLAM POE HOWL'))
51 assert binascii.unhexlify('b203e28fa525be47') == (
52 otp2289.OTPGenerator.tokens_to_bytes('LONG IVY JULY AJAR BOND LEE'))
53
54 assert binascii.unhexlify('bb9e6ae1979d8ff4') == (
55 otp2289.OTPGenerator.tokens_to_bytes('MILT VARY MAST OK SEES WENT'))
56 assert binascii.unhexlify('63d936639734385b') == (
57 otp2289.OTPGenerator.tokens_to_bytes('CART OTTO HIVE ODE VAT NUT'))
58 assert binascii.unhexlify('87fec7768b73ccf9') == (
59 otp2289.OTPGenerator.tokens_to_bytes('GAFF WAIT SKID GIG SKY EYED'))
60 assert binascii.unhexlify('ad85f658ebe383c9') == (
61 otp2289.OTPGenerator.tokens_to_bytes('LEST OR HEEL SCOT ROB SUIT'))
62 assert binascii.unhexlify('d07ce229b5cf119b') == (
63 otp2289.OTPGenerator.tokens_to_bytes('RITE TAKE GELD COST TUNE RECK'))
64 assert binascii.unhexlify('27bc71035aaf3dc6') == (
65 otp2289.OTPGenerator.tokens_to_bytes('MAY STAR TIN LYON VEDA STAN'))
66 assert binascii.unhexlify('d51f3e99bf8e6f0b') == (
67 otp2289.OTPGenerator.tokens_to_bytes('RUST WELT KICK FELL TAIL FRAU'))
68 assert binascii.unhexlify('82aeb52d943774e4') == (
69 otp2289.OTPGenerator.tokens_to_bytes('FLIT DOSE ALSO MEW DRUM DEFY'))
70 assert binascii.unhexlify('4f296a74fe1567ec') == (
71 otp2289.OTPGenerator.tokens_to_bytes('AURA ALOE HURL WING BERG WAIT'))
72
73
74def test_random_bytes():
75 """Implement a few tests with random bytes"""
76 for _ in range(10):
77 rnd_bytes = os.urandom(8) # 64 bits
78 tokens = otp2289.OTPGenerator.bytes_to_tokens(rnd_bytes)
79 assert rnd_bytes == otp2289.OTPGenerator.tokens_to_bytes(tokens)