Compare commits

...

32 Commits

Author SHA1 Message Date
scito
ca23497c9a docu clean ups 2023-02-06 20:42:54 +01:00
scito
ebfe0dc58d ci_release: add release notes table 2023-02-06 11:54:09 +01:00
scito
78118c73e8 make zbar lib optional
- refactor: global variable renaming
2023-02-05 21:24:57 +01:00
scito
4fc5559e15 add version number to asset executables 2023-02-05 21:24:57 +01:00
scito
7faf530863 reproducible builds, build.sh: linux/arm64 2023-02-05 21:24:57 +01:00
scito
709aa111be ci_release: build linux arm64 exe 2023-02-03 22:14:53 +01:00
scito
fe60acb24f release: upload hashes as binary to keep eol 2023-01-30 20:44:33 +01:00
scito
5c36f07f41 ci_release: build exe without upload on PR 2023-01-30 14:39:50 +01:00
scito
861d7d0da8 build buster image in ci_docker for speeding up releases 2023-01-30 11:28:07 +01:00
scito
63fc21cc90 ci_release: upload hashes and run on pull_request and run scheduled 2023-01-30 11:28:07 +01:00
scito
d234cf4112 cv2: fix save as csv/json/keepass 2023-01-30 11:28:07 +01:00
scito
970dbd3759 cv2: save as csv/json/keppass by key command 2023-01-29 16:50:55 +01:00
scito
197347a3e9 update README 2023-01-27 20:58:33 +01:00
scito
365d5ac432 use hub.docker for linux exe build
since it is more stable and thus faster
2023-01-26 01:14:45 +01:00
scito
88ff584e47 ci: workaround for failing builds 2023-01-26 00:18:41 +01:00
scito
dbb5d8f755 workaround ghcr.io failed to copy
failed to copy: io: read/write on closed pipe

- use DockerHub instead of dhcr.io
- ref: https://github.com/containerd/containerd/issues/7972
2023-01-25 23:46:22 +01:00
scito
580f94256f Add files of Visual C++ 2013 Redistributable Package
- add files of vcredist_x64.exe as binary data to pyzbar
- thus avoid manual installation of vcredist_x64.exe
- test all qr readers in exe smoketests
- improve README
2023-01-25 23:14:38 +01:00
scito
f4bff86a5c fix curl upload windows
- set for pre-releases 99 in windows version
- fix TZ: use TZ=Europe/Zurich
2023-01-25 01:54:20 +01:00
scito
cff5fe1cda ci_release: ignore response and remove silent mode 2023-01-24 23:18:28 +01:00
scito
576b1e68c5 ci: ignore markdown files 2023-01-24 23:00:32 +01:00
scito
84e1922979 fix VERSION_PATCH for pre-tags, e.g. 1b1 2023-01-24 22:48:22 +01:00
scito
7f89168b92 ci_release: change order of build matrix 2023-01-24 22:15:13 +01:00
scito
b9f17c4a95 rename build docker jobs 2023-01-24 22:12:02 +01:00
scito
6a7a7233a4 build and upload executables created by PyInstaller
- create release on tag push
- build executables by PyInstaller:
    - extract_otp_secrets_linux_x86_64 (glibc 2.28)
    - extract_otp_secrets_win_x86_64.exe
    - extract_otp_secrets_macos_x86_64 (untested)
- add --version
- build linux executable in docker container
- update README
    - add TOC
    - improve badges
    - add PyInstaller section
- docker
    - build BASE_IMAGE as ARG
    - copy only required files to image
    - add .alias
- build.sh
    - fix clean
    - fix generate results
    - generate TOC
2023-01-24 21:19:38 +01:00
scito
445d77783c update Pipfile.lock 2023-01-17 21:23:46 +01:00
scito
f0134fa907 add zbar installation for archlinux [skip ci] 2023-01-17 21:04:42 +01:00
scito
e0588285c9 require qreader < 2.0.0 due to breaking changes
- add more classifiers
2023-01-15 10:44:55 +01:00
scito
ff9401687e ci: do not run ci tests on pull_request 2023-01-14 09:42:44 +01:00
scito
2478edb7a1 decode only QR in zbar (which avoids assertion pdf417)
Warning on Windows:
WARNING: .\zbar\decoder\pdf417.c:89: <unknown>: Assertion "g[0] >= 0 && g[1] >= 0 && g[2] >= 0" failed.

        dir=0 sig=1b455 k=3 g0=04a g1=ffffffff g2=b78 buf[0000]=
2023-01-13 21:28:38 +01:00
scito
5e439b9396 update Pipfile.lock packages 2023-01-11 20:48:20 +01:00
scito
5c4d3ce696 improve README 2023-01-07 09:39:15 +01:00
scito
ec09b5daad improve README; add google-authenticator-exporter link 2023-01-06 10:13:51 +01:00
23 changed files with 1469 additions and 288 deletions

View File

@@ -7,7 +7,10 @@ name: tests
on:
push:
pull_request:
paths-ignore:
- 'docs/**'
- '**.md'
# pull_request:
schedule:
# Run daily on default branch
- cron: '37 3 * * *'
@@ -29,6 +32,7 @@ jobs:
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
check-latest: false
- name: Install zbar shared lib for QReader (Linux)
if: runner.os == 'Linux'
run: |

View File

@@ -11,14 +11,98 @@ name: docker
on:
# run it on push to the default repository branch
push:
paths-ignore:
- 'docs/**'
- '**.md'
tags-ignore:
- '**'
# branches is needed if tags-ignore is used
branches:
- '**'
schedule:
# Run weekly on default branch
- cron: '47 3 * * 6'
jobs:
# define job to build and publish docker image
build-and-push-docker-image:
name: Build Docker image and push to repositories
build-and-push-docker-debian-image:
name: Build Docker Bullseye image 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 and push to Docker Hub and GitHub Container Registry"
id: docker_build_qr_reader_latest
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 }}
# Note: tags has to be all lower-case
pull: true
tags: |
scit0/extract_otp_secrets:latest
scit0/extract_otp_secrets:bullseye
ghcr.io/scito/extract_otp_secrets:latest
ghcr.io/scito/extract_otp_secrets:bullseye
# build on feature branches, push only on master branch
push: ${{ github.ref == 'refs/heads/master' }}
- name: Image digest
# TODO upload digests to assets
run: |
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
# run only when code is compiling and tests are passing
runs-on: ubuntu-latest
@@ -57,9 +141,9 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GHCR_IO_TOKEN }}
- name: "no_qr_reader: Build image and push to Docker Hub and GitHub Container Registry"
- name: "only_txt: Build image and push to Docker Hub and GitHub Container Registry"
id: docker_build_only_txt
uses: docker/build-push-action@v2
uses: docker/build-push-action@v3
with:
# relative path to the place where source code with Dockerfile is located
platforms: linux/amd64,linux/arm64
@@ -67,30 +151,102 @@ jobs:
file: Dockerfile_only_txt
# builder: ${{ steps.buildx.outputs.name }}
# Note: tags has to be all lower-case
pull: true
tags: |
scit0/extract_otp_secrets:latest-only-txt
ghcr.io/scito/extract_otp_secrets:latest-only-txt
scit0/extract_otp_secrets:only-txt
scit0/extract_otp_secrets:alpine
ghcr.io/scito/extract_otp_secrets:only-txt
ghcr.io/scito/extract_otp_secrets:alpine
# build on feature branches, push only on master branch
push: ${{ github.ref == 'refs/heads/master' }}
build-args: |
RUN_TESTS=true
- name: "qr_reader: Build image and push to Docker Hub and GitHub Container Registry"
id: docker_build_qr_reader
uses: docker/build-push-action@v2
- 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:latest
ghcr.io/scito/extract_otp_secrets:latest
# build on feature branches, push only on master branch
push: ${{ github.ref == 'refs/heads/master' }}
scit0/extract_otp_secrets:buster
push: true
- name: Image digest
# TODO upload digests to assets
run: |
echo "extract_otp_secrets: ${{ steps.docker_build_qr_reader.outputs.digest }}"
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

374
.github/workflows/ci_release.yml vendored Normal file
View File

