Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
157e247416 | ||
|
|
a0bf28f4cc | ||
|
|
fd8b165212 | ||
|
|
a467955b13 | ||
|
|
eea0179b5e | ||
|
|
d20d709d6e | ||
|
|
a0c21273e8 | ||
|
|
c7ab0b8ef4 | ||
|
|
8e70ae9da9 | ||
|
|
511bb4a91c | ||
|
|
15cbd77b07 | ||
|
|
2610afe5d8 | ||
|
|
8a4e2e3641 | ||
|
|
a0cf4246c1 | ||
|
|
b3c0912e1f | ||
|
|
1d8b49efcc | ||
|
|
f846ee40af | ||
|
|
f8b0283bdd | ||
|
|
bcbf189289 | ||
|
|
7c295e0963 | ||
|
|
6b72fad3c7 | ||
|
|
c937bede32 | ||
|
|
7402263dfe | ||
|
|
b721571101 | ||
|
|
72bac4d951 | ||
|
|
6c16d28194 | ||
|
|
87d2d4a05d | ||
|
|
ad3f184a56 | ||
|
|
994f96f749 | ||
|
|
8177a97d39 | ||
|
|
0c0ac817b1 | ||
|
|
5133711179 | ||
|
|
5c02e7d478 | ||
|
|
6932793236 | ||
|
|
9c3edea75c | ||
|
|
0ec1314b3d | ||
|
|
414e80c469 | ||
|
|
7f6959783f | ||
|
|
419f65fdea | ||
|
|
9783a5f4f2 | ||
|
|
eccb1948c7 | ||
|
|
01a38e570c | ||
|
|
91b9490afb | ||
|
|
11e530e04a | ||
|
|
9e334748ac | ||
|
|
05f3d89c42 | ||
|
|
6bf02d045c | ||
|
|
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 | ||
|
|
4ba0fad000 | ||
|
|
9d052dc78a | ||
|
|
915efcf192 |
@@ -5,4 +5,11 @@ indent_style = space
|
||||
indent_size = 4
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.md]
|
||||
indent_size = 4
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
[*.{yml,toml}]
|
||||
indent_size = 2
|
||||
|
||||
52
.github/workflows/ci.yml
vendored
@@ -1,21 +1,28 @@
|
||||
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:
|
||||
- cron: '47 3 * * *'
|
||||
# Run daily on default branch
|
||||
- cron: '37 3 * * *'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.x", "3.11", "3.10", "3.9", "pypy-3.9", "3.8", "pypy-3.8", "3.7", "pypy-3.7"]
|
||||
python-version: ["3.x", "3.12", "3.11", "3.10", "3.9", "3.8"]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
exclude:
|
||||
- platform: windows-latest
|
||||
- python-version: [pypy-3.9]
|
||||
# exclude:
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
@@ -25,16 +32,45 @@ 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: |
|
||||
sudo apt-get install -y libzbar0
|
||||
- name: Install zbar shared lib for QReader (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew install zbar
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
pip install --use-pep517 -r requirements.txt
|
||||
pip install -U -r requirements-dev.txt
|
||||
pip install -U .
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics
|
||||
if: matrix.python-version != '3.7'
|
||||
- name: Type checking with mypy
|
||||
run: |
|
||||
mypy --install-types --non-interactive src/*.py tests/*.py
|
||||
mypy --strict src/*.py tests/*.py
|
||||
if: matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||
- name: Test with pytest
|
||||
run: pytest
|
||||
if: (matrix.python-version != '3.x' || matrix.platform != 'ubuntu-latest') && (matrix.python-version != '3.10' && matrix.platform != 'macos-latest')
|
||||
- name: Test with pytest (with code coverage)
|
||||
run: pytest --cov=extract_otp_secrets_test --junitxml=pytest.xml --cov-report=term-missing | tee pytest-coverage.txt
|
||||
if: matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||
# https://github.com/marketplace/actions/pytest-coverage-comment
|
||||
- name: Pytest coverage comment
|
||||
uses: MishaKav/pytest-coverage-comment@main
|
||||
with:
|
||||
pytest-coverage-path: ./pytest-coverage.txt
|
||||
junitxml-path: ./pytest.xml
|
||||
if: |
|
||||
false && matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||
&& !contains(github.ref, 'refs/tags/')
|
||||
|
||||
|
||||
259
.github/workflows/ci_docker.yml
vendored
Normal file
@@ -0,0 +1,259 @@
|
||||
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/
|
||||
|
||||
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:
|
||||
build-and-push-docker-debian-image:
|
||||
name: Build Docker Bookworm 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: docker/Dockerfile
|
||||
# builder: ${{ steps.buildx.outputs.name }}
|
||||
# Note: tags has to be all lower-case
|
||||
build-args: |
|
||||
BASE_IMAGE=python:3.12-slim-bookworm
|
||||
pull: true
|
||||
tags: |
|
||||
scit0/extract_otp_secrets:latest
|
||||
scit0/extract_otp_secrets:bookworm
|
||||
ghcr.io/scito/extract_otp_secrets:latest
|
||||
ghcr.io/scito/extract_otp_secrets:bookworm
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
- 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: "only_txt: Build image and push to Docker Hub and GitHub Container Registry"
|
||||
id: docker_build_only_txt
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
# relative path to the place where source code with Dockerfile is located
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
file: docker/Dockerfile_only_txt
|
||||
# builder: ${{ steps.buildx.outputs.name }}
|
||||
# Note: tags has to be all lower-case
|
||||
pull: true
|
||||
tags: |
|
||||
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' && github.secret_source == 'Actions'}}
|
||||
build-args: |
|
||||
RUN_TESTS=true
|
||||
|
||||
- name: Image digest
|
||||
# TODO upload digests to assets
|
||||
run: |
|
||||
echo "extract_otp_secrets:only-txt digests: ${{ steps.docker_build_only_txt.outputs.digest }}"
|
||||
echo "${{ steps.docker_build_qr_reader_latest.outputs.digest }}" > digests.txt
|
||||
|
||||
- name: Save docker digests as artifacts
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: alpine_digests
|
||||
path: digests.txt
|
||||
|
||||
build-and-push-docker-bullseye-image:
|
||||
name: Build Docker Bullseye 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 Bullseye and push to GitHub Container Registry"
|
||||
id: docker_build_bullseye
|
||||
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
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
# builder: ${{ steps.buildx.outputs.name }}
|
||||
build-args: |
|
||||
BASE_IMAGE=python:3.12-slim-bullseye
|
||||
# Note: tags has to be all lower-case
|
||||
pull: true
|
||||
tags: |
|
||||
scit0/extract_otp_secrets:bullseye
|
||||
push: ${{ 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: bullseye_digests
|
||||
path: digests.txt
|
||||
410
.github/workflows/ci_release.yml
vendored
Normal file
@@ -0,0 +1,410 @@
|
||||
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.31: extract_otp_secrets:bullseye
|
||||
# - Linux x86_64 glibc 2.36: extract_otp_secrets:bookworm
|
||||
# - 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.31) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_linux_arm64 | Linux arm64 (glibc >= 2.31) |\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_bullseye.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:bullseye -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 linux/amd64
|
||||
if: matrix.PLATFORM == 'linux/amd64'
|
||||
run: |
|
||||
dist/${{ matrix.EXE }} -V
|
||||
dist/${{ matrix.EXE }} -h
|
||||
dist/${{ matrix.EXE }} --debug
|
||||
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 linux/arm64
|
||||
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.12" >> $GITHUB_ENV
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.12
|
||||
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 }} --debug
|
||||
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
|
||||
25
.gitignore
vendored
@@ -9,10 +9,29 @@ venv/
|
||||
!example_output.csv
|
||||
!.github/
|
||||
!.flake8
|
||||
.vscode
|
||||
!.vscode/settings.json
|
||||
!.vscode/
|
||||
!.devcontainer/
|
||||
!.devcontainer/*.json
|
||||
*.whl
|
||||
build/
|
||||
extract_otp_secret_keys.egg-info/
|
||||
dist/
|
||||
*.egg-info/
|
||||
*.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_bookworm.spec
|
||||
extract_otp_secrets_linux_x86_64.spec
|
||||
extract_otp_secrets.spec
|
||||
test.txt
|
||||
extract_otp_secrets.build/
|
||||
extract_otp_secrets.dist/
|
||||
extract_otp_secrets.onefile-build/
|
||||
extract_otp_secrets.bin
|
||||
|
||||
4
.vscode/extensions.json
vendored
@@ -1,5 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-python.python"
|
||||
"ms-python.python",
|
||||
"ms-python.isort",
|
||||
"tamasfe.even-better-toml",
|
||||
]
|
||||
}
|
||||
|
||||
39
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: extract_otp_secrets.py",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "src/extract_otp_secrets.py",
|
||||
"args": [
|
||||
"example_export.txt"
|
||||
],
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Python: extract_otp_secrets.py stdin pic",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "src/extract_otp_secrets.py",
|
||||
"args": [
|
||||
"-",
|
||||
"<",
|
||||
"test/test_googleauth_export.png",
|
||||
],
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "Python: extract_otp_secrets.py capture",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "src/extract_otp_secrets.py",
|
||||
"args": [
|
||||
],
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
]
|
||||
}
|
||||
8
.vscode/settings.json
vendored
@@ -2,7 +2,7 @@
|
||||
"python.testing.pytestArgs": [
|
||||
"."
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.unittestEnabled": true,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"cSpell.words": [
|
||||
"devbox",
|
||||
@@ -16,5 +16,9 @@
|
||||
"qrcode",
|
||||
"TOTP",
|
||||
"venv"
|
||||
]
|
||||
],
|
||||
"search.exclude": {
|
||||
"**/build": true,
|
||||
"**/dist": true
|
||||
},
|
||||
}
|
||||
|
||||
11
Dockerfile
@@ -1,11 +0,0 @@
|
||||
FROM python:3.11-alpine
|
||||
|
||||
WORKDIR /extract
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN pip install -r requirements.txt
|
||||
|
||||
WORKDIR /files
|
||||
|
||||
ENTRYPOINT [ "python", "/extract/extract_otp_secret_keys.py" ]
|
||||
20
Pipfile
@@ -4,15 +4,29 @@ verify_ssl = true
|
||||
name = "pypi"
|
||||
|
||||
[packages]
|
||||
colorama = ">=0.4.6"
|
||||
opencv-contrib-python = "*"
|
||||
# for macOS: opencv-contrib-python = "<=4.7.0"
|
||||
pillow = "*"
|
||||
pyzbar = "*"
|
||||
protobuf = "*"
|
||||
qrcode = "*"
|
||||
pillow = "*"
|
||||
qreader = "<2.0.0"
|
||||
|
||||
[dev-packages]
|
||||
pytest = "*"
|
||||
wheel = "*"
|
||||
build = "*"
|
||||
flake8 = "*"
|
||||
gfm-toc = "*"
|
||||
mypy = "*"
|
||||
mypy-protobuf = "*"
|
||||
nuitka = "*"
|
||||
pylint = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-mock = "*"
|
||||
setuptools-git-versioning = "*"
|
||||
types-protobuf = "*"
|
||||
wheel = "*"
|
||||
|
||||
[requires]
|
||||
python_version = "3.11"
|
||||
|
||||
729
Pipfile.lock
generated
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "38a1c4d86c2546c0ac33f77c1a20dc325b50be44b8c4b310050b63033558507d"
|
||||
"sha256": "d9664aaa55d180a006624f7c65977a97f23b7c25444e2b2abe1f85ceb0e3337b"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
@@ -16,173 +16,321 @@
|
||||
]
|
||||
},
|
||||
"default": {
|
||||
"pillow": {
|
||||
"colorama": {
|
||||
"hashes": [
|
||||
"sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040",
|
||||
"sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8",
|
||||
"sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65",
|
||||
"sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2",
|
||||
"sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627",
|
||||
"sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07",
|
||||
"sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef",
|
||||
"sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535",
|
||||
"sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c",
|
||||
"sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc",
|
||||
"sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3",
|
||||
"sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1",
|
||||
"sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c",
|
||||
"sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa",
|
||||
"sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32",
|
||||
"sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502",
|
||||
"sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4",
|
||||
"sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f",
|
||||
"sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812",
|
||||
"sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636",
|
||||
"sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20",
|
||||
"sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c",
|
||||
"sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91",
|
||||
"sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe",
|
||||
"sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b",
|
||||
"sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad",
|
||||
"sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9",
|
||||
"sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72",
|
||||
"sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4",
|
||||
"sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de",
|
||||
"sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29",
|
||||
"sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee",
|
||||
"sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c",
|
||||
"sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7",
|
||||
"sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11",
|
||||
"sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c",
|
||||
"sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c",
|
||||
"sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448",
|
||||
"sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b",
|
||||
"sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20",
|
||||
"sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228",
|
||||
"sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd",
|
||||
"sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699",
|
||||
"sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b",
|
||||
"sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2",
|
||||
"sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4",
|
||||
"sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c",
|
||||
"sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f",
|
||||
"sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2",
|
||||
"sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c",
|
||||
"sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3",
|
||||
"sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193",
|
||||
"sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48",
|
||||
"sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02",
|
||||
"sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8",
|
||||
"sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e",
|
||||
"sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f",
|
||||
"sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b",
|
||||
"sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74",
|
||||
"sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb",
|
||||
"sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0"
|
||||
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
|
||||
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==9.3.0"
|
||||
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6'",
|
||||
"version": "==0.4.6"
|
||||
},
|
||||
"numpy": {
|
||||
"hashes": [
|
||||
"sha256:020cdbee66ed46b671429c7265cf00d8ac91c046901c55684954c3958525dab2",
|
||||
"sha256:0621f7daf973d34d18b4e4bafb210bbaf1ef5e0100b5fa750bd9cde84c7ac292",
|
||||
"sha256:0792824ce2f7ea0c82ed2e4fecc29bb86bee0567a080dacaf2e0a01fe7654369",
|
||||
"sha256:09aaee96c2cbdea95de76ecb8a586cb687d281c881f5f17bfc0fb7f5890f6b91",
|
||||
"sha256:166b36197e9debc4e384e9c652ba60c0bacc216d0fc89e78f973a9760b503388",
|
||||
"sha256:186ba67fad3c60dbe8a3abff3b67a91351100f2661c8e2a80364ae6279720299",
|
||||
"sha256:306545e234503a24fe9ae95ebf84d25cba1fdc27db971aa2d9f1ab6bba19a9dd",
|
||||
"sha256:436c8e9a4bdeeee84e3e59614d38c3dbd3235838a877af8c211cfcac8a80b8d3",
|
||||
"sha256:4a873a8180479bc829313e8d9798d5234dfacfc2e8a7ac188418189bb8eafbd2",
|
||||
"sha256:4acc65dd65da28060e206c8f27a573455ed724e6179941edb19f97e58161bb69",
|
||||
"sha256:51be5f8c349fdd1a5568e72713a21f518e7d6707bcf8503b528b88d33b57dc68",
|
||||
"sha256:546b7dd7e22f3c6861463bebb000646fa730e55df5ee4a0224408b5694cc6148",
|
||||
"sha256:5671338034b820c8d58c81ad1dafc0ed5a00771a82fccc71d6438df00302094b",
|
||||
"sha256:637c58b468a69869258b8ae26f4a4c6ff8abffd4a8334c830ffb63e0feefe99a",
|
||||
"sha256:767254ad364991ccfc4d81b8152912e53e103ec192d1bb4ea6b1f5a7117040be",
|
||||
"sha256:7d484292eaeb3e84a51432a94f53578689ffdea3f90e10c8b203a99be5af57d8",
|
||||
"sha256:7f6bad22a791226d0a5c7c27a80a20e11cfe09ad5ef9084d4d3fc4a299cca505",
|
||||
"sha256:86f737708b366c36b76e953c46ba5827d8c27b7a8c9d0f471810728e5a2fe57c",
|
||||
"sha256:8c6adc33561bd1d46f81131d5352348350fc23df4d742bb246cdfca606ea1208",
|
||||
"sha256:914b28d3215e0c721dc75db3ad6d62f51f630cb0c277e6b3bcb39519bed10bd8",
|
||||
"sha256:b44e6a09afc12952a7d2a58ca0a2429ee0d49a4f89d83a0a11052da696440e49",
|
||||
"sha256:bb0d9a1aaf5f1cb7967320e80690a1d7ff69f1d47ebc5a9bea013e3a21faec95",
|
||||
"sha256:c0b45c8b65b79337dee5134d038346d30e109e9e2e9d43464a2970e5c0e93229",
|
||||
"sha256:c2e698cb0c6dda9372ea98a0344245ee65bdc1c9dd939cceed6bb91256837896",
|
||||
"sha256:c78a22e95182fb2e7874712433eaa610478a3caf86f28c621708d35fa4fd6e7f",
|
||||
"sha256:e062aa24638bb5018b7841977c360d2f5917268d125c833a686b7cbabbec496c",
|
||||
"sha256:e5e18e5b14a7560d8acf1c596688f4dfd19b4f2945b245a71e5af4ddb7422feb",
|
||||
"sha256:eae430ecf5794cb7ae7fa3808740b015aa80747e5266153128ef055975a72b99",
|
||||
"sha256:ee84ca3c58fe48b8ddafdeb1db87388dce2c3c3f701bf447b05e4cfcc3679112",
|
||||
"sha256:f042f66d0b4ae6d48e70e28d487376204d3cbf43b84c03bac57e28dac6151581",
|
||||
"sha256:f8db2f125746e44dce707dd44d4f4efeea8d7e2b43aace3f8d1f235cfa2733dd",
|
||||
"sha256:f93fc78fe8bf15afe2b8d6b6499f1c73953169fad1e9a8dd086cdff3190e7fdf"
|
||||
],
|
||||
"markers": "python_version >= '3.10'",
|
||||
"version": "==1.26.0"
|
||||
},
|
||||
"opencv-contrib-python": {
|
||||
"hashes": [
|
||||
"sha256:377936b02dcf82dc70261101381a8ad82b03d1298f185886be298d74fe35c328",
|
||||
"sha256:3fb62bc5967a79bce7c576ef94dea0b9172e52bab91630103e042cb5f29a148a",
|
||||
"sha256:6cad1720ac701cb3742f48f95bef5cfa288b916b6ac5700f63d5809e3ad5999e",
|
||||
"sha256:81804332299d656905d4f404fcec5f400d692c652d7a47926b7a441272ce795b",
|
||||
"sha256:8d6feb39d4af2cd1e8919110229bfedd13d4798a089bbe88fbd1a001b664d552",
|
||||
"sha256:8d97192471c7d42532103ecebf8ad9d9534b7cd655ffadbccacb9ff3d4d49b40",
|
||||
"sha256:f8737cf3055a6156c66c75432ed28ee3c1d52532b17d91ed73d508ae351b3e66"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.8.1.78"
|
||||
},
|
||||
"opencv-python": {
|
||||
"hashes": [
|
||||
"sha256:91d5f6f5209dc2635d496f6b8ca6573ecdad051a09e6b5de4c399b8e673c60da",
|
||||
"sha256:9814beca408d3a0eca1bae7e3e5be68b07c17ecceb392b94170881216e09b319",
|
||||
"sha256:a7aac3900fbacf55b551e7b53626c3dad4c71ce85643645c43e91fcb19045e47",
|
||||
"sha256:b983197f97cfa6fcb74e1da1802c7497a6f94ed561aba6980f1f33123f904956",
|
||||
"sha256:bc31f47e05447da8b3089faa0a07ffe80e114c91ce0b171e6424f9badbd1c5cd",
|
||||
"sha256:c4c406bdb41eb21ea51b4e90dfbc989c002786c3f601c236a99c59a54670a394",
|
||||
"sha256:cc7adbbcd1112877a39274106cb2752e04984bc01a031162952e97450d6117f6"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==4.8.1.78"
|
||||
},
|
||||
"pillow": {
|
||||
"hashes": [
|
||||
"sha256:0462b1496505a3462d0f35dc1c4d7b54069747d65d00ef48e736acda2c8cbdff",
|
||||
"sha256:186f7e04248103482ea6354af6d5bcedb62941ee08f7f788a1c7707bc720c66f",
|
||||
"sha256:19e9adb3f22d4c416e7cd79b01375b17159d6990003633ff1d8377e21b7f1b21",
|
||||
"sha256:28444cb6ad49726127d6b340217f0627abc8732f1194fd5352dec5e6a0105635",
|
||||
"sha256:2872f2d7846cf39b3dbff64bc1104cc48c76145854256451d33c5faa55c04d1a",
|
||||
"sha256:2cc6b86ece42a11f16f55fe8903595eff2b25e0358dec635d0a701ac9586588f",
|
||||
"sha256:2d7e91b4379f7a76b31c2dda84ab9e20c6220488e50f7822e59dac36b0cd92b1",
|
||||
"sha256:2fa6dd2661838c66f1a5473f3b49ab610c98a128fc08afbe81b91a1f0bf8c51d",
|
||||
"sha256:32bec7423cdf25c9038fef614a853c9d25c07590e1a870ed471f47fb80b244db",
|
||||
"sha256:3855447d98cced8670aaa63683808df905e956f00348732448b5a6df67ee5849",
|
||||
"sha256:3a04359f308ebee571a3127fdb1bd01f88ba6f6fb6d087f8dd2e0d9bff43f2a7",
|
||||
"sha256:3a0d3e54ab1df9df51b914b2233cf779a5a10dfd1ce339d0421748232cea9876",
|
||||
"sha256:44e7e4587392953e5e251190a964675f61e4dae88d1e6edbe9f36d6243547ff3",
|
||||
"sha256:459307cacdd4138edee3875bbe22a2492519e060660eaf378ba3b405d1c66317",
|
||||
"sha256:4ce90f8a24e1c15465048959f1e94309dfef93af272633e8f37361b824532e91",
|
||||
"sha256:50bd5f1ebafe9362ad622072a1d2f5850ecfa44303531ff14353a4059113b12d",
|
||||
"sha256:522ff4ac3aaf839242c6f4e5b406634bfea002469656ae8358644fc6c4856a3b",
|
||||
"sha256:552912dbca585b74d75279a7570dd29fa43b6d93594abb494ebb31ac19ace6bd",
|
||||
"sha256:5d6c9049c6274c1bb565021367431ad04481ebb54872edecfcd6088d27edd6ed",
|
||||
"sha256:697a06bdcedd473b35e50a7e7506b1d8ceb832dc238a336bd6f4f5aa91a4b500",
|
||||
"sha256:71671503e3015da1b50bd18951e2f9daf5b6ffe36d16f1eb2c45711a301521a7",
|
||||
"sha256:723bd25051454cea9990203405fa6b74e043ea76d4968166dfd2569b0210886a",
|
||||
"sha256:764d2c0daf9c4d40ad12fbc0abd5da3af7f8aa11daf87e4fa1b834000f4b6b0a",
|
||||
"sha256:787bb0169d2385a798888e1122c980c6eff26bf941a8ea79747d35d8f9210ca0",
|
||||
"sha256:7f771e7219ff04b79e231d099c0a28ed83aa82af91fd5fa9fdb28f5b8d5addaf",
|
||||
"sha256:847e8d1017c741c735d3cd1883fa7b03ded4f825a6e5fcb9378fd813edee995f",
|
||||
"sha256:84efb46e8d881bb06b35d1d541aa87f574b58e87f781cbba8d200daa835b42e1",
|
||||
"sha256:898f1d306298ff40dc1b9ca24824f0488f6f039bc0e25cfb549d3195ffa17088",
|
||||
"sha256:8b451d6ead6e3500b6ce5c7916a43d8d8d25ad74b9102a629baccc0808c54971",
|
||||
"sha256:8f06be50669087250f319b706decf69ca71fdecd829091a37cc89398ca4dc17a",
|
||||
"sha256:92a23b0431941a33242b1f0ce6c88a952e09feeea9af4e8be48236a68ffe2205",
|
||||
"sha256:93139acd8109edcdeffd85e3af8ae7d88b258b3a1e13a038f542b79b6d255c54",
|
||||
"sha256:98533fd7fa764e5f85eebe56c8e4094db912ccbe6fbf3a58778d543cadd0db08",
|
||||
"sha256:9f665d1e6474af9f9da5e86c2a3a2d2d6204e04d5af9c06b9d42afa6ebde3f21",
|
||||
"sha256:b059ac2c4c7a97daafa7dc850b43b2d3667def858a4f112d1aa082e5c3d6cf7d",
|
||||
"sha256:b1be1c872b9b5fcc229adeadbeb51422a9633abd847c0ff87dc4ef9bb184ae08",
|
||||
"sha256:b7cf63d2c6928b51d35dfdbda6f2c1fddbe51a6bc4a9d4ee6ea0e11670dd981e",
|
||||
"sha256:bc2e3069569ea9dbe88d6b8ea38f439a6aad8f6e7a6283a38edf61ddefb3a9bf",
|
||||
"sha256:bcf1207e2f2385a576832af02702de104be71301c2696d0012b1b93fe34aaa5b",
|
||||
"sha256:ca26ba5767888c84bf5a0c1a32f069e8204ce8c21d00a49c90dabeba00ce0145",
|
||||
"sha256:cbe68deb8580462ca0d9eb56a81912f59eb4542e1ef8f987405e35a0179f4ea2",
|
||||
"sha256:d6caf3cd38449ec3cd8a68b375e0c6fe4b6fd04edb6c9766b55ef84a6e8ddf2d",
|
||||
"sha256:d72967b06be9300fed5cfbc8b5bafceec48bf7cdc7dab66b1d2549035287191d",
|
||||
"sha256:d889b53ae2f030f756e61a7bff13684dcd77e9af8b10c6048fb2c559d6ed6eaf",
|
||||
"sha256:de596695a75496deb3b499c8c4f8e60376e0516e1a774e7bc046f0f48cd620ad",
|
||||
"sha256:e6a90167bcca1216606223a05e2cf991bb25b14695c518bc65639463d7db722d",
|
||||
"sha256:ed2d9c0704f2dc4fa980b99d565c0c9a543fe5101c25b3d60488b8ba80f0cce1",
|
||||
"sha256:ee7810cf7c83fa227ba9125de6084e5e8b08c59038a7b2c9045ef4dde61663b4",
|
||||
"sha256:f0b4b06da13275bc02adfeb82643c4a6385bd08d26f03068c2796f60d125f6f2",
|
||||
"sha256:f11c9102c56ffb9ca87134bd025a43d2aba3f1155f508eff88f694b33a9c6d19",
|
||||
"sha256:f5bb289bb835f9fe1a1e9300d011eef4d69661bb9b34d5e196e5e82c4cb09b37",
|
||||
"sha256:f6d3d4c905e26354e8f9d82548475c46d8e0889538cb0657aa9c6f0872a37aa4",
|
||||
"sha256:fcb59711009b0168d6ee0bd8fb5eb259c4ab1717b2f538bbf36bacf207ef7a68",
|
||||
"sha256:fd2a5403a75b54661182b75ec6132437a181209b901446ee5724b589af8edef1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==10.0.1"
|
||||
},
|
||||
"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"
|
||||
"sha256:02212557a76cd99574775a81fefeba8738d0f668d6abd0c6b1d3adcc75503dbe",
|
||||
"sha256:1badab72aa8a3a2b812eacfede5020472e16c6b2212d737cefd685884c191085",
|
||||
"sha256:2fa3886dfaae6b4c5ed2730d3bf47c7a38a72b3a1f0acb4d4caf68e6874b947b",
|
||||
"sha256:5a70731910cd9104762161719c3d883c960151eea077134458503723b60e3667",
|
||||
"sha256:6b7d2e1c753715dcfe9d284a25a52d67818dd43c4932574307daf836f0071e37",
|
||||
"sha256:80797ce7424f8c8d2f2547e2d42bfbb6c08230ce5832d6c099a37335c9c90a92",
|
||||
"sha256:8e61a27f362369c2f33248a0ff6896c20dcd47b5d48239cb9720134bef6082e4",
|
||||
"sha256:9fee5e8aa20ef1b84123bb9232b3f4a5114d9897ed89b4b8142d81924e05d79b",
|
||||
"sha256:b493cb590960ff863743b9ff1452c413c2ee12b782f48beca77c8da3e2ffe9d9",
|
||||
"sha256:b77272f3e28bb416e2071186cb39efd4abbf696d682cbb5dc731308ad37fa6dd",
|
||||
"sha256:bffa46ad9612e6779d0e51ae586fde768339b791a50610d85eb162daeb23661e",
|
||||
"sha256:dbbed8a56e56cee8d9d522ce844a1379a72a70f453bde6243e3c86c30c2a3d46",
|
||||
"sha256:ec9912d5cb6714a5710e28e592ee1093d68c5ebfeda61983b3f40331da0b1ebb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.21.12"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.24.4"
|
||||
},
|
||||
"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"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==7.4.2"
|
||||
},
|
||||
"qreader": {
|
||||
"hashes": [
|
||||
"sha256:f96d2879c9f47f9641da55dc3e1df765cd4d6f1cfbc8b7fa811b70c1e27d4fba"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.3.2"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
|
||||
"sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.8.0"
|
||||
}
|
||||
},
|
||||
"develop": {
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907",
|
||||
"sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"
|
||||
"sha256:1defdbca052635dd29657ea674edfc45e4b5be9cd53630c5b084fcfed94344a8",
|
||||
"sha256:f2510e7fdcd6cfda4ec50014726d4857abf79acfc010084ce8c26091913f1b25"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.2'",
|
||||
"version": "==2.12.13"
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"attrs": {
|
||||
"build": {
|
||||
"hashes": [
|
||||
"sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6",
|
||||
"sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"
|
||||
"sha256:538aab1b64f9828977f84bc63ae570b060a8ed1be419e7870b8b4fc5e6ea553b",
|
||||
"sha256:589bf99a67df7c9cf07ec0ac0e5e2ea5d4b37ac63301c4986d1acb126aa83f8f"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==22.1.0"
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.0.3"
|
||||
},
|
||||
"coverage": {
|
||||
"extras": [
|
||||
"toml"
|
||||
],
|
||||
"hashes": [
|
||||
"sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1",
|
||||
"sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63",
|
||||
"sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9",
|
||||
"sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312",
|
||||
"sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3",
|
||||
"sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb",
|
||||
"sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25",
|
||||
"sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92",
|
||||
"sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda",
|
||||
"sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148",
|
||||
"sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6",
|
||||
"sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216",
|
||||
"sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a",
|
||||
"sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640",
|
||||
"sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836",
|
||||
"sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c",
|
||||
"sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f",
|
||||
"sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2",
|
||||
"sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901",
|
||||
"sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed",
|
||||
"sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a",
|
||||
"sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074",
|
||||
"sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc",
|
||||
"sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84",
|
||||
"sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083",
|
||||
"sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f",
|
||||
"sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c",
|
||||
"sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c",
|
||||
"sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637",
|
||||
"sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2",
|
||||
"sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82",
|
||||
"sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f",
|
||||
"sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce",
|
||||
"sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef",
|
||||
"sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f",
|
||||
"sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611",
|
||||
"sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c",
|
||||
"sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76",
|
||||
"sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9",
|
||||
"sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce",
|
||||
"sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9",
|
||||
"sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf",
|
||||
"sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf",
|
||||
"sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9",
|
||||
"sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6",
|
||||
"sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2",
|
||||
"sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a",
|
||||
"sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a",
|
||||
"sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf",
|
||||
"sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738",
|
||||
"sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a",
|
||||
"sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==7.3.2"
|
||||
},
|
||||
"dill": {
|
||||
"hashes": [
|
||||
"sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0",
|
||||
"sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"
|
||||
"sha256:76b122c08ef4ce2eedcd4d1abd8e641114bfc6c2867f49f3c41facf65bf19f5e",
|
||||
"sha256:cc1c8b182eb3013e24bd475ff2e9295af86c1a38eb1aff128dac8962a9ce3c03"
|
||||
],
|
||||
"markers": "python_version >= '3.11'",
|
||||
"version": "==0.3.6"
|
||||
"version": "==0.3.7"
|
||||
},
|
||||
"flake8": {
|
||||
"hashes": [
|
||||
"sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7",
|
||||
"sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"
|
||||
"sha256:d5b3857f07c030bdb5bf41c7f53799571d75c4491748a3adcd47de929e34cd23",
|
||||
"sha256:ffdfce58ea94c6580c77888a86506937f9a1a227dfcd15f245d694ae20a6b6e5"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==6.0.0"
|
||||
"markers": "python_full_version >= '3.8.1'",
|
||||
"version": "==6.1.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:83155ffa936239d986b0f190347a3f2285f42a9b9e1725c89d865b27dd0627e5",
|
||||
"sha256:a8ca25fbfad0f7d5d8447a4314837298d9f6b23aed8618584c894574f626b64b"
|
||||
"sha256:8bef7dde241278824a6d83f44a544709b065191b95b6e50894bdc722fcba0504",
|
||||
"sha256:f84c2818376e66cf843d497486ea8fed8700b340f308f076c6fb1229dff318b6"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.0'",
|
||||
"version": "==5.11.3"
|
||||
},
|
||||
"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"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.8.0"
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"version": "==5.12.0"
|
||||
},
|
||||
"mccabe": {
|
||||
"hashes": [
|
||||
@@ -192,147 +340,274 @@
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.7.0"
|
||||
},
|
||||
"packaging": {
|
||||
"mypy": {
|
||||
"hashes": [
|
||||
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
|
||||
"sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"
|
||||
"sha256:159aa9acb16086b79bbb0016145034a1a05360626046a929f84579ce1666b315",
|
||||
"sha256:258b22210a4a258ccd077426c7a181d789d1121aca6db73a83f79372f5569ae0",
|
||||
"sha256:26f71b535dfc158a71264e6dc805a9f8d2e60b67215ca0bfa26e2e1aa4d4d373",
|
||||
"sha256:26fb32e4d4afa205b24bf645eddfbb36a1e17e995c5c99d6d00edb24b693406a",
|
||||
"sha256:2fc3a600f749b1008cc75e02b6fb3d4db8dbcca2d733030fe7a3b3502902f161",
|
||||
"sha256:32cb59609b0534f0bd67faebb6e022fe534bdb0e2ecab4290d683d248be1b275",
|
||||
"sha256:330857f9507c24de5c5724235e66858f8364a0693894342485e543f5b07c8693",
|
||||
"sha256:361da43c4f5a96173220eb53340ace68cda81845cd88218f8862dfb0adc8cddb",
|
||||
"sha256:4a465ea2ca12804d5b34bb056be3a29dc47aea5973b892d0417c6a10a40b2d65",
|
||||
"sha256:51cb1323064b1099e177098cb939eab2da42fea5d818d40113957ec954fc85f4",
|
||||
"sha256:57b10c56016adce71fba6bc6e9fd45d8083f74361f629390c556738565af8eeb",
|
||||
"sha256:596fae69f2bfcb7305808c75c00f81fe2829b6236eadda536f00610ac5ec2243",
|
||||
"sha256:5d627124700b92b6bbaa99f27cbe615c8ea7b3402960f6372ea7d65faf376c14",
|
||||
"sha256:6ac9c21bfe7bc9f7f1b6fae441746e6a106e48fc9de530dea29e8cd37a2c0cc4",
|
||||
"sha256:82cb6193de9bbb3844bab4c7cf80e6227d5225cc7625b068a06d005d861ad5f1",
|
||||
"sha256:8f772942d372c8cbac575be99f9cc9d9fb3bd95c8bc2de6c01411e2c84ebca8a",
|
||||
"sha256:9fece120dbb041771a63eb95e4896791386fe287fefb2837258925b8326d6160",
|
||||
"sha256:a156e6390944c265eb56afa67c74c0636f10283429171018446b732f1a05af25",
|
||||
"sha256:a9ec1f695f0c25986e6f7f8778e5ce61659063268836a38c951200c57479cc12",
|
||||
"sha256:abed92d9c8f08643c7d831300b739562b0a6c9fcb028d211134fc9ab20ccad5d",
|
||||
"sha256:b031b9601f1060bf1281feab89697324726ba0c0bae9d7cd7ab4b690940f0b92",
|
||||
"sha256:c543214ffdd422623e9fedd0869166c2f16affe4ba37463975043ef7d2ea8770",
|
||||
"sha256:d28ddc3e3dfeab553e743e532fb95b4e6afad51d4706dd22f28e1e5e664828d2",
|
||||
"sha256:f33592ddf9655a4894aef22d134de7393e95fcbdc2d15c1ab65828eee5c66c70",
|
||||
"sha256:f6b0e77db9ff4fda74de7df13f30016a0a663928d669c9f2c057048ba44f09bb",
|
||||
"sha256:f757063a83970d67c444f6e01d9550a7402322af3557ce7630d3c957386fa8f5",
|
||||
"sha256:ff0cedc84184115202475bbb46dd99f8dcb87fe24d5d0ddfc0fe6b8575c88d2f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.5.1"
|
||||
},
|
||||
"mypy-extensions": {
|
||||
"hashes": [
|
||||
"sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d",
|
||||
"sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"
|
||||
],
|
||||
"markers": "python_version >= '3.5'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"mypy-protobuf": {
|
||||
"hashes": [
|
||||
"sha256:0d0548c6b9a6faf14ce1a9ce2831c403a5c1f2a9363e85b1e2c51d5d57aa8393",
|
||||
"sha256:21f270da0a9792a9dac76b0df463c027e561664ab6973c59be4e4d064dfe67dc"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.5.0"
|
||||
},
|
||||
"nuitka": {
|
||||
"hashes": [
|
||||
"sha256:496d95f5a3d2298e79720ab7f8e0b33bd18e726dd39b33341e69758dc51f9881"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==1.8.3"
|
||||
},
|
||||
"ordered-set": {
|
||||
"hashes": [
|
||||
"sha256:046e1132c71fcf3330438a539928932caf51ddbc582496833e23de611de14562",
|
||||
"sha256:694a8e44c87657c59292ede72891eb91d34131f6531463aab3009191c77364a8"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==22.0"
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"packaging": {
|
||||
"hashes": [
|
||||
"sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5",
|
||||
"sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==23.2"
|
||||
},
|
||||
"platformdirs": {
|
||||
"hashes": [
|
||||
"sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca",
|
||||
"sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"
|
||||
"sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3",
|
||||
"sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==2.6.0"
|
||||
"version": "==3.11.0"
|
||||
},
|
||||
"pluggy": {
|
||||
"hashes": [
|
||||
"sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159",
|
||||
"sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"
|
||||
"sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12",
|
||||
"sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==1.0.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==1.3.0"
|
||||
},
|
||||
"protobuf": {
|
||||
"hashes": [
|
||||
"sha256:02212557a76cd99574775a81fefeba8738d0f668d6abd0c6b1d3adcc75503dbe",
|
||||
"sha256:1badab72aa8a3a2b812eacfede5020472e16c6b2212d737cefd685884c191085",
|
||||
"sha256:2fa3886dfaae6b4c5ed2730d3bf47c7a38a72b3a1f0acb4d4caf68e6874b947b",
|
||||
"sha256:5a70731910cd9104762161719c3d883c960151eea077134458503723b60e3667",
|
||||
"sha256:6b7d2e1c753715dcfe9d284a25a52d67818dd43c4932574307daf836f0071e37",
|
||||
"sha256:80797ce7424f8c8d2f2547e2d42bfbb6c08230ce5832d6c099a37335c9c90a92",
|
||||
"sha256:8e61a27f362369c2f33248a0ff6896c20dcd47b5d48239cb9720134bef6082e4",
|
||||
"sha256:9fee5e8aa20ef1b84123bb9232b3f4a5114d9897ed89b4b8142d81924e05d79b",
|
||||
"sha256:b493cb590960ff863743b9ff1452c413c2ee12b782f48beca77c8da3e2ffe9d9",
|
||||
"sha256:b77272f3e28bb416e2071186cb39efd4abbf696d682cbb5dc731308ad37fa6dd",
|
||||
"sha256:bffa46ad9612e6779d0e51ae586fde768339b791a50610d85eb162daeb23661e",
|
||||
"sha256:dbbed8a56e56cee8d9d522ce844a1379a72a70f453bde6243e3c86c30c2a3d46",
|
||||
"sha256:ec9912d5cb6714a5710e28e592ee1093d68c5ebfeda61983b3f40331da0b1ebb"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.24.4"
|
||||
},
|
||||
"pycodestyle": {
|
||||
"hashes": [
|
||||
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
|
||||
"sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"
|
||||
"sha256:259bcc17857d8a8b3b4a2327324b79e5f020a13c16074670f9c8c8f872ea76d0",
|
||||
"sha256:5d1013ba8dc7895b548be5afb05740ca82454fd899971563d2ef625d090326f8"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==2.10.0"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==2.11.0"
|
||||
},
|
||||
"pyflakes": {
|
||||
"hashes": [
|
||||
"sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf",
|
||||
"sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"
|
||||
"sha256:4132f6d49cb4dae6819e5379898f2b8cce3c5f23994194c24b77d5da2e36f774",
|
||||
"sha256:a0aae034c444db0071aa077972ba4768d40c830d9539fd45bf4cd3f8f6992efc"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==3.0.1"
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==3.1.0"
|
||||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4",
|
||||
"sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb"
|
||||
"sha256:21da8ed1294f88d66c82eb3e624a0993291613548bb17fbccaa220c31c41293b",
|
||||
"sha256:d22816c963816d7810b87afe0bdf5c80009e1078ecbb9c8f2e2a24d4430039b1"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.15.9"
|
||||
"markers": "python_full_version >= '3.8.0'",
|
||||
"version": "==3.0.0"
|
||||
},
|
||||
"pyproject-hooks": {
|
||||
"hashes": [
|
||||
"sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8",
|
||||
"sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"
|
||||
],
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.0.0"
|
||||
},
|
||||
"pytest": {
|
||||
"hashes": [
|
||||
"sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
|
||||
"sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
|
||||
"sha256:1d881c6124e08ff0a1bb75ba3ec0bfd8b5354a01c194ddd5a0a870a48d99b002",
|
||||
"sha256:a766259cfab564a2ad52cb1aae1b881a75c3eb7e34ca3779697c23ed47c47069"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==7.2.0"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==7.4.2"
|
||||
},
|
||||
"pytest-cov": {
|
||||
"hashes": [
|
||||
"sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6",
|
||||
"sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==4.1.0"
|
||||
},
|
||||
"pytest-mock": {
|
||||
"hashes": [
|
||||
"sha256:21c279fff83d70763b05f8874cc9cfb3fcacd6d354247a976f9529d19f9acf39",
|
||||
"sha256:7f6b125602ac6d743e523ae0bfa71e1a697a2f5534064528c6ff84c2f7c2fc7f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==3.11.1"
|
||||
},
|
||||
"setuptools": {
|
||||
"hashes": [
|
||||
"sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87",
|
||||
"sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==68.2.2"
|
||||
},
|
||||
"setuptools-git-versioning": {
|
||||
"hashes": [
|
||||
"sha256:0c7a5a3e138c85385c8296e248673652173535f33889010f9213497635300cf3",
|
||||
"sha256:af9ad1e8103b5abb5b128c2db4fef99407328ac9c12f65d3ff9550c4bb39ad1c"
|
||||
],
|
||||
"index": "pypi",
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==1.13.5"
|
||||
},
|
||||
"tomlkit": {
|
||||
"hashes": [
|
||||
"sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b",
|
||||
"sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"
|
||||
"sha256:38e1ff8edb991273ec9f6181244a6a391ac30e9f5098e7535640ea6be97a7c86",
|
||||
"sha256:712cbd236609acc6a3e2e97253dfc52d4c2082982a88f61b640ecf0817eab899"
|
||||
],
|
||||
"markers": "python_version >= '3.6'",
|
||||
"version": "==0.11.6"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.12.1"
|
||||
},
|
||||
"types-protobuf": {
|
||||
"hashes": [
|
||||
"sha256:598bb2290b9b0ea65f4f63569a09deaa4475edd7bb0d8589057a38deb0b19f4f",
|
||||
"sha256:b86b0deefd1cb1582d355be4fd7a2a807cf49c993d9744d3c9fbe1cbf1e6b044"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.24.0.2"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
"sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0",
|
||||
"sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"
|
||||
],
|
||||
"markers": "python_version >= '3.8'",
|
||||
"version": "==4.8.0"
|
||||
},
|
||||
"wheel": {
|
||||
"hashes": [
|
||||
"sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac",
|
||||
"sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"
|
||||
"sha256:0c5ac5ff2afb79ac23ab82bab027a0be7b5dbcf2e54dc50efe4bf507de1f7985",
|
||||
"sha256:75909db2664838d015e3d9139004ee16711748a52c8f336b52882266540215d8"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.38.4"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.41.2"
|
||||
},
|
||||
"wrapt": {
|
||||
"zstandard": {
|
||||
"hashes": [
|
||||
"sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3",
|
||||
"sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b",
|
||||
"sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4",
|
||||
"sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2",
|
||||
"sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656",
|
||||
"sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3",
|
||||
"sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff",
|
||||
"sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310",
|
||||
"sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a",
|
||||
"sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57",
|
||||
"sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069",
|
||||
"sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383",
|
||||
"sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe",
|
||||
"sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87",
|
||||
"sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d",
|
||||
"sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b",
|
||||
"sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907",
|
||||
"sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f",
|
||||
"sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0",
|
||||
"sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28",
|
||||
"sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1",
|
||||
"sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853",
|
||||
"sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc",
|
||||
"sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3",
|
||||
"sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3",
|
||||
"sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164",
|
||||
"sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1",
|
||||
"sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c",
|
||||
"sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1",
|
||||
"sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7",
|
||||
"sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1",
|
||||
"sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320",
|
||||
"sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed",
|
||||
"sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1",
|
||||
"sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248",
|
||||
"sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c",
|
||||
"sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456",
|
||||
"sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77",
|
||||
"sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef",
|
||||
"sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1",
|
||||
"sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7",
|
||||
"sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86",
|
||||
"sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4",
|
||||
"sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d",
|
||||
"sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d",
|
||||
"sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8",
|
||||
"sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5",
|
||||
"sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471",
|
||||
"sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00",
|
||||
"sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68",
|
||||
"sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3",
|
||||
"sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d",
|
||||
"sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735",
|
||||
"sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d",
|
||||
"sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569",
|
||||
"sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7",
|
||||
"sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59",
|
||||
"sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5",
|
||||
"sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb",
|
||||
"sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b",
|
||||
"sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f",
|
||||
"sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462",
|
||||
"sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015",
|
||||
"sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"
|
||||
"sha256:0aad6090ac164a9d237d096c8af241b8dcd015524ac6dbec1330092dba151657",
|
||||
"sha256:0bdbe350691dec3078b187b8304e6a9c4d9db3eb2d50ab5b1d748533e746d099",
|
||||
"sha256:0e1e94a9d9e35dc04bf90055e914077c80b1e0c15454cc5419e82529d3e70728",
|
||||
"sha256:1243b01fb7926a5a0417120c57d4c28b25a0200284af0525fddba812d575f605",
|
||||
"sha256:144a4fe4be2e747bf9c646deab212666e39048faa4372abb6a250dab0f347a29",
|
||||
"sha256:14e10ed461e4807471075d4b7a2af51f5234c8f1e2a0c1d37d5ca49aaaad49e8",
|
||||
"sha256:1545fb9cb93e043351d0cb2ee73fa0ab32e61298968667bb924aac166278c3fc",
|
||||
"sha256:1e6e131a4df2eb6f64961cea6f979cdff22d6e0d5516feb0d09492c8fd36f3bc",
|
||||
"sha256:25fbfef672ad798afab12e8fd204d122fca3bc8e2dcb0a2ba73bf0a0ac0f5f07",
|
||||
"sha256:2769730c13638e08b7a983b32cb67775650024632cd0476bf1ba0e6360f5ac7d",
|
||||
"sha256:48b6233b5c4cacb7afb0ee6b4f91820afbb6c0e3ae0fa10abbc20000acdf4f11",
|
||||
"sha256:4af612c96599b17e4930fe58bffd6514e6c25509d120f4eae6031b7595912f85",
|
||||
"sha256:52b2b5e3e7670bd25835e0e0730a236f2b0df87672d99d3bf4bf87248aa659fb",
|
||||
"sha256:57ac078ad7333c9db7a74804684099c4c77f98971c151cee18d17a12649bc25c",
|
||||
"sha256:62957069a7c2626ae80023998757e27bd28d933b165c487ab6f83ad3337f773d",
|
||||
"sha256:649a67643257e3b2cff1c0a73130609679a5673bf389564bc6d4b164d822a7ce",
|
||||
"sha256:67829fdb82e7393ca68e543894cd0581a79243cc4ec74a836c305c70a5943f07",
|
||||
"sha256:7d3bc4de588b987f3934ca79140e226785d7b5e47e31756761e48644a45a6766",
|
||||
"sha256:7f2afab2c727b6a3d466faee6974a7dad0d9991241c498e7317e5ccf53dbc766",
|
||||
"sha256:8070c1cdb4587a8aa038638acda3bd97c43c59e1e31705f2766d5576b329e97c",
|
||||
"sha256:8257752b97134477fb4e413529edaa04fc0457361d304c1319573de00ba796b1",
|
||||
"sha256:9980489f066a391c5572bc7dc471e903fb134e0b0001ea9b1d3eff85af0a6f1b",
|
||||
"sha256:9cff89a036c639a6a9299bf19e16bfb9ac7def9a7634c52c257166db09d950e7",
|
||||
"sha256:a8d200617d5c876221304b0e3fe43307adde291b4a897e7b0617a61611dfff6a",
|
||||
"sha256:a9fec02ce2b38e8b2e86079ff0b912445495e8ab0b137f9c0505f88ad0d61296",
|
||||
"sha256:b1367da0dde8ae5040ef0413fb57b5baeac39d8931c70536d5f013b11d3fc3a5",
|
||||
"sha256:b69cccd06a4a0a1d9fb3ec9a97600055cf03030ed7048d4bcb88c574f7895773",
|
||||
"sha256:b72060402524ab91e075881f6b6b3f37ab715663313030d0ce983da44960a86f",
|
||||
"sha256:c053b7c4cbf71cc26808ed67ae955836232f7638444d709bfc302d3e499364fa",
|
||||
"sha256:cff891e37b167bc477f35562cda1248acc115dbafbea4f3af54ec70821090965",
|
||||
"sha256:d12fa383e315b62630bd407477d750ec96a0f438447d0e6e496ab67b8b451d39",
|
||||
"sha256:d2d61675b2a73edcef5e327e38eb62bdfc89009960f0e3991eae5cc3d54718de",
|
||||
"sha256:db62cbe7a965e68ad2217a056107cc43d41764c66c895be05cf9c8b19578ce9c",
|
||||
"sha256:ddb086ea3b915e50f6604be93f4f64f168d3fc3cef3585bb9a375d5834392d4f",
|
||||
"sha256:df28aa5c241f59a7ab524f8ad8bb75d9a23f7ed9d501b0fed6d40ec3064784e8",
|
||||
"sha256:e1e0c62a67ff425927898cf43da2cf6b852289ebcc2054514ea9bf121bec10a5",
|
||||
"sha256:e6048a287f8d2d6e8bc67f6b42a766c61923641dd4022b7fd3f7439e17ba5a4d",
|
||||
"sha256:e7d560ce14fd209db6adacce8908244503a009c6c39eee0c10f138996cd66d3e",
|
||||
"sha256:ea68b1ba4f9678ac3d3e370d96442a6332d431e5050223626bdce748692226ea",
|
||||
"sha256:f08e3a10d01a247877e4cb61a82a319ea746c356a3786558bed2481e6c405546",
|
||||
"sha256:f1b9703fe2e6b6811886c44052647df7c37478af1b4a1a9078585806f42e5b15",
|
||||
"sha256:fe6c821eb6870f81d73bf10e5deed80edcac1e63fbc40610e61f340723fd5f7c",
|
||||
"sha256:ff0852da2abe86326b20abae912d0367878dd0854b8931897d44cfeb18985472"
|
||||
],
|
||||
"markers": "python_version >= '3.11'",
|
||||
"version": "==1.14.1"
|
||||
"markers": "python_version >= '3.7'",
|
||||
"version": "==0.21.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
648
README.md
@@ -1,90 +1,378 @@
|
||||
# Extract TOTP/HOTP secret keys from Google Authenticator
|
||||
# Extract TOTP/HOTP secrets from QR codes exported by two-factor authentication apps
|
||||
|
||||
[](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml)
|
||||

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

|
||||
[](https://github.com/scito/extract_otp_secret_keys/blob/master/LICENSE)
|
||||
[](https://github.com/scito/extract_otp_secret_keys/tags)
|
||||
[](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/LICENSE)
|
||||
[](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/) -->
|
||||
|
||||
---
|
||||
|
||||
Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of "Google Authenticator" app.
|
||||
The secret and otp values can be printed and exported to json or csv. The QR codes can be printed or saved as PNG images.
|
||||
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:
|
||||
|
||||
## Installation
|
||||
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.
|
||||
|
||||
git clone https://github.com/scito/extract_otp_secret_keys.git
|
||||
cd extract_otp_secret_keys
|
||||
The secrets can be exported to JSON or CSV, or printed as QR codes to console or saved as PNG.
|
||||
|
||||
⚡ **This project/script was renamed from `extract_otp_secret_keys` to `extract_otp_secrets`.** ⚡
|
||||
|
||||
<details>
|
||||
<summary>Table of contents</summary>
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Download and run binary executable (🆕 since v2.1)](#download-and-run-binary-executable--since-v21)
|
||||
- [MacOS](#macos)
|
||||
- [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_X.Y.Z_OS_ARCH`
|
||||
3. Start executable by clicking or from command line (macOS: startable only from command line, see [below](#macos))
|
||||
|
||||
: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
|
||||
|
||||
> 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 folder in Terminal: `cd $HOME/Downloads`
|
||||
4. Set executable bit for the downloaded file: `chmod +x extract_otp_secrets_X.Y.Z_macos_x86_64`
|
||||
5. Start executable from command line: `./extract_otp_secrets_X.Y.Z_macos_x86_64`
|
||||
|
||||
:information_source: Replace `X.Y.Z` in above commands with the version number of your downloaded file, e.g. `extract_otp_secrets_2.4.0_macos_x86_64`
|
||||
|
||||
:information_source: If Rosetta2 emulation is installed, these steps work also for M1 and M2 Apple Silicon processors and the program can be executed directly.
|
||||
|
||||
> :warning: It seems the GUI mode is not working in Terminal on macOS. In tests no [GUI window](#usage) was opened. (Remarks and hints about macOS are welcome since I do not know macOS.)
|
||||
|
||||
## 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
|
||||
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=...`)
|
||||
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.
|
||||
|
||||
python extract_otp_secret_keys.py example_export.txt
|
||||
## 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
|
||||
|
||||
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 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, …)
|
||||
|
||||
sudo apt-get install libzbar0
|
||||
|
||||
#### Linux (OpenSUSE)
|
||||
|
||||
sudo zypper install libzbar0
|
||||
|
||||
#### Linux (Fedora)
|
||||
|
||||
sudo dnf install zbar
|
||||
|
||||
#### Linux (Arch Linux)
|
||||
|
||||
pacman -S zbar
|
||||
|
||||
#### Mac OS X
|
||||
|
||||
brew install zbar
|
||||
|
||||
#### Windows
|
||||
|
||||
##### zbar
|
||||
|
||||
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 (CV2)
|
||||
|
||||
OpenCV requires [Visual C++ redistributable 2015](https://www.microsoft.com/en-us/download/details.aspx?id=48145). For more information see [opencv-python](https://pypi.org/project/opencv-python/)
|
||||
|
||||
## Program help: arguments and options
|
||||
|
||||
<pre>usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv FILE] [--keepass FILE] [--printqr] [--saveqr DIR] [--verbose | --quiet] infile
|
||||
<pre>usage: extract_otp_secrets.py [-h] [--csv FILE] [--keepass FILE] [--json FILE] [--txt 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.
|
||||
|
||||
positional arguments:
|
||||
infile file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored
|
||||
infile 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
|
||||
|
||||
options:
|
||||
-h, --help show this help message and exit
|
||||
--json FILE, -j FILE export json file or - for stdout
|
||||
--csv FILE, -c FILE export csv file or - for stdout
|
||||
--keepass FILE, -k FILE export totp/hotp csv file(s) for KeePass, - for stdout
|
||||
--printqr, -p print QR code(s) as text to the terminal (requires qrcode module)
|
||||
--saveqr DIR, -s DIR save QR code(s) as images to the given folder (requires qrcode module)
|
||||
--verbose, -v verbose output
|
||||
--quiet, -q no stdout output, except output set by -</pre>
|
||||
-h, --help show this help message and exit
|
||||
--csv FILE, -c FILE export csv file or - for stdout
|
||||
--keepass FILE, -k FILE export totp/hotp csv file(s) for KeePass, - for stdout
|
||||
--json FILE, -j FILE export json file or - for stdout
|
||||
--txt FILE, -t FILE export txt file or - for stdout
|
||||
--printqr, -p print QR code(s) as text to the terminal
|
||||
--saveqr DIR, -s DIR save QR code(s) as images to directory
|
||||
--camera NUMBER, -C NUMBER camera number of system (default camera: 0)
|
||||
--qr {ZBAR,QREADER,QREADER_DEEP,CV2,CV2_WECHAT}, -Q {ZBAR,QREADER,QREADER_DEEP,CV2,CV2_WECHAT}
|
||||
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 -
|
||||
|
||||
## Dependencies
|
||||
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</pre>
|
||||
|
||||
pip install -r requirements.txt
|
||||
## Examples
|
||||
|
||||
Known to work with
|
||||
### Printing otp secrets form text file
|
||||
|
||||
* Python 3.10.8, protobuf 4.21.9, qrcode 7.3.1, and pillow 9.2
|
||||
* Python 3.11.1, protobuf 4.21.12, qrcode 7.3.1, and pillow 9.2
|
||||
python src/extract_otp_secrets.py example_export.txt
|
||||
|
||||
For protobuf versions 3.14.0 or similar or Python 3.6, use the extract_otp_secret_keys version 1.4.0.
|
||||
### Printing otp secrets from image file
|
||||
|
||||
### Optional
|
||||
python src/extract_otp_secrets.py example_export.png
|
||||
|
||||
For printing QR codes, the qrcode module is required, otherwise it can be omitted.
|
||||
### Writing otp secrets to csv file
|
||||
|
||||
pip install qrcode[pil]
|
||||
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
|
||||
python src/extract_otp_secrets.py example_*.png
|
||||
python src/extract_otp_secrets.py example_export.*
|
||||
python src/extract_otp_secrets.py example_*.txt example_*.png
|
||||
|
||||
### Printing otp secrets from stdin (text)
|
||||
|
||||
python src/extract_otp_secrets.py - < example_export.txt
|
||||
|
||||
### Printing otp secrets from stdin (image)
|
||||
|
||||
python src/extract_otp_secrets.py = < example_export.png
|
||||
|
||||
### Printing otp secrets csv to stdout
|
||||
|
||||
python src/extract_otp_secrets.py --csv - example_export.txt
|
||||
|
||||
### Printing otp secrets csv to stdout without header line
|
||||
|
||||
python src/extract_otp_secrets.py --csv - example_*.png | tail -n+2
|
||||
|
||||
### Reading from stdin and printing to stdout
|
||||
|
||||
cat example_*.txt | python src/extract_otp_secrets.py --csv - - | tail -n+2
|
||||
|
||||
## Features
|
||||
|
||||
* Free and open source
|
||||
* Supports Google Authenticator export
|
||||
* All functionality in one Python script: extract_otp_secret_keys.py (except protobuf generated code in protobuf_generated_python)
|
||||
* Supports TOTP and HOTP
|
||||
* Supports Google Authenticator exports (and compatible apps like Aegis Authenticator)
|
||||
* Captures the the QR codes directly from the camera using different QR code libraries (based on OpenCV) (🆕 since v2.0)
|
||||
* ZBAR: [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) - fast and reliable, good for images and video capture (default and recommended) [if [libzbar](#installation-of-optional-shared-system-libraries-recommended) is installed]
|
||||
* QREADER: [QReader](https://github.com/Eric-Canas/QReader) [if [libzbar](#installation-of-optional-shared-system-libraries-recommended) is installed]
|
||||
* QREADER_DEEP: [QReader](https://github.com/Eric-Canas/QReader) - very slow in GUI [if [libzbar](#installation-of-optional-shared-system-libraries-recommended) is installed]
|
||||
* CV2: [QRCodeDetector](https://docs.opencv.org/4.x/de/dc3/classcv_1_1QRCodeDetector.html)
|
||||
* CV2_WECHAT: [WeChatQRCode](https://docs.opencv.org/4.x/dd/d63/group__wechat__qrcode.html)
|
||||
* Program usable as pure GUI application without any command line switches (🆕 since v2.2)
|
||||
* Save otp secrets as csv file (🆕 since v2.2)
|
||||
* Save otp secrets as json file (🆕 since v2.2)
|
||||
* Save otp secrets as KeePass csv file(s) (🆕 since v2.2)
|
||||
* Supports [TOTP](https://www.ietf.org/rfc/rfc6238.txt) and [HOTP](https://www.ietf.org/rfc/rfc4226.txt) standards
|
||||
* Generates QR codes
|
||||
* Various export formats:
|
||||
* Exports to various formats:
|
||||
* CSV
|
||||
* JSON
|
||||
* Dedicated CSV for KeePass
|
||||
* QR code images
|
||||
* Supports reading from stdin and writing to stdout by specifying '-'
|
||||
* Errors and warnings are written to stderr
|
||||
* Supports reading from stdin and writing to stdout, thus pipes can be used
|
||||
* Handles multiple input files (🆕 since v2.0)
|
||||
* Reads QR codes images: (See [OpenCV docu](https://docs.opencv.org/4.x/d4/da8/group__imgcodecs.html#ga288b8b3da0892bd651fce07b3bbd3a56)) (🆕 since v2.0)
|
||||
* Portable Network Graphics - *.png
|
||||
* WebP - *.webp
|
||||
* JPEG files - *.jpeg, *.jpg, *.jpe
|
||||
* TIFF files - *.tiff, *.tif
|
||||
* Windows bitmaps - *.bmp, *.dib
|
||||
* JPEG 2000 files - *.jp2
|
||||
* Portable image format - *.pbm, *.pgm, *.ppm *.pxm, *.pnm
|
||||
* Prints errors and warnings to stderr (🆕 since v2.0)
|
||||
* Prints colored output (🆕 since v2.0)
|
||||
* Startable as executable (script, Python, and all dependencies packed in one executable) (🆕 since v2.1)
|
||||
* extract_otp_secrets_linux_x86_64 (requires glibc >= 2.31)
|
||||
* extract_otp_secrets_linux_arm64 (requires glibc >= 2.31)
|
||||
* 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)
|
||||
* extract_otp_secrets_macos_x86_64.pkg N/A, see [why](#macos)
|
||||
* Prebuilt Docker images provided for amd64 and arm64 (🆕 since v2.0)
|
||||
* Many ways to run the script:
|
||||
* Native Python
|
||||
* pipenv
|
||||
* pip
|
||||
* venv
|
||||
* Docker
|
||||
* VSCode devcontainer
|
||||
* devbox
|
||||
* pip
|
||||
* Compatible with major platforms:
|
||||
* Linux
|
||||
* macOS
|
||||
* Windows
|
||||
* Uses UTF-8 on all platforms
|
||||
* Supports Python >= 3.8
|
||||
* 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
|
||||
* All functionality in one Python script: src/extract_otp_secrets.py (except protobuf generated code in protobuf_generated_python)
|
||||
|
||||
## KeePass
|
||||
|
||||
@@ -124,21 +412,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 --python_out=protobuf_generated_python google_auth.proto
|
||||
|
||||
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
|
||||
|
||||
## 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
|
||||
|
||||
@@ -151,43 +436,64 @@ The generated protobuf Python code was generated by protoc 21.12 (https://github
|
||||
|
||||
## Alternative installation methods
|
||||
|
||||
### pip
|
||||
### pip using github
|
||||
|
||||
```
|
||||
pip install git+https://github.com/scito/extract_otp_secret_keys
|
||||
python -m extract_otp_secret_keys
|
||||
pip install -U git+https://github.com/scito/extract_otp_secrets
|
||||
extract_otp_secrets
|
||||
```
|
||||
|
||||
#### Example
|
||||
or run it
|
||||
|
||||
```
|
||||
wget https://raw.githubusercontent.com/scito/extract_otp_secret_keys/master/example_export.txt
|
||||
python -m extract_otp_secret_keys example_export.txt
|
||||
python -m extract_otp_secrets
|
||||
```
|
||||
|
||||
or from a specific tag
|
||||
|
||||
```
|
||||
pip install -U git+https://github.com/scito/extract_otp_secrets.git@v2.0.0
|
||||
extract_otp_secrets
|
||||
curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/example_export.txt | python -m extract_otp_secrets -
|
||||
```
|
||||
|
||||
### local pip
|
||||
|
||||
```
|
||||
git clone https://github.com/scito/extract_otp_secrets.git
|
||||
pip install -U -e extract_otp_secrets
|
||||
extract_otp_secrets extract_otp_secrets/example_export.txt
|
||||
```
|
||||
|
||||
or run it
|
||||
|
||||
```
|
||||
python -m extract_otp_secrets extract_otp_secrets/example_export.txt
|
||||
```
|
||||
|
||||
### pipenv
|
||||
|
||||
You can you use [Pipenv](https://github.com/pypa/pipenv) for running extract_otp_secret_keys.
|
||||
You can you use [Pipenv](https://github.com/pypa/pipenv) for running extract_otp_secrets.
|
||||
|
||||
```
|
||||
pipenv --rm
|
||||
pipenv install
|
||||
pipenv shell
|
||||
python extract_otp_secret_keys.py example_export.txt
|
||||
python src/extract_otp_secrets.py example_export.txt
|
||||
```
|
||||
|
||||
### Visual Studio Code Remote - Containers / VSCode devcontainer
|
||||
|
||||
You can you use [VSCode devcontainer](https://code.visualstudio.com/docs/remote/containers-tutorial) for running extract_otp_secret_keys.
|
||||
You can you use [VSCode devcontainer](https://code.visualstudio.com/docs/remote/containers-tutorial) for running extract_otp_secrets.
|
||||
|
||||
Requirement: Docker
|
||||
|
||||
1. Start VSCode
|
||||
2. Open extract_otp_secret_keys.code-workspace
|
||||
2. Open extract_otp_secrets.code-workspace
|
||||
3. Open VSCode command palette (Ctrl-Shift-P)
|
||||
4. Type command "Remote-Containers: Reopen in Container"
|
||||
5. Open integrated bash terminal in VSCode
|
||||
6. Execute: python extract_otp_secret_keys.py example_export.txt
|
||||
6. Execute: python src/extract_otp_secrets.py example_export.txt
|
||||
|
||||
### venv
|
||||
|
||||
@@ -195,8 +501,8 @@ Alternatively, you can use a python virtual env for the dependencies:
|
||||
|
||||
python -m venv venv
|
||||
. venv/bin/activate
|
||||
pip install -r requirements-dev.txt
|
||||
pip install -r requirements.txt
|
||||
pip install -U -r requirements-dev.txt
|
||||
pip install -U -r requirements.txt
|
||||
|
||||
The requirements\*.txt files contain all the dependencies (also the optional ones).
|
||||
To leave the python virtual env just call `deactivate`.
|
||||
@@ -209,22 +515,50 @@ Install [devbox](https://github.com/jetpack-io/devbox), which is a wrapper for n
|
||||
devbox shell
|
||||
```
|
||||
|
||||
### Docker
|
||||
### docker
|
||||
|
||||
Install [Docker](https://docs.docker.com/get-docker/).
|
||||
|
||||
Build and run the app within the container:
|
||||
Prebuilt docker images are available for amd64 and arm64 architectures on [Docker Hub](https://hub.docker.com/repository/docker/scit0/extract_otp_secrets) and on [GitHub Packages](https://github.com/users/scito/packages/container/package/extract_otp_secrets).
|
||||
|
||||
Extracting from an QR image file:
|
||||
|
||||
```bash
|
||||
docker build . -t extract_otp
|
||||
docker run --rm -v "$(pwd)":/files:ro extract_otp -p example_export.txt
|
||||
```
|
||||
curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/example_export.png | docker run --pull always -i --rm -v "$(pwd)":/files:ro scit0/extract_otp_secrets =
|
||||
```
|
||||
|
||||
Capturing from camera in GUI window (X Window system required on host):
|
||||
|
||||
```
|
||||
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
|
||||
```
|
||||
|
||||
If only text processing is required, there is a small Image based on Alpine Linux:
|
||||
|
||||
```
|
||||
curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/example_export.txt | docker run --pull always -i --rm -v "$(pwd)":/files:ro scit0/extract_otp_secrets:latest-only-txt -
|
||||
```
|
||||
|
||||
Docker image from GitHub:
|
||||
|
||||
```
|
||||
docker login ghcr.io -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 ghcr.io/scito/extract_otp_secrets =
|
||||
```
|
||||
|
||||
### More docker examples
|
||||
|
||||
docker run --pull always --rm -v "$(pwd)":/files:ro scit0/extract_otp_secrets example_export.png
|
||||
|
||||
docker run --pull always --rm -i -v "$(pwd)":/files:ro scit0/extract_otp_secrets_only_txt - < example_export.txt
|
||||
|
||||
cat example_export.txt | docker run --pull always --rm -i -v "$(pwd)":/files:ro scit0/extract_otp_secrets:latest_only_txt - -c - > example_out.csv
|
||||
|
||||
## Tests
|
||||
|
||||
### PyTest
|
||||
|
||||
There are basic [pytest](https://pytest.org)s, see `test_extract_otp_secret_keys_pytest.py`.
|
||||
The script is covered by [pytest](https://pytest.org)s, see `extract_otp_secrets_test.py`.
|
||||
|
||||
Run tests:
|
||||
|
||||
@@ -236,9 +570,19 @@ or
|
||||
python -m pytest
|
||||
```
|
||||
|
||||
#### Hints
|
||||
|
||||
Your tests can run against an installed version after executing pip install .
|
||||
|
||||
Your tests can run against the local copy with an editable install after executing pip install --editable .
|
||||
|
||||
If you don’t use an editable install and are relying on the fact that Python by default puts the current directory in sys.path to import your package, you can execute python -m pytest to execute the tests against the local copy directly, without using pip.
|
||||
|
||||
https://docs.pytest.org/en/7.1.x/explanation/pythonpath.html#pytest-vs-python-m-pytest
|
||||
|
||||
### unittest
|
||||
|
||||
There are basic [unittest](https://docs.python.org/3.10/library/unittest.html)s, see `test_extract_otp_secret_keys_unittest.py`.
|
||||
There are some [unittest](https://docs.python.org/3.10/library/unittest.html)s, see `extract_otp_secrets_txt_unit_test.py`.
|
||||
|
||||
Run tests:
|
||||
|
||||
@@ -246,6 +590,8 @@ Run tests:
|
||||
python -m unittest
|
||||
```
|
||||
|
||||
Note the `pytest`s are preferred and complete. For each unittest there is also a test in `pytest`.
|
||||
|
||||
### VSCode Setup
|
||||
|
||||
Setup for running the tests in VSCode.
|
||||
@@ -255,7 +601,19 @@ Setup for running the tests in VSCode.
|
||||
3. Choose unittest or pytest. (pytest is recommended, both are supported)
|
||||
4. Set ". Root" directory
|
||||
|
||||
## Maintenance
|
||||
## Development
|
||||
|
||||
### Build
|
||||
|
||||
```
|
||||
cd extract_otp_secrets/
|
||||
pip install -U -e .
|
||||
python src/extract_otp_secrets.py
|
||||
|
||||
pip wheel .
|
||||
```
|
||||
|
||||
Note: `python -m build --wheel` = `pip wheel --no-deps .`
|
||||
|
||||
### Upgrade pip Packages
|
||||
|
||||
@@ -263,16 +621,146 @@ Setup for running the tests in VSCode.
|
||||
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 -f docker/Dockerfile --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 docker/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
|
||||
-e Build exe
|
||||
-n Build nuitka exe
|
||||
-L Do not build local (exes)
|
||||
-d Build docker
|
||||
-a Build arm
|
||||
-X Do not build x86_64
|
||||
-B Do not build base
|
||||
-V Do not run pipenv
|
||||
-g Start extract_otp_secrets.py in GUI mode
|
||||
-c Clean everything
|
||||
-r Generate result files
|
||||
-h, --help Show help and quit
|
||||
```
|
||||
|
||||
## 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 24.4 (https://github.com/protocolbuffers/protobuf/releases/tag/v24.4).
|
||||
|
||||
For Python type hint generation the [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) package is used.
|
||||
|
||||
## References
|
||||
|
||||
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
||||
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
|
||||
|
||||
## Issues
|
||||
|
||||
* Segmentation fault on macOS with CV2 4.7.0: https://github.com/opencv/opencv/issues/23072
|
||||
* CV2 window does not show icons: https://github.com/opencv/opencv-python/issues/585
|
||||
|
||||
## Problems and Troubleshooting
|
||||
|
||||
### Windows error message
|
||||
|
||||
If you see an ugly ImportError on Windows you will most likely need the [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.
|
||||
|
||||
This library shared library is required by [pyzbar](https://pypi.org/project/pyzbar/).
|
||||
|
||||
```
|
||||
Traceback (most recent call last):
|
||||
File "C:\Users\Admin\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\pyzbar\zbar_library.py", line 58, in load
|
||||
dependencies, libzbar = load_objects(Path(''))
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "C:\Users\Admin\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\pyzbar\zbar_library.py", line 50, in load_objects
|
||||
deps = [
|
||||
^
|
||||
File "C:\Users\Admin\AppData\Local\Packages\PythonSoftwareFoundation.Python.3.11_qbz5n2kfra8p0\LocalCache\local-packages\Python311\site-packages\pyzbar\zbar_library.py", line 51, in <listcomp>
|
||||
cdll.LoadLibrary(str(directory.joinpath(dep)))
|
||||
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.496.0_x64__qbz5n2kfra8p0\Lib\ctypes\__init__.py", line 454, in LoadLibrary
|
||||
return self._dlltype(name)
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.11_3.11.496.0_x64__qbz5n2kfra8p0\Lib\ctypes\__init__.py", line 376, in __init__
|
||||
self._handle = _dlopen(self._name, mode)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
FileNotFoundError: Could not find module 'libiconv.dll' (or one of its dependencies). Try using the full path with constructor syntax.
|
||||
```
|
||||
|
||||
## Related projects
|
||||
|
||||
* [ZBar](https://github.com/mchehab/zbar) is an open source software suite for reading bar codes from various sources, including webcams.
|
||||
* [Aegis Authenticator](https://github.com/beemdevelopment/Aegis) is a free, secure and open source 2FA app for Android.
|
||||
* [Aegis Authenticator](https://github.com/beemdevelopment/Aegis) is a free, secure and open source 2FA app for Android. This app can scan Google export QR codes and export the secrets, e.g. as JSON. However, a second device is required.
|
||||
* [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.
|
||||
|
||||
***
|
||||
|
||||
# #StandWithUkraine 🇺🇦
|
||||
|
||||
I have Ukrainian relatives and friends.
|
||||
|
||||
#RussiaInvadedUkraine on 24 of February 2022, at 05:00 the armed forces of the Russian Federation attacked Ukraine. Please, stand with Ukraine, stay tuned for updates on Ukraine's official sources and channels in English and support Ukraine in its fight for freedom and democracy in Europe.
|
||||
|
||||
675
build.sh
Executable file
@@ -0,0 +1,675 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Upgrades Protoc from https://github.com/protocolbuffers/protobuf/releases
|
||||
|
||||
black='\e[0;30m'
|
||||
blackBold='\e[1;30m'
|
||||
blackBackground='\e[1;40m'
|
||||
red='\e[0;31m'
|
||||
redBold='\e[1;31m'
|
||||
redBackground='\e[0;41m'
|
||||
green='\e[0;32m'
|
||||
greenBold='\e[1;32m'
|
||||
greenBackground='\e[0;42m'
|
||||
yellow='\e[0;33m'
|
||||
yellowBold='\e[1;33m'
|
||||
yellowBackground='\e[0;43m'
|
||||
blue='\e[0;34m'
|
||||
blueBold='\e[1;34m'
|
||||
blueBackground='\e[0;44m'
|
||||
magenta='\e[0;35m'
|
||||
magentaBold='\e[1;35m'
|
||||
magentaBackground='\e[0;45m'
|
||||
cyan='\e[0;36m'
|
||||
cyanBold='\e[1;36m'
|
||||
cyanBackground='\e[0;46m'
|
||||
white='\e[0;37m'
|
||||
whiteBold='\e[1;37m'
|
||||
whiteBackground='\e[0;47m'
|
||||
reset='\e[0m'
|
||||
|
||||
abort() {
|
||||
echo '
|
||||
***************
|
||||
*** ABORTED ***
|
||||
***************
|
||||
' >&2
|
||||
echo "An error occurred on line $1. Exiting..." >&2
|
||||
date -Iseconds >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap 'abort $LINENO' ERR
|
||||
set -e -o pipefail
|
||||
|
||||
quit() {
|
||||
trap : 0
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Asks if [Yn] if script shoud continue, otherwise exit 1
|
||||
# $1: msg or nothing
|
||||
# Example call 1: askContinueYn
|
||||
# Example call 1: askContinueYn "Backup DB?"
|
||||
askContinueYn() {
|
||||
if [[ $1 ]]; then
|
||||
msg="$1 "
|
||||
else
|
||||
msg=""
|
||||
fi
|
||||
|
||||
# http://stackoverflow.com/questions/3231804/in-bash-how-to-add-are-you-sure-y-n-to-any-command-or-alias
|
||||
read -e -p "${msg}Continue? [Y/n] " response
|
||||
response=${response,,} # tolower
|
||||
if [[ $response =~ ^(yes|y|)$ ]] ; then
|
||||
# echo ""
|
||||
# OK
|
||||
:
|
||||
else
|
||||
echo "Aborted"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Reference: https://gist.github.com/steinwaywhw/a4cd19cda655b8249d908261a62687f8
|
||||
|
||||
interactive=false
|
||||
ignore_version_check=true
|
||||
clean=false
|
||||
clean_flag=""
|
||||
build_base=true
|
||||
build_arm=false
|
||||
build_x86_64=true
|
||||
build_docker=false
|
||||
build_local=true
|
||||
build_exe=false
|
||||
build_nuitka_exe=false
|
||||
run_pipenv=true
|
||||
run_gui=false
|
||||
generate_result_files=false
|
||||
PYTHONHASHSEED=31
|
||||
|
||||
while test $# -gt 0; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
echo "Build extract_otp_secrets project"
|
||||
echo
|
||||
echo "$0 [options]"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo "-i Interactive mode, all steps must be confirmed"
|
||||
echo "-C Ignore version check of protobuf/protoc"
|
||||
echo "-e Build exe"
|
||||
echo "-n Build nuitka exe"
|
||||
echo "-L Do not build local (exe)"
|
||||
echo "-d Build docker"
|
||||
echo "-a Build arm"
|
||||
echo "-X Do not build x86_64"
|
||||
echo "-B Do not build base"
|
||||
echo "-V Do not run pipenv"
|
||||
echo "-g Start extract_otp_secrets.py in GUI mode"
|
||||
echo "-c Clean everything"
|
||||
echo "-r Generate result files"
|
||||
echo "-h, --help Show help and quit"
|
||||
quit
|
||||
;;
|
||||
-i)
|
||||
interactive=true
|
||||
shift
|
||||
;;
|
||||
-C)
|
||||
ignore_version_check=false
|
||||
shift
|
||||
;;
|
||||
-B)
|
||||
build_base=false
|
||||
shift
|
||||
;;
|
||||
-L)
|
||||
build_local=false
|
||||
build_base=false
|
||||
shift
|
||||
;;
|
||||
-a)
|
||||
build_arm=true
|
||||
shift
|
||||
;;
|
||||
-X)
|
||||
build_x86_64=false
|
||||
shift
|
||||
;;
|
||||
-e)
|
||||
build_exe=true
|
||||
shift
|
||||
;;
|
||||
-n)
|
||||
build_nuitka_exe=true
|
||||
shift
|
||||
;;
|
||||
-d)
|
||||
build_docker=true
|
||||
shift
|
||||
;;
|
||||
-V)
|
||||
run_pipenv=false
|
||||
shift
|
||||
;;
|
||||
-g)
|
||||
run_gui=true
|
||||
shift
|
||||
;;
|
||||
-r)
|
||||
generate_result_files=true
|
||||
shift
|
||||
;;
|
||||
-c)
|
||||
clean=true
|
||||
clean_flag="--clean"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
BIN="$HOME/bin"
|
||||
DOWNLOADS="$HOME/downloads"
|
||||
|
||||
PYTHON="python3.11"
|
||||
PIP="pip3.11"
|
||||
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"
|
||||
|
||||
if $clean; then
|
||||
cmd="docker image prune -f || echo 'No docker image pruned'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP uninstall -y extract-otp-secrets || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze | grep -v -E '^-e|^#' | xargs sudo $PIP uninstall -y || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze --user | grep -v -E '^-e|^#' | xargs $PIP uninstall -y || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze | cut -d \"@\" -f1 | xargs pip uninstall -y || echo Nothing to do"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="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"
|
||||
|
||||
cmd="pipenv --rm || true"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="sudo pipenv --rm || true"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="mkdir -p dist"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_local; then
|
||||
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"
|
||||
|
||||
NAME="protoc-$VERSION"
|
||||
ARCHIVE="$NAME.zip"
|
||||
|
||||
mkdir -p $DOWNLOADS
|
||||
# https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protoc-21.6-linux-x86_64.zip
|
||||
cmd="wget --trust-server-names https://github.com/protocolbuffers/protobuf/releases/download/v$VERSION/protoc-$VERSION-linux-x86_64.zip -O $DOWNLOADS/$ARCHIVE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="echo -e '\nSize [Byte]'; stat --printf='%s\n' $DOWNLOADS/$ARCHIVE; echo -e '\nMD5'; md5sum $DOWNLOADS/$ARCHIVE; echo -e '\nSHA256'; sha256sum $DOWNLOADS/$ARCHIVE;"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="mkdir -p $BIN/$NAME; unzip $DOWNLOADS/$ARCHIVE -d $BIN/$NAME"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="echo $VERSION > $BIN/$NAME/.VERSION.txt; echo $VERSION > $BIN/$NAME/.VERSION_$VERSION.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="[ -d $BIN/$DEST.old ] && rm -rf $BIN/$DEST.old || echo 'No old dir to delete'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="[ -d $BIN/$DEST ] && mv -iT $BIN/$DEST $BIN/$DEST.old || echo 'No previous dir to keep'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="mv -iT $BIN/$NAME $BIN/$DEST"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="rm $DOWNLOADS/$ARCHIVE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$BIN/$DEST/bin/protoc --plugin=protoc-gen-mypy=$HOME/.local/bin/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python --proto_path=src google_auth.proto"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Update README.md
|
||||
|
||||
cmd="perl -i -pe 's%proto(buf|c)([- ])(\d\.)?$OLDVERSION%proto\$1\$2\${3}$VERSION%g' README.md && perl -i -pe 's%(protobuf/releases/tag/v)$OLDVERSION%\${1}$VERSION%g' README.md"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
else
|
||||
echo -e "\nVersion has not changed. Quit"
|
||||
fi
|
||||
|
||||
# Upgrade pip requirements
|
||||
|
||||
cmd="pip install -U pip"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIP --version
|
||||
|
||||
cmd="$PIP install --use-pep517 -U -r requirements.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
if $build_base; then
|
||||
# Lint
|
||||
|
||||
LINT_OUT_FILE="tests/reports/flake8_results.txt"
|
||||
cmd="$FLAKE8 . --count --select=E9,F63,F7,F82 --show-source --statistics | tee $LINT_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$FLAKE8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,protobuf_generated_python | tee -a $LINT_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Type checking
|
||||
|
||||
TYPE_CHECK_OUT_FILE="tests/reports/mypy_results.txt"
|
||||
cmd="$MYPY --install-types --non-interactive src/*.py tests/*.py"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# change to src as python -m mypy adds the current dir Python sys.path
|
||||
# execute in a subshell in order not to loose the exit code and not to change the dir in the currrent shell
|
||||
cmd="$MYPY --strict src/*.py tests/*.py | tee $TYPE_CHECK_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
|
||||
# Generate results files
|
||||
|
||||
if $generate_result_files; then
|
||||
cmd="for color in '' '-n'; do for level in '' '-v' '-vv' '-vvv'; do $PYTHON src/extract_otp_secrets.py example_export.txt \$color \$level > tests/data/print_verbose_output\$color\$level.txt; done; done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# pip -e install
|
||||
|
||||
cmd="$PIP install -U -e ."
|
||||
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
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PYTHON src/extract_otp_secrets.py - < example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
COVERAGE_OUT_FILE="tests/reports/pytest-coverage.txt"
|
||||
cmd="pytest --cov=extract_otp_secrets_test --junitxml=tests/reports/pytest.xml --cov-report html:tests/reports/html --cov-report=term-missing tests/ | tee $COVERAGE_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Pipenv
|
||||
|
||||
if $run_pipenv; then
|
||||
cmd="$PIP install -U pipenv"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIPENV --version
|
||||
|
||||
cmd="$PIPENV --rm && $PIPENV update && $PIPENV install"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIPENV run python --version
|
||||
|
||||
cmd="$PIPENV run pytest --cov=extract_otp_secrets_test tests/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# Build wheel
|
||||
|
||||
cmd="$PIP wheel ."
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# Build executable
|
||||
if $build_exe; then
|
||||
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"
|
||||
|
||||
cmd="dist/extract_otp_secrets --qr ZBAR example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# Build compiled executable
|
||||
|
||||
if $build_nuitka_exe; then
|
||||
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="$PIP install -U pyqt5"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PYTHON -m nuitka --enable-plugin=tk-inter --enable-plugin=pyqt5 --include-data-dir=$HOME/.local/__yolo_v3_qr_detector/=__yolo_v3_qr_detector/ --onefile --output-dir=build/nuitka --output-filename=extract_otp_secrets_compiled src/extract_otp_secrets.py"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="cp build/nuitka/extract_otp_secrets_compiled dist/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets_compiled -h"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets_compiled --qr ZBAR example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# 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
|
||||
|
||||
# https://github.com/marketplace/actions/pytest-coverage-comment
|
||||
# Coverage-95%25-yellowgreen
|
||||
echo -e "Update code coverage in README.md"
|
||||
TOTAL_COVERAGE=$(cat $COVERAGE_OUT_FILE | grep 'TOTAL' | perl -ne 'print "$&" if /\b(\d{1,3})%/') && perl -i -pe "s/coverage-(\d{1,3}%)25-/coverage-${TOTAL_COVERAGE}25-/" README.md
|
||||
|
||||
# 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"
|
||||
fi
|
||||
|
||||
if $build_docker; then
|
||||
# Build docker
|
||||
|
||||
if $build_x86_64; then
|
||||
# Build Dockerfile_only_txt (Alpine)
|
||||
cmd="docker build . -t extract_otp_secrets_only_txt -t extract_otp_secrets:only-txt -t extract_otp_secrets:alpine -f docker/Dockerfile_only_txt --pull --build-arg RUN_TESTS=false"
|
||||
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_only_txt example_export.txt"
|
||||
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_only_txt - < example_export.txt"
|
||||
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 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 Bookworm)
|
||||
cmd="docker build . -t extract_otp_secrets -t extract_otp_secrets:bookworm --pull -f docker/Dockerfile --build-arg RUN_TESTS=false"
|
||||
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 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 - -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 = < 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"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build extract_otp_secrets (Debian Bullseye)
|
||||
cmd="docker build . -t extract_otp_secrets:bullseye -t extract_otp_secrets:bullseye-x86_64 --pull -f docker/Dockerfile --build-arg RUN_TESTS=false --build-arg BASE_IMAGE=python:3.12-slim-bullseye"
|
||||
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:bullseye 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:bullseye - -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:bullseye = < 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:bullseye"
|
||||
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
|
||||
BOOKWORM_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
|
||||
echo "Bookworm glibc: $BOOKWORM_GLIBC_VERSION"
|
||||
fi
|
||||
|
||||
if $build_arm; then
|
||||
# build linux/arm64
|
||||
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"
|
||||
|
||||
# Build extract_otp_secrets (Debian Bullseye)
|
||||
cmd="docker buildx build --platform=linux/arm64 . -t extract_otp_secrets:bullseye-arm64 --pull -f docker/Dockerfile --build-arg RUN_TESTS=false --build-arg BASE_IMAGE=python:3.12-slim-bullseye"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_exe; then
|
||||
if $build_x86_64; then
|
||||
cmd="docker run --platform linux/amd64 --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_bookworm --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_bookworm -h || echo 'Could not run exe; see error message above'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets_linux_x86_64_bookworm --qr ZBAR example_export.png || echo 'Could not run exe; see error message above'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build executable from Docker bullseye
|
||||
BULLSEYE_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets:bullseye -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
|
||||
echo "Bullseye glibc: $BULLSEYE_GLIBC_VERSION"
|
||||
|
||||
cmd="docker run --platform linux/amd64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye -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"
|
||||
|
||||
cmd="dist/extract_otp_secrets_linux_x86_64 --qr ZBAR example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_arm; then
|
||||
# build linux/arm64
|
||||
cmd="docker run --platform linux/arm64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye-arm64 -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:bullseye-arm64 -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"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $build_nuitka_exe; then
|
||||
if $build_x86_64; then
|
||||
cmd="docker run --platform linux/amd64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets -c 'apt-get update && apt-get -y install binutils build-essential patchelf && pip install -U pip && pip install -U -r /files/requirements.txt && pip install nuitka pyqt5 && PYTHONHASHSEED=31 && python -m nuitka --enable-plugin=tk-inter --enable-plugin=pyqt5 --include-data-dir=/usr/local/__yolo_v3_qr_detector/=__yolo_v3_qr_detector/ --onefile --output-dir=/files/build/docker/nuitka --output-filename=extract_otp_secrets_linux_x86_64_bookworm_compiled /files/src/extract_otp_secrets.py'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="build/docker/nuitka/extract_otp_secrets_linux_x86_64_bookworm_compiled -h"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="build/docker/nuitka/extract_otp_secrets_linux_x86_64_bookworm_compiled --qr ZBAR example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="cp build/docker/nuitka/extract_otp_secrets_linux_x86_64_bookworm_compiled dist/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build executable from Docker bullseye
|
||||
BULLSEYE_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets:bullseye -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
|
||||
echo "Bookworm glibc: $BULLSEYE_GLIBC_VERSION"
|
||||
|
||||
cmd="docker run --platform linux/amd64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye -c 'apt-get update && apt-get -y install binutils build-essential patchelf && pip install -U pip && pip install -U -r /files/requirements.txt && pip install nuitka pyqt5 && PYTHONHASHSEED=31 && python -m nuitka --enable-plugin=tk-inter --enable-plugin=pyqt5 --include-data-dir=/usr/local/__yolo_v3_qr_detector/=__yolo_v3_qr_detector/ --onefile --output-dir=/files/build/docker/nuitka --output-filename=extract_otp_secrets_linux_x86_64_bullseye_compiled /files/src/extract_otp_secrets.py'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="build/docker/nuitka/extract_otp_secrets_linux_x86_64_bullseye_compiled -h"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="build/docker/nuitka/extract_otp_secrets_linux_x86_64_bullseye_compiled --qr ZBAR example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="cp build/docker/nuitka/extract_otp_secrets_linux_x86_64_bullseye_compiled dist/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_arm; then
|
||||
# build linux/arm64
|
||||
cmd="docker run --platform linux/arm64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye-arm64 -c 'apt-get update && apt-get -y install binutils build-essential patchelf qt5-default && pip install -U pip && pip install -U -r /files/requirements.txt && pip install nuitka pyqt5 && PYTHONHASHSEED=31 && python -m nuitka --enable-plugin=tk-inter --enable-plugin=pyqt5 --include-data-dir=/usr/local/__yolo_v3_qr_detector/=__yolo_v3_qr_detector/ --onefile --output-dir=/files/build/docker/nuitka --output-filename=extract_otp_secrets_linux_arm64_compiled /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='build/docker/nuitka/extract_otp_secrets_linux_arm64_compiled' && docker run --platform \"\$PLATFORM\" --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye-arm64 -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"
|
||||
|
||||
cmd="cp build/docker/nuitka/extract_otp_secrets_linux_arm64_compiled dist/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run GUI from Docker
|
||||
if $build_x86_64 && $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
|
||||
eval "$cmd"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $run_gui; then
|
||||
cmd="$PYTHON src/extract_otp_secrets.py &"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_base; then
|
||||
line=$(printf '#%.0s' $(eval echo {1..$(( ($COLUMNS - 10) / 2))}))
|
||||
echo -e "\n${blueBold}$line RESULTS $line${reset}"
|
||||
|
||||
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"
|
||||
fi
|
||||
|
||||
line=$(printf '#%.0s' $(eval echo {1..$(( ($COLUMNS - 10) / 2))}))
|
||||
echo -e "\n${greenBold}$line SUCCESS $line${reset}"
|
||||
|
||||
git status
|
||||
|
||||
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'
|
||||
36
docker/Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
||||
# --build-arg BASE_IMAGE=python:3.11-slim-buster
|
||||
ARG BASE_IMAGE=python:3.12-slim-bookworm
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
# https://docs.docker.com/engine/reference/builder/
|
||||
|
||||
# For debugging
|
||||
# docker build . -t extract_otp_secrets --pull --build-arg RUN_TESTS=false
|
||||
# docker run --rm -v "$(pwd)":/files:ro extract_otp_secrets
|
||||
# docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets
|
||||
# docker run --entrypoint /bin/bash -it --rm -v "$(pwd)":/files:ro --device="/dev/video0:/dev/video0" --env="DISPLAY" -v /tmp/.X11-unix:/tmp/.X11-unix:ro extract_otp_secrets
|
||||
|
||||
WORKDIR /extract
|
||||
|
||||
COPY requirements*.txt src/ run_pytest.sh pytest.ini tests/ example_*.txt example_*.png example_*.csv example*.json docker/.alias ./
|
||||
|
||||
ARG RUN_TESTS=true
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libgl1 \
|
||||
libglib2.0-0 \
|
||||
libsm6 \
|
||||
libzbar0 \
|
||||
python3-tk \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& pip install --no-cache-dir -U pip -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/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"
|
||||
48
docker/Dockerfile_only_txt
Normal file
@@ -0,0 +1,48 @@
|
||||
ARG BASE_IMAGE=python:3.12-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/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 requirements*.txt src/ run_pytest.sh pytest.ini tests/ example_*.txt example_*.png example_*.csv example*.json docker/.alias ./
|
||||
|
||||
ARG RUN_TESTS=true
|
||||
|
||||
RUN apk add --no-cache \
|
||||
jpeg \
|
||||
zlib \
|
||||
&& echo "Arch: $(apk --print-arch)" \
|
||||
&& if [[ "$(apk --print-arch)" == "aarch64" ]]; then apk add --no-cache --virtual .build-deps \
|
||||
gcc \
|
||||
jpeg-dev \
|
||||
libc-dev \
|
||||
py3-setuptools \
|
||||
python3-dev \
|
||||
zlib-dev \
|
||||
; fi \
|
||||
&& pip install --no-cache-dir -U \
|
||||
pip \
|
||||
colorama \
|
||||
Pillow \
|
||||
protobuf \
|
||||
qrcode \
|
||||
&& if [[ "$(apk --print-arch)" == "aarch64" ]]; then apk del .build-deps; 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/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"
|
||||
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](#macos)
|
||||
- [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 |
BIN
docs/cv2_capture_screenshot.png
Normal file
|
After Width: | Height: | Size: 528 KiB |
3
docs/meta.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#scito/extract_otp_secrets&Date)
|
||||
BIN
example_export.png
Normal file
|
After Width: | Height: | Size: 653 KiB |
@@ -1,4 +1,5 @@
|
||||
# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/
|
||||
|
||||
# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
||||
|
||||
@@ -1,325 +0,0 @@
|
||||
# Extract two-factor authentication (2FA, TFA) secret keys from export QR codes of "Google Authenticator" app
|
||||
#
|
||||
# Usage:
|
||||
# 1. Export the QR codes from "Google Authenticator" app
|
||||
# 2. Read QR codes with QR code reader (e.g. with a second device)
|
||||
# 3. Save the captured QR codes in a text file. Save each QR code on a new line. (The captured QR codes look like "otpauth-migration://offline?data=...")
|
||||
# 4. Call this script with the file as input:
|
||||
# python extract_otp_secret_keys.py example_export.txt
|
||||
#
|
||||
# Requirement:
|
||||
# The protobuf package of Google for proto3 is required for running this script.
|
||||
# pip install protobuf
|
||||
#
|
||||
# Optional:
|
||||
# For printing QR codes, the qrcode module is required
|
||||
# pip install qrcode
|
||||
#
|
||||
# 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):
|
||||
# protoc --python_out=generated_python google_auth.proto
|
||||
#
|
||||
# References:
|
||||
# Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
||||
# Template code: https://github.com/beemdevelopment/Aegis/pull/406
|
||||
|
||||
# Author: Scito (https://scito.ch)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import fileinput
|
||||
import sys
|
||||
import csv
|
||||
import json
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, quote
|
||||
from os import path, makedirs
|
||||
from re import compile as rcompile
|
||||
import protobuf_generated_python.google_auth_pb2
|
||||
|
||||
|
||||
def sys_main():
|
||||
main(sys.argv[1:])
|
||||
|
||||
|
||||
def main(sys_args):
|
||||
global verbose, quiet
|
||||
|
||||
# allow to use sys.stdout with with (avoid closing)
|
||||
sys.stdout.close = lambda: None
|
||||
|
||||
args = parse_args(sys_args)
|
||||
verbose = args.verbose if args.verbose else 0
|
||||
quiet = args.quiet
|
||||
|
||||
otps = extract_otps(args)
|
||||
write_csv(args, otps)
|
||||
write_keepass_csv(args, otps)
|
||||
write_json(args, otps)
|
||||
|
||||
|
||||
def parse_args(sys_args):
|
||||
formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=52)
|
||||
arg_parser = argparse.ArgumentParser(formatter_class=formatter)
|
||||
arg_parser.add_argument('infile', help='file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
|
||||
arg_parser.add_argument('--json', '-j', help='export json file or - for stdout', metavar=('FILE'))
|
||||
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('--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'))
|
||||
output_group = arg_parser.add_mutually_exclusive_group()
|
||||
output_group.add_argument('--verbose', '-v', help='verbose output', action='count')
|
||||
output_group.add_argument('--quiet', '-q', help='no stdout output, except output set by -', action='store_true')
|
||||
args = arg_parser.parse_args(sys_args)
|
||||
if args.csv == '-' or args.json == '-' or args.keepass == '-':
|
||||
args.quiet = args.q = True
|
||||
return args
|
||||
|
||||
|
||||
def extract_otps(args):
|
||||
global verbose, quiet
|
||||
quiet = args.quiet
|
||||
|
||||
otps = []
|
||||
|
||||
i = j = 0
|
||||
finput = fileinput.input(args.infile)
|
||||
try:
|
||||
for line in (line.strip() for line in finput):
|
||||
if verbose: print(line)
|
||||
if line.startswith('#') or line == '': continue
|
||||
i += 1
|
||||
payload = get_payload_from_line(line, i, args)
|
||||
|
||||
# pylint: disable=no-member
|
||||
for raw_otp in payload.otp_parameters:
|
||||
j += 1
|
||||
if verbose: print('\n{}. Secret Key'.format(j))
|
||||
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
||||
otp_type_enum = 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 = {
|
||||
"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 not quiet:
|
||||
print_otp(otp)
|
||||
if args.printqr:
|
||||
print_qr(args, otp_url)
|
||||
if args.saveqr:
|
||||
save_qr(otp, args, j)
|
||||
if not quiet:
|
||||
print()
|
||||
|
||||
otps.append(otp)
|
||||
finally:
|
||||
finput.close()
|
||||
return otps
|
||||
|
||||
|
||||
def get_payload_from_line(line, i, args):
|
||||
global verbose
|
||||
if not line.startswith('otpauth-migration://'):
|
||||
eprint('\nWARN: line is not a otpauth-migration:// URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line))
|
||||
parsed_url = urlparse(line)
|
||||
if verbose > 1: print('\nDEBUG: parsed_url={}'.format(parsed_url))
|
||||
try:
|
||||
params = parse_qs(parsed_url.query, strict_parsing=True)
|
||||
except: # Not necessary for Python >= 3.11
|
||||
params = []
|
||||
if verbose > 1: print('\nDEBUG: querystring params={}'.format(params))
|
||||
if 'data' not in params:
|
||||
eprint('\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line))
|
||||
sys.exit(1)
|
||||
data_base64 = params['data'][0]
|
||||
if verbose > 1: print('\nDEBUG: data_base64={}'.format(data_base64))
|
||||
data_base64_fixed = data_base64.replace(' ', '+')
|
||||
if verbose > 1: print('\nDEBUG: data_base64_fixed={}'.format(data_base64))
|
||||
data = base64.b64decode(data_base64_fixed, validate=True)
|
||||
payload = protobuf_generated_python.google_auth_pb2.MigrationPayload()
|
||||
try:
|
||||
payload.ParseFromString(data)
|
||||
except:
|
||||
eprint('\nERROR: Cannot decode otpauth-migration migration payload.')
|
||||
eprint('data={}'.format(data_base64))
|
||||
exit(1);
|
||||
if verbose:
|
||||
print('\n{}. Payload Line'.format(i), payload, sep='\n')
|
||||
|
||||
return payload
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf
|
||||
def get_enum_name_by_number(parent, field_name):
|
||||
field_value = getattr(parent, field_name)
|
||||
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name
|
||||
|
||||
|
||||
def get_otp_type_str_from_code(otp_type):
|
||||
return 'totp' if otp_type == 2 else 'hotp'
|
||||
|
||||
|
||||
def convert_secret_from_bytes_to_base32_str(bytes):
|
||||
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
|
||||
|
||||
|
||||
def build_otp_url(secret, raw_otp):
|
||||
url_params = {'secret': secret}
|
||||
if raw_otp.type == 1: url_params['counter'] = raw_otp.counter
|
||||
if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
|
||||
otp_url = 'otpauth://{}/{}?'.format(get_otp_type_str_from_code(raw_otp.type), quote(raw_otp.name)) + urlencode(url_params)
|
||||
return otp_url
|
||||
|
||||
|
||||
def print_otp(otp):
|
||||
print('Name: {}'.format(otp['name']))
|
||||
print('Secret: {}'.format(otp['secret']))
|
||||
if otp['issuer']: print('Issuer: {}'.format(otp['issuer']))
|
||||
print('Type: {}'.format(otp['type']))
|
||||
if otp['type'] == 'hotp':
|
||||
print('Counter: {}'.format(otp['counter']))
|
||||
if verbose:
|
||||
print(otp['url'])
|
||||
|
||||
|
||||
def save_qr(otp, args, j):
|
||||
dir = args.saveqr
|
||||
if not (path.exists(dir)): makedirs(dir, exist_ok=True)
|
||||
pattern = rcompile(r'[\W_]+')
|
||||
file_otp_name = pattern.sub('', otp['name'])
|
||||
file_otp_issuer = pattern.sub('', otp['issuer'])
|
||||
save_qr_file(args, otp['url'], '{}/{}-{}{}.png'.format(dir, j, file_otp_name, '-' + file_otp_issuer if file_otp_issuer else ''))
|
||||
return file_otp_issuer
|
||||
|
||||
|
||||
def save_qr_file(args, data, name):
|
||||
from qrcode import QRCode
|
||||
global verbose
|
||||
qr = QRCode()
|
||||
qr.add_data(data)
|
||||
img = qr.make_image(fill_color='black', back_color='white')
|
||||
if verbose: print('Saving to {}'.format(name))
|
||||
img.save(name)
|
||||
|
||||
|
||||
def print_qr(args, data):
|
||||
from qrcode import QRCode
|
||||
qr = QRCode()
|
||||
qr.add_data(data)
|
||||
qr.print_ascii()
|
||||
|
||||
|
||||
def write_csv(args, otps):
|
||||
global verbose, quiet
|
||||
if args.csv and len(otps) > 0:
|
||||
with open_file_or_stdout_for_csv(args.csv) as outfile:
|
||||
writer = csv.DictWriter(outfile, otps[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(otps)
|
||||
if not quiet: print("Exported {} otps to csv {}".format(len(otps), args.csv))
|
||||
|
||||
|
||||
def write_keepass_csv(args, otps):
|
||||
global verbose, quiet
|
||||
if args.keepass and len(otps) > 0:
|
||||
has_totp = has_otp_type(otps, 'totp')
|
||||
has_hotp = has_otp_type(otps, 'hotp')
|
||||
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")
|
||||
count_totp_entries = 0
|
||||
count_hotp_entries = 0
|
||||
if has_totp:
|
||||
with open_file_or_stdout_for_csv(otp_filename_totp) as outfile:
|
||||
writer = csv.DictWriter(outfile, ["Title", "User Name", "TimeOtp-Secret-Base32", "Group"])
|
||||
writer.writeheader()
|
||||
for otp in otps:
|
||||
if otp['type'] == 'totp':
|
||||
writer.writerow({
|
||||
'Title': otp['issuer'],
|
||||
'User Name': otp['name'],
|
||||
'TimeOtp-Secret-Base32': otp['secret'] if otp['type'] == 'totp' else None,
|
||||
'Group': "OTP/{}".format(otp['type'].upper())
|
||||
})
|
||||
count_totp_entries += 1
|
||||
if has_hotp:
|
||||
with open_file_or_stdout_for_csv(otp_filename_hotp) as outfile:
|
||||
writer = csv.DictWriter(outfile, ["Title", "User Name", "HmacOtp-Secret-Base32", "HmacOtp-Counter", "Group"])
|
||||
writer.writeheader()
|
||||
for otp in otps:
|
||||
if otp['type'] == 'hotp':
|
||||
writer.writerow({
|
||||
'Title': otp['issuer'],
|
||||
'User Name': otp['name'],
|
||||
'HmacOtp-Secret-Base32': otp['secret'] if otp['type'] == 'hotp' else None,
|
||||
'HmacOtp-Counter': otp['counter'] if otp['type'] == 'hotp' else None,
|
||||
'Group': "OTP/{}".format(otp['type'].upper())
|
||||
})
|
||||
count_hotp_entries += 1
|
||||
if not quiet:
|
||||
if count_totp_entries > 0: print("Exported {} totp entries to keepass csv file {}".format(count_totp_entries, otp_filename_totp))
|
||||
if count_hotp_entries > 0: print("Exported {} hotp entries to keepass csv file {}".format(count_hotp_entries, otp_filename_hotp))
|
||||
|
||||
|
||||
def write_json(args, otps):
|
||||
global verbose, quiet
|
||||
if args.json:
|
||||
with open_file_or_stdout(args.json) as outfile:
|
||||
json.dump(otps, outfile, indent=4)
|
||||
if not quiet: print("Exported {} otp entries to json {}".format(len(otps), args.json))
|
||||
|
||||
|
||||
def has_otp_type(otps, otp_type):
|
||||
for otp in otps:
|
||||
if otp['type'] == otp_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_pre_suffix(file, pre_suffix):
|
||||
'''filename.ext, pre -> filename.pre.ext'''
|
||||
name, ext = path.splitext(file)
|
||||
return name + "." + pre_suffix + (ext if ext else "")
|
||||
|
||||
|
||||
def open_file_or_stdout(filename):
|
||||
'''stdout is denoted as "-".
|
||||
Note: Set before the following line:
|
||||
sys.stdout.close = lambda: None'''
|
||||
return open(filename, "w", encoding='utf-8') if filename != '-' else sys.stdout
|
||||
|
||||
|
||||
def open_file_or_stdout_for_csv(filename):
|
||||
'''stdout is denoted as "-".
|
||||
newline=''
|
||||
Note: Set before the following line:
|
||||
sys.stdout.close = lambda: None'''
|
||||
return open(filename, "w", encoding='utf-8', newline='') if filename != '-' else sys.stdout
|
||||
|
||||
|
||||
def eprint(*args, **kwargs):
|
||||
'''Print to stderr.'''
|
||||
print(*args, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys_main()
|
||||
@@ -11,10 +11,10 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Python: extract_otp_secret_keys.py",
|
||||
"name": "Python: extract_otp_secrets.py",
|
||||
"type": "python",
|
||||
"request": "launch",
|
||||
"program": "extract_otp_secret_keys.py",
|
||||
"program": "extract_otp_secrets.py",
|
||||
"args": [
|
||||
"example_export.txt"
|
||||
],
|
||||
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])])
|
||||
]
|
||||
)
|
||||
75
pyproject.toml
Normal file
@@ -0,0 +1,75 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"pip",
|
||||
"nuitka",
|
||||
# https://setuptools-git-versioning.readthedocs.io/en/latest/differences.html
|
||||
"setuptools>=64.0.0",
|
||||
"setuptools-git-versioning",
|
||||
"wheel>=0.37.0",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "extract_otp_secrets"
|
||||
# https://pypi.org/classifiers/
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Environment :: X11 Applications :: Qt",
|
||||
"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",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Intended Audience :: End Users/Desktop",
|
||||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"Programming Language :: Python",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: Microsoft :: Windows :: Windows 10",
|
||||
"Operating System :: Microsoft :: Windows :: Windows 11",
|
||||
"Operating System :: MacOS",
|
||||
"Natural Language :: English",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Typing :: Typed",
|
||||
]
|
||||
dependencies = [
|
||||
"colorama>=0.4.6",
|
||||
"opencv-contrib-python; sys_platform != 'darwin'",
|
||||
"opencv-contrib-python<=4.7.0; sys_platform == 'darwin'",
|
||||
"Pillow",
|
||||
"protobuf",
|
||||
"pyzbar",
|
||||
"qrcode",
|
||||
"qreader<2.0.0",
|
||||
]
|
||||
description = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as 'Google Authenticator'"
|
||||
dynamic = ["version"]
|
||||
keywords = ["python", "security", "json", "otp", "csv", "protobuf", "qrcode", "two-factor", "totp", "google-authenticator", "recovery", "proto3", "mfa", "two-factor-authentication", "tfa", "qr-codes", "otpauth", "2fa", "security-tools", "cv2"]
|
||||
license = {text = "GNU General Public License v3 (GPLv3)"}
|
||||
readme = "README.md"
|
||||
authors = [{name = "scito", email = "info@scito.ch"}]
|
||||
maintainers = [{name = "scito", email = "info@scito.ch"}]
|
||||
requires-python = ">=3.7, <4"
|
||||
scripts = {extract_otp_secrets = "extract_otp_secrets:sys_main"}
|
||||
urls = {Project-URL = "https://github.com/scito/extract_otp_secrets", Bug-Reports = "https://github.com/scito/extract_otp_secrets/issues", Source = "https://github.com/scito/extract_otp_secrets"}
|
||||
|
||||
# [tool.setuptools]
|
||||
# Still in beta, once it is stable move config from setup.cfg to pyproject.toml
|
||||
# py-modules = ["extract_otp_secrets", "protobuf_generated_python.protobuf_generated_python"]
|
||||
|
||||
# [tool.setuptools.dynamic]
|
||||
# version = {attr = "extract_otp_secrets.VERSION"}
|
||||
|
||||
[tool.setuptools-git-versioning]
|
||||
enabled = true
|
||||
|
||||
# https://blog.ionelmc.ro/2014/05/25/python-packaging/#the-structure%3E
|
||||
# https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#which-import-mode
|
||||
[tool.pytest.ini_options]
|
||||
addopts = [ "--import-mode=importlib", ]
|
||||
3
pytest.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
[pytest]
|
||||
markers =
|
||||
qreader: QR image reader tests
|
||||
@@ -1,4 +1,15 @@
|
||||
wheel
|
||||
pytest
|
||||
build
|
||||
flake8
|
||||
gfm-toc
|
||||
mypy
|
||||
mypy-protobuf
|
||||
nuitka
|
||||
pyinstaller
|
||||
pylint
|
||||
pytest
|
||||
pytest-cov
|
||||
pytest-mock
|
||||
setuptools
|
||||
setuptools-git-versioning
|
||||
types-protobuf
|
||||
wheel
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
protobuf
|
||||
qrcode
|
||||
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'
|
||||
|
||||
5
run_pytest.sh
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
cd /extract
|
||||
mkdir -p tests
|
||||
ln -sf /extract/data tests/data
|
||||
pip install -U pytest pytest-mock && pytest "$@"
|
||||
16
setup.cfg
Normal file
@@ -0,0 +1,16 @@
|
||||
[metadata]
|
||||
name = extract_otp_secrets
|
||||
|
||||
[options]
|
||||
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]
|
||||
# where=src
|
||||
61
setup.py
@@ -1,60 +1,5 @@
|
||||
import pathlib
|
||||
#!/usr/bin/python3
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='extract_otp_secret_keys',
|
||||
version='1.6.0',
|
||||
description='Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of "Google Authenticator" app',
|
||||
|
||||
long_description=(pathlib.Path(__file__).parent / 'README.md').read_text(),
|
||||
long_description_content_type='text/markdown',
|
||||
|
||||
url='https://github.com/scito/extract_otp_secret_keys',
|
||||
author='scito',
|
||||
author_email='info@scito.ch',
|
||||
|
||||
classifiers=[
|
||||
'Development Status :: 5 - Production/Stable',
|
||||
|
||||
'Environment :: Console',
|
||||
'Topic :: System :: Archiving :: Backup',
|
||||
'Topic :: Utilities',
|
||||
|
||||
'Programming Language :: Python :: 3.7',
|
||||
'Programming Language :: Python :: 3.8',
|
||||
'Programming Language :: Python :: 3.9',
|
||||
'Programming Language :: Python :: 3.10',
|
||||
'Programming Language :: Python :: 3.11',
|
||||
|
||||
'Intended Audience :: End Users/Desktop',
|
||||
'Intended Audience :: Developers',
|
||||
'Intended Audience :: System Administrators',
|
||||
|
||||
'Programming Language :: Python'
|
||||
'Natural Language :: English',
|
||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
||||
],
|
||||
|
||||
keywords='python security json otp csv protobuf qrcode two-factor totp google-authenticator recovery proto3 mfa two-factor-authentication tfa qr-codes otpauth 2fa security-tools',
|
||||
|
||||
py_modules=['extract_otp_secret_keys', 'protobuf_generated_python.google_auth_pb2'],
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
'extract_otp_secret_keys = extract_otp_secret_keys:sys_main',
|
||||
]
|
||||
},
|
||||
|
||||
python_requires='>=3.7, <4',
|
||||
install_requires=[
|
||||
'protobuf',
|
||||
'qrcode',
|
||||
'Pillow'
|
||||
],
|
||||
|
||||
project_urls={
|
||||
'Bug Reports': 'https://github.com/scito/extract_otp_secret_keys/issues',
|
||||
'Source': 'https://github.com/scito/extract_otp_secret_keys',
|
||||
},
|
||||
|
||||
license='GNU General Public License v3 (GPLv3)',
|
||||
)
|
||||
# compatibility with legacy builds or versions of tools that don’t support certain packaging standards
|
||||
setup()
|
||||
|
||||
930
src/extract_otp_secrets.py
Normal file
@@ -0,0 +1,930 @@
|
||||
# Extract one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as "Google Authenticator"
|
||||
#
|
||||
# For more information, see README.md
|
||||
#
|
||||
# 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 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):
|
||||
# protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python --proto_path=src google_auth.proto
|
||||
#
|
||||
# References:
|
||||
# Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
||||
# Template code: https://github.com/beemdevelopment/Aegis/pull/406
|
||||
|
||||
# Author: Scito (https://scito.ch)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations # workaround for PYTHON <= 3.10
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
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 importlib.metadata import PackageNotFoundError, version
|
||||
from typing import (Any, Final, List, Optional, Sequence, TextIO, Tuple,
|
||||
TypedDict, Union)
|
||||
|
||||
import colorama
|
||||
from qrcode import QRCode # type: ignore
|
||||
|
||||
import protobuf_generated_python.google_auth_pb2 as pb
|
||||
|
||||
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
|
||||
import numpy as np
|
||||
|
||||
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
|
||||
zbar_available = True
|
||||
except Exception as e:
|
||||
if not quiet:
|
||||
print(f"""
|
||||
WARN: Cannot import pyzbar module. This problem is probably due to the missing zbar shared library. (The zbar library is optional.)
|
||||
See in README.md for the installation of the zbar shared library.
|
||||
Exception: {e}\n""", file=sys.stderr)
|
||||
zbar_available = False
|
||||
if debug_mode:
|
||||
raise e
|
||||
|
||||
# Types
|
||||
# workaround for PYTHON <= 3.9: Final[tuple[int]]
|
||||
ColorBGR = Tuple[int, int, int] # RGB Color specified as Blue, Green, Red
|
||||
Point = Tuple[int, int]
|
||||
|
||||
# CV2 camera capture constants
|
||||
FONT: Final[int] = cv2.FONT_HERSHEY_PLAIN
|
||||
FONT_SCALE: Final[float] = 1.3
|
||||
FONT_THICKNESS: Final[int] = 1
|
||||
FONT_LINE_STYLE: Final[int] = cv2.LINE_AA
|
||||
FONT_COLOR: Final[ColorBGR] = 255, 0, 0
|
||||
BOX_THICKNESS: Final[int] = 5
|
||||
WINDOW_X: Final[int] = 0
|
||||
WINDOW_Y: Final[int] = 1
|
||||
WINDOW_WIDTH: Final[int] = 2
|
||||
WINDOW_HEIGHT: Final[int] = 3
|
||||
TEXT_WIDTH: Final[int] = 0
|
||||
TEXT_HEIGHT: Final[int] = 1
|
||||
BORDER: Final[int] = 5
|
||||
START_Y: Final[int] = 20
|
||||
START_POS_TEXT: Final[Point] = BORDER, START_Y
|
||||
NORMAL_COLOR: Final[ColorBGR] = 255, 0, 255
|
||||
SUCCESS_COLOR: Final[ColorBGR] = 0, 255, 0
|
||||
FAILURE_COLOR: Final[ColorBGR] = 0, 0, 255
|
||||
CHAR_DX: Final[int] = (lambda text: cv2.getTextSize(text, FONT, FONT_SCALE, FONT_THICKNESS)[0][TEXT_WIDTH] // len(text))("28 QR codes capturedMMM")
|
||||
FONT_DY: Final[int] = cv2.getTextSize("M", FONT, FONT_SCALE, FONT_THICKNESS)[0][TEXT_HEIGHT] + 5
|
||||
WINDOW_NAME: Final[str] = "Extract OTP Secrets: Capture QR Codes from Camera"
|
||||
|
||||
TextPosition = Enum('TextPosition', ['LEFT', 'RIGHT'])
|
||||
|
||||
cv2_available = True
|
||||
except ImportError as e:
|
||||
cv2_available = False
|
||||
if debug_mode:
|
||||
raise e
|
||||
|
||||
# Workaround for PYTHON <= 3.9: Generally Union[int, None] used instead of int | None
|
||||
|
||||
# Types
|
||||
Args = argparse.Namespace
|
||||
OtpUrl = str
|
||||
# Workaround for PYTHON <= 3.9: Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': int | None, 'url': OtpUrl})
|
||||
Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': Union[int, None], 'url': OtpUrl})
|
||||
# workaround for PYTHON <= 3.9: Otps = list[Otp]
|
||||
Otps = List[Otp]
|
||||
# workaround for PYTHON <= 3.9: OtpUrls = list[OtpUrl]
|
||||
OtpUrls = List[OtpUrl]
|
||||
|
||||
QRMode = Enum('QRMode', ['ZBAR', 'QREADER', 'QREADER_DEEP', 'CV2', 'CV2_WECHAT'], start=0)
|
||||
LogLevel = IntEnum('LogLevel', ['QUIET', 'NORMAL', 'VERBOSE', 'MORE_VERBOSE', 'DEBUG'], start=-1)
|
||||
|
||||
|
||||
# Constants
|
||||
CAMERA: Final[str] = 'camera'
|
||||
CV2_QRMODES: List[str] = [QRMode.CV2.name, QRMode.CV2_WECHAT.name]
|
||||
|
||||
# Global variable declaration
|
||||
verbose: IntEnum = LogLevel.NORMAL
|
||||
colored: bool = True
|
||||
executable: bool = False
|
||||
__version__: str
|
||||
tk_root: tkinter.Tk
|
||||
|
||||
|
||||
def sys_main() -> None:
|
||||
main(sys.argv[1:])
|
||||
|
||||
|
||||
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
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8') # type: ignore
|
||||
sys.stderr.reconfigure(encoding='utf-8') # type: ignore
|
||||
except AttributeError: # '_io.StringIO' object has no attribute 'reconfigure'
|
||||
# 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.csv, otps)
|
||||
write_keepass_csv(args.keepass, otps)
|
||||
write_json(args.json, otps)
|
||||
write_txt(args.txt, otps, True)
|
||||
|
||||
|
||||
# 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(otp_url)
|
||||
if args.saveqr:
|
||||
save_qr_image(otp, args.saveqr, 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
|
||||
|
||||
cmd = f"python {name}" if (name := os.path.basename(sys.argv[0])).endswith('.py') else f"{name}"
|
||||
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
|
||||
if cv2_available:
|
||||
description_text += "\nIf no infiles are provided, a GUI window starts and QR codes are captured from the camera."
|
||||
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 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('--txt', '-t', help='export txt file or - for stdout', metavar=('FILE'))
|
||||
arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal', action='store_true')
|
||||
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to directory', metavar=('DIR'))
|
||||
if cv2_available:
|
||||
arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, metavar=('NUMBER'))
|
||||
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')
|
||||
output_group.add_argument('-q', '--quiet', help='no stdout output, except output set by -', action='store_true')
|
||||
args = arg_parser.parse_args(sys_args)
|
||||
colored = not args.no_color
|
||||
if args.csv == '-' or args.json == '-' or args.keepass == '-' or args.txt == '-':
|
||||
args.quiet = args.q = True
|
||||
|
||||
verbose = args.verbose if args.verbose else LogLevel.NORMAL
|
||||
if args.debug:
|
||||
verbose = LogLevel.DEBUG
|
||||
log_debug('Debug mode start')
|
||||
quiet = True if args.quiet else False
|
||||
if verbose: print(f"QReader installed: {cv2_available}")
|
||||
if cv2_available:
|
||||
if verbose >= LogLevel.VERBOSE: print(f"CV2 version: {cv2.__version__}") # type: ignore # cv2.__version__ is not available
|
||||
if verbose: print(f"QR reading mode: {args.qr}\n")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def extract_otps(args: Args) -> Otps:
|
||||
if not args.infile:
|
||||
return extract_otps_from_camera(args)
|
||||
else:
|
||||
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/T to save as csv/json/keepass/txt 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
|
||||
else:
|
||||
if otp_url:
|
||||
return FAILURE_COLOR
|
||||
else:
|
||||
return NORMAL_COLOR
|
||||
|
||||
|
||||
# TODO use cv2 types if available
|
||||
def cv2_draw_box(img: list[tuple[Any, Any]], raw_pts: list[tuple[Any, Any]], color: ColorBGR) -> np.ndarray[Any, np.dtype[np.int32]]:
|
||||
pts = np.array([raw_pts], np.int32)
|
||||
pts = pts.reshape((-1, 1, 2))
|
||||
cv2.polylines(img, [pts], True, color, BOX_THICKNESS)
|
||||
return pts
|
||||
|
||||
|
||||
# TODO use cv2 types if available
|
||||
def cv2_print_text(img: cv2.UMat, text: str, line_number: int, position: TextPosition, color: ColorBGR, opposite_len: Optional[int] = None) -> None:
|
||||
window_dim = cv2.getWindowImageRect(WINDOW_NAME)
|
||||
out_text = text
|
||||
if opposite_len:
|
||||
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
|
||||
actual_width = text_dim[TEXT_WIDTH] + opposite_len * CHAR_DX + 4 * BORDER
|
||||
if actual_width >= window_dim[WINDOW_WIDTH]:
|
||||
out_text = out_text[:(window_dim[WINDOW_WIDTH] - actual_width) // CHAR_DX] + '.'
|
||||
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
|
||||
if position == TextPosition.LEFT:
|
||||
pos = BORDER, START_Y + line_number * FONT_DY
|
||||
else:
|
||||
pos = window_dim[WINDOW_WIDTH] - text_dim[TEXT_WIDTH] - BORDER, START_Y + line_number * FONT_DY
|
||||
|
||||
cv2.putText(img, out_text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
|
||||
|
||||
def cv2_handle_pressed_keys(qr_mode: QRMode, otps: Otps) -> Tuple[bool, QRMode]:
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
quit = False
|
||||
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 == ord('t') or key == ord('T')) and is_not_headless():
|
||||
if has_no_otps_show_warning(otps):
|
||||
pass
|
||||
else:
|
||||
file_name = tkinter.filedialog.asksaveasfilename(
|
||||
title="Save extracted otp secrets as text",
|
||||
defaultextension='.txt',
|
||||
filetypes=[('Text', '*.txt'), ('All', '*.*')]
|
||||
)
|
||||
tk_root.update()
|
||||
if len(file_name) > 0:
|
||||
write_txt(file_name, otps, True)
|
||||
elif key == 32:
|
||||
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
|
||||
quit = True
|
||||
return quit, qr_mode
|
||||
|
||||
|
||||
def extract_otps_from_otp_url(otp_url: str, otp_urls: OtpUrls, otps: Otps, args: Args) -> int:
|
||||
'''Returns -1 if opt_url was already added.'''
|
||||
if otp_url and verbose >= LogLevel.VERBOSE: print(otp_url)
|
||||
if not otp_url:
|
||||
return 0
|
||||
if otp_url not in otp_urls:
|
||||
new_otps_count = extract_otp_from_otp_url(otp_url, otps, len(otp_urls), CAMERA, args)
|
||||
if new_otps_count:
|
||||
otp_urls.append(otp_url)
|
||||
if verbose: print(f"Extracted {new_otps_count} otp{'s'[:len(otps) != 1]}. {len(otps)} otp{'s'[:len(otps) != 1]} from {len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} extracted")
|
||||
return new_otps_count
|
||||
return -1
|
||||
|
||||
|
||||
def extract_otps_from_files(args: Args) -> Otps:
|
||||
otps: Otps = []
|
||||
|
||||
files_count = urls_count = otps_count = 0
|
||||
if verbose: print(f"Input files: {args.infile}")
|
||||
for infile in args.infile:
|
||||
if verbose >= LogLevel.MORE_VERBOSE: log_verbose(f"Processing infile {infile}")
|
||||
files_count += 1
|
||||
for line in get_otp_urls_from_file(infile, args):
|
||||
if verbose >= LogLevel.MORE_VERBOSE: log_verbose(line)
|
||||
if line.startswith('#') or line == '': continue
|
||||
urls_count += 1
|
||||
otps_count += extract_otp_from_otp_url(line, otps, urls_count, infile, args)
|
||||
if verbose: print(f"Extracted {otps_count} otp{'s'[:otps_count != 1]} from {urls_count} otp url{'s'[:urls_count != 1]} by reading {files_count} infile{'s'[:files_count != 1]}")
|
||||
return otps
|
||||
|
||||
|
||||
def get_otp_urls_from_file(filename: str, args: Args) -> OtpUrls:
|
||||
# stdin stream cannot be rewinded, thus distinguish, use - for utf-8 stdin and = for binary image stdin
|
||||
if filename != '=':
|
||||
check_file_exists(filename)
|
||||
lines = read_lines_from_text_file(filename)
|
||||
if lines or filename == '-':
|
||||
return lines
|
||||
|
||||
# could not process text file, try reading as image
|
||||
if filename != '-' and cv2_available:
|
||||
return convert_img_to_otp_urls(filename, args)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def read_lines_from_text_file(filename: str) -> list[str]:
|
||||
if verbose >= LogLevel.DEBUG: print(f"Reading lines of {filename}")
|
||||
# workaround for PYTHON <= 3.9 support encoding
|
||||
if sys.version_info >= (3, 10):
|
||||
finput = fileinput.input(filename, encoding='utf-8')
|
||||
else:
|
||||
finput = fileinput.input(filename)
|
||||
try:
|
||||
lines = []
|
||||
for line in (line.strip() for line in finput):
|
||||
if verbose >= LogLevel.DEBUG: log_verbose(line)
|
||||
if is_binary(line):
|
||||
abort("Binary input was given in stdin, please use = instead of - as infile argument for images.")
|
||||
# unfortunately yield line leads to random test fails
|
||||
lines.append(line)
|
||||
if not lines:
|
||||
log_warn(f"{filename.replace('-', 'stdin')} is empty")
|
||||
except UnicodeDecodeError as e:
|
||||
if filename == '-':
|
||||
abort("Unable to open text file form stdin. "
|
||||
"In case you want read an image file from stdin, you must use '=' instead of '-'.", e)
|
||||
else: # The file is probably an image, process below
|
||||
return []
|
||||
finally:
|
||||
finput.close()
|
||||
return lines
|
||||
|
||||
|
||||
def convert_img_to_otp_urls(filename: str, args: Args) -> OtpUrls:
|
||||
if verbose: print(f"Reading image {filename}")
|
||||
try:
|
||||
if filename != '=':
|
||||
img = cv2.imread(filename)
|
||||
else:
|
||||
try:
|
||||
stdin = sys.stdin.buffer.read()
|
||||
except AttributeError:
|
||||
# Workaround for pytest, since pytest cannot monkeypatch sys.stdin.buffer
|
||||
stdin = sys.stdin.read() # type: ignore # Workaround for pytest fixtures
|
||||
if not stdin:
|
||||
log_warn("stdin is empty")
|
||||
try:
|
||||
img_array = np.frombuffer(stdin, dtype='uint8')
|
||||
except TypeError as e:
|
||||
abort("Cannot read binary stdin buffer.", e)
|
||||
if not img_array.size:
|
||||
return []
|
||||
img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
|
||||
|
||||
if img is None:
|
||||
abort(f"Unable to open file for reading.\ninput file: {filename}")
|
||||
|
||||
qr_mode = QRMode[args.qr]
|
||||
otp_urls = decode_qr_img_otp_urls(img, qr_mode)
|
||||
if len(otp_urls) == 0:
|
||||
abort(f"Unable to read QR Code from file.\ninput file: {filename}")
|
||||
except Exception as e:
|
||||
abort(f"Encountered exception\ninput file: {filename}", e)
|
||||
return otp_urls
|
||||
|
||||
|
||||
def decode_qr_img_otp_urls(img: Any, qr_mode: QRMode) -> OtpUrls:
|
||||
otp_urls: OtpUrls = []
|
||||
if qr_mode in [QRMode.QREADER, QRMode.QREADER_DEEP]:
|
||||
otp_url = QReader().detect_and_decode(img, qr_mode == QRMode.QREADER_DEEP)
|
||||
otp_urls.append(otp_url)
|
||||
elif qr_mode == QRMode.CV2:
|
||||
otp_url, _, _ = cv2.QRCodeDetector().detectAndDecode(img)
|
||||
otp_urls.append(otp_url)
|
||||
elif qr_mode == QRMode.CV2_WECHAT:
|
||||
otp_url, _ = cv2.wechat_qrcode.WeChatQRCode().detectAndDecode(img)
|
||||
otp_urls += list(otp_url)
|
||||
elif qr_mode == QRMode.ZBAR:
|
||||
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}"
|
||||
|
||||
return otp_urls
|
||||
|
||||
|
||||
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}"
|
||||
if source == CAMERA:
|
||||
log_warn(f"{msg}")
|
||||
return False
|
||||
else:
|
||||
log_warn(f"{msg}\nMaybe a wrong file was given")
|
||||
return True
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf
|
||||
def get_enum_name_by_number(parent: Any, field_name: str) -> str:
|
||||
field_value = getattr(parent, field_name)
|
||||
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name # type: ignore # generic code
|
||||
|
||||
|
||||
def get_otp_type_str_from_code(otp_type: int) -> str:
|
||||
return 'totp' if otp_type == 2 else 'hotp'
|
||||
|
||||
|
||||
def convert_secret_from_bytes_to_base32_str(bytes: bytes) -> str:
|
||||
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
|
||||
|
||||
|
||||
def build_otp_url(secret: str, raw_otp: pb.MigrationPayload.OtpParameters) -> str:
|
||||
url_params = {'secret': secret}
|
||||
if raw_otp.type == 1: url_params['counter'] = str(raw_otp.counter)
|
||||
if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
|
||||
otp_url = f"otpauth://{get_otp_type_str_from_code(raw_otp.type)}/{urlparse.quote(raw_otp.name)}?" + urlparse.urlencode(url_params)
|
||||
return otp_url
|
||||
|
||||
|
||||
def print_otp(otp: Otp, out: Optional[TextIO] = None) -> None:
|
||||
print(f"Name: {otp['name']}", file=out)
|
||||
print(f"Secret: {otp['secret']}", file=out)
|
||||
if otp['issuer']: print(f"Issuer: {otp['issuer']}", file=out)
|
||||
print(f"Type: {otp['type']}", file=out)
|
||||
if otp['type'] == 'hotp':
|
||||
print(f"Counter: {otp['counter']}", file=out)
|
||||
if verbose:
|
||||
print(otp['url'], file=out)
|
||||
|
||||
|
||||
def save_qr_image(otp: Otp, dir: str, j: int) -> str:
|
||||
if not (os.path.exists(dir)): os.makedirs(dir, exist_ok=True)
|
||||
pattern = re.compile(r'[\W_]+')
|
||||
file_otp_name = pattern.sub('', otp['name'])
|
||||
file_otp_issuer = pattern.sub('', otp['issuer'])
|
||||
save_qr_image_file(otp['url'], f"{dir}/{j}-{file_otp_name}{'-' + file_otp_issuer if file_otp_issuer else ''}.png")
|
||||
return file_otp_name
|
||||
|
||||
|
||||
def save_qr_image_file(otp_url: OtpUrl, name: str) -> None:
|
||||
qr = QRCode()
|
||||
qr.add_data(otp_url)
|
||||
img = qr.make_image(fill_color='black', back_color='white')
|
||||
if verbose: print(f"Saving to {name}")
|
||||
img.save(name)
|
||||
|
||||
|
||||
def print_qr(otp_url: str, out: Optional[TextIO] = None) -> None:
|
||||
qr = QRCode()
|
||||
qr.add_data(otp_url)
|
||||
qr.print_ascii(out)
|
||||
|
||||
|
||||
def write_txt(file: str, otps: Otps, write_qr: bool = False) -> None:
|
||||
if file and len(file) > 0 and len(otps) > 0:
|
||||
with open_file_or_stdout(file) as outfile:
|
||||
for otp in otps:
|
||||
print_otp(otp, outfile)
|
||||
if write_qr:
|
||||
print_qr(otp['url'], outfile)
|
||||
print(file=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 {file}")
|
||||
|
||||
|
||||
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 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:
|
||||
count_totp_entries = write_keepass_totp_csv(otp_filename_totp, otps)
|
||||
if has_hotp:
|
||||
count_hotp_entries = write_keepass_htop_csv(otp_filename_hotp, otps)
|
||||
if not quiet:
|
||||
if has_totp and count_totp_entries: print(f"Exported {count_totp_entries} totp entrie{'s'[:count_totp_entries != 1]} to keepass csv file {otp_filename_totp}")
|
||||
if has_hotp and 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(file: str, otps: Otps) -> int:
|
||||
count_entries = 0
|
||||
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:
|
||||
if otp['type'] == 'totp':
|
||||
writer.writerow({
|
||||
'Title': otp['issuer'],
|
||||
'User Name': otp['name'],
|
||||
'TimeOtp-Secret-Base32': otp['secret'] if otp['type'] == 'totp' else None,
|
||||
'Group': f"OTP/{otp['type'].upper()}"
|
||||
})
|
||||
count_entries += 1
|
||||
return count_entries
|
||||
|
||||
|
||||
def write_keepass_htop_csv(file: str, otps: Otps) -> int:
|
||||
count_entries = 0
|
||||
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:
|
||||
if otp['type'] == 'hotp':
|
||||
writer.writerow({
|
||||
'Title': otp['issuer'],
|
||||
'User Name': otp['name'],
|
||||
'HmacOtp-Secret-Base32': otp['secret'] if otp['type'] == 'hotp' else None,
|
||||
'HmacOtp-Counter': otp['counter'] if otp['type'] == 'hotp' else None,
|
||||
'Group': f"OTP/{otp['type'].upper()}"
|
||||
})
|
||||
count_entries += 1
|
||||
return count_entries
|
||||
|
||||
|
||||
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 {file}")
|
||||
|
||||
|
||||
def has_otp_type(otps: Otps, otp_type: str) -> bool:
|
||||
for otp in otps:
|
||||
if otp['type'] == otp_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_pre_suffix(file: str, pre_suffix: str) -> str:
|
||||
'''filename.ext, pre -> filename.pre.ext'''
|
||||
name, ext = os.path.splitext(file)
|
||||
return name + "." + pre_suffix + (ext if ext else "")
|
||||
|
||||
|
||||
def open_file_or_stdout(filename: str) -> TextIO:
|
||||
'''stdout is denoted as "-".
|
||||
Note: Set before the following line:
|
||||
sys.stdout.close = lambda: None'''
|
||||
return open(filename, "w", encoding='utf-8') if filename != '-' else sys.stdout
|
||||
|
||||
|
||||
def open_file_or_stdout_for_csv(filename: str) -> TextIO:
|
||||
'''stdout is denoted as "-".
|
||||
newline=''
|
||||
Note: Set before the following line:
|
||||
sys.stdout.close = lambda: None'''
|
||||
return open(filename, "w", encoding='utf-8', newline='') if filename != '-' else sys.stdout
|
||||
|
||||
|
||||
def check_file_exists(filename: str) -> None:
|
||||
if filename != '-' and not os.path.isfile(filename):
|
||||
abort(f"Input file provided is non-existent or not a file."
|
||||
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('#')
|
||||
return False
|
||||
except (UnicodeDecodeError, AttributeError, TypeError):
|
||||
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))
|
||||
|
||||
|
||||
def do_debug_checks() -> bool:
|
||||
log_debug('Do debug checks')
|
||||
log_debug('Try: import cv2')
|
||||
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
|
||||
return ''
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def log_debug(*values: object, sep: Optional[str] = ' ') -> None:
|
||||
if colored:
|
||||
print(f"{colorama.Fore.CYAN}\nDEBUG: {str(values[0])}", *values[1:], colorama.Fore.RESET, sep)
|
||||
else:
|
||||
print(f"\nDEBUG: {str(values[0])}", *values[1:], sep)
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def log_verbose(msg: str) -> None:
|
||||
print(color(msg, colorama.Fore.CYAN))
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def log_warn(msg: str, exception: Optional[BaseException] = None) -> None:
|
||||
exception_text = "\nException: "
|
||||
eprint(color(f"\nWARN: {msg}{(exception_text + str(exception)) if exception else ''}", colorama.Fore.RED))
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def log_error(msg: str, exception: Optional[BaseException] = None) -> None:
|
||||
exception_text = "\nException: "
|
||||
eprint(color(f"\nERROR: {msg}{(exception_text + str(exception)) if exception else ''}", colorama.Fore.RED))
|
||||
|
||||
|
||||
def color(msg: str, color: Optional[str] = None) -> str:
|
||||
return f"{color if colored and color else ''}{msg}{colorama.Fore.RESET if colored and color else ''}"
|
||||
|
||||
|
||||
def eprint(*values: object, **kwargs: Any) -> None:
|
||||
'''Print to stderr.'''
|
||||
print(*values, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def abort(msg: str, exception: Optional[BaseException] = None) -> None:
|
||||
log_error(msg, exception)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys_main()
|
||||
@@ -2,10 +2,10 @@
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: google_auth.proto
|
||||
"""Generated protocol buffer code."""
|
||||
from google.protobuf.internal import builder as _builder
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import descriptor_pool as _descriptor_pool
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf.internal import builder as _builder
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
@@ -15,17 +15,17 @@ _sym_db = _symbol_database.Default()
|
||||
|
||||
DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11google_auth.proto\"\xb7\x03\n\x10MigrationPayload\x12\x37\n\x0eotp_parameters\x18\x01 \x03(\x0b\x32\x1f.MigrationPayload.OtpParameters\x12\x0f\n\x07version\x18\x02 \x01(\x05\x12\x12\n\nbatch_size\x18\x03 \x01(\x05\x12\x13\n\x0b\x62\x61tch_index\x18\x04 \x01(\x05\x12\x10\n\x08\x62\x61tch_id\x18\x05 \x01(\x05\x1a\xb7\x01\n\rOtpParameters\x12\x0e\n\x06secret\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06issuer\x18\x03 \x01(\t\x12.\n\talgorithm\x18\x04 \x01(\x0e\x32\x1b.MigrationPayload.Algorithm\x12\x0e\n\x06\x64igits\x18\x05 \x01(\x05\x12\'\n\x04type\x18\x06 \x01(\x0e\x32\x19.MigrationPayload.OtpType\x12\x0f\n\x07\x63ounter\x18\x07 \x01(\x03\",\n\tAlgorithm\x12\x10\n\x0c\x41LGO_INVALID\x10\x00\x12\r\n\tALGO_SHA1\x10\x01\"6\n\x07OtpType\x12\x0f\n\x0bOTP_INVALID\x10\x00\x12\x0c\n\x08OTP_HOTP\x10\x01\x12\x0c\n\x08OTP_TOTP\x10\x02\x62\x06proto3')
|
||||
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals())
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google_auth_pb2', globals())
|
||||
_globals = globals()
|
||||
_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
|
||||
_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google_auth_pb2', _globals)
|
||||
if _descriptor._USE_C_DESCRIPTORS == False:
|
||||
|
||||
DESCRIPTOR._options = None
|
||||
_MIGRATIONPAYLOAD._serialized_start=22
|
||||
_MIGRATIONPAYLOAD._serialized_end=461
|
||||
_MIGRATIONPAYLOAD_OTPPARAMETERS._serialized_start=176
|
||||
_MIGRATIONPAYLOAD_OTPPARAMETERS._serialized_end=359
|
||||
_MIGRATIONPAYLOAD_ALGORITHM._serialized_start=361
|
||||
_MIGRATIONPAYLOAD_ALGORITHM._serialized_end=405
|
||||
_MIGRATIONPAYLOAD_OTPTYPE._serialized_start=407
|
||||
_MIGRATIONPAYLOAD_OTPTYPE._serialized_end=461
|
||||
_globals['_MIGRATIONPAYLOAD']._serialized_start=22
|
||||
_globals['_MIGRATIONPAYLOAD']._serialized_end=461
|
||||
_globals['_MIGRATIONPAYLOAD_OTPPARAMETERS']._serialized_start=176
|
||||
_globals['_MIGRATIONPAYLOAD_OTPPARAMETERS']._serialized_end=359
|
||||
_globals['_MIGRATIONPAYLOAD_ALGORITHM']._serialized_start=361
|
||||
_globals['_MIGRATIONPAYLOAD_ALGORITHM']._serialized_end=405
|
||||
_globals['_MIGRATIONPAYLOAD_OTPTYPE']._serialized_start=407
|
||||
_globals['_MIGRATIONPAYLOAD_OTPTYPE']._serialized_end=461
|
||||
# @@protoc_insertion_point(module_scope)
|
||||
108
src/protobuf_generated_python/google_auth_pb2.pyi
Normal file
@@ -0,0 +1,108 @@
|
||||
"""
|
||||
@generated by mypy-protobuf. Do not edit manually!
|
||||
isort:skip_file
|
||||
"""
|
||||
import builtins
|
||||
import collections.abc
|
||||
import google.protobuf.descriptor
|
||||
import google.protobuf.internal.containers
|
||||
import google.protobuf.internal.enum_type_wrapper
|
||||
import google.protobuf.message
|
||||
import sys
|
||||
import typing
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
import typing as typing_extensions
|
||||
else:
|
||||
import typing_extensions
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.FileDescriptor
|
||||
|
||||
@typing_extensions.final
|
||||
class MigrationPayload(google.protobuf.message.Message):
|
||||
"""Copied from: https://github.com/beemdevelopment/Aegis/blob/master/app/src/main/proto/google_auth.proto"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
class _Algorithm:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _AlgorithmEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[MigrationPayload._Algorithm.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
ALGO_INVALID: MigrationPayload._Algorithm.ValueType # 0
|
||||
ALGO_SHA1: MigrationPayload._Algorithm.ValueType # 1
|
||||
|
||||
class Algorithm(_Algorithm, metaclass=_AlgorithmEnumTypeWrapper): ...
|
||||
ALGO_INVALID: MigrationPayload.Algorithm.ValueType # 0
|
||||
ALGO_SHA1: MigrationPayload.Algorithm.ValueType # 1
|
||||
|
||||
class _OtpType:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _OtpTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[MigrationPayload._OtpType.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
OTP_INVALID: MigrationPayload._OtpType.ValueType # 0
|
||||
OTP_HOTP: MigrationPayload._OtpType.ValueType # 1
|
||||
OTP_TOTP: MigrationPayload._OtpType.ValueType # 2
|
||||
|
||||
class OtpType(_OtpType, metaclass=_OtpTypeEnumTypeWrapper): ...
|
||||
OTP_INVALID: MigrationPayload.OtpType.ValueType # 0
|
||||
OTP_HOTP: MigrationPayload.OtpType.ValueType # 1
|
||||
OTP_TOTP: MigrationPayload.OtpType.ValueType # 2
|
||||
|
||||
@typing_extensions.final
|
||||
class OtpParameters(google.protobuf.message.Message):
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
SECRET_FIELD_NUMBER: builtins.int
|
||||
NAME_FIELD_NUMBER: builtins.int
|
||||
ISSUER_FIELD_NUMBER: builtins.int
|
||||
ALGORITHM_FIELD_NUMBER: builtins.int
|
||||
DIGITS_FIELD_NUMBER: builtins.int
|
||||
TYPE_FIELD_NUMBER: builtins.int
|
||||
COUNTER_FIELD_NUMBER: builtins.int
|
||||
secret: builtins.bytes
|
||||
name: builtins.str
|
||||
issuer: builtins.str
|
||||
algorithm: global___MigrationPayload.Algorithm.ValueType
|
||||
digits: builtins.int
|
||||
type: global___MigrationPayload.OtpType.ValueType
|
||||
counter: builtins.int
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
secret: builtins.bytes = ...,
|
||||
name: builtins.str = ...,
|
||||
issuer: builtins.str = ...,
|
||||
algorithm: global___MigrationPayload.Algorithm.ValueType = ...,
|
||||
digits: builtins.int = ...,
|
||||
type: global___MigrationPayload.OtpType.ValueType = ...,
|
||||
counter: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["algorithm", b"algorithm", "counter", b"counter", "digits", b"digits", "issuer", b"issuer", "name", b"name", "secret", b"secret", "type", b"type"]) -> None: ...
|
||||
|
||||
OTP_PARAMETERS_FIELD_NUMBER: builtins.int
|
||||
VERSION_FIELD_NUMBER: builtins.int
|
||||
BATCH_SIZE_FIELD_NUMBER: builtins.int
|
||||
BATCH_INDEX_FIELD_NUMBER: builtins.int
|
||||
BATCH_ID_FIELD_NUMBER: builtins.int
|
||||
@property
|
||||
def otp_parameters(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MigrationPayload.OtpParameters]: ...
|
||||
version: builtins.int
|
||||
batch_size: builtins.int
|
||||
batch_index: builtins.int
|
||||
batch_id: builtins.int
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
otp_parameters: collections.abc.Iterable[global___MigrationPayload.OtpParameters] | None = ...,
|
||||
version: builtins.int = ...,
|
||||
batch_size: builtins.int = ...,
|
||||
batch_index: builtins.int = ...,
|
||||
batch_id: builtins.int = ...,
|
||||
) -> None: ...
|
||||
def ClearField(self, field_name: typing_extensions.Literal["batch_id", b"batch_id", "batch_index", b"batch_index", "batch_size", b"batch_size", "otp_parameters", b"otp_parameters", "version", b"version"]) -> None: ...
|
||||
|
||||
global___MigrationPayload = MigrationPayload
|
||||
@@ -1,484 +0,0 @@
|
||||
# pytest for extract_otp_secret_keys.py
|
||||
|
||||
# Run tests:
|
||||
# pytest
|
||||
|
||||
# Author: Scito (https://scito.ch)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from utils import read_csv, read_csv_str, read_json, read_json_str, remove_files, remove_dir_with_files, read_file_to_str, file_exits
|
||||
from os import path
|
||||
from pytest import raises, mark
|
||||
from io import StringIO
|
||||
from sys import implementation
|
||||
|
||||
import extract_otp_secret_keys
|
||||
|
||||
|
||||
def test_extract_stdout(capsys):
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['example_export.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_stdin_stdout(capsys, monkeypatch):
|
||||
# Arrange
|
||||
monkeypatch.setattr('sys.stdin', StringIO(read_file_to_str('example_export.txt')))
|
||||
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_csv(capsys):
|
||||
# Arrange
|
||||
cleanup()
|
||||
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-q', '-c', 'test_example_output.csv', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
expected_csv = read_csv('example_output.csv')
|
||||
actual_csv = read_csv('test_example_output.csv')
|
||||
|
||||
assert actual_csv == expected_csv
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
# Clean up
|
||||
cleanup()
|
||||
|
||||
|
||||
def test_extract_csv_stdout(capsys):
|
||||
# Arrange
|
||||
cleanup()
|
||||
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-c', '-', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
assert not file_exits('test_example_output.csv')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_csv = read_csv('example_output.csv')
|
||||
actual_csv = read_csv_str(captured.out)
|
||||
|
||||
assert actual_csv == expected_csv
|
||||
assert captured.err == ''
|
||||
|
||||
# Clean up
|
||||
cleanup()
|
||||
|
||||
|
||||
def test_extract_stdin_and_csv_stdout(capsys, monkeypatch):
|
||||
# Arrange
|
||||
cleanup()
|
||||
monkeypatch.setattr('sys.stdin', StringIO(read_file_to_str('example_export.txt')))
|
||||
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-c', '-', '-'])
|
||||
|
||||
# Assert
|
||||
assert not file_exits('test_example_output.csv')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_csv = read_csv('example_output.csv')
|
||||
actual_csv = read_csv_str(captured.out)
|
||||
|
||||
assert actual_csv == expected_csv
|
||||
assert captured.err == ''
|
||||
|
||||
# Clean up
|
||||
cleanup()
|
||||
|
||||
|
||||
def test_keepass_csv(capsys):
|
||||
'''Two csv files .totp and .htop are generated.'''
|
||||
# Arrange
|
||||
cleanup()
|
||||
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-q', '-k', 'test_example_keepass_output.csv', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
expected_totp_csv = read_csv('example_keepass_output.totp.csv')
|
||||
expected_hotp_csv = read_csv('example_keepass_output.hotp.csv')
|
||||
actual_totp_csv = read_csv('test_example_keepass_output.totp.csv')
|
||||
actual_hotp_csv = read_csv('test_example_keepass_output.hotp.csv')
|
||||
|
||||
assert actual_totp_csv == expected_totp_csv
|
||||
assert actual_hotp_csv == expected_hotp_csv
|
||||
assert not file_exits('test_example_keepass_output.csv')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
# Clean up
|
||||
cleanup()
|
||||
|
||||
|
||||
def test_keepass_csv_stdout(capsys):
|
||||
'''Two csv files .totp and .htop are generated.'''
|
||||
# Arrange
|
||||
cleanup()
|
||||
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-k', '-', 'test/example_export_only_totp.txt'])
|
||||
|
||||
# Assert
|
||||
expected_totp_csv = read_csv('example_keepass_output.totp.csv')
|
||||
expected_hotp_csv = read_csv('example_keepass_output.hotp.csv')
|
||||
assert not file_exits('test_example_keepass_output.totp.csv')
|
||||
assert not file_exits('test_example_keepass_output.hotp.csv')
|
||||
assert not file_exits('test_example_keepass_output.csv')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
actual_totp_csv = read_csv_str(captured.out)
|
||||
|
||||
assert actual_totp_csv == expected_totp_csv
|
||||
assert captured.err == ''
|
||||
|
||||
# Clean up
|
||||
cleanup()
|
||||
|
||||
|
||||
def test_single_keepass_csv(capsys):
|
||||
'''Does not add .totp or .hotp pre-suffix'''
|
||||
# Arrange
|
||||
cleanup()
|
||||
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-q', '-k', 'test_example_keepass_output.csv', 'test/example_export_only_totp.txt'])
|
||||
|
||||
# Assert
|
||||
expected_totp_csv = read_csv('example_keepass_output.totp.csv')
|
||||
actual_totp_csv = read_csv('test_example_keepass_output.csv')
|
||||
|
||||
assert actual_totp_csv == expected_totp_csv
|
||||
assert not file_exits('test_example_keepass_output.totp.csv')
|
||||
assert not file_exits('test_example_keepass_output.hotp.csv')
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
# Clean up
|
||||
cleanup()
|
||||
|
||||
|
||||
def test_extract_json(capsys):
|
||||
# Arrange
|
||||
cleanup()
|
||||
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-q', '-j', 'test_example_output.json', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
expected_json = read_json('example_output.json')
|
||||
actual_json = read_json('test_example_output.json')
|
||||
|
||||
assert actual_json == expected_json
|
||||
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
# Clean up
|
||||
cleanup()
|
||||
|
||||
|
||||
def test_extract_json_stdout(capsys):
|
||||
# Arrange
|
||||
cleanup()
|
||||
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-j', '-', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
expected_json = read_json('example_output.json')
|
||||
assert not file_exits('test_example_output.json')
|
||||
captured = capsys.readouterr()
|
||||
actual_json = read_json_str(captured.out)
|
||||
|
||||
assert actual_json == expected_json
|
||||
assert captured.err == ''
|
||||
|
||||
# Clean up
|
||||
cleanup()
|
||||
|
||||
|
||||
def test_extract_not_encoded_plus(capsys):
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_stdout = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
||||
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test2@serenitylabs.co.uk
|
||||
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test3@serenitylabs.co.uk
|
||||
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test4@serenitylabs.co.uk
|
||||
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
'''
|
||||
|
||||
assert captured.out == expected_stdout
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_printqr(capsys):
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-p', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_stdout = read_file_to_str('test/printqr_output.txt')
|
||||
|
||||
assert captured.out == expected_stdout
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_saveqr(capsys):
|
||||
# Arrange
|
||||
cleanup()
|
||||
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == ''
|
||||
|
||||
assert path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png')
|
||||
assert path.isfile('testout/qr/2-piraspberrypi.png')
|
||||
assert path.isfile('testout/qr/3-piraspberrypi.png')
|
||||
assert path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png')
|
||||
|
||||
# Clean up
|
||||
cleanup()
|
||||
|
||||
|
||||
@mark.skipif(implementation.name == 'pypy', reason="Encoding problems in verbose mode in pypy.")
|
||||
def test_extract_verbose(capsys):
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
|
||||
|
||||
assert captured.out == expected_stdout
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_debug(capsys):
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-vv', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
|
||||
|
||||
assert len(captured.out) > len(expected_stdout)
|
||||
assert "DEBUG: " in captured.out
|
||||
assert captured.err == ''
|
||||
|
||||
|
||||
def test_extract_help(capsys):
|
||||
with raises(SystemExit) as pytest_wrapped_e:
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-h'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert len(captured.out) > 0
|
||||
assert "-h, --help" in captured.out and "--verbose, -v" in captured.out
|
||||
assert captured.err == ''
|
||||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 0
|
||||
|
||||
|
||||
def test_extract_no_arguments(capsys):
|
||||
# Act
|
||||
with raises(SystemExit) as pytest_wrapped_e:
|
||||
extract_otp_secret_keys.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 == ''
|
||||
|
||||
|
||||
def test_verbose_and_quiet(capsys):
|
||||
with raises(SystemExit) as pytest_wrapped_e:
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['-v', '-q', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert len(captured.err) > 0
|
||||
assert 'error: argument --quiet/-q: not allowed with argument --verbose/-v' in captured.err
|
||||
assert captured.out == ''
|
||||
|
||||
|
||||
def test_wrong_data(capsys):
|
||||
with raises(SystemExit) as pytest_wrapped_e:
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['test/test_export_wrong_data.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_stderr = '''
|
||||
ERROR: Cannot decode otpauth-migration migration payload.
|
||||
data=XXXX
|
||||
'''
|
||||
|
||||
assert captured.err == expected_stderr
|
||||
assert captured.out == ''
|
||||
|
||||
|
||||
def test_wrong_content(capsys):
|
||||
with raises(SystemExit) as pytest_wrapped_e:
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['test/test_export_wrong_content.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_stderr = '''
|
||||
WARN: line is not a otpauth-migration:// URL
|
||||
input file: test/test_export_wrong_content.txt
|
||||
line "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."
|
||||
Probably a wrong file was given
|
||||
|
||||
ERROR: no data query parameter in input URL
|
||||
input file: test/test_export_wrong_content.txt
|
||||
line "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."
|
||||
Probably a wrong file was given
|
||||
'''
|
||||
|
||||
assert captured.out == ''
|
||||
assert captured.err == expected_stderr
|
||||
|
||||
|
||||
def test_wrong_prefix(capsys):
|
||||
# Act
|
||||
extract_otp_secret_keys.main(['test/test_export_wrong_prefix.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_stderr = '''
|
||||
WARN: line is not a otpauth-migration:// URL
|
||||
input file: test/test_export_wrong_prefix.txt
|
||||
line "QR-Code:otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B"
|
||||
Probably a wrong file was given
|
||||
'''
|
||||
|
||||
expected_stdout = '''Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
'''
|
||||
|
||||
assert captured.out == expected_stdout
|
||||
assert captured.err == expected_stderr
|
||||
|
||||
|
||||
def test_add_pre_suffix(capsys):
|
||||
assert extract_otp_secret_keys.add_pre_suffix("name.csv", "totp") == "name.totp.csv"
|
||||
assert extract_otp_secret_keys.add_pre_suffix("name.csv", "") == "name..csv"
|
||||
assert extract_otp_secret_keys.add_pre_suffix("name", "totp") == "name.totp"
|
||||
|
||||
|
||||
def cleanup():
|
||||
remove_files('test_example_*.csv')
|
||||
remove_files('test_example_*.json')
|
||||
remove_dir_with_files('testout/')
|
||||
|
||||
|
||||
EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT = '''Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
'''
|
||||
21
tests/conftest.py
Normal file
@@ -0,0 +1,21 @@
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def pytest_addoption(parser: pytest.Parser) -> None:
|
||||
parser.addoption("--relaxed", action='store_true', help="run tests in relaxed mode")
|
||||
parser.addoption("--fast", action="store_true", help="faster execution, do not run all combinations")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def relaxed(request: pytest.FixtureRequest) -> Any:
|
||||
return request.config.getoption("--relaxed")
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc: pytest.Metafunc) -> None:
|
||||
if "qr_mode" in metafunc.fixturenames:
|
||||
all_qr_modes = ['ZBAR', 'QREADER', 'QREADER_DEEP', 'CV2', 'CV2_WECHAT']
|
||||
number = 2 if metafunc.config.getoption("fast") else len(all_qr_modes)
|
||||
qr_modes = [mode for mode in all_qr_modes]
|
||||
metafunc.parametrize("qr_mode", qr_modes[0:number])
|
||||
0
tests/data/empty_file.txt
Normal file
BIN
tests/data/lena_std.tif
Normal file
3
tests/data/only_comments.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# comment 1
|
||||
|
||||
# comment 2
|
||||
51
tests/data/print_verbose_output-n-v.txt
Normal file
@@ -0,0 +1,51 @@
|
||||
QReader installed: True
|
||||
CV2 version: 4.7.0
|
||||
QR reading mode: ZBAR
|
||||
|
||||
Input files: ['example_export.txt']
|
||||
|
||||
1. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
2. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
3. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
5. Secret
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
|
||||
|
||||
6. Secret
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
Extracted 6 otps from 5 otp urls by reading 1 infile
|
||||
@@ -1,23 +1,16 @@
|
||||
QReader installed: True
|
||||
CV2 version: 4.7.0
|
||||
QR reading mode: ZBAR
|
||||
|
||||
Input files: ['example_export.txt']
|
||||
Processing infile example_export.txt
|
||||
# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/
|
||||
|
||||
# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
||||
|
||||
1. Payload Line
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
issuer: "raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1320898453
|
||||
|
||||
|
||||
1. Secret Key
|
||||
1. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
@@ -28,20 +21,7 @@ otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspber
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
2. Payload Line
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -2094403140
|
||||
|
||||
|
||||
2. Secret Key
|
||||
2. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
@@ -52,35 +32,14 @@ otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
|
||||
|
||||
3. Payload Line
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
issuer: "raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1822886384
|
||||
|
||||
|
||||
3. Secret Key
|
||||
3. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret Key
|
||||
4. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
@@ -91,21 +50,7 @@ otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspber
|
||||
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
4. Payload Line
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "hotp demo"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_HOTP
|
||||
counter: 4
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1558849573
|
||||
|
||||
|
||||
5. Secret Key
|
||||
5. Secret
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
@@ -117,22 +62,10 @@ otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
# Name: "encoding: ¿äÄéÉ? (demo)"
|
||||
otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D
|
||||
|
||||
5. Payload Line
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "encoding: ¿äÄéÉ? (demo)"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -171198419
|
||||
|
||||
|
||||
6. Secret Key
|
||||
6. Secret
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
Extracted 6 otps from 5 otp urls by reading 1 infile
|
||||
225
tests/data/print_verbose_output-n-vvv.txt
Normal file
@@ -0,0 +1,225 @@
|
||||
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
|
||||
# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/
|
||||
|
||||
# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
|
||||
|
||||
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# Name: "encoding: ¿äÄéÉ? (demo)"
|
||||
otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D
|
||||
# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/
|
||||
|
||||
# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
||||
|
||||
DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B', fragment='')
|
||||
|
||||
DEBUG: querystring params={'data': ['CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B']}
|
||||
|
||||
DEBUG: data_base64=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B
|
||||
|
||||
DEBUG: data_base64_fixed=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B
|
||||
|
||||
DEBUG:
|
||||
1. Payload Line otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
issuer: "raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1320898453
|
||||
|
||||
|
||||
|
||||
1. Secret
|
||||
|
||||
DEBUG: OTP enum type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D', fragment='')
|
||||
|
||||
DEBUG: querystring params={'data': ['CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE=']}
|
||||
|
||||
DEBUG: data_base64=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE=
|
||||
|
||||
DEBUG: data_base64_fixed=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE=
|
||||
|
||||
DEBUG:
|
||||
2. Payload Line otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -2094403140
|
||||
|
||||
|
||||
|
||||
2. Secret
|
||||
|
||||
DEBUG: OTP enum type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
|
||||
|
||||
DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B', fragment='')
|
||||
|
||||
DEBUG: querystring params={'data': ['CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B']}
|
||||
|
||||
DEBUG: data_base64=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B
|
||||
|
||||
DEBUG: data_base64_fixed=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B
|
||||
|
||||
DEBUG:
|
||||
3. Payload Line otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
issuer: "raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1822886384
|
||||
|
||||
|
||||
|
||||
3. Secret
|
||||
|
||||
DEBUG: OTP enum type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret
|
||||
|
||||
DEBUG: OTP enum type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D', fragment='')
|
||||
|
||||
DEBUG: querystring params={'data': ['CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE=']}
|
||||
|
||||
DEBUG: data_base64=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE=
|
||||
|
||||
DEBUG: data_base64_fixed=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE=
|
||||
|
||||
DEBUG:
|
||||
4. Payload Line otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "hotp demo"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_HOTP
|
||||
counter: 4
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1558849573
|
||||
|
||||
|
||||
|
||||
5. Secret
|
||||
|
||||
DEBUG: OTP enum type: OTP_HOTP
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
|
||||
|
||||
# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
# Name: "encoding: ¿äÄéÉ? (demo)"
|
||||
otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D
|
||||
|
||||
DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D', fragment='')
|
||||
|
||||
DEBUG: querystring params={'data': ['CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ==']}
|
||||
|
||||
DEBUG: data_base64=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ==
|
||||
|
||||
DEBUG: data_base64_fixed=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ==
|
||||
|
||||
DEBUG:
|
||||
5. Payload Line otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "encoding: ¿äÄéÉ? (demo)"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -171198419
|
||||
|
||||
|
||||
|
||||
6. Secret
|
||||
|
||||
DEBUG: OTP enum type: OTP_TOTP
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
Extracted 6 otps from 5 otp urls by reading 1 infile
|
||||
27
tests/data/print_verbose_output-n.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
51
tests/data/print_verbose_output-v.txt
Normal file
@@ -0,0 +1,51 @@
|
||||
QReader installed: True
|
||||
CV2 version: 4.7.0
|
||||
QR reading mode: ZBAR
|
||||
|
||||
Input files: ['example_export.txt']
|
||||
|
||||
1. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
2. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
3. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
5. Secret
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
|
||||
|
||||
6. Secret
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
Extracted 6 otps from 5 otp urls by reading 1 infile
|
||||
71
tests/data/print_verbose_output-vv.txt
Normal file
@@ -0,0 +1,71 @@
|
||||
QReader installed: True
|
||||
CV2 version: 4.7.0
|
||||
QR reading mode: ZBAR
|
||||
|
||||
Input files: ['example_export.txt']
|
||||
[36mProcessing infile example_export.txt[39m
|
||||
[36m# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/[39m
|
||||
[36m[39m
|
||||
[36m# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi[39m
|
||||
[36motpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B[39m
|
||||
|
||||
1. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
[36m[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36motpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D[39m
|
||||
|
||||
2. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
[36m[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36motpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B[39m
|
||||
|
||||
3. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
[36m[39m
|
||||
[36m# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4[39m
|
||||
[36motpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D[39m
|
||||
|
||||
5. Secret
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
|
||||
[36m[39m
|
||||
[36m# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36m# Name: "encoding: ¿äÄéÉ? (demo)"[39m
|
||||
[36motpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D[39m
|
||||
|
||||
6. Secret
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
Extracted 6 otps from 5 otp urls by reading 1 infile
|
||||
225
tests/data/print_verbose_output-vvv.txt
Normal file
@@ -0,0 +1,225 @@
|
||||
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
|
||||
[36m# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/[39m
|
||||
[36m[39m
|
||||
[36m# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi[39m
|
||||
[36motpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B[39m
|
||||
[36m[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36motpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D[39m
|
||||
[36m[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36motpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B[39m
|
||||
[36m[39m
|
||||
[36m# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4[39m
|
||||
[36motpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D[39m
|
||||
[36m[39m
|
||||
[36m# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36m# Name: "encoding: ¿äÄéÉ? (demo)"[39m
|
||||
[36motpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D[39m
|
||||
[36m# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/[39m
|
||||
[36m[39m
|
||||
[36m# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi[39m
|
||||
[36motpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B[39m
|
||||
[36m
|
||||
DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B', fragment='') [39m
|
||||
[36m
|
||||
DEBUG: querystring params={'data': ['CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B']} [39m
|
||||
[36m
|
||||
DEBUG: data_base64=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B [39m
|
||||
[36m
|
||||
DEBUG: data_base64_fixed=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B [39m
|
||||
[36m
|
||||
DEBUG:
|
||||
1. Payload Line otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
issuer: "raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1320898453
|
||||
[39m
|
||||
|
||||
|
||||
1. Secret
|
||||
[36m
|
||||
DEBUG: OTP enum type: OTP_TOTP [39m
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
[36m[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36motpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D[39m
|
||||
[36m
|
||||
DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D', fragment='') [39m
|
||||
[36m
|
||||
DEBUG: querystring params={'data': ['CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE=']} [39m
|
||||
[36m
|
||||
DEBUG: data_base64=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE= [39m
|
||||
[36m
|
||||
DEBUG: data_base64_fixed=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE= [39m
|
||||
[36m
|
||||
DEBUG:
|
||||
2. Payload Line otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -2094403140
|
||||
[39m
|
||||
|
||||
|
||||
2. Secret
|
||||
[36m
|
||||
DEBUG: OTP enum type: OTP_TOTP [39m
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
[36m[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi[39m
|
||||
[36m# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36motpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B[39m
|
||||
[36m
|
||||
DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B', fragment='') [39m
|
||||
[36m
|
||||
DEBUG: querystring params={'data': ['CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B']} [39m
|
||||
[36m
|
||||
DEBUG: data_base64=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B [39m
|
||||
[36m
|
||||
DEBUG: data_base64_fixed=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B [39m
|
||||
[36m
|
||||
DEBUG:
|
||||
3. Payload Line otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "pi@raspberrypi"
|
||||
issuer: "raspberrypi"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1822886384
|
||||
[39m
|
||||
|
||||
|
||||
3. Secret
|
||||
[36m
|
||||
DEBUG: OTP enum type: OTP_TOTP [39m
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret
|
||||
[36m
|
||||
DEBUG: OTP enum type: OTP_TOTP [39m
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
[36m[39m
|
||||
[36m# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4[39m
|
||||
[36motpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D[39m
|
||||
[36m
|
||||
DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D', fragment='') [39m
|
||||
[36m
|
||||
DEBUG: querystring params={'data': ['CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE=']} [39m
|
||||
[36m
|
||||
DEBUG: data_base64=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE= [39m
|
||||
[36m
|
||||
DEBUG: data_base64_fixed=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE= [39m
|
||||
[36m
|
||||
DEBUG:
|
||||
4. Payload Line otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "hotp demo"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_HOTP
|
||||
counter: 4
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -1558849573
|
||||
[39m
|
||||
|
||||
|
||||
5. Secret
|
||||
[36m
|
||||
DEBUG: OTP enum type: OTP_HOTP [39m
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
|
||||
[36m[39m
|
||||
[36m# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY[39m
|
||||
[36m# Name: "encoding: ¿äÄéÉ? (demo)"[39m
|
||||
[36motpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D[39m
|
||||
[36m
|
||||
DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D', fragment='') [39m
|
||||
[36m
|
||||
DEBUG: querystring params={'data': ['CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ==']} [39m
|
||||
[36m
|
||||
DEBUG: data_base64=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ== [39m
|
||||
[36m
|
||||
DEBUG: data_base64_fixed=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ== [39m
|
||||
[36m
|
||||
DEBUG:
|
||||
5. Payload Line otp_parameters {
|
||||
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||
name: "encoding: ¿äÄéÉ? (demo)"
|
||||
algorithm: ALGO_SHA1
|
||||
digits: 1
|
||||
type: OTP_TOTP
|
||||
}
|
||||
version: 1
|
||||
batch_size: 1
|
||||
batch_id: -171198419
|
||||
[39m
|
||||
|
||||
|
||||
6. Secret
|
||||
[36m
|
||||
DEBUG: OTP enum type: OTP_TOTP [39m
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
Extracted 6 otps from 5 otp urls by reading 1 infile
|
||||
27
tests/data/print_verbose_output.txt
Normal file
@@ -0,0 +1,27 @@
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
BIN
tests/data/qr_but_without_otp.png
Normal file
|
After Width: | Height: | Size: 478 B |
BIN
tests/data/test_googleauth_export.png
Normal file
|
After Width: | Height: | Size: 653 KiB |
1
tests/data/text_masquerading_as_image.jpeg
Normal file
@@ -0,0 +1 @@
|
||||
This is just a text file masquerading as an image file.
|
||||
89
tests/extract_otp_secrets_img_unit_test.py
Normal file
@@ -0,0 +1,89 @@
|
||||
# Unit test for extract_otp_secrets.py
|
||||
|
||||
# Run tests:
|
||||
# python -m unittest
|
||||
|
||||
# Author: sssudame
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations # workaround for PYTHON <= 3.10
|
||||
import unittest
|
||||
|
||||
import extract_otp_secrets
|
||||
from utils import Capturing
|
||||
|
||||
|
||||
class TestQRImageExtract(unittest.TestCase):
|
||||
def test_img_qr_reader_happy_path(self) -> None:
|
||||
with Capturing() as actual_output:
|
||||
extract_otp_secrets.main(['tests/data/test_googleauth_export.png'])
|
||||
|
||||
expected_output =\
|
||||
['Name: Test1:test1@example1.com', 'Secret: JBSWY3DPEHPK3PXP', 'Issuer: Test1', 'Type: totp', '',
|
||||
'Name: Test2:test2@example2.com', 'Secret: JBSWY3DPEHPK3PXQ', 'Issuer: Test2', 'Type: totp', '',
|
||||
'Name: Test3:test3@example3.com', 'Secret: JBSWY3DPEHPK3PXR', 'Issuer: Test3', 'Type: totp', '']
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_img_qr_reader_no_qr_code_in_image(self) -> None:
|
||||
with Capturing() as actual_output:
|
||||
with self.assertRaises(SystemExit) as context:
|
||||
extract_otp_secrets.main(['-n', 'tests/data/lena_std.tif'])
|
||||
|
||||
expected_output = ['', 'ERROR: Unable to read QR Code from file.', 'input file: tests/data/lena_std.tif']
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
self.assertEqual(context.exception.code, 1)
|
||||
|
||||
def test_img_qr_reader_nonexistent_file(self) -> None:
|
||||
with Capturing() as actual_output:
|
||||
with self.assertRaises(SystemExit) as context:
|
||||
extract_otp_secrets.main(['-n', 'nonexistent.bmp'])
|
||||
|
||||
expected_output = ['', 'ERROR: Input file provided is non-existent or not a file.', 'input file: nonexistent.bmp']
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
self.assertEqual(context.exception.code, 1)
|
||||
|
||||
def test_img_qr_reader_non_image_file(self) -> None:
|
||||
with Capturing() as actual_output:
|
||||
extract_otp_secrets.main(['-n', 'tests/data/text_masquerading_as_image.jpeg'])
|
||||
|
||||
expected_output = [
|
||||
'',
|
||||
'WARN: input is not a otpauth-migration:// url',
|
||||
'source: tests/data/text_masquerading_as_image.jpeg',
|
||||
"input: This is just a text file masquerading as an image file.",
|
||||
'Maybe a wrong file was given',
|
||||
'',
|
||||
'ERROR: could not parse query parameter in input url',
|
||||
'source: tests/data/text_masquerading_as_image.jpeg',
|
||||
"url: This is just a text file masquerading as an image file.",
|
||||
]
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.cleanup()
|
||||
|
||||
def tearDown(self) -> None:
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
1173
tests/extract_otp_secrets_test.py
Normal file
@@ -1,4 +1,4 @@
|
||||
# Unit test for extract_otp_secret_keys.py
|
||||
# Unit test for extract_otp_secrets.py
|
||||
|
||||
# Run tests:
|
||||
# python -m unittest
|
||||
@@ -18,37 +18,44 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import unittest
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
from utils import read_csv, read_json, remove_file, remove_dir_with_files, Capturing, read_file_to_str
|
||||
from os import path
|
||||
from sys import implementation
|
||||
from __future__ import annotations # workaround for PYTHON <= 3.10
|
||||
|
||||
import extract_otp_secret_keys
|
||||
import io
|
||||
import os
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
|
||||
from utils import (Capturing, count_files_in_dir, read_csv, read_file_to_str,
|
||||
read_json, remove_dir_with_files, remove_file)
|
||||
|
||||
import extract_otp_secrets
|
||||
|
||||
# Conditional skip example
|
||||
# if sys.implementation.name == 'pypy' or sys.platform.startswith("win") or sys.version_info < (3, 10):
|
||||
# self.skipTest("Avoid encoding problems")
|
||||
|
||||
|
||||
class TestExtract(unittest.TestCase):
|
||||
|
||||
def test_extract_csv(self):
|
||||
extract_otp_secret_keys.main(['-q', '-c', 'test_example_output.csv', 'example_export.txt'])
|
||||
def test_extract_csv(self) -> None:
|
||||
extract_otp_secrets.main(['-q', '-c', 'test_example_output.csv', 'example_export.txt'])
|
||||
|
||||
expected_csv = read_csv('example_output.csv')
|
||||
actual_csv = read_csv('test_example_output.csv')
|
||||
|
||||
self.assertEqual(actual_csv, expected_csv)
|
||||
|
||||
def test_extract_json(self):
|
||||
extract_otp_secret_keys.main(['-q', '-j', 'test_example_output.json', 'example_export.txt'])
|
||||
def test_extract_json(self) -> None:
|
||||
extract_otp_secrets.main(['-q', '-j', 'test_example_output.json', 'example_export.txt'])
|
||||
|
||||
expected_json = read_json('example_output.json')
|
||||
actual_json = read_json('test_example_output.json')
|
||||
|
||||
self.assertEqual(actual_json, expected_json)
|
||||
|
||||
def test_extract_stdout_1(self):
|
||||
def test_extract_stdout_1(self) -> None:
|
||||
with Capturing() as output:
|
||||
extract_otp_secret_keys.main(['example_export.txt'])
|
||||
extract_otp_secrets.main(['example_export.txt'])
|
||||
|
||||
expected_output = [
|
||||
'Name: pi@raspberrypi',
|
||||
@@ -82,10 +89,10 @@ class TestExtract(unittest.TestCase):
|
||||
self.assertEqual(output, expected_output)
|
||||
|
||||
# Ref for capturing https://stackoverflow.com/a/40984270
|
||||
def test_extract_stdout_2(self):
|
||||
def test_extract_stdout_2(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secret_keys.main(['example_export.txt'])
|
||||
extract_otp_secrets.main(['example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = '''Name: pi@raspberrypi
|
||||
@@ -118,10 +125,10 @@ Type: totp
|
||||
'''
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_not_encoded_plus(self):
|
||||
def test_extract_not_encoded_plus(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
|
||||
extract_otp_secrets.main(['tests/data/test_plus_problem_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
||||
@@ -147,66 +154,80 @@ Type: totp
|
||||
'''
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_printqr(self):
|
||||
def test_extract_printqr(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secret_keys.main(['-p', 'example_export.txt'])
|
||||
extract_otp_secrets.main(['-p', 'example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = read_file_to_str('test/printqr_output.txt')
|
||||
expected_output = read_file_to_str('tests/data/printqr_output.txt')
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_saveqr(self):
|
||||
extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
|
||||
def test_extract_saveqr(self) -> None:
|
||||
extract_otp_secrets.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
|
||||
|
||||
self.assertTrue(path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png'))
|
||||
self.assertTrue(path.isfile('testout/qr/2-piraspberrypi.png'))
|
||||
self.assertTrue(path.isfile('testout/qr/3-piraspberrypi.png'))
|
||||
self.assertTrue(path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/2-piraspberrypi.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/3-piraspberrypi.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/5-hotpdemo.png'))
|
||||
self.assertTrue(os.path.isfile('testout/qr/6-encodingäÄéÉdemo.png'))
|
||||
self.assertEqual(count_files_in_dir('testout/qr'), 6)
|
||||
|
||||
def test_extract_verbose(self):
|
||||
if implementation.name == 'pypy': self.skipTest("Encoding problems in verbose mode in pypy.")
|
||||
def test_extract_debug(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
|
||||
extract_otp_secrets.main(['-vvv', 'example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = read_file_to_str('test/print_verbose_output.txt')
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_debug(self):
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secret_keys.main(['-vv', 'example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
|
||||
expected_stdout = read_file_to_str('tests/data/print_verbose_output.txt')
|
||||
|
||||
self.assertGreater(len(actual_output), len(expected_stdout))
|
||||
self.assertTrue("DEBUG: " in actual_output)
|
||||
|
||||
def test_extract_help(self):
|
||||
def test_extract_help_1(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
try:
|
||||
extract_otp_secret_keys.main(['-h'])
|
||||
except SystemExit:
|
||||
pass
|
||||
extract_otp_secrets.main(['-h'])
|
||||
self.fail("Must abort")
|
||||
except SystemExit as e:
|
||||
self.assertEqual(e.code, 0)
|
||||
|
||||
actual_output = out.getvalue()
|
||||
|
||||
self.assertGreater(len(actual_output), 0)
|
||||
self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output)
|
||||
self.assertTrue("-h, --help" in actual_output and "-v, --verbose" in actual_output)
|
||||
|
||||
def setUp(self):
|
||||
def test_extract_help_2(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
with self.assertRaises(SystemExit) as context:
|
||||
extract_otp_secrets.main(['-h'])
|
||||
|
||||
actual_output = out.getvalue()
|
||||
|
||||
self.assertGreater(len(actual_output), 0)
|
||||
self.assertTrue("-h, --help" in actual_output and "-v, --verbose" in actual_output)
|
||||
self.assertEqual(context.exception.code, 0)
|
||||
|
||||
def test_extract_help_3(self) -> None:
|
||||
with Capturing() as actual_output:
|
||||
with self.assertRaises(SystemExit) as context:
|
||||
extract_otp_secrets.main(['-h'])
|
||||
|
||||
self.assertGreater(len(actual_output), 0)
|
||||
self.assertTrue("-h, --help" in "\n".join(actual_output) and "-v, --verbose" in "\n".join(actual_output))
|
||||
self.assertEqual(context.exception.code, 0)
|
||||
|
||||
def setUp(self) -> None:
|
||||
self.cleanup()
|
||||
|
||||
def tearDown(self):
|
||||
def tearDown(self) -> None:
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
def cleanup(self) -> None:
|
||||
remove_file('test_example_output.csv')
|
||||
remove_file('test_example_output.json')
|
||||
remove_dir_with_files('testout/')
|
||||
140
tests/utils.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# Author: Scito (https://scito.ch)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations # workaround for PYTHON <= 3.10
|
||||
import csv
|
||||
import glob
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import pathlib
|
||||
from typing import BinaryIO, Any, Union, List
|
||||
|
||||
|
||||
# Types
|
||||
# workaround for PYTHON <= 3.9: Workaround for str | pathlib.Path
|
||||
PathLike = Union[str, pathlib.Path]
|
||||
|
||||
|
||||
# Ref. https://stackoverflow.com/a/16571630
|
||||
# workaround for PYTHON <= 3.10: class Capturing(list[Any]):
|
||||
class Capturing(List[Any]):
|
||||
'''Capture stdout and stderr
|
||||
Usage:
|
||||
with Capturing() as output:
|
||||
print("Output")
|
||||
'''
|
||||
# TODO remove type ignore if fixed, see https://github.com/python/mypy/issues/11871, https://stackoverflow.com/questions/72174409/type-hinting-the-return-value-of-a-class-method-that-returns-self
|
||||
def __enter__(self): # type: ignore
|
||||
self._stdout = sys.stdout
|
||||
sys.stdout = self._stringio_std = io.StringIO()
|
||||
self._stderr = sys.stderr
|
||||
sys.stderr = self._stringio_err = io.StringIO()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args: Any) -> None:
|
||||
self.extend(self._stringio_std.getvalue().splitlines())
|
||||
del self._stringio_std # free up some memory
|
||||
sys.stdout = self._stdout
|
||||
|
||||
self.extend(self._stringio_err.getvalue().splitlines())
|
||||
del self._stringio_err # free up some memory
|
||||
sys.stderr = self._stderr
|
||||
|
||||
|
||||
def file_exits(file: PathLike) -> bool:
|
||||
return os.path.isfile(file)
|
||||
|
||||
|
||||
def remove_file(file: PathLike) -> None:
|
||||
if file_exits(file): os.remove(file)
|
||||
|
||||
|
||||
def remove_files(glob_pattern: str) -> None:
|
||||
for f in glob.glob(glob_pattern):
|
||||
os.remove(f)
|
||||
|
||||
|
||||
def remove_dir_with_files(dir: PathLike) -> None:
|
||||
if os.path.exists(dir): shutil.rmtree(dir)
|
||||
|
||||
|
||||
def read_csv(filename: str) -> List[List[str]]:
|
||||
"""Returns a list of lines."""
|
||||
with open(filename, "r", encoding="utf-8", newline='') as infile:
|
||||
lines: List[List[str]] = []
|
||||
reader = csv.reader(infile)
|
||||
for line in reader:
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def read_csv_str(data_str: str) -> List[List[str]]:
|
||||
"""Returns a list of lines."""
|
||||
lines: List[List[str]] = []
|
||||
reader = csv.reader(data_str.splitlines())
|
||||
for line in reader:
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def read_json(filename: str) -> Any:
|
||||
"""Returns a list or a dictionary."""
|
||||
with open(filename, "r", encoding="utf-8") as infile:
|
||||
return json.load(infile)
|
||||
|
||||
|
||||
def read_json_str(data_str: str) -> Any:
|
||||
"""Returns a list or a dictionary."""
|
||||
return json.loads(data_str)
|
||||
|
||||
|
||||
def read_file_to_list(filename: str) -> List[str]:
|
||||
"""Returns a list of lines."""
|
||||
with open(filename, "r", encoding="utf-8") as infile:
|
||||
return infile.readlines()
|
||||
|
||||
|
||||
def read_file_to_str(filename: str) -> str:
|
||||
"""Returns a str."""
|
||||
return "".join(read_file_to_list(filename))
|
||||
|
||||
|
||||
def read_binary_file_as_stream(filename: str) -> BinaryIO:
|
||||
"""Returns binary file content."""
|
||||
with open(filename, "rb",) as infile:
|
||||
return io.BytesIO(infile.read())
|
||||
|
||||
|
||||
def replace_escaped_octal_utf8_bytes_with_str(str: str) -> str:
|
||||
encoded_name_strings = re.findall(r'name: .*$', str, flags=re.MULTILINE)
|
||||
for encoded_name_string in encoded_name_strings:
|
||||
escaped_bytes = re.findall(r'((?:\\[0-9]+)+)', encoded_name_string)
|
||||
for byte_sequence in escaped_bytes:
|
||||
unicode_str = b''.join([int(byte, 8).to_bytes(1, 'little') for byte in byte_sequence.split('\\') if byte]).decode('utf-8')
|
||||
print("Replace '{}' by '{}'".format(byte_sequence, unicode_str))
|
||||
str = str.replace(byte_sequence, unicode_str)
|
||||
return str
|
||||
|
||||
|
||||
def quick_and_dirty_workaround_encoding_problem(str: str) -> str:
|
||||
return re.sub(r'name: "encoding: .*$', '', str, flags=re.MULTILINE)
|
||||
|
||||
|
||||
def count_files_in_dir(path: PathLike) -> int:
|
||||
return len([name for name in os.listdir(path) if os.path.isfile(os.path.join(path, name))])
|
||||
214
upgrade_deps.sh
@@ -1,214 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Upgrades Protoc from https://github.com/protocolbuffers/protobuf/releases
|
||||
|
||||
black='\e[0;30m'
|
||||
blackBold='\e[1;30m'
|
||||
blackBackground='\e[1;40m'
|
||||
red='\e[0;31m'
|
||||
redBold='\e[1;31m'
|
||||
redBackground='\e[0;41m'
|
||||
green='\e[0;32m'
|
||||
greenBold='\e[1;32m'
|
||||
greenBackground='\e[0;42m'
|
||||
yellow='\e[0;33m'
|
||||
yellowBold='\e[1;33m'
|
||||
yellowBackground='\e[0;43m'
|
||||
blue='\e[0;34m'
|
||||
blueBold='\e[1;34m'
|
||||
blueBackground='\e[0;44m'
|
||||
magenta='\e[0;35m'
|
||||
magentaBold='\e[1;35m'
|
||||
magentaBackground='\e[0;45m'
|
||||
cyan='\e[0;36m'
|
||||
cyanBold='\e[1;36m'
|
||||
cyanBackground='\e[0;46m'
|
||||
white='\e[0;37m'
|
||||
whiteBold='\e[1;37m'
|
||||
whiteBackground='\e[0;47m'
|
||||
reset='\e[0m'
|
||||
|
||||
abort() {
|
||||
echo '
|
||||
***************
|
||||
*** ABORTED ***
|
||||
***************
|
||||
' >&2
|
||||
echo "An error occurred on line $1. Exiting..." >&2
|
||||
date -Iseconds >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trap 'abort $LINENO' ERR
|
||||
set -e -o pipefail
|
||||
|
||||
quit() {
|
||||
trap : 0
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Asks if [Yn] if script shoud continue, otherwise exit 1
|
||||
# $1: msg or nothing
|
||||
# Example call 1: askContinueYn
|
||||
# Example call 1: askContinueYn "Backup DB?"
|
||||
askContinueYn() {
|
||||
if [[ $1 ]]; then
|
||||
msg="$1 "
|
||||
else
|
||||
msg=""
|
||||
fi
|
||||
|
||||
# http://stackoverflow.com/questions/3231804/in-bash-how-to-add-are-you-sure-y-n-to-any-command-or-alias
|
||||
read -e -p "${msg}Continue? [Y/n] " response
|
||||
response=${response,,} # tolower
|
||||
if [[ $response =~ ^(yes|y|)$ ]] ; then
|
||||
# echo ""
|
||||
# OK
|
||||
:
|
||||
else
|
||||
echo "Aborted"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 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=true
|
||||
check_version=true
|
||||
|
||||
while test $# -gt 0; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
echo "Upgrade Protoc"
|
||||
echo
|
||||
echo "$0 [options]"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo "-a Automatic mode"
|
||||
echo "-C Ignore version check"
|
||||
echo "-h, --help Help"
|
||||
quit
|
||||
;;
|
||||
-a)
|
||||
interactive=false
|
||||
shift
|
||||
;;
|
||||
-C)
|
||||
check_version=false
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
BIN="$HOME/bin"
|
||||
DOWNLOADS="$HOME/downloads"
|
||||
|
||||
PIP="pip3.11"
|
||||
PIPENV="python3.11 -m pipenv"
|
||||
|
||||
# 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 [ "$OLDVERSION" != "$VERSION" ]; then
|
||||
echo "Upgrade protoc from $OLDVERSION to $VERSION"
|
||||
|
||||
NAME="protoc-$VERSION"
|
||||
ARCHIVE="$NAME.zip"
|
||||
|
||||
mkdir -p $DOWNLOADS
|
||||
# https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protoc-21.6-linux-x86_64.zip
|
||||
cmd="wget --trust-server-names https://github.com/protocolbuffers/protobuf/releases/download/v$VERSION/protoc-$VERSION-linux-x86_64.zip -O $DOWNLOADS/$ARCHIVE"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="echo -e '\nSize [Byte]'; stat --printf='%s\n' $DOWNLOADS/$ARCHIVE; echo -e '\nMD5'; md5sum $DOWNLOADS/$ARCHIVE; echo -e '\nSHA256'; sha256sum $DOWNLOADS/$ARCHIVE;"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="mkdir -p $BIN/$NAME; unzip $DOWNLOADS/$ARCHIVE -d $BIN/$NAME"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="echo $VERSION > $BIN/$NAME/.VERSION.txt; echo $VERSION > $BIN/$NAME/.VERSION_$VERSION.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="[ -d $BIN/$DEST.old ] && rm -rf $BIN/$DEST.old || echo 'No old dir to delete'"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="[ -d $BIN/$DEST ] && mv -iT $BIN/$DEST $BIN/$DEST.old || echo 'No previous dir to keep'"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="mv -iT $BIN/$NAME $BIN/$DEST"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="rm $DOWNLOADS/$ARCHIVE"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$BIN/$DEST/bin/protoc --python_out=protobuf_generated_python google_auth.proto"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
# Update README.md
|
||||
|
||||
cmd="perl -i -pe 's%proto(buf|c)([- ])(\d\.)?$OLDVERSION%proto\$1\$2\${3}$VERSION%g' README.md && perl -i -pe 's%(protobuf/releases/tag/v)$OLDVERSION%\${1}$VERSION%g' README.md"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
else
|
||||
echo -e "\nVersion has not changed. Quit"
|
||||
fi
|
||||
|
||||
|
||||
# Upgrade pip requirements
|
||||
|
||||
cmd="sudo pip install --upgrade pip"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIP --version
|
||||
|
||||
cmd="$PIP install --use-pep517 -U -r requirements.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP install -U pipenv"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIPENV --version
|
||||
|
||||
cmd="$PIPENV update && $PIPENV --rm && $PIPENV install"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIPENV run python --version
|
||||
|
||||
# Test
|
||||
|
||||
cmd="pytest"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIPENV run pytest"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
quit
|
||||
98
utils.py
@@ -1,98 +0,0 @@
|
||||
# Author: Scito (https://scito.ch)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
from io import StringIO
|
||||
import sys
|
||||
import glob
|
||||
|
||||
|
||||
# Ref. https://stackoverflow.com/a/16571630
|
||||
class Capturing(list):
|
||||
'''Capture stdout and stderr
|
||||
Usage:
|
||||
with Capturing() as output:
|
||||
print("Output")
|
||||
'''
|
||||
def __enter__(self):
|
||||
self._stdout = sys.stdout
|
||||
sys.stdout = self._stringio = StringIO()
|
||||
return self
|
||||
|
||||
def __exit__(self, *args):
|
||||
self.extend(self._stringio.getvalue().splitlines())
|
||||
del self._stringio # free up some memory
|
||||
sys.stdout = self._stdout
|
||||
|
||||
|
||||
def file_exits(file):
|
||||
return os.path.isfile(file)
|
||||
|
||||
|
||||
def remove_file(file):
|
||||
if file_exits(file): os.remove(file)
|
||||
|
||||
|
||||
def remove_files(glob_pattern):
|
||||
for f in glob.glob(glob_pattern):
|
||||
os.remove(f)
|
||||
|
||||
|
||||
def remove_dir_with_files(dir):
|
||||
if os.path.exists(dir): shutil.rmtree(dir)
|
||||
|
||||
|
||||
def read_csv(filename):
|
||||
"""Returns a list of lines."""
|
||||
with open(filename, "r", encoding="utf-8", newline='') as infile:
|
||||
lines = []
|
||||
reader = csv.reader(infile)
|
||||
for line in reader:
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def read_csv_str(str):
|
||||
"""Returns a list of lines."""
|
||||
lines = []
|
||||
reader = csv.reader(str.splitlines())
|
||||
for line in reader:
|
||||
lines.append(line)
|
||||
return lines
|
||||
|
||||
|
||||
def read_json(filename):
|
||||
"""Returns a list or a dictionary."""
|
||||
with open(filename, "r", encoding="utf-8") as infile:
|
||||
return json.load(infile)
|
||||
|
||||
|
||||
def read_json_str(str):
|
||||
"""Returns a list or a dictionary."""
|
||||
return json.loads(str)
|
||||
|
||||
|
||||
def read_file_to_list(filename):
|
||||
"""Returns a list of lines."""
|
||||
with open(filename, "r", encoding="utf-8") as infile:
|
||||
return infile.readlines()
|
||||
|
||||
|
||||
def read_file_to_str(filename):
|
||||
"""Returns a str."""
|
||||
return "".join(read_file_to_list(filename))
|
||||