Compare commits

..

159 Commits

Author SHA1 Message Date
scito
66c68fd9ef fix Python 3.7 compatibility 2023-01-02 22:41:30 +01:00
scito
6cfbc10e69 build wheel 2023-01-02 22:18:54 +01:00
scito
064fe81b2e docker remove python3-opencv, but add required libs; add debug mode 2023-01-02 22:11:56 +01:00
scito
cd5160f123 fix Alpine tests 2023-01-02 20:29:37 +01:00
scito
e1c8568ba2 fix protoc generation 2023-01-02 19:28:11 +01:00
scito
ef0fbc3586 update README 2023-01-02 19:09:28 +01:00
scito
b86c4f9a61 add ignore duplicate entries option and add quiet test; fixed -k stdout 2023-01-02 17:30:46 +01:00
scito
722009b172 build: add generate result files option 2023-01-02 14:16:30 +01:00
scito
1c106150b0 sort docker imports 2023-01-02 13:53:29 +01:00
scito
77e23b4ae4 refactor cv2 window and logging
- increase font and cut if too long
- refactor logging
- extract key handling
- refactor big methods
2023-01-02 13:52:04 +01:00
scito
fe3e371897 build: print restult at the end 2023-01-02 12:50:41 +01:00
scito
7c6d341270 rename github urls; update descriptions 2023-01-01 23:21:29 +01:00
scito
160a558825 some renaming and README updating 2023-01-01 22:47:12 +01:00
scito
8d8b993f12 add CV2 screenshot 2023-01-01 20:15:38 +01:00
scito
e177f860e1 abort instead of assertion 2023-01-01 19:55:28 +01:00
scito
8545dab7a5 extract draw_box and print_text functions 2023-01-01 19:22:46 +01:00
scito
16047a5b15 fix test compatibility with Alpine Linux 2023-01-01 19:19:41 +01:00
scito
604c461549 rename to build.sh 2023-01-01 19:19:21 +01:00
scito
f5acd1dee9 test docker locally after build, easier to reproduce 2023-01-01 17:56:06 +01:00
scito
1086e28056 fix pylint and mypy problems 2023-01-01 17:45:50 +01:00
scito
2c0cfd83ee docker: pip install -U 2023-01-01 17:38:08 +01:00
scito
a3bda6ff8e make PYTHON workaround uniform 2023-01-01 17:25:24 +01:00
scito
67c4f737c4 do catch AssertionError in camera capture 2023-01-01 15:21:18 +01:00
scito
fff74fc638 handle errors while reading QR from camera; log errors 2023-01-01 15:12:24 +01:00
scito
19c8e9df23 update docs 2023-01-01 10:03:44 +01:00
scito
13fcdcd022 fix problem of outdated colorama 2023-01-01 01:22:06 +01:00
scito
91b9b3671c ci: add missing colorama 2023-01-01 01:02:39 +01:00
scito
be6b9c8a7c ci: mypy only for latest Python on ubuntu 2023-01-01 00:57:18 +01:00
scito
3d61f1d316 ci: use pip install -e . for smoke tests 2023-01-01 00:56:19 +01:00
scito
a8559db6e0 ci: mypy only for latest Python on ubuntu 2023-01-01 00:52:28 +01:00
scito
9f725b227f ci: use requirements.txt for smoke tests 2023-01-01 00:48:52 +01:00
scito
869c404489 fix fileinput.input encoding only since Python 3.10 2023-01-01 00:41:11 +01:00
scito
003e122808 ignore types for stdout.reconfigure 2023-01-01 00:22:09 +01:00
scito
b3fc854078 colored warn and error messages
- add log_warn() and log_error()
- adapt tests
2023-01-01 00:14:56 +01:00
scito
fc1619d9c7 disable verbose unittest for Windows 2022-12-31 21:34:28 +01:00
scito
5be6e9c322 fix finput encoding problem on Windows 2022-12-31 21:27:40 +01:00
scito
739ae4c012 fix pytest.skipif 2022-12-31 21:16:19 +01:00
scito
1af6fe3161 fix camera type and enhance readme with pyzbar problem 2022-12-31 21:02:05 +01:00
scito
e311386a15 skip verbose tests for windows as there are encoding problems 2022-12-31 20:42:40 +01:00
scito
496564a605 avoid AttributeError in tests
StringIO in tests do not have all attributes, ignore it
2022-12-31 20:19:21 +01:00
scito
6406fcaef7 set encoding to utf-8 for stdout, needed for Windows 2022-12-31 19:52:09 +01:00
scito
7bb92f00b2 avoid mypy problems for duplicate Final
src/extract_otp_secrets.py:63: error: Incompatible import of "Final"
(imported name has type "typing_extensions._SpecialForm", local name has type "typing._SpecialForm")
[assignment]
2022-12-31 19:42:53 +01:00
scito
965f721caf ci_docker: run smoke tests before building 2022-12-31 19:20:01 +01:00
scito
61cca0c476 import Final from __future__ for Python 3.7 2022-12-31 19:11:37 +01:00
scito
ebd4d61f5f try workaround for Python 3.7: must use () for assignments 2022-12-31 19:06:57 +01:00
scito
e058010be3 fix docker invalid tags from comments 2022-12-31 18:59:01 +01:00
scito
463a9851be try workaround for Python 3.7: avoid Final in tuple assignments 2022-12-31 18:55:16 +01:00
scito
dcbb128e7c try workaround for Python 3.7: do not use Types in Final 2022-12-31 18:51:26 +01:00
scito
1b572fc9ab fix pipenv problem 2022-12-31 18:40:26 +01:00
scito
c3e9883216 try workaround for Python 3.7 NORMAL_COLOR = 255, 0, 255 problem 2022-12-31 18:21:13 +01:00
scito
3f9f7d2b8a better formatting of help 2022-12-31 18:00:49 +01:00
scito
0212e54f42 ci: runs on windows, exclude linting for Python 3.7 2022-12-31 17:52:19 +01:00
scito
3558eba93b use detect_and_decode since qrreader is fixed 2022-12-31 17:30:24 +01:00
scito
5225af0621 update README with --qr option 2022-12-31 16:42:12 +01:00
scito
1f04dd71e2 allow to choose qr reader for images 2022-12-31 15:54:21 +01:00
scito
2dea161cdc add argument for initial setting of qr_mode 2022-12-31 12:44:16 +01:00
scito
f731530f57 improve handling of wrong urls
- adapt tests
- improve messages for files
- show red box camera
2022-12-31 11:32:07 +01:00
scito
4c0bb8dc61 vscode debug: fix path to script: add missing src/ 2022-12-31 11:26:11 +01:00
scito
ad9c4a22db ci_docker: upload to only one repo 2022-12-31 11:25:33 +01:00
scito
2cdf2480a0 make bulding docker optional in script 2022-12-31 11:25:02 +01:00
scito
5aa1a35b8f add licence label to docker images 2022-12-31 11:23:44 +01:00
scito
3f3903cc81 add version to PYTHON todos 2022-12-30 21:22:03 +01:00
scito
97e4f084cb mv docker extract_otp_secrets_no_qr_reader -> extract_otp_secrets_only_txt 2022-12-30 20:43:51 +01:00
scito
549c128fb7 renaming extract_otp_secret_keys -> extract_otp_secrets and test file names 2022-12-30 20:37:38 +01:00
scito
10ff533a42 downgrade opencv for macos in requirements.txt and Pipfile 2022-12-30 19:26:45 +01:00
scito
7eb6f036ab ci: tryout downgrade opencv for macos 2022-12-30 18:51:13 +01:00
scito
652ecf57f0 ci: workaround macOS pytest segfauls -> exclude macOS (2) 2022-12-30 18:33:26 +01:00
scito
9592e6ebfe ci: workaround macOS pytest segfauls -> exclude macOS 2022-12-30 18:29:23 +01:00
scito
d6c285e59d ci: segfault: try python -m pytest 2022-12-30 18:23:31 +01:00
scito
5eed47364e ci: revert try out changes 2022-12-30 18:18:15 +01:00
scito
26e4632f90 ci: another try, install module 2022-12-30 18:14:16 +01:00
scito
c84ca46861 ci: try to avoid random test failures 2022-12-30 18:09:25 +01:00
scito
63f5ab37c4 docu pyproject.toml 2022-12-30 18:02:22 +01:00
scito
f97d7143c5 ci: fix mypy and pytest-cov after clean 2022-12-30 17:49:46 +01:00
scito
0566683203 reenable pytest --import-mode=importlib 2022-12-30 17:31:34 +01:00
scito
ee404576d5 set package_dir=src and clean option 2022-12-30 17:14:49 +01:00
scito
60d7362eee add protobuf_generated_python to py-modules 2022-12-30 15:47:02 +01:00
scito
1beba7587f enable setuptools-git-versioning 2022-12-30 15:44:58 +01:00
scito
144c9e6320 fixes after change to src-layout 2022-12-30 15:31:41 +01:00
scito
3e4476e317 change to src-layout 2022-12-30 12:37:05 +01:00
scito
7f5d4b37ee initial pyproject.toml 2022-12-30 11:14:15 +01:00
scito
82e43172c3 update setup.py 2022-12-30 09:23:31 +01:00
scito
149a548610 upgrade opencv 4.7.0 & run program at the end 2022-12-30 08:14:13 +01:00
scito
d8de89de36 improve README, add docker ci badge 2022-12-30 02:30:16 +01:00
scito
3c164fea28 coverage from yellowgreen to brightgreen 2022-12-30 02:01:10 +01:00
scito
23d8cfa151 ci: Pytest coverage comment only for 3.x and ubuntu-latest 2022-12-30 01:58:54 +01:00
scito
f5ee59368e python 3.7 compatibility: use TypedDict from typing_extensions 2022-12-30 01:44:11 +01:00
scito
b2a877061c add typing_extensions for compatibility 2022-12-30 01:37:13 +01:00
scito
c525c06480 quit on window close click 2022-12-30 01:22:12 +01:00
scito
fb43c6793c type hinting fixes 2022-12-30 01:22:05 +01:00
scito
58fc1b85ac type compatibility for Python < 3.11 2022-12-30 01:07:39 +01:00
scito
04d864c093 add code coverage bade & ci: fix pytest-cov 2022-12-30 00:58:52 +01:00
scito
51094a1a18 use PathLike type instead of str | Path 2022-12-29 23:17:31 +01:00
scito
a5768ba1e6 Workaround for PYTHON < 3.10: use Union[int, None] instead of int | None 2022-12-29 22:34:07 +01:00
scito
faafb61241 fix type hint compatibility with Python < 3.11 2022-12-29 22:12:07 +01:00
scito
d5a088135e ci: if matrix.python-version 2022-12-29 22:04:52 +01:00
scito
45a9693586 ci: fix if ${{ matrix.python-version }} 2022-12-29 21:58:44 +01:00
scito
66b41d86e6 ci: use ${{ }} for mypy python check 2022-12-29 21:52:54 +01:00
scito
89564448c6 ci: fix mypy if condition 2022-12-29 21:41:11 +01:00
scito
9ab33bd02b sort imports 2022-12-29 21:34:14 +01:00
scito
f4ab540283 rename protobuf module as pb 2022-12-29 21:32:19 +01:00
scito
201e6510f8 add type hints (Python 3.11) 2022-12-29 21:29:20 +01:00
scito
f933cd0d32 initial mypy type checking 2022-12-29 16:30:18 +01:00
scito
f4389ca8a3 fix linting 2022-12-29 15:52:17 +01:00
scito
b89a338246 enable mypy type checking 2022-12-29 14:24:12 +01:00
scito
631bacc409 use tmp_path fixture instead of clean_up() in pytests 2022-12-29 13:44:19 +01:00
scito
833afa7c13 use 3.11-slim-bullseye, add missing package 2022-12-29 11:50:19 +01:00
scito
4209a5db3d move abort to end, use SystemExit instead; improve colors 2022-12-29 11:18:57 +01:00
scito
d9a4c7ca9f fix docker image digest 2022-12-29 04:36:31 +01:00
scito
829fe65b1e ci: pip install pytest-mock 2022-12-29 04:24:00 +01:00
scito
c90526dcf2 fix undefined name 'abort' and 'qreader' 2022-12-29 04:15:36 +01:00
scito
47e84e4462 run docker capture version 2022-12-29 03:19:09 +01:00
scito
b4931856ba apk add --no-cache nano zlib jpeg && re-enable tests 2022-12-29 02:34:29 +01:00
scito
f532dc668d indent_size = 2 for yml 2022-12-29 01:59:54 +01:00
scito
1dee86668a rename ARG run_tests to RUN_TESTS 2022-12-29 01:48:00 +01:00
scito
aa0de699fe disable tests for alpine 2022-12-29 01:43:22 +01:00
scito
7e684ff19e --build-arg run_tests=false 2022-12-29 01:35:43 +01:00
scito
b159b9e70d apk add musl-dev and apk del .build-deps 2022-12-29 01:13:48 +01:00
scito
951878d027 apk add gcc 2022-12-29 01:02:41 +01:00
scito
2a44bbfa27 use $(apk --print-arch) 2022-12-29 00:53:47 +01:00
scito
540ae7438d use $(uname -m) 2022-12-29 00:42:13 +01:00
scito
c346c085b6 apk add python3-dev py3-setuptools 2022-12-29 00:33:52 +01:00
scito
7cb3b2ac21 always apk add on alpine 2022-12-29 00:29:26 +01:00
scito
0eb5014eb0 ci_docker: add docker/setup-buildx-action@v2 2022-12-29 00:25:29 +01:00
scito
d4f5eb243e change to "$TARGETARCH" == "arm64" 2022-12-29 00:02:14 +01:00
scito
b05decc10f apk add zlib-dev jpeg-dev only for alpine linux/arm64 2022-12-28 23:58:09 +01:00
scito
21ebccbba5 apk add zlib-dev 2022-12-28 23:44:30 +01:00
scito
912825034f add zlib for alpine/arm64 docker image 2022-12-28 23:37:54 +01:00
scito
95e7d73173 mock no_args tests 2022-12-28 23:37:21 +01:00
scito
9f0872c2d0 extract from camera
- add help description
- use f-strings
- handle plural correctly
- rename methods, use otp_url instead of line
- remove importlib.util
- move cv2 imports to top
- remove unnecessary global delcarations
- group image tests
2022-12-28 22:43:40 +01:00
scito
7964c687f6 make running tests optional in docker build 2022-12-28 09:02:02 +01:00
scito
1d0b568b1e setup QEMU 2022-12-27 11:36:53 +01:00
scito
aaa7bd3da1 build also on arm64 2022-12-27 10:34:32 +01:00
scito
5ab5f84ff3 add docker label for source 2022-12-27 01:11:36 +01:00
scito
a4c4badd54 fix docker hub username 2022-12-27 00:57:49 +01:00
scito
f272c35a1f enable docker push in ci_docker 2022-12-27 00:52:14 +01:00
scito
e4e5271c0f github actions docker build no_qr_reader 2022-12-27 00:43:30 +01:00
scito
158564e79a fix to_bytes() mandatory byte_order 2022-12-27 00:18:37 +01:00
scito
672d18a5ca build docker images, run tests in docker build
- qreader_available flag
- echo commands in upgrade_deps.sh
2022-12-26 23:57:38 +01:00
scito
0490e227e1 docker image with qreader, 2nd image without qreader
- organize imports
- add qreader pytest.mark
- relaxed mode for pytest
- run tests in docker
- more tests
2022-12-26 18:31:09 +01:00
scito
2bcaa35251 organize imports 2022-12-25 11:00:15 +01:00
scito
b0b4c29e7b improve README 2022-12-24 15:50:44 +01:00
scito
e754befb52 refactor; update setup.py
more verbose logging
better error messages
2022-12-24 15:30:17 +01:00
scito
06b8efff62 add zypper and dnf for libzbar0 2022-12-24 05:31:17 +01:00
scito
5d0feacdba update README 2022-12-24 05:12:52 +01:00
scito
343520acb8 support multiple infiles 2022-12-24 04:48:12 +01:00
scito
c2d7c905ff handle wrong stdin streams 2022-12-24 04:19:43 +01:00
scito
bc329e24d5 renable libzbar 2022-12-24 03:29:14 +01:00
scito
4612ab6e7f catch zbar import error 2022-12-24 03:25:10 +01:00
scito
05db190de3 test qreader import 2022-12-24 03:12:06 +01:00
scito
0ad3c2d8ed change name to nightly tests 2022-12-24 03:06:03 +01:00
scito
31bb2909da fix ci jobs 2022-12-24 03:05:07 +01:00
scito
c1a55fb874 test pypy only in nightly builds 2022-12-24 03:02:08 +01:00
scito
82da427d1a refactor code: extract method 2022-12-24 02:56:40 +01:00
scito
af0d7ffd5d dynamic import of QReader
since this module has a dependency to zbar lib
2022-12-24 02:46:36 +01:00
scito
9a308b148f fix macOS ci 2022-12-24 02:37:16 +01:00
scito
cd07851e30 install zbar lib 2022-12-24 02:29:43 +01:00
scito
f4934192ae WIP 2022-12-24 01:59:35 +01:00
qwertyca
483fcc0163 Add the ability so provide an image file as the infile. If the file contains a QR code generated by Google Authenticator's "Transfer Accounts" function, it will be decoded directly in a single step. This is meant to help users who need to access their secrets from Google Authenticator but don't have a QR code decoder and don't want to use an online one due to security concerns. 2022-12-23 10:17:04 +01:00
20 changed files with 317 additions and 626 deletions