@@ -0,0 +1,374 @@
name: release
# https://data-dive.com/multi-os-deployment-in-cloud-using-pyinstaller-and-github-actions
# https://github.com/actions/upload-artifact
# https://github.com/actions/download-artifact
# https://github.com/docker/metadata-action
# https://github.com/marketplace/actions/generate-release-hashes
# https://github.com/oleksis/pyinstaller-manylinux
# https://github.com/pypa/manylinux
# https://github.com/batonogov/docker-pyinstaller
# 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
# https://docs.github.com/en/rest/releases/releases
# https://peps.python.org/pep-0440/
# https://semver.org/
# Build matrix:
# - Linux x86_64 glibc 2.35: ubuntu-latest
# - Linux x86_64 glibc 2.34: extract_otp_secrets:buster
# - Windows x86_64: windows-latest
# - MacOS x86_64: macos-11
# - Linux x86_64 glibc 2.28: extract_otp_secrets:buster
# - Linux aarch64 glibc 2.28: extract_otp_secrets:buster
# - MacOS universal2: macos-11
# - Windows arm64: [buildx + https://github.com/batonogov/docker-pyinstaller]
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:
create-release:
name: Create Release
runs-on: ubuntu-latest
outputs:
date: ${{ steps.meta.outputs.date }}
version: ${{ steps.meta.outputs.version }}
inline_version: ${{ steps.meta.outputs.inline_version }}
tag_name: ${{ steps.meta.outputs.tag_name }}
tag_message: ${{ steps.meta.outputs.tag_message }}
steps:
- name: Set meta data
id: meta
# Writing to env with >> $GITHUB_ENV is an alternative
run: |
echo "date=$(TZ=Europe/Zurich date +'%d.%m.%Y')" >> $GITHUB_OUTPUT
echo "version=${TAG_NAME/v/}" >> $GITHUB_OUTPUT
echo "inline_version=${TAG_NAME/v/_}" >> $GITHUB_OUTPUT
echo "tag_name=${{ github.ref_name }}" >> $GITHUB_OUTPUT
echo "tag_message=$(git tag -l --format='%(contents:subject)' ${{ github.ref_name }})" >> $GITHUB_OUTPUT
env:
TAG_NAME: ${{ github.ref_name }}
PYTHONHASHSEED: 31
- 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 \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/scito/extract_otp_secrets/releases \
--silent \
--show-error \
-d '{"tag_name":"${{ github.ref }}","target_commitish":"master","name":"${{ steps.meta.outputs.version }} - ${{ steps.meta.outputs.date }}","body":"${{ steps.meta.outputs.tag_message }}\n\nJust download the executable and execute it.\n\n | Executable | Description |\n | --- | --- |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_linux_x86_64 | Linux x86_64/amd64 (glibc >= 2.28) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_linux_arm64 | Linux arm64 (glibc >= 2.28) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_win_x86_64.exe | Windows x86_64/amd64/x64 |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_win_arm64.exe | N/A |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_macos_x86_64 | MacOS x86_64/amd64 (barely tested due to no access to Apple environment; libzbar must be installed separately, see README.md) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_macos_arm64 | N/A |\n ","draft":true,"prerelease":false,"generate_release_notes":true}')
echo upload_url=$(jq '.upload_url' <<< "$response") >> $GITHUB_OUTPUT
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-linux-executable-in-docker:
name: Build ${{ matrix.PLATFORM }} release in docker container
# run only when code is compiling and tests are passing
runs-on: ubuntu-latest
needs: create-release
strategy:
matrix:
include:
- PLATFORM: linux/amd64
EXE: extract_otp_secrets_linux_x86_64
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_linux_x86_64
- PLATFORM: linux/arm64
EXE: extract_otp_secrets_linux_arm64
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_linux_arm64
# 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: Image digest
# TODO upload digests to assets
run: |
echo "extract_otp_secrets: ${{ steps.docker_build_buster.outputs.digest }}"
# TODO use local docker image https://stackoverflow.com/a/61155718/1663871
# https://github.com/multiarch/qemu-user-static
# https://hub.docker.com/r/multiarch/qemu-user-static/
- name: Run Pyinstaller in container for ${{ matrix.EXE }}
run: |
docker run --pull always --rm --privileged multiarch/qemu-user-static --reset -p yes
docker run --platform ${{ matrix.PLATFORM }} --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 && PYTHONHASHSEED=31 && pyinstaller -y --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name ${{ matrix.EXE }} --distpath /files/dist/ /files/src/extract_otp_secrets.py'
- name: Smoke tests ${{ matrix.PLATFORM }}
if: matrix.PLATFORM == 'linux/amd64'
run: |
dist/${{ matrix.EXE }} -V
dist/${{ matrix.EXE }} -h
dist/${{ matrix.EXE }} example_export.png
dist/${{ matrix.EXE }} - < example_export.txt
dist/${{ matrix.EXE }} --qr ZBAR example_export.png
dist/${{ matrix.EXE }} --qr QREADER example_export.png
dist/${{ matrix.EXE }} --qr QREADER_DEEP example_export.png
dist/${{ matrix.EXE }} --qr CV2 example_export.png
dist/${{ matrix.EXE }} --qr CV2_WECHAT example_export.png
- name: Smoke tests ${{ matrix.PLATFORM }}
if: matrix.PLATFORM == 'linux/arm64'
run: |
docker run --platform ${{ matrix.PLATFORM }} --pull always --entrypoint /bin/bash --rm -v "$(pwd)":/files -w /files scit0/extract_otp_secrets -c 'dist/${{ matrix.EXE }} -V && dist/${{ matrix.EXE }} -h && dist/${{ matrix.EXE }} example_export.png && dist/${{ matrix.EXE }} - < example_export.txt && dist/${{ matrix.EXE }} --qr ZBAR example_export.png && dist/${{ matrix.EXE }} --qr QREADER example_export.png && dist/${{ matrix.EXE }} --qr QREADER_DEEP example_export.png && dist/${{ matrix.EXE }} --qr CV2 example_export.png && dist/${{ matrix.EXE }} --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
- name: Display structure of files
run: ls -R
- name: Upload Release Asset
id: upload-release-asset
if: startsWith(github.ref, 'refs/tags/v')
run: |
response=$(curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Content-Type: application/x-executable" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\
-H "X-GitHub-Api-Version: 2022-11-28" \
--silent \
--show-error \
--data-binary @dist/${{ matrix.EXE }} \
$(cat release_url.txt)=${{ matrix.ASSET_NAME }})
build-native-executables:
name: Build native packages
needs: create-release
runs-on: ${{ matrix.os }}
strategy:
matrix:
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#choosing-github-hosted-runners
include:
- os: windows-latest
TARGET: windows
# TODO add --icon
# TODO add --manifest
# TODO find more elegant solution for pyzbar\libiconv.dll and pyzbar\libzbar-64.dll
# Files of Visual C++ 2013 Redistributable Package: https://support.microsoft.com/en-us/topic/update-for-visual-c-2013-redistributable-package-d8ccd6a5-4e26-c290-517b-8da6cfdf4f10
OUT_FILE_NAME: extract_otp_secrets.exe
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_win_x86_64.exe
ASSET_MIME: application/vnd.microsoft.portable-executable
UPLOAD: true
CMD_BUILD: |
pyinstaller -y --add-data "$($Env:pythonLocation)\__yolo_v3_qr_detector;__yolo_v3_qr_detector" --add-binary "$($Env:pythonLocation)\Lib\site-packages\pyzbar\libiconv.dll;pyzbar" --add-binary "$($Env:pythonLocation)\Lib\site-packages\pyzbar\libzbar-64.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\msvcr120.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\msvcp120.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\vcamp120.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\vcomp120.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\vccorlib120.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120u.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120chs.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120cht.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120deu.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120enu.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120esn.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120fra.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120ita.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120jpn.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120kor.dll;pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120rus.dll;pyzbar" --onefile --version-file build\file_version_info.txt src\extract_otp_secrets.py
- os: macos-11
TARGET: macos
# TODO add --icon
# TODO add --osx-bundle-identifier
# TODO add --codesign-identity
# TODO add --osx-entitlements-file
# TODO https://pyinstaller.org/en/stable/spec-files.html#spec-file-options-for-a-macos-bundle
# TODO --target-arch universal2
OUT_FILE_NAME: extract_otp_secrets
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_macos_x86_64
ASSET_MIME: application/x-newton-compatible-pkg
UPLOAD: true
CMD_BUILD: |
pyinstaller -y --add-data $macos_python_path/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --argv-emulation src/extract_otp_secrets.py
- os: ubuntu-latest
TARGET: linux
OUT_FILE_NAME: extract_otp_secrets
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_linux_x86_64_ubuntu_latest
ASSET_MIME: application/x-executable
UPLOAD: false
CMD_BUILD: |
pyinstaller -y --add-data $pythonLocation/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile src/extract_otp_secrets.py
steps:
- name: Output path
if: runner.os == 'Windows'
run: echo "$($Env:Path)"
- name: List Windir
if: runner.os == 'Windows'
run: ls "$($Env:WinDir)\system32"
- uses: actions/checkout@v3
- name: Set macos macos_python_path
# TODO use variable for Python version
run: echo "macos_python_path=/Library/Frameworks/Python.framework/Versions/3.11" >> $GITHUB_ENV
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.11
check-latest: true
- name: Install zbar shared lib for QReader (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get install -y libzbar0
- name: Install zbar shared lib for QReader (macOS)
if: runner.os == 'macOS'
run: |
brew install zbar
- name: Install dependencies
# TODO fix --use-pep517
run: |
python -m pip install --upgrade pip
pip install -U -r requirements-dev.txt
pip install -U .
- name: Create Windows file_version_info.txt
shell: bash
run: |
mkdir -p build/
VERSION_STR=$(setuptools-git-versioning) VERSION_MAJOR=$(cut -d '.' -f 1 <<< "$(setuptools-git-versioning)") VERSION_MINOR=$(cut -d '.' -f 2 <<< "$(setuptools-git-versioning)") VERSION_PATCH=$(echo $(cut -d '.' -f 3 <<< "$(setuptools-git-versioning)") | sed -E -n "s/^([0-9]+).*/\1/p") VERSION_BUILD=$(echo $(cut -d '.' -f 3 <<< "$(setuptools-git-versioning)") | sed -E -n -e"s/^[0-9]+.+/99/p")$(($(git rev-list --count $(git tag | sort -V -r | sed '1!d')..HEAD))) YEARS='2020-2023' envsubst < file_version_info_template.txt > build/file_version_info.txt
- name: Build with pyinstaller for ${{ matrix.TARGET }}
env:
# Reproducible build: https://pyinstaller.org/en/stable/advanced-topics.html#creating-a-reproducible-build
PYTHONHASHSEED: 31
run: ${{ matrix.CMD_BUILD }}
- name: Smoke tests for generated exe (general)
run: |
dist/${{ matrix.OUT_FILE_NAME }} -V
dist/${{ matrix.OUT_FILE_NAME }} -h
dist/${{ matrix.OUT_FILE_NAME }} example_export.png
dist/${{ matrix.OUT_FILE_NAME }} --qr ZBAR example_export.png
dist/${{ matrix.OUT_FILE_NAME }} --qr QREADER example_export.png
dist/${{ matrix.OUT_FILE_NAME }} --qr QREADER_DEEP example_export.png
dist/${{ matrix.OUT_FILE_NAME }} --qr CV2 example_export.png
dist/${{ matrix.OUT_FILE_NAME }} --qr CV2_WECHAT example_export.png
- name: Smoke tests for generated exe (stdin)
if: runner.os != 'Windows'
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
- name: Display structure of files
run: ls -R
- name: Set meta data
id: meta
if: startsWith(github.ref, 'refs/tags/v')
shell: bash
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: Upload Release Asset
id: upload-release-asset
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

7
.gitignore vendored
View File

@@ -19,3 +19,10 @@ dist/
*.xml
pytest-coverage.txt
tests/reports/
dist_*/
*.spec
file_version_info_python.txt
file_version_info_explorer.txt
file_version_info.txt
assets/*

View File

@@ -1,4 +1,6 @@
FROM python:3.11-slim-bullseye
# --build-arg BASE_IMAGE=python:3.11-slim-buster
ARG BASE_IMAGE=python:3.11-slim-bullseye
FROM $BASE_IMAGE
# https://docs.docker.com/engine/reference/builder/
@@ -10,7 +12,7 @@ FROM python:3.11-slim-bullseye
WORKDIR /extract
COPY . .
COPY requirements*.txt src/ run_pytest.sh pytest.ini tests/ example_*.txt example_*.png example_*.csv example*.json docker/.alias ./
ARG RUN_TESTS=true
@@ -19,14 +21,16 @@ 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
&& pip install --no-cache-dir -U -r requirements.txt \
&& if [ "$RUN_TESTS" = "true" ]; then /extract/run_pytest.sh; else echo "Not running tests..."; fi \
&& echo 'test -s /extract/.alias && . /extract/.alias || true' >> ~/.bashrc
WORKDIR /files
ENTRYPOINT ["python", "/extract/src/extract_otp_secrets.py"]
ENTRYPOINT ["python", "/extract/extract_otp_secrets.py"]
LABEL org.opencontainers.image.source https://github.com/scito/extract_otp_secrets
LABEL org.opencontainers.image.license GPL-3.0+
LABEL maintainer="Scito https://scito.ch, https://github.com/scito"

View File

@@ -1,16 +1,19 @@
FROM python:3.11-alpine
ARG BASE_IMAGE=python:3.11-alpine
FROM $BASE_IMAGE
# https://docs.docker.com/engine/reference/builder/
# For debugging
# docker run --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt
# docker build . -t extract_otp_secrets_only_txt -f Dockerfile_only_txt --pull --build-arg RUN_TESTS=false
# docker run --entrypoint /bin/sh -it --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt
# 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 -vvv -s
# docker run --entrypoint /bin/ash -it --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt -l
# 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 -vvv -s
# https://github.com/pypa/manylinux/blob/main/docker/Dockerfile
WORKDIR /extract
COPY . .
COPY requirements*.txt src/ run_pytest.sh pytest.ini tests/ example_*.txt example_*.png example_*.csv example*.json docker/.alias ./
ARG RUN_TESTS=true
@@ -32,11 +35,13 @@ RUN apk add --no-cache \
protobuf \
qrcode \
&& if [[ "$(apk --print-arch)" == "aarch64" ]]; then apk del .build-deps; fi \
&& if [[ "$RUN_TESTS" == "true" ]]; then /extract/run_pytest.sh tests/extract_otp_secrets_test.py -k "not qreader" --relaxed; else echo "Not running tests..."; fi
&& if [[ "$RUN_TESTS" == "true" ]]; then /extract/run_pytest.sh extract_otp_secrets_test.py -k "not qreader" --relaxed; else echo "Not running tests..."; fi \
&& echo 'test -s /extract/.alias && . /extract/.alias || true' >> ~/.profile
WORKDIR /files
ENTRYPOINT ["python", "/extract/src/extract_otp_secrets.py"]
ENTRYPOINT ["python", "/extract/extract_otp_secrets.py"]
LABEL org.opencontainers.image.source https://github.com/scito/extract_otp_secrets
LABEL org.opencontainers.image.license GPL-3.0+
LABEL maintainer="Scito https://scito.ch, https://github.com/scito"

View File

@@ -9,19 +9,22 @@ opencv-contrib-python = "*"
# for macOS: opencv-contrib-python = "<=4.7.0"
# for PYTHON <= 3.7: typing_extensions = "*"
pillow = "*"
pyzbar = "*"
protobuf = "*"
qrcode = "*"
qreader = "*"
qreader = "<2.0.0"
[dev-packages]
build = "*"
flake8 = "*"
gfm-toc = "*"
mypy = "*"
mypy-protobuf = "*"
pylint = "*"
pytest = "*"
pytest-cov = "*"
pytest-mock = "*"
setuptools-git-versioning = "*"
types-protobuf = "*"
wheel = "*"

356
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "13e2cc849fbc56593a2179a51a091717bcd4baeb9235b0843bb3abd8a9d1c698"
"sha256": "42b14c5eae25b0924354520fe0a26a8d826c905f4613d717f3bfa52e98ed5e8e"
},
"pipfile-spec": 6,
"requires": {
@@ -26,37 +26,37 @@
},
"numpy": {
"hashes": [
"sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9",
"sha256:0e3463e6ac25313462e04aea3fb8a0a30fb906d5d300f58b3bc2c23da6a15398",
"sha256:179a7ef0889ab769cc03573b6217f54c8bd8e16cef80aad369e1e8185f994cd7",
"sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2",
"sha256:26089487086f2648944f17adaa1a97ca6aee57f513ba5f1c0b7ebdabbe2b9954",
"sha256:28bc9750ae1f75264ee0f10561709b1462d450a4808cd97c013046073ae64ab6",
"sha256:28e418681372520c992805bb723e29d69d6b7aa411065f48216d8329d02ba032",
"sha256:442feb5e5bada8408e8fcd43f3360b78683ff12a4444670a7d9e9824c1817d36",
"sha256:6ec0c021cd9fe732e5bab6401adea5a409214ca5592cd92a114f7067febcba0c",
"sha256:7094891dcf79ccc6bc2a1f30428fa5edb1e6fb955411ffff3401fb4ea93780a8",
"sha256:84e789a085aabef2f36c0515f45e459f02f570c4b4c4c108ac1179c34d475ed7",
"sha256:87a118968fba001b248aac90e502c0b13606721b1343cdaddbc6e552e8dfb56f",
"sha256:8e669fbdcdd1e945691079c2cae335f3e3a56554e06bbd45d7609a6cf568c700",
"sha256:ad2925567f43643f51255220424c23d204024ed428afc5aad0f86f3ffc080086",
"sha256:b0677a52f5d896e84414761531947c7a330d1adc07c3a4372262f25d84af7bf7",
"sha256:b07b40f5fb4fa034120a5796288f24c1fe0e0580bbfff99897ba6267af42def2",
"sha256:b09804ff570b907da323b3d762e74432fb07955701b17b08ff1b5ebaa8cfe6a9",
"sha256:b162ac10ca38850510caf8ea33f89edcb7b0bb0dfa5592d59909419986b72407",
"sha256:b31da69ed0c18be8b77bfce48d234e55d040793cebb25398e2a7d84199fbc7e2",
"sha256:caf65a396c0d1f9809596be2e444e3bd4190d86d5c1ce21f5fc4be60a3bc5b36",
"sha256:cfa1161c6ac8f92dea03d625c2d0c05e084668f4a06568b77a25a89111621566",
"sha256:dae46bed2cb79a58d6496ff6d8da1e3b95ba09afeca2e277628171ca99b99db1",
"sha256:ddc7ab52b322eb1e40521eb422c4e0a20716c271a306860979d450decbb51b8e",
"sha256:de92efa737875329b052982e37bd4371d52cabf469f83e7b8be9bb7752d67e51",
"sha256:e274f0f6c7efd0d577744f52032fdd24344f11c5ae668fe8d01aac0422611df1",
"sha256:ed5fb71d79e771ec930566fae9c02626b939e37271ec285e9efaf1b5d4370e7d",
"sha256:ef85cf1f693c88c1fd229ccd1055570cb41cdf4875873b7728b6301f12cd05bf",
"sha256:f1b739841821968798947d3afcefd386fa56da0caf97722a5de53e07c4ccedc7"
"sha256:003a9f530e880cb2cd177cba1af7220b9aa42def9c4afc2a2fc3ee6be7eb2b22",
"sha256:150947adbdfeceec4e5926d956a06865c1c690f2fd902efede4ca6fe2e657c3f",
"sha256:2620e8592136e073bd12ee4536149380695fbe9ebeae845b81237f986479ffc9",
"sha256:2eabd64ddb96a1239791da78fa5f4e1693ae2dadc82a76bc76a14cbb2b966e96",
"sha256:4173bde9fa2a005c2c6e2ea8ac1618e2ed2c1c6ec8a7657237854d42094123a0",
"sha256:4199e7cfc307a778f72d293372736223e39ec9ac096ff0a2e64853b866a8e18a",
"sha256:4cecaed30dc14123020f77b03601559fff3e6cd0c048f8b5289f4eeabb0eb281",
"sha256:557d42778a6869c2162deb40ad82612645e21d79e11c1dc62c6e82a2220ffb04",
"sha256:63e45511ee4d9d976637d11e6c9864eae50e12dc9598f531c035265991910468",
"sha256:6524630f71631be2dabe0c541e7675db82651eb998496bbe16bc4f77f0772253",
"sha256:76807b4063f0002c8532cfeac47a3068a69561e9c8715efdad3c642eb27c0756",
"sha256:7de8fdde0003f4294655aa5d5f0a89c26b9f22c0a58790c38fae1ed392d44a5a",
"sha256:889b2cc88b837d86eda1b17008ebeb679d82875022200c6e8e4ce6cf549b7acb",
"sha256:92011118955724465fb6853def593cf397b4a1367495e0b59a7e69d40c4eb71d",
"sha256:97cf27e51fa078078c649a51d7ade3c92d9e709ba2bfb97493007103c741f1d0",
"sha256:9a23f8440561a633204a67fb44617ce2a299beecf3295f0d13c495518908e910",
"sha256:a51725a815a6188c662fb66fb32077709a9ca38053f0274640293a14fdd22978",
"sha256:a77d3e1163a7770164404607b7ba3967fb49b24782a6ef85d9b5f54126cc39e5",
"sha256:adbdce121896fd3a17a77ab0b0b5eedf05a9834a18699db6829a64e1dfccca7f",
"sha256:c29e6bd0ec49a44d7690ecb623a8eac5ab8a923bce0bea6293953992edf3a76a",
"sha256:c72a6b2f4af1adfe193f7beb91ddf708ff867a3f977ef2ec53c0ffb8283ab9f5",
"sha256:d0a2db9d20117bf523dde15858398e7c0858aadca7c0f088ac0d6edd360e9ad2",
"sha256:e3ab5d32784e843fc0dd3ab6dcafc67ef806e6b6828dc6af2f689be0eb4d781d",
"sha256:e428c4fbfa085f947b536706a2fc349245d7baa8334f0c5723c56a10595f9b95",
"sha256:e8d2859428712785e8a8b7d2b3ef0a1d1565892367b32f915c4a4df44d0e64f5",
"sha256:eef70b4fc1e872ebddc38cddacc87c19a3709c0e3e5d20bf3954c147b1dd941d",
"sha256:f64bb98ac59b3ea3bf74b02f13836eb2e24e48e0ab0145bbda646295769bd780",
"sha256:f9006288bcf4895917d02583cf3411f98631275bc67cce355a7f39f8c14338fa"
],
"markers": "python_version >= '3.10'",
"version": "==1.24.1"
"version": "==1.24.2"
},
"opencv-contrib-python": {
"hashes": [
@@ -93,6 +93,7 @@
},
"pillow": {
"hashes": [
"sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33",
"sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b",
"sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e",
"sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35",
@@ -101,9 +102,12 @@
"sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569",
"sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57",
"sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8",
"sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1",
"sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264",
"sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157",
"sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9",
"sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133",
"sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9",
"sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab",
"sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6",
"sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5",
@@ -123,10 +127,16 @@
"sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a",
"sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e",
"sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f",
"sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848",
"sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57",
"sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f",
"sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c",
"sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9",
"sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5",
"sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9",
"sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d",
"sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0",
"sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1",
"sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e",
"sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815",
"sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0",
@@ -134,6 +144,8 @@
"sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd",
"sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c",
"sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3",
"sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab",
"sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858",
"sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5",
"sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee",
"sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343",
@@ -154,8 +166,10 @@
"sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070",
"sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe",
"sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a",
"sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd",
"sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391",
"sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a"
"sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a",
"sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"
],
"index": "pypi",
"version": "==9.4.0"
@@ -180,20 +194,29 @@
"index": "pypi",
"version": "==4.21.12"
},
"pypng": {
"hashes": [
"sha256:4a43e969b8f5aaafb2a415536c1a8ec7e341cd6a3f957fd5b5f32a4cfeed902c",
"sha256:739c433ba96f078315de54c0db975aee537cbc3e1d0ae4ed9aab0ca1e427e2c1"
],
"version": "==0.20220715.0"
},
"pyzbar": {
"hashes": [
"sha256:13e3ee5a2f3a545204a285f41814d5c0db571967e8d4af8699a03afc55182a9c",
"sha256:4559628b8192feb25766d954b36a3753baaf5c97c03135aec7e4a026036b475d",
"sha256:8f4c5264c9c7c6b9f20d01efc52a4eba1ded47d9ba857a94130afe33703eb518"
],
"index": "pypi",
"version": "==0.1.9"
},
"qrcode": {
"hashes": [
"sha256:375a6ff240ca9bd41adc070428b5dfc1dcfbb0f2507f1ac848f6cded38956578"
"sha256:581dca7a029bcb2deef5d01068e39093e80ef00b4a61098a2182eac59d01643a",
"sha256:9dd969454827e127dbd93696b20747239e6d540e082937c90f14ac95b30f5845"
],
"index": "pypi",
"version": "==7.3.1"
"version": "==7.4.2"
},
"qreader": {
"hashes": [
@@ -201,16 +224,24 @@
],
"index": "pypi",
"version": "==1.3.2"
},
"typing-extensions": {
"hashes": [
"sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa",
"sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"
],
"markers": "python_version >= '3.7'",
"version": "==4.4.0"
}
},
"develop": {
"astroid": {
"hashes": [
"sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907",
"sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"
"sha256:23c718921acab5f08cbbbe9293967f1f8fec40c336d19cd75dc12a9ea31d2eb2",
"sha256:bd1aa4f9915c98e8aaebcd4e71930154d4e8c9aaf05d35ac0a63d1956091ae3f"
],
"markers": "python_full_version >= '3.7.2'",
"version": "==2.12.13"
"version": "==2.14.1"
},
"attrs": {
"hashes": [
@@ -222,71 +253,71 @@
},
"build": {
"hashes": [
"sha256:1a07724e891cbd898923145eb7752ee7653674c511378eb9c7691aab1612bc3c",
"sha256:38a7a2b7a0bdc61a42a0a67509d88c71ecfc37b393baba770fae34e20929ff69"
"sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171",
"sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269"
],
"index": "pypi",
"version": "==0.9.0"
"version": "==0.10.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:04481245ef966fbd24ae9b9e537ce899ae584d521dfbe78f89cad003c38ca2ab",
"sha256:0c45948f613d5d18c9ec5eaa203ce06a653334cf1bd47c783a12d0dd4fd9c851",
"sha256:10188fe543560ec4874f974b5305cd1a8bdcfa885ee00ea3a03733464c4ca265",
"sha256:218fe982371ac7387304153ecd51205f14e9d731b34fb0568181abaf7b443ba0",
"sha256:29571503c37f2ef2138a306d23e7270687c0efb9cab4bd8038d609b5c2393a3a",
"sha256:2a60d6513781e87047c3e630b33b4d1e89f39836dac6e069ffee28c4786715f5",
"sha256:2bf1d5f2084c3932b56b962a683074a3692bce7cabd3aa023c987a2a8e7612f6",
"sha256:3164d31078fa9efe406e198aecd2a02d32a62fecbdef74f76dad6a46c7e48311",
"sha256:32df215215f3af2c1617a55dbdfb403b772d463d54d219985ac7cd3bf124cada",
"sha256:33d1ae9d4079e05ac4cc1ef9e20c648f5afabf1a92adfaf2ccf509c50b85717f",
"sha256:33ff26d0f6cc3ca8de13d14fde1ff8efe1456b53e3f0273e63cc8b3c84a063d8",
"sha256:38da2db80cc505a611938d8624801158e409928b136c8916cd2e203970dde4dc",
"sha256:3b155caf3760408d1cb903b21e6a97ad4e2bdad43cbc265e3ce0afb8e0057e73",
"sha256:3b946bbcd5a8231383450b195cfb58cb01cbe7f8949f5758566b881df4b33baf",
"sha256:3baf5f126f30781b5e93dbefcc8271cb2491647f8283f20ac54d12161dff080e",
"sha256:4b14d5e09c656de5038a3f9bfe5228f53439282abcab87317c9f7f1acb280352",
"sha256:51b236e764840a6df0661b67e50697aaa0e7d4124ca95e5058fa3d7cbc240b7c",
"sha256:63ffd21aa133ff48c4dff7adcc46b7ec8b565491bfc371212122dd999812ea1c",
"sha256:6a43c7823cd7427b4ed763aa7fb63901ca8288591323b58c9cd6ec31ad910f3c",
"sha256:755e89e32376c850f826c425ece2c35a4fc266c081490eb0a841e7c1cb0d3bda",
"sha256:7a726d742816cb3a8973c8c9a97539c734b3a309345236cd533c4883dda05b8d",
"sha256:7c7c0d0827e853315c9bbd43c1162c006dd808dbbe297db7ae66cd17b07830f0",
"sha256:7ed681b0f8e8bcbbffa58ba26fcf5dbc8f79e7997595bf071ed5430d8c08d6f3",
"sha256:7ee5c9bb51695f80878faaa5598040dd6c9e172ddcf490382e8aedb8ec3fec8d",
"sha256:8361be1c2c073919500b6601220a6f2f98ea0b6d2fec5014c1d9cfa23dd07038",
"sha256:8ae125d1134bf236acba8b83e74c603d1b30e207266121e76484562bc816344c",
"sha256:9817733f0d3ea91bea80de0f79ef971ae94f81ca52f9b66500c6a2fea8e4b4f8",
"sha256:98b85dd86514d889a2e3dd22ab3c18c9d0019e696478391d86708b805f4ea0fa",
"sha256:9ccb092c9ede70b2517a57382a601619d20981f56f440eae7e4d7eaafd1d1d09",
"sha256:9d58885215094ab4a86a6aef044e42994a2bd76a446dc59b352622655ba6621b",
"sha256:b643cb30821e7570c0aaf54feaf0bfb630b79059f85741843e9dc23f33aaca2c",
"sha256:bc7c85a150501286f8b56bd8ed3aa4093f4b88fb68c0843d21ff9656f0009d6a",
"sha256:beeb129cacea34490ffd4d6153af70509aa3cda20fdda2ea1a2be870dfec8d52",
"sha256:c31b75ae466c053a98bf26843563b3b3517b8f37da4d47b1c582fdc703112bc3",
"sha256:c4e4881fa9e9667afcc742f0c244d9364d197490fbc91d12ac3b5de0bf2df146",
"sha256:c5b15ed7644ae4bee0ecf74fee95808dcc34ba6ace87e8dfbf5cb0dc20eab45a",
"sha256:d12d076582507ea460ea2a89a8c85cb558f83406c8a41dd641d7be9a32e1274f",
"sha256:d248cd4a92065a4d4543b8331660121b31c4148dd00a691bfb7a5cdc7483cfa4",
"sha256:d47dd659a4ee952e90dc56c97d78132573dc5c7b09d61b416a9deef4ebe01a0c",
"sha256:d4a5a5879a939cb84959d86869132b00176197ca561c664fc21478c1eee60d75",
"sha256:da9b41d4539eefd408c46725fb76ecba3a50a3367cafb7dea5f250d0653c1040",
"sha256:db61a79c07331e88b9a9974815c075fbd812bc9dbc4dc44b366b5368a2936063",
"sha256:ddb726cb861c3117a553f940372a495fe1078249ff5f8a5478c0576c7be12050",
"sha256:ded59300d6330be27bc6cf0b74b89ada58069ced87c48eaf9344e5e84b0072f7",
"sha256:e2617759031dae1bf183c16cef8fcfb3de7617f394c813fa5e8e46e9b82d4222",
"sha256:e5cdbb5cafcedea04924568d990e20ce7f1945a1dd54b560f879ee2d57226912",
"sha256:ec8e767f13be637d056f7e07e61d089e555f719b387a7070154ad80a0ff31801",
"sha256:ef382417db92ba23dfb5864a3fc9be27ea4894e86620d342a116b243ade5d35d",
"sha256:f2cba5c6db29ce991029b5e4ac51eb36774458f0a3b8d3137241b32d1bb91f06",
"sha256:f5b4198d85a3755d27e64c52f8c95d6333119e49fd001ae5798dac872c95e0f8",
"sha256:ffeeb38ee4a80a30a6877c5c4c359e5498eec095878f1581453202bfacc8fbc2"
],
"markers": "python_version >= '3.7'",
"version": "==7.0.3"
"version": "==7.1.0"
},
"dill": {
"hashes": [
@@ -304,45 +335,71 @@
"index": "pypi",
"version": "==6.0.0"
},
"gfm-toc": {
"hashes": [
"sha256:247af7267a6cbbdd4213f8383157997bcb07e39e819db737bd2dbfbdb94ee7ae",
"sha256:c53ed0e2cd400e89051377017ca98c11c9cef628b2effddf787db4fc19ff343d"
],
"index": "pypi",
"version": "==0.0.7"
},
"iniconfig": {
"hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
],
"version": "==1.1.1"
"markers": "python_version >= '3.7'",
"version": "==2.0.0"
},
"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": [
"sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada",
"sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d",
"sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7",
"sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe",
"sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd",
"sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c",
"sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858",
"sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288",
"sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec",
"sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f",
"sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891",
"sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c",
"sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25",
"sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156",
"sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8",
"sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f",
"sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e",
"sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0",
"sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"
"sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382",
"sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82",
"sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9",
"sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494",
"sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46",
"sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30",
"sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63",
"sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4",
"sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae",
"sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be",
"sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701",
"sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd",
"sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006",
"sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a",
"sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586",
"sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8",
"sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821",
"sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07",
"sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b",
"sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171",
"sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b",
"sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2",
"sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7",
"sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4",
"sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8",
"sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e",
"sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f",
"sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda",
"sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4",
"sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e",
"sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671",
"sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11",
"sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455",
"sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734",
"sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb",
"sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"
],
"markers": "python_version >= '3.7'",
"version": "==1.8.0"
"version": "==1.9.0"
},
"mccabe": {
"hashes": [
@@ -390,10 +447,11 @@
},
"mypy-extensions": {
"hashes": [
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
"sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
"sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
],
"version": "==0.4.3"
"markers": "python_version >= '3.5'",
"version": "==1.0.0"
},
"mypy-protobuf": {
"hashes": [
@@ -405,19 +463,11 @@
},
"packaging": {
"hashes": [
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
"sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
"sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
],
"markers": "python_version >= '3.7'",
"version": "==22.0"
},
"pep517": {
"hashes": [
"sha256:4ba4446d80aed5b5eac6509ade100bff3e7943a8489de249654a5ae9b33ee35b",
"sha256:ae69927c5c172be1add9203726d4b84cf3ebad1edcd5f71fcdc746e66e829f59"
],
"markers": "python_version >= '3.6'",
"version": "==0.13.0"
"version": "==23.0"
},
"platformdirs": {
"hashes": [
@@ -473,19 +523,27 @@
},
"pylint": {
"hashes": [
"sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4",
"sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb"
"sha256:bad9d7c36037f6043a1e848a43004dfd5ea5ceb05815d713ba56ca4503a9fe37",
"sha256:ffe7fa536bb38ba35006a7c8a6d2efbfdd3d95bbf21199cad31f76b1c50aaf30"
],
"index": "pypi",
"version": "==2.15.9"
"version": "==2.16.1"
},
"pyproject-hooks": {
"hashes": [
"sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8",
"sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"
],
"markers": "python_version >= '3.7'",
"version": "==1.0.0"
},
"pytest": {
"hashes": [
"sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
"sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
"sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5",
"sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"
],
"index": "pypi",
"version": "==7.2.0"
"version": "==7.2.1"
},
"pytest-cov": {
"hashes": [
@@ -503,6 +561,22 @@
"index": "pypi",
"version": "==3.10.0"
},
"setuptools": {
"hashes": [
"sha256:a7687c12b444eaac951ea87a9627c4f904ac757e7abdc5aac32833234af90378",
"sha256:e261cdf010c11a41cb5cb5f1bf3338a7433832029f559a6a7614bd42a967c300"
],
"markers": "python_version >= '3.7'",
"version": "==67.1.0"
},
"setuptools-git-versioning": {
"hashes": [
"sha256:648481f7e1e9e12ccd2b069d616b909a338b4223956319649351751cbc0207f4",
"sha256:fde1a7cb3b2566979e5651cfca0d33cd5a82771711cd38a056216391936cf0ff"
],
"index": "pypi",
"version": "==1.13.1"
},
"tomlkit": {
"hashes": [
"sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b",
@@ -513,11 +587,11 @@
},
"types-protobuf": {
"hashes": [
"sha256:7df483d34ad3fcb1fa7fff1073560d596c9ac1f419cfa851b220c9a93386c998",
"sha256:aeefcf39d637016998b3c7b699750847071b555f7c2e0c9873d42ab6103d1a39"
"sha256:09d39f2c84d0c9c323f44a4c6f1f88fbb1aac0c855f89a3c2746b50c823360cc",
"sha256:7e1f8641b013f1500ee3b56ab4596df26b325d61bc0c69c6eb58ec65f4e41ad8"
],
"index": "pypi",
"version": "==4.21.0.2"
"version": "==4.21.0.4"
},
"typing-extensions": {
"hashes": [

228
README.md
View File

@@ -1,28 +1,111 @@
# Extract TOTP/HOTP 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)
[![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)
![protobuf version](https://img.shields.io/badge/protobuf-4.21.12-informational)
![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
[![License](https://img.shields.io/github/license/scito/extract_otp_secrets)](https://github.com/scito/extract_otp_secrets/blob/master/LICENSE)
[![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/scito/extract_otp_secrets?sort=semver&label=version)](https://github.com/scito/extract_otp_secrets/tags)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/scito/extract_otp_secrets?sort=semver)](https://github.com/scito/extract_otp_secrets/releases/latest)
![python versions](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)
[![Docker image](https://img.shields.io/badge/docker-image-blue)](https://hub.docker.com/repository/docker/scit0/extract_otp_secrets/general)
[![Linux](https://img.shields.io/badge/os-linux-yellow)](https://github.com/scito/extract_otp_secrets/releases/latest)
[![Windows](https://img.shields.io/badge/os-windows-yellow)](https://github.com/scito/extract_otp_secrets/releases/latest)
[![MacOS](https://img.shields.io/badge/os-macos-yellow)](https://github.com/scito/extract_otp_secrets/releases/latest)
[![Download executable](https://img.shields.io/badge/download-exe-blue)](https://github.com/scito/extract_otp_secrets/releases/latest)
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
<!-- ![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)
![protobuf version](https://img.shields.io/badge/protobuf-4.21.12-informational)-->
<!-- [![Github all releases](https://img.shields.io/github/downloads/scito/extract_otp_secrets/total.svg)](https://GitHub.com/scito/extract_otp_secrets/releases/) -->
---
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:
1. Capture from the system camera using a GUI, 🆕
1. Capture the QR codes with 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.
The secret and otp values can be exported to json or csv files, as well as printed or saved to PNG images.
The secrets can be exported to JSON or CSV, or printed as QR codes to console or saved as PNG.
**This project/script was renamed from `extract_otp_secret_keys` to `extract_otp_secrets`.**
<details>
<summary>Table of contents</summary>
## Table of contents
- [Download binary executable (🆕 since v2.1)](#download-binary-executable--since-v21)
- [Usage](#usage)
- [Capture QR codes from camera (🆕 since version 2.0)](#capture-qr-codes-from-camera--since-version-20)
- [With builtin QR decoder from image files (🆕 since version 2.0)](#with-builtin-qr-decoder-from-image-files--since-version-20)
- [With external QR decoder app from text files](#with-external-qr-decoder-app-from-text-files)
- [Installation of Python script (recommended for developers or advanced users)](#installation-of-python-script-recommended-for-developers-or-advanced-users)
- [Installation of optional shared system libraries (recommended)](#installation-of-optional-shared-system-libraries-recommended)
- [Program help: arguments and options](#program-help-arguments-and-options)
- [Examples](#examples)
- [Printing otp secrets form text file](#printing-otp-secrets-form-text-file)
- [Printing otp secrets from image file](#printing-otp-secrets-from-image-file)
- [Writing otp secrets to csv file](#writing-otp-secrets-to-csv-file)
- [Writing otp secrets to json file](#writing-otp-secrets-to-json-file)
- [Printing otp secrets multiple files](#printing-otp-secrets-multiple-files)
- [Printing otp secrets from stdin (text)](#printing-otp-secrets-from-stdin-text)
- [Printing otp secrets from stdin (image)](#printing-otp-secrets-from-stdin-image)
- [Printing otp secrets csv to stdout](#printing-otp-secrets-csv-to-stdout)
- [Printing otp secrets csv to stdout without header line](#printing-otp-secrets-csv-to-stdout-without-header-line)
- [Reading from stdin and printing to stdout](#reading-from-stdin-and-printing-to-stdout)
- [Features](#features)
- [KeePass](#keepass)
- [How to export otp secrets from Google Authenticator app](#how-to-export-otp-secrets-from-google-authenticator-app)
- [Glossary](#glossary)
- [Alternative installation methods](#alternative-installation-methods)
- [pip using github](#pip-using-github)
- [local pip](#local-pip)
- [pipenv](#pipenv)
- [Visual Studio Code Remote - Containers / VSCode devcontainer](#visual-studio-code-remote---containers--vscode-devcontainer)
- [venv](#venv)
- [devbox](#devbox)
- [docker](#docker)
- [More docker examples](#more-docker-examples)
- [Tests](#tests)
- [PyTest](#pytest)
- [unittest](#unittest)
- [VSCode Setup](#vscode-setup)
- [Development](#development)
- [Build](#build)
- [Upgrade pip Packages](#upgrade-pip-packages)
- [Build docker images](#build-docker-images)
- [Create executables with pyinstaller](#create-executables-with-pyinstaller)
- [Full local build (bash)](#full-local-build-bash)
- [Technical background](#technical-background)
- [References](#references)
- [Issues](#issues)
- [Problems and Troubleshooting](#problems-and-troubleshooting)
- [Windows error message](#windows-error-message)
- [Related projects](#related-projects)
</details>
## Download binary executable (🆕 since v2.1)
1. Download executable for your platform from [latest release](https://github.com/scito/extract_otp_secrets/releases/latest), see assets
2. Start executable by clicking or from command line
: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: If you are a developer, you might prefer to run the Python script directly, see [Installation](#installation-of-python-script-recommended-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.
> This is a known problem for executables generated by PyInstaller.
> If you have any doubt, please use directly the [Python script](#installation-of-python-script-recommended-for-developers-or-advanced-users).
:information_source: The executables are not signed. Thus, the operating system may show a warning about download from unknown source.
## Usage
### Capture QR codes from camera (🆕 since version 2.0)
@@ -30,11 +113,12 @@ The secret and otp values can be exported to json or csv files, as well as print
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:
4. Run the program without infile parameters:
```
extract_otp_secrets
```
python src/extract_otp_secrets.py
![CV2 Capture from camera screenshot](cv2_capture_screenshot.png)
![CV2 Capture from camera screenshot](docs/cv2_capture_screenshot.png)
Detected QR codes are surrounded with a frame. The color of the frame indicates the extracting result:
@@ -42,30 +126,44 @@ 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)
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
2. Export the QR codes from "Google Authenticator" app (see [how to export](#how-to-export-otp-secrets-from-google-authenticator-app))
3. Save the QR code as image file, e.g. example_export.png
4. Transfer the images files to the computer where his script is installed.
5. Call this script with the file as input:
```
extract_otp_secrets example_export.png
```
6. Remove unencrypted files with secrets from your computer and mobile.
### 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))
2. Export the QR codes from "Google Authenticator" app (see [how to export](#how-to-export-otp-secrets-from-google-authenticator-app))
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=...`)
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
## Installation
```
extract_otp_secrets example_export.txt
```
7. Remove unencrypted files with secrets from your computer and mobile.
## Installation of Python script (recommended for developers or advanced users)
```bash
git clone https://github.com/scito/extract_otp_secrets.git
cd extract_otp_secrets
pip install -U -r requirements.txt
@@ -75,14 +173,14 @@ python src/extract_otp_secrets.py example_export.txt
In case this script is not starting properly, the debug mode can be activated by adding parameter `-d` in the command line.
### Installation of shared system libraries
### Installation of optional shared system libraries (recommended)
For reading QR codes with `ZBAR` QR reader, the zbar library must be installed.
If you do not use the `ZBAR` QR reader, you do not need to install the zbar shared library. Note: The `ZBAR` QR reader is the showed for me the best results and is thus default QR Reader.
For a detailed installation documentation of [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar#installation).
#### Linux (Debian, Ubuntu, ...)
#### Linux (Debian, Ubuntu, )
sudo apt-get install libzbar0
@@ -94,6 +192,10 @@ For a detailed installation documentation of [pyzbar](https://github.com/Natural
sudo dnf install libzbar0
#### Linux (Arch Linux)
pacman -S zbar
#### Mac OS X
brew install zbar
@@ -104,13 +206,13 @@ For a detailed installation documentation of [pyzbar](https://github.com/Natural
The zbar DLLs are included with the Windows Python wheels. However, you might need additionally to install [Visual C++ Redistributable Packages for Visual Studio 2013](https://www.microsoft.com/en-US/download/details.aspx?id=40784). Install `vcredist_x64.exe` if using 64-bit Python, `vcredist_x86.exe` if using 32-bit Python. For more information see [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar)
##### OpenCV
##### OpenCV (CV2)
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/)
## 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 ...]
<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] [--version] [-d | -v | -q] [infile ...]
Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps
If no infiles are provided, a GUI window starts and QR codes are captured from the camera.
@@ -131,6 +233,7 @@ options:
QR reader (default: ZBAR)
-i, --ignore ignore duplicate otps
--no-color, -n do not use ANSI colors in console output
--version, -V print version and quit
-d, --debug enter debug mode, do checks and quit
-v, --verbose verbose output
-q, --quiet no stdout output, except output set by -
@@ -152,6 +255,14 @@ python extract_otp_secrets.py = < example_export.png</pre>
python src/extract_otp_secrets.py example_export.png
### Writing otp secrets to csv file
python src/extract_otp_secrets.py -q --csv extracted_secrets.csv example_export.txt
### Writing otp secrets to json file
python src/extract_otp_secrets.py -q --json extracted_secrets.json example_export.txt
### Printing otp secrets multiple files
python src/extract_otp_secrets.py example_*.txt
@@ -184,11 +295,15 @@ 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)
* 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
* ZBAR: [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) - fast and reliable, good for images and video capture (default and recommended) [if [libzbar](#installation-of-optional-shared-system-libraries-recommended) is installed]
* QREADER: [QReader](https://github.com/Eric-Canas/QReader) [if [libzbar](#installation-of-optional-shared-system-libraries-recommended) is installed]
* QREADER_DEEP: [QReader](https://github.com/Eric-Canas/QReader) - very slow in GUI [if [libzbar](#installation-of-optional-shared-system-libraries-recommended) is installed]
* 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:
@@ -208,6 +323,12 @@ python extract_otp_secrets.py = < example_export.png</pre>
* Portable image format - *.pbm, *.pgm, *.ppm *.pxm, *.pnm
* Prints errors and warnings to stderr (🆕 since v2.0)
* Prints colored output (🆕 since v2.0)
* Startable as executable (script, Python, and all dependencies packed in one executable) (🆕 since v2.1)
* extract_otp_secrets_linux_x86_64 (requires glibc >= 2.28)
* extract_otp_secrets_linux_arm64 (requires glibc >= 2.28)
* extract_otp_secrets_win_x86_64.exe
* extract_otp_secrets_macos_x86_64 (barely tested, [libzbar](#installation-of-optional-shared-system-libraries-recommended) needs to be installed additionally if needed)
* Prebuilt Docker images provided for amd64 and arm64 (🆕 since v2.0)
* Many ways to run the script:
* Native Python
* pipenv
@@ -216,13 +337,13 @@ python extract_otp_secrets.py = < example_export.png</pre>
* Docker
* VSCode devcontainer
* devbox
* Prebuilt Docker images provided for amd64 and arm64 (🆕 since v2.0)
* Compatible with major platforms:
* Linux
* macOS
* Windows
* Uses UTF-8 on all platforms
* Supports Python >= 3.7
* Installation of shared system libraries is optional (🆕 since v2.3)
* Provides a debug mode (-d) for analyzing import problems
* Written in modern Python using type hints and following best practices
* All these features are backed by tests ran nightly
@@ -290,10 +411,16 @@ KeePass can be used as a backup for one time passwords (second factor) from the
## Alternative installation methods
### pip
### pip using github
```
pip install -U git+https://github.com/scito/extract_otp_secrets
extract_otp_secrets
```
or run it
```
python -m extract_otp_secrets
```
@@ -301,7 +428,7 @@ or from a specific tag
```
pip install -U git+https://github.com/scito/extract_otp_secrets.git@v2.0.0
python -m extract_otp_secrets
extract_otp_secrets
curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/example_export.txt | python -m extract_otp_secrets -
```
@@ -310,6 +437,12 @@ 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
extract_otp_secrets extract_otp_secrets/example_export.txt
```
or run it
```
python -m extract_otp_secrets extract_otp_secrets/example_export.txt
```
@@ -482,16 +615,34 @@ docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extrac
#### Alpine (only text file processing)
```bash
docker build . -t extract_otp_secrets_only_txt --pull -f Dockerfile_only_txt --build-arg RUN_TESTS=false
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
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
```
### Full local build
### Create executables with pyinstaller
#### Linux
```bash
pyinstaller -y --add-data $pythonLocation/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile src/extract_otp_secrets.py
```
Output is executable `dist/extract_otp_secrets`
#### Windows
```
pyinstaller -y --add-data "%pythonLocation%\__yolo_v3_qr_detector;__yolo_v3_qr_detector" --add-binary "%pythonLocation%\pyzbar\libiconv.dll;pyzbar" --add-binary "%pythonLocation%\pyzbar\libzbar-64.dll;pyzbar" --add-binary "%windir%\system32\msvcr120.dll;pyzbar" --add-binary "%windir%\system32\msvcp120.dll;pyzbar" --add-binary "%windir%\system32\vcamp120.dll;pyzbar" --add-binary "%windir%\system32\vcomp120.dll;pyzbar" --add-binary "%windir%\system32\vccorlib120.dll;pyzbar" --add-binary "%windir%\system32\mfc120.dll;pyzbar" --add-binary "%windir%\system32\mfc120u.dll;pyzbar" --add-binary "%windir%\system32\mfc120chs.dll;pyzbar" --add-binary "%windir%\system32\mfc120cht.dll;pyzbar" --add-binary "%windir%\system32\mfc120deu.dll;pyzbar" --add-binary "%windir%\system32\mfc120enu.dll;pyzbar" --add-binary "%windir%\system32\mfc120esn.dll;pyzbar" --add-binary "%windir%\system32\mfc120fra.dll;pyzbar" --add-binary "%windir%\system32\mfc120ita.dll;pyzbar" --add-binary "%windir%\system32\mfc120jpn.dll;pyzbar" --add-binary "%windir%\system32\mfc120kor.dll;pyzbar" --add-binary "%windir%\system32\mfc120rus.dll;pyzbar" --onefile --version-file build\file_version_info.txt src\extract_otp_secrets.py
```
Output is `dist\extract_otp_secrets.exe`
### Full local build (bash)
There is a Bash script for a full local build including linting and type checking.
@@ -518,7 +669,7 @@ Options:
## Technical background
The export QR code of "Google Authenticator" contains the URL `otpauth-migration://offline?data=...`.
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):
@@ -574,6 +725,7 @@ FileNotFoundError: Could not find module 'libiconv.dll' (or one of its dependenc
* [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.]
* [Google Authenticator secret extractor](https://github.com/krissrex/google-authenticator-exporter) is similar project written in JavaScript. It also extracts otp secrets from Google Authenticator.
***

118
build.sh
View File

@@ -76,9 +76,11 @@ askContinueYn() {
interactive=false
ignore_version_check=true
clean=false
clean_flag=""
build_docker=true
run_gui=true
generate_result_files=false
PYTHONHASHSEED=31
while test $# -gt 0; do
case $1 in
@@ -119,6 +121,7 @@ while test $# -gt 0; do
;;
-c)
clean=true
clean_flag="--clean"
shift
;;
esac
@@ -160,7 +163,7 @@ if $clean; then
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="sudo rm -rf dist/ build/ dist_*/ *.whl extracted_*.csv extracted_*.json 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"
@@ -277,6 +280,15 @@ 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"
# Generate results files
if $generate_result_files; then
cmd="for color in '' '-n'; do for level in '' '-v' '-vv' '-vvv'; do $PYTHON src/extract_otp_secrets.py example_export.txt \$color \$level > tests/data/print_verbose_output\$color\$level.txt; done; done"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
fi
# pip -e install
cmd="$PIP install -U -e ."
@@ -330,13 +342,26 @@ cmd="$PIP wheel ."
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Generate results files
# Build executable
if $generate_result_files; then
cmd="for color in '' '-n'; do for level in '' '-v' '-vv' '-vvv'; do $PYTHON src/extract_otp_secrets.py example_export.txt $color $level > tests/data/print_verbose_output$color$level.txt; done; done"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
fi
cmd="LOCAL_GLIBC_VERSION=$(ldd --version | sed '1!d' | sed -E 's/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/')"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
echo "local glibc: $LOCAL_GLIBC_VERSION"
cmd="pyinstaller -y --add-data $HOME/.local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile $clean_flag src/extract_otp_secrets.py"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="dist/extract_otp_secrets -h"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Generate README.md TOC
cmd="gfm-toc -s 2 -e 3 -t -o README.md > docs/README_TOC.md"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Update Code Coverage in README.md
@@ -349,7 +374,7 @@ if $build_docker; then
# Build docker
# Build Dockerfile_only_txt (Alpine)
cmd="docker build . -t extract_otp_secrets_only_txt -f Dockerfile_only_txt --pull --build-arg RUN_TESTS=false"
cmd="docker build . -t extract_otp_secrets_only_txt -t extract_otp_secrets:only-txt -t extract_otp_secrets:alpine -f Dockerfile_only_txt --pull --build-arg RUN_TESTS=false"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
@@ -361,13 +386,12 @@ if $build_docker; then
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="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' -vvv --relaxed"
cmd="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' -vvv --relaxed"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Build extract_otp_secrets (Debian)
cmd="docker build . -t extract_otp_secrets --pull --build-arg RUN_TESTS=false"
# Build extract_otp_secrets (Debian Bullseye)
cmd="docker build . -t extract_otp_secrets -t extract_otp_secrets:bullseye --pull --build-arg RUN_TESTS=false"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
@@ -387,6 +411,76 @@ if $build_docker; then
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Build extract_otp_secrets (Debian Buster)
cmd="docker build . -t extract_otp_secrets:buster --pull --build-arg RUN_TESTS=false --build-arg BASE_IMAGE=python:3.11-slim-buster"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="docker run --rm -v \"$(pwd)\":/files:ro extract_otp_secrets:buster example_export.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="cat example_export.txt | docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets:buster - -c - > example_output.csv"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets:buster = < example_export.png"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secrets:buster"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Build executable from Docker latest
# sed "1!d" is workaround for head -n 1 since it head procduces exit code != 0
BULLSEYE_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
echo "Bullseye glibc: $BULLSEYE_GLIBC_VERSION"
cmd="docker run --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets -c 'apt-get update && apt-get -y install binutils && pip install -U pip && pip install -U -r /files/requirements.txt && pip install pyinstaller && PYTHONHASHSEED=31 && pyinstaller -y --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_linux_x86_64_bullseye --distpath /files/dist/ /files/src/extract_otp_secrets.py'"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="dist/extract_otp_secrets_linux_x86_64_bullseye -h"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Build executable from Docker buster
BUSTER_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets:buster -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
echo "Bullseye glibc: $BUSTER_GLIBC_VERSION"
cmd="docker run --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:buster -c 'apt-get update && apt-get -y install binutils && pip install -U pip && pip install -U -r /files/requirements.txt && pip install pyinstaller && PYTHONHASHSEED=31 && 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'"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="dist/extract_otp_secrets_linux_x86_64 -h"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# create Windows file_version_info.txt
cmd="VERSION_STR=$(setuptools-git-versioning) VERSION_MAJOR=$(cut -d '.' -f 1 <<< "$(setuptools-git-versioning)") VERSION_MINOR=$(cut -d '.' -f 2 <<< "$(setuptools-git-versioning)") VERSION_PATCH=$(cut -d '.' -f 3 <<< "$(setuptools-git-versioning)") VERSION_BUILD=$(($(git rev-list --count $(git tag | sort -V -r | sed '1!d')..HEAD))) YEARS='2020-2023' envsubst < file_version_info_template.txt > build/file_version_info.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# build linux/arm64
# Build extract_otp_secrets (Debian Buster)
cmd="docker buildx build --platform=linux/arm64 . -t extract_otp_secrets:buster --pull --build-arg RUN_TESTS=false --build-arg BASE_IMAGE=python:3.11-slim-buster"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="docker run --pull always --rm --privileged multiarch/qemu-user-static --reset -p yes"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="docker run --platform linux/arm64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:buster -c 'apt-get update && apt-get -y install binutils && pip install -U pip && pip install -U -r /files/requirements.txt && pip install pyinstaller && PYTHONHASHSEED=31 && pyinstaller -y --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_linux_arm64 --distpath /files/dist/ /files/src/extract_otp_secrets.py'"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="PLATFORM='linux/arm64' && EXE='dist/extract_otp_secrets_linux_arm64' && docker run --platform \"\$PLATFORM\" --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:buster -c \"\$EXE -V && \$EXE -h && \$EXE example_export.png && \$EXE - < example_export.txt && \$EXE --qr ZBAR example_export.png && \$EXE --qr QREADER example_export.png && \$EXE --qr QREADER_DEEP example_export.png && \$EXE --qr CV2 example_export.png && \$EXE --qr CV2_WECHAT example_export.png\""
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Run GUI from Docker
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

4
docker/.alias Normal file
View File

@@ -0,0 +1,4 @@
alias ll='ls -lh'
alias la='ls -lha'
alias l='ls -alhF'
alias ls-l='ls -lh'

55
docs/README_TOC.md Normal file
View File

@@ -0,0 +1,55 @@
Generate from file: README.md
## Table of contents
- [Table of contents](#table-of-contents)
- [Download binary executable (🆕 since v2.1)](#download-binary-executable--since-v21)
- [Usage](#usage)
- [Capture QR codes from camera (🆕 since version 2.0)](#capture-qr-codes-from-camera--since-version-20)
- [With builtin QR decoder from image files (🆕 since version 2.0)](#with-builtin-qr-decoder-from-image-files--since-version-20)
- [With external QR decoder app from text files](#with-external-qr-decoder-app-from-text-files)
- [Installation of Python script (recommended for developers or advanced users)](#installation-of-python-script-recommended-for-developers-or-advanced-users)
- [Installation of optional shared system libraries (recommended)](#installation-of-optional-shared-system-libraries-recommended)
- [Program help: arguments and options](#program-help-arguments-and-options)
- [Examples](#examples)
- [Printing otp secrets form text file](#printing-otp-secrets-form-text-file)
- [Printing otp secrets from image file](#printing-otp-secrets-from-image-file)
- [Writing otp secrets to csv file](#writing-otp-secrets-to-csv-file)
- [Writing otp secrets to json file](#writing-otp-secrets-to-json-file)
- [Printing otp secrets multiple files](#printing-otp-secrets-multiple-files)
- [Printing otp secrets from stdin (text)](#printing-otp-secrets-from-stdin-text)
- [Printing otp secrets from stdin (image)](#printing-otp-secrets-from-stdin-image)
- [Printing otp secrets csv to stdout](#printing-otp-secrets-csv-to-stdout)
- [Printing otp secrets csv to stdout without header line](#printing-otp-secrets-csv-to-stdout-without-header-line)
- [Reading from stdin and printing to stdout](#reading-from-stdin-and-printing-to-stdout)
- [Features](#features)
- [KeePass](#keepass)
- [How to export otp secrets from Google Authenticator app](#how-to-export-otp-secrets-from-google-authenticator-app)
- [Glossary](#glossary)
- [Alternative installation methods](#alternative-installation-methods)
- [pip using github](#pip-using-github)
- [local pip](#local-pip)
- [pipenv](#pipenv)
- [Visual Studio Code Remote - Containers / VSCode devcontainer](#visual-studio-code-remote---containers--vscode-devcontainer)
- [venv](#venv)
- [devbox](#devbox)
- [docker](#docker)
- [More docker examples](#more-docker-examples)
- [Tests](#tests)
- [PyTest](#pytest)
- [unittest](#unittest)
- [VSCode Setup](#vscode-setup)
- [Development](#development)
- [Build](#build)
- [Upgrade pip Packages](#upgrade-pip-packages)
- [Build docker images](#build-docker-images)
- [Create executables with pyinstaller](#create-executables-with-pyinstaller)
- [Full local build (bash)](#full-local-build-bash)
- [Technical background](#technical-background)
- [References](#references)
- [Issues](#issues)
- [Problems and Troubleshooting](#problems-and-troubleshooting)
- [Windows error message](#windows-error-message)
- [Related projects](#related-projects)
Table of contents generated.

View File

Before

Width:  |  Height:  |  Size: 528 KiB

After

Width:  |  Height:  |  Size: 528 KiB

View File

@@ -0,0 +1,46 @@
# UTF-8
#
# For more details about fixed file info 'ffi' see:
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
VSVersionInfo(
ffi=FixedFileInfo(
# The elements of each tuple represent 16-bit values from most-significant to least-significant. For example the value (2, 0, 4, 0) resolves to 0002000000040000 in hex.
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
# Set not needed items to zero 0.
filevers=($VERSION_MAJOR, $VERSION_MINOR, $VERSION_PATCH, $VERSION_BUILD),
prodvers=($VERSION_MAJOR, $VERSION_MINOR, $VERSION_PATCH, $VERSION_BUILD),
# Contains a bitmask that specifies the valid bits 'flags'r
mask=0x3f,
# Contains a bitmask that specifies the Boolean attributes of the file.
flags=0x0,
# The operating system for which this file was designed.
# 0x4 - NT and there is no need to change it.
OS=0x4,
# The general type of file.
# 0x1 - the file is an application.
fileType=0x1,
# The function of the file.
# 0x0 - the function is not defined for this fileType
subtype=0x0,
# Creation date and time stamp.
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
# 0x0409 (U.S. English) + 04B0 (1200 = Unicode), https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block
'040904B0',
[StringStruct('CompanyName', 'scito'),
StringStruct('FileDescription', 'extract_otp_secrets'),
StringStruct('FileVersion', '$VERSION_STR'),
StringStruct('InternalName', 'extract_otp_secrets'),
StringStruct('LegalCopyright', 'Copyright © $YEARS Scito.'),
StringStruct('OriginalFilename', 'extract_otp_secrets.exe'),
StringStruct('ProductName', 'extract_otp_secrets'),
StringStruct('ProductVersion', '$VERSION_STR')])
]),
# 1033 (0x0409 = U.S. English), 1200 (Unicode)
VarFileInfo([VarStruct('Translation', [0, 1200])])
]
)

View File

@@ -16,6 +16,8 @@ classifiers = [
"Environment :: Win32 (MS Windows)",
"Topic :: System :: Archiving :: Backup",
"Topic :: Utilities",
"Topic :: Security",
"Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
@@ -25,8 +27,13 @@ classifiers = [
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Programming Language :: Python",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows :: Windows 10",
"Operating System :: Microsoft :: Windows :: Windows 11",
"Operating System :: MacOS",
"Natural Language :: English",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Typing :: Typed",
]
dependencies = [
"colorama>=0.4.6",
@@ -36,8 +43,10 @@ dependencies = [
"protobuf",
"pyzbar",
"qrcode",
"qreader",
"qreader<2.0.0",
# workaround for PYTHON <= 3.7: compatibility
"typing_extensions; python_version<='3.7'",
"importlib_metadata; 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'"
dynamic = ["version"]

View File

@@ -1,11 +1,14 @@
build
flake8
gfm-toc
mypy
mypy-protobuf
pyinstaller
pylint
pytest
pytest-cov
pytest-mock
setuptools
setuptools-git-versioning
types-protobuf
wheel

View File

@@ -1,9 +1,10 @@
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
protobuf
pyzbar
qrcode
qreader
qreader<2.0.0
typing_extensions; python_version<='3.7'

View File

@@ -1,3 +1,5 @@
#!/bin/sh
cd /extract
pip install -U pytest pytest-mock && pip install --no-deps . && pytest "$@"
mkdir -p tests
ln -sf /extract/data tests/data
pip install -U pytest pytest-mock && pytest "$@"

View File

@@ -6,6 +6,10 @@ python_requires = >=3.7, <4
py_modules = extract_otp_secrets, protobuf_generated_python.google_auth_pb2
package_dir =
=src
platforms =
Linux
Windows
MacOS
# packages=find:
# [options.packages.find]

View File

@@ -5,7 +5,7 @@
# Source code available on https://github.com/scito/extract_otp_secrets
#
# Technical background:
# The export QR code from "Google Authenticator" contains the URL "otpauth-migration://offline?data=...".
# The export QR code from "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):
@@ -38,11 +38,18 @@ import csv
import fileinput
import json
import os
import platform
import re
import sys
import urllib.parse as urlparse
from enum import Enum, IntEnum
from typing import Any, List, Optional, TextIO, Tuple, Union
from typing import Any, List, Optional, Sequence, TextIO, Tuple, Union
import colorama
from pkg_resources import DistributionNotFound, get_distribution
from qrcode import QRCode # type: ignore
import protobuf_generated_python.google_auth_pb2 as pb
# workaround for PYTHON <= 3.7: compatibility
if sys.version_info >= (3, 8):
@@ -50,28 +57,43 @@ if sys.version_info >= (3, 8):
else:
from typing_extensions import Final, TypedDict
from qrcode import QRCode # type: ignore
# workaround for PYTHON <= 3.7: compatibility
if sys.version_info >= (3, 8):
from importlib.metadata import PackageNotFoundError, version
else:
from importlib_metadata import PackageNotFoundError, version
import protobuf_generated_python.google_auth_pb2 as pb
import colorama
debug_mode = '-d' in sys.argv[1:] or '--debug' in sys.argv[1:]
quiet = '-q' in sys.argv[1:] or '--quiet' 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
except ImportError as e:
print(f"""
ERROR: Cannot import QReader module. This problem is probably due to the missing zbar shared library.
zbar_available = True
except Exception as e:
if not quiet:
print(f"""
WARN: Cannot import pyzbar or qreader module. This problem is probably due to the missing zbar shared library.
On Linux and macOS libzbar0 must be installed.
See in README.md for the installation of the libzbar0.
Exception: {e}\n""", file=sys.stderr)
raise e
zbar_available = False
if debug_mode:
raise e
# Types
# workaround for PYTHON <= 3.9: Final[tuple[int]]
@@ -104,9 +126,9 @@ Exception: {e}\n""", file=sys.stderr)
TextPosition = Enum('TextPosition', ['LEFT', 'RIGHT'])
qreader_available = True
cv2_available = True
except ImportError as e:
qreader_available = False
cv2_available = False
if debug_mode:
raise e
@@ -128,11 +150,14 @@ LogLevel = IntEnum('LogLevel', ['QUIET', 'NORMAL', 'VERBOSE', 'MORE_VERBOSE', 'D
# Constants
CAMERA: Final[str] = 'camera'
CV2_QRMODES: List[str] = [QRMode.CV2.name, QRMode.CV2_WECHAT.name]
# Global variable declaration
verbose: IntEnum = LogLevel.NORMAL
quiet: bool = False
colored: bool = True
executable: bool = False
__version__: str
tk_root: tkinter.Tk
def sys_main() -> None:
@@ -140,6 +165,7 @@ def sys_main() -> None:
def main(sys_args: list[str]) -> None:
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
@@ -150,18 +176,30 @@ def main(sys_args: list[str]) -> None:
# StringIO in tests do not have all attributes, ignore it
pass
# https://pyinstaller.org/en/stable/runtime-information.html#run-time-information
executable = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
if cv2_available and not headless:
try:
tk_root = tkinter.Tk()
tk_root.withdraw()
except tkinter.TclError:
headless = True
args = parse_args(sys_args)
if colored:
colorama.just_fix_windows_console()
if verbose >= LogLevel.DEBUG:
print(f"Version: {get_full_version()}\n")
if args.debug:
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
@@ -237,31 +275,39 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count:
def parse_args(sys_args: list[str]) -> Args:
global verbose, quiet, colored
# For PYTHON <= 3.7: Use :=
name = os.path.basename(sys.argv[0])
cmd = f"python {name}" if name.endswith('.py') else f"{name}"
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
if qreader_available:
if cv2_available:
description_text += "\nIf no infiles are provided, a GUI window starts and QR codes are captured from the camera."
example_text = """examples:
python extract_otp_secrets.py
python extract_otp_secrets.py example_*.txt
python extract_otp_secrets.py - < example_export.txt
python extract_otp_secrets.py --csv - example_*.png | tail -n+2
python extract_otp_secrets.py = < example_export.png"""
example_text = f"""examples:
{cmd}
{cmd} example_*.txt
{cmd} - < example_export.txt
{cmd} --csv - example_*.png | tail -n+2
{cmd} = < example_export.png"""
arg_parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=32),
description=description_text,
epilog=example_text)
arg_parser.add_argument('infile', help="""a) file or - for stdin with 'otpauth-migration://...' URLs separated by newlines, lines starting with # are ignored;
b) image file containing a QR code or = for stdin for an image containing a QR code""", nargs='*' if qreader_available else '+')
b) image file containing a QR code or = for stdin for an image containing a QR code""", nargs='*' if cv2_available else '+')
arg_parser.add_argument('--csv', '-c', help='export csv file or - for stdout', metavar=('FILE'))
arg_parser.add_argument('--keepass', '-k', help='export totp/hotp csv file(s) for KeePass, - for stdout', metavar=('FILE'))
arg_parser.add_argument('--json', '-j', help='export json file or - for stdout', metavar=('FILE'))
arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal (requires qrcode module)', action='store_true')
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
if qreader_available:
if cv2_available:
arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, metavar=('NUMBER'))
arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.ZBAR.name})', type=str, choices=[mode.name for mode in QRMode], default=QRMode.ZBAR.name)
if not zbar_available:
arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.CV2.name})', type=str, choices=[QRMode.CV2.name, QRMode.CV2_WECHAT.name], default=QRMode.CV2.name)
else:
arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.ZBAR.name})', type=str, choices=[mode.name for mode in QRMode], default=QRMode.ZBAR.name)
arg_parser.add_argument('-i', '--ignore', help='ignore duplicate otps', action='store_true')
arg_parser.add_argument('--no-color', '-n', help='do not use ANSI colors in console output', action='store_true')
arg_parser.add_argument('--version', '-V', help='print version and quit', action=PrintVersionAction)
output_group = arg_parser.add_mutually_exclusive_group()
output_group.add_argument('-d', '--debug', help='enter debug mode, do checks and quit', action='count')
output_group.add_argument('-v', '--verbose', help='verbose output', action='count')
@@ -276,8 +322,8 @@ b) image file containing a QR code or = for stdin for an image containing a QR c
verbose = LogLevel.DEBUG
log_debug('Debug mode start')
quiet = True if args.quiet else False
if verbose: print(f"QReader installed: {qreader_available}")
if qreader_available:
if verbose: print(f"QReader installed: {cv2_available}")
if cv2_available:
if verbose >= LogLevel.VERBOSE: print(f"CV2 version: {cv2.__version__}")
if verbose: print(f"QR reading mode: {args.qr}\n")
@@ -301,7 +347,8 @@ def extract_otps_from_camera(args: Args) -> Otps:
cam = cv2.VideoCapture(args.camera)
cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_AUTOSIZE)
qreader = QReader()
if zbar_available:
qreader = QReader()
cv2_qr = cv2.QRCodeDetector()
cv2_qr_wechat = cv2.wechat_qrcode.WeChatQRCode()
while True:
@@ -322,7 +369,7 @@ def extract_otps_from_camera(args: Args) -> Otps:
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))
elif qr_mode == QRMode.ZBAR:
for qrcode in zbar.decode(img):
for qrcode in zbar.decode(img, symbols=[zbar.ZBarSymbol.QRCODE]):
otp_url = qrcode.data.decode('utf-8')
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
cv2_draw_box(img, [qrcode.polygon], get_color(new_otps_count, otp_url))
@@ -342,15 +389,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
@@ -396,14 +444,50 @@ 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)
qr_mode = next_valid_qr_mode(qr_mode, zbar_available)
if verbose >= LogLevel.MORE_VERBOSE: print(f"QR reading mode: {qr_mode}")
if cv2.getWindowProperty(WINDOW_NAME, cv2.WND_PROP_VISIBLE) < 1:
# Window close clicked
@@ -451,7 +535,7 @@ def get_otp_urls_from_file(filename: str, args: Args) -> OtpUrls:
return lines
# could not process text file, try reading as image
if filename != '-' and qreader_available:
if filename != '-' and cv2_available:
return convert_img_to_otp_urls(filename, args)
return []
@@ -530,7 +614,7 @@ def decode_qr_img_otp_urls(img: Any, qr_mode: QRMode) -> OtpUrls:
otp_url, _ = cv2.wechat_qrcode.WeChatQRCode().detectAndDecode(img)
otp_urls += list(otp_url)
elif qr_mode == QRMode.ZBAR:
qrcodes = zbar.decode(img)
qrcodes = zbar.decode(img, symbols=[zbar.ZBarSymbol.QRCODE])
otp_urls += [qrcode.data.decode('utf-8') for qrcode in qrcodes]
else:
assert False, f"Wrong QReader mode {qr_mode.name}"
@@ -606,22 +690,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:
@@ -633,9 +717,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:
@@ -650,9 +734,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:
@@ -668,11 +752,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:
@@ -709,6 +793,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('#')
@@ -717,6 +808,14 @@ def is_binary(line: str) -> bool:
return True
def next_valid_qr_mode(qr_mode: QRMode, with_zbar: bool = True) -> QRMode:
ok = False
while not ok:
qr_mode = next_qr_mode(qr_mode)
ok = True if with_zbar else qr_mode.name in CV2_QRMODES
return qr_mode
def next_qr_mode(qr_mode: QRMode) -> QRMode:
return QRMode((qr_mode.value + 1) % len(QRMode))
@@ -727,10 +826,71 @@ def do_debug_checks() -> bool:
import cv2 # noqa: F401 # This is only a debug import
log_debug('Try: import numpy as np')
import numpy as np # noqa: F401 # This is only a debug import
log_debug('Try: import pyzbar.pyzbar as zbar')
import pyzbar.pyzbar as zbar # noqa: F401 # This is only a debug import
log_debug('Try: from qreader import QReader')
from qreader import QReader # noqa: F401 # This is only a debug import
print(color('\nDebug checks passed', colorama.Fore.GREEN))
return True
def is_not_headless() -> bool:
if headless:
log_warn("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)
def __call__(self, parser: argparse.ArgumentParser, namespace: Args, values: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None:
print_version()
parser.exit()
def print_version() -> None:
print(get_full_version())
def get_full_version() -> str:
version = get_raw_version()
meta = [
platform.python_implementation()
]
if executable: meta.append('exe')
meta.append(f"called as {'package' if __package__ else 'script'}")
return (
f"extract_otp_secrets {version} {platform.system()} {platform.machine()}"
f" Python {platform.python_version()}"
f" ({'/'.join(meta)})"
)
# https://setuptools-git-versioning.readthedocs.io/en/stable/runtime_version.html
def get_raw_version() -> str:
global __version__
try:
__version__ = version("extract_otp_secrets")
return __version__
except PackageNotFoundError:
# package is not installed
pass
# In some cases importlib cannot properly detect package version, for example it was compiled into executable file, so it uses some custom import mechanism.
# Instead, use pkg_resources which is included in setuptools (but has a significant runtime cost)
try:
__version__ = get_distribution("package-name").version
return __version__
except DistributionNotFound:
# package is not installed
pass
return ''
# workaround for PYTHON <= 3.9 use: BaseException | None
def log_debug(*values: object, sep: Optional[str] = ' ') -> None:
if colored:

View File

@@ -2,6 +2,8 @@ QReader installed: True
CV2 version: 4.7.0
QR reading mode: ZBAR
Version: extract_otp_secrets 2.0.2.post50+git.158245dd.dirty Linux x86_64 Python 3.11.1 (CPython/called as script)
Input files: ['example_export.txt']
Processing infile example_export.txt
Reading lines of example_export.txt

View File

@@ -2,6 +2,8 @@ QReader installed: True
CV2 version: 4.7.0
QR reading mode: ZBAR
Version: extract_otp_secrets 2.0.2.post50+git.158245dd.dirty Linux x86_64 Python 3.11.1 (CPython/called as script)
Input files: ['example_export.txt']
Processing infile example_export.txt
Reading lines of example_export.txt

View File

@@ -45,11 +45,11 @@ except ImportError:
# ignore
pass
qreader_available: bool = extract_otp_secrets.qreader_available
cv2_available: bool = extract_otp_secrets.cv2_available
# Quickfix comment
# @pytest.mark.skipif(sys.platform.startswith("win") or not qreader_available or sys.implementation.name == 'pypy' or sys.version_info >= (3, 10), reason="Quickfix")
# @pytest.mark.skipif(sys.platform.startswith("win") or not cv2 or sys.implementation.name == 'pypy' or sys.version_info >= (3, 10), reason="Quickfix")
def test_extract_stdout(capsys: pytest.CaptureFixture[str]) -> None:
@@ -122,7 +122,7 @@ def test_extract_stdin_only_comments(capsys: pytest.CaptureFixture[str], monkeyp
def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None:
if qreader_available:
if cv2_available:
# Act
with pytest.raises(SystemExit) as e:
extract_otp_secrets.main(['-n', 'tests/data/empty_file.txt'])
@@ -509,8 +509,8 @@ def test_extract_verbose(verbose_level: str, color: str, capsys: pytest.CaptureF
def normalize_verbose_text(text: str, relaxed: bool) -> str:
normalized = re.sub('^.+ version: .+$', '', text, flags=re.MULTILINE | re.IGNORECASE)
if not qreader_available:
normalized = re.sub('^.*version: .+$', '', text, flags=re.MULTILINE | re.IGNORECASE)
if not cv2_available:
normalized = normalized \
.replace('QReader installed: True', 'QReader installed: False') \
.replace('\nQR reading mode: ZBAR\n\n', '')
@@ -549,8 +549,22 @@ def test_extract_help(capsys: pytest.CaptureFixture[str]) -> None:
assert e.value.code == 0
def test_extract_version(capsys: pytest.CaptureFixture[str]) -> None:
with pytest.raises(SystemExit) as e:
# Act
extract_otp_secrets.main(['--version'])
# Assert
captured = capsys.readouterr()
assert captured.out.startswith('extract_otp_secrets ')
assert captured.err == ''
assert e.type == SystemExit
assert e.value.code == 0
def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
if qreader_available:
if cv2_available:
# Arrange
otps = read_json('example_output.json')
mocker.patch('extract_otp_secrets.extract_otps_from_camera', return_value=otps)
@@ -634,7 +648,7 @@ class MockCam:
('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:
if cv2_available:
# Arrange
mockCam = MockCam([file])
mocker.patch('cv2.VideoCapture', return_value=mockCam)
@@ -719,7 +733,7 @@ def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
('-n', None, False, False),
])
def test_quiet(parameter: str, parameter_value: Optional[str], stdout_expected: bool, stderr_expected: bool, capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
if parameter in ['-Q', '-C'] and not qreader_available:
if parameter in ['-Q', '-C'] and not cv2_available:
return
# Arrange
@@ -1062,6 +1076,12 @@ url: This is just a text file masquerading as an image file.
assert captured.out == ''
def test_next_valid_qr_mode() -> None:
assert extract_otp_secrets.next_valid_qr_mode(extract_otp_secrets.QRMode.CV2, True) == extract_otp_secrets.QRMode.CV2_WECHAT
assert extract_otp_secrets.next_valid_qr_mode(extract_otp_secrets.QRMode.CV2_WECHAT, True) == extract_otp_secrets.QRMode.ZBAR
assert extract_otp_secrets.next_valid_qr_mode(extract_otp_secrets.QRMode.CV2_WECHAT, False) == extract_otp_secrets.QRMode.CV2
EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT = '''Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi