Compare commits
46 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5555dda9c2 | ||
|
|
eb8ea3330f | ||
|
|
75e5d2671f | ||
|
|
5e94c43140 | ||
|
|
2ef34a2d69 | ||
|
|
8df61f6ed7 | ||
|
|
def5d58af0 | ||
|
|
ca23497c9a | ||
|
|
ebfe0dc58d | ||
|
|
78118c73e8 | ||
|
|
4fc5559e15 | ||
|
|
7faf530863 | ||
|
|
709aa111be | ||
|
|
fe60acb24f | ||
|
|
5c36f07f41 | ||
|
|
861d7d0da8 | ||
|
|
63fc21cc90 | ||
|
|
d234cf4112 | ||
|
|
970dbd3759 | ||
|
|
197347a3e9 | ||
|
|
365d5ac432 | ||
|
|
88ff584e47 | ||
|
|
dbb5d8f755 | ||
|
|
580f94256f | ||
|
|
f4bff86a5c | ||
|
|
cff5fe1cda | ||
|
|
576b1e68c5 | ||
|
|
84e1922979 | ||
|
|
7f89168b92 | ||
|
|
b9f17c4a95 | ||
|
|
6a7a7233a4 | ||
|
|
445d77783c | ||
|
|
f0134fa907 | ||
|
|
e0588285c9 | ||
|
|
ff9401687e | ||
|
|
2478edb7a1 | ||
|
|
5e439b9396 | ||
|
|
5c4d3ce696 | ||
|
|
ec09b5daad | ||
|
|
2ed923591e | ||
|
|
36fd0c0bb6 | ||
|
|
b215b78dad | ||
|
|
851cb6532c | ||
|
|
2bef64e5f6 | ||
|
|
3502294172 | ||
|
|
2707e244be |
13
.github/workflows/ci.yml
vendored
@@ -2,10 +2,15 @@ name: tests
|
||||
|
||||
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
# https://docs.github.com/en/actions/using-workflows
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
# https://docs.github.com/en/actions/learn-github-actions/expressions
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
# pull_request:
|
||||
schedule:
|
||||
# Run daily on default branch
|
||||
- cron: '37 3 * * *'
|
||||
@@ -27,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: |
|
||||
@@ -65,4 +71,7 @@ jobs:
|
||||
with:
|
||||
pytest-coverage-path: ./pytest-coverage.txt
|
||||
junitxml-path: ./pytest.xml
|
||||
if: matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||
if: |
|
||||
matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||
&& !contains(github.ref, 'refs/tags/')
|
||||
|
||||
|
||||
198
.github/workflows/ci_docker.yml
vendored
@@ -2,6 +2,8 @@ name: docker
|
||||
|
||||
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
# https://docs.github.com/en/actions/using-workflows
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
# https://docs.github.com/en/actions/learn-github-actions/expressions
|
||||
|
||||
# How to setup: https://event-driven.io/en/how_to_buid_and_push_docker_image_with_github_actions/
|
||||
# How to run: https://aschmelyun.com/blog/using-docker-run-inside-of-github-actions/
|
||||
@@ -9,14 +11,100 @@ 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
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v2
|
||||
if: github.secret_source == 'Actions'
|
||||
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' && github.secret_source == 'Actions'}}
|
||||
|
||||
- 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
|
||||
|
||||
@@ -44,20 +132,22 @@ jobs:
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v2
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
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
|
||||
@@ -65,30 +155,104 @@ 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' }}
|
||||
push: ${{ github.ref == 'refs/heads/master' && github.secret_source == 'Actions'}}
|
||||
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
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v2
|
||||
if: github.secret_source == 'Actions'
|
||||
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: ${{ github.secret_source == 'Actions' }}
|
||||
|
||||
- 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
|
||||
|
||||
407
.github/workflows/ci_release.yml
vendored
Normal file
@@ -0,0 +1,407 @@
|
||||
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/
|
||||
|
||||
# macOS:
|
||||
# https://pyinstaller.org/en/stable/usage.html#building-macos-app-bundles
|
||||
# https://github.com/pyinstaller/pyinstaller/wiki/Recipe-OSX-Code-Signing
|
||||
|
||||
# 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: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
- 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')
|
||||
# https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release
|
||||
run: |
|
||||
echo "date: ${{ steps.meta.outputs.date }}"
|
||||
echo "version: ${{ steps.meta.outputs.version }}"
|
||||
echo "inline_version: ${{ steps.meta.outputs.inline_version }}"
|
||||
echo "tag_name: ${{ steps.meta.outputs.tag_name }}"
|
||||
echo "tag_message: ${{ steps.meta.outputs.tag_message }}"
|
||||
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\n## Executables\n\nDownload the executable for your platform and execute it, see [README.md](https://github.com/scito/extract_otp_secrets#readme)\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.dmg | N/A, see [README.md](https://github.com/scito/extract_otp_secrets#readme) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_macos_x86_64.pkg | N/A, see [README.md](https://github.com/scito/extract_otp_secrets#readme) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_macos_x86_64 | MacOS x86_64/amd64 (bare executable, see [README.md](https://github.com/scito/extract_otp_secrets#readme); optional libzbar must be installed manually, see [README.md](https://github.com/scito/extract_otp_secrets#readme)) |\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
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v2
|
||||
if: github.secret_source == 'Actions'
|
||||
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 EXE to artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.EXE }}
|
||||
path: dist/${{ matrix.EXE }}
|
||||
- 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
|
||||
EXE: 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\win_file_version_info.txt --name extract_otp_secrets.exe src\extract_otp_secrets.py
|
||||
- os: macos-11
|
||||
TARGET: macos
|
||||
# https://pyinstaller.org/en/stable/spec-files.html#spec-file-options-for-a-macos-bundle
|
||||
EXE: extract_otp_secrets
|
||||
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_macos_x86_64
|
||||
DMG: extract_otp_secrets.dmg
|
||||
ASSET_NAME_DMG: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_macos_x86_64.dmg
|
||||
ASSET_MIME: application/octet-stream
|
||||
UPLOAD: true
|
||||
CMD_BUILD: |
|
||||
VERSION_STR=$(setuptools-git-versioning) COPYRIGHT_YEARS='2020-2023' envsubst < installer/extract_otp_secrets_macos_template.spec > extract_otp_secrets_macos.spec
|
||||
pyinstaller -y extract_otp_secrets_macos.spec
|
||||
installer/build_dmg.sh
|
||||
- os: ubuntu-latest
|
||||
TARGET: linux
|
||||
EXE: extract_otp_secrets_ubuntu
|
||||
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 --name extract_otp_secrets_ubuntu 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 create-dmg
|
||||
- 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 win_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))) COPYRIGHT_YEARS='2020-2023' envsubst < installer/win_file_version_info_template.txt > build/win_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.EXE }} -V
|
||||
dist/${{ matrix.EXE }} -h
|
||||
dist/${{ matrix.EXE }} example_export.png
|
||||
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 for generated exe (stdin)
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
dist/${{ matrix.EXE }} - < 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 EXE to artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: ${{ matrix.EXE }}
|
||||
path: dist/${{ matrix.EXE }}
|
||||
- name: Upload DMG to artifacts
|
||||
uses: actions/upload-artifact@v3
|
||||
if: runner.os == 'macOS'
|
||||
with:
|
||||
name: ${{ matrix.DMG }}
|
||||
path: dist/${{ matrix.DMG }}
|
||||
- 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.EXE }} ${{ steps.meta.outputs.upload_url }}=${{ matrix.ASSET_NAME }}
|
||||
- name: Upload Release Asset DMG (macOS)
|
||||
id: upload-release-asset-dmg
|
||||
if: false && matrix.UPLOAD && startsWith(github.ref, 'refs/tags/v') && runner.os == 'macOS'
|
||||
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.DMG }} ${{ steps.meta.outputs.upload_url }}=${{ matrix.ASSET_NAME_DMG }}
|
||||
|
||||
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
|
||||
10
.gitignore
vendored
@@ -19,3 +19,13 @@ dist/
|
||||
*.xml
|
||||
pytest-coverage.txt
|
||||
tests/reports/
|
||||
dist_*/
|
||||
|
||||
file_version_info_python.txt
|
||||
file_version_info_explorer.txt
|
||||
file_version_info.txt
|
||||
assets/*
|
||||
extract_otp_secrets_linux_arm64.spec
|
||||
extract_otp_secrets_linux_x86_64_bullseye.spec
|
||||
extract_otp_secrets_linux_x86_64.spec
|
||||
extract_otp_secrets.spec
|
||||
|
||||
16
Dockerfile
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
25
Pipfile
@@ -4,24 +4,29 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
protobuf = "*"
|
||||
qrcode = "*"
|
||||
pillow = "*"
|
||||
qreader = "*"
|
||||
opencv-contrib-python = "*"
|
||||
colorama = ">=0.4.6"
|
||||
opencv-contrib-python = "*"
|
||||
# for macOS: opencv-contrib-python = "<=4.7.0"
|
||||
# for PYTHON <= 3.7: typing_extensions = "*"
|
||||
pillow = "*"
|
||||
pyzbar = "*"
|
||||
protobuf = "*"
|
||||
qrcode = "*"
|
||||
qreader = "<2.0.0"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
pytest-mock = "*"
|
||||
pytest-cov = "*"
|
||||
wheel = "*"
|
||||
build = "*"
|
||||
flake8 = "*"
|
||||
pylint = "*"
|
||||
gfm-toc = "*"
|
||||
mypy = "*"
|
||||
mypy-protobuf = "*"
|
||||
pylint = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-mock = "*"
|
||||
setuptools-git-versioning = "*"
|
||||
types-protobuf = "*"
|
||||
wheel = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.11"
|
||||
|
||||
442
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "25b244c44cb891ac15ef20c4011eb043b87fb1f112396d68f470d0bb362e97f7"
|
||||
"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:0e0e3709d64fbffd3037e4ff403580550f14471fd3eaae9fa11cc9a5c7901153",
|
||||
"sha256:a3cf9f02c53dd259144a7e8f3ccd75d67c9a8c716ef183e0c1f291bc5d7bb3cf"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.2'",
|
||||
"version": "==2.12.13"
|
||||
"version": "==2.14.2"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
@@ -220,65 +251,73 @@
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==22.2.0"
|
||||
},
|
||||
"build": {
|
||||
"hashes": [
|
||||
"sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171",
|
||||
"sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.10.0"
|
||||
},
|
||||
"coverage": {
|
||||
"extras": [
|
||||
"toml"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:04691b8e832a900ed15f5bcccc2008fc2d1c8e7411251fd7d1422a84e1d72841",
|
||||
"sha256:1a613d60be1a02c7a5184ea5c4227f48c08e0635608b9c17ae2b17efef8f2501",
|
||||
"sha256:1d732b5dcafed67d81c5b5a0c404c31a61e13148946a3b910a340f72fdd1ec95",
|
||||
"sha256:2b31f7f246dbff339b3b76ee81329e3eca5022ce270c831c65e841dbbb40115f",
|
||||
"sha256:312fd77258bf1044ef4faa82091f2e88216e4b62dcf1a461d3e917144c8b09b7",
|
||||
"sha256:321316a7b979892a13c148a9d37852b5a76f26717e4b911b606a649394629532",
|
||||
"sha256:36c1a1b6d38ebf8a4335f65226ec36b5d6fd67743fdcbad5c52bdcd46c4f5842",
|
||||
"sha256:38f281bb9bdd4269c451fed9451203512dadefd64676f14ed1e82c77eb5644fc",
|
||||
"sha256:3a2d81c95d3b02638ee6ae647edc79769fd29bf5e9e5b6b0c29040579f33c260",
|
||||
"sha256:3d40ad86a348c79c614e2b90566267dd6d45f2e6b4d2bfb794d78ea4a4b60b63",
|
||||
"sha256:3d72e3d20b03e63bd27b1c4d6b754cd93eca82ecc5dd77b99262d5f64862ca35",
|
||||
"sha256:3fbb59f84c8549113dcdce7c6d16c5731fe53651d0b46c0a25a5ebc7bb655869",
|
||||
"sha256:405d8528a0ea07ca516d9007ecad4e1bd10e2eeef27411c6188d78c4e2dfcddc",
|
||||
"sha256:420f10c852b9a489cf5a764534669a19f49732a0576c76d9489ebf287f81af6d",
|
||||
"sha256:426895ac9f2938bec193aa998e7a77a3e65d3e46903f348e794b4192b9a5b61e",
|
||||
"sha256:4438ba539bef21e288092b30ea2fc30e883d9af5b66ebeaf2fd7c25e2f074e39",
|
||||
"sha256:46db409fc0c3ee5c859b84c7de9cb507166287d588390889fdf06a1afe452e16",
|
||||
"sha256:483e120ea324c7fced6126bb9bf0535c71e9233d29cbc7e2fc4560311a5f8a32",
|
||||
"sha256:4d7be755d7544dac2b9814e98366a065d15a16e13847eb1f5473bb714483391e",
|
||||
"sha256:4e97b21482aa5c21e049e4755c95955ad71fb54c9488969e2f17cf30922aa5f6",
|
||||
"sha256:5f44ba7c07e0aa4a7a2723b426c254e952da82a33d65b4a52afae4bef74a4203",
|
||||
"sha256:62e5b942378d5f0b87caace567a70dc634ddd4d219a236fa221dc97d2fc412c8",
|
||||
"sha256:7c669be1b01e4b2bf23aa49e987d9bedde0234a7da374a9b77ca5416d7c57002",
|
||||
"sha256:7d47d666e17e57ef65fefc87229fde262bd5c9039ae8424bc53aa2d8f07dc178",
|
||||
"sha256:7e184aa18f921b612ea08666c25dd92a71241c8ed40917f2824219c92289b8c7",
|
||||
"sha256:80583c536e7e010e301002088919d4ea90566d1789ee02551574fdf3faa275ae",
|
||||
"sha256:8217f73faf08623acb25fb2affd5d20cbcd8185213db308e46a37e6fd6a56a49",
|
||||
"sha256:87d95eea58fb71f69b4f1c761099a19e0e9cb27d45dc1cc7042523128ee56337",
|
||||
"sha256:8bd466135fb07f693dbdd999a5569ffbc0590e9c64df859243162f0ebee950c8",
|
||||
"sha256:8e133ca2f8141b415ff396ba789bdeffdea8ff9a5c7fc9996ccf591d7d40ee93",
|
||||
"sha256:8e6c0ca447b557a32642f22d0987be37950eda51c4f19fc788cebc99426284b6",
|
||||
"sha256:9de96025ce25b9f4e744fbe558a003e673004af255da9b1f6ec243720ac5deeb",
|
||||
"sha256:a27a8dca0dc6f0944ed9fd83c556d862e227a5cd4220e62af5d4c750389938f0",
|
||||
"sha256:a2d4f68e4fa286fb6b00d58a1e87c79840e289d3f6e5dcb912ad7b0fbd9629fb",
|
||||
"sha256:a6e1c77ff6f10eab496fbbcdaa7dfae84968928a0aadc43ce3c3453cec29bd79",
|
||||
"sha256:a7b018811a0e1d3869d8d0600849953acd355a3a29c6bee0fbd24d7772bcc0a2",
|
||||
"sha256:a99b2f2dd1236e8d9dc35974a3dc298a408cdfd512b0bb2451798cff1ce63408",
|
||||
"sha256:ac1033942851bf01f28c76318155ea92d6648aecb924cab81fa23781d095e9ab",
|
||||
"sha256:b6936cd38757dd323fefc157823e46436610328f0feb1419a412316f24b77f36",
|
||||
"sha256:b6eab230b18458707b5c501548e997e42934b1c189fb4d1b78bf5aacc1c6a137",
|
||||
"sha256:bcb57d175ff0cb4ff97fc547c74c1cb8d4c9612003f6d267ee78dad8f23d8b30",
|
||||
"sha256:c1f02d016b9b6b5ad21949a21646714bfa7b32d6041a30f97674f05d6d6996e3",
|
||||
"sha256:c40aaf7930680e0e5f3bd6d3d3dc97a7897f53bdce925545c4b241e0c5c3ca6a",
|
||||
"sha256:c5e1874c601128cf54c1d4b471e915658a334fbc56d7b3c324ddc7511597ea82",
|
||||
"sha256:c8805673b1953313adfc487d5323b4c87864e77057944a0888c98dd2f7a6052f",
|
||||
"sha256:da458bdc9b0bcd9b8ca85bc73148631b18cc8ba03c47f29f4c017809990351aa",
|
||||
"sha256:dcb708ab06f3f4dfc99e9f84821c9120e5f12113b90fad132311a2cb97525379",
|
||||
"sha256:dfafc350f43fd7dc67df18c940c3b7ed208ebb797abe9fb3047f0c65be8e4c0f",
|
||||
"sha256:e8931af864bd599c6af626575a02103ae626f57b34e3af5537d40b040d33d2ad",
|
||||
"sha256:efa9d943189321f67f71070c309aa6f6130fa1ec35c9dfd0da0ed238938ce573",
|
||||
"sha256:fd22ee7bff4b5c37bb6385efee1c501b75e29ca40286f037cb91c2182d1348ce"
|
||||
"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.2"
|
||||
"version": "==7.1.0"
|
||||
},
|
||||
"dill": {
|
||||
"hashes": [
|
||||
@@ -296,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": [
|
||||
@@ -346,62 +411,67 @@
|
||||
},
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d",
|
||||
"sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6",
|
||||
"sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf",
|
||||
"sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f",
|
||||
"sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813",
|
||||
"sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33",
|
||||
"sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad",
|
||||
"sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05",
|
||||
"sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297",
|
||||
"sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06",
|
||||
"sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd",
|
||||
"sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243",
|
||||
"sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305",
|
||||
"sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476",
|
||||
"sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711",
|
||||
"sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70",
|
||||
"sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5",
|
||||
"sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461",
|
||||
"sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab",
|
||||
"sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c",
|
||||
"sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d",
|
||||
"sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135",
|
||||
"sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93",
|
||||
"sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648",
|
||||
"sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a",
|
||||
"sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb",
|
||||
"sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3",
|
||||
"sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372",
|
||||
"sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb",
|
||||
"sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"
|
||||
"sha256:01b1b9e1ed40544ef486fa8ac022232ccc57109f379611633ede8e71630d07d2",
|
||||
"sha256:0ab090d9240d6b4e99e1fa998c2d0aa5b29fc0fb06bd30e7ad6183c95fa07593",
|
||||
"sha256:14d776869a3e6c89c17eb943100f7868f677703c8a4e00b3803918f86aafbc52",
|
||||
"sha256:1ace23f6bb4aec4604b86c4843276e8fa548d667dbbd0cb83a3ae14b18b2db6c",
|
||||
"sha256:2efa963bdddb27cb4a0d42545cd137a8d2b883bd181bbc4525b568ef6eca258f",
|
||||
"sha256:2f6ac8c87e046dc18c7d1d7f6653a66787a4555085b056fe2d599f1f1a2a2d21",
|
||||
"sha256:3ae4c7a99e5153496243146a3baf33b9beff714464ca386b5f62daad601d87af",
|
||||
"sha256:3cfad08f16a9c6611e6143485a93de0e1e13f48cfb90bcad7d5fde1c0cec3d36",
|
||||
"sha256:4e5175026618c178dfba6188228b845b64131034ab3ba52acaffa8f6c361f805",
|
||||
"sha256:50979d5efff8d4135d9db293c6cb2c42260e70fb010cbc697b1311a4d7a39ddb",
|
||||
"sha256:5cd187d92b6939617f1168a4fe68f68add749902c010e66fe574c165c742ed88",
|
||||
"sha256:5cfca124f0ac6707747544c127880893ad72a656e136adc935c8600740b21ff5",
|
||||
"sha256:5e398652d005a198a7f3c132426b33c6b85d98aa7dc852137a2a3be8890c4072",
|
||||
"sha256:67cced7f15654710386e5c10b96608f1ee3d5c94ca1da5a2aad5889793a824c1",
|
||||
"sha256:7306edca1c6f1b5fa0bc9aa645e6ac8393014fa82d0fa180d0ebc990ebe15964",
|
||||
"sha256:7cc2c01dfc5a3cbddfa6c13f530ef3b95292f926329929001d45e124342cd6b7",
|
||||
"sha256:87edfaf344c9401942883fad030909116aa77b0fa7e6e8e1c5407e14549afe9a",
|
||||
"sha256:8845125d0b7c57838a10fd8925b0f5f709d0e08568ce587cc862aacce453e3dd",
|
||||
"sha256:92024447a339400ea00ac228369cd242e988dd775640755fa4ac0c126e49bb74",
|
||||
"sha256:a86b794e8a56ada65c573183756eac8ac5b8d3d59daf9d5ebd72ecdbb7867a43",
|
||||
"sha256:bb2782a036d9eb6b5a6efcdda0986774bf798beef86a62da86cb73e2a10b423d",
|
||||
"sha256:be78077064d016bc1b639c2cbcc5be945b47b4261a4f4b7d8923f6c69c5c9457",
|
||||
"sha256:c7cf862aef988b5fbaa17764ad1d21b4831436701c7d2b653156a9497d92c83c",
|
||||
"sha256:e0626db16705ab9f7fa6c249c017c887baf20738ce7f9129da162bb3075fc1af",
|
||||
"sha256:f34495079c8d9da05b183f9f7daec2878280c2ad7cc81da686ef0b484cea2ecf",
|
||||
"sha256:fe523fcbd52c05040c7bee370d66fee8373c5972171e4fbc323153433198592d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.991"
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"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": [
|
||||
"sha256:7d75a079651b105076776a35a5405e3fa773b8a167118f1b712e443e9a6c18a2",
|
||||
"sha256:da33dfde7547ff57e5ba5564126cbfa114f14413b2fa50759b1fa5de1e4ab511"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==3.4.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
|
||||
"sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"
|
||||
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
|
||||
"sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==22.0"
|
||||
"version": "==23.0"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490",
|
||||
"sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"
|
||||
"sha256:8a1228abb1ef82d788f74139988b137e78692984ec7b08eaa6c65f1723af28f9",
|
||||
"sha256:b1d5eb14f221506f50d6604a561f4c5786d9e80355219694a1b244bcd96f4567"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.6.2"
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
@@ -411,6 +481,26 @@
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30",
|
||||
"sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b",
|
||||
"sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc",
|
||||
"sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791",
|
||||
"sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717",
|
||||
"sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec",
|
||||
"sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7",
|
||||
"sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab",
|
||||
"sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2",
|
||||
"sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5",
|
||||
"sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1",
|
||||
"sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462",
|
||||
"sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97",
|
||||
"sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.21.12"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
|
||||
@@ -429,19 +519,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": [
|
||||
@@ -459,6 +557,22 @@
|
||||
"index": "pypi",
|
||||
"version": "==3.10.0"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:16ccf598aab3b506593c17378473978908a2734d7336755a8769b480906bec1c",
|
||||
"sha256:b440ee5f7e607bb8c9de15259dba2583dd41a38879a7abc1d43a71c59524da48"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==67.2.0"
|
||||
},
|
||||
"setuptools-git-versioning": {
|
||||
"hashes": [
|
||||
"sha256:648481f7e1e9e12ccd2b069d616b909a338b4223956319649351751cbc0207f4",
|
||||
"sha256:fde1a7cb3b2566979e5651cfca0d33cd5a82771711cd38a056216391936cf0ff"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.13.1"
|
||||
},
|
||||
"tomlkit": {
|
||||
"hashes": [
|
||||
"sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b",
|
||||
@@ -469,11 +583,11 @@
|
||||
},
|
||||
"types-protobuf": {
|
||||
"hashes": [
|
||||
"sha256:7df483d34ad3fcb1fa7fff1073560d596c9ac1f419cfa851b220c9a93386c998",
|
||||
"sha256:aeefcf39d637016998b3c7b699750847071b555f7c2e0c9873d42ab6103d1a39"
|
||||
"sha256:46ffa6647e2f8d53a4828e905f8fb0e8ff8c918309b425572dd34ab4d0b48553",
|
||||
"sha256:819a7c67e69476e39c3f0c9871bbb9ee82313645d317b6daeb60ac95a309dbd3"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.21.0.2"
|
||||
"version": "==4.21.0.5"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
|
||||
394
README.md
@@ -1,31 +1,185 @@
|
||||
# Extract TOTP/HOTP secrets from QR codes exported by two-factor authentication apps
|
||||
|
||||
[](https://github.com/scito/extract_otp_secrets/actions/workflows/ci.yml)
|
||||

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

|
||||
[](https://github.com/scito/extract_otp_secrets/blob/master/Pipfile.lock)
|
||||

|
||||

|
||||
[](https://github.com/scito/extract_otp_secrets/blob/master/LICENSE)
|
||||
[](https://github.com/scito/extract_otp_secrets/tags)
|
||||
[](https://github.com/scito/extract_otp_secrets/releases/latest)
|
||||

|
||||
[](https://hub.docker.com/repository/docker/scit0/extract_otp_secrets/general)
|
||||
[](https://github.com/scito/extract_otp_secrets/releases/latest)
|
||||
[](https://github.com/scito/extract_otp_secrets/releases/latest)
|
||||
[](https://github.com/scito/extract_otp_secrets/releases/latest)
|
||||
[](https://github.com/scito/extract_otp_secrets/releases/latest)
|
||||
[](https://stand-with-ukraine.pp.ua)
|
||||
<!-- 
|
||||
[](https://github.com/scito/extract_otp_secrets/blob/master/Pipfile.lock)
|
||||
-->
|
||||
|
||||
<!-- [](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, _(new!)_
|
||||
2. Read image files containing the QR codes, and _(new!)_
|
||||
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.
|
||||
|
||||
**The project and the script were renamed from extract_otp_secret_keys to extract_otp_secrets in version 2.0.0.**
|
||||
⚡ **This project/script was renamed from `extract_otp_secret_keys` to `extract_otp_secrets`.** ⚡
|
||||
|
||||
## Installation
|
||||
<details>
|
||||
<summary>Table of contents</summary>
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Download and run binary executable (🆕 since v2.1)](#download-and-run-binary-executable--since-v21)
|
||||
- [MacOS application](#macos-application)
|
||||
- [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 from 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 and run 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. Linux and macOS: Set executable bit for the downloaded file, e.g in terminal with `chmod +x extract_otp_secrets_OS_vX.X.X`
|
||||
3. Start executable by clicking or from command line (macOS: startable only from command line, see [below](#macOS-application))
|
||||
|
||||
: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.
|
||||
|
||||
### MacOS application
|
||||
|
||||
> Beginning in macOS 10.15, all software built after June 1, 2019, and distributed with Developer ID must be notarized. However, you aren’t required to notarize software that you distribute through the Mac App Store because the App Store submission process already includes equivalent security checks. <small>[developer.apple.com](https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution)</small>
|
||||
|
||||
:x: Unfortunately, I cannot provide a signed and notarized installable application for macOS as .dmg or .pkg. Apple is not Open Source friendly and requires a yearly Developer ID subscription. I am not willing to pay [USD 99 per year](https://developer.apple.com/support/compare-memberships/) to Apple for this little open source tool.
|
||||
|
||||
However, the bare executable can be executed from the command line:
|
||||
|
||||
1. Download executable for macOS platform from [latest release](https://github.com/scito/extract_otp_secrets/releases/latest), see assets
|
||||
2. Open Terminal application
|
||||
3. Change to Downloads: `cd $HOME/Downloads`
|
||||
4. Set executable bit for the downloaded file: `chmod +x extract_otp_secrets_OS_vX.X.X`
|
||||
5. Start executable from command line: `./extract_otp_secrets_OS_vX.X.X`
|
||||
|
||||
## Usage
|
||||
|
||||
### Capture QR codes from camera (🆕 since version 2.0)
|
||||
|
||||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app (see [how to export](#how-to-export-otp-secrets-from-google-authenticator-app))
|
||||
3. Point the exported QR codes to the camera of your computer
|
||||
4. Run the program without infile parameters:
|
||||
```
|
||||
extract_otp_secrets
|
||||
```
|
||||
|
||||

|
||||
|
||||
Detected QR codes are surrounded with a frame. The color of the frame indicates the extracting result:
|
||||
|
||||
* Green: The QR code is detected, decoded and the OTP secret was successfully extracted.
|
||||
* Red: The QR code is detected and decoded, but could not be successfully extracted. This is the case if a QR code not containing OTP data is captured.
|
||||
* Magenta: The QR code is detected, but could not be decoded. The QR code should be presented better to the camera or another QR reader could be used.
|
||||
|
||||
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-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-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=…`)
|
||||
5. Transfer the file to the computer where his script is installed.
|
||||
6. Call this script with the file as input:
|
||||
```
|
||||
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
|
||||
@@ -35,14 +189,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
|
||||
|
||||
@@ -54,6 +208,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
|
||||
@@ -64,47 +222,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/)
|
||||
|
||||
## Usage
|
||||
|
||||
### Capture QR codes from camera (since version 2.0.0)
|
||||
|
||||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app
|
||||
3. Point the QR codes to the camera of your computer
|
||||
4. Call this script without infile parameters:
|
||||
|
||||
python src/extract_otp_secrets.py
|
||||
|
||||

|
||||
|
||||
### With builtin QR decoder from image files (since version 2.0.0)
|
||||
|
||||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app
|
||||
4. Save the QR code as image file, e.g. example_export.png
|
||||
5. Transfer the images files to the computer where his script is installed.
|
||||
6. Call this script with the file as input:
|
||||
|
||||
python src/extract_otp_secrets.py example_export.png
|
||||
|
||||
### With external QR decoder app from text files
|
||||
|
||||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app
|
||||
3. Read QR codes with a QR code reader (e.g. from another phone)
|
||||
4. Save the captured QR codes in the QR code reader to a text file, e.g. example_export.txt. Save each QR code on a new line. (The captured QR codes look like `otpauth-migration://offline?data=...`)
|
||||
5. Transfer the file to the computer where his script is installed.
|
||||
6. Call this script with the file as input:
|
||||
|
||||
python src/extract_otp_secrets.py example_export.txt
|
||||
|
||||
## Program help: arguments and options
|
||||
|
||||
<pre>usage: extract_otp_secrets.py [-h] [--csv FILE] [--keepass FILE] [--json FILE] [--printqr] [--saveqr DIR] [--camera NUMBER] [--qr {ZBAR,QREADER,QREADER_DEEP,CV2,CV2_WECHAT}] [-i] [--no-color] [-d | -v | -q] [infile ...]
|
||||
<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.
|
||||
@@ -125,6 +249,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 -
|
||||
@@ -146,6 +271,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
|
||||
@@ -177,13 +310,17 @@ 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)
|
||||
* 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
|
||||
* 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) [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)
|
||||
* Supports TOTP and HOTP standards
|
||||
* 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:
|
||||
* CSV
|
||||
@@ -191,7 +328,8 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
||||
* Dedicated CSV for KeePass
|
||||
* QR code images
|
||||
* Supports reading from stdin and writing to stdout, thus pipes can be used
|
||||
* Reads QR codes images: (See [OpenCV docu](https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56))
|
||||
* Handles multiple input files (🆕 since v2.0)
|
||||
* Reads QR codes images: (See [OpenCV docu](https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56)) (🆕 since v2.0)
|
||||
* Portable Network Graphics - *.png
|
||||
* WebP - *.webp
|
||||
* JPEG files - *.jpeg, *.jpg, *.jpe
|
||||
@@ -199,8 +337,16 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
||||
* Windows bitmaps - *.bmp, *.dib
|
||||
* JPEG 2000 files - *.jp2
|
||||
* Portable image format - *.pbm, *.pgm, *.ppm *.pxm, *.pnm
|
||||
* Prints errors and warnings to stderr
|
||||
* Prints colored output
|
||||
* 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 (optional [libzbar](#installation-of-optional-shared-system-libraries-recommended) needs to be installed manually if needed)
|
||||
* extract_otp_secrets_macos_x86_64.dmg N/A, see [why](#macos-application)
|
||||
* extract_otp_secrets_macos_x86_64.pkg N/A, see [why](#macos-application)
|
||||
* Prebuilt Docker images provided for amd64 and arm64 (🆕 since v2.0)
|
||||
* Many ways to run the script:
|
||||
* Native Python
|
||||
* pipenv
|
||||
@@ -209,13 +355,13 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
||||
* Docker
|
||||
* VSCode devcontainer
|
||||
* devbox
|
||||
* Prebuilt Docker images provided for amd64 and arm64
|
||||
* 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
|
||||
@@ -259,23 +405,18 @@ Import CSV with HOTP entries in KeePass as
|
||||
|
||||
KeePass can be used as a backup for one time passwords (second factor) from the mobile phone.
|
||||
|
||||
## Technical background
|
||||
## How to export otp secrets from Google Authenticator app
|
||||
|
||||
The export QR code of "Google Authenticator" contains the URL `otpauth-migration://offline?data=...`.
|
||||
The data parameter is a base64 encoded proto3 message (Google Protocol Buffers).
|
||||
|
||||
Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition or new protobuf versions):
|
||||
|
||||
protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python src/google_auth.proto
|
||||
|
||||
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
|
||||
|
||||
For Python type hint generation the [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) package is used.
|
||||
|
||||
## References
|
||||
|
||||
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
||||
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
|
||||
1. Open "Google Authenticator" app
|
||||
2. Select "Transfer accounts" in the three dot menu of the app.
|
||||

|
||||
3. Select "Export accounts"
|
||||

|
||||
4. Pass the verification by password or fingerprint.
|
||||
5. Select your accounts
|
||||
6. Press "Next" button
|
||||
7. The exported QR code(s) ready for extraction are shown.
|
||||

|
||||
|
||||
## Glossary
|
||||
|
||||
@@ -288,10 +429,16 @@ For Python type hint generation the [mypy-protobuf](https://github.com/nipunn131
|
||||
|
||||
## 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
|
||||
```
|
||||
|
||||
@@ -299,7 +446,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 -
|
||||
```
|
||||
|
||||
@@ -308,7 +455,13 @@ curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/examp
|
||||
```
|
||||
git clone https://github.com/scito/extract_otp_secrets.git
|
||||
pip install -U -e extract_otp_secrets
|
||||
python -m extract_otp_secrets example_export.txt
|
||||
extract_otp_secrets extract_otp_secrets/example_export.txt
|
||||
```
|
||||
|
||||
or run it
|
||||
|
||||
```
|
||||
python -m extract_otp_secrets extract_otp_secrets/example_export.txt
|
||||
```
|
||||
|
||||
### pipenv
|
||||
@@ -364,11 +517,10 @@ Prebuilt docker images are available for amd64 and arm64 architectures on [Docke
|
||||
Extracting from an QR image file:
|
||||
|
||||
```
|
||||
docker login -u USERNAME
|
||||
curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/example_export.png | docker run --pull always -i --rm -v "$(pwd)":/files:ro scit0/extract_otp_secrets =
|
||||
```
|
||||
|
||||
Capturing from camera in GUI (X Window system required on host):
|
||||
Capturing from camera in GUI window (X Window system required on host):
|
||||
|
||||
```
|
||||
docker run --pull always --rm -v "$(pwd)":/files:ro -i --device="/dev/video0:/dev/video0" --env="DISPLAY" -v /tmp/.X11-unix:/tmp/.X11-unix:ro scit0/extract_otp_secrets
|
||||
@@ -447,6 +599,7 @@ Setup for running the tests in VSCode.
|
||||
### Build
|
||||
|
||||
```
|
||||
cd extract_otp_secrets/
|
||||
pip install -U -e .
|
||||
python src/extract_otp_secrets.py
|
||||
|
||||
@@ -463,29 +616,97 @@ pip install -U -r requirements.txt
|
||||
|
||||
### Build docker images
|
||||
|
||||
#### Debian (full functionality)
|
||||
|
||||
Build and run the app within the container:
|
||||
|
||||
```bash
|
||||
docker build . -t extract_otp_secrets --pull --build-arg RUN_TESTS=false
|
||||
```
|
||||
|
||||
```bash
|
||||
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
|
||||
```
|
||||
|
||||
#### Alpine (only text file processing)
|
||||
|
||||
```bash
|
||||
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 extract_otp_secrets_test.py -k "not qreader" --relaxed
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
```bash
|
||||
./build.sh
|
||||
```
|
||||
|
||||
The options of the build script:
|
||||
|
||||
```
|
||||
Build extract_otp_secrets project
|
||||
|
||||
./build.sh [options]
|
||||
|
||||
Options:
|
||||
-i Interactive mode, all steps must be confirmed
|
||||
-C Ignore version check of protobuf/protoc
|
||||
-D Do not build docker
|
||||
-G Do not start extract_otp_secrets.py in GUI mode
|
||||
-c Clean everything
|
||||
-r Generate result files
|
||||
-h, --help Help
|
||||
```
|
||||
|
||||
## Technical background
|
||||
|
||||
The export QR code of "Google Authenticator" contains the URL `otpauth-migration://offline?data=…`.
|
||||
The data parameter is a base64 encoded proto3 message (Google Protocol Buffers).
|
||||
|
||||
Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition or new protobuf versions):
|
||||
|
||||
protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python src/google_auth.proto
|
||||
|
||||
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
|
||||
|
||||
For Python type hint generation the [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) package is used.
|
||||
|
||||
## References
|
||||
|
||||
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
||||
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
|
||||
|
||||
## Issues
|
||||
|
||||
* Known issue for macOS: https://github.com/opencv/opencv/issues/23072
|
||||
* Segmentation fault on macOS with CV2 4.7.0: https://github.com/opencv/opencv/issues/23072
|
||||
* CV2 window does not show icons: https://github.com/opencv/opencv-python/issues/585
|
||||
|
||||
## Problems and Troubleshooting
|
||||
|
||||
@@ -518,10 +739,11 @@ FileNotFoundError: Could not find module 'libiconv.dll' (or one of its dependenc
|
||||
|
||||
* [ZBar](https://github.com/mchehab/zbar) is an open source software suite for reading bar codes from various sources, including webcams.
|
||||
* [Aegis Authenticator](https://github.com/beemdevelopment/Aegis) is a free, secure and open source 2FA app for Android.
|
||||
* [Android OTP Extractor](https://github.com/puddly/android-otp-extractor) can extract your tokens from popular Android OTP apps and export them in a standard format or just display them as QR codes for easy importing. [Requires a _rooted_ Android phone.]
|
||||
* [Python QReader](https://github.com/Eric-Canas/QReader)
|
||||
* [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar)
|
||||
* [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) is a good QR code reader Python module
|
||||
* [OpenCV](https://docs.opencv.org/4.x/) (CV2) Open Source Computer Vision library with [opencv-python](https://github.com/opencv/opencv-python)
|
||||
* [Python QReader](https://github.com/Eric-Canas/QReader) Python QR code readers
|
||||
* [Android OTP Extractor](https://github.com/puddly/android-otp-extractor) can extract your tokens from popular Android OTP apps and export them in a standard format or just display them as QR codes for easy importing. [Requires a _rooted_ Android phone.]
|
||||
* [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.
|
||||
|
||||
***
|
||||
|
||||
|
||||
233
build.sh
@@ -73,31 +73,28 @@ askContinueYn() {
|
||||
|
||||
# Reference: https://gist.github.com/steinwaywhw/a4cd19cda655b8249d908261a62687f8
|
||||
|
||||
echo "Checking Protoc version..."
|
||||
VERSION=$(curl -sL https://github.com/protocolbuffers/protobuf/releases/latest | grep -E "<title>" | perl -pe's%.*Protocol Buffers v(\d+\.\d+(\.\d+)?).*%\1%')
|
||||
BASEVERSION=4
|
||||
echo
|
||||
|
||||
interactive=false
|
||||
ignore_version_check=true
|
||||
clean=false
|
||||
clean_flag=""
|
||||
build_docker=true
|
||||
run_gui=true
|
||||
generate_result_files=false
|
||||
PYTHONHASHSEED=31
|
||||
|
||||
while test $# -gt 0; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
echo "Upgrade Protoc"
|
||||
echo "Build extract_otp_secrets project"
|
||||
echo
|
||||
echo "$0 [options]"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo "-i Interactive"
|
||||
echo "-C Ignore version check"
|
||||
echo "-D No docker build"
|
||||
echo "-G No not run gui"
|
||||
echo "-c Clean"
|
||||
echo "-i Interactive mode, all steps must be confirmed"
|
||||
echo "-C Ignore version check of protobuf/protoc"
|
||||
echo "-D Do not build docker"
|
||||
echo "-G Do not start extract_otp_secrets.py in GUI mode"
|
||||
echo "-c Clean everything"
|
||||
echo "-r Generate result files"
|
||||
echo "-h, --help Help"
|
||||
quit
|
||||
@@ -124,6 +121,7 @@ while test $# -gt 0; do
|
||||
;;
|
||||
-c)
|
||||
clean=true
|
||||
clean_flag="--clean"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
@@ -138,16 +136,34 @@ PIPENV="$PYTHON -m pipenv"
|
||||
FLAKE8="$PYTHON -m flake8"
|
||||
MYPY="$PYTHON -m mypy"
|
||||
|
||||
# sudo ln -s /usr/bin/python3.11 /usr/bin/python
|
||||
|
||||
# Upgrade protoc
|
||||
|
||||
DEST="protoc"
|
||||
|
||||
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
|
||||
echo -e "\nProtoc remote version $VERSION\n"
|
||||
echo -e "Protoc local version: $OLDVERSION\n"
|
||||
|
||||
if $clean; then
|
||||
cmd="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="docker image prune -f || echo 'No docker image pruned'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP uninstall -y extract-otp-secrets || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze | grep -v -E '^-e|^#' | xargs sudo $PIP uninstall -y || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze --user | grep -v -E '^-e|^#' | xargs $PIP uninstall -y || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze | cut -d \"@\" -f1 | xargs pip uninstall -y || echo Nothing to do"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="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"
|
||||
|
||||
@@ -160,6 +176,20 @@ if $clean; then
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
|
||||
echo -e "\n\nChecking Protoc version..."
|
||||
VERSION=$(curl -sL https://github.com/protocolbuffers/protobuf/releases/latest | grep -E "<title>" | perl -pe's%.*Protocol Buffers v(\d+\.\d+(\.\d+)?).*%\1%')
|
||||
BASEVERSION=4
|
||||
echo
|
||||
|
||||
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
|
||||
echo -e "\nProtoc remote version $VERSION\n"
|
||||
echo -e "Protoc local version: $OLDVERSION\n"
|
||||
|
||||
if [ "$OLDVERSION" != "$VERSION" ] || ! $ignore_version_check; then
|
||||
echo "Upgrade protoc from $OLDVERSION to $VERSION"
|
||||
|
||||
@@ -216,7 +246,7 @@ fi
|
||||
|
||||
# Upgrade pip requirements
|
||||
|
||||
cmd="sudo pip install -U pip"
|
||||
cmd="pip install -U pip"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
@@ -226,10 +256,6 @@ cmd="$PIP install --use-pep517 -U -r requirements.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Lint
|
||||
|
||||
LINT_OUT_FILE="tests/reports/flake8_results.txt"
|
||||
@@ -254,7 +280,30 @@ 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"
|
||||
|
||||
# Test
|
||||
|
||||
# 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 ."
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="extract_otp_secrets example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="extract_otp_secrets - < example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Test (needs module)
|
||||
|
||||
cmd="$PYTHON src/extract_otp_secrets.py example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
@@ -287,47 +336,32 @@ cmd="$PIPENV run pytest --cov=extract_otp_secrets_test tests/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# sudo pip
|
||||
|
||||
cmd="sudo $PIP install --use-pep517 -U -r requirements.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="sudo $PIP install --use-pep517 -U -r requirements-dev.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="sudo $PIP install -U pipenv"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# pip -e install (must be after other pip installs in order to have this environment for development)
|
||||
|
||||
cmd="$PIP install -U -e ."
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="extract_otp_secrets example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="extract_otp_secrets - < example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build wheel
|
||||
|
||||
cmd="$PIP wheel .
|
||||
cmd="$PIP wheel ."
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# 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 --specpath installer --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
|
||||
|
||||
@@ -340,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"
|
||||
|
||||
@@ -352,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"
|
||||
|
||||
@@ -378,10 +411,81 @@ if $build_docker; then
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker image prune -f || echo 'No docker image pruned'"
|
||||
# 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 --specpath installer --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 --specpath installer --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 win_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))) COPYRIGHT_YEARS='2020-2023' envsubst < installer/win_file_version_info_template.txt > build/win_file_version_info.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# create macOS extract_otp_secrets_macos.spec from extract_otp_secrets_macos_template.spec
|
||||
cmd="VERSION_STR=$(setuptools-git-versioning) COPYRIGHT_YEARS='2020-2023' envsubst < installer/extract_otp_secrets_macos_template.spec > build/extract_otp_secrets_macos.spec"
|
||||
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 --specpath installer -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
|
||||
@@ -402,6 +506,7 @@ cmd="cat $TYPE_CHECK_OUT_FILE $LINT_OUT_FILE $COVERAGE_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
echo -e "\n${greenBold}SUCCESS${reset}"
|
||||
line=$(printf '#%.0s' $(eval echo {1..$(( ($COLUMNS - 10) / 2))}))
|
||||
echo -e "\n${greenBold}$line SUCCESS $line${reset}"
|
||||
|
||||
quit
|
||||
|
||||
4
docker/.alias
Normal file
@@ -0,0 +1,4 @@
|
||||
alias ll='ls -lh'
|
||||
alias la='ls -lha'
|
||||
alias l='ls -alhF'
|
||||
alias ls-l='ls -lh'
|
||||
BIN
docs/Export-account-option-in-the-Google-Authenticator.webp
Normal file
|
After Width: | Height: | Size: 6.5 KiB |
|
After Width: | Height: | Size: 7.9 KiB |
BIN
docs/Exported-QR-codes.webp
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/Exported-QR-codes_300px.webp
Normal file
|
After Width: | Height: | Size: 38 KiB |
56
docs/README_TOC.md
Normal file
@@ -0,0 +1,56 @@
|
||||
Generate from file: README.md
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Download and run binary executable (🆕 since v2.1)](#download-and-run-binary-executable--since-v21)
|
||||
- [MacOS application](#macos-application)
|
||||
- [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.
|
||||
BIN
docs/Transfer-accounts-option-in-the-Google-Authenticator.webp
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 528 KiB |
22
installer/build_dmg.sh
Executable file
@@ -0,0 +1,22 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Create a folder (named dmg) to prepare our DMG in (if it doesn't already exist).
|
||||
|
||||
# https://www.pythonguis.com/tutorials/packaging-pyqt5-applications-pyinstaller-macos-dmg/
|
||||
|
||||
mkdir -p dist/dmg
|
||||
# Empty the dmg folder.
|
||||
rm -r dist/dmg/*
|
||||
# Copy the app bundle to the dmg folder.
|
||||
cp -r "dist/extract_otp_secrets.app" dist/dmg
|
||||
# If the DMG already exists, delete it.
|
||||
test -f "dist/extract_otp_secrets.dmg" && rm "dist/extract_otp_secrets.dmg"
|
||||
create-dmg \
|
||||
--volname "Extract OTP Secrets" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 600 300 \
|
||||
--icon-size 100 \
|
||||
--hide-extension "extract_otp_secrets.app" \
|
||||
--app-drop-link 425 120 \
|
||||
"dist/extract_otp_secrets.dmg" \
|
||||
"dist/dmg/"
|
||||
111
installer/extract_otp_secrets_macos_template.spec
Normal file
@@ -0,0 +1,111 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
# https://www.pythonguis.com/tutorials/packaging-pyqt5-applications-pyinstaller-macos-dmg/
|
||||
# https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['src/extract_otp_secrets.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('$macos_python_path/__yolo_v3_qr_detector/', '__yolo_v3_qr_detector/')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='extract_otp_secrets',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=True,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
app = BUNDLE(
|
||||
exe,
|
||||
name='extract_otp_secrets.app',
|
||||
icon=None,
|
||||
bundle_identifier='ch.scito.tools.extract_otp_secrets',
|
||||
version='$VERSION_STR',
|
||||
info_plist={
|
||||
'NSPrincipalClass': 'NSApplication',
|
||||
'NSAppleScriptEnabled': False,
|
||||
'NSHumanReadableCopyright': 'Copyright © $COPYRIGHT_YEARS Scito.',
|
||||
'CFBundleDocumentTypes': [
|
||||
# Reference: https://chromium.googlesource.com/chromium/src/+/lkgr/chrome/app/app-Info.plist
|
||||
# https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html#//apple_ref/doc/uid/TP40001319-CH202-SW6
|
||||
# https://developer.apple.com/documentation/uniformtypeidentifiers/system-declared_uniform_type_identifiers
|
||||
{
|
||||
'CFBundleTypeName': 'GIF image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['com.compuserve.gif'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'JPEG image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['public.jpeg'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'PNG image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['public.png'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'WebP image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['org.webmproject.webp'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'Tiff image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['public.tiff'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'Bmp image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['com.microsoft.bmp'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'Plain text document',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['public.plain-text'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
||||
46
installer/win_file_version_info_template.txt
Normal 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])])
|
||||
]
|
||||
)
|
||||
@@ -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,19 +27,26 @@ 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 = [
|
||||
"protobuf",
|
||||
"qrcode",
|
||||
"Pillow",
|
||||
"qreader",
|
||||
"pyzbar",
|
||||
"opencv-contrib-python<=4.7.0; sys_platform == 'darwin'",
|
||||
"opencv-contrib-python; sys_platform != 'darwin'",
|
||||
"typing_extensions; python_version<='3.7'",
|
||||
"colorama>=0.4.6",
|
||||
"opencv-contrib-python; sys_platform != 'darwin'",
|
||||
"opencv-contrib-python<=4.7.0; sys_platform == 'darwin'",
|
||||
"Pillow",
|
||||
"protobuf",
|
||||
"pyzbar",
|
||||
"qrcode",
|
||||
"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"]
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
build
|
||||
flake8
|
||||
gfm-toc
|
||||
mypy
|
||||
types-protobuf
|
||||
mypy-protobuf
|
||||
pyinstaller
|
||||
pylint
|
||||
pytest
|
||||
pytest-mock
|
||||
pytest-cov
|
||||
pytest-mock
|
||||
setuptools
|
||||
setuptools-git-versioning
|
||||
types-protobuf
|
||||
wheel
|
||||
build
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
protobuf
|
||||
qrcode
|
||||
Pillow
|
||||
qreader
|
||||
opencv-contrib-python<=4.7.0; sys_platform == 'darwin'
|
||||
opencv-contrib-python; sys_platform != 'darwin'
|
||||
pyzbar
|
||||
typing_extensions; python_version<='3.7'
|
||||
colorama>=0.4.6
|
||||
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<2.0.0
|
||||
typing_extensions; python_version<='3.7'
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,47 +176,138 @@ 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
|
||||
def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.MigrationPayload]:
|
||||
'''Extracts the otp migration payload from an otp url. This function is the core of the this appliation.'''
|
||||
if not is_opt_url(otp_url, source):
|
||||
return None
|
||||
parsed_url = urlparse.urlparse(otp_url)
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"parsed_url={parsed_url}")
|
||||
try:
|
||||
params = urlparse.parse_qs(parsed_url.query, strict_parsing=True)
|
||||
except Exception: # workaround for PYTHON <= 3.10
|
||||
params = {}
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"querystring params={params}")
|
||||
if 'data' not in params:
|
||||
log_error(f"could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}")
|
||||
return None
|
||||
data_base64 = params['data'][0]
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64={data_base64}")
|
||||
data_base64_fixed = data_base64.replace(' ', '+')
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64_fixed={data_base64_fixed}")
|
||||
data = base64.b64decode(data_base64_fixed, validate=True)
|
||||
payload = pb.MigrationPayload()
|
||||
try:
|
||||
payload.ParseFromString(data)
|
||||
except Exception as e:
|
||||
abort(f"Cannot decode otpauth-migration migration payload.\n"
|
||||
f"data={data_base64}", e)
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"\n{i}. Payload Line", payload, sep='\n')
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: int, infile: str, args: Args) -> int:
|
||||
'''Converts the otp migration payload into a normal Python dictionary. This function is the core of the this appliation.'''
|
||||
payload = get_payload_from_otp_url(otpauth_migration_url, urls_count, infile)
|
||||
|
||||
if not payload:
|
||||
return 0
|
||||
|
||||
new_otps_count = 0
|
||||
# pylint: disable=no-member
|
||||
for raw_otp in payload.otp_parameters:
|
||||
if verbose: print(f"\n{len(otps) + 1}. Secret")
|
||||
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
||||
if verbose >= LogLevel.DEBUG: log_debug('OTP enum type:', get_enum_name_by_number(raw_otp, 'type'))
|
||||
otp_type = get_otp_type_str_from_code(raw_otp.type)
|
||||
otp_url = build_otp_url(secret, raw_otp)
|
||||
otp: Otp = {
|
||||
"name": raw_otp.name,
|
||||
"secret": secret,
|
||||
"issuer": raw_otp.issuer,
|
||||
"type": otp_type,
|
||||
"counter": raw_otp.counter if raw_otp.type == 1 else None,
|
||||
"url": otp_url
|
||||
}
|
||||
if otp not in otps or not args.ignore:
|
||||
otps.append(otp)
|
||||
new_otps_count += 1
|
||||
if not quiet:
|
||||
print_otp(otp)
|
||||
if args.printqr:
|
||||
print_qr(args, otp_url)
|
||||
if args.saveqr:
|
||||
save_qr(otp, args, len(otps))
|
||||
if not quiet:
|
||||
print()
|
||||
elif args.ignore and not quiet:
|
||||
eprint(f"Ignored duplicate otp: {otp['name']}", f" / {otp['issuer']}\n" if otp['issuer'] else '\n', sep='')
|
||||
|
||||
return new_otps_count
|
||||
|
||||
|
||||
def parse_args(sys_args: list[str]) -> Args:
|
||||
global verbose, quiet, colored
|
||||
|
||||
# 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')
|
||||
@@ -205,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")
|
||||
|
||||
@@ -220,6 +337,77 @@ def extract_otps(args: Args) -> Otps:
|
||||
return extract_otps_from_files(args)
|
||||
|
||||
|
||||
def extract_otps_from_camera(args: Args) -> Otps:
|
||||
if verbose: print("Capture QR codes from camera")
|
||||
otp_urls: OtpUrls = []
|
||||
otps: Otps = []
|
||||
|
||||
qr_mode = QRMode[args.qr]
|
||||
|
||||
cam = cv2.VideoCapture(args.camera)
|
||||
cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_AUTOSIZE)
|
||||
|
||||
if zbar_available:
|
||||
qreader = QReader()
|
||||
cv2_qr = cv2.QRCodeDetector()
|
||||
cv2_qr_wechat = cv2.wechat_qrcode.WeChatQRCode()
|
||||
while True:
|
||||
success, img = cam.read()
|
||||
new_otps_count = 0
|
||||
if not success:
|
||||
log_error("Failed to capture image from camera")
|
||||
break
|
||||
try:
|
||||
if qr_mode in [QRMode.QREADER, QRMode.QREADER_DEEP]:
|
||||
found, bbox = qreader.detect(img)
|
||||
if qr_mode == QRMode.QREADER_DEEP:
|
||||
otp_url = qreader.detect_and_decode(img, True)
|
||||
elif qr_mode == QRMode.QREADER:
|
||||
otp_url = qreader.decode(img, bbox) if found else None
|
||||
if otp_url:
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
if found:
|
||||
cv2_draw_box(img, [(bbox[0], bbox[1]), (bbox[2], bbox[1]), (bbox[2], bbox[3]), (bbox[0], bbox[3])], get_color(new_otps_count, otp_url))
|
||||
elif qr_mode == QRMode.ZBAR:
|
||||
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))
|
||||
elif qr_mode in [QRMode.CV2, QRMode.CV2_WECHAT]:
|
||||
if QRMode.CV2:
|
||||
otp_url, raw_pts, _ = cv2_qr.detectAndDecode(img)
|
||||
else:
|
||||
otp_url, raw_pts = cv2_qr_wechat.detectAndDecode(img)
|
||||
if raw_pts is not None:
|
||||
if otp_url:
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
cv2_draw_box(img, raw_pts, get_color(new_otps_count, otp_url))
|
||||
else:
|
||||
abort(f"Invalid QReader mode: {qr_mode.name}")
|
||||
except Exception as e:
|
||||
log_error(f'An error occured during QR detection and decoding for QR reader {qr_mode}. Changed to the next QR reader.', e)
|
||||
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, "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, otps)
|
||||
if quit:
|
||||
break
|
||||
|
||||
cam.release()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
return otps
|
||||
|
||||
|
||||
def get_color(new_otps_count: int, otp_url: str) -> ColorBGR:
|
||||
if new_otps_count:
|
||||
return SUCCESS_COLOR
|
||||
@@ -256,83 +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 extract_otps_from_camera(args: Args) -> Otps:
|
||||
if verbose: print("Capture QR codes from camera")
|
||||
otp_urls: OtpUrls = []
|
||||
otps: Otps = []
|
||||
|
||||
qr_mode = QRMode[args.qr]
|
||||
|
||||
cam = cv2.VideoCapture(args.camera)
|
||||
cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_AUTOSIZE)
|
||||
|
||||
qreader = QReader()
|
||||
cv2_qr = cv2.QRCodeDetector()
|
||||
cv2_qr_wechat = cv2.wechat_qrcode.WeChatQRCode()
|
||||
while True:
|
||||
success, img = cam.read()
|
||||
new_otps_count = 0
|
||||
if not success:
|
||||
log_error("Failed to capture image from camera")
|
||||
break
|
||||
try:
|
||||
if qr_mode in [QRMode.QREADER, QRMode.QREADER_DEEP]:
|
||||
found, bbox = qreader.detect(img)
|
||||
if qr_mode == QRMode.QREADER_DEEP:
|
||||
otp_url = qreader.detect_and_decode(img, True)
|
||||
elif qr_mode == QRMode.QREADER:
|
||||
otp_url = qreader.decode(img, bbox) if found else None
|
||||
if otp_url:
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
if found:
|
||||
cv2.rectangle(img, (bbox[0], bbox[1]), (bbox[2], bbox[3]), get_color(new_otps_count, otp_url), BOX_THICKNESS)
|
||||
elif qr_mode == QRMode.ZBAR:
|
||||
for qrcode in zbar.decode(img):
|
||||
otp_url = qrcode.data.decode('utf-8')
|
||||
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))
|
||||
elif qr_mode in [QRMode.CV2, QRMode.CV2_WECHAT]:
|
||||
if QRMode.CV2:
|
||||
otp_url, raw_pts, _ = cv2_qr.detectAndDecode(img)
|
||||
else:
|
||||
otp_url, raw_pts = cv2_qr_wechat.detectAndDecode(img)
|
||||
if raw_pts is not None:
|
||||
if otp_url:
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
cv2_draw_box(img, raw_pts, get_color(new_otps_count, otp_url))
|
||||
else:
|
||||
abort(f"Invalid QReader mode: {qr_mode.name}")
|
||||
except Exception as e:
|
||||
log_error(f'An error occured during QR detection and decoding for QR reader {qr_mode}. Changed to the next QR reader.', e)
|
||||
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"{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)
|
||||
if quit:
|
||||
break
|
||||
|
||||
cam.release()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
return otps
|
||||
|
||||
|
||||
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
|
||||
@@ -380,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 []
|
||||
@@ -414,45 +569,6 @@ def read_lines_from_text_file(filename: str) -> list[str]:
|
||||
return lines
|
||||
|
||||
|
||||
def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: int, infile: str, args: Args) -> int:
|
||||
payload = get_payload_from_otp_url(otpauth_migration_url, urls_count, infile)
|
||||
|
||||
if not payload:
|
||||
return 0
|
||||
|
||||
new_otps_count = 0
|
||||
# pylint: disable=no-member
|
||||
for raw_otp in payload.otp_parameters:
|
||||
if verbose: print(f"\n{len(otps) + 1}. Secret")
|
||||
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
||||
if verbose >= LogLevel.DEBUG: log_debug('OTP enum type:', get_enum_name_by_number(raw_otp, 'type'))
|
||||
otp_type = get_otp_type_str_from_code(raw_otp.type)
|
||||
otp_url = build_otp_url(secret, raw_otp)
|
||||
otp: Otp = {
|
||||
"name": raw_otp.name,
|
||||
"secret": secret,
|
||||
"issuer": raw_otp.issuer,
|
||||
"type": otp_type,
|
||||
"counter": raw_otp.counter if raw_otp.type == 1 else None,
|
||||
"url": otp_url
|
||||
}
|
||||
if otp not in otps or not args.ignore:
|
||||
otps.append(otp)
|
||||
new_otps_count += 1
|
||||
if not quiet:
|
||||
print_otp(otp)
|
||||
if args.printqr:
|
||||
print_qr(args, otp_url)
|
||||
if args.saveqr:
|
||||
save_qr(otp, args, len(otps))
|
||||
if not quiet:
|
||||
print()
|
||||
elif args.ignore and not quiet:
|
||||
eprint(f"Ignored duplicate otp: {otp['name']}", f" / {otp['issuer']}\n" if otp['issuer'] else '\n', sep='')
|
||||
|
||||
return new_otps_count
|
||||
|
||||
|
||||
def convert_img_to_otp_urls(filename: str, args: Args) -> OtpUrls:
|
||||
if verbose: print(f"Reading image {filename}")
|
||||
try:
|
||||
@@ -498,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}"
|
||||
@@ -506,37 +622,6 @@ def decode_qr_img_otp_urls(img: Any, qr_mode: QRMode) -> OtpUrls:
|
||||
return otp_urls
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: pb.MigrationPayload | None
|
||||
def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.MigrationPayload]:
|
||||
'''Extracts the otp migration payload from an otp url. This function is the core of the this appliation.'''
|
||||
if not is_opt_url(otp_url, source):
|
||||
return None
|
||||
parsed_url = urlparse.urlparse(otp_url)
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"parsed_url={parsed_url}")
|
||||
try:
|
||||
params = urlparse.parse_qs(parsed_url.query, strict_parsing=True)
|
||||
except Exception: # workaround for PYTHON <= 3.10
|
||||
params = {}
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"querystring params={params}")
|
||||
if 'data' not in params:
|
||||
log_error(f"could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}")
|
||||
return None
|
||||
data_base64 = params['data'][0]
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64={data_base64}")
|
||||
data_base64_fixed = data_base64.replace(' ', '+')
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64_fixed={data_base64_fixed}")
|
||||
data = base64.b64decode(data_base64_fixed, validate=True)
|
||||
payload = pb.MigrationPayload()
|
||||
try:
|
||||
payload.ParseFromString(data)
|
||||
except Exception as e:
|
||||
abort(f"Cannot decode otpauth-migration migration payload.\n"
|
||||
f"data={data_base64}", e)
|
||||
if verbose >= LogLevel.DEBUG: log_debug(f"\n{i}. Payload Line", payload, sep='\n')
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
def is_opt_url(otp_url: str, source: str) -> bool:
|
||||
if not otp_url.startswith('otpauth-migration://'):
|
||||
msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}"
|
||||
@@ -605,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:
|
||||
@@ -632,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:
|
||||
@@ -649,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:
|
||||
@@ -667,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:
|
||||
@@ -708,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('#')
|
||||
@@ -716,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))
|
||||
|
||||
@@ -726,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:
|
||||
|
||||
@@ -2,8 +2,6 @@ from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from extract_otp_secrets import QRMode
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
parser.addoption("--relaxed", action='store_true', help="run tests in relaxed mode")
|
||||
@@ -17,6 +15,7 @@ def relaxed(request: pytest.FixtureRequest) -> Any:
|
||||
|
||||
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
|
||||
if "qr_mode" in metafunc.fixturenames:
|
||||
number = 2 if metafunc.config.getoption("fast") else len(QRMode)
|
||||
qr_modes = [mode.name for mode in QRMode]
|
||||
all_qr_modes = ['ZBAR', 'QREADER', 'QREADER_DEEP', 'CV2', 'CV2_WECHAT']
|
||||
number = 2 if metafunc.config.getoption("fast") else len(all_qr_modes)
|
||||
qr_modes = [mode for mode in all_qr_modes]
|
||||
metafunc.parametrize("qr_mode", qr_modes[0:number])
|
||||
|
||||
3
tests/data/only_comments.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# comment 1
|
||||
|
||||
# comment 2
|
||||
@@ -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
|
||||
|
||||
@@ -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']
|
||||
[36mProcessing infile example_export.txt[39m
|
||||
Reading lines of example_export.txt
|
||||
|
||||
BIN
tests/data/qr_but_without_otp.png
Normal file
|
After Width: | Height: | Size: 478 B |
@@ -26,7 +26,8 @@ import pathlib
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
import colorama
|
||||
import pytest
|
||||
@@ -37,11 +38,18 @@ from utils import (count_files_in_dir, file_exits, read_binary_file_as_stream,
|
||||
|
||||
import extract_otp_secrets
|
||||
|
||||
qreader_available: bool = extract_otp_secrets.qreader_available
|
||||
try:
|
||||
import cv2 # type: ignore
|
||||
from extract_otp_secrets import SUCCESS_COLOR, FAILURE_COLOR, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE
|
||||
except ImportError:
|
||||
# ignore
|
||||
pass
|
||||
|
||||
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:
|
||||
@@ -99,8 +107,22 @@ def test_extract_stdin_empty(capsys: pytest.CaptureFixture[str], monkeypatch: py
|
||||
assert captured.err == '\nWARN: stdin is empty\n'
|
||||
|
||||
|
||||
def test_extract_stdin_only_comments(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
monkeypatch.setattr('sys.stdin', io.StringIO("\n\n# comment 1\n\n\n#comment 2"))
|
||||
|
||||
# Act
|
||||
extract_otp_secrets.main(['-n', '-'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
if qreader_available:
|
||||
if cv2_available:
|
||||
# Act
|
||||
with pytest.raises(SystemExit) as e:
|
||||
extract_otp_secrets.main(['-n', 'tests/data/empty_file.txt'])
|
||||
@@ -125,6 +147,17 @@ def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> No
|
||||
assert captured.out == ''
|
||||
|
||||
|
||||
def test_extract_only_comments_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['-n', 'tests/data/only_comments.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.err == ''
|
||||
assert captured.out == ''
|
||||
|
||||
|
||||
@pytest.mark.qreader
|
||||
def test_extract_stdin_img_empty(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
@@ -140,6 +173,24 @@ def test_extract_stdin_img_empty(capsys: pytest.CaptureFixture[str], monkeypatch
|
||||
assert captured.err == '\nWARN: stdin is empty\n'
|
||||
|
||||
|
||||
@pytest.mark.qreader
|
||||
def test_extract_stdin_img_garbage(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
monkeypatch.setattr('sys.stdin', io.BytesIO("garbage".encode('utf-8')))
|
||||
|
||||
# Act
|
||||
with pytest.raises(SystemExit) as e:
|
||||
extract_otp_secrets.main(['-n', '='])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == '\nERROR: Unable to open file for reading.\ninput file: =\n'
|
||||
assert e.type == SystemExit
|
||||
assert e.value.code == 1
|
||||
|
||||
|
||||
def test_extract_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
|
||||
# Arrange
|
||||
output_file = str(tmp_path / 'test_example_output.csv')
|
||||
@@ -175,6 +226,19 @@ def test_extract_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_csv_stdout_only_comments(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['-c', '-', 'tests/data/only_comments.txt'])
|
||||
|
||||
# Assert
|
||||
assert not file_exits('test_example_output.csv')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_stdin_and_csv_stdout(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
# Arrange
|
||||
monkeypatch.setattr('sys.stdin', io.StringIO(read_file_to_str('example_export.txt')))
|
||||
@@ -218,6 +282,18 @@ def test_keepass_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path)
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_keepass_empty(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['-k', '-', 'tests/data/only_comments.txt'])
|
||||
|
||||
# Assert
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_keepass_csv_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
'''Two csv files .totp and .htop are generated.'''
|
||||
# Act
|
||||
@@ -288,6 +364,18 @@ def test_extract_json_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_json_stdout_only_comments(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['-j', '-', 'tests/data/only_comments.txt'])
|
||||
|
||||
# Assert
|
||||
assert not file_exits('test_example_output.json')
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == '[]'
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_not_encoded_plus(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['tests/data/test_plus_problem_export.txt'])
|
||||
@@ -421,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', '')
|
||||
@@ -461,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)
|
||||
@@ -494,6 +596,113 @@ def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: Mocker
|
||||
assert e.type == SystemExit
|
||||
|
||||
|
||||
MockMode = Enum('MockMode', ['REPEAT_FIRST_ENDLESS', 'LOOP_LIST'])
|
||||
|
||||
|
||||
class MockCam:
|
||||
|
||||
read_counter: int = 0
|
||||
read_files: List[str] = []
|
||||
mock_mode: MockMode
|
||||
|
||||
def __init__(self, files: List[str] = ['example_export.png'], mock_mode: MockMode = MockMode.REPEAT_FIRST_ENDLESS):
|
||||
self.read_files = files
|
||||
self.image_mode = mock_mode
|
||||
|
||||
def read(self) -> Tuple[bool, Any]:
|
||||
if self.image_mode == MockMode.REPEAT_FIRST_ENDLESS:
|
||||
file = self.read_files[0]
|
||||
elif self.image_mode == MockMode.LOOP_LIST:
|
||||
file = self.read_files[self.read_counter]
|
||||
self.read_counter += 1
|
||||
|
||||
if file:
|
||||
img = cv2.imread(file)
|
||||
return True, img
|
||||
else:
|
||||
return False, None
|
||||
|
||||
def release(self) -> None:
|
||||
# ignore
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.parametrize("qr_reader,file,success", [
|
||||
(None, 'example_export.png', True),
|
||||
('ZBAR', 'example_export.png', True),
|
||||
('QREADER', 'example_export.png', True),
|
||||
('QREADER_DEEP', 'example_export.png', True),
|
||||
('CV2', 'example_export.png', True),
|
||||
('CV2_WECHAT', 'example_export.png', True),
|
||||
(None, 'tests/data/qr_but_without_otp.png', False),
|
||||
('ZBAR', 'tests/data/qr_but_without_otp.png', False),
|
||||
('QREADER', 'tests/data/qr_but_without_otp.png', False),
|
||||
('QREADER_DEEP', 'tests/data/qr_but_without_otp.png', False),
|
||||
('CV2', 'tests/data/qr_but_without_otp.png', False),
|
||||
('CV2_WECHAT', 'tests/data/qr_but_without_otp.png', False),
|
||||
(None, 'tests/data/lena_std.tif', None),
|
||||
('ZBAR', 'tests/data/lena_std.tif', None),
|
||||
('QREADER', 'tests/data/lena_std.tif', None),
|
||||
('QREADER_DEEP', 'tests/data/lena_std.tif', None),
|
||||
('CV2', 'tests/data/lena_std.tif', None),
|
||||
('CV2_WECHAT', 'tests/data/lena_std.tif', None),
|
||||
])
|
||||
def test_extract_otps_from_camera(qr_reader: Optional[str], file: str, success: bool, capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
|
||||
if cv2_available:
|
||||
# Arrange
|
||||
mockCam = MockCam([file])
|
||||
mocker.patch('cv2.VideoCapture', return_value=mockCam)
|
||||
mocker.patch('cv2.namedWindow')
|
||||
mocked_polylines = mocker.patch('cv2.polylines')
|
||||
mocker.patch('cv2.imshow')
|
||||
mocker.patch('cv2.getTextSize', return_value=([8, 200], False))
|
||||
mocked_putText = mocker.patch('cv2.putText')
|
||||
mocker.patch('cv2.getWindowImageRect', return_value=[0, 0, 640, 480])
|
||||
mocker.patch('cv2.waitKey', return_value=27)
|
||||
mocker.patch('cv2.getWindowProperty', return_value=False)
|
||||
mocker.patch('cv2.destroyAllWindows')
|
||||
|
||||
args = []
|
||||
if qr_reader:
|
||||
args.append('-Q')
|
||||
args.append(qr_reader)
|
||||
# Act
|
||||
extract_otp_secrets.main(args)
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
if success:
|
||||
assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG
|
||||
assert captured.err == ''
|
||||
mocked_polylines.assert_called_with(mocker.ANY, mocker.ANY, True, SUCCESS_COLOR, mocker.ANY)
|
||||
mocked_putText.assert_called_with(mocker.ANY, "3 otps extracted", mocker.ANY, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
elif success is None:
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
mocked_polylines.assert_not_called()
|
||||
mocked_putText.assert_called_with(mocker.ANY, "0 otps extracted", mocker.ANY, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
else:
|
||||
assert captured.out == ''
|
||||
assert captured.err != ''
|
||||
mocked_polylines.assert_called_with(mocker.ANY, mocker.ANY, True, FAILURE_COLOR, mocker.ANY)
|
||||
mocked_putText.assert_called_with(mocker.ANY, "0 otps extracted", mocker.ANY, FONT, FONT_SCALE, FONT_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
else:
|
||||
# Act
|
||||
with pytest.raises(SystemExit) as e:
|
||||
extract_otp_secrets.main([])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_err_msg = 'error: the following arguments are required: infile'
|
||||
|
||||
assert expected_err_msg in captured.err
|
||||
assert captured.out == ''
|
||||
assert e.value.code == 2
|
||||
assert e.type == SystemExit
|
||||
|
||||
|
||||
def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
with pytest.raises(SystemExit) as e:
|
||||
# Act
|
||||
@@ -524,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
|
||||
@@ -867,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
|
||||
|
||||