View File

@@ -2,8 +2,6 @@ name: tests
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
# https://docs.github.com/en/actions/using-workflows
# https://docs.github.com/en/actions/learn-github-actions/contexts
# https://docs.github.com/en/actions/learn-github-actions/expressions
on:
push:
@@ -67,7 +65,4 @@ jobs:
with:
pytest-coverage-path: ./pytest-coverage.txt
junitxml-path: ./pytest.xml
if: |
matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
&& !contains(github.ref, 'refs/tags/')
if: matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'

View File

@@ -2,8 +2,6 @@ name: docker
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
# https://docs.github.com/en/actions/using-workflows
# https://docs.github.com/en/actions/learn-github-actions/contexts
# https://docs.github.com/en/actions/learn-github-actions/expressions
# How to setup: https://event-driven.io/en/how_to_buid_and_push_docker_image_with_github_actions/
# How to run: https://aschmelyun.com/blog/using-docker-run-inside-of-github-actions/
@@ -11,6 +9,7 @@ name: docker
on:
# run it on push to the default repository branch
push:
branches: [master]
schedule:
# Run weekly on default branch
- cron: '47 3 * * 6'

22
Pipfile
View File

@@ -4,26 +4,24 @@ verify_ssl = true
name = "pypi"
[packages]
colorama = ">=0.4.6"
opencv-contrib-python = "*"
# for macOS: opencv-contrib-python = "<=4.7.0"
# for PYTHON <= 3.7: typing_extensions = "*"
pillow = "*"
protobuf = "*"
qrcode = "*"
pillow = "*"
qreader = "*"
opencv-contrib-python = "*"
colorama = ">=0.4.6"
# for macOS: opencv-contrib-python = "<=4.7.0"
# for PYTHON <= 3.7: typing_extensions = "*"
[dev-packages]
build = "*"
flake8 = "*"
mypy = "*"
mypy-protobuf = "*"
pylint = "*"
pytest = "*"
pytest-cov = "*"
pytest-mock = "*"
types-protobuf = "*"
pytest-cov = "*"
wheel = "*"
flake8 = "*"
pylint = "*"
mypy = "*"
types-protobuf = "*"
[requires]
python_version = "3.11"

