mirror of
https://github.com/scito/extract_otp_secrets.git
synced 2025-12-12 17:59:48 +01:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fe60acb24f | ||
|
|
5c36f07f41 | ||
|
|
861d7d0da8 | ||
|
|
63fc21cc90 | ||
|
|
d234cf4112 | ||
|
|
970dbd3759 | ||
|
|
197347a3e9 | ||
|
|
365d5ac432 | ||
|
|
88ff584e47 | ||
|
|
dbb5d8f755 |
99
.github/workflows/ci_docker.yml
vendored
99
.github/workflows/ci_docker.yml
vendored
@@ -50,6 +50,11 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Workaround for failing builds: https://github.com/docker/build-push-action/issues/761#issuecomment-1383822381
|
||||
# TODO remove workaround when fixed
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
@@ -87,7 +92,14 @@ jobs:
|
||||
- name: Image digest
|
||||
# TODO upload digests to assets
|
||||
run: |
|
||||
echo "extract_otp_secrets: ${{ steps.docker_build_qr_reader_latest.outputs.digest }}"
|
||||
echo "extract_otp_secrets digests: ${{ steps.docker_build_qr_reader_latest.outputs.digest }}"
|
||||
echo "${{ steps.docker_build_qr_reader_latest.outputs.digest }}" > digests.txt
|
||||
- name: Save docker digests as artifacts
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: debian_digests
|
||||
path: digests.txt
|
||||
|
||||
build-and-push-docker-alpine-image:
|
||||
name: Build Docker Alpine image and push to repositories
|
||||
@@ -150,8 +162,91 @@ jobs:
|
||||
build-args: |
|
||||
RUN_TESTS=true
|
||||
|
||||
- name: Image digest
|
||||
# TODO upload digests to assets
|
||||
run: |
|
||||
echo "extract_otp_secrets:only-txt digests: ${{ steps.docker_build_only_txt.outputs.digest }}"
|
||||
echo "${{ steps.docker_build_qr_reader_latest.outputs.digest }}" > digests.txt
|
||||
|
||||
- name: Save docker digests as artifacts
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: alpine_digests
|
||||
path: digests.txt
|
||||
|
||||
build-and-push-docker-buster-image:
|
||||
name: Build Docker Buster image (for PyInstsaller) and push to repositories
|
||||
# run only when code is compiling and tests are passing
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# steps to perform in job
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
|
||||
# avoid building if there are testing errors
|
||||
- name: Run smoke test
|
||||
run: |
|
||||
sudo apt-get install -y libzbar0
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U -r requirements-dev.txt
|
||||
pip install -U .
|
||||
pytest
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
# setup Docker build action
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Workaround for failing builds: https://github.com/docker/build-push-action/issues/761#issuecomment-1383822381
|
||||
# TODO remove workaround when fixed
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_IO_TOKEN }}
|
||||
|
||||
- name: "Build image from Buster and push to GitHub Container Registry"
|
||||
id: docker_build_buster
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
# relative path to the place where source code with Dockerfile is located
|
||||
# TODO file:, move to docker/
|
||||
context: .
|
||||
file: Dockerfile
|
||||
# builder: ${{ steps.buildx.outputs.name }}
|
||||
build-args: |
|
||||
BASE_IMAGE=python:3.11-slim-buster
|
||||
# Note: tags has to be all lower-case
|
||||
pull: true
|
||||
tags: |
|
||||
scit0/extract_otp_secrets:buster
|
||||
push: true
|
||||
|
||||
- name: Image digest
|
||||
# TODO upload digests to assets
|
||||
run: |
|
||||
echo "extract_otp_secrets:only-txt: ${{ steps.docker_build_only_txt.outputs.digest }}"
|
||||
echo "extract_otp_secrets digests: ${{ steps.docker_build_qr_reader_latest.outputs.digest }}"
|
||||
echo "${{ steps.docker_build_qr_reader_latest.outputs.digest }}" > digests.txt
|
||||
- name: Save docker digests as artifacts
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: buster_digests
|
||||
path: digests.txt
|
||||
|
||||
90
.github/workflows/ci_release.yml
vendored
90
.github/workflows/ci_release.yml
vendored
@@ -1,10 +1,8 @@
|
||||
name: release
|
||||
|
||||
# https://data-dive.com/multi-os-deployment-in-cloud-using-pyinstaller-and-github-actions
|
||||
# https://github.com/actions/create-release (archived)
|
||||
# https://github.com/actions/upload-artifact
|
||||
# https://github.com/actions/download-artifact
|
||||
# https://github.com/actions/upload-release-asset (archived)
|
||||
# https://github.com/docker/metadata-action
|
||||
# https://github.com/marketplace/actions/generate-release-hashes
|
||||
|
||||
@@ -36,6 +34,10 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
pull_request:
|
||||
schedule:
|
||||
# Run weekly on default branch
|
||||
- cron: '47 4 * * 6'
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -45,6 +47,7 @@ jobs:
|
||||
steps:
|
||||
- name: Set meta data
|
||||
id: meta
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
# Writing to env with >> $GITHUB_ENV is an alternative
|
||||
run: |
|
||||
echo "date=$(TZ=Europe/Zurich date +'%d.%m.%Y')" >> $GITHUB_OUTPUT
|
||||
@@ -55,6 +58,7 @@ jobs:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
# https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release
|
||||
response=$(curl \
|
||||
@@ -70,17 +74,19 @@ jobs:
|
||||
echo $(jq -r '.upload_url' <<< "$response") > release_url.txt
|
||||
echo $(jq -r '.id' <<< "$response") > release_id.txt
|
||||
- name: Save Release URL File for publish
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: release_url
|
||||
path: release_url.txt
|
||||
- name: Save asset upload id for publish
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: release_id
|
||||
path: release_id.txt
|
||||
|
||||
build-and-push-docker-image:
|
||||
build-linux-executable-in-docker:
|
||||
name: Build Linux release in docker container
|
||||
# run only when code is compiling and tests are passing
|
||||
runs-on: ubuntu-latest
|
||||
@@ -107,6 +113,11 @@ jobs:
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
# Workaround for failing builds: https://github.com/docker/build-push-action/issues/761#issuecomment-1383822381
|
||||
# TODO remove workaround when fixed
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
@@ -123,6 +134,8 @@ jobs:
|
||||
|
||||
- name: "Build image from Buster and push to GitHub Container Registry"
|
||||
id: docker_build_buster
|
||||
# Disable and build in ci_docker for speeding up releases
|
||||
if: false
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
@@ -136,7 +149,7 @@ jobs:
|
||||
# Note: tags has to be all lower-case
|
||||
pull: true
|
||||
tags: |
|
||||
ghcr.io/scito/extract_otp_secrets:buster
|
||||
scit0/extract_otp_secrets:buster
|
||||
push: true
|
||||
|
||||
# # https://stackoverflow.com/a/61155718/1663871
|
||||
@@ -153,7 +166,7 @@ jobs:
|
||||
- name: Run Pyinstaller in container
|
||||
run: |
|
||||
# TODO use local docker image https://stackoverflow.com/a/61155718/1663871
|
||||
docker run --pull always --entrypoint /bin/bash --rm -v "$(pwd)":/files -w /files ghcr.io/scito/extract_otp_secrets:buster -c 'apt-get update && apt-get -y install binutils && pip install -U -r /files/requirements.txt && pip install pyinstaller && pyinstaller -y --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_linux_x86_64 --distpath /files/dist/ /files/src/extract_otp_secrets.py'
|
||||
docker run --pull always --entrypoint /bin/bash --rm -v "$(pwd)":/files -w /files scit0/extract_otp_secrets:buster -c 'apt-get update && apt-get -y install binutils && pip install -U -r /files/requirements.txt && pip install pyinstaller && pyinstaller -y --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_linux_x86_64 --distpath /files/dist/ /files/src/extract_otp_secrets.py'
|
||||
|
||||
- name: Smoke tests
|
||||
run: |
|
||||
@@ -167,6 +180,7 @@ jobs:
|
||||
dist/extract_otp_secrets_linux_x86_64 --qr CV2 example_export.png
|
||||
dist/extract_otp_secrets_linux_x86_64 --qr CV2_WECHAT example_export.png
|
||||
- name: Load Release URL File from release job
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: release_url
|
||||
@@ -174,7 +188,7 @@ jobs:
|
||||
run: ls -R
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
# TODO only for tags
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
response=$(curl \
|
||||
-X POST \
|
||||
@@ -187,8 +201,8 @@ jobs:
|
||||
--data-binary @dist/extract_otp_secrets_linux_x86_64 \
|
||||
$(cat release_url.txt)=extract_otp_secrets_linux_x86_64)
|
||||
|
||||
build:
|
||||
name: Build packages
|
||||
build-native-executables:
|
||||
name: Build native packages
|
||||
needs: create-release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
@@ -281,10 +295,12 @@ jobs:
|
||||
run: |
|
||||
dist/${{ matrix.OUT_FILE_NAME }} - < example_export.txt
|
||||
- name: Load Release URL File from release job
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: release_url
|
||||
- name: Load Release Id File from release job
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: release_id
|
||||
@@ -292,14 +308,66 @@ jobs:
|
||||
run: ls -R
|
||||
- name: Set meta data
|
||||
id: meta
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: |
|
||||
cat release_url.txt
|
||||
echo "release_url=$(cat release_url.txt)" >> $GITHUB_OUTPUT
|
||||
echo "release_id=$(cat release_id.txt)" >> $GITHUB_OUTPUT
|
||||
echo "upload_url=https://uploads.github.com/repos/scito/extract_otp_secrets/releases/$(cat release_id.txt)/assets?name=" >> $GITHUB_OUTPUT
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
if: ${{ matrix.UPLOAD }}
|
||||
if: matrix.UPLOAD && startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
curl -X POST -H "Accept: application/vnd.github+json" -H "Content-Type: ${{ matrix.ASSET_MIME }}" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" --show-error --data-binary @dist/${{ matrix.OUT_FILE_NAME }} ${{ steps.meta.outputs.upload_url }}=${{ matrix.ASSET_NAME }}
|
||||
|
||||
upload-hashes:
|
||||
name: Upload hashes
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs:
|
||||
- build-linux-executable-in-docker
|
||||
- build-native-executables
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Load Release Id File from release job
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: release_id
|
||||
- name: Set meta data
|
||||
id: meta
|
||||
run: |
|
||||
echo "release_id=$(cat release_id.txt)" >> $GITHUB_OUTPUT
|
||||
echo "upload_url=https://uploads.github.com/repos/scito/extract_otp_secrets/releases/$(cat release_id.txt)/assets?name=" >> $GITHUB_OUTPUT
|
||||
- name: Calculate and upload hashes from assets
|
||||
run: |
|
||||
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
|
||||
for asset_url in $(curl \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN"\
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--silent \
|
||||
--show-error \
|
||||
https://api.github.com/repos/scito/extract_otp_secrets/releases/${{ steps.meta.outputs.release_id }}/assets |
|
||||
jq -r '.[].url'); do
|
||||
echo "Download $asset_url"
|
||||
name=$(curl \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN"\
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--output-dir assets \
|
||||
-L \
|
||||
$asset_url |
|
||||
jq -r '.name')
|
||||
curl \
|
||||
-H "Accept: application/octet-stream" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN"\
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--create-dirs \
|
||||
--output-dir assets \
|
||||
-L \
|
||||
-o $name \
|
||||
$asset_url
|
||||
done
|
||||
(cd assets/ && sha256sum * > ../sha256_hashes.txt)
|
||||
curl -X POST -H "Accept: application/vnd.github+json" -H "Content-Type: text/plain" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" --show-error --data-binary @sha256_hashes.txt ${{ steps.meta.outputs.upload_url }}=sha256_hashes.txt
|
||||
|
||||
(cd assets/ && sha512sum * > ../sha512_hashes.txt)
|
||||
curl -X POST -H "Accept: application/vnd.github+json" -H "Content-Type: text/plain" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" --show-error --data-binary @sha512_hashes.txt ${{ steps.meta.outputs.upload_url }}=sha512_hashes.txt
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -25,3 +25,4 @@ dist_*/
|
||||
file_version_info_python.txt
|
||||
file_version_info_explorer.txt
|
||||
file_version_info.txt
|
||||
assets/*
|
||||
|
||||
@@ -21,6 +21,7 @@ RUN apt-get update && apt-get install -y \
|
||||
libglib2.0-0 \
|
||||
libsm6 \
|
||||
libzbar0 \
|
||||
python3-tk \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& pip install --no-cache-dir -U -r requirements.txt \
|
||||
&& if [ "$RUN_TESTS" = "true" ]; then /extract/run_pytest.sh; else echo "Not running tests..."; fi \
|
||||
|
||||
1
Pipfile
1
Pipfile
@@ -9,6 +9,7 @@ opencv-contrib-python = "*"
|
||||
# for macOS: opencv-contrib-python = "<=4.7.0"
|
||||
# for PYTHON <= 3.7: typing_extensions = "*"
|
||||
pillow = "*"
|
||||
pyzbar = "*"
|
||||
protobuf = "*"
|
||||
qrcode = "*"
|
||||
qreader = "<2.0.0"
|
||||
|
||||
17
Pipfile.lock
generated
17
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "41edd4aebe075d6c39d035ec7cb10f0253a3ad21f9b4aa5b9c57deccca87874f"
|
||||
"sha256": "42b14c5eae25b0924354520fe0a26a8d826c905f4613d717f3bfa52e98ed5e8e"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -200,6 +200,7 @@
|
||||
"sha256:4559628b8192feb25766d954b36a3753baaf5c97c03135aec7e4a026036b475d",
|
||||
"sha256:8f4c5264c9c7c6b9f20d01efc52a4eba1ded47d9ba857a94130afe33703eb518"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.1.9"
|
||||
},
|
||||
"qrcode": {
|
||||
@@ -336,11 +337,11 @@
|
||||
},
|
||||
"isort": {
|
||||
"hashes": [
|
||||
"sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6",
|
||||
"sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"
|
||||
"sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504",
|
||||
"sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==5.11.4"
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"version": "==5.12.0"
|
||||
},
|
||||
"lazy-object-proxy": {
|
||||
"hashes": [
|
||||
@@ -545,11 +546,11 @@
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:6f590d76b713d5de4e49fe4fbca24474469f53c83632d5d0fd056f7ff7e8112b",
|
||||
"sha256:ac4008d396bc9cd983ea483cb7139c0240a07bbc74ffb6232fceffedc6cf03a8"
|
||||
"sha256:883131c5b6efa70b9101c7ef30b2b7b780a4283d5fc1616383cdf22c83cbefe6",
|
||||
"sha256:9d790961ba6219e9ff7d9557622d2fe136816a264dd01d5997cfc057d804853d"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==66.1.1"
|
||||
"version": "==67.0.0"
|
||||
},
|
||||
"setuptools-git-versioning": {
|
||||
"hashes": [
|
||||
|
||||
17
README.md
17
README.md
@@ -94,7 +94,10 @@ The secrets can be exported to JSON or CSV, or printed as QR codes to console or
|
||||
:heavy_check_mark: Everything is just packed in one executable.
|
||||
:heavy_check_mark: No installation needed, neither Python nor any dependencies have to be installed.
|
||||
:heavy_check_mark: Easy and convenient
|
||||
:information_source: There is a delay after starting the executable since the files have internally to be unpacked.
|
||||
|
||||
> :information_source: There is a delay after starting the executable since the files have internally to be unpacked.
|
||||
|
||||
> :information_source: If you are a developer, you might prefer to run the Python script directly, see [Installation](#installation-of-python-script-recommend-for-developers-or-advanced-users)
|
||||
|
||||
> :warning: Some antivirus tools may show a virus or trojan alert for the executable.
|
||||
> This alert is a false positive.
|
||||
@@ -123,6 +126,14 @@ Detected QR codes are surrounded with a frame. The color of the frame indicates
|
||||
* 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.
|
||||
|
||||
Key commands:
|
||||
|
||||
* Space: change QR code reader
|
||||
* C: save as csv file (🆕 since v2.2)
|
||||
* J: save as json file (🆕 since v2.2)
|
||||
* K: save as KeePass csv file (🆕 since v2.2)
|
||||
* ESC, ENTER, Q: quit the program
|
||||
|
||||
The secrets are printed by default to the console. [Set program parameters](#program-help-arguments-and-options) for other types of output, e.g. `--csv exported_secrets.csv`.
|
||||
|
||||
### With builtin QR decoder from image files (🆕 since version 2.0)
|
||||
@@ -289,6 +300,10 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
||||
* 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)
|
||||
* Program usable as pure GUI application without any command line switches (🆕 since v2.2)
|
||||
* Save otp secrets as csv file (🆕 since v2.2)
|
||||
* Save otp secrets as json file (🆕 since v2.2)
|
||||
* Save otp secrets as KeePass csv file(s) (🆕 since v2.2)
|
||||
* Supports [TOTP](https://www.ietf.org/rfc/rfc6238.txt) and [HOTP](https://www.ietf.org/rfc/rfc4226.txt) standards
|
||||
* Generates QR codes
|
||||
* Exports to various formats:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
colorama>=0.4.6
|
||||
importlib_metadata; python_version<='3.7'
|
||||
opencv-contrib-python; sys_platform != 'darwin'
|
||||
opencv-contrib-python<=4.7.0; sys_platform == 'darwin'
|
||||
Pillow
|
||||
@@ -7,4 +8,3 @@ pyzbar
|
||||
qrcode
|
||||
qreader<2.0.0
|
||||
typing_extensions; python_version<='3.7'
|
||||
importlib_metadata; python_version<='3.7'
|
||||
|
||||
@@ -65,11 +65,20 @@ else:
|
||||
|
||||
|
||||
debug_mode = '-d' in sys.argv[1:] or '--debug' in sys.argv[1:]
|
||||
headless: bool = False
|
||||
|
||||
|
||||
try:
|
||||
import cv2 # type: ignore # TODO use cv2 types if available
|
||||
import numpy as np # TODO use numpy types if available
|
||||
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.filedialog
|
||||
import tkinter.messagebox
|
||||
except ImportError:
|
||||
headless = True
|
||||
|
||||
try:
|
||||
import pyzbar.pyzbar as zbar # type: ignore
|
||||
from qreader import QReader # type: ignore
|
||||
@@ -143,6 +152,7 @@ quiet: bool = False
|
||||
colored: bool = True
|
||||
executable: bool = False
|
||||
__version__: str
|
||||
tk_root: tkinter.Tk
|
||||
|
||||
|
||||
def sys_main() -> None:
|
||||
@@ -150,7 +160,7 @@ def sys_main() -> None:
|
||||
|
||||
|
||||
def main(sys_args: list[str]) -> None:
|
||||
global executable
|
||||
global executable, tk_root, headless
|
||||
# allow to use sys.stdout with with (avoid closing)
|
||||
sys.stdout.close = lambda: None # type: ignore
|
||||
# set encoding to utf-8, needed for Windows
|
||||
@@ -164,6 +174,13 @@ def main(sys_args: list[str]) -> None:
|
||||
# https://pyinstaller.org/en/stable/runtime-information.html#run-time-information
|
||||
executable = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
|
||||
|
||||
if qreader_available and not headless:
|
||||
try:
|
||||
tk_root = tkinter.Tk()
|
||||
tk_root.withdraw()
|
||||
except tkinter.TclError:
|
||||
headless = True
|
||||
|
||||
args = parse_args(sys_args)
|
||||
|
||||
if colored:
|
||||
@@ -174,9 +191,10 @@ def main(sys_args: list[str]) -> None:
|
||||
sys.exit(0 if do_debug_checks() else 1)
|
||||
|
||||
otps = extract_otps(args)
|
||||
write_csv(args, otps)
|
||||
write_keepass_csv(args, otps)
|
||||
write_json(args, otps)
|
||||
|
||||
write_csv(args.csv, otps)
|
||||
write_keepass_csv(args.keepass, otps)
|
||||
write_json(args.json, otps)
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: pb.MigrationPayload | None
|
||||
@@ -362,15 +380,16 @@ def extract_otps_from_camera(args: Args) -> Otps:
|
||||
qr_mode = next_qr_mode(qr_mode)
|
||||
continue
|
||||
|
||||
cv2_print_text(img, f"Mode: {qr_mode.name} (Hit space to change)", 0, TextPosition.LEFT, FONT_COLOR, 20)
|
||||
cv2_print_text(img, "Hit ESC to quit", 1, TextPosition.LEFT, FONT_COLOR, 17)
|
||||
cv2_print_text(img, f"Mode: {qr_mode.name} (Hit SPACE to change)", 0, TextPosition.LEFT, FONT_COLOR, 20)
|
||||
cv2_print_text(img, "Press ESC to quit", 1, TextPosition.LEFT, FONT_COLOR, 17)
|
||||
cv2_print_text(img, "Press C/J/K to save as csv/json/keepass file", 2, TextPosition.LEFT, FONT_COLOR, None)
|
||||
|
||||
cv2_print_text(img, f"{len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} captured", 0, TextPosition.RIGHT, FONT_COLOR)
|
||||
cv2_print_text(img, f"{len(otps)} otp{'s'[:len(otps) != 1]} extracted", 1, TextPosition.RIGHT, FONT_COLOR)
|
||||
|
||||
cv2.imshow(WINDOW_NAME, img)
|
||||
|
||||
quit, qr_mode = cv2_handle_pressed_keys(qr_mode)
|
||||
quit, qr_mode = cv2_handle_pressed_keys(qr_mode, otps)
|
||||
if quit:
|
||||
break
|
||||
|
||||
@@ -416,12 +435,48 @@ def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition
|
||||
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, otps: Otps) -> Tuple[bool, QRMode]:
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
quit = False
|
||||
if key == 27 or key == ord('q') or key == 13:
|
||||
if key == 27 or key == ord('q') or key == ord('Q') or key == 13:
|
||||
# ESC or Enter or q pressed
|
||||
quit = True
|
||||
elif (key == ord('c') or key == ord('C')) and is_not_headless():
|
||||
if has_no_otps_show_warning(otps):
|
||||
pass
|
||||
else:
|
||||
file_name = tkinter.filedialog.asksaveasfilename(
|
||||
title="Save extracted otp secrets as CSV",
|
||||
defaultextension='.csv',
|
||||
filetypes=[('CSV', '*.csv'), ('All', '*.*')]
|
||||
)
|
||||
tk_root.update()
|
||||
if len(file_name) > 0:
|
||||
write_csv(file_name, otps)
|
||||
elif (key == ord('j') or key == ord('J')) and is_not_headless():
|
||||
if has_no_otps_show_warning(otps):
|
||||
pass
|
||||
else:
|
||||
file_name = tkinter.filedialog.asksaveasfilename(
|
||||
title="Save extracted otp secrets as JSON",
|
||||
defaultextension='.json',
|
||||
filetypes=[('JSON', '*.json'), ('All', '*.*')]
|
||||
)
|
||||
tk_root.update()
|
||||
if len(file_name) > 0:
|
||||
write_json(file_name, otps)
|
||||
elif (key == ord('k') or key == ord('K')) and is_not_headless():
|
||||
if has_no_otps_show_warning(otps):
|
||||
pass
|
||||
else:
|
||||
file_name = tkinter.filedialog.asksaveasfilename(
|
||||
title="Save extracted otp secrets as KeePass CSV file(s)",
|
||||
defaultextension='.csv',
|
||||
filetypes=[('CSV', '*.csv'), ('All', '*.*')]
|
||||
)
|
||||
tk_root.update()
|
||||
if len(file_name) > 0:
|
||||
write_keepass_csv(file_name, otps)
|
||||
elif key == 32:
|
||||
qr_mode = next_qr_mode(qr_mode)
|
||||
if verbose >= LogLevel.MORE_VERBOSE: print(f"QR reading mode: {qr_mode}")
|
||||
@@ -626,22 +681,22 @@ def print_qr(args: Args, otp_url: str) -> None:
|
||||
qr.print_ascii()
|
||||
|
||||
|
||||
def write_csv(args: Args, otps: Otps) -> None:
|
||||
if args.csv and len(otps) > 0:
|
||||
with open_file_or_stdout_for_csv(args.csv) as outfile:
|
||||
def write_csv(file: str, otps: Otps) -> None:
|
||||
if file and len(file) > 0 and len(otps) > 0:
|
||||
with open_file_or_stdout_for_csv(file) as outfile:
|
||||
writer = csv.DictWriter(outfile, otps[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(otps)
|
||||
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to csv {args.csv}")
|
||||
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to csv {file}")
|
||||
|
||||
|
||||
def write_keepass_csv(args: Args, otps: Otps) -> None:
|
||||
if args.keepass and len(otps) > 0:
|
||||
def write_keepass_csv(file: str, otps: Otps) -> None:
|
||||
if file and len(file) > 0 and len(otps) > 0:
|
||||
has_totp = has_otp_type(otps, 'totp')
|
||||
has_hotp = has_otp_type(otps, 'hotp')
|
||||
if args.keepass != '-':
|
||||
otp_filename_totp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "totp")
|
||||
otp_filename_hotp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "hotp")
|
||||
if file != '-':
|
||||
otp_filename_totp = file if has_totp != has_hotp else add_pre_suffix(file, "totp")
|
||||
otp_filename_hotp = file if has_totp != has_hotp else add_pre_suffix(file, "hotp")
|
||||
else:
|
||||
otp_filename_totp = otp_filename_hotp = '-'
|
||||
if has_totp:
|
||||
@@ -653,9 +708,9 @@ def write_keepass_csv(args: Args, otps: Otps) -> None:
|
||||
if count_hotp_entries: print(f"Exported {count_hotp_entries} hotp entrie{'s'[:count_hotp_entries != 1]} to keepass csv file {otp_filename_hotp}")
|
||||
|
||||
|
||||
def write_keepass_totp_csv(otp_filename: str, otps: Otps) -> int:
|
||||
def write_keepass_totp_csv(file: str, otps: Otps) -> int:
|
||||
count_entries = 0
|
||||
with open_file_or_stdout_for_csv(otp_filename) as outfile:
|
||||
with open_file_or_stdout_for_csv(file) as outfile:
|
||||
writer = csv.DictWriter(outfile, ["Title", "User Name", "TimeOtp-Secret-Base32", "Group"])
|
||||
writer.writeheader()
|
||||
for otp in otps:
|
||||
@@ -670,9 +725,9 @@ def write_keepass_totp_csv(otp_filename: str, otps: Otps) -> int:
|
||||
return count_entries
|
||||
|
||||
|
||||
def write_keepass_htop_csv(otp_filename: str, otps: Otps) -> int:
|
||||
def write_keepass_htop_csv(file: str, otps: Otps) -> int:
|
||||
count_entries = 0
|
||||
with open_file_or_stdout_for_csv(otp_filename) as outfile:
|
||||
with open_file_or_stdout_for_csv(file) as outfile:
|
||||
writer = csv.DictWriter(outfile, ["Title", "User Name", "HmacOtp-Secret-Base32", "HmacOtp-Counter", "Group"])
|
||||
writer.writeheader()
|
||||
for otp in otps:
|
||||
@@ -688,11 +743,11 @@ def write_keepass_htop_csv(otp_filename: str, otps: Otps) -> int:
|
||||
return count_entries
|
||||
|
||||
|
||||
def write_json(args: Args, otps: Otps) -> None:
|
||||
if args.json:
|
||||
with open_file_or_stdout(args.json) as outfile:
|
||||
def write_json(file: str, otps: Otps) -> None:
|
||||
if file and len(file) > 0:
|
||||
with open_file_or_stdout(file) as outfile:
|
||||
json.dump(otps, outfile, indent=4)
|
||||
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to json {args.json}")
|
||||
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to json {file}")
|
||||
|
||||
|
||||
def has_otp_type(otps: Otps, otp_type: str) -> bool:
|
||||
@@ -729,6 +784,13 @@ def check_file_exists(filename: str) -> None:
|
||||
f"\ninput file: {filename}")
|
||||
|
||||
|
||||
def has_no_otps_show_warning(otps: Otps) -> bool:
|
||||
if len(otps) == 0:
|
||||
tkinter.messagebox.showinfo(title="No data", message="There are no otp secrets to write")
|
||||
tk_root.update() # dispose dialog
|
||||
return len(otps) == 0
|
||||
|
||||
|
||||
def is_binary(line: str) -> bool:
|
||||
try:
|
||||
line.startswith('#')
|
||||
@@ -751,6 +813,12 @@ def do_debug_checks() -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def is_not_headless() -> bool:
|
||||
if headless:
|
||||
log_warn(f"Cannot open dialog in headless mode")
|
||||
return not headless
|
||||
|
||||
|
||||
class PrintVersionAction(argparse.Action):
|
||||
def __init__(self, option_strings: Sequence[str], dest: str, nargs: int = 0, **kwargs: Any) -> None:
|
||||
super().__init__(option_strings, dest, nargs, **kwargs)
|
||||
|
||||
Reference in New Issue
Block a user