change to src-layout

cv2_1
scito 1 year ago
parent 7f5d4b37ee
commit 3e4476e317

1
.gitignore vendored

@ -15,6 +15,7 @@ venv/
!.devcontainer/*.json
*.whl
build/
dist/
extract_otp_secret_keys.egg-info/
*.xml
pytest-coverage.txt

@ -12,6 +12,9 @@
---
TODO add src/
TODO rename extract_otp_secret_keys
Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of "Google Authenticator" app.
The secret and otp values can be printed and exported to json or csv. The QR codes can be printed or saved as PNG images.
@ -293,6 +296,12 @@ wget https://raw.githubusercontent.com/scito/extract_otp_secret_keys/master/exam
python -m extract_otp_secret_keys example_export.txt
```
### local pip
```
pip install -e .
```
### pipenv
You can you use [Pipenv](https://github.com/pypa/pipenv) for running extract_otp_secret_keys.
@ -385,6 +394,16 @@ or
python -m pytest
```
#### Hints
Your tests can run against an installed version after executing pip install .
Your tests can run against the local copy with an editable install after executing pip install --editable .
If you dont use an editable install and are relying on the fact that Python by default puts the current directory in sys.path to import your package, you can execute python -m pytest to execute the tests against the local copy directly, without using pip.
https://docs.pytest.org/en/7.1.x/explanation/pythonpath.html#pytest-vs-python-m-pytest
### unittest
There are basic [unittest](https://docs.python.org/3.10/library/unittest.html)s, see `test_extract_otp_secret_keys_unittest.py`.
@ -404,7 +423,22 @@ Setup for running the tests in VSCode.
3. Choose unittest or pytest. (pytest is recommended, both are supported)
4. Set ". Root" directory
## Maintenance
## Development
### Build
```
pip install -e .
python src/extract_otp_secret_keys.py
pip wheel .
# --isolated
# --prefer-binary
python3.11 -m build --wheel
# =
pip wheel --no-deps .
```
### Upgrade pip Packages

@ -54,4 +54,9 @@ py-modules = ["extract_otp_secret_keys", "protobuf_generated_python.google_auth_
# TODO version = {attr = "extract_otp_secret_keys.VERSION"}
[tool.setuptools.package-data]
"*" = ["*.txt", "*.json", "*.png", "*.md"]
"." = ["*.txt", "*.json", "*.csv", "*.png", "*.md"]
# https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure%3E
# https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#which-import-mode
[tool.pytest.ini_options]
addopts = [ "--import-mode=importlib", ]

@ -1,36 +1,5 @@
[metadata]
name = extract_otp_secret_keys
version = 1.6.0
long_description = file: README.md
description = Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of "Google Authenticator" app
long_description_content_type = text/markdown
url = https://github.com/scito/extract_otp_secret_keys
author = scito
author_email = info@scito.ch
maintainer = scito
maintainer_email = info@scito.ch
license = GNU General Public License v3 (GPLv3)
keywords = python security json otp csv protobuf qrcode two-factor totp google-authenticator recovery proto3 mfa two-factor-authentication tfa qr-codes otpauth 2fa security-tools
# https://pypi.org/classifiers/
classifiers =
Development Status :: 5 - Production/Stable
Environment :: Console
Environment :: X11 Applications :: Qt
Environment :: Win32 (MS Windows)
Topic :: System :: Archiving :: Backup
Topic :: Utilities
Programming Language :: Python :: 3.7
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Intended Audience :: End Users/Desktop
Intended Audience :: Developers
Intended Audience :: System Administrators
Programming Language :: Python
Natural Language :: English
License :: OSI Approved :: GNU General Public License v3 (GPLv3)
[options]
python_requires = >=3.7, <4
@ -45,21 +14,7 @@ py_modules = extract_otp_secret_keys, protobuf_generated_python.google_auth_pb2
# typing_extensions;python_version<='3.7'
packages = find:
package_dir =
= .
project_urls =
Bug Reports = https://github.com/scito/extract_otp_secret_keys/issues
Source = https://github.com/scito/extract_otp_secret_keys
# [options.entry_points]
# console_scripts =
# extract_otp_secret_keys = extract_otp_secret_keys:sys_main
= src
[options.packages.find]
where = "."
[options.package_data]
* =
*.txt
*.json
*.png
*.md
where = src

@ -1,5 +1,5 @@
#!/usr/bin/python3
from setuptools import setup
# compatibility with legacy builds or versions of tools that dont support certain packaging standards
setup(
)
setup()

Before

Width:  |  Height:  |  Size: 653 KiB

After

Width:  |  Height:  |  Size: 653 KiB

@ -50,12 +50,12 @@ def test_extract_stdout(capsys: pytest.CaptureFixture[str]) -> None:
def test_extract_non_existent_file(capsys: pytest.CaptureFixture[str]) -> None:
# Act
with pytest.raises(SystemExit) as e:
extract_otp_secret_keys.main(['test/non_existent_file.txt'])
extract_otp_secret_keys.main(['non_existent_file.txt'])
# Assert
captured = capsys.readouterr()
expected_stderr = '\nERROR: Input file provided is non-existent or not a file.\ninput file: test/non_existent_file.txt\n'
expected_stderr = '\nERROR: Input file provided is non-existent or not a file.\ninput file: non_existent_file.txt\n'
assert captured.err == expected_stderr
assert captured.out == ''
@ -96,12 +96,12 @@ def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> No
if qreader_available:
# Act
with pytest.raises(SystemExit) as e:
extract_otp_secret_keys.main(['test/empty_file.txt'])
extract_otp_secret_keys.main(['tests/data/empty_file.txt'])
# Assert
captured = capsys.readouterr()
expected_stderr = 'WARN: test/empty_file.txt is empty\n\nERROR: Unable to open file for reading.\ninput file: test/empty_file.txt\n'
expected_stderr = 'WARN: tests/data/empty_file.txt is empty\n\nERROR: Unable to open file for reading.\ninput file: tests/data/empty_file.txt\n'
assert captured.err == expected_stderr
assert captured.out == ''
@ -109,7 +109,7 @@ def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> No
assert e.type == SystemExit
else:
# Act
extract_otp_secret_keys.main(['test/empty_file.txt'])
extract_otp_secret_keys.main(['tests/data/empty_file.txt'])
# Assert
captured = capsys.readouterr()
@ -214,7 +214,7 @@ def test_keepass_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path)
def test_keepass_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
'''Two csv files .totp and .htop are generated.'''
# Act
extract_otp_secret_keys.main(['-k', '-', 'test/example_export_only_totp.txt'])
extract_otp_secret_keys.main(['-k', '-', 'tests/data/example_export_only_totp.txt'])
# Assert
expected_totp_csv = read_csv('example_keepass_output.totp.csv')
@ -232,7 +232,7 @@ def test_keepass_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
def test_single_keepass_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
'''Does not add .totp or .hotp pre-suffix'''
# Act
extract_otp_secret_keys.main(['-q', '-k', str(tmp_path / 'test_example_keepass_output.csv'), 'test/example_export_only_totp.txt'])
extract_otp_secret_keys.main(['-q', '-k', str(tmp_path / 'test_example_keepass_output.csv'), 'tests/data/example_export_only_totp.txt'])
# Assert
expected_totp_csv = read_csv('example_keepass_output.totp.csv')
@ -283,7 +283,7 @@ def test_extract_json_stdout(capsys: pytest.CaptureFixture[str]) -> None:
def test_extract_not_encoded_plus(capsys: pytest.CaptureFixture[str]) -> None:
# Act
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
extract_otp_secret_keys.main(['tests/data/test_plus_problem_export.txt'])
# Assert
captured = capsys.readouterr()
@ -321,7 +321,7 @@ def test_extract_printqr(capsys: pytest.CaptureFixture[str]) -> None:
# Assert
captured = capsys.readouterr()
expected_stdout = read_file_to_str('test/printqr_output.txt')
expected_stdout = read_file_to_str('tests/data/printqr_output.txt')
assert captured.out == expected_stdout
assert captured.err == ''
@ -355,7 +355,7 @@ def test_extract_verbose(capsys: pytest.CaptureFixture[str], relaxed: bool) -> N
# Assert
captured = capsys.readouterr()
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
expected_stdout = read_file_to_str('tests/data/print_verbose_output.txt')
if not qreader_available:
expected_stdout = expected_stdout.replace('QReader installed: True', 'QReader installed: False')
@ -377,7 +377,7 @@ def test_extract_debug(capsys: pytest.CaptureFixture[str]) -> None:
# Assert
captured = capsys.readouterr()
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
expected_stdout = read_file_to_str('tests/data/print_verbose_output.txt')
assert len(captured.out) > len(expected_stdout)
assert "DEBUG: " in captured.out
@ -450,7 +450,7 @@ def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
def test_wrong_data(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as e:
# Act
extract_otp_secret_keys.main(['test/test_export_wrong_data.txt'])
extract_otp_secret_keys.main(['tests/data/test_export_wrong_data.txt'])
# Assert
captured = capsys.readouterr()
@ -469,19 +469,19 @@ data=XXXX
def test_wrong_content(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as e:
# Act
extract_otp_secret_keys.main(['test/test_export_wrong_content.txt'])
extract_otp_secret_keys.main(['tests/data/test_export_wrong_content.txt'])
# Assert
captured = capsys.readouterr()
expected_stderr = '''
WARN: line is not a otpauth-migration:// URL
input: test/test_export_wrong_content.txt
input: tests/data/test_export_wrong_content.txt
line 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.'
Probably a wrong file was given
ERROR: no data query parameter in input URL
input file: test/test_export_wrong_content.txt
input file: tests/data/test_export_wrong_content.txt
line 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.'
Probably a wrong file was given
'''
@ -494,14 +494,14 @@ Probably a wrong file was given
def test_wrong_prefix(capsys: pytest.CaptureFixture[str]) -> None:
# Act
extract_otp_secret_keys.main(['test/test_export_wrong_prefix.txt'])
extract_otp_secret_keys.main(['tests/data/test_export_wrong_prefix.txt'])
# Assert
captured = capsys.readouterr()
expected_stderr = '''
WARN: line is not a otpauth-migration:// URL
input: test/test_export_wrong_prefix.txt
input: tests/data/test_export_wrong_prefix.txt
line 'QR-Code:otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B'
Probably a wrong file was given
'''
@ -526,7 +526,7 @@ def test_add_pre_suffix(capsys: pytest.CaptureFixture[str]) -> None:
@pytest.mark.qreader
def test_img_qr_reader_from_file_happy_path(capsys: pytest.CaptureFixture[str]) -> None:
# Act
extract_otp_secret_keys.main(['test/test_googleauth_export.png'])
extract_otp_secret_keys.main(['tests/data/test_googleauth_export.png'])
# Assert
captured = capsys.readouterr()
@ -540,9 +540,9 @@ def test_extract_multiple_files_and_mixed(capsys: pytest.CaptureFixture[str]) ->
# Act
extract_otp_secret_keys.main([
'example_export.txt',
'test/test_googleauth_export.png',
'tests/data/test_googleauth_export.png',
'example_export.txt',
'test/test_googleauth_export.png'])
'tests/data/test_googleauth_export.png'])
# Assert
captured = capsys.readouterr()
@ -555,7 +555,7 @@ def test_extract_multiple_files_and_mixed(capsys: pytest.CaptureFixture[str]) ->
def test_img_qr_reader_from_stdin(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
# sys.stdin.buffer should be monkey patched, but it does not work
monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('test/test_googleauth_export.png'))
monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('tests/data/test_googleauth_export.png'))
# Act
extract_otp_secret_keys.main(['='])
@ -588,7 +588,7 @@ Type: totp
def test_img_qr_reader_from_stdin_wrong_symbol(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
# sys.stdin.buffer should be monkey patched, but it does not work
monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('test/test_googleauth_export.png'))
monkeypatch.setattr('sys.stdin', read_binary_file_as_stream('tests/data/test_googleauth_export.png'))
# Act
with pytest.raises(SystemExit) as e:
@ -629,12 +629,12 @@ def test_extract_stdin_stdout_wrong_symbol(capsys: pytest.CaptureFixture[str], m
def test_img_qr_reader_no_qr_code_in_image(capsys: pytest.CaptureFixture[str]) -> None:
# Act
with pytest.raises(SystemExit) as e:
extract_otp_secret_keys.main(['test/lena_std.tif'])
extract_otp_secret_keys.main(['tests/data/lena_std.tif'])
# Assert
captured = capsys.readouterr()
expected_stderr = '\nERROR: Unable to read QR Code from file.\ninput file: test/lena_std.tif\n'
expected_stderr = '\nERROR: Unable to read QR Code from file.\ninput file: tests/data/lena_std.tif\n'
assert captured.err == expected_stderr
assert captured.out == ''
@ -646,12 +646,12 @@ def test_img_qr_reader_no_qr_code_in_image(capsys: pytest.CaptureFixture[str]) -
def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> None:
# Act
with pytest.raises(SystemExit) as e:
extract_otp_secret_keys.main(['test/nonexistent.bmp'])
extract_otp_secret_keys.main(['nonexistent.bmp'])
# Assert
captured = capsys.readouterr()
expected_stderr = '\nERROR: Input file provided is non-existent or not a file.\ninput file: test/nonexistent.bmp\n'
expected_stderr = '\nERROR: Input file provided is non-existent or not a file.\ninput file: nonexistent.bmp\n'
assert captured.err == expected_stderr
assert captured.out == ''
@ -662,18 +662,18 @@ def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> N
def test_non_image_file(capsys: pytest.CaptureFixture[str]) -> None:
# Act
with pytest.raises(SystemExit) as e:
extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg'])
extract_otp_secret_keys.main(['tests/data/text_masquerading_as_image.jpeg'])
# Assert
captured = capsys.readouterr()
expected_stderr = '''
WARN: line is not a otpauth-migration:// URL
input: test/text_masquerading_as_image.jpeg
input: tests/data/text_masquerading_as_image.jpeg
line 'This is just a text file masquerading as an image file.'
Probably a wrong file was given
ERROR: no data query parameter in input URL
input file: test/text_masquerading_as_image.jpeg
input file: tests/data/text_masquerading_as_image.jpeg
line 'This is just a text file masquerading as an image file.'
Probably a wrong file was given
'''

@ -123,7 +123,7 @@ Type: totp
def test_extract_not_encoded_plus(self) -> None:
out = io.StringIO()
with redirect_stdout(out):
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
extract_otp_secret_keys.main(['tests/data/test_plus_problem_export.txt'])
actual_output = out.getvalue()
expected_output = '''Name: SerenityLabs:test1@serenitylabs.co.uk
@ -155,7 +155,7 @@ Type: totp
extract_otp_secret_keys.main(['-p', 'example_export.txt'])
actual_output = out.getvalue()
expected_output = read_file_to_str('test/printqr_output.txt')
expected_output = read_file_to_str('tests/data/printqr_output.txt')
self.assertEqual(actual_output, expected_output)
@ -174,7 +174,7 @@ Type: totp
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
actual_output = out.getvalue()
expected_output = read_file_to_str('test/print_verbose_output.txt')
expected_output = read_file_to_str('tests/data/print_verbose_output.txt')
self.assertEqual(actual_output, expected_output)
@ -184,7 +184,7 @@ Type: totp
extract_otp_secret_keys.main(['-vvv', 'example_export.txt'])
actual_output = out.getvalue()
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
expected_stdout = read_file_to_str('tests/data/print_verbose_output.txt')
self.assertGreater(len(actual_output), len(expected_stdout))
self.assertTrue("DEBUG: " in actual_output)

@ -28,7 +28,7 @@ from utils import Capturing
class TestQRImageExtract(unittest.TestCase):
def test_img_qr_reader_happy_path(self) -> None:
with Capturing() as actual_output:
extract_otp_secret_keys.main(['test/test_googleauth_export.png'])
extract_otp_secret_keys.main(['tests/data/test_googleauth_export.png'])
expected_output =\
['Name: Test1:test1@example1.com', 'Secret: JBSWY3DPEHPK3PXP', 'Issuer: Test1', 'Type: totp', '',
@ -40,9 +40,9 @@ class TestQRImageExtract(unittest.TestCase):
def test_img_qr_reader_no_qr_code_in_image(self) -> None:
with Capturing() as actual_output:
with self.assertRaises(SystemExit) as context:
extract_otp_secret_keys.main(['test/lena_std.tif'])
extract_otp_secret_keys.main(['tests/data/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: tests/data/lena_std.tif']
self.assertEqual(actual_output, expected_output)
self.assertEqual(context.exception.code, 1)
@ -50,9 +50,9 @@ class TestQRImageExtract(unittest.TestCase):
def test_img_qr_reader_nonexistent_file(self) -> None:
with Capturing() as actual_output:
with self.assertRaises(SystemExit) as context:
extract_otp_secret_keys.main(['test/nonexistent.bmp'])
extract_otp_secret_keys.main(['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: nonexistent.bmp']
self.assertEqual(actual_output, expected_output)
self.assertEqual(context.exception.code, 1)
@ -60,17 +60,17 @@ class TestQRImageExtract(unittest.TestCase):
def test_img_qr_reader_non_image_file(self) -> None:
with Capturing() as actual_output:
with self.assertRaises(SystemExit) as context:
extract_otp_secret_keys.main(['test/text_masquerading_as_image.jpeg'])
extract_otp_secret_keys.main(['tests/data/text_masquerading_as_image.jpeg'])
expected_output = [
'',
'WARN: line is not a otpauth-migration:// URL',
'input: test/text_masquerading_as_image.jpeg',
'input: tests/data/text_masquerading_as_image.jpeg',
"line 'This is just a text file masquerading as an image file.'",
'Probably a wrong file was given',
'',
'ERROR: no data query parameter in input URL',
'input file: test/text_masquerading_as_image.jpeg',
'input file: tests/data/text_masquerading_as_image.jpeg',
"line 'This is just a text file masquerading as an image file.'",
'Probably a wrong file was given'
]
Loading…
Cancel
Save