Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36fd0c0bb6 | ||
|
|
b215b78dad | ||
|
|
851cb6532c | ||
|
|
2bef64e5f6 | ||
|
|
3502294172 | ||
|
|
2707e244be |
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -2,6 +2,8 @@ name: tests
|
|||||||
|
|
||||||
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
|
# 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/using-workflows
|
||||||
|
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||||
|
# https://docs.github.com/en/actions/learn-github-actions/expressions
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -65,4 +67,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
pytest-coverage-path: ./pytest-coverage.txt
|
pytest-coverage-path: ./pytest-coverage.txt
|
||||||
junitxml-path: ./pytest.xml
|
junitxml-path: ./pytest.xml
|
||||||
if: matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
if: |
|
||||||
|
matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||||
|
&& !contains(github.ref, 'refs/tags/')
|
||||||
|
|
||||||
|
|||||||
2
.github/workflows/ci_docker.yml
vendored
2
.github/workflows/ci_docker.yml
vendored
@@ -2,6 +2,8 @@ name: docker
|
|||||||
|
|
||||||
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
|
# 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/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 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/
|
# How to run: https://aschmelyun.com/blog/using-docker-run-inside-of-github-actions/
|
||||||
|
|||||||
22
Pipfile
22
Pipfile
@@ -4,24 +4,26 @@ verify_ssl = true
|
|||||||
name = "pypi"
|
name = "pypi"
|
||||||
|
|
||||||
[packages]
|
[packages]
|
||||||
protobuf = "*"
|
|
||||||
qrcode = "*"
|
|
||||||
pillow = "*"
|
|
||||||
qreader = "*"
|
|
||||||
opencv-contrib-python = "*"
|
|
||||||
colorama = ">=0.4.6"
|
colorama = ">=0.4.6"
|
||||||
|
opencv-contrib-python = "*"
|
||||||
# for macOS: opencv-contrib-python = "<=4.7.0"
|
# for macOS: opencv-contrib-python = "<=4.7.0"
|
||||||
# for PYTHON <= 3.7: typing_extensions = "*"
|
# for PYTHON <= 3.7: typing_extensions = "*"
|
||||||
|
pillow = "*"
|
||||||
|
protobuf = "*"
|
||||||
|
qrcode = "*"
|
||||||
|
qreader = "*"
|
||||||
|
|
||||||
[dev-packages]
|
[dev-packages]
|
||||||
pytest = "*"
|
build = "*"
|
||||||
pytest-mock = "*"
|
|
||||||
pytest-cov = "*"
|
|
||||||
wheel = "*"
|
|
||||||
flake8 = "*"
|
flake8 = "*"
|
||||||
pylint = "*"
|
|
||||||
mypy = "*"
|
mypy = "*"
|
||||||
|
mypy-protobuf = "*"
|
||||||
|
pylint = "*"
|
||||||
|
pytest = "*"
|
||||||
|
pytest-cov = "*"
|
||||||
|
pytest-mock = "*"
|
||||||
types-protobuf = "*"
|
types-protobuf = "*"
|
||||||
|
wheel = "*"
|
||||||
|
|
||||||
[requires]
|
[requires]
|
||||||
python_version = "3.11"
|
python_version = "3.11"
|
||||||
|
|||||||
46
Pipfile.lock
generated
46
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "25b244c44cb891ac15ef20c4011eb043b87fb1f112396d68f470d0bb362e97f7"
|
"sha256": "13e2cc849fbc56593a2179a51a091717bcd4baeb9235b0843bb3abd8a9d1c698"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
@@ -220,6 +220,14 @@
|
|||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==22.2.0"
|
"version": "==22.2.0"
|
||||||
},
|
},
|
||||||
|
"build": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:1a07724e891cbd898923145eb7752ee7653674c511378eb9c7691aab1612bc3c",
|
||||||
|
"sha256:38a7a2b7a0bdc61a42a0a67509d88c71ecfc37b393baba770fae34e20929ff69"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.9.0"
|
||||||
|
},
|
||||||
"coverage": {
|
"coverage": {
|
||||||
"extras": [
|
"extras": [
|
||||||
"toml"
|
"toml"
|
||||||
@@ -387,6 +395,14 @@
|
|||||||
],
|
],
|
||||||
"version": "==0.4.3"
|
"version": "==0.4.3"
|
||||||
},
|
},
|
||||||
|
"mypy-protobuf": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:7d75a079651b105076776a35a5405e3fa773b8a167118f1b712e443e9a6c18a2",
|
||||||
|
"sha256:da33dfde7547ff57e5ba5564126cbfa114f14413b2fa50759b1fa5de1e4ab511"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==3.4.0"
|
||||||
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
|
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
|
||||||
@@ -395,6 +411,14 @@
|
|||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==22.0"
|
"version": "==22.0"
|
||||||
},
|
},
|
||||||
|
"pep517": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b",
|
||||||
|
"sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59"
|
||||||
|
],
|
||||||
|
"markers": "python_version >= '3.6'",
|
||||||
|
"version": "==0.13.0"
|
||||||
|
},
|
||||||
"platformdirs": {
|
"platformdirs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490",
|
"sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490",
|
||||||
@@ -411,6 +435,26 @@
|
|||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.6'",
|
||||||
"version": "==1.0.0"
|
"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": {
|
"pycodestyle": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
|
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
|
||||||
|
|||||||
62
README.md
62
README.md
@@ -1,7 +1,7 @@
|
|||||||
# Extract TOTP/HOTP secrets from QR codes exported by two-factor authentication apps
|
# Extract TOTP/HOTP secrets from QR codes exported by two-factor authentication apps
|
||||||
|
|
||||||
[](https://github.com/scito/extract_otp_secrets/actions/workflows/ci.yml)
|
[](https://github.com/scito/extract_otp_secrets/actions/workflows/ci.yml)
|
||||||

|

|
||||||
[](https://github.com/scito/extract_otp_secrets/actions/workflows/ci_docker.yml)
|
[](https://github.com/scito/extract_otp_secrets/actions/workflows/ci_docker.yml)
|
||||||

|

|
||||||
[](https://github.com/scito/extract_otp_secrets/blob/master/Pipfile.lock)
|
[](https://github.com/scito/extract_otp_secrets/blob/master/Pipfile.lock)
|
||||||
@@ -15,13 +15,13 @@
|
|||||||
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 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 exported QR codes from authentication apps can be read in three ways:
|
||||||
|
|
||||||
1. Capture from the system camera using a GUI, _(new!)_
|
1. Capture from the system camera using a GUI, 🆕
|
||||||
2. Read image files containing the QR codes, and _(new!)_
|
2. Read image files containing the QR codes, and 🆕
|
||||||
3. Read text files containing the QR code data generated by third-party QR readers.
|
3. Read text files containing the QR code data generated by third-party QR readers.
|
||||||
|
|
||||||
The secret and otp values can be exported to json or csv files, as well as printed or saved to PNG images.
|
The secret and otp values can be exported to json or csv files, as well as printed or saved to PNG images.
|
||||||
|
|
||||||
**The project and the script were renamed from extract_otp_secret_keys to extract_otp_secrets in version 2.0.0.**
|
⚡ **The project and the script were renamed from `extract_otp_secret_keys` to `extract_otp_secrets` in version 2.0.** ⚡
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ OpenCV requires [Visual C++ redistributable 2015](https://www.microsoft.com/en-u
|
|||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Capture QR codes from camera (since version 2.0.0)
|
### Capture QR codes from camera (🆕 since version 2.0)
|
||||||
|
|
||||||
1. Open "Google Authenticator" app on the mobile phone
|
1. Open "Google Authenticator" app on the mobile phone
|
||||||
2. Export the QR codes from "Google Authenticator" app
|
2. Export the QR codes from "Google Authenticator" app
|
||||||
@@ -81,7 +81,13 @@ OpenCV requires [Visual C++ redistributable 2015](https://www.microsoft.com/en-u
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
### With builtin QR decoder from image files (since version 2.0.0)
|
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
|
1. Open "Google Authenticator" app on the mobile phone
|
||||||
2. Export the QR codes from "Google Authenticator" app
|
2. Export the QR codes from "Google Authenticator" app
|
||||||
@@ -177,13 +183,13 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
|||||||
|
|
||||||
* Free and open source
|
* Free and open source
|
||||||
* Supports Google Authenticator exports (and compatible apps like Aegis Authenticator)
|
* 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)
|
* Captures the the QR codes directly from the camera using different QR code libraries (based on OpenCV) (🆕 since v2.0)
|
||||||
* ZBAR: [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) - fast and reliable, good for images and video capture (default and recommended)
|
* 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: [QReader](https://github.com/Eric-Canas/QReader)
|
||||||
* QREADER_DEEP: [QReader](https://github.com/Eric-Canas/QReader) - very slow in GUI
|
* 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: [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)
|
* CV2_WECHAT: [WeChatQRCode](https://docs.opencv.org/4.x/dd/d63/group__wechat__qrcode.html)
|
||||||
* Supports TOTP and HOTP standards
|
* Supports [TOTP](https://www.ietf.org/rfc/rfc6238.txt) and [HOTP](https://www.ietf.org/rfc/rfc4226.txt) standards
|
||||||
* Generates QR codes
|
* Generates QR codes
|
||||||
* Exports to various formats:
|
* Exports to various formats:
|
||||||
* CSV
|
* CSV
|
||||||
@@ -191,7 +197,8 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
|||||||
* Dedicated CSV for KeePass
|
* Dedicated CSV for KeePass
|
||||||
* QR code images
|
* QR code images
|
||||||
* Supports reading from stdin and writing to stdout, thus pipes can be used
|
* Supports reading from stdin and writing to stdout, thus pipes can be used
|
||||||
* Reads QR codes images: (See [OpenCV docu](https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56))
|
* 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)
|
||||||
* Portable Network Graphics - *.png
|
* Portable Network Graphics - *.png
|
||||||
* WebP - *.webp
|
* WebP - *.webp
|
||||||
* JPEG files - *.jpeg, *.jpg, *.jpe
|
* JPEG files - *.jpeg, *.jpg, *.jpe
|
||||||
@@ -199,8 +206,8 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
|||||||
* Windows bitmaps - *.bmp, *.dib
|
* Windows bitmaps - *.bmp, *.dib
|
||||||
* JPEG 2000 files - *.jp2
|
* JPEG 2000 files - *.jp2
|
||||||
* Portable image format - *.pbm, *.pgm, *.ppm *.pxm, *.pnm
|
* Portable image format - *.pbm, *.pgm, *.ppm *.pxm, *.pnm
|
||||||
* Prints errors and warnings to stderr
|
* Prints errors and warnings to stderr (🆕 since v2.0)
|
||||||
* Prints colored output
|
* Prints colored output (🆕 since v2.0)
|
||||||
* Many ways to run the script:
|
* Many ways to run the script:
|
||||||
* Native Python
|
* Native Python
|
||||||
* pipenv
|
* pipenv
|
||||||
@@ -209,7 +216,7 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
|||||||
* Docker
|
* Docker
|
||||||
* VSCode devcontainer
|
* VSCode devcontainer
|
||||||
* devbox
|
* devbox
|
||||||
* Prebuilt Docker images provided for amd64 and arm64
|
* Prebuilt Docker images provided for amd64 and arm64 (🆕 since v2.0)
|
||||||
* Compatible with major platforms:
|
* Compatible with major platforms:
|
||||||
* Linux
|
* Linux
|
||||||
* macOS
|
* macOS
|
||||||
@@ -308,7 +315,7 @@ curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/examp
|
|||||||
```
|
```
|
||||||
git clone https://github.com/scito/extract_otp_secrets.git
|
git clone https://github.com/scito/extract_otp_secrets.git
|
||||||
pip install -U -e extract_otp_secrets
|
pip install -U -e extract_otp_secrets
|
||||||
python -m extract_otp_secrets example_export.txt
|
python -m extract_otp_secrets extract_otp_secrets/example_export.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
### pipenv
|
### pipenv
|
||||||
@@ -368,7 +375,7 @@ 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 =
|
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 (X Window system required on host):
|
Capturing from camera in GUI window (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
|
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
|
||||||
@@ -447,6 +454,7 @@ Setup for running the tests in VSCode.
|
|||||||
### Build
|
### Build
|
||||||
|
|
||||||
```
|
```
|
||||||
|
cd extract_otp_secrets/
|
||||||
pip install -U -e .
|
pip install -U -e .
|
||||||
python src/extract_otp_secrets.py
|
python src/extract_otp_secrets.py
|
||||||
|
|
||||||
@@ -463,29 +471,37 @@ pip install -U -r requirements.txt
|
|||||||
|
|
||||||
### Build docker images
|
### Build docker images
|
||||||
|
|
||||||
|
#### Debian (full functionality)
|
||||||
|
|
||||||
Build and run the app within the container:
|
Build and run the app within the container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build . -t extract_otp_secrets --pull --build-arg RUN_TESTS=false
|
docker build . -t extract_otp_secrets --pull --build-arg RUN_TESTS=false
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build . -t extract_otp_secrets_only_txt --pull -f Dockerfile_only_txt --build-arg RUN_TESTS=false
|
|
||||||
```
|
|
||||||
|
|
||||||
Run tests in docker container:
|
Run tests in docker container:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets
|
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Alpine (only text file processing)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
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
|
docker build . -t extract_otp_secrets_only_txt --pull -f Dockerfile_only_txt --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_only_txt tests/extract_otp_secrets_test.py -k "not qreader" --relaxed
|
||||||
```
|
```
|
||||||
|
|
||||||
## Issues
|
## Issues
|
||||||
|
|
||||||
* Known issue for macOS: https://github.com/opencv/opencv/issues/23072
|
* 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
|
||||||
|
|
||||||
## Problems and Troubleshooting
|
## Problems and Troubleshooting
|
||||||
|
|
||||||
@@ -518,10 +534,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.
|
* [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.
|
* [Aegis Authenticator](https://github.com/beemdevelopment/Aegis) is a free, secure and open source 2FA app for Android.
|
||||||
* [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.]
|
* [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) is a good QR code reader Python module
|
||||||
* [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)
|
* [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.]
|
||||||
|
|
||||||
***
|
***
|
||||||
|
|
||||||
|
|||||||
85
build.sh
85
build.sh
@@ -138,6 +138,8 @@ PIPENV="$PYTHON -m pipenv"
|
|||||||
FLAKE8="$PYTHON -m flake8"
|
FLAKE8="$PYTHON -m flake8"
|
||||||
MYPY="$PYTHON -m mypy"
|
MYPY="$PYTHON -m mypy"
|
||||||
|
|
||||||
|
# sudo ln -s /usr/bin/python3.11 /usr/bin/python
|
||||||
|
|
||||||
# Upgrade protoc
|
# Upgrade protoc
|
||||||
|
|
||||||
DEST="protoc"
|
DEST="protoc"
|
||||||
@@ -147,6 +149,26 @@ echo -e "\nProtoc remote version $VERSION\n"
|
|||||||
echo -e "Protoc local version: $OLDVERSION\n"
|
echo -e "Protoc local version: $OLDVERSION\n"
|
||||||
|
|
||||||
if $clean; then
|
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;"
|
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
|
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
@@ -160,6 +182,10 @@ if $clean; then
|
|||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
fi
|
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"
|
||||||
|
|
||||||
if [ "$OLDVERSION" != "$VERSION" ] || ! $ignore_version_check; then
|
if [ "$OLDVERSION" != "$VERSION" ] || ! $ignore_version_check; then
|
||||||
echo "Upgrade protoc from $OLDVERSION to $VERSION"
|
echo "Upgrade protoc from $OLDVERSION to $VERSION"
|
||||||
|
|
||||||
@@ -216,7 +242,7 @@ fi
|
|||||||
|
|
||||||
# Upgrade pip requirements
|
# Upgrade pip requirements
|
||||||
|
|
||||||
cmd="sudo pip install -U pip"
|
cmd="pip install -U pip"
|
||||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
|
|
||||||
@@ -226,10 +252,6 @@ cmd="$PIP install --use-pep517 -U -r requirements.txt"
|
|||||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||||
eval "$cmd"
|
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
|
||||||
|
|
||||||
LINT_OUT_FILE="tests/reports/flake8_results.txt"
|
LINT_OUT_FILE="tests/reports/flake8_results.txt"
|
||||||
@@ -254,7 +276,21 @@ cmd="$MYPY --strict src/*.py tests/*.py | tee $TYPE_CHECK_OUT_FILE"
|
|||||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
|
|
||||||
# Test
|
# 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)
|
||||||
|
|
||||||
cmd="$PYTHON src/extract_otp_secrets.py example_export.txt"
|
cmd="$PYTHON src/extract_otp_secrets.py example_export.txt"
|
||||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||||
@@ -287,37 +323,9 @@ cmd="$PIPENV run pytest --cov=extract_otp_secrets_test tests/"
|
|||||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||||
eval "$cmd"
|
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
|
# Build wheel
|
||||||
|
|
||||||
cmd="$PIP wheel .
|
cmd="$PIP wheel ."
|
||||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
|
|
||||||
@@ -378,10 +386,6 @@ if $build_docker; then
|
|||||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||||
eval "$cmd"
|
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
|
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 &"
|
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
|
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||||
@@ -402,6 +406,7 @@ cmd="cat $TYPE_CHECK_OUT_FILE $LINT_OUT_FILE $COVERAGE_OUT_FILE"
|
|||||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
|
|
||||||
echo -e "\n${greenBold}SUCCESS${reset}"
|
line=$(printf '#%.0s' $(eval echo {1..$(( ($COLUMNS - 10) / 2))}))
|
||||||
|
echo -e "\n${greenBold}$line SUCCESS $line${reset}"
|
||||||
|
|
||||||
quit
|
quit
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ classifiers = [
|
|||||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"protobuf",
|
|
||||||
"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",
|
"colorama>=0.4.6",
|
||||||
|
"opencv-contrib-python; sys_platform != 'darwin'",
|
||||||
|
"opencv-contrib-python<=4.7.0; sys_platform == 'darwin'",
|
||||||
|
"Pillow",
|
||||||
|
"protobuf",
|
||||||
|
"pyzbar",
|
||||||
|
"qrcode",
|
||||||
|
"qreader",
|
||||||
|
"typing_extensions; python_version<='3.7'",
|
||||||
]
|
]
|
||||||
description = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as 'Google Authenticator'"
|
description = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as 'Google Authenticator'"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
|
build
|
||||||
flake8
|
flake8
|
||||||
mypy
|
mypy
|
||||||
types-protobuf
|
mypy-protobuf
|
||||||
pylint
|
pylint
|
||||||
pytest
|
pytest
|
||||||
pytest-mock
|
|
||||||
pytest-cov
|
pytest-cov
|
||||||
|
pytest-mock
|
||||||
setuptools
|
setuptools
|
||||||
|
types-protobuf
|
||||||
wheel
|
wheel
|
||||||
build
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
protobuf
|
|
||||||
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
|
colorama>=0.4.6
|
||||||
|
opencv-contrib-python; sys_platform != 'darwin'
|
||||||
|
opencv-contrib-python<=4.7.0; sys_platform == 'darwin'
|
||||||
|
Pillow
|
||||||
|
protobuf
|
||||||
|
pyzbar
|
||||||
|
qrcode
|
||||||
|
qreader
|
||||||
|
typing_extensions; python_version<='3.7'
|
||||||
|
|||||||
@@ -220,42 +220,6 @@ def extract_otps(args: Args) -> Otps:
|
|||||||
return extract_otps_from_files(args)
|
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:
|
def extract_otps_from_camera(args: Args) -> Otps:
|
||||||
if verbose: print("Capture QR codes from camera")
|
if verbose: print("Capture QR codes from camera")
|
||||||
otp_urls: OtpUrls = []
|
otp_urls: OtpUrls = []
|
||||||
@@ -325,6 +289,42 @@ def extract_otps_from_camera(args: Args) -> Otps:
|
|||||||
return 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]:
|
def cv2_handle_pressed_keys(qr_mode: QRMode) -> Tuple[bool, QRMode]:
|
||||||
key = cv2.waitKey(1) & 0xFF
|
key = cv2.waitKey(1) & 0xFF
|
||||||
quit = False
|
quit = False
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ from typing import Any
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from extract_otp_secrets import QRMode
|
|
||||||
|
|
||||||
|
|
||||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||||
parser.addoption("--relaxed", action='store_true', help="run tests in relaxed mode")
|
parser.addoption("--relaxed", action='store_true', help="run tests in relaxed mode")
|
||||||
@@ -17,6 +15,7 @@ def relaxed(request: pytest.FixtureRequest) -> Any:
|
|||||||
|
|
||||||
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
|
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
|
||||||
if "qr_mode" in metafunc.fixturenames:
|
if "qr_mode" in metafunc.fixturenames:
|
||||||
number = 2 if metafunc.config.getoption("fast") else len(QRMode)
|
all_qr_modes = ['ZBAR', 'QREADER', 'QREADER_DEEP', 'CV2', 'CV2_WECHAT']
|
||||||
qr_modes = [mode.name for mode in QRMode]
|
number = 2 if metafunc.config.getoption("fast") else len(all_qr_modes)
|
||||||
|
qr_modes = [mode for mode in all_qr_modes]
|
||||||
metafunc.parametrize("qr_mode", qr_modes[0:number])
|
metafunc.parametrize("qr_mode", qr_modes[0:number])
|
||||||
|
|||||||
3
tests/data/only_comments.txt
Normal file
3
tests/data/only_comments.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
# comment 1
|
||||||
|
|
||||||
|
# comment 2
|
||||||
@@ -26,7 +26,8 @@ import pathlib
|
|||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
from typing import Optional
|
from enum import Enum
|
||||||
|
from typing import Any, List, Optional, Tuple
|
||||||
|
|
||||||
import colorama
|
import colorama
|
||||||
import pytest
|
import pytest
|
||||||
@@ -37,6 +38,12 @@ from utils import (count_files_in_dir, file_exits, read_binary_file_as_stream,
|
|||||||
|
|
||||||
import extract_otp_secrets
|
import extract_otp_secrets
|
||||||
|
|
||||||
|
try:
|
||||||
|
import cv2 # type: ignore
|
||||||
|
except ImportError:
|
||||||
|
# ignore
|
||||||
|
pass
|
||||||
|
|
||||||
qreader_available: bool = extract_otp_secrets.qreader_available
|
qreader_available: bool = extract_otp_secrets.qreader_available
|
||||||
|
|
||||||
|
|
||||||
@@ -218,6 +225,18 @@ def test_keepass_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path)
|
|||||||
assert captured.err == ''
|
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:
|
def test_keepass_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
'''Two csv files .totp and .htop are generated.'''
|
'''Two csv files .totp and .htop are generated.'''
|
||||||
# Act
|
# Act
|
||||||
@@ -494,6 +513,89 @@ def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: Mocker
|
|||||||
assert e.type == SystemExit
|
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", [
|
||||||
|
None,
|
||||||
|
'ZBAR',
|
||||||
|
'QREADER',
|
||||||
|
'QREADER_DEEP',
|
||||||
|
'CV2',
|
||||||
|
'CV2_WECHAT'
|
||||||
|
])
|
||||||
|
def test_extract_otps_from_camera(qr_reader: Optional[str], capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
|
||||||
|
if qreader_available:
|
||||||
|
# Arrange
|
||||||
|
mockCam = MockCam()
|
||||||
|
mocker.patch('cv2.VideoCapture', return_value=mockCam)
|
||||||
|
mocker.patch('cv2.namedWindow')
|
||||||
|
mocker.patch('cv2.rectangle')
|
||||||
|
mocker.patch('cv2.polylines')
|
||||||
|
mocker.patch('cv2.imshow')
|
||||||
|
mocker.patch('cv2.getTextSize', return_value=([8, 200], False))
|
||||||
|
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()
|
||||||
|
|
||||||
|
assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG
|
||||||
|
assert captured.err == ''
|
||||||
|
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:
|
def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
# Act
|
# Act
|
||||||
|
|||||||
Reference in New Issue
Block a user