150
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "13e2cc849fbc56593a2179a51a091717bcd4baeb9235b0843bb3abd8a9d1c698"
"sha256": "25b244c44cb891ac15ef20c4011eb043b87fb1f112396d68f470d0bb362e97f7"
},
"pipfile-spec": 6,
"requires": {
@@ -220,73 +220,65 @@
"markers": "python_version >= '3.6'",
"version": "==22.2.0"
},
"build": {
"hashes": [
"sha256:1a07724e891cbd898923145eb7752ee7653674c511378eb9c7691aab1612bc3c",
"sha256:38a7a2b7a0bdc61a42a0a67509d88c71ecfc37b393baba770fae34e20929ff69"
],
"index": "pypi",
"version": "==0.9.0"
},
"coverage": {
"extras": [
"toml"
],
"hashes": [
"sha256:037b51ee86bc600f99b3b957c20a172431c35c2ef9c1ca34bc813ab5b51fd9f5",
"sha256:0bce4ad5bdd0b02e177a085d28d2cea5fc57bb4ba2cead395e763e34cf934eb1",
"sha256:112cfead1bd22eada8a8db9ed387bd3e8be5528debc42b5d3c1f7da4ffaf9fb5",
"sha256:13121fa22dcd2c7b19c5161e3fd725692448f05377b788da4502a383573227b3",
"sha256:18b09811f849cc958d23f733a350a66b54a8de3fed1e6128ba55a5c97ffb6f65",
"sha256:25fde928306034e8deecd5fc91a07432dcc282c8acb76749581a28963c9f4f3f",
"sha256:2b865aa679bee7fbd1c55960940dbd3252621dd81468268786c67122bbd15343",
"sha256:2f7c51b6074a8a3063c341953dffe48fd6674f8e4b1d3c8aa8a91f58d6e716a8",
"sha256:349d0b545520e8516f7b4f12373afc705d17d901e1de6a37a20e4ec9332b61f7",
"sha256:44d6a556de4418f1f3bfd57094b8c49f0408df5a433cf0d253eeb3075261c762",
"sha256:4959dc506be74e4963bd2c42f7b87d8e4b289891201e19ec551e64c6aa5441f8",
"sha256:49da0ff241827ebb52d5d6d5a36d33b455fa5e721d44689c95df99fd8db82437",
"sha256:55e46fa4168ccb7497c9be78627fcb147e06f474f846a10d55feeb5108a24ef0",
"sha256:5722269ed05fbdb94eef431787c66b66260ff3125d1a9afcc00facff8c45adf9",
"sha256:628f47eaf66727fc986d3b190d6fa32f5e6b7754a243919d28bc0fd7974c449f",
"sha256:62ef3800c4058844e2e3fa35faa9dd0ccde8a8aba6c763aae50342e00d4479d4",
"sha256:75e43c6f4ea4d122dac389aabdf9d4f0e160770a75e63372f88005d90f5bcc80",
"sha256:7e8b0642c38b3d3b3c01417643ccc645345b03c32a2e84ef93cdd6844d6fe530",
"sha256:88834e5d56d01c141c29deedacba5773fe0bed900b1edc957595a8a6c0da1c3c",
"sha256:985ad2af5ec3dbb4fd75d5b0735752c527ad183455520055a08cf8d6794cabfc",
"sha256:a530663a361eb27375cec28aea5cd282089b5e4b022ae451c4c3493b026a68a5",
"sha256:acef7f3a3825a2d218a03dd02f5f3cc7f27aa31d882dd780191d1ad101120d74",
"sha256:ae871d09901911eedda1981ea6fd0f62a999107293cdc4c4fd612321c5b34745",
"sha256:af6cef3796b8068713a48dd67d258dc9a6e2ebc3bd4645bfac03a09672fa5d20",
"sha256:af87e906355fa42447be5c08c5d44e6e1c005bf142f303f726ddf5ed6e0c8a4d",
"sha256:b07651e3b9af8f1a092861d88b4c74d913634a7f1f2280fca0ad041ad84e9e96",
"sha256:b1ffc8f58b81baed3f8962e28c30d99442079b82ce1ec836a1f67c0accad91c1",
"sha256:b5b38813eee5b4739f505d94247604c72eae626d5088a16dd77b08b8b1724ab3",
"sha256:b791beb17b32ac019a78cfbe6184f992b6273fdca31145b928ad2099435e2fcb",
"sha256:b82343a5bc51627b9d606f0b6b6b9551db7b6311a5dd920fa52a94beae2e8959",
"sha256:ba9af1218fa01b1f11c72271bc7290b701d11ad4dbc2ae97c445ecacf6858dba",
"sha256:bdbda870e0fda7dd0fe7db7135ca226ec4c1ade8aa76e96614829b56ca491012",
"sha256:bf76d79dfaea802f0f28f50153ffbc1a74ae1ee73e480baeda410b4f3e7ab25f",
"sha256:c1cee10662c25c94415bbb987f2ec0e6ba9e8fce786334b10be7e6a7ab958f69",
"sha256:c5648c7eec5cf1ba5db1cf2d6c10036a582d7f09e172990474a122e30c841361",
"sha256:c58cd6bb46dcb922e0d5792850aab5964433d511b3a020867650f8d930dde4f4",
"sha256:c5d9b480ebae60fc2cbc8d6865194136bc690538fa542ba58726433bed6e04cc",
"sha256:ca15308ef722f120967af7474ba6a453e0f5b6f331251e20b8145497cf1bc14a",
"sha256:d0df04495b76a885bfef009f45eebe8fe2fbf815ad7a83dabcf5aced62f33162",
"sha256:d5be4e93acce64f516bf4fd239c0e6118fc913c93fa1a3f52d15bdcc60d97b2d",
"sha256:d8249666c23683f74f8f93aeaa8794ac87cc61c40ff70374a825f3352a4371dc",
"sha256:e3f1cd1cd65695b1540b3cf7828d05b3515974a9d7c7530f762ac40f58a18161",
"sha256:e56fae4292e216b8deeee38ace84557b9fa85b52db005368a275427cdabb8192",
"sha256:e6dcc70a25cb95df0ae33dfc701de9b09c37f7dd9f00394d684a5b57257f8246",
"sha256:e89d5abf86c104de808108a25d171ad646c07eda96ca76c8b237b94b9c71e518",
"sha256:ed7c9debf7bfc63c9b9f8b595409237774ff4b061bf29fba6f53b287a2fdeab9",
"sha256:ef001a60e888f8741e42e5aa79ae55c91be73761e4df5e806efca1ddd62fd400",
"sha256:f30090e22a301952c5abd0e493a1c8358b4f0b368b49fa3e4568ed3ed68b8d1f",
"sha256:f79691335257d60951638dd43576b9bcd6f52baa5c1c2cd07a509bb003238372",
"sha256:f918e9ef4c98f477a5458238dde2a1643aed956c7213873ab6b6b82e32b8ef61",
"sha256:fd0a8aa431f9b7ad9eb8264f55ef83cbb254962af3775092fb6e93890dea9ca2"
"sha256:04691b8e832a900ed15f5bcccc2008fc2d1c8e7411251fd7d1422a84e1d72841",
"sha256:1a613d60be1a02c7a5184ea5c4227f48c08e0635608b9c17ae2b17efef8f2501",
"sha256:1d732b5dcafed67d81c5b5a0c404c31a61e13148946a3b910a340f72fdd1ec95",
"sha256:2b31f7f246dbff339b3b76ee81329e3eca5022ce270c831c65e841dbbb40115f",
"sha256:312fd77258bf1044ef4faa82091f2e88216e4b62dcf1a461d3e917144c8b09b7",
"sha256:321316a7b979892a13c148a9d37852b5a76f26717e4b911b606a649394629532",
"sha256:36c1a1b6d38ebf8a4335f65226ec36b5d6fd67743fdcbad5c52bdcd46c4f5842",
"sha256:38f281bb9bdd4269c451fed9451203512dadefd64676f14ed1e82c77eb5644fc",
"sha256:3a2d81c95d3b02638ee6ae647edc79769fd29bf5e9e5b6b0c29040579f33c260",
"sha256:3d40ad86a348c79c614e2b90566267dd6d45f2e6b4d2bfb794d78ea4a4b60b63",
"sha256:3d72e3d20b03e63bd27b1c4d6b754cd93eca82ecc5dd77b99262d5f64862ca35",
"sha256:3fbb59f84c8549113dcdce7c6d16c5731fe53651d0b46c0a25a5ebc7bb655869",
"sha256:405d8528a0ea07ca516d9007ecad4e1bd10e2eeef27411c6188d78c4e2dfcddc",
"sha256:420f10c852b9a489cf5a764534669a19f49732a0576c76d9489ebf287f81af6d",
"sha256:426895ac9f2938bec193aa998e7a77a3e65d3e46903f348e794b4192b9a5b61e",
"sha256:4438ba539bef21e288092b30ea2fc30e883d9af5b66ebeaf2fd7c25e2f074e39",
"sha256:46db409fc0c3ee5c859b84c7de9cb507166287d588390889fdf06a1afe452e16",
"sha256:483e120ea324c7fced6126bb9bf0535c71e9233d29cbc7e2fc4560311a5f8a32",
"sha256:4d7be755d7544dac2b9814e98366a065d15a16e13847eb1f5473bb714483391e",
"sha256:4e97b21482aa5c21e049e4755c95955ad71fb54c9488969e2f17cf30922aa5f6",
"sha256:5f44ba7c07e0aa4a7a2723b426c254e952da82a33d65b4a52afae4bef74a4203",
"sha256:62e5b942378d5f0b87caace567a70dc634ddd4d219a236fa221dc97d2fc412c8",
"sha256:7c669be1b01e4b2bf23aa49e987d9bedde0234a7da374a9b77ca5416d7c57002",
"sha256:7d47d666e17e57ef65fefc87229fde262bd5c9039ae8424bc53aa2d8f07dc178",
"sha256:7e184aa18f921b612ea08666c25dd92a71241c8ed40917f2824219c92289b8c7",
"sha256:80583c536e7e010e301002088919d4ea90566d1789ee02551574fdf3faa275ae",
"sha256:8217f73faf08623acb25fb2affd5d20cbcd8185213db308e46a37e6fd6a56a49",
"sha256:87d95eea58fb71f69b4f1c761099a19e0e9cb27d45dc1cc7042523128ee56337",
"sha256:8bd466135fb07f693dbdd999a5569ffbc0590e9c64df859243162f0ebee950c8",
"sha256:8e133ca2f8141b415ff396ba789bdeffdea8ff9a5c7fc9996ccf591d7d40ee93",
"sha256:8e6c0ca447b557a32642f22d0987be37950eda51c4f19fc788cebc99426284b6",
"sha256:9de96025ce25b9f4e744fbe558a003e673004af255da9b1f6ec243720ac5deeb",
"sha256:a27a8dca0dc6f0944ed9fd83c556d862e227a5cd4220e62af5d4c750389938f0",
"sha256:a2d4f68e4fa286fb6b00d58a1e87c79840e289d3f6e5dcb912ad7b0fbd9629fb",
"sha256:a6e1c77ff6f10eab496fbbcdaa7dfae84968928a0aadc43ce3c3453cec29bd79",
"sha256:a7b018811a0e1d3869d8d0600849953acd355a3a29c6bee0fbd24d7772bcc0a2",
"sha256:a99b2f2dd1236e8d9dc35974a3dc298a408cdfd512b0bb2451798cff1ce63408",
"sha256:ac1033942851bf01f28c76318155ea92d6648aecb924cab81fa23781d095e9ab",
"sha256:b6936cd38757dd323fefc157823e46436610328f0feb1419a412316f24b77f36",
"sha256:b6eab230b18458707b5c501548e997e42934b1c189fb4d1b78bf5aacc1c6a137",
"sha256:bcb57d175ff0cb4ff97fc547c74c1cb8d4c9612003f6d267ee78dad8f23d8b30",
"sha256:c1f02d016b9b6b5ad21949a21646714bfa7b32d6041a30f97674f05d6d6996e3",
"sha256:c40aaf7930680e0e5f3bd6d3d3dc97a7897f53bdce925545c4b241e0c5c3ca6a",
"sha256:c5e1874c601128cf54c1d4b471e915658a334fbc56d7b3c324ddc7511597ea82",
"sha256:c8805673b1953313adfc487d5323b4c87864e77057944a0888c98dd2f7a6052f",
"sha256:da458bdc9b0bcd9b8ca85bc73148631b18cc8ba03c47f29f4c017809990351aa",
"sha256:dcb708ab06f3f4dfc99e9f84821c9120e5f12113b90fad132311a2cb97525379",
"sha256:dfafc350f43fd7dc67df18c940c3b7ed208ebb797abe9fb3047f0c65be8e4c0f",
"sha256:e8931af864bd599c6af626575a02103ae626f57b34e3af5537d40b040d33d2ad",
"sha256:efa9d943189321f67f71070c309aa6f6130fa1ec35c9dfd0da0ed238938ce573",
"sha256:fd22ee7bff4b5c37bb6385efee1c501b75e29ca40286f037cb91c2182d1348ce"
],
"markers": "python_version >= '3.7'",
"version": "==7.0.3"
"version": "==7.0.2"
},
"dill": {
"hashes": [
@@ -395,14 +387,6 @@
],
"version": "==0.4.3"
},
"mypy-protobuf": {
"hashes": [
"sha256:7d75a079651b105076776a35a5405e3fa773b8a167118f1b712e443e9a6c18a2",
"sha256:da33dfde7547ff57e5ba5564126cbfa114f14413b2fa50759b1fa5de1e4ab511"
],
"index": "pypi",
"version": "==3.4.0"
},
"packaging": {
"hashes": [
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
@@ -411,14 +395,6 @@
"markers": "python_version >= '3.7'",
"version": "==22.0"
},
"pep517": {
"hashes": [
"sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b",
"sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59"
],
"markers": "python_version >= '3.6'",
"version": "==0.13.0"
},
"platformdirs": {
"hashes": [
"sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490",
@@ -435,26 +411,6 @@
"markers": "python_version >= '3.6'",
"version": "==1.0.0"
},
"protobuf": {
"hashes": [
"sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30",
"sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b",
"sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc",
"sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791",
"sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717",
"sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec",
"sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7",
"sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab",
"sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2",
"sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5",
"sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1",
"sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462",
"sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97",
"sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"
],
"index": "pypi",
"version": "==4.21.12"
},
"pycodestyle": {
"hashes": [
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",

196
README.md
View File

@@ -1,7 +1,7 @@
# Extract TOTP/HOTP secrets from QR codes exported by two-factor authentication apps
# Extract secrets from QR codes exported by two-factor authentication apps
[![CI tests](https://github.com/scito/extract_otp_secrets/actions/workflows/ci.yml/badge.svg)](https://github.com/scito/extract_otp_secrets/actions/workflows/ci.yml)
![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
![coverage](https://img.shields.io/badge/coverage-96%25-brightgreen)
[![CI docker](https://github.com/scito/extract_otp_secrets/actions/workflows/ci_docker.yml/badge.svg)](https://github.com/scito/extract_otp_secrets/actions/workflows/ci_docker.yml)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/protobuf)
[![GitHub Pipenv locked Python version](https://img.shields.io/github/pipenv/locked/python-version/scito/extract_otp_secrets)](https://github.com/scito/extract_otp_secrets/blob/master/Pipfile.lock)
@@ -13,55 +13,15 @@
---
The Python script `extract_otp_secrets.py` extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as "Google Authenticator".
The exported QR codes from authentication apps can be read in three ways:
The export QR codes from authentication apps can be provided in three ways to this script:
1. Capture from the system camera using a GUI, 🆕
2. Read image files containing the QR codes, and 🆕
3. Read text files containing the QR code data generated by third-party QR readers.
1. Capture from the system camera in a GUI,
2. Image files containing the QR codes, and
3. Text files containing the QR code data generated by QR readers.
The secret and otp values can be exported to json or csv files, as well as printed or saved to PNG images.
**This project/script was renamed from `extract_otp_secret_keys` to `extract_otp_secrets`.**
## Usage
### Capture QR codes from camera (🆕 since version 2.0)
1. Open "Google Authenticator" app on the mobile phone
2. Export the QR codes from "Google Authenticator" app (see [how to export](#how-to-export-otp-secrets-from-google-authenticator-app))
3. Point the exported QR codes to the camera of your computer
4. Call this script without infile parameters:
python src/extract_otp_secrets.py
![CV2 Capture from camera screenshot](cv2_capture_screenshot.png)
Detected QR codes are surrounded with a frame. The color of the frame indicates the extracting result:
* Green: The QR code is detected, decoded and the OTP secret was successfully extracted.
* Red: The QR code is detected and decoded, but could not be successfully extracted. This is the case if a QR code not containing OTP data is captured.
* Magenta: The QR code is detected, but could not be decoded. The QR code should be presented better to the camera or another QR reader could be used.
### With builtin QR decoder from image files (🆕 since version 2.0)
1. Open "Google Authenticator" app on the mobile phone
2. Export the QR codes from "Google Authenticator" app (see [how to export](#how-to-export-otp-secrets-from-Google-Authenticator))
4. Save the QR code as image file, e.g. example_export.png
5. Transfer the images files to the computer where his script is installed.
6. Call this script with the file as input:
python src/extract_otp_secrets.py example_export.png
### With external QR decoder app from text files
1. Open "Google Authenticator" app on the mobile phone
2. Export the QR codes from "Google Authenticator" app (see [how to export](#how-to-export-otp-secrets-from-Google-Authenticator))
3. Read QR codes with a third-party QR code reader (e.g. from another phone)
4. Save the captured QR codes from the QR code reader to a text file, e.g. example_export.txt. Save each QR code on a new line. (The captured QR codes look like `otpauth-migration://offline?data=...`)
5. Transfer the file to the computer where his script is installed.
6. Call this script with the file as input:
python src/extract_otp_secrets.py example_export.txt
This script/project was renamed from extract_otp_secret_keys to extract_otp_secrets in version 2.0.0.
## Installation
@@ -108,6 +68,40 @@ The zbar DLLs are included with the Windows Python wheels. However, you might ne
OpenCV requires [Visual C++ redistributable 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48145). For more information see [opencv-python](https://pypi.org/project/opencv-python/)
## Usage
### Capture QR codes from camera (since version 2.0.0)
1. Open "Google Authenticator" app on the mobile phone
2. Export the QR codes from "Google Authenticator" app
3. Point the QR codes to the camera of your computer
4. Call this script without infile parameters:
python src/extract_otp_secrets.py
![CV2 Capture from camera screenshot](cv2_capture_screenshot.png)
### With builtin QR decoder from image files (since version 2.0.0)
1. Open "Google Authenticator" app on the mobile phone
2. Export the QR codes from "Google Authenticator" app
4. Save the QR code as image file, e.g. example_export.png
5. Transfer the images files to the computer where his script is installed.
6. Call this script with the file as input:
python src/extract_otp_secrets.py example_export.png
### With external QR decoder app from text files
1. Open "Google Authenticator" app on the mobile phone
2. Export the QR codes from "Google Authenticator" app
3. Read QR codes with a QR code reader (e.g. from another phone)
4. Save the captured QR codes in the QR code reader to a text file, e.g. example_export.txt. Save each QR code on a new line. (The captured QR codes look like `otpauth-migration://offline?data=...`)
5. Transfer the file to the computer where his script is installed.
6. Call this script with the file as input:
python src/extract_otp_secrets.py example_export.txt
## Program help: arguments and options
<pre>usage: extract_otp_secrets.py [-h] [--csv FILE] [--keepass FILE] [--json FILE] [--printqr] [--saveqr DIR] [--camera NUMBER] [--qr {ZBAR,QREADER,QREADER_DEEP,CV2,CV2_WECHAT}] [-i] [--no-color] [-d | -v | -q] [infile ...]
@@ -183,13 +177,13 @@ python extract_otp_secrets.py = < example_export.png</pre>
* Free and open source
* Supports Google Authenticator exports (and compatible apps like Aegis Authenticator)
* Captures the the QR codes directly from the camera using different QR code libraries (based on OpenCV) (🆕 since v2.0)
* Captures the the QR codes directly from the camera using different QR code libraries (based on OpenCV)
* ZBAR: [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) - fast and reliable, good for images and video capture (default and recommended)
* QREADER: [QReader](https://github.com/Eric-Canas/QReader)
* QREADER_DEEP: [QReader](https://github.com/Eric-Canas/QReader) - very slow in GUI
* CV2: [QRCodeDetector](https://docs.opencv.org/4.x/de/dc3/classcv_1_1QRCodeDetector.html)
* CV2_WECHAT: [WeChatQRCode](https://docs.opencv.org/4.x/dd/d63/group__wechat__qrcode.html)
* Supports [TOTP](https://www.ietf.org/rfc/rfc6238.txt) and [HOTP](https://www.ietf.org/rfc/rfc4226.txt) standards
* Supports TOTP and HOTP standards
* Generates QR codes
* Exports to various formats:
* CSV
@@ -197,8 +191,7 @@ python extract_otp_secrets.py = < example_export.png</pre>
* Dedicated CSV for KeePass
* QR code images
* Supports reading from stdin and writing to stdout, thus pipes can be used
* Handles multiple input files (🆕 since v2.0)
* Reads QR codes images: (See [OpenCV docu](https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56)) (🆕 since v2.0)
* Reads QR codes images: (See [OpenCV docu](https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56))
* Portable Network Graphics - *.png
* WebP - *.webp
* JPEG files - *.jpeg, *.jpg, *.jpe
@@ -206,8 +199,8 @@ python extract_otp_secrets.py = < example_export.png</pre>
* Windows bitmaps - *.bmp, *.dib
* JPEG 2000 files - *.jp2
* Portable image format - *.pbm, *.pgm, *.ppm *.pxm, *.pnm
* Prints errors and warnings to stderr (🆕 since v2.0)
* Prints colored output (🆕 since v2.0)
* Prints errors and warnings to stderr
* Prints colored output
* Many ways to run the script:
* Native Python
* pipenv
@@ -216,7 +209,7 @@ python extract_otp_secrets.py = < example_export.png</pre>
* Docker
* VSCode devcontainer
* devbox
* Prebuilt Docker images provided for amd64 and arm64 (🆕 since v2.0)
* Prebuilt Docker images provided for amd64 and arm64
* Compatible with major platforms:
* Linux
* macOS
@@ -266,18 +259,23 @@ Import CSV with HOTP entries in KeePass as
KeePass can be used as a backup for one time passwords (second factor) from the mobile phone.
## How to export otp secrets from Google Authenticator app
## Technical background
1. Open "Google Authenticator" app
2. Select "Transfer accounts" in the three dot menu of the app.
![Transfer accounts option in the Google Authenticator.](docs/Transfer-accounts-option-in-the-Google-Authenticator_300px.webp)
3. Select "Export accounts"
![Export account option in the Google Authenticator.](docs/Export-account-option-in-the-Google-Authenticator_300px.webp)
4. Pass the verification by password or fingerprint.
5. Select your accounts
6. Press "Next" button
7. The exported QR code(s) ready for extraction are shown.
![Exported Google Authenticator QR codes](docs/Exported-QR-codes_300px.webp)
The export QR code of "Google Authenticator" contains the URL `otpauth-migration://offline?data=...`.
The data parameter is a base64 encoded proto3 message (Google Protocol Buffers).
Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition or new protobuf versions):
protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python src/google_auth.proto
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
For Python type hint generation the [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) package is used.
## References
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
## Glossary
@@ -310,7 +308,7 @@ curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/examp
```
git clone https://github.com/scito/extract_otp_secrets.git
pip install -U -e extract_otp_secrets
python -m extract_otp_secrets extract_otp_secrets/example_export.txt
python -m extract_otp_secrets example_export.txt
```
### pipenv
@@ -366,10 +364,11 @@ Prebuilt docker images are available for amd64 and arm64 architectures on [Docke
Extracting from an QR image file:
```
docker login -u USERNAME
curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/example_export.png | docker run --pull always -i --rm -v "$(pwd)":/files:ro scit0/extract_otp_secrets =
```
Capturing from camera in GUI window (X Window system required on host):
Capturing from camera in GUI (X Window system required on host):
```
docker run --pull always --rm -v "$(pwd)":/files:ro -i --device="/dev/video0:/dev/video0" --env="DISPLAY" -v /tmp/.X11-unix:/tmp/.X11-unix:ro scit0/extract_otp_secrets
@@ -448,7 +447,6 @@ Setup for running the tests in VSCode.
### Build
```
cd extract_otp_secrets/
pip install -U -e .
python src/extract_otp_secrets.py
@@ -465,22 +463,12 @@ pip install -U -r requirements.txt
### Build docker images
#### Debian (full functionality)
Build and run the app within the container:
```bash
docker build . -t extract_otp_secrets --pull --build-arg RUN_TESTS=false
```
Run tests in docker container:
```bash
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets
```
#### Alpine (only text file processing)
```bash
docker build . -t extract_otp_secrets_only_txt --pull -f Dockerfile_only_txt --build-arg RUN_TESTS=false
```
@@ -488,56 +476,16 @@ docker build . -t extract_otp_secrets_only_txt --pull -f Dockerfile_only_txt --b
Run tests in docker container:
```bash
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt tests/extract_otp_secrets_test.py -k "not qreader" --relaxed
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets
```
### Full local build
There is a Bash script for a full local build including linting and type checking.
```bash
./build.sh
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt extract_otp_secrets_test.py -k "not qreader" --relaxed
```
The options of the build script:
```
Build extract_otp_secrets project
./build.sh [options]
Options:
-i Interactive mode, all steps must be confirmed
-C Ignore version check of protobuf/protoc
-D Do not build docker
-G Do not start extract_otp_secrets.py in GUI mode
-c Clean everything
-r Generate result files
-h, --help Help
```
## Technical background
The export QR code of "Google Authenticator" contains the URL `otpauth-migration://offline?data=...`.
The data parameter is a base64 encoded proto3 message (Google Protocol Buffers).
Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition or new protobuf versions):
protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python src/google_auth.proto
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
For Python type hint generation the [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) package is used.
## References
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
## Issues
* Segmentation fault on macOS with CV2 4.7.0: https://github.com/opencv/opencv/issues/23072
* CV2 window does not show icons: https://github.com/opencv/opencv-python/issues/585
* Known issue for macOS: https://github.com/opencv/opencv/issues/23072
## Problems and Troubleshooting
@@ -570,10 +518,10 @@ FileNotFoundError: Could not find module 'libiconv.dll' (or one of its dependenc
* [ZBar](https://github.com/mchehab/zbar) is an open source software suite for reading bar codes from various sources, including webcams.
* [Aegis Authenticator](https://github.com/beemdevelopment/Aegis) is a free, secure and open source 2FA app for Android.
* [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) is a good QR code reader Python module
* [OpenCV](https://docs.opencv.org/4.x/) (CV2) Open Source Computer Vision library with [opencv-python](https://github.com/opencv/opencv-python)
* [Python QReader](https://github.com/Eric-Canas/QReader) Python QR code readers
* [Android OTP Extractor](https://github.com/puddly/android-otp-extractor) can extract your tokens from popular Android OTP apps and export them in a standard format or just display them as QR codes for easy importing. [Requires a _rooted_ Android phone.]
* [Python QReader](https://github.com/Eric-Canas/QReader)
* [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar)
* [OpenCV](https://docs.opencv.org/4.x/) (CV2) Open Source Computer Vision library with [opencv-python](https://github.com/opencv/opencv-python)
***

116
build.sh
View File

@@ -73,6 +73,11 @@ askContinueYn() {
# Reference: https://gist.github.com/steinwaywhw/a4cd19cda655b8249d908261a62687f8
echo "Checking Protoc version..."
VERSION=$(curl -sL https://github.com/protocolbuffers/protobuf/releases/latest | grep -E "<title>" | perl -pe's%.*Protocol Buffers v(\d+\.\d+(\.\d+)?).*%\1%')
BASEVERSION=4
echo
interactive=false
ignore_version_check=true
clean=false
@@ -83,16 +88,16 @@ generate_result_files=false
while test $# -gt 0; do
case $1 in
-h|--help)
echo "Build extract_otp_secrets project"
echo "Upgrade Protoc"
echo
echo "$0 [options]"
echo
echo "Options:"
echo "-i Interactive mode, all steps must be confirmed"
echo "-C Ignore version check of protobuf/protoc"
echo "-D Do not build docker"
echo "-G Do not start extract_otp_secrets.py in GUI mode"
echo "-c Clean everything"
echo "-i Interactive"
echo "-C Ignore version check"
echo "-D No docker build"
echo "-G No not run gui"
echo "-c Clean"
echo "-r Generate result files"
echo "-h, --help Help"
quit
@@ -133,33 +138,15 @@ PIPENV="$PYTHON -m pipenv"
FLAKE8="$PYTHON -m flake8"
MYPY="$PYTHON -m mypy"
# sudo ln -s /usr/bin/python3.11 /usr/bin/python
# Upgrade protoc
DEST="protoc"
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
echo -e "\nProtoc remote version $VERSION\n"
echo -e "Protoc local version: $OLDVERSION\n"
if $clean; then
cmd="docker image prune -f || echo 'No docker image pruned'"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="$PIP uninstall -y extract-otp-secrets || echo nothing done"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="$PIP freeze | grep -v -E '^-e|^#' | xargs sudo $PIP uninstall -y || echo nothing done"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="$PIP freeze --user | grep -v -E '^-e|^#' | xargs $PIP uninstall -y || echo nothing done"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="$PIP freeze | cut -d \"@\" -f1 | xargs pip uninstall -y || echo Nothing to do"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="rm -r dist/ build/ *.whl pytest.xml pytest-coverage.txt .coverage tests/reports || true; find . -name '*.pyc' -type f -delete; find . -name '__pycache__' -type d -exec rm -r {} \; || true; find . -name '*.egg-info' -type d -exec rm -r {} \; || true; find . -name '*_cache' -type d -exec rm -r {} \; || true; mkdir -p tests/reports;"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
@@ -173,20 +160,6 @@ if $clean; then
eval "$cmd"
fi
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
echo -e "\n\nChecking Protoc version..."
VERSION=$(curl -sL https://github.com/protocolbuffers/protobuf/releases/latest | grep -E "<title>" | perl -pe's%.*Protocol Buffers v(\d+\.\d+(\.\d+)?).*%\1%')
BASEVERSION=4
echo
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
echo -e "\nProtoc remote version $VERSION\n"
echo -e "Protoc local version: $OLDVERSION\n"
if [ "$OLDVERSION" != "$VERSION" ] || ! $ignore_version_check; then
echo "Upgrade protoc from $OLDVERSION to $VERSION"
@@ -243,7 +216,7 @@ fi
# Upgrade pip requirements
cmd="pip install -U pip"
cmd="sudo pip install -U pip"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
@@ -253,6 +226,10 @@ cmd="$PIP install --use-pep517 -U -r requirements.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Lint
LINT_OUT_FILE="tests/reports/flake8_results.txt"
@@ -277,21 +254,7 @@ cmd="$MYPY --strict src/*.py tests/*.py | tee $TYPE_CHECK_OUT_FILE"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# pip -e install
cmd="$PIP install -U -e ."
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="extract_otp_secrets example_export.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="extract_otp_secrets - < example_export.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Test (needs module)
# Test
cmd="$PYTHON src/extract_otp_secrets.py example_export.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
@@ -324,9 +287,37 @@ cmd="$PIPENV run pytest --cov=extract_otp_secrets_test tests/"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# sudo pip
cmd="sudo $PIP install --use-pep517 -U -r requirements.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="sudo $PIP install --use-pep517 -U -r requirements-dev.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="sudo $PIP install -U pipenv"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# pip -e install (must be after other pip installs in order to have this environment for development)
cmd="$PIP install -U -e ."
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="extract_otp_secrets example_export.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="extract_otp_secrets - < example_export.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Build wheel
cmd="$PIP wheel ."
cmd="$PIP wheel .
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
@@ -387,6 +378,10 @@ if $build_docker; then
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="docker image prune -f || echo 'No docker image pruned'"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
if $run_gui; then
cmd="docker run --rm -v "$(pwd)":/files:ro --device=\"/dev/video0:/dev/video0\" --env=\"DISPLAY\" -v /tmp/.X11-unix:/tmp/.X11-unix:ro extract_otp_secrets &"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
@@ -407,7 +402,6 @@ cmd="cat $TYPE_CHECK_OUT_FILE $LINT_OUT_FILE $COVERAGE_OUT_FILE"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
line=$(printf '#%.0s' $(eval echo {1..$(( ($COLUMNS - 10) / 2))}))
echo -e "\n${greenBold}$line SUCCESS $line${reset}"
echo -e "\n${greenBold}SUCCESS${reset}"
quit

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -29,15 +29,15 @@ classifiers = [
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
]
dependencies = [
"colorama>=0.4.6",
"opencv-contrib-python; sys_platform != 'darwin'",
"opencv-contrib-python<=4.7.0; sys_platform == 'darwin'",
"Pillow",
"protobuf",
"pyzbar",
"qrcode",
"Pillow",
"qreader",
"pyzbar",
"opencv-contrib-python<=4.7.0; sys_platform == 'darwin'",
"opencv-contrib-python; sys_platform != 'darwin'",
"typing_extensions; python_version<='3.7'",
"colorama>=0.4.6",
]
description = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as 'Google Authenticator'"
dynamic = ["version"]

View File

@@ -1,11 +1,10 @@
build
flake8
mypy
mypy-protobuf
types-protobuf
pylint
pytest
pytest-cov
pytest-mock
pytest-cov
setuptools
types-protobuf
wheel
build

View File

@@ -1,9 +1,9 @@
colorama>=0.4.6
opencv-contrib-python; sys_platform != 'darwin'
opencv-contrib-python<=4.7.0; sys_platform == 'darwin'
Pillow
protobuf
pyzbar
qrcode
Pillow
qreader
opencv-contrib-python<=4.7.0; sys_platform == 'darwin'
opencv-contrib-python; sys_platform != 'darwin'
pyzbar
typing_extensions; python_version<='3.7'
colorama>=0.4.6

View File

@@ -164,77 +164,6 @@ def main(sys_args: list[str]) -> None:
write_json(args, otps)
# workaround for PYTHON <= 3.9 use: pb.MigrationPayload | None
def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.MigrationPayload]:
'''Extracts the otp migration payload from an otp url. This function is the core of the this appliation.'''
if not is_opt_url(otp_url, source):
return None
parsed_url = urlparse.urlparse(otp_url)
if verbose >= LogLevel.DEBUG: log_debug(f"parsed_url={parsed_url}")
try:
params = urlparse.parse_qs(parsed_url.query, strict_parsing=True)
except Exception: # workaround for PYTHON <= 3.10
params = {}
if verbose >= LogLevel.DEBUG: log_debug(f"querystring params={params}")
if 'data' not in params:
log_error(f"could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}")
return None
data_base64 = params['data'][0]
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64={data_base64}")
data_base64_fixed = data_base64.replace(' ', '+')
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64_fixed={data_base64_fixed}")
data = base64.b64decode(data_base64_fixed, validate=True)
payload = pb.MigrationPayload()
try:
payload.ParseFromString(data)
except Exception as e:
abort(f"Cannot decode otpauth-migration migration payload.\n"
f"data={data_base64}", e)
if verbose >= LogLevel.DEBUG: log_debug(f"\n{i}. Payload Line", payload, sep='\n')
return payload
def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: int, infile: str, args: Args) -> int:
'''Converts the otp migration payload into a normal Python dictionary. This function is the core of the this appliation.'''
payload = get_payload_from_otp_url(otpauth_migration_url, urls_count, infile)
if not payload:
return 0
new_otps_count = 0
# pylint: disable=no-member
for raw_otp in payload.otp_parameters:
if verbose: print(f"\n{len(otps) + 1}. Secret")
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
if verbose >= LogLevel.DEBUG: log_debug('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: Otp = {
"name": raw_otp.name,
"secret": secret,
"issuer": raw_otp.issuer,
"type": otp_type,
"counter": raw_otp.counter if raw_otp.type == 1 else None,
"url": otp_url
}
if otp not in otps or not args.ignore:
otps.append(otp)
new_otps_count += 1
if not quiet:
print_otp(otp)
if args.printqr:
print_qr(args, otp_url)
if args.saveqr:
save_qr(otp, args, len(otps))
if not quiet:
print()
elif args.ignore and not quiet:
eprint(f"Ignored duplicate otp: {otp['name']}", f" / {otp['issuer']}\n" if otp['issuer'] else '\n', sep='')
return new_otps_count
def parse_args(sys_args: list[str]) -> Args:
global verbose, quiet, colored
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
@@ -291,6 +220,42 @@ def extract_otps(args: Args) -> Otps:
return extract_otps_from_files(args)
def get_color(new_otps_count: int, otp_url: str) -> ColorBGR:
if new_otps_count:
return SUCCESS_COLOR
else:
if otp_url:
return FAILURE_COLOR
else:
return NORMAL_COLOR
# TODO use cv2 types if available
def cv2_draw_box(img: Any, raw_pts: Any, color: ColorBGR) -> Any:
pts = np.array([raw_pts], np.int32)
pts = pts.reshape((-1, 1, 2))
cv2.polylines(img, [pts], True, color, BOX_THICKNESS)
return pts
# TODO use cv2 types if available
def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition, color: ColorBGR, opposite_len: Optional[int] = None) -> None:
window_dim = cv2.getWindowImageRect(WINDOW_NAME)
out_text = text
if opposite_len:
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
actual_width = text_dim[TEXT_WIDTH] + opposite_len * CHAR_DX + 4 * BORDER
if actual_width >= window_dim[WINDOW_WIDTH]:
out_text = out_text[:(window_dim[WINDOW_WIDTH] - actual_width) // CHAR_DX] + '.'
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
if position == TextPosition.LEFT:
pos = BORDER, START_Y + line_number * FONT_DY
else:
pos = window_dim[WINDOW_WIDTH] - text_dim[TEXT_WIDTH] - BORDER, START_Y + line_number * FONT_DY
cv2.putText(img, out_text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE)
def extract_otps_from_camera(args: Args) -> Otps:
if verbose: print("Capture QR codes from camera")
otp_urls: OtpUrls = []
@@ -320,7 +285,7 @@ def extract_otps_from_camera(args: Args) -> Otps:
if otp_url:
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
if found:
cv2_draw_box(img, [(bbox[0], bbox[1]), (bbox[2], bbox[1]), (bbox[2], bbox[3]), (bbox[0], bbox[3])], get_color(new_otps_count, otp_url))
cv2.rectangle(img, (bbox[0], bbox[1]), (bbox[2], bbox[3]), get_color(new_otps_count, otp_url), BOX_THICKNESS)
elif qr_mode == QRMode.ZBAR:
for qrcode in zbar.decode(img):
otp_url = qrcode.data.decode('utf-8')
@@ -360,42 +325,6 @@ def extract_otps_from_camera(args: Args) -> Otps:
return otps
def get_color(new_otps_count: int, otp_url: str) -> ColorBGR:
if new_otps_count:
return SUCCESS_COLOR
else:
if otp_url:
return FAILURE_COLOR
else:
return NORMAL_COLOR
# TODO use cv2 types if available
def cv2_draw_box(img: Any, raw_pts: Any, color: ColorBGR) -> Any:
pts = np.array([raw_pts], np.int32)
pts = pts.reshape((-1, 1, 2))
cv2.polylines(img, [pts], True, color, BOX_THICKNESS)
return pts
# TODO use cv2 types if available
def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition, color: ColorBGR, opposite_len: Optional[int] = None) -> None:
window_dim = cv2.getWindowImageRect(WINDOW_NAME)
out_text = text
if opposite_len:
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
actual_width = text_dim[TEXT_WIDTH] + opposite_len * CHAR_DX + 4 * BORDER
if actual_width >= window_dim[WINDOW_WIDTH]:
out_text = out_text[:(window_dim[WINDOW_WIDTH] - actual_width) // CHAR_DX] + '.'
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
if position == TextPosition.LEFT:
pos = BORDER, START_Y + line_number * FONT_DY
else:
pos = window_dim[WINDOW_WIDTH] - text_dim[TEXT_WIDTH] - BORDER, START_Y + line_number * FONT_DY
cv2.putText(img, out_text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE)
def cv2_handle_pressed_keys(qr_mode: QRMode) -> Tuple[bool, QRMode]:
key = cv2.waitKey(1) & 0xFF
quit = False
@@ -485,6 +414,45 @@ def read_lines_from_text_file(filename: str) -> list[str]:
return lines
def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: int, infile: str, args: Args) -> int:
payload = get_payload_from_otp_url(otpauth_migration_url, urls_count, infile)
if not payload:
return 0
new_otps_count = 0
# pylint: disable=no-member
for raw_otp in payload.otp_parameters:
if verbose: print(f"\n{len(otps) + 1}. Secret")
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
if verbose >= LogLevel.DEBUG: log_debug('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: Otp = {
"name": raw_otp.name,
"secret": secret,
"issuer": raw_otp.issuer,
"type": otp_type,
"counter": raw_otp.counter if raw_otp.type == 1 else None,
"url": otp_url
}
if otp not in otps or not args.ignore:
otps.append(otp)
new_otps_count += 1
if not quiet:
print_otp(otp)
if args.printqr:
print_qr(args, otp_url)
if args.saveqr:
save_qr(otp, args, len(otps))
if not quiet:
print()
elif args.ignore and not quiet:
eprint(f"Ignored duplicate otp: {otp['name']}", f" / {otp['issuer']}\n" if otp['issuer'] else '\n', sep='')
return new_otps_count
def convert_img_to_otp_urls(filename: str, args: Args) -> OtpUrls:
if verbose: print(f"Reading image {filename}")
try:
@@ -538,6 +506,37 @@ def decode_qr_img_otp_urls(img: Any, qr_mode: QRMode) -> OtpUrls:
return otp_urls
# workaround for PYTHON <= 3.9 use: pb.MigrationPayload | None
def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.MigrationPayload]:
'''Extracts the otp migration payload from an otp url. This function is the core of the this appliation.'''
if not is_opt_url(otp_url, source):
return None
parsed_url = urlparse.urlparse(otp_url)
if verbose >= LogLevel.DEBUG: log_debug(f"parsed_url={parsed_url}")
try:
params = urlparse.parse_qs(parsed_url.query, strict_parsing=True)
except Exception: # workaround for PYTHON <= 3.10
params = {}
if verbose >= LogLevel.DEBUG: log_debug(f"querystring params={params}")
if 'data' not in params:
log_error(f"could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}")
return None
data_base64 = params['data'][0]
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64={data_base64}")
data_base64_fixed = data_base64.replace(' ', '+')
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64_fixed={data_base64_fixed}")
data = base64.b64decode(data_base64_fixed, validate=True)
payload = pb.MigrationPayload()
try:
payload.ParseFromString(data)
except Exception as e:
abort(f"Cannot decode otpauth-migration migration payload.\n"
f"data={data_base64}", e)
if verbose >= LogLevel.DEBUG: log_debug(f"\n{i}. Payload Line", payload, sep='\n')
return payload
def is_opt_url(otp_url: str, source: str) -> bool:
if not otp_url.startswith('otpauth-migration://'):
msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}"

View File

@@ -2,6 +2,8 @@ from typing import Any
import pytest
from extract_otp_secrets import QRMode
def pytest_addoption(parser: pytest.Parser) -> None:
parser.addoption("--relaxed", action='store_true', help="run tests in relaxed mode")
@@ -15,7 +17,6 @@ def relaxed(request: pytest.FixtureRequest) -> Any:
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
if "qr_mode" in metafunc.fixturenames:
all_qr_modes = ['ZBAR', 'QREADER', 'QREADER_DEEP', 'CV2', 'CV2_WECHAT']
number = 2 if metafunc.config.getoption("fast") else len(all_qr_modes)
qr_modes = [mode for mode in all_qr_modes]
number = 2 if metafunc.config.getoption("fast") else len(QRMode)
qr_modes = [mode.name for mode in QRMode]
metafunc.parametrize("qr_mode", qr_modes[0:number])

View File

@@ -1,3 +0,0 @@
# comment 1
# comment 2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 478 B

View File

@@ -26,8 +26,7 @@ import pathlib
import re
import sys
import time
from enum import Enum
from typing import Any, List, Optional, Tuple
from typing import Optional
import colorama
import pytest
@@ -38,13 +37,6 @@ from utils import (count_files_in_dir, file_exits, read_binary_file_as_stream,
import extract_otp_secrets
try:
import cv2 # type: ignore
from extract_otp_secrets import SUCCESS_COLOR, FAILURE_COLOR, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE
except ImportError:
# ignore
pass
qreader_available: bool = extract_otp_secrets.qreader_available
@@ -107,20 +99,6 @@ def test_extract_stdin_empty(capsys: pytest.CaptureFixture[str], monkeypatch: py
assert captured.err == '\nWARN: stdin is empty\n'
def test_extract_stdin_only_comments(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
monkeypatch.setattr('sys.stdin', io.StringIO("\n\n# comment 1\n\n\n#comment 2"))
# Act
extract_otp_secrets.main(['-n', '-'])
# Assert
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None:
if qreader_available:
# Act
@@ -147,17 +125,6 @@ def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> No
assert captured.out == ''
def test_extract_only_comments_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None:
# Act
extract_otp_secrets.main(['-n', 'tests/data/only_comments.txt'])
# Assert
captured = capsys.readouterr()
assert captured.err == ''
assert captured.out == ''
@pytest.mark.qreader
def test_extract_stdin_img_empty(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
@@ -173,24 +140,6 @@ def test_extract_stdin_img_empty(capsys: pytest.CaptureFixture[str], monkeypatch
assert captured.err == '\nWARN: stdin is empty\n'
@pytest.mark.qreader
def test_extract_stdin_img_garbage(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
monkeypatch.setattr('sys.stdin', io.BytesIO("garbage".encode('utf-8')))
# Act
with pytest.raises(SystemExit) as e:
extract_otp_secrets.main(['-n', '='])
# Assert
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == '\nERROR: Unable to open file for reading.\ninput file: =\n'
assert e.type == SystemExit
assert e.value.code == 1
def test_extract_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
# Arrange
output_file = str(tmp_path / 'test_example_output.csv')
@@ -226,19 +175,6 @@ def test_extract_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
assert captured.err == ''
def test_extract_csv_stdout_only_comments(capsys: pytest.CaptureFixture[str]) -> None:
# Act
extract_otp_secrets.main(['-c', '-', 'tests/data/only_comments.txt'])
# Assert
assert not file_exits('test_example_output.csv')
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
def test_extract_stdin_and_csv_stdout(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
# Arrange
monkeypatch.setattr('sys.stdin', io.StringIO(read_file_to_str('example_export.txt')))
@@ -282,18 +218,6 @@ def test_keepass_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path)
assert captured.err == ''
def test_keepass_empty(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
# Act
extract_otp_secrets.main(['-k', '-', 'tests/data/only_comments.txt'])
# Assert
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
def test_keepass_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
'''Two csv files .totp and .htop are generated.'''
# Act
@@ -364,18 +288,6 @@ def test_extract_json_stdout(capsys: pytest.CaptureFixture[str]) -> None:
assert captured.err == ''
def test_extract_json_stdout_only_comments(capsys: pytest.CaptureFixture[str]) -> None:
# Act
extract_otp_secrets.main(['-j', '-', 'tests/data/only_comments.txt'])
# Assert
assert not file_exits('test_example_output.json')
captured = capsys.readouterr()
assert captured.out == '[]'
assert captured.err == ''
def test_extract_not_encoded_plus(capsys: pytest.CaptureFixture[str]) -> None:
# Act
extract_otp_secrets.main(['tests/data/test_plus_problem_export.txt'])
@@ -582,113 +494,6 @@ def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: Mocker
assert e.type == SystemExit
MockMode = Enum('MockMode', ['REPEAT_FIRST_ENDLESS', 'LOOP_LIST'])
class MockCam:
read_counter: int = 0
read_files: List[str] = []
mock_mode: MockMode
def __init__(self, files: List[str] = ['example_export.png'], mock_mode: MockMode = MockMode.REPEAT_FIRST_ENDLESS):
self.read_files = files
self.image_mode = mock_mode
def read(self) -> Tuple[bool, Any]:
if self.image_mode == MockMode.REPEAT_FIRST_ENDLESS:
file = self.read_files[0]
elif self.image_mode == MockMode.LOOP_LIST:
file = self.read_files[self.read_counter]
self.read_counter += 1
if file:
img = cv2.imread(file)
return True, img
else:
return False, None
def release(self) -> None:
# ignore
pass
@pytest.mark.parametrize("qr_reader,file,success", [
(None, 'example_export.png', True),
('ZBAR', 'example_export.png', True),
('QREADER', 'example_export.png', True),
('QREADER_DEEP', 'example_export.png', True),
('CV2', 'example_export.png', True),
('CV2_WECHAT', 'example_export.png', True),
(None, 'tests/data/qr_but_without_otp.png', False),
('ZBAR', 'tests/data/qr_but_without_otp.png', False),
('QREADER', 'tests/data/qr_but_without_otp.png', False),
('QREADER_DEEP', 'tests/data/qr_but_without_otp.png', False),
('CV2', 'tests/data/qr_but_without_otp.png', False),
('CV2_WECHAT', 'tests/data/qr_but_without_otp.png', False),
(None, 'tests/data/lena_std.tif', None),
('ZBAR', 'tests/data/lena_std.tif', None),
('QREADER', 'tests/data/lena_std.tif', None),
('QREADER_DEEP', 'tests/data/lena_std.tif', None),
('CV2', 'tests/data/lena_std.tif', None),
('CV2_WECHAT', 'tests/data/lena_std.tif', None),
])
def test_extract_otps_from_camera(qr_reader: Optional[str], file: str, success: bool, capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
if qreader_available:
# Arrange
mockCam = MockCam([file])
mocker.patch('cv2.VideoCapture', return_value=mockCam)
mocker.patch('cv2.namedWindow')
mocked_polylines = mocker.patch('cv2.polylines')
mocker.patch('cv2.imshow')
mocker.patch('cv2.getTextSize', return_value=([8, 200], False))
mocked_putText = mocker.patch('cv2.putText')
mocker.patch('cv2.getWindowImageRect', return_value=[0, 0, 640, 480])
mocker.patch('cv2.waitKey', return_value=27)
mocker.patch('cv2.getWindowProperty', return_value=False)
mocker.patch('cv2.destroyAllWindows')
args = []
if qr_reader:
args.append('-Q')
args.append(qr_reader)
# Act
extract_otp_secrets.main(args)
# Assert
captured = capsys.readouterr()
if success:
assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG
assert captured.err == ''
mocked_polylines.assert_called_with(mocker.ANY, mocker.ANY, True, SUCCESS_COLOR, mocker.ANY)
mocked_putText.assert_called_with(mocker.ANY, "3 otps extracted", mocker.ANY, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
elif success is None:
assert captured.out == ''
assert captured.err == ''
mocked_polylines.assert_not_called()
mocked_putText.assert_called_with(mocker.ANY, "0 otps extracted", mocker.ANY, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
else:
assert captured.out == ''
assert captured.err != ''
mocked_polylines.assert_called_with(mocker.ANY, mocker.ANY, True, FAILURE_COLOR, mocker.ANY)
mocked_putText.assert_called_with(mocker.ANY, "0 otps extracted", mocker.ANY, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
else:
# Act
with pytest.raises(SystemExit) as e:
extract_otp_secrets.main([])
# Assert
captured = capsys.readouterr()
expected_err_msg = 'error: the following arguments are required: infile'
assert expected_err_msg in captured.err
assert captured.out == ''
assert e.value.code == 2
assert e.type == SystemExit
def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as e:
# Act