summaryrefslogtreecommitdiff
path: root/tests/test_generator.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/test_generator.py')
-rw-r--r--tests/test_generator.py251
1 files changed, 251 insertions, 0 deletions
diff --git a/tests/test_generator.py b/tests/test_generator.py
new file mode 100644
index 0000000..08947e5
--- /dev/null
+++ b/tests/test_generator.py
@@ -0,0 +1,251 @@
1# SPDX-License-Identifier: BSD-2-Clause
2#
3# Copyright (c) 2020-2026, Simeon Simeonov
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9# 1. Redistributions of source code must retain the above copyright
10# notice, this list of conditions and the following disclaimer.
11# 2. Redistributions in binary form must reproduce the above copyright notice,
12# this list of conditions and the following disclaimer in the documentation
13# and/or other materials provided with the distribution.
14#
15# THIS SOFTWARE IS PROVIDED BY THE AUTHORS ``AS IS'' AND ANY EXPRESS OR
16# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
17# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
18# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
19# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
20# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
24# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25"""Tests for otp2289.generator"""
26
27import pytest
28
29import otp2289
30
31
32def test_caller_exceptions() -> None:
33 """Tests the exceptions when calling an initialized object"""
34 gen = otp2289.OTPGenerator(
35 b'This is a test.', 'TeSt', otp2289.OTP_ALGO_MD5
36 )
37 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
38 gen.generate_otp_words('3') # ty: ignore[invalid-argument-type]
39 assert exc_info.type is otp2289.OTPGeneratorError
40 assert exc_info.value.args[0] == 'Step value MUST be an int'
41 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
42 gen.generate_otp_hexdigest(-1)
43 assert exc_info.type is otp2289.OTPGeneratorError
44 assert exc_info.value.args[0] == 'Step value MUST be >= 0'
45 with pytest.raises(otp2289.OTPChallengeError) as exc_info:
46 gen.generate_otp_hexdigest_from_challenge(
47 b'md5 fbd TeSt' # ty: ignore[invalid-argument-type]
48 )
49 assert exc_info.type is otp2289.OTPChallengeError
50 assert exc_info.value.args[0] == 'Challenge must be str'
51 with pytest.raises(otp2289.OTPChallengeError) as exc_info:
52 gen.generate_otp_hexdigest_from_challenge('md5 fbd TeSt')
53 assert exc_info.type is otp2289.OTPChallengeError
54 assert exc_info.value.args[0] == 'Invalid challenge'
55 with pytest.raises(otp2289.generator.OTPChallengeError) as exc_info:
56 gen.generate_otp_hexdigest_from_challenge('otp-md5 fbd TeSt')
57 assert exc_info.type is otp2289.generator.OTPChallengeError
58 assert exc_info.value.args[0] == 'Invalid challenge'
59
60
61def test_constructor_exceptions() -> None:
62 """
63 Tests the exceptions when initializing a new object (in the constructor)
64 """
65 # test the otp2289.OTPGenerator __init__ and validators
66 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
67 otp2289.OTPGenerator(
68 b'This is a test.',
69 'TeStø'.encode(), # ty: ignore[invalid-argument-type]
70 otp2289.OTP_ALGO_MD5,
71 )
72 assert exc_info.type is otp2289.OTPGeneratorError
73 assert exc_info.value.args[0] == 'Seed must be a string'
74 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
75 otp2289.OTPGenerator(
76 b'This is a test.', 'TeStøtEsTteSTteStTest', otp2289.OTP_ALGO_SHA1
77 )
78 assert exc_info.type is otp2289.OTPGeneratorError
79 assert exc_info.value.args[0] == (
80 'The seed MUST be of 1 to 16 characters in length'
81 )
82 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
83 otp2289.OTPGenerator(
84 b'This is a test.', 'TeStø', otp2289.OTP_ALGO_SHA1
85 )
86 assert exc_info.type is otp2289.OTPGeneratorError
87 assert exc_info.value.args[0] == (
88 'The seed MUST consist of purely alphanumeric characters'
89 )
90 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
91 otp2289.OTPGenerator(b'This is a test.', 'TeSt', 9)
92 assert exc_info.type is otp2289.OTPGeneratorError
93 assert exc_info.value.args[0] == (
94 'hash_algo is not among the known algorithms'
95 )
96 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
97 otp2289.OTPGenerator(
98 b'This is a test.',
99 'TeSt',
100 b'md5', # ty: ignore[invalid-argument-type]
101 )
102 assert exc_info.type is otp2289.OTPGeneratorError
103 assert exc_info.value.args[0] == 'hash_algo must be an int or a str'
104 # test the package structure as well
105 with pytest.raises(otp2289.generator.OTPGeneratorError) as exc_info:
106 otp2289.generator.OTPGenerator(b'This is a test.', 'TeSt', 'foo')
107 assert exc_info.type is otp2289.generator.OTPGeneratorError
108 assert exc_info.value.args[0] == (
109 'foo is not supported by this version of the hashlib module'
110 )
111 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
112 otp2289.OTPGenerator(
113 '1234567', # ty: ignore[invalid-argument-type]
114 'TeSt',
115 otp2289.OTP_ALGO_MD5,
116 )
117 assert exc_info.type is otp2289.OTPGeneratorError
118 assert exc_info.value.args[0] == 'Password must be a byte-string'
119 with pytest.raises(otp2289.OTPGeneratorError) as exc_info:
120 otp2289.OTPGenerator(b'1234567', 'TeSt', otp2289.OTP_ALGO_MD5)
121 assert exc_info.type is otp2289.OTPGeneratorError
122 assert exc_info.value.args[0] == 'Password must be longer than 10 bytes'
123
124
125def test_md5() -> None:
126 """
127 Tests the MD5 functionality of the OTPGenerator as described in the RFC
128
129 Those are the tests from 'RFC-2289 Appendix C - OTP Verification Examples'
130 """
131 # We could run this in a loop, but I guess "Readability counts."
132 # pass='This is a test.', seed='TeSt'
133 gen = otp2289.OTPGenerator(
134 b'This is a test.', 'TeSt', otp2289.OTP_ALGO_MD5
135 )
136 res_words = gen.generate_otp_words(0)
137 res_hex = gen.generate_otp_hexdigest(0)
138 assert isinstance(res_words, str)
139 assert isinstance(res_hex, str)
140 assert res_hex == '0x9e876134d90499dd'
141 assert res_words == 'INCH SEA ANNE LONG AHEM TOUR'
142 # step 1
143 assert gen.generate_otp_hexdigest(1) == '0x7965e05436f5029f'
144 assert gen.generate_otp_words(1) == 'EASE OIL FUM CURE AWRY AVIS'
145 assert gen.generate_otp_hexdigest_from_challenge('otp-md5 1 TeSt') == (
146 '0x7965e05436f5029f'
147 )
148 assert gen.generate_otp_words_from_challenge('otp-md5 1 TeSt') == (
149 'EASE OIL FUM CURE AWRY AVIS'
150 )
151 # step 99
152 assert gen.generate_otp_hexdigest(99) == '0x50fe1962c4965880'
153 assert gen.generate_otp_words(99) == 'BAIL TUFT BITS GANG CHEF THY'
154 assert gen.generate_otp_hexdigest_from_challenge('otp-md5 99 TeSt') == (
155 '0x50fe1962c4965880'
156 )
157 assert gen.generate_otp_words_from_challenge('otp-md5 99 TeSt') == (
158 'BAIL TUFT BITS GANG CHEF THY'
159 )
160 # iterator test
161 hexdigests = list(gen.hexdigest_range(105)) # testing the range itself
162 words = list(gen.words_range(99))
163 hexdigests.reverse()
164 words.reverse()
165 assert hexdigests[0] == '0x9e876134d90499dd'
166 assert hexdigests[1] == '0x7965e05436f5029f'
167 assert hexdigests[99] == '0x50fe1962c4965880'
168 assert words[0] == 'INCH SEA ANNE LONG AHEM TOUR'
169 assert words[1] == 'EASE OIL FUM CURE AWRY AVIS'
170 assert words[99] == 'BAIL TUFT BITS GANG CHEF THY'
171 # pass='AbCdEfGhIjK', seed='alpha1'
172 gen = otp2289.OTPGenerator(b'AbCdEfGhIjK', 'alpha1', otp2289.OTP_ALGO_MD5)
173 assert gen.generate_otp_hexdigest(0) == '0x87066dd9644bf206'
174 assert gen.generate_otp_words(0) == 'FULL PEW DOWN ONCE MORT ARC'
175 assert gen.generate_otp_hexdigest(1) == '0x7cd34c1040add14b'
176 assert gen.generate_otp_words(1) == 'FACT HOOF AT FIST SITE KENT'
177 assert gen.generate_otp_hexdigest(99) == '0x5aa37a81f212146c'
178 assert gen.generate_otp_words(99) == 'BODE HOP JAKE STOW JUT RAP'
179 # pass="OTP's are good", seed='correct'
180 gen = otp2289.OTPGenerator(
181 b"OTP's are good", 'correct', otp2289.OTP_ALGO_MD5
182 )
183 assert gen.generate_otp_hexdigest(0) == '0xf205753943de4cf9'
184 assert gen.generate_otp_words(0) == 'ULAN NEW ARMY FUSE SUIT EYED'
185 assert gen.generate_otp_hexdigest(1) == '0xddcdac956f234937'
186 assert gen.generate_otp_words(1) == 'SKIM CULT LOB SLAM POE HOWL'
187 assert gen.generate_otp_hexdigest(99) == '0xb203e28fa525be47'
188 assert gen.generate_otp_words(99) == 'LONG IVY JULY AJAR BOND LEE'
189
190
191def test_sha1() -> None:
192 """
193 Tests the SHA-1 functionality of the OTPGenerator as described in the RFC
194
195 Those are the tests from 'RFC-2289 Appendix C - OTP Verification Examples'
196 """
197 # pass='This is a test.', seed='TeSt'
198 gen = otp2289.OTPGenerator(
199 b'This is a test.', 'TeSt', otp2289.OTP_ALGO_SHA1
200 )
201 res_hex = gen.generate_otp_hexdigest(step=0)
202 res_words = gen.generate_otp_words(step=0)
203 assert isinstance(res_words, str)
204 assert isinstance(res_hex, str)
205 assert res_hex == '0xbb9e6ae1979d8ff4'
206 assert res_words == 'MILT VARY MAST OK SEES WENT'
207 assert gen.generate_otp_hexdigest(1) == '0x63d936639734385b'
208 assert gen.generate_otp_words(1) == 'CART OTTO HIVE ODE VAT NUT'
209 assert gen.generate_otp_hexdigest_from_challenge('otp-sha1 1 TeSt') == (
210 '0x63d936639734385b'
211 )
212 assert gen.generate_otp_words_from_challenge('otp-sha1 1 TeSt') == (
213 'CART OTTO HIVE ODE VAT NUT'
214 )
215 assert gen.generate_otp_hexdigest(99) == '0x87fec7768b73ccf9'
216 assert gen.generate_otp_words(99) == 'GAFF WAIT SKID GIG SKY EYED'
217 assert gen.generate_otp_hexdigest_from_challenge('otp-sha1 99 TeSt') == (
218 '0x87fec7768b73ccf9'
219 )
220 assert gen.generate_otp_words_from_challenge('otp-sha1 99 TeSt') == (
221 'GAFF WAIT SKID GIG SKY EYED'
222 )
223 # iterator test
224 hexdigests = list(gen.hexdigest_range(105))
225 words = list(gen.words_range(99))
226 hexdigests.reverse()
227 words.reverse()
228 assert hexdigests[0] == '0xbb9e6ae1979d8ff4'
229 assert hexdigests[1] == '0x63d936639734385b'
230 assert hexdigests[99] == '0x87fec7768b73ccf9'
231 assert words[0] == 'MILT VARY MAST OK SEES WENT'
232 assert words[1] == 'CART OTTO HIVE ODE VAT NUT'
233 assert words[99] == 'GAFF WAIT SKID GIG SKY EYED'
234 # pass='AbCdEfGhIjK', seed='alpha1'
235 gen = otp2289.OTPGenerator(b'AbCdEfGhIjK', 'alpha1', otp2289.OTP_ALGO_SHA1)
236 assert gen.generate_otp_hexdigest(0) == '0xad85f658ebe383c9'
237 assert gen.generate_otp_words(0) == 'LEST OR HEEL SCOT ROB SUIT'
238 assert gen.generate_otp_hexdigest(1) == '0xd07ce229b5cf119b'
239 assert gen.generate_otp_words(1) == 'RITE TAKE GELD COST TUNE RECK'
240 assert gen.generate_otp_hexdigest(99) == '0x27bc71035aaf3dc6'
241 assert gen.generate_otp_words(99) == 'MAY STAR TIN LYON VEDA STAN'
242 # pass="OTP's are good", seed='correct'
243 gen = otp2289.OTPGenerator(
244 b"OTP's are good", 'correct', otp2289.OTP_ALGO_SHA1
245 )
246 assert gen.generate_otp_hexdigest(0) == '0xd51f3e99bf8e6f0b'
247 assert gen.generate_otp_words(0) == 'RUST WELT KICK FELL TAIL FRAU'
248 assert gen.generate_otp_hexdigest(1) == '0x82aeb52d943774e4'
249 assert gen.generate_otp_words(1) == 'FLIT DOSE ALSO MEW DRUM DEFY'
250 assert gen.generate_otp_hexdigest(99) == '0x4f296a74fe1567ec'
251 assert gen.generate_otp_words(99) == 'AURA ALOE HURL WING BERG WAIT'