Skip to content

Commit 3caa2ed

Browse files
author
Dave Hein
committed
Fixed bug that rejected 26 character secrets
JeNeSuisPasDave#1 Now add base32 padding as needed to get to a multiple of 8 characters in the secret string.
1 parent 09a600f commit 3caa2ed

File tree

5 files changed

+67
-75
lines changed

5 files changed

+67
-75
lines changed

src/authenticator/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@
4040

4141
__author__ = "Dave Hein <[email protected]>"
4242
__license__ = "MIT"
43-
__version__ = "1.1.2"
43+
__version__ = "1.1.3"

src/authenticator/hotp.py

+9
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,15 @@ def convert_base32_secret_key(self, base32_secret_key):
185185
import base64
186186
import binascii
187187

188+
# Pad the base32 string to a multiple of 8 characters
189+
#
190+
secret_length = len(base32_secret_key)
191+
pad_length = (8 - (secret_length % 8)) % 8
192+
pad = "=" * pad_length
193+
base32_secret_key = base32_secret_key + pad
194+
195+
# Decode it
196+
#
188197
try:
189198
secret_key = base64.b32decode(base32_secret_key)
190199
except binascii.Error:

src/setup.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
# the version across setup.py and the project code, see
3131
# https://packaging.python.org/en/latest/single_source_version.html
3232
#
33-
version='1.1.2',
33+
version='1.1.3',
3434

3535
description='A HOTP/TOTP code generator for the command line.',
3636
long_description=long_description,

tests/test_CLI.py

-59
Original file line numberDiff line numberDiff line change
@@ -1244,65 +1244,6 @@ def test_prompt_add_time_based_hotp_googlized_secret(
12441244
self.assertEqual(expected_passphrase, cut._CLI__passphrase)
12451245
self.assertEqual(expected_shared_secret, cut._CLI__shared_secret)
12461246

1247-
@unittest.mock.patch('authenticator.data.ClientFile._get_key_stretches')
1248-
@unittest.mock.patch('os.path.expanduser')
1249-
def test_prompt_add_time_based_hotp_wrong_length_secret(
1250-
self, mock_expanduser, mock_key_stretches):
1251-
"""Test CLI.Prompt().
1252-
1253-
At first supply a shared secret that is an invalid length for a base32
1254-
string, but then supply a correct base32 string.
1255-
1256-
"""
1257-
mock_key_stretches.return_value = 64
1258-
mock_expanduser.side_effect = \
1259-
lambda x: self._side_effect_expand_user(x)
1260-
expected_passphrase = "Maresy doats and dosey doats."
1261-
expected_shared_secret = "ABCDEFGHABCDEFGHABCDEFGH"
1262-
provided_shared_secret = "ABCDEFGHABCDEFGHABCD"
1263-
rw_mock = unittest.mock.MagicMock()
1264-
rw_mock.readline.side_effect = [
1265-
'yes', expected_passphrase, expected_passphrase,
1266-
provided_shared_secret, expected_shared_secret]
1267-
cut = CLI(stdin=rw_mock, stdout=rw_mock)
1268-
args = ("add", "[email protected]")
1269-
cut.parse_command_args(args)
1270-
cut.create_data_file()
1271-
cut.prompt_for_secrets()
1272-
calls = [
1273-
unittest.mock.call.write(
1274-
"No data file was found. Do you want to create your data" +
1275-
" file? (yes|no) [yes]: "),
1276-
unittest.mock.call.write(""),
1277-
unittest.mock.call.flush(),
1278-
unittest.mock.call.readline(),
1279-
unittest.mock.call.write("Enter passphrase: "),
1280-
unittest.mock.call.write(""),
1281-
unittest.mock.call.flush(),
1282-
unittest.mock.call.readline(),
1283-
unittest.mock.call.write("Confirm passphrase: "),
1284-
unittest.mock.call.write(""),
1285-
unittest.mock.call.flush(),
1286-
unittest.mock.call.readline(),
1287-
unittest.mock.call.write("Enter shared secret: "),
1288-
unittest.mock.call.write(""),
1289-
unittest.mock.call.flush(),
1290-
unittest.mock.call.readline(),
1291-
unittest.mock.call.write(
1292-
"Invalid shared secret string. " +
1293-
"Wrong length, incorrect padding, or embedded whitespace."),
1294-
unittest.mock.call.write("\n"),
1295-
unittest.mock.call.write("Enter shared secret: "),
1296-
unittest.mock.call.write(""),
1297-
unittest.mock.call.flush(),
1298-
unittest.mock.call.readline()]
1299-
rw_mock.assert_has_calls(calls)
1300-
self.assertEqual(12, rw_mock.write.call_count)
1301-
self.assertEqual(5, rw_mock.flush.call_count)
1302-
self.assertEqual(5, rw_mock.readline.call_count)
1303-
self.assertEqual(expected_passphrase, cut._CLI__passphrase)
1304-
self.assertEqual(expected_shared_secret, cut._CLI__shared_secret)
1305-
13061247
@unittest.mock.patch('authenticator.data.ClientFile._get_key_stretches')
13071248
@unittest.mock.patch('os.path.expanduser')
13081249
def test_prompt_add_time_based_hotp_empty_secret(

tests/test_hotp.py

+56-14
Original file line numberDiff line numberDiff line change
@@ -318,25 +318,67 @@ def test_convert_base32(self):
318318
actual_bytes = cut.convert_base32_secret_key(in_string)
319319
self.assertEqual(expected_bytes, actual_bytes)
320320

321-
def test_convert_base32_too_short(self):
321+
def test_convert_base32_16_chars(self):
322322
"""Test Otp.convert_base32_secret_key().
323323
324-
Check that an input base32 encoded string that is not multiples of 8
325-
characters in length (too short) throws the expected exception.
324+
Typical input string lengths are 16, 26, 32, and 64 significant
325+
(ignoring padding) base32 characters. This tests a 16 character
326+
base32 encoded secret.
326327
327328
"""
328329
cut = HOTP()
329-
# First be certain the method works with a correct base32-encoded
330-
# input string
331-
#
332-
in_string = "ABCDEFGH"
333-
cut.convert_base32_secret_key(in_string)
334-
# Then check that it throws the expected exception with an
335-
# incorrectly sized input string.
336-
#
337-
in_string = "ABCDE"
338-
with self.assertRaises(ValueError):
339-
cut.convert_base32_secret_key(in_string)
330+
in_string = "mfzw s5dv mf2g s33o"
331+
in_string = "".join(in_string.split()).upper()
332+
expected_bytes = b"asituation"
333+
actual_bytes = cut.convert_base32_secret_key(in_string)
334+
self.assertEqual(expected_bytes, actual_bytes)
335+
336+
def test_convert_base32_26_chars(self):
337+
"""Test Otp.convert_base32_secret_key().
338+
339+
Typical input string lengths are 16, 26, 32, and 64 significant
340+
(ignoring padding) base32 characters. This tests a 26 character
341+
base32 encoded secret, which requires padding the base32 secret
342+
before decoding
343+
344+
"""
345+
cut = HOTP()
346+
in_string = "onux i5lb oruw 63ra nzxx e3lb nq"
347+
in_string = "".join(in_string.split()).upper()
348+
expected_bytes = b"situation normal"
349+
actual_bytes = cut.convert_base32_secret_key(in_string)
350+
self.assertEqual(expected_bytes, actual_bytes)
351+
352+
def test_convert_base32_32_chars(self):
353+
"""Test Otp.convert_base32_secret_key().
354+
355+
Typical input string lengths are 16, 26, 32, and 64 significant
356+
(ignoring padding) base32 characters. This tests a 32 character
357+
base32 encoded secret
358+
359+
"""
360+
cut = HOTP()
361+
in_string = "nf2c a2lt ebqw y3ba mzxx k3df mqqh k4bo"
362+
in_string = "".join(in_string.split()).upper()
363+
expected_bytes = b"it is all fouled up."
364+
actual_bytes = cut.convert_base32_secret_key(in_string)
365+
self.assertEqual(expected_bytes, actual_bytes)
366+
367+
def test_convert_base32_64_chars(self):
368+
"""Test Otp.convert_base32_secret_key().
369+
370+
Typical input string lengths are 16, 26, 32, and 64 significant
371+
(ignoring padding) base32 characters. This tests a 64 character
372+
base32 encoded secret
373+
374+
"""
375+
cut = HOTP()
376+
in_string = "knux i5lb oruw 63ra nzxx e3lb nqwc a2lu e5zs" + \
377+
" aylm nqqg m33v nrsw iidv oaqd ulji"
378+
in_string = "".join(in_string.split()).upper()
379+
expected_bytes = b"Situation normal, it\'s all fouled up :-("
380+
actual_bytes = cut.convert_base32_secret_key(in_string)
381+
self.assertEqual(expected_bytes, actual_bytes)
340382

341383
def test_convert_base32_too_long(self):
342384
"""Test Otp.convert_base32_secret_key().

0 commit comments

Comments
 (0)