diff --git a/.vscode/settings.json b/.vscode/settings.json index e46fded..b3addfc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,7 +2,7 @@ "python.testing.pytestArgs": [ "." ], - "python.testing.unittestEnabled": false, + "python.testing.unittestEnabled": true, "python.testing.pytestEnabled": true, "cSpell.words": [ "devbox", @@ -16,5 +16,9 @@ "qrcode", "TOTP", "venv" - ] + ], + "search.exclude": { + "**/build": true, + "**/dist": true + }, } diff --git a/conftest.py b/conftest.py index 9c66a08..f17de12 100644 --- a/conftest.py +++ b/conftest.py @@ -2,7 +2,7 @@ import pytest def pytest_addoption(parser): - parser.addoption( "--relaxed", action='store_true', help="run tests in relaxed mode") + parser.addoption("--relaxed", action='store_true', help="run tests in relaxed mode") @pytest.fixture diff --git a/extract_otp_secret_keys.py b/extract_otp_secret_keys.py index a5161ef..9ae628e 100644 --- a/extract_otp_secret_keys.py +++ b/extract_otp_secret_keys.py @@ -53,18 +53,18 @@ import urllib.parse as urlparse from enum import Enum from operator import add -from qrcode import QRCode # type: ignore +from qrcode import QRCode # type: ignore -import protobuf_generated_python.google_auth_pb2 # type: ignore +import protobuf_generated_python.google_auth_pb2 # type: ignore try: - import cv2 # type: ignore - import numpy + import cv2 # type: ignore + import numpy # type: ignore try: - import pyzbar.pyzbar as zbar # type: ignore - from qreader import QReader # type: ignore + import pyzbar.pyzbar as zbar # type: ignore + from qreader import QReader # type: ignore except ImportError as e: raise SystemExit(f""" ERROR: Cannot import QReader module. This problem is probably due to the missing zbar shared library. @@ -72,7 +72,7 @@ On Linux and macOS libzbar0 must be installed. See in README.md for the installation of the libzbar0. Exception: {e}""") qreader_available = True -except ImportError as e: +except ImportError: qreader_available = False @@ -95,7 +95,6 @@ def main(sys_args): def parse_args(sys_args): global verbose, quiet - formatter = lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=52) description_text = "Extracts one time password (OTP) secret keys from QR codes, e.g. from Google Authenticator app." if qreader_available: description_text += "\nIf no infiles are provided, the QR codes are interactively captured from the camera." @@ -106,7 +105,7 @@ python extract_otp_secret_keys.py - < example_export.txt python extract_otp_secret_keys.py --csv - example_*.png | tail -n+2 python extract_otp_secret_keys.py = < example_export.png""" - arg_parser = argparse.ArgumentParser(formatter_class=formatter, + arg_parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=52), description=description_text, epilog=example_text) arg_parser.add_argument('infile', help="""a) file or - for stdin with 'otpauth-migration://...' URLs separated by newlines, lines starting with # are ignored; @@ -143,7 +142,7 @@ def extract_otps_from_camera(args): otp_urls = [] otps = [] - QRMode = Enum('QRMode', ['QREADER', 'DEEP_QREADER', 'CV2'], start = 0) + QRMode = Enum('QRMode', ['QREADER', 'DEEP_QREADER', 'CV2'], start=0) qr_mode = QRMode.QREADER if verbose: print(f"QR reading mode: {qr_mode}") @@ -270,8 +269,8 @@ def read_lines_from_text_file(filename): except UnicodeDecodeError: if filename == '-': abort("\nERROR: Unable to open text file form stdin. " - "In case you want read an image file from stdin, you must use '=' instead of '-'.") - else: # The file is probably an image, process below + "In case you want read an image file from stdin, you must use '=' instead of '-'.") + else: # The file is probably an image, process below return None finally: finput.close() @@ -285,7 +284,7 @@ def extract_otp_from_otp_url(otpauth_migration_url, otps, i, j, infile, args): j += 1 if verbose: print(f"\n{j}. Secret Key") secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret) - otp_type_enum = get_enum_name_by_number(raw_otp, 'type') + if verbose: print('OTP enum type:', get_enum_name_by_number(raw_otp, 'type')) otp_type = get_otp_type_str_from_code(raw_otp.type) otp_url = build_otp_url(secret, raw_otp) otp = { @@ -349,7 +348,7 @@ def get_payload_from_otp_url(otpauth_migration_url, i, input_source): if verbose > 2: print(f"\nDEBUG: parsed_url={parsed_url}") try: params = urlparse.parse_qs(parsed_url.query, strict_parsing=True) - except: # Not necessary for Python >= 3.11 + except Exception: # Not necessary for Python >= 3.11 params = [] if verbose > 2: print(f"\nDEBUG: querystring params={params}") if 'data' not in params: @@ -362,9 +361,9 @@ def get_payload_from_otp_url(otpauth_migration_url, i, input_source): payload = protobuf_generated_python.google_auth_pb2.MigrationPayload() try: payload.ParseFromString(data) - except: + except Exception: abort(f"\nERROR: Cannot decode otpauth-migration migration payload.\n" - f"data={data_base64}") + f"data={data_base64}") if verbose: print(f"\n{i}. Payload Line", payload, sep='\n') @@ -515,7 +514,7 @@ def open_file_or_stdout_for_csv(filename): def check_file_exists(filename): if filename != '-' and not os.path.isfile(filename): abort(f"\nERROR: Input file provided is non-existent or not a file." - f"\ninput file: {filename}") + f"\ninput file: {filename}") def is_binary(line): diff --git a/setup.py b/setup.py index 433782c..1b291aa 100644 --- a/setup.py +++ b/setup.py @@ -1,5 +1,5 @@ import pathlib -from setuptools import setup # type: ignore +from setuptools import setup # type: ignore setup( name='extract_otp_secret_keys', diff --git a/test/print_verbose_output.txt b/test/print_verbose_output.txt index c81199b..aefbb4f 100644 --- a/test/print_verbose_output.txt +++ b/test/print_verbose_output.txt @@ -42,6 +42,7 @@ batch_id: -1320898453 1. Secret Key +OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Issuer: raspberrypi @@ -66,6 +67,7 @@ batch_id: -2094403140 2. Secret Key +OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Type: totp @@ -98,6 +100,7 @@ batch_id: -1822886384 3. Secret Key +OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Type: totp @@ -105,6 +108,7 @@ otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY 4. Secret Key +OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Issuer: raspberrypi @@ -130,6 +134,7 @@ batch_id: -1558849573 5. Secret Key +OTP enum type: OTP_HOTP Name: hotp demo Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Type: hotp @@ -155,6 +160,7 @@ batch_id: -171198419 6. Secret Key +OTP enum type: OTP_TOTP Name: encoding: ¿äÄéÉ? (demo) Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Type: totp diff --git a/test_extract_otp_secret_keys_pytest.py b/test_extract_otp_secret_keys_pytest.py index 2567ce0..e1f5515 100644 --- a/test_extract_otp_secret_keys_pytest.py +++ b/test_extract_otp_secret_keys_pytest.py @@ -20,13 +20,15 @@ import io import os -import re import sys import pytest import extract_otp_secret_keys -from utils import * +from utils import (file_exits, quick_and_dirty_workaround_encoding_problem, + read_binary_file_as_stream, read_csv, read_csv_str, + read_file_to_str, read_json, read_json_str, + replace_escaped_octal_utf8_bytes_with_str) qreader_available = extract_otp_secret_keys.qreader_available @@ -213,7 +215,6 @@ def test_keepass_csv_stdout(capsys): # Assert expected_totp_csv = read_csv('example_keepass_output.totp.csv') - expected_hotp_csv = read_csv('example_keepass_output.hotp.csv') assert not file_exits('test_example_keepass_output.totp.csv') assert not file_exits('test_example_keepass_output.hotp.csv') assert not file_exits('test_example_keepass_output.csv') @@ -340,7 +341,8 @@ def test_extract_saveqr(capsys, tmp_path): def test_normalize_bytes(): - assert replace_escaped_octal_utf8_bytes_with_str('Before\\\\302\\\\277\\\\303\nname: enc: \\302\\277\\303\\244\\303\\204\\303\\251\\303\\211?\nAfter') == 'Before\\\\302\\\\277\\\\303\nname: enc: ¿äÄéÉ?\nAfter' + assert replace_escaped_octal_utf8_bytes_with_str( + 'Before\\\\302\\\\277\\\\303\nname: enc: \\302\\277\\303\\244\\303\\204\\303\\251\\303\\211?\nAfter') == 'Before\\\\302\\\\277\\\\303\nname: enc: ¿äÄéÉ?\nAfter' def test_extract_verbose(capsys, relaxed): @@ -352,6 +354,9 @@ def test_extract_verbose(capsys, relaxed): expected_stdout = read_file_to_str('test/print_verbose_output.txt') + if not qreader_available: + expected_stdout = expected_stdout.replace('QReader installed: True', 'QReader installed: False') + if relaxed or sys.implementation.name == 'pypy': print('\nRelaxed mode\n') @@ -390,6 +395,7 @@ def test_extract_help(capsys): assert e.type == SystemExit assert e.value.code == 0 + def test_extract_no_arguments(capsys, mocker): if qreader_available: # Arrange @@ -554,8 +560,7 @@ def test_img_qr_reader_from_stdin(capsys, monkeypatch): # Assert captured = capsys.readouterr() - expected_stdout =\ -'''Name: Test1:test1@example1.com + expected_stdout = '''Name: Test1:test1@example1.com Secret: JBSWY3DPEHPK3PXP Issuer: Test1 Type: totp @@ -705,8 +710,7 @@ Type: totp ''' -EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG =\ -'''Name: Test1:test1@example1.com +EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG = '''Name: Test1:test1@example1.com Secret: JBSWY3DPEHPK3PXP Issuer: Test1 Type: totp diff --git a/test_extract_qrcode_unittest.py b/test_extract_qrcode_unittest.py index 9152511..422d370 100644 --- a/test_extract_qrcode_unittest.py +++ b/test_extract_qrcode_unittest.py @@ -23,15 +23,16 @@ from utils import Capturing import extract_otp_secret_keys + class TestQRImageExtract(unittest.TestCase): def test_img_qr_reader_happy_path(self): with Capturing() as actual_output: extract_otp_secret_keys.main(['test/test_googleauth_export.png']) expected_output =\ - ['Name: Test1:test1@example1.com', 'Secret: JBSWY3DPEHPK3PXP', 'Issuer: Test1', 'Type: totp', '', - 'Name: Test2:test2@example2.com', 'Secret: JBSWY3DPEHPK3PXQ', 'Issuer: Test2', 'Type: totp', '', - 'Name: Test3:test3@example3.com', 'Secret: JBSWY3DPEHPK3PXR', 'Issuer: Test3', 'Type: totp', ''] + ['Name: Test1:test1@example1.com', 'Secret: JBSWY3DPEHPK3PXP', 'Issuer: Test1', 'Type: totp', '', + 'Name: Test2:test2@example2.com', 'Secret: JBSWY3DPEHPK3PXQ', 'Issuer: Test2', 'Type: totp', '', + 'Name: Test3:test3@example3.com', 'Secret: JBSWY3DPEHPK3PXR', 'Issuer: Test3', 'Type: totp', ''] self.assertEqual(actual_output, expected_output) @@ -40,8 +41,7 @@ class TestQRImageExtract(unittest.TestCase): with self.assertRaises(SystemExit) as context: extract_otp_secret_keys.main(['test/lena_std.tif']) - expected_output =\ - ['', 'ERROR: Unable to read QR Code from file.', 'input file: test/lena_std.tif'] + expected_output = ['', 'ERROR: Unable to read QR Code from file.', 'input file: test/lena_std.tif'] self.assertEqual(actual_output, expected_output) self.assertEqual(context.exception.code, 1) @@ -51,8 +51,7 @@ class TestQRImageExtract(unittest.TestCase): with self.assertRaises(SystemExit) as context: extract_otp_secret_keys.main(['test/nonexistent.bmp']) - expected_output =\ - ['', 'ERROR: Input file provided is non-existent or not a file.', 'input file: test/nonexistent.bmp'] + expected_output = ['', 'ERROR: Input file provided is non-existent or not a file.', 'input file: test/nonexistent.bmp'] self.assertEqual(actual_output, expected_output) self.assertEqual(context.exception.code, 1) diff --git a/upgrade_deps.sh b/upgrade_deps.sh index a17fe93..d09229b 100755 --- a/upgrade_deps.sh +++ b/upgrade_deps.sh @@ -209,7 +209,7 @@ cmd="$FLAKE8 . --count --select=E9,F63,F7,F82 --show-source --statistics" if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi eval "$cmd" -cmd="$FLAKE8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics" +cmd="$FLAKE8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,protobuf_generated_python" if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi eval "$cmd" @@ -239,7 +239,7 @@ cmd="docker build . -t extract_otp_secret_keys_no_qr_reader -f Dockerfile_no_qr_ if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi eval "$cmd" -cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secret_keys_no_qr_reader test_extract_otp_secret_keys_pytest.py -k 'not qreader' -vvv --relaxed" +cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secret_keys_no_qr_reader test_extract_otp_secret_keys_pytest.py -k 'not qreader' -vvv --relaxed" if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi eval "$cmd" @@ -247,7 +247,7 @@ cmd="docker build . -t extract_otp_secret_keys --pull" if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi eval "$cmd" -cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secret_keys" +cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secret_keys" if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi eval "$cmd" diff --git a/utils.py b/utils.py index 19cb52c..a7edfd8 100644 --- a/utils.py +++ b/utils.py @@ -104,11 +104,13 @@ def read_file_to_str(filename): """Returns a str.""" return "".join(read_file_to_list(filename)) + def read_binary_file_as_stream(filename): """Returns binary file content.""" with open(filename, "rb",) as infile: return io.BytesIO(infile.read()) + def replace_escaped_octal_utf8_bytes_with_str(str): encoded_name_strings = re.findall(r'name: .*$', str, flags=re.MULTILINE) for encoded_name_string in encoded_name_strings: