diff --git a/src/extract_otp_secrets.py b/src/extract_otp_secrets.py index 41ece3a..e937e20 100644 --- a/src/extract_otp_secrets.py +++ b/src/extract_otp_secrets.py @@ -36,6 +36,7 @@ import argparse import base64 import csv import fileinput +import glob import json import os import platform @@ -527,14 +528,20 @@ def extract_otps_from_files(args: Args) -> Otps: files_count = urls_count = otps_count = 0 if verbose: print(f"Input files: {args.infile}") - for infile in args.infile: - if verbose >= LogLevel.MORE_VERBOSE: log_verbose(f"Processing infile {infile}") - files_count += 1 - for line in get_otp_urls_from_file(infile, args): - if verbose >= LogLevel.MORE_VERBOSE: log_verbose(line) - if line.startswith('#') or line == '': continue - urls_count += 1 - otps_count += extract_otp_from_otp_url(line, otps, urls_count, infile, args) + for infile_raw in args.infile: + expanded_infiles = glob.glob(infile_raw) + if not expanded_infiles: + expanded_infiles = [infile_raw] + if verbose >= LogLevel.DEBUG: log_debug(f"Could not expand input files, fallback to infile") + if verbose >= LogLevel.DEBUG: log_debug(f"Expanded input files: {expanded_infiles}") + for infile in expanded_infiles: + if verbose >= LogLevel.MORE_VERBOSE: log_verbose(f"Processing infile {infile}") + files_count += 1 + for line in get_otp_urls_from_file(infile, args): + if verbose >= LogLevel.MORE_VERBOSE: log_verbose(line) + if line.startswith('#') or line == '': continue + urls_count += 1 + otps_count += extract_otp_from_otp_url(line, otps, urls_count, infile, args) if verbose: print(f"Extracted {otps_count} otp{'s'[:otps_count != 1]} from {urls_count} otp url{'s'[:urls_count != 1]} by reading {files_count} infile{'s'[:files_count != 1]}") return otps diff --git a/tests/data/print_verbose_output-n-vvv.txt b/tests/data/print_verbose_output-n-vvv.txt index 0bf4522..78a0bc7 100644 --- a/tests/data/print_verbose_output-n-vvv.txt +++ b/tests/data/print_verbose_output-n-vvv.txt @@ -2,9 +2,11 @@ QReader installed: True CV2 version: 4.10.0 QR reading mode: ZBAR -Version: extract_otp_secrets 2.8.1.post17+git.3dc7d1c2.dirty Linux x86_64 Python 3.11.9 (CPython/called as script) +Version: extract_otp_secrets 2.8.4.post4+git.7ce765dd.dirty Linux x86_64 Python 3.11.10 (CPython/called as script) Input files: ['example_export.txt'] + +DEBUG: Expanded input files: ['example_export.txt'] Processing infile example_export.txt Reading lines of example_export.txt # 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/ diff --git a/tests/data/print_verbose_output-vvv.txt b/tests/data/print_verbose_output-vvv.txt index 5fc8b8b..13fbe24 100644 --- a/tests/data/print_verbose_output-vvv.txt +++ b/tests/data/print_verbose_output-vvv.txt @@ -2,9 +2,11 @@ QReader installed: True CV2 version: 4.10.0 QR reading mode: ZBAR -Version: extract_otp_secrets 2.8.1.post17+git.3dc7d1c2.dirty Linux x86_64 Python 3.11.9 (CPython/called as script) +Version: extract_otp_secrets 2.8.4.post4+git.7ce765dd.dirty Linux x86_64 Python 3.11.10 (CPython/called as script) Input files: ['example_export.txt'] + +DEBUG: Expanded input files: ['example_export.txt']  Processing infile example_export.txt Reading lines of example_export.txt # 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/ diff --git a/tests/extract_otp_secrets_test.py b/tests/extract_otp_secrets_test.py index 1434457..f0ace31 100644 --- a/tests/extract_otp_secrets_test.py +++ b/tests/extract_otp_secrets_test.py @@ -869,19 +869,8 @@ def test_wrong_content(capsys: pytest.CaptureFixture[str]) -> None: # Assert captured = capsys.readouterr() - expected_stderr = ''' -WARN: input is not a otpauth-migration:// url -source: tests/data/test_export_wrong_content.txt -input: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. -Maybe a wrong file was given - -ERROR: could not parse query parameter in input url -source: tests/data/test_export_wrong_content.txt -url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. -''' - assert captured.out == '' - assert captured.err == expected_stderr + assert captured.err == EXPECTED_STDERR_OTP_URL_WRONG def test_one_wrong_file(capsys: pytest.CaptureFixture[str]) -> None: @@ -891,19 +880,8 @@ def test_one_wrong_file(capsys: pytest.CaptureFixture[str]) -> None: # Assert captured = capsys.readouterr() - expected_stderr = ''' -WARN: input is not a otpauth-migration:// url -source: tests/data/test_export_wrong_content.txt -input: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. -Maybe a wrong file was given - -ERROR: could not parse query parameter in input url -source: tests/data/test_export_wrong_content.txt -url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. -''' - assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT - assert captured.err == expected_stderr + assert captured.err == EXPECTED_STDERR_OTP_URL_WRONG def test_one_wrong_file_colored(capsys: pytest.CaptureFixture[str]) -> None: @@ -913,19 +891,8 @@ def test_one_wrong_file_colored(capsys: pytest.CaptureFixture[str]) -> None: # Assert captured = capsys.readouterr() - expected_stderr = f'''{colorama.Fore.RED} -WARN: input is not a otpauth-migration:// url -source: tests/data/test_export_wrong_content.txt -input: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. -Maybe a wrong file was given{colorama.Fore.RESET} -{colorama.Fore.RED} -ERROR: could not parse query parameter in input url -source: tests/data/test_export_wrong_content.txt -url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.{colorama.Fore.RESET} -''' - assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT - assert captured.err == expected_stderr + assert captured.err == EXPECTED_STDERR_COLORED_OTP_URL_WRONG def test_one_wrong_line(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None: @@ -997,6 +964,46 @@ def test_img_qr_reader_from_file_happy_path(capsys: pytest.CaptureFixture[str]) assert captured.err == '' +@pytest.mark.qreader +def test_img_qr_reader_but_no_otp_from_file(capsys: pytest.CaptureFixture[str]) -> None: + # Act + extract_otp_secrets.main(['-n', 'tests/data/qr_but_without_otp.png']) + + # Assert + captured = capsys.readouterr() + + assert captured.out == '' + assert captured.err == EXPECTED_STDERR_NO_OTP_URL + + +@pytest.mark.qreader +def test_img_qr_reader_from_wildcard(capsys: pytest.CaptureFixture[str]) -> None: + # Act + extract_otp_secrets.main(['-n', 'tests/data/*.png']) + + # Assert + captured = capsys.readouterr() + + assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG + assert normalize_testfile_path(captured.err) == EXPECTED_STDERR_NO_OTP_URL + + +def normalize_testfile_path(text: str): + return text.replace('tests/data\\', 'tests/data/') if sys.platform.startswith("win") else text + + +@pytest.mark.qreader +def test_img_qr_reader_from_multiple_files(capsys: pytest.CaptureFixture[str]) -> None: + # Act + extract_otp_secrets.main(['-n', 'tests/data/test_googleauth_export.png', 'tests/data/text_masquerading_as_image.jpeg']) + + # Assert + captured = capsys.readouterr() + + assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG + assert captured.err == EXPECTED_STDERR_BAD_IMAGE + + @pytest.mark.qreader def test_img_qr_reader_by_parameter(capsys: pytest.CaptureFixture[str], qr_mode: str) -> None: # Act @@ -1041,24 +1048,7 @@ def test_img_qr_reader_from_stdin(capsys: pytest.CaptureFixture[str], monkeypatc # Assert captured = capsys.readouterr() - expected_stdout = '''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 - -''' - - assert captured.out == expected_stdout + assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG assert captured.err == '' @@ -1143,19 +1133,9 @@ def test_non_image_file(capsys: pytest.CaptureFixture[str]) -> None: # Assert captured = capsys.readouterr() - expected_stderr = ''' -WARN: input is not a otpauth-migration:// url -source: tests/data/text_masquerading_as_image.jpeg -input: This is just a text file masquerading as an image file. -Maybe a wrong file was given -ERROR: could not parse query parameter in input url -source: tests/data/text_masquerading_as_image.jpeg -url: This is just a text file masquerading as an image file. -''' - - assert captured.err == expected_stderr assert captured.out == '' + assert captured.err == EXPECTED_STDERR_BAD_IMAGE def test_next_valid_qr_mode() -> None: @@ -1209,3 +1189,47 @@ Issuer: Test3 Type: totp ''' + +EXPECTED_STDERR_OTP_URL_WRONG = ''' +WARN: input is not a otpauth-migration:// url +source: tests/data/test_export_wrong_content.txt +input: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. +Maybe a wrong file was given + +ERROR: could not parse query parameter in input url +source: tests/data/test_export_wrong_content.txt +url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. +''' + +EXPECTED_STDERR_COLORED_OTP_URL_WRONG = f'''{colorama.Fore.RED} +WARN: input is not a otpauth-migration:// url +source: tests/data/test_export_wrong_content.txt +input: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. +Maybe a wrong file was given{colorama.Fore.RESET} +{colorama.Fore.RED} +ERROR: could not parse query parameter in input url +source: tests/data/test_export_wrong_content.txt +url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.{colorama.Fore.RESET} +''' + +EXPECTED_STDERR_NO_OTP_URL = ''' +WARN: input is not a otpauth-migration:// url +source: tests/data/qr_but_without_otp.png +input: NOT A otpauth-migration:// URL +Maybe a wrong file was given + +ERROR: could not parse query parameter in input url +source: tests/data/qr_but_without_otp.png +url: NOT A otpauth-migration:// URL +''' + +EXPECTED_STDERR_BAD_IMAGE = ''' +WARN: input is not a otpauth-migration:// url +source: tests/data/text_masquerading_as_image.jpeg +input: This is just a text file masquerading as an image file. +Maybe a wrong file was given + +ERROR: could not parse query parameter in input url +source: tests/data/text_masquerading_as_image.jpeg +url: This is just a text file masquerading as an image file. +'''