Compare commits

..

39 Commits

Author SHA1 Message Date
scito
84e1922979 fix VERSION_PATCH for pre-tags, e.g. 1b1 2023-01-24 22:48:22 +01:00
scito
7f89168b92 ci_release: change order of build matrix 2023-01-24 22:15:13 +01:00
scito
b9f17c4a95 rename build docker jobs 2023-01-24 22:12:02 +01:00
scito
6a7a7233a4 build and upload executables created by PyInstaller
- create release on tag push
- build executables by PyInstaller:
    - extract_otp_secrets_linux_x86_64 (glibc 2.28)
    - extract_otp_secrets_win_x86_64.exe
    - extract_otp_secrets_macos_x86_64 (untested)
- add --version
- build linux executable in docker container
- update README
    - add TOC
    - improve badges
    - add PyInstaller section
- docker
    - build BASE_IMAGE as ARG
    - copy only required files to image
    - add .alias
- build.sh
    - fix clean
    - fix generate results
    - generate TOC
2023-01-24 21:19:38 +01:00
scito
445d77783c update Pipfile.lock 2023-01-17 21:23:46 +01:00
scito
f0134fa907 add zbar installation for archlinux [skip ci] 2023-01-17 21:04:42 +01:00
scito
e0588285c9 require qreader < 2.0.0 due to breaking changes
- add more classifiers
2023-01-15 10:44:55 +01:00
scito
ff9401687e ci: do not run ci tests on pull_request 2023-01-14 09:42:44 +01:00
scito
2478edb7a1 decode only QR in zbar (which avoids assertion pdf417)
Warning on Windows:
WARNING: .\zbar\decoder\pdf417.c:89: <unknown>: Assertion "g[0] >= 0 && g[1] >= 0 && g[2] >= 0" failed.

        dir=0 sig=1b455 k=3 g0=04a g1=ffffffff g2=b78 buf[0000]=
2023-01-13 21:28:38 +01:00
scito
5e439b9396 update Pipfile.lock packages 2023-01-11 20:48:20 +01:00
scito
5c4d3ce696 improve README 2023-01-07 09:39:15 +01:00
scito
ec09b5daad improve README; add google-authenticator-exporter link 2023-01-06 10:13:51 +01:00
scito
2ed923591e use only cv2_draw_box, move core functions to top
- improve camera test
- add more tests
- improve README
    - add "How to export otp secrets from Google Authenticator app"
    - reorder: put usage before installation
    - add "Full local build"
2023-01-04 23:12:38 +01:00
scito
36fd0c0bb6 add test keepass with no data 2023-01-03 23:51:33 +01:00
scito
b215b78dad test extract_otps_from_camera() 2023-01-03 23:51:33 +01:00
scito
851cb6532c improve build and README
- clean pip
- do not use sudo anymore
- add missing mypy-protobuf package
- sort package dependencies
- fix order of build calls
- add frame color docu to README
2023-01-03 23:51:33 +01:00
scito
2bef64e5f6 ci: no Pytest coverage comments for tags
Create commit comment
##[error]HttpError: No commit found for SHA: 19b3368
##[error]No commit found for SHA: 19b3368
2023-01-03 23:51:33 +01:00
scito
3502294172 remove dependency to module and fix build script 2023-01-03 02:00:07 +01:00
scito
2707e244be update README 2023-01-03 01:04:44 +01:00
scito
4ba0fad000 capture QR codes from camera and major refactoring
- add GUI for QR code capturing from camera (CV2 is used)
- support different QR readers: ZBAR,QREADER,QREADER_DEEP,CV2,CV2_WECHAT
- support several input files
- add option to ignore duplicate otps
- write warnings and errors to stderr
- add output coloring
- rename project from extract_otp_secret_keys to extract_otp_secrets
- improve help
- clean verbose level output
- use Python type hints and check with mypy
- use f-strings
- clean up code
- add more tests
- calculate code coverage
- use src-layout: move files and folders
- support wheel packing
- enhance README.md
- bugfixes
    * fix -k -
    * fix utf-8 encoding on windows
2023-01-03 00:28:32 +01:00
scito
9d052dc78a refactor image import and add Alpine docker image
- dynamic import of QR reader
- build docker also for arm64
2023-01-03 00:26:46 +01:00
qwertyca
915efcf192 Add the ability so provide an image file as the infile. If the file contains a QR code generated by Google Authenticator's "Transfer Accounts" function, it will be decoded directly in a single step. This is meant to help users who need to access their secrets from Google Authenticator but don't have a QR code decoder and don't want to use an online one due to security concerns. 2023-01-03 00:26:46 +01:00
scito
65c52f4d81 skip verbose tests for pypy 2022-12-19 22:37:23 +01:00
scito
8ba4439305 add utf-8 encoding tests 2022-12-19 22:37:23 +01:00
scito
10bc6959a3 add python to vscode recommended extensions 2022-12-19 17:48:17 +01:00
u231053
a51507b701 fix #30: enforce utf-8 encoding
since windows used non-utf8-encoding
2022-12-19 17:48:17 +01:00
scito
7af4017910 ci: daily tests and order python from new to old
- more recent version first
2022-12-19 00:11:30 +01:00
scito
7af631ff1e ci: setup testing for linux, macos and windows platforms
- fix ci for windows: remove bash if/fi
- exlucde failing test from windows
- enable scheduled tests
- use --use-pep517 for pip install: avoid deprecation message
- exlcude windows-latest and pypy-3.9 since there is a problem with installing (missing zlib.h)
2022-12-18 23:06:52 +01:00
scito
ca4a0bc7d2 improvements for - (stdout)
- - implies -q
- use add_mutually_exclusive_group for -v and -q
- adapt and improve tests
2022-12-18 21:34:24 +01:00
scito
1be4c7e0ef suppor writing csv and json to stdout; print errors to stderr
- add tests
2022-12-18 19:24:07 +01:00
scito
fd1841f8dd update Pipfile.lock 2022-12-18 17:42:42 +01:00
scito
81c2cb498a add stdin pytest 2022-12-18 17:41:35 +01:00
scito
21c16ed44e add pypy Python versions 3.7, 3.8, and 3.9 to GitHub CI testing 2022-12-18 13:26:17 +01:00
scito
30638041d8 upgrade to protobuf 4.21.12 2022-12-16 13:17:39 +01:00
scito
892f4f92ae fix compatibility for Python < 3.11 2022-12-16 13:10:22 +01:00
scito
bda0186d10 test wrong data and improve error handling 2022-12-16 12:43:32 +01:00
scito
96c8836a98 upgrade to protobuf 4.21.11 2022-12-10 12:25:15 +01:00
scito
c44a3f45de add pip installation; improve README
- improve README
    - add Installation section
    - add Features section
    - add Glossary
    - add related projects

- add setup.py for pip installation:
    pip install git+https://github.com/scito/extract_otp_secret_keys
2022-12-10 12:23:13 +01:00
dependabot[bot]
5783d086ad Bump pylint from 2.15.7 to 2.15.8
Bumps [pylint](https://github.com/PyCQA/pylint) from 2.15.7 to 2.15.8.
- [Release notes](https://github.com/PyCQA/pylint/releases)
- [Commits](https://github.com/PyCQA/pylint/compare/v2.15.7...v2.15.8)

---
updated-dependencies:
- dependency-name: pylint
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-05 17:36:39 +01:00
71 changed files with 5545 additions and 1211 deletions

View File

@@ -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

View File

@@ -1,14 +1,29 @@
name: tests
on: [push]
# 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:
paths-ignore:
- 'docs/**'
# pull_request:
schedule:
# Run daily on default branch
- cron: '37 3 * * *'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.x"]
python-version: ["3.x", "3.11", "3.10", "3.9", "3.8", "3.7"]
platform: [ubuntu-latest, macos-latest, windows-latest]
# exclude:
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
@@ -16,17 +31,46 @@ 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
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
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
- name: Test with pytest
if: matrix.python-version != '3.7'
- name: Type checking with mypy
run: |
pytest
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.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: |
matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
&& !contains(github.ref, 'refs/tags/')

156
.github/workflows/ci_docker.yml vendored Normal file
View File

@@ -0,0 +1,156 @@
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/**'
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 Bullseye image and push to repositories
# run only when code is compiling and tests are passing
runs-on: ubuntu-latest
# steps to perform in job
steps:
- name: Checkout code
uses: actions/checkout@v3
# avoid building if there are testing errors
- name: Run smoke test
run: |
sudo apt-get install -y libzbar0
python -m pip install --upgrade pip
pip install -U -r requirements-dev.txt
pip install -U .
pytest
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
# setup Docker build action
- name: Set up Docker Buildx
id: buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Packages
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_IO_TOKEN }}
- name: "Build image and push to Docker Hub and GitHub Container Registry"
id: docker_build_qr_reader_latest
uses: docker/build-push-action@v3
with:
platforms: linux/amd64,linux/arm64
# relative path to the place where source code with Dockerfile is located
# TODO file:, move to docker/
context: .
file: Dockerfile
# builder: ${{ steps.buildx.outputs.name }}
# Note: tags has to be all lower-case
pull: true
tags: |
scit0/extract_otp_secrets:latest
scit0/extract_otp_secrets:bullseye
ghcr.io/scito/extract_otp_secrets:latest
ghcr.io/scito/extract_otp_secrets:bullseye
# build on feature branches, push only on master branch
push: ${{ github.ref == 'refs/heads/master' }}
- name: Image digest
# TODO upload digests to assets
run: |
echo "extract_otp_secrets: ${{ steps.docker_build_qr_reader_latest.outputs.digest }}"
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
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Packages
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_IO_TOKEN }}
- name: "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: 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' }}
build-args: |
RUN_TESTS=true
- name: Image digest
# TODO upload digests to assets
run: |
echo "extract_otp_secrets:only-txt: ${{ steps.docker_build_only_txt.outputs.digest }}"

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

@@ -0,0 +1,281 @@
name: release
# https://data-dive.com/multi-os-deployment-in-cloud-using-pyinstaller-and-github-actions
# https://github.com/actions/create-release (archived)
# https://github.com/actions/upload-artifact
# https://github.com/actions/download-artifact
# https://github.com/actions/upload-release-asset (archived)
# https://github.com/docker/metadata-action
# https://github.com/marketplace/actions/generate-release-hashes
# https://github.com/oleksis/pyinstaller-manylinux
# https://github.com/pypa/manylinux
# https://github.com/batonogov/docker-pyinstaller
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
# https://docs.github.com/en/actions/using-workflows
# https://docs.github.com/en/actions/learn-github-actions/contexts
# https://docs.github.com/en/actions/learn-github-actions/expressions
# https://docs.github.com/en/rest/releases/releases
# https://peps.python.org/pep-0440/
# https://semver.org/
# Build matrix:
# - Linux x86_64 glibc 2.35: ubuntu-latest
# - Linux x86_64 glibc 2.34: extract_otp_secrets:buster
# - Windows x86_64: windows-latest
# - MacOS x86_64: macos-11
# - Linux x86_64 glibc 2.28: extract_otp_secrets:buster
# - Linux aarch64 glibc 2.28: extract_otp_secrets:buster
# - MacOS universal2: macos-11
# - Windows arm64: [buildx + https://github.com/batonogov/docker-pyinstaller]
on:
push:
tags:
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
jobs:
create-release:
name: Create Release
runs-on: ubuntu-latest
steps:
- name: Set meta data
id: meta
# Writing to env with >> $GITHUB_ENV is an alternative
run: |
echo "date=$(TZ=Europe/Bern date +'%d.%m.%Y')" >> $GITHUB_OUTPUT
echo "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 }}
- name: Create Release
id: create_release
run: |
# https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release
response=$(curl \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/scito/extract_otp_secrets/releases \
--silent \
--show-error \
-d '{"tag_name":"${{ github.ref }}","target_commitish":"master","name":"${{ steps.meta.outputs.version }} - ${{ steps.meta.outputs.date }}","body":"${{ steps.meta.outputs.tag_message }}","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
- name: Save Release URL File for publish
uses: actions/upload-artifact@v3
with:
name: release_url
path: release_url.txt
build-and-push-docker-image:
name: Build Linux release in docker container
# run only when code is compiling and tests are passing
runs-on: ubuntu-latest
needs: create-release
# 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
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Github Packages
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GHCR_IO_TOKEN }}
- name: "Build image from Buster and push to GitHub Container Registry"
id: docker_build_buster
uses: docker/build-push-action@v3
with:
platforms: linux/amd64,linux/arm64
# relative path to the place where source code with Dockerfile is located
# TODO file:, move to docker/
context: .
file: Dockerfile
# builder: ${{ steps.buildx.outputs.name }}
build-args: |
BASE_IMAGE=python:3.11-slim-buster
# Note: tags has to be all lower-case
pull: true
tags: |
ghcr.io/scito/extract_otp_secrets:buster
push: true
# # https://stackoverflow.com/a/61155718/1663871
# - name: Build docker images
# run: docker build -t local < .devcontainer/Dockerfile
# - name: Run tests
# run: docker run -it -v $PWD:/srv -w/srv local make test
- name: Image digest
# TODO upload digests to assets
run: |
echo "extract_otp_secrets: ${{ steps.docker_build_buster.outputs.digest }}"
- name: Run Pyinstaller in container
run: |
# TODO use local docker image https://stackoverflow.com/a/61155718/1663871
docker run --pull always --entrypoint /bin/bash --rm -v "$(pwd)":/files -w /files ghcr.io/scito/extract_otp_secrets:buster -c 'apt-get update && apt-get -y install binutils && pip install -U -r /files/requirements.txt && pip install pyinstaller && pyinstaller -y --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_linux_x86_64 --distpath /files/dist/ /files/src/extract_otp_secrets.py'
- name: Smoke tests
run: |
dist/extract_otp_secrets_linux_x86_64 -h
dist/extract_otp_secrets_linux_x86_64 example_export.png
dist/extract_otp_secrets_linux_x86_64 - < example_export.txt
- name: Load Release URL File from release job
uses: actions/download-artifact@v3
with:
name: release_url
- name: Display structure of files
run: ls -R
- name: Upload Release Asset
id: upload-release-asset
# TODO only for tags
shell: bash
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/extract_otp_secrets_linux_x86_64 \
$(cat release_url.txt)=extract_otp_secrets_linux_x86_64)
build:
name: Build 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
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" --onefile --version-file build\file_version_info.txt src\extract_otp_secrets.py
OUT_FILE_NAME: extract_otp_secrets.exe
ASSET_NAME: extract_otp_secrets_win_x86_64.exe
ASSET_MIME: application/vnd.microsoft.portable-executable
UPLOAD: true
- os: macos-11
TARGET: macos
# TODO add --icon
# TODO add --osx-bundle-identifier
# TODO add --codesign-identity
# TODO add --osx-entitlements-file
# TODO https://pyinstaller.org/en/stable/spec-files.html#spec-file-options-for-a-macos-bundle
# TODO --target-arch universal2
CMD_BUILD: |
pyinstaller -y --add-data $macos_python_path/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --argv-emulation src/extract_otp_secrets.py
OUT_FILE_NAME: extract_otp_secrets
ASSET_NAME: extract_otp_secrets_macos_x86_64
ASSET_MIME: application/x-newton-compatible-pkg
UPLOAD: true
- os: ubuntu-latest
TARGET: linux
CMD_BUILD: |
pyinstaller -y --add-data $pythonLocation/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile src/extract_otp_secrets.py
OUT_FILE_NAME: extract_otp_secrets
ASSET_NAME: extract_otp_secrets_linux_x86_64_ubuntu_latest
ASSET_MIME: application/x-executable
UPLOAD: false
steps:
- uses: actions/checkout@v3
- name: Set macos macos_python_path
# TODO use variable for Python version
run: echo "macos_python_path=/Library/Frameworks/Python.framework/Versions/3.11" >> $GITHUB_ENV
- name: Set up Python 3.11
uses: actions/setup-python@v4
with:
python-version: 3.11
check-latest: true
- name: Install zbar shared lib for QReader (Linux)
if: runner.os == 'Linux'
run: |
sudo apt-get install -y libzbar0
- name: Install zbar shared lib for QReader (macOS)
if: runner.os == 'macOS'
run: |
brew install zbar
- name: Install dependencies
# TODO fix --use-pep517
run: |
python -m pip install --upgrade pip
pip install -U -r requirements-dev.txt
pip install -U .
- name: Create Windows file_version_info.txt
shell: bash
run: |
mkdir -p build/
VERSION_STR=$(setuptools-git-versioning) VERSION_MAJOR=$(cut -d '.' -f 1 <<< "$(setuptools-git-versioning)") VERSION_MINOR=$(cut -d '.' -f 2 <<< "$(setuptools-git-versioning)") VERSION_PATCH=$(echo $(cut -d '.' -f 3 <<< "$(setuptools-git-versioning)") | sed -E "s/^([0-9]+).*/\1/") VERSION_BUILD=$(($(git rev-list --count $(git tag | sort -V -r | sed '1!d')..HEAD))) YEARS='2020-2023' envsubst < file_version_info_template.txt > build/file_version_info.txt
- name: Build with pyinstaller for ${{ matrix.TARGET }}
run: ${{ matrix.CMD_BUILD }}
- name: Smoke tests for generated exe (general)
run: |
dist/${{ matrix.OUT_FILE_NAME }} -h
dist/${{ matrix.OUT_FILE_NAME }} example_export.png
- name: Smoke tests for generated exe (stdin)
if: runner.os != 'Windows'
run: |
dist/${{ matrix.OUT_FILE_NAME }} - < example_export.txt
- name: Load Release URL File from release job
uses: actions/download-artifact@v3
with:
name: release_url
- name: Display structure of files
run: ls -R
- name: Upload Release Asset
id: upload-release-asset
# TODO only for tags
if: ${{ matrix.UPLOAD }}
shell: bash
run: |
response=$(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" \
--silent \
--show-error \
--data-binary @dist/${{ matrix.OUT_FILE_NAME }} \
$(cat release_url.txt)=${{ matrix.ASSET_NAME }})

15
.gitignore vendored
View File

@@ -9,8 +9,19 @@ venv/
!example_output.csv
!.github/
!.flake8
.vscode
!.vscode/settings.json
!.vscode/
!.devcontainer/
!.devcontainer/*.json
*.whl
build/
dist/
*.egg-info/
*.xml
pytest-coverage.txt
tests/reports/
dist_*/
*.spec
file_version_info_python.txt
file_version_info_explorer.txt
file_version_info.txt

7
.vscode/extensions.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"recommendations": [
"ms-python.python",
"ms-python.isort",
"tamasfe.even-better-toml",
]
}

39
.vscode/launch.json vendored Normal file
View 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"
},
]
}

View File

@@ -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
},
}

View File

@@ -1,11 +1,35 @@
FROM python:3.11-alpine
# --build-arg BASE_IMAGE=python:3.11-slim-buster
ARG BASE_IMAGE=python:3.11-slim-bullseye
FROM $BASE_IMAGE
# https://docs.docker.com/engine/reference/builder/
# 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 . .
COPY requirements*.txt src/ run_pytest.sh pytest.ini tests/ example_*.txt example_*.png example_*.csv example*.json docker/.alias ./
RUN pip install -r requirements.txt
ARG RUN_TESTS=true
RUN apt-get update && apt-get install -y \
libgl1 \
libglib2.0-0 \
libsm6 \
libzbar0 \
&& rm -rf /var/lib/apt/lists/* \
&& pip install --no-cache-dir -U -r requirements.txt \
&& if [ "$RUN_TESTS" = "true" ]; then /extract/run_pytest.sh; else echo "Not running tests..."; fi \
&& echo 'test -s /extract/.alias && . /extract/.alias || true' >> ~/.bashrc
WORKDIR /files
ENTRYPOINT [ "python", "/extract/extract_otp_secret_keys.py" ]
ENTRYPOINT ["python", "/extract/extract_otp_secrets.py"]
LABEL org.opencontainers.image.source https://github.com/scito/extract_otp_secrets
LABEL org.opencontainers.image.license GPL-3.0+
LABEL maintainer="Scito https://scito.ch, https://github.com/scito"

47
Dockerfile_only_txt Normal file
View File

@@ -0,0 +1,47 @@
ARG BASE_IMAGE=python:3.11-alpine
FROM $BASE_IMAGE
# https://docs.docker.com/engine/reference/builder/
# For debugging
# docker run --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt
# docker build . -t extract_otp_secrets_only_txt -f Dockerfile_only_txt --pull --build-arg RUN_TESTS=false
# docker run --entrypoint /bin/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 \
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"

19
Pipfile
View File

@@ -4,15 +4,28 @@ verify_ssl = true
name = "pypi"
[packages]
colorama = ">=0.4.6"
opencv-contrib-python = "*"
# for macOS: opencv-contrib-python = "<=4.7.0"
# for PYTHON <= 3.7: typing_extensions = "*"
pillow = "*"
protobuf = "*"
qrcode = "*"
pillow = "*"
qreader = "<2.0.0"
[dev-packages]
pytest = "*"
wheel = "*"
build = "*"
flake8 = "*"
gfm-toc = "*"
mypy = "*"
mypy-protobuf = "*"
pylint = "*"
pytest = "*"
pytest-cov = "*"
pytest-mock = "*"
setuptools-git-versioning = "*"
types-protobuf = "*"
wheel = "*"
[requires]
python_version = "3.11"

583
Pipfile.lock generated
View File

@@ -1,7 +1,7 @@
{
"_meta": {
"hash": {
"sha256": "38a1c4d86c2546c0ac33f77c1a20dc325b50be44b8c4b310050b63033558507d"
"sha256": "41edd4aebe075d6c39d035ec7cb10f0253a3ad21f9b4aa5b9c57deccca87874f"
},
"pipfile-spec": 6,
"requires": {
@@ -16,92 +16,191 @@
]
},
"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"
"version": "==0.4.6"
},
"numpy": {
"hashes": [
"sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9",
"sha256:0e3463e6ac25313462e04aea3fb8a0a30fb906d5d300f58b3bc2c23da6a15398",
"sha256:179a7ef0889ab769cc03573b6217f54c8bd8e16cef80aad369e1e8185f994cd7",
"sha256:2386da9a471cc00a1f47845e27d916d5ec5346ae9696e01a8a34760858fe9dd2",
"sha256:26089487086f2648944f17adaa1a97ca6aee57f513ba5f1c0b7ebdabbe2b9954",
"sha256:28bc9750ae1f75264ee0f10561709b1462d450a4808cd97c013046073ae64ab6",
"sha256:28e418681372520c992805bb723e29d69d6b7aa411065f48216d8329d02ba032",
"sha256:442feb5e5bada8408e8fcd43f3360b78683ff12a4444670a7d9e9824c1817d36",
"sha256:6ec0c021cd9fe732e5bab6401adea5a409214ca5592cd92a114f7067febcba0c",
"sha256:7094891dcf79ccc6bc2a1f30428fa5edb1e6fb955411ffff3401fb4ea93780a8",
"sha256:84e789a085aabef2f36c0515f45e459f02f570c4b4c4c108ac1179c34d475ed7",
"sha256:87a118968fba001b248aac90e502c0b13606721b1343cdaddbc6e552e8dfb56f",
"sha256:8e669fbdcdd1e945691079c2cae335f3e3a56554e06bbd45d7609a6cf568c700",
"sha256:ad2925567f43643f51255220424c23d204024ed428afc5aad0f86f3ffc080086",
"sha256:b0677a52f5d896e84414761531947c7a330d1adc07c3a4372262f25d84af7bf7",
"sha256:b07b40f5fb4fa034120a5796288f24c1fe0e0580bbfff99897ba6267af42def2",
"sha256:b09804ff570b907da323b3d762e74432fb07955701b17b08ff1b5ebaa8cfe6a9",
"sha256:b162ac10ca38850510caf8ea33f89edcb7b0bb0dfa5592d59909419986b72407",
"sha256:b31da69ed0c18be8b77bfce48d234e55d040793cebb25398e2a7d84199fbc7e2",
"sha256:caf65a396c0d1f9809596be2e444e3bd4190d86d5c1ce21f5fc4be60a3bc5b36",
"sha256:cfa1161c6ac8f92dea03d625c2d0c05e084668f4a06568b77a25a89111621566",
"sha256:dae46bed2cb79a58d6496ff6d8da1e3b95ba09afeca2e277628171ca99b99db1",
"sha256:ddc7ab52b322eb1e40521eb422c4e0a20716c271a306860979d450decbb51b8e",
"sha256:de92efa737875329b052982e37bd4371d52cabf469f83e7b8be9bb7752d67e51",
"sha256:e274f0f6c7efd0d577744f52032fdd24344f11c5ae668fe8d01aac0422611df1",
"sha256:ed5fb71d79e771ec930566fae9c02626b939e37271ec285e9efaf1b5d4370e7d",
"sha256:ef85cf1f693c88c1fd229ccd1055570cb41cdf4875873b7728b6301f12cd05bf",
"sha256:f1b739841821968798947d3afcefd386fa56da0caf97722a5de53e07c4ccedc7"
],
"markers": "python_version >= '3.10'",
"version": "==1.24.1"
},
"opencv-contrib-python": {
"hashes": [
"sha256:1a48c2f24440cfd6e49c84dbe39c39feff5efbc90be8299c76e7141973d403b6",
"sha256:2b8e3a1a7af31ebed28487d161ca4be0edd0b0e241667c6e9c842ac683313b2f",
"sha256:2f0c32b0f2f55255632a44bdcfa185f88c7fb6d2616869942aff9d5a39df4997",
"sha256:35e9a3809da10a47189c06d4d78b8e7821b9a3578dec8cbddf6ee1675bd83557",
"sha256:3a00e12546e5578f6bb7ed408c37fcfea533d74e9691cfaf40926f6b43295577",
"sha256:6d1c993811f92ddd7919314ada7b9be1f23db1c73f1384915c834dee8549c0b9",
"sha256:7a08f9d1f9dd52de63a7bb448ab7d6d4a1a85b767c2358501d968d1e4d95098d",
"sha256:7a75f1775790106e54bcfb101c0e00e1f801a57d9baebc82d0b6758fc83a4ca0",
"sha256:86f4b60b9536948f16d2170ba3a9b22d3955a957dc61a9bc56e53692c6db2c7e",
"sha256:9829e6efedde1d1b8419c5bd4d62d289ecbf44ae35b843c6da9e3cbcba1a9a8a",
"sha256:abc6adfa8694f71a4caffa922b279bd9d96954a37eee40b147f613c64310b411",
"sha256:b4033a164b2e2ea0049ba8c1194dab82dca680953ac36f33d1cc2c060906555f",
"sha256:e3967b1f3d74b8c70be724dbc07921faec87e8806cc87b2db5e7057815d6a08c",
"sha256:e770e9f653a0e5e72b973adb8213fae2df4642730ba1faf31e73a54287a4d5d4"
],
"index": "pypi",
"version": "==4.7.0.68"
},
"opencv-python": {
"hashes": [
"sha256:3a00e12546e5578f6bb7ed408c37fcfea533d74e9691cfaf40926f6b43295577",
"sha256:6d1c993811f92ddd7919314ada7b9be1f23db1c73f1384915c834dee8549c0b9",
"sha256:7a08f9d1f9dd52de63a7bb448ab7d6d4a1a85b767c2358501d968d1e4d95098d",
"sha256:86f4b60b9536948f16d2170ba3a9b22d3955a957dc61a9bc56e53692c6db2c7e",
"sha256:9829e6efedde1d1b8419c5bd4d62d289ecbf44ae35b843c6da9e3cbcba1a9a8a",
"sha256:abc6adfa8694f71a4caffa922b279bd9d96954a37eee40b147f613c64310b411",
"sha256:e770e9f653a0e5e72b973adb8213fae2df4642730ba1faf31e73a54287a4d5d4"
],
"markers": "python_version >= '3.6'",
"version": "==4.7.0.68"
},
"pillow": {
"hashes": [
"sha256:013016af6b3a12a2f40b704677f8b51f72cb007dac785a9933d5c86a72a7fe33",
"sha256:0845adc64fe9886db00f5ab68c4a8cd933ab749a87747555cec1c95acea64b0b",
"sha256:0884ba7b515163a1a05440a138adeb722b8a6ae2c2b33aea93ea3118dd3a899e",
"sha256:09b89ddc95c248ee788328528e6a2996e09eaccddeeb82a5356e92645733be35",
"sha256:0dd4c681b82214b36273c18ca7ee87065a50e013112eea7d78c7a1b89a739153",
"sha256:0e51f608da093e5d9038c592b5b575cadc12fd748af1479b5e858045fff955a9",
"sha256:0f3269304c1a7ce82f1759c12ce731ef9b6e95b6df829dccd9fe42912cc48569",
"sha256:16a8df99701f9095bea8a6c4b3197da105df6f74e6176c5b410bc2df2fd29a57",
"sha256:19005a8e58b7c1796bc0167862b1f54a64d3b44ee5d48152b06bb861458bc0f8",
"sha256:1b4b4e9dda4f4e4c4e6896f93e84a8f0bcca3b059de9ddf67dac3c334b1195e1",
"sha256:28676836c7796805914b76b1837a40f76827ee0d5398f72f7dcc634bae7c6264",
"sha256:2968c58feca624bb6c8502f9564dd187d0e1389964898f5e9e1fbc8533169157",
"sha256:3f4cc516e0b264c8d4ccd6b6cbc69a07c6d582d8337df79be1e15a5056b258c9",
"sha256:3fa1284762aacca6dc97474ee9c16f83990b8eeb6697f2ba17140d54b453e133",
"sha256:43521ce2c4b865d385e78579a082b6ad1166ebed2b1a2293c3be1d68dd7ca3b9",
"sha256:451f10ef963918e65b8869e17d67db5e2f4ab40e716ee6ce7129b0cde2876eab",
"sha256:46c259e87199041583658457372a183636ae8cd56dbf3f0755e0f376a7f9d0e6",
"sha256:46f39cab8bbf4a384ba7cb0bc8bae7b7062b6a11cfac1ca4bc144dea90d4a9f5",
"sha256:519e14e2c49fcf7616d6d2cfc5c70adae95682ae20f0395e9280db85e8d6c4df",
"sha256:53dcb50fbdc3fb2c55431a9b30caeb2f7027fcd2aeb501459464f0214200a503",
"sha256:54614444887e0d3043557d9dbc697dbb16cfb5a35d672b7a0fcc1ed0cf1c600b",
"sha256:575d8912dca808edd9acd6f7795199332696d3469665ef26163cd090fa1f8bfa",
"sha256:5dd5a9c3091a0f414a963d427f920368e2b6a4c2f7527fdd82cde8ef0bc7a327",
"sha256:5f532a2ad4d174eb73494e7397988e22bf427f91acc8e6ebf5bb10597b49c493",
"sha256:60e7da3a3ad1812c128750fc1bc14a7ceeb8d29f77e0a2356a8fb2aa8925287d",
"sha256:653d7fb2df65efefbcbf81ef5fe5e5be931f1ee4332c2893ca638c9b11a409c4",
"sha256:6663977496d616b618b6cfa43ec86e479ee62b942e1da76a2c3daa1c75933ef4",
"sha256:6abfb51a82e919e3933eb137e17c4ae9c0475a25508ea88993bb59faf82f3b35",
"sha256:6c6b1389ed66cdd174d040105123a5a1bc91d0aa7059c7261d20e583b6d8cbd2",
"sha256:6d9dfb9959a3b0039ee06c1a1a90dc23bac3b430842dcb97908ddde05870601c",
"sha256:765cb54c0b8724a7c12c55146ae4647e0274a839fb6de7bcba841e04298e1011",
"sha256:7a21222644ab69ddd9967cfe6f2bb420b460dae4289c9d40ff9a4896e7c35c9a",
"sha256:7ac7594397698f77bce84382929747130765f66406dc2cd8b4ab4da68ade4c6e",
"sha256:7cfc287da09f9d2a7ec146ee4d72d6ea1342e770d975e49a8621bf54eaa8f30f",
"sha256:83125753a60cfc8c412de5896d10a0a405e0bd88d0470ad82e0869ddf0cb3848",
"sha256:847b114580c5cc9ebaf216dd8c8dbc6b00a3b7ab0131e173d7120e6deade1f57",
"sha256:87708d78a14d56a990fbf4f9cb350b7d89ee8988705e58e39bdf4d82c149210f",
"sha256:8a2b5874d17e72dfb80d917213abd55d7e1ed2479f38f001f264f7ce7bae757c",
"sha256:8f127e7b028900421cad64f51f75c051b628db17fb00e099eb148761eed598c9",
"sha256:94cdff45173b1919350601f82d61365e792895e3c3a3443cf99819e6fbf717a5",
"sha256:99d92d148dd03fd19d16175b6d355cc1b01faf80dae93c6c3eb4163709edc0a9",
"sha256:9a3049a10261d7f2b6514d35bbb7a4dfc3ece4c4de14ef5876c4b7a23a0e566d",
"sha256:9d9a62576b68cd90f7075876f4e8444487db5eeea0e4df3ba298ee38a8d067b0",
"sha256:9e5f94742033898bfe84c93c831a6f552bb629448d4072dd312306bab3bd96f1",
"sha256:a1c2d7780448eb93fbcc3789bf3916aa5720d942e37945f4056680317f1cd23e",
"sha256:a2e0f87144fcbbe54297cae708c5e7f9da21a4646523456b00cc956bd4c65815",
"sha256:a4dfdae195335abb4e89cc9762b2edc524f3c6e80d647a9a81bf81e17e3fb6f0",
"sha256:a96e6e23f2b79433390273eaf8cc94fec9c6370842e577ab10dabdcc7ea0a66b",
"sha256:aabdab8ec1e7ca7f1434d042bf8b1e92056245fb179790dc97ed040361f16bfd",
"sha256:b222090c455d6d1a64e6b7bb5f4035c4dff479e22455c9eaa1bdd4c75b52c80c",
"sha256:b52ff4f4e002f828ea6483faf4c4e8deea8d743cf801b74910243c58acc6eda3",
"sha256:b70756ec9417c34e097f987b4d8c510975216ad26ba6e57ccb53bc758f490dab",
"sha256:b8c2f6eb0df979ee99433d8b3f6d193d9590f735cf12274c108bd954e30ca858",
"sha256:b9b752ab91e78234941e44abdecc07f1f0d8f51fb62941d32995b8161f68cfe5",
"sha256:ba6612b6548220ff5e9df85261bddc811a057b0b465a1226b39bfb8550616aee",
"sha256:bd752c5ff1b4a870b7661234694f24b1d2b9076b8bf337321a814c612665f343",
"sha256:c3c4ed2ff6760e98d262e0cc9c9a7f7b8a9f61aa4d47c58835cdaf7b0b8811bb",
"sha256:c5c1362c14aee73f50143d74389b2c158707b4abce2cb055b7ad37ce60738d47",
"sha256:cb362e3b0976dc994857391b776ddaa8c13c28a16f80ac6522c23d5257156bed",
"sha256:d197df5489004db87d90b918033edbeee0bd6df3848a204bca3ff0a903bef837",
"sha256:d3b56206244dc8711f7e8b7d6cad4663917cd5b2d950799425076681e8766286",
"sha256:d5b2f8a31bd43e0f18172d8ac82347c8f37ef3e0b414431157718aa234991b28",
"sha256:d7081c084ceb58278dd3cf81f836bc818978c0ccc770cbbb202125ddabec6628",
"sha256:db74f5562c09953b2c5f8ec4b7dfd3f5421f31811e97d1dbc0a7c93d6e3a24df",
"sha256:df41112ccce5d47770a0c13651479fbcd8793f34232a2dd9faeccb75eb5d0d0d",
"sha256:e1339790c083c5a4de48f688b4841f18df839eb3c9584a770cbd818b33e26d5d",
"sha256:e621b0246192d3b9cb1dc62c78cfa4c6f6d2ddc0ec207d43c0dedecb914f152a",
"sha256:e8c5cf126889a4de385c02a2c3d3aba4b00f70234bfddae82a5eaa3ee6d5e3e6",
"sha256:e9d7747847c53a16a729b6ee5e737cf170f7a16611c143d95aa60a109a59c336",
"sha256:eaef5d2de3c7e9b21f1e762f289d17b726c2239a42b11e25446abf82b26ac132",
"sha256:ed3e4b4e1e6de75fdc16d3259098de7c6571b1a6cc863b1a49e7d3d53e036070",
"sha256:ef21af928e807f10bf4141cad4746eee692a0dd3ff56cfb25fce076ec3cc8abe",
"sha256:f09598b416ba39a8f489c124447b007fe865f786a89dbfa48bb5cf395693132a",
"sha256:f0caf4a5dcf610d96c3bd32932bfac8aee61c96e60481c2a0ea58da435e25acd",
"sha256:f6e78171be3fb7941f9910ea15b4b14ec27725865a73c15277bc39f5ca4f8391",
"sha256:f715c32e774a60a337b2bb8ad9839b4abf75b267a0f18806f6f4f5f1688c4b5a",
"sha256:fb5c1ad6bad98c57482236a21bf985ab0ef42bd51f7ad4e4538e89a997624e12"
],
"index": "pypi",
"version": "==9.4.0"
},
"protobuf": {
"hashes": [
"sha256:0413addc126c40a5440ee59be098de1007183d68e9f5f20ed5fbc44848f417ca",
"sha256:05cbcb9a25cd781fd949f93f6f98a911883868c0360c6d2264fc99a903c8f0d7",
"sha256:0c968753028cb14b1d24cc839723f7e9505b305fc588a37a9e0f7d270cb59d89",
"sha256:2a172741b5b041a896b621cef4277077afd571e0d3a6e524e7171f1c70e33200",
"sha256:3f08f04b4f101dd469efbcc1731fbf48068eccd8a42f4e2ea530aa012a5f56f8",
"sha256:4d97c16c0d11155b3714a29245461f0eb60cace294455077f3a3b8a629afa383",
"sha256:5096b3922b45e4b7a04d3d3cb855d13bb5ccd4d5e44b129e706232ebf0ffb870",
"sha256:5efa8a8162ada7e10847140308fbf84fdc5b89dc21655d12ec04aed87284fe07",
"sha256:6b809f20923b6ef49dc1755cb50bdb21be179b4a3c7ffcab1fe5d3f139b58a51",
"sha256:81b233a06c62387ea5c9be2cd9aedd2ba09940e91da53b920e9ff5bd98e48e7f",
"sha256:a5e89eabaa0ca72ce1b7c8104a740d44cdb67942cbbed00c69a4c0541de17107",
"sha256:b78d7c2c36b51c0041b9bf000be4adb09f4112bfc40bc7a9d48ac0b0dfad139e",
"sha256:e53165dd14d19abc7f50733f365de431e51d1d262db40c0ee22e271a074fac59",
"sha256:e92768d17473657c87e98b79a4c7724b0ddfa23211b05ce137bfdc55e734e36f"
"sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30",
"sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b",
"sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc",
"sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791",
"sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717",
"sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec",
"sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7",
"sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab",
"sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2",
"sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5",
"sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1",
"sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462",
"sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97",
"sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"
],
"index": "pypi",
"version": "==4.21.10"
"version": "==4.21.12"
},
"pyzbar": {
"hashes": [
"sha256:13e3ee5a2f3a545204a285f41814d5c0db571967e8d4af8699a03afc55182a9c",
"sha256:4559628b8192feb25766d954b36a3753baaf5c97c03135aec7e4a026036b475d",
"sha256:8f4c5264c9c7c6b9f20d01efc52a4eba1ded47d9ba857a94130afe33703eb518"
],
"version": "==0.1.9"
},
"qrcode": {
"hashes": [
@@ -109,31 +208,106 @@
],
"index": "pypi",
"version": "==7.3.1"
},
"qreader": {
"hashes": [
"sha256:f96d2879c9f47f9641da55dc3e1df765cd4d6f1cfbc8b7fa811b70c1e27d4fba"
],
"index": "pypi",
"version": "==1.3.2"
}
},
"develop": {
"astroid": {
"hashes": [
"sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907",
"sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"
"sha256:14c1603c41cc61aae731cad1884a073c4645e26f126d13ac8346113c95577f3b",
"sha256:6afc22718a48a689ca24a97981ad377ba7fb78c133f40335dfd16772f29bcfb1"
],
"markers": "python_full_version >= '3.7.2'",
"version": "==2.12.13"
"version": "==2.13.3"
},
"attrs": {
"hashes": [
"sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6",
"sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"
"sha256:29e95c7f6778868dbd49170f98f8818f78f3dc5e0e37c0b1f474e3561b240836",
"sha256:c9227bfc2f01993c03f68db37d1d15c9690188323c067c641f1a35ca58185f99"
],
"markers": "python_version >= '3.5'",
"version": "==22.1.0"
"markers": "python_version >= '3.6'",
"version": "==22.2.0"
},
"build": {
"hashes": [
"sha256:af266720050a66c893a6096a2f410989eeac74ff9a68ba194b3f6473e8e26171",
"sha256:d5b71264afdb5951d6704482aac78de887c80691c52b88a9ad195983ca2c9269"
],
"index": "pypi",
"version": "==0.10.0"
},
"coverage": {
"extras": [
"toml"
],
"hashes": [
"sha256:051afcbd6d2ac39298d62d340f94dbb6a1f31de06dfaf6fcef7b759dd3860c45",
"sha256:0a1890fca2962c4f1ad16551d660b46ea77291fba2cc21c024cd527b9d9c8809",
"sha256:0ee30375b409d9a7ea0f30c50645d436b6f5dfee254edffd27e45a980ad2c7f4",
"sha256:13250b1f0bd023e0c9f11838bdeb60214dd5b6aaf8e8d2f110c7e232a1bff83b",
"sha256:17e01dd8666c445025c29684d4aabf5a90dc6ef1ab25328aa52bedaa95b65ad7",
"sha256:19245c249aa711d954623d94f23cc94c0fd65865661f20b7781210cb97c471c0",
"sha256:1caed2367b32cc80a2b7f58a9f46658218a19c6cfe5bc234021966dc3daa01f0",
"sha256:1f66862d3a41674ebd8d1a7b6f5387fe5ce353f8719040a986551a545d7d83ea",
"sha256:220e3fa77d14c8a507b2d951e463b57a1f7810a6443a26f9b7591ef39047b1b2",
"sha256:276f4cd0001cd83b00817c8db76730938b1ee40f4993b6a905f40a7278103b3a",
"sha256:29de916ba1099ba2aab76aca101580006adfac5646de9b7c010a0f13867cba45",
"sha256:2a7f23bbaeb2a87f90f607730b45564076d870f1fb07b9318d0c21f36871932b",
"sha256:2c407b1950b2d2ffa091f4e225ca19a66a9bd81222f27c56bd12658fc5ca1209",
"sha256:30b5fec1d34cc932c1bc04017b538ce16bf84e239378b8f75220478645d11fca",
"sha256:3c2155943896ac78b9b0fd910fb381186d0c345911f5333ee46ac44c8f0e43ab",
"sha256:411d4ff9d041be08fdfc02adf62e89c735b9468f6d8f6427f8a14b6bb0a85095",
"sha256:436e103950d05b7d7f55e39beeb4d5be298ca3e119e0589c0227e6d0b01ee8c7",
"sha256:49640bda9bda35b057b0e65b7c43ba706fa2335c9a9896652aebe0fa399e80e6",
"sha256:4a950f83fd3f9bca23b77442f3a2b2ea4ac900944d8af9993743774c4fdc57af",
"sha256:50a6adc2be8edd7ee67d1abc3cd20678987c7b9d79cd265de55941e3d0d56499",
"sha256:52ab14b9e09ce052237dfe12d6892dd39b0401690856bcfe75d5baba4bfe2831",
"sha256:54f7e9705e14b2c9f6abdeb127c390f679f6dbe64ba732788d3015f7f76ef637",
"sha256:66e50680e888840c0995f2ad766e726ce71ca682e3c5f4eee82272c7671d38a2",
"sha256:790e4433962c9f454e213b21b0fd4b42310ade9c077e8edcb5113db0818450cb",
"sha256:7a38362528a9115a4e276e65eeabf67dcfaf57698e17ae388599568a78dcb029",
"sha256:7b05ed4b35bf6ee790832f68932baf1f00caa32283d66cc4d455c9e9d115aafc",
"sha256:7e109f1c9a3ece676597831874126555997c48f62bddbcace6ed17be3e372de8",
"sha256:949844af60ee96a376aac1ded2a27e134b8c8d35cc006a52903fc06c24a3296f",
"sha256:95304068686545aa368b35dfda1cdfbbdbe2f6fe43de4a2e9baa8ebd71be46e2",
"sha256:9e662e6fc4f513b79da5d10a23edd2b87685815b337b1a30cd11307a6679148d",
"sha256:a9fed35ca8c6e946e877893bbac022e8563b94404a605af1d1e6accc7eb73289",
"sha256:b69522b168a6b64edf0c33ba53eac491c0a8f5cc94fa4337f9c6f4c8f2f5296c",
"sha256:b78729038abea6a5df0d2708dce21e82073463b2d79d10884d7d591e0f385ded",
"sha256:b8c56bec53d6e3154eaff6ea941226e7bd7cc0d99f9b3756c2520fc7a94e6d96",
"sha256:b9727ac4f5cf2cbf87880a63870b5b9730a8ae3a4a360241a0fdaa2f71240ff0",
"sha256:ba3027deb7abf02859aca49c865ece538aee56dcb4871b4cced23ba4d5088904",
"sha256:be9fcf32c010da0ba40bf4ee01889d6c737658f4ddff160bd7eb9cac8f094b21",
"sha256:c18d47f314b950dbf24a41787ced1474e01ca816011925976d90a88b27c22b89",
"sha256:c76a3075e96b9c9ff00df8b5f7f560f5634dffd1658bafb79eb2682867e94f78",
"sha256:cbfcba14a3225b055a28b3199c3d81cd0ab37d2353ffd7f6fd64844cebab31ad",
"sha256:d254666d29540a72d17cc0175746cfb03d5123db33e67d1020e42dae611dc196",
"sha256:d66187792bfe56f8c18ba986a0e4ae44856b1c645336bd2c776e3386da91e1dd",
"sha256:d8d04e755934195bdc1db45ba9e040b8d20d046d04d6d77e71b3b34a8cc002d0",
"sha256:d8f3e2e0a1d6777e58e834fd5a04657f66affa615dae61dd67c35d1568c38882",
"sha256:e057e74e53db78122a3979f908973e171909a58ac20df05c33998d52e6d35757",
"sha256:e4ce984133b888cc3a46867c8b4372c7dee9cee300335e2925e197bcd45b9e16",
"sha256:ea76dbcad0b7b0deb265d8c36e0801abcddf6cc1395940a24e3595288b405ca0",
"sha256:ecb0f73954892f98611e183f50acdc9e21a4653f294dfbe079da73c6378a6f47",
"sha256:ef14d75d86f104f03dea66c13188487151760ef25dd6b2dbd541885185f05f40",
"sha256:f26648e1b3b03b6022b48a9b910d0ae209e2d51f50441db5dce5b530fad6d9b1",
"sha256:f67472c09a0c7486e27f3275f617c964d25e35727af952869dd496b9b5b7f6a3"
],
"markers": "python_version >= '3.7'",
"version": "==7.0.5"
},
"dill": {
"hashes": [
"sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0",
"sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"
],
"markers": "python_version >= '3.7'",
"markers": "python_version >= '3.11'",
"version": "==0.3.6"
},
"flake8": {
@@ -144,45 +318,71 @@
"index": "pypi",
"version": "==6.0.0"
},
"gfm-toc": {
"hashes": [
"sha256:247af7267a6cbbdd4213f8383157997bcb07e39e819db737bd2dbfbdb94ee7ae",
"sha256:c53ed0e2cd400e89051377017ca98c11c9cef628b2effddf787db4fc19ff343d"
],
"index": "pypi",
"version": "==0.0.7"
},
"iniconfig": {
"hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
"sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
"sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"
],
"version": "==1.1.1"
"markers": "python_version >= '3.7'",
"version": "==2.0.0"
},
"isort": {
"hashes": [
"sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7",
"sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"
"sha256:6db30c5ded9815d813932c04c2f85a360bcdd35fed496f4d8f35495ef0a261b6",
"sha256:c033fd0edb91000a7f09527fe5c75321878f98322a77ddcc81adbd83724afb7b"
],
"markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
"version": "==5.10.1"
"markers": "python_full_version >= '3.7.0'",
"version": "==5.11.4"
},
"lazy-object-proxy": {
"hashes": [
"sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada",
"sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d",
"sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7",
"sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe",
"sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd",
"sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c",
"sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858",
"sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288",
"sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec",
"sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f",
"sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891",
"sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c",
"sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25",
"sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156",
"sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8",
"sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f",
"sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e",
"sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0",
"sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"
"sha256:09763491ce220c0299688940f8dc2c5d05fd1f45af1e42e636b2e8b2303e4382",
"sha256:0a891e4e41b54fd5b8313b96399f8b0e173bbbfc03c7631f01efbe29bb0bcf82",
"sha256:189bbd5d41ae7a498397287c408617fe5c48633e7755287b21d741f7db2706a9",
"sha256:18b78ec83edbbeb69efdc0e9c1cb41a3b1b1ed11ddd8ded602464c3fc6020494",
"sha256:1aa3de4088c89a1b69f8ec0dcc169aa725b0ff017899ac568fe44ddc1396df46",
"sha256:212774e4dfa851e74d393a2370871e174d7ff0ebc980907723bb67d25c8a7c30",
"sha256:2d0daa332786cf3bb49e10dc6a17a52f6a8f9601b4cf5c295a4f85854d61de63",
"sha256:5f83ac4d83ef0ab017683d715ed356e30dd48a93746309c8f3517e1287523ef4",
"sha256:659fb5809fa4629b8a1ac5106f669cfc7bef26fbb389dda53b3e010d1ac4ebae",
"sha256:660c94ea760b3ce47d1855a30984c78327500493d396eac4dfd8bd82041b22be",
"sha256:66a3de4a3ec06cd8af3f61b8e1ec67614fbb7c995d02fa224813cb7afefee701",
"sha256:721532711daa7db0d8b779b0bb0318fa87af1c10d7fe5e52ef30f8eff254d0cd",
"sha256:7322c3d6f1766d4ef1e51a465f47955f1e8123caee67dd641e67d539a534d006",
"sha256:79a31b086e7e68b24b99b23d57723ef7e2c6d81ed21007b6281ebcd1688acb0a",
"sha256:81fc4d08b062b535d95c9ea70dbe8a335c45c04029878e62d744bdced5141586",
"sha256:8fa02eaab317b1e9e03f69aab1f91e120e7899b392c4fc19807a8278a07a97e8",
"sha256:9090d8e53235aa280fc9239a86ae3ea8ac58eff66a705fa6aa2ec4968b95c821",
"sha256:946d27deaff6cf8452ed0dba83ba38839a87f4f7a9732e8f9fd4107b21e6ff07",
"sha256:9990d8e71b9f6488e91ad25f322898c136b008d87bf852ff65391b004da5e17b",
"sha256:9cd077f3d04a58e83d04b20e334f678c2b0ff9879b9375ed107d5d07ff160171",
"sha256:9e7551208b2aded9c1447453ee366f1c4070602b3d932ace044715d89666899b",
"sha256:9f5fa4a61ce2438267163891961cfd5e32ec97a2c444e5b842d574251ade27d2",
"sha256:b40387277b0ed2d0602b8293b94d7257e17d1479e257b4de114ea11a8cb7f2d7",
"sha256:bfb38f9ffb53b942f2b5954e0f610f1e721ccebe9cce9025a38c8ccf4a5183a4",
"sha256:cbf9b082426036e19c6924a9ce90c740a9861e2bdc27a4834fd0a910742ac1e8",
"sha256:d9e25ef10a39e8afe59a5c348a4dbf29b4868ab76269f81ce1674494e2565a6e",
"sha256:db1c1722726f47e10e0b5fdbf15ac3b8adb58c091d12b3ab713965795036985f",
"sha256:e7c21c95cae3c05c14aafffe2865bbd5e377cfc1348c4f7751d9dc9a48ca4bda",
"sha256:e8c6cfb338b133fbdbc5cfaa10fe3c6aeea827db80c978dbd13bc9dd8526b7d4",
"sha256:ea806fd4c37bf7e7ad82537b0757999264d5f70c45468447bb2b91afdbe73a6e",
"sha256:edd20c5a55acb67c7ed471fa2b5fb66cb17f61430b7a6b9c3b4a1e40293b1671",
"sha256:f0117049dd1d5635bbff65444496c90e0baa48ea405125c088e93d9cf4525b11",
"sha256:f0705c376533ed2a9e5e97aacdbfe04cecd71e0aa84c7c0595d02ef93b6e4455",
"sha256:f12ad7126ae0c98d601a7ee504c1122bcef553d1d5e0c3bfa77b16b3968d2734",
"sha256:f2457189d8257dd41ae9b434ba33298aec198e30adf2dcdaaa3a28b9994f6adb",
"sha256:f699ac1c768270c9e384e4cbd268d6e67aebcfae6cd623b4d7c3bfde5a35db59"
],
"markers": "python_version >= '3.7'",
"version": "==1.8.0"
"version": "==1.9.0"
},
"mccabe": {
"hashes": [
@@ -192,21 +392,72 @@
"markers": "python_version >= '3.6'",
"version": "==0.7.0"
},
"mypy": {
"hashes": [
"sha256:0714258640194d75677e86c786e80ccf294972cc76885d3ebbb560f11db0003d",
"sha256:0c8f3be99e8a8bd403caa8c03be619544bc2c77a7093685dcf308c6b109426c6",
"sha256:0cca5adf694af539aeaa6ac633a7afe9bbd760df9d31be55ab780b77ab5ae8bf",
"sha256:1c8cd4fb70e8584ca1ed5805cbc7c017a3d1a29fb450621089ffed3e99d1857f",
"sha256:1f7d1a520373e2272b10796c3ff721ea1a0712288cafaa95931e66aa15798813",
"sha256:209ee89fbb0deed518605edddd234af80506aec932ad28d73c08f1400ef80a33",
"sha256:26efb2fcc6b67e4d5a55561f39176821d2adf88f2745ddc72751b7890f3194ad",
"sha256:37bd02ebf9d10e05b00d71302d2c2e6ca333e6c2a8584a98c00e038db8121f05",
"sha256:3a700330b567114b673cf8ee7388e949f843b356a73b5ab22dd7cff4742a5297",
"sha256:3c0165ba8f354a6d9881809ef29f1a9318a236a6d81c690094c5df32107bde06",
"sha256:3d80e36b7d7a9259b740be6d8d906221789b0d836201af4234093cae89ced0cd",
"sha256:4175593dc25d9da12f7de8de873a33f9b2b8bdb4e827a7cae952e5b1a342e243",
"sha256:4307270436fd7694b41f913eb09210faff27ea4979ecbcd849e57d2da2f65305",
"sha256:5e80e758243b97b618cdf22004beb09e8a2de1af481382e4d84bc52152d1c476",
"sha256:641411733b127c3e0dab94c45af15fea99e4468f99ac88b39efb1ad677da5711",
"sha256:652b651d42f155033a1967739788c436491b577b6a44e4c39fb340d0ee7f0d70",
"sha256:6d7464bac72a85cb3491c7e92b5b62f3dcccb8af26826257760a552a5e244aa5",
"sha256:74e259b5c19f70d35fcc1ad3d56499065c601dfe94ff67ae48b85596b9ec1461",
"sha256:7d17e0a9707d0772f4a7b878f04b4fd11f6f5bcb9b3813975a9b13c9332153ab",
"sha256:901c2c269c616e6cb0998b33d4adbb4a6af0ac4ce5cd078afd7bc95830e62c1c",
"sha256:98e781cd35c0acf33eb0295e8b9c55cdbef64fcb35f6d3aa2186f289bed6e80d",
"sha256:a12c56bf73cdab116df96e4ff39610b92a348cc99a1307e1da3c3768bbb5b135",
"sha256:ac6e503823143464538efda0e8e356d871557ef60ccd38f8824a4257acc18d93",
"sha256:b8472f736a5bfb159a5e36740847808f6f5b659960115ff29c7cecec1741c648",
"sha256:b86ce2c1866a748c0f6faca5232059f881cda6dda2a893b9a8373353cfe3715a",
"sha256:bc9ec663ed6c8f15f4ae9d3c04c989b744436c16d26580eaa760ae9dd5d662eb",
"sha256:c9166b3f81a10cdf9b49f2d594b21b31adadb3d5e9db9b834866c3258b695be3",
"sha256:d13674f3fb73805ba0c45eb6c0c3053d218aa1f7abead6e446d474529aafc372",
"sha256:de32edc9b0a7e67c2775e574cb061a537660e51210fbf6006b0b36ea695ae9bb",
"sha256:e62ebaad93be3ad1a828a11e90f0e76f15449371ffeecca4a0a0b9adc99abcef"
],
"index": "pypi",
"version": "==0.991"
},
"mypy-extensions": {
"hashes": [
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
],
"version": "==0.4.3"
},
"mypy-protobuf": {
"hashes": [
"sha256:7d75a079651b105076776a35a5405e3fa773b8a167118f1b712e443e9a6c18a2",
"sha256:da33dfde7547ff57e5ba5564126cbfa114f14413b2fa50759b1fa5de1e4ab511"
],
"index": "pypi",
"version": "==3.4.0"
},
"packaging": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
"sha256:714ac14496c3e68c99c29b00845f7a2b85f3bb6f1078fd9f72fd20f0570002b2",
"sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
"markers": "python_version >= '3.7'",
"version": "==23.0"
},
"platformdirs": {
"hashes": [
"sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7",
"sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"
"sha256:83c8f6d04389165de7c9b6f0c682439697887bca0aa2f1c87ef1826be3584490",
"sha256:e1fea1fe471b9ff8332e229df3cb7de4f53eeea4998d3b6bfff542115e998bd2"
],
"markers": "python_version >= '3.7'",
"version": "==2.5.4"
"version": "==2.6.2"
},
"pluggy": {
"hashes": [
@@ -216,6 +467,26 @@
"markers": "python_version >= '3.6'",
"version": "==1.0.0"
},
"protobuf": {
"hashes": [
"sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30",
"sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b",
"sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc",
"sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791",
"sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717",
"sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec",
"sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7",
"sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab",
"sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2",
"sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5",
"sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1",
"sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462",
"sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97",
"sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"
],
"index": "pypi",
"version": "==4.21.12"
},
"pycodestyle": {
"hashes": [
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
@@ -234,27 +505,59 @@
},
"pylint": {
"hashes": [
"sha256:1d561d1d3e8be9dd880edc685162fbdaa0409c88b9b7400873c0cf345602e326",
"sha256:91e4776dbcb4b4d921a3e4b6fec669551107ba11f29d9199154a01622e460a57"
"sha256:9df0d07e8948a1c3ffa3b6e2d7e6e63d9fb457c5da5b961ed63106594780cc7e",
"sha256:b3dc5ef7d33858f297ac0d06cc73862f01e4f2e74025ec3eff347ce0bc60baf5"
],
"index": "pypi",
"version": "==2.15.7"
"version": "==2.15.10"
},
"pyparsing": {
"pyproject-hooks": {
"hashes": [
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
"sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
"sha256:283c11acd6b928d2f6a7c73fa0d01cb2bdc5f07c57a2eeb6e83d5e56b97976f8",
"sha256:f271b298b97f5955d53fb12b72c1fb1948c22c1a6b70b315c54cedaca0264ef5"
],
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.9"
"markers": "python_version >= '3.7'",
"version": "==1.0.0"
},
"pytest": {
"hashes": [
"sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
"sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
"sha256:c7c6ca206e93355074ae32f7403e8ea12163b1163c976fee7d4d84027c162be5",
"sha256:d45e0952f3727241918b8fd0f376f5ff6b301cc0777c6f9a556935c92d8a7d42"
],
"index": "pypi",
"version": "==7.2.0"
"version": "==7.2.1"
},
"pytest-cov": {
"hashes": [
"sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b",
"sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"
],
"index": "pypi",
"version": "==4.0.0"
},
"pytest-mock": {
"hashes": [
"sha256:f4c973eeae0282963eb293eb173ce91b091a79c1334455acfac9ddee8a1c784b",
"sha256:fbbdb085ef7c252a326fd8cdcac0aa3b1333d8811f131bdcc701002e1be7ed4f"
],
"index": "pypi",
"version": "==3.10.0"
},
"setuptools": {
"hashes": [
"sha256:6f590d76b713d5de4e49fe4fbca24474469f53c83632d5d0fd056f7ff7e8112b",
"sha256:ac4008d396bc9cd983ea483cb7139c0240a07bbc74ffb6232fceffedc6cf03a8"
],
"markers": "python_version >= '3.7'",
"version": "==66.1.1"
},
"setuptools-git-versioning": {
"hashes": [
"sha256:648481f7e1e9e12ccd2b069d616b909a338b4223956319649351751cbc0207f4",
"sha256:fde1a7cb3b2566979e5651cfca0d33cd5a82771711cd38a056216391936cf0ff"
],
"index": "pypi",
"version": "==1.13.1"
},
"tomlkit": {
"hashes": [
@@ -264,6 +567,22 @@
"markers": "python_version >= '3.6'",
"version": "==0.11.6"
},
"types-protobuf": {
"hashes": [
"sha256:6c87c7f8df61d57a53de8221777e4fcc3c7ed24419fbf43b8e9f50887f3773fa",
"sha256:824109e0fe87525a9d2da4cc4eec36ca004f1a0f3d1c0838cfd2873a484cffdd"
],
"index": "pypi",
"version": "==4.21.0.3"
},
"typing-extensions": {
"hashes": [
"sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa",
"sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"
],
"markers": "python_version >= '3.7'",
"version": "==4.4.0"
},
"wheel": {
"hashes": [
"sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac",

616
README.md
View File

@@ -1,62 +1,334 @@
# Extract TOTP/HOTP secret keys from Google Authenticator
# Extract TOTP/HOTP secrets from QR codes exported by two-factor authentication apps
[![CI Status](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml/badge.svg)](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/protobuf)
[![GitHub Pipenv locked Python version](https://img.shields.io/github/pipenv/locked/python-version/scito/extract_otp_secret_keys)](https://github.com/scito/extract_otp_secret_keys/blob/master/Pipfile.lock)
![protobuf version](https://img.shields.io/badge/protobuf-4.21.10-informational)
[![License](https://img.shields.io/github/license/scito/extract_otp_secret_keys)](https://github.com/scito/extract_otp_secret_keys/blob/master/LICENSE)
[![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/scito/extract_otp_secret_keys?sort=semver&label=version)](https://github.com/scito/extract_otp_secret_keys/tags)
[![CI tests](https://github.com/scito/extract_otp_secrets/actions/workflows/ci.yml/badge.svg)](https://github.com/scito/extract_otp_secrets/actions/workflows/ci.yml)
[![CI docker](https://github.com/scito/extract_otp_secrets/actions/workflows/ci_docker.yml/badge.svg)](https://github.com/scito/extract_otp_secrets/actions/workflows/ci_docker.yml)
![coverage](https://img.shields.io/badge/coverage-94%25-brightgreen)
<!-- ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/protobuf)
[![GitHub Pipenv locked Python version](https://img.shields.io/github/pipenv/locked/python-version/scito/extract_otp_secrets)](https://github.com/scito/extract_otp_secrets/blob/master/Pipfile.lock)
![protobuf version](https://img.shields.io/badge/protobuf-4.21.12-informational)-->
![python versions](https://img.shields.io/badge/python-3.7%20%7C%203.8%20%7C%203.9%20%7C%203.10%20%7C%203.11-blue)
[![License](https://img.shields.io/github/license/scito/extract_otp_secrets)](https://github.com/scito/extract_otp_secrets/blob/master/LICENSE)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/scito/extract_otp_secrets?sort=semver)](https://github.com/scito/extract_otp_secrets/releases/latest)
[![Download executable](https://img.shields.io/badge/download-exe-blue)](https://github.com/scito/extract_otp_secrets/releases/latest)
[![Linux](https://img.shields.io/badge/os-linux-yellow)](https://github.com/scito/extract_otp_secrets/releases/latest)
[![Windows](https://img.shields.io/badge/os-windows-yellow)](https://github.com/scito/extract_otp_secrets/releases/latest)
[![MacOS](https://img.shields.io/badge/os-macos-yellow)](https://github.com/scito/extract_otp_secrets/releases/latest)
[![Docker image](https://img.shields.io/badge/docker-image-blue)](https://hub.docker.com/repository/docker/scit0/extract_otp_secrets/general)
<!-- [![Github all releases](https://img.shields.io/github/downloads/scito/extract_otp_secrets/total.svg)](https://GitHub.com/scito/extract_otp_secrets/releases/) -->
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
---
Extract two-factor authentication (2FA, TFA, one time passwords, 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:
1. Capture the QR codes with the system camera using a GUI, 🆕
2. Read image files containing the QR codes, and 🆕
3. Read text files containing the QR code data generated by third-party QR readers.
The 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
- [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](#installation)
- [Download binary executable (🆕 since v2.1)](#download-binary-executable--since-v21)
- [Run as script (recommend for developers or advanced users)](#run-as-script-recommend-for-developers-or-advanced-users)
- [Installation of shared system libraries](#installation-of-shared-system-libraries)
- [Program help: arguments and options](#program-help-arguments-and-options)
- [Examples](#examples)
- [Printing otp secrets form text file](#printing-otp-secrets-form-text-file)
- [Printing otp secrets from image file](#printing-otp-secrets-from-image-file)
- [Writing otp secrets to csv file](#writing-otp-secrets-to-csv-file)
- [Writing otp secrets to json file](#writing-otp-secrets-to-json-file)
- [Printing otp secrets multiple files](#printing-otp-secrets-multiple-files)
- [Printing otp secrets from stdin (text)](#printing-otp-secrets-from-stdin-text)
- [Printing otp secrets from stdin (image)](#printing-otp-secrets-from-stdin-image)
- [Printing otp secrets csv to stdout](#printing-otp-secrets-csv-to-stdout)
- [Printing otp secrets csv to stdout without header line](#printing-otp-secrets-csv-to-stdout-without-header-line)
- [Reading from stdin and printing to stdout](#reading-from-stdin-and-printing-to-stdout)
- [Features](#features)
- [KeePass](#keepass)
- [How to export otp secrets from Google Authenticator app](#how-to-export-otp-secrets-from-google-authenticator-app)
- [Glossary](#glossary)
- [Alternative installation methods](#alternative-installation-methods)
- [pip using github](#pip-using-github)
- [local pip](#local-pip)
- [pipenv](#pipenv)
- [Visual Studio Code Remote - Containers / VSCode devcontainer](#visual-studio-code-remote---containers--vscode-devcontainer)
- [venv](#venv)
- [devbox](#devbox)
- [docker](#docker)
- [More docker examples](#more-docker-examples)
- [Tests](#tests)
- [PyTest](#pytest)
- [unittest](#unittest)
- [VSCode Setup](#vscode-setup)
- [Development](#development)
- [Build](#build)
- [Upgrade pip Packages](#upgrade-pip-packages)
- [Build docker images](#build-docker-images)
- [Create executables with pyinstaller](#create-executables-with-pyinstaller)
- [Full local build (bash)](#full-local-build-bash)
- [Technical background](#technical-background)
- [References](#references)
- [Issues](#issues)
- [Problems and Troubleshooting](#problems-and-troubleshooting)
- [Windows error message](#windows-error-message)
- [Related projects](#related-projects)
</details>
## 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
```
or
```
python src/extract_otp_secrets.py
```
![CV2 Capture from camera screenshot](docs/cv2_capture_screenshot.png)
Detected QR codes are surrounded with a frame. The color of the frame indicates the extracting result:
* 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.
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:
```
python src/extract_otp_secrets.py 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:
```
python src/extract_otp_secrets.py example_export.txt
```
7. Remove unencrypted files with secrets from your computer and mobile.
python extract_otp_secret_keys.py example_export.txt
## Installation
### Download binary executable (🆕 since v2.1)
1. Download executable for your platform from [latest release](https://github.com/scito/extract_otp_secrets/releases/latest), see assets
2. Start executable by clicking or from command line
✅ Everything is just packed in one executable.
✅ No installation needed, neither Python nor dependencies have to be installed.
✅ Easy and convenient
### Run as script (recommend 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 shared system libraries
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 libzbar0
#### 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] [--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 (default: -) 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
--csv FILE, -c FILE export csv file
--keepass FILE, -k FILE export totp/hotp csv file(s) for KeePass
--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</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
--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)
--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.0, protobuf 4.21.10, 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 exports (and compatible apps like Aegis Authenticator)
* Captures the the QR codes directly from the camera using different QR code libraries (based on OpenCV) (🆕 since v2.0)
* ZBAR: [pyzbar](https://github.com/NaturalHistoryMuseum/pyzbar) - fast and reliable, good for images and video capture (default and recommended)
* QREADER: [QReader](https://github.com/Eric-Canas/QReader)
* QREADER_DEEP: [QReader](https://github.com/Eric-Canas/QReader) - very slow in GUI
* CV2: [QRCodeDetector](https://docs.opencv.org/4.x/de/dc3/classcv_1_1QRCodeDetector.html)
* CV2_WECHAT: [WeChatQRCode](https://docs.opencv.org/4.x/dd/d63/group__wechat__qrcode.html)
* Supports [TOTP](https://www.ietf.org/rfc/rfc6238.txt) and [HOTP](https://www.ietf.org/rfc/rfc4226.txt) standards
* Generates QR codes
* Exports to various formats:
* CSV
* JSON
* Dedicated CSV for KeePass
* QR code images
* 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.28)
* extract_otp_secrets_win_x86_64.exe
* extract_otp_secrets_macos_x86_64 (untested)
* 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
* Compatible with major platforms:
* Linux
* macOS
* Windows
* Uses UTF-8 on all platforms
* Supports Python >= 3.7
* 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
@@ -96,47 +368,88 @@ 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).
1. Open "Google Authenticator" app
2. Select "Transfer accounts" in the three dot menu of the app.
![Transfer accounts option in the Google Authenticator.](docs/Transfer-accounts-option-in-the-Google-Authenticator_300px.webp)
3. Select "Export accounts"
![Export account option in the Google Authenticator.](docs/Export-account-option-in-the-Google-Authenticator_300px.webp)
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.
![Exported Google Authenticator QR codes](docs/Exported-QR-codes_300px.webp)
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):
## Glossary
protoc --python_out=protobuf_generated_python google_auth.proto
The generated protobuf Python code was generated by protoc 21.10 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.10).
## References
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
* OTP = One-time password
* TOTP = Time-based one-time password
* HOTP = HMAC-based one-time password (using a counter)
* 2FA = Second factor authentication
* TFA = Two factor authentication
* QR code = Quick response code
## Alternative installation methods
### pip using github
```
pip install -U git+https://github.com/scito/extract_otp_secrets
extract_otp_secrets
```
or run it
```
python -m extract_otp_secrets
```
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
@@ -144,8 +457,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`.
@@ -158,22 +471,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:
@@ -185,9 +526,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 dont 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:
@@ -195,6 +546,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.
@@ -204,7 +557,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
@@ -212,10 +577,139 @@ 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 --build-arg RUN_TESTS=false
```
Run tests in docker container:
```bash
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets
```
#### Alpine (only text file processing)
```bash
docker build . -t extract_otp_secrets:only_txt --pull -f Dockerfile_only_txt --build-arg RUN_TESTS=false
```
Run tests in docker container:
```bash
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt extract_otp_secrets_test.py -k "not qreader" --relaxed
```
### Create executables with pyinstaller
#### Linux
```bash
pyinstaller -y --add-data $pythonLocation/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile src/extract_otp_secrets.py
```
Output is executable `dist/extract_otp_secrets`
#### Windows
```
pyinstaller -y --add-data "%pythonLocation%\__yolo_v3_qr_detector;__yolo_v3_qr_detector" --add-binary "%pythonLocation%\pyzbar\libiconv.dll;pyzbar" --add-binary "%pythonLocation%\pyzbar\libzbar-64.dll;pyzbar" --onefile --version-file build\file_version_info.txt src\extract_otp_secrets.py
```
Output is `dist\extract_otp_secrets.exe`
### Full local build (bash)
There is a Bash script for a full local build including linting and type checking.
```bash
./build.sh
```
The options of the build script:
```
Build extract_otp_secrets project
./build.sh [options]
Options:
-i Interactive mode, all steps must be confirmed
-C Ignore version check of protobuf/protoc
-D Do not build docker
-G Do not start extract_otp_secrets.py in GUI mode
-c Clean everything
-r Generate result files
-h, --help Help
```
## Technical background
The export QR code of "Google Authenticator" contains the URL `otpauth-migration://offline?data=…`.
The data parameter is a base64 encoded proto3 message (Google Protocol Buffers).
Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition or new protobuf versions):
protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python src/google_auth.proto
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
For Python type hint generation the [mypy-protobuf](https://github.com/nipunn1313/mypy-protobuf) package is used.
## References
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
## Issues
* 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.
* [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.

488
build.sh Executable file
View File

@@ -0,0 +1,488 @@
#!/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_docker=true
run_gui=true
generate_result_files=false
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 "-D Do not build docker"
echo "-G Do not start extract_otp_secrets.py in GUI mode"
echo "-c Clean everything"
echo "-r Generate result files"
echo "-h, --help Help"
quit
;;
-a)
interactive=true
shift
;;
-C)
ignore_version_check=false
shift
;;
-D)
build_docker=false
shift
;;
-G)
run_gui=false
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"
fi
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
echo -e "\n\nChecking Protoc version..."
VERSION=$(curl -sL https://github.com/protocolbuffers/protobuf/releases/latest | grep -E "<title>" | perl -pe's%.*Protocol Buffers v(\d+\.\d+(\.\d+)?).*%\1%')
BASEVERSION=4
echo
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
echo -e "\nProtoc remote version $VERSION\n"
echo -e "Protoc local version: $OLDVERSION\n"
if [ "$OLDVERSION" != "$VERSION" ] || ! $ignore_version_check; then
echo "Upgrade protoc from $OLDVERSION to $VERSION"
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"
# 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
cmd="$PIP install -U pipenv"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
$PIPENV --version
cmd="$PIPENV update && $PIPENV --rm && $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"
# Build wheel
cmd="$PIP wheel ."
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Build executable
cmd="LOCAL_GLIBC_VERSION=$(ldd --version | sed '1!d' | sed -E 's/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/')"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
echo "local glibc: $LOCAL_GLIBC_VERSION"
cmd="pyinstaller -y --add-data $HOME/.local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile $clean_flag src/extract_otp_secrets.py"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="dist/extract_otp_secrets -h"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Generate README.md TOC
cmd="gfm-toc -s 2 -e 3 -t -o README.md > docs/README_TOC.md"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Update Code Coverage in README.md
# 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
if $build_docker; then
# Build docker
# 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 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 Bullseye)
cmd="docker build . -t extract_otp_secrets -t extract_otp_secrets:bullseye --pull --build-arg RUN_TESTS=false"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
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 Buster)
cmd="docker build . -t extract_otp_secrets:buster --pull --build-arg RUN_TESTS=false --build-arg BASE_IMAGE=python:3.11-slim-buster"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="docker run --rm -v \"$(pwd)\":/files:ro extract_otp_secrets:buster example_export.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="cat example_export.txt | docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets:buster - -c - > example_output.csv"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets:buster = < example_export.png"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secrets:buster"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Build executable from Docker latest
# sed "1!d" is workaround for head -n 1 since it head procduces exit code != 0
BULLSEYE_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
echo "Bullseye glibc: $BULLSEYE_GLIBC_VERSION"
cmd="docker run --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets -c 'apt-get update && apt-get -y install binutils && pip install -U -r /files/requirements.txt && pip install pyinstaller && pyinstaller -y --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_linux_x86_64_bullseye --distpath /files/dist/ /files/src/extract_otp_secrets.py'"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="dist/extract_otp_secrets_linux_x86_64_bullseye -h"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Build executable from Docker buster
BUSTER_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets:buster -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
echo "Bullseye glibc: $BUSTER_GLIBC_VERSION"
cmd="docker run --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:buster -c 'apt-get update && apt-get -y install binutils && pip install -U -r /files/requirements.txt && pip install pyinstaller && pyinstaller -y --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_linux_x86_64 --distpath /files/dist/ /files/src/extract_otp_secrets.py'"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
cmd="dist/extract_otp_secrets_linux_x86_64 -h"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# create Windows file_version_info.txt
cmd="VERSION_STR=$(setuptools-git-versioning) VERSION_MAJOR=$(cut -d '.' -f 1 <<< "$(setuptools-git-versioning)") VERSION_MINOR=$(cut -d '.' -f 2 <<< "$(setuptools-git-versioning)") VERSION_PATCH=$(cut -d '.' -f 3 <<< "$(setuptools-git-versioning)") VERSION_BUILD=$(($(git rev-list --count $(git tag | sort -V -r | sed '1!d')..HEAD))) YEARS='2020-2023' envsubst < file_version_info_template.txt > build/file_version_info.txt"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
eval "$cmd"
# Run GUI from Docker
if $run_gui; then
cmd="docker run --rm -v "$(pwd)":/files:ro --device=\"/dev/video0:/dev/video0\" --env=\"DISPLAY\" -v /tmp/.X11-unix:/tmp/.X11-unix:ro extract_otp_secrets &"
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
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
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"
line=$(printf '#%.0s' $(eval echo {1..$(( ($COLUMNS - 10) / 2))}))
echo -e "\n${greenBold}$line SUCCESS $line${reset}"
quit

4
docker/.alias Normal file
View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
docs/Exported-QR-codes.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

56
docs/README_TOC.md Normal file
View File

@@ -0,0 +1,56 @@
Generate from file: README.md
## Table of contents
- [Table of contents](#table-of-contents)
- [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](#installation)
- [Download binary executable (🆕 since v2.1)](#download-binary-executable--since-v21)
- [Run as script (recommend for developers or advanced users)](#run-as-script-recommend-for-developers-or-advanced-users)
- [Installation of shared system libraries](#installation-of-shared-system-libraries)
- [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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 KiB

BIN
example_export.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

View File

@@ -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
@@ -12,3 +13,7 @@ otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXB
# 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

View File

@@ -3,3 +3,4 @@ raspberrypi,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
raspberrypi,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
,encoding: ¿äÄéÉ? (demo),7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
1 Title User Name TimeOtp-Secret-Base32 Group
3 pi@raspberrypi 7KSQL2JTUDIS5EF65KLMRQIIGY OTP/TOTP
4 pi@raspberrypi 7KSQL2JTUDIS5EF65KLMRQIIGY OTP/TOTP
5 raspberrypi pi@raspberrypi 7KSQL2JTUDIS5EF65KLMRQIIGY OTP/TOTP
6 encoding: ¿äÄéÉ? (demo) 7KSQL2JTUDIS5EF65KLMRQIIGY OTP/TOTP

View File

@@ -4,3 +4,4 @@ pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
hotp demo,7KSQL2JTUDIS5EF65KLMRQIIGY,,hotp,4,otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
encoding: ¿äÄéÉ? (demo),7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
1 name secret issuer type counter url
4 pi@raspberrypi 7KSQL2JTUDIS5EF65KLMRQIIGY totp otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
5 pi@raspberrypi 7KSQL2JTUDIS5EF65KLMRQIIGY raspberrypi totp otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
6 hotp demo 7KSQL2JTUDIS5EF65KLMRQIIGY hotp 4 otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
7 encoding: ¿äÄéÉ? (demo) 7KSQL2JTUDIS5EF65KLMRQIIGY totp otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY

View File

@@ -38,5 +38,13 @@
"type": "hotp",
"counter": 4,
"url": "otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4"
},
{
"name": "encoding: ¿äÄéÉ? (demo)",
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
"issuer": "",
"type": "totp",
"counter": null,
"url": "otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY"
}
]
]

View File

@@ -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"
],

View File

@@ -1,289 +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
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 (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
arg_parser.add_argument('--json', '-j', help='export json file', metavar=('FILE'))
arg_parser.add_argument('--csv', '-c', help='export csv file', metavar=('FILE'))
arg_parser.add_argument('--keepass', '-k', help='export totp/hotp csv file(s) for KeePass', 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'))
arg_parser.add_argument('--verbose', '-v', help='verbose output', action='count')
arg_parser.add_argument('--quiet', '-q', help='no stdout output', action='store_true')
args = arg_parser.parse_args(sys_args)
if args.verbose and args.quiet:
print("The arguments --verbose and --quiet are mutually exclusive.")
sys.exit(1)
return args
def extract_otps(args):
global verbose, quiet
quiet = args.quiet
otps = []
i = j = 0
for line in (line.strip() for line in fileinput.input(args.infile)):
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)
return otps
def get_payload_from_line(line, i, args):
global verbose
if not line.startswith('otpauth-migration://'):
print('\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))
params = parse_qs(parsed_url.query, strict_parsing=True)
if verbose > 1: print('\nDEBUG: querystring params={}'.format(params))
if 'data' not in params:
print('\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()
payload.ParseFromString(data)
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(args.csv, "w") 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(otp_filename_totp, "w") 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(otp_filename_hotp, "w") 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(args.json, "w") 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 "")
if __name__ == '__main__':
sys_main()

View File

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

4
mypy.ini Normal file
View File

@@ -0,0 +1,4 @@
[mypy]
[mypy-protobuf_generated_python.*]
ignore_errors = True

75
pyproject.toml Normal file
View File

@@ -0,0 +1,75 @@
[build-system]
requires = [
"setuptools>=64.0.0", "wheel>=0.37.0", "pip",
# https://setuptools-git-versioning.readthedocs.io/en/latest/differences.html
"setuptools-git-versioning",
]
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",
# workaround for PYTHON <= 3.7: compatibility
"typing_extensions; python_version<='3.7'",
"importlib_metadata; python_version<='3.7'",
]
description = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as 'Google Authenticator'"
dynamic = ["version"]
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
View File

@@ -0,0 +1,3 @@
[pytest]
markers =
qreader: QR image reader tests

View File

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

View File

@@ -1,3 +1,10 @@
protobuf
qrcode
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
typing_extensions; python_version<='3.7'
importlib_metadata; python_version<='3.7'

5
run_pytest.sh Executable file
View 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
View 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

5
setup.py Normal file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/python3
from setuptools import setup
# compatibility with legacy builds or versions of tools that dont support certain packaging standards
setup()

846
src/extract_otp_secrets.py Normal file
View File

@@ -0,0 +1,846 @@
# 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 typing import Any, List, Optional, Sequence, TextIO, Tuple, Union
import colorama
from pkg_resources import DistributionNotFound, get_distribution
from qrcode import QRCode # type: ignore
import protobuf_generated_python.google_auth_pb2 as pb
# workaround for PYTHON <= 3.7: compatibility
if sys.version_info >= (3, 8):
from typing import Final, TypedDict
else:
from typing_extensions import Final, TypedDict
# workaround for PYTHON <= 3.7: compatibility
if sys.version_info >= (3, 8):
from importlib.metadata import PackageNotFoundError, version
else:
from importlib_metadata import PackageNotFoundError, version
debug_mode = '-d' in sys.argv[1:] or '--debug' in sys.argv[1:]
try:
import cv2 # type: ignore # TODO use cv2 types if available
import numpy as np # TODO use numpy types if available
try:
import pyzbar.pyzbar as zbar # type: ignore
from qreader import QReader # type: ignore
except ImportError as e:
print(f"""
ERROR: Cannot import QReader module. This problem is probably due to the missing zbar shared library.
On Linux and macOS libzbar0 must be installed.
See in README.md for the installation of the libzbar0.
Exception: {e}\n""", file=sys.stderr)
raise e
# 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
# workaround for PYTHON <= 3.7: must use () for assignments
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'])
qreader_available = True
except ImportError as e:
qreader_available = False
if debug_mode:
raise e
# Workaround for PYTHON <= 3.9: Union[int, None] used instead of int | None
# Types
Args = argparse.Namespace
OtpUrl = str
# workaround for PYTHON <= 3.7: 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'
# Global variable declaration
verbose: IntEnum = LogLevel.NORMAL
quiet: bool = False
colored: bool = True
executable: bool = False
__version__: str
def sys_main() -> None:
main(sys.argv[1:])
def main(sys_args: list[str]) -> None:
global executable
# 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')
args = parse_args(sys_args)
if colored:
colorama.just_fix_windows_console()
if verbose >= LogLevel.DEBUG:
print(f"Version: {get_full_version()}\n")
if args.debug:
sys.exit(0 if do_debug_checks() else 1)
otps = extract_otps(args)
write_csv(args, otps)
write_keepass_csv(args, otps)
write_json(args, otps)
# workaround for PYTHON <= 3.9 use: pb.MigrationPayload | None
def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.MigrationPayload]:
'''Extracts the otp migration payload from an otp url. This function is the core of the this appliation.'''
if not is_opt_url(otp_url, source):
return None
parsed_url = urlparse.urlparse(otp_url)
if verbose >= LogLevel.DEBUG: log_debug(f"parsed_url={parsed_url}")
try:
params = urlparse.parse_qs(parsed_url.query, strict_parsing=True)
except Exception: # workaround for PYTHON <= 3.10
params = {}
if verbose >= LogLevel.DEBUG: log_debug(f"querystring params={params}")
if 'data' not in params:
log_error(f"could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}")
return None
data_base64 = params['data'][0]
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64={data_base64}")
data_base64_fixed = data_base64.replace(' ', '+')
if verbose >= LogLevel.DEBUG: log_debug(f"data_base64_fixed={data_base64_fixed}")
data = base64.b64decode(data_base64_fixed, validate=True)
payload = pb.MigrationPayload()
try:
payload.ParseFromString(data)
except Exception as e:
abort(f"Cannot decode otpauth-migration migration payload.\n"
f"data={data_base64}", e)
if verbose >= LogLevel.DEBUG: log_debug(f"\n{i}. Payload Line", payload, sep='\n')
return payload
def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: int, infile: str, args: Args) -> int:
'''Converts the otp migration payload into a normal Python dictionary. This function is the core of the this appliation.'''
payload = get_payload_from_otp_url(otpauth_migration_url, urls_count, infile)
if not payload:
return 0
new_otps_count = 0
# pylint: disable=no-member
for raw_otp in payload.otp_parameters:
if verbose: print(f"\n{len(otps) + 1}. Secret")
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
if verbose >= LogLevel.DEBUG: log_debug('OTP enum type:', get_enum_name_by_number(raw_otp, 'type'))
otp_type = get_otp_type_str_from_code(raw_otp.type)
otp_url = build_otp_url(secret, raw_otp)
otp: Otp = {
"name": raw_otp.name,
"secret": secret,
"issuer": raw_otp.issuer,
"type": otp_type,
"counter": raw_otp.counter if raw_otp.type == 1 else None,
"url": otp_url
}
if otp not in otps or not args.ignore:
otps.append(otp)
new_otps_count += 1
if not quiet:
print_otp(otp)
if args.printqr:
print_qr(args, otp_url)
if args.saveqr:
save_qr(otp, args, len(otps))
if not quiet:
print()
elif args.ignore and not quiet:
eprint(f"Ignored duplicate otp: {otp['name']}", f" / {otp['issuer']}\n" if otp['issuer'] else '\n', sep='')
return new_otps_count
def parse_args(sys_args: list[str]) -> Args:
global verbose, quiet, colored
# For PYTHON <= 3.7: Use :=
name = os.path.basename(sys.argv[0])
cmd = f"python {name}" if name.endswith('.py') else f"{name}"
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
if qreader_available:
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 qreader_available else '+')
arg_parser.add_argument('--csv', '-c', help='export csv file or - for stdout', metavar=('FILE'))
arg_parser.add_argument('--keepass', '-k', help='export totp/hotp csv file(s) for KeePass, - for stdout', metavar=('FILE'))
arg_parser.add_argument('--json', '-j', help='export json file or - for stdout', metavar=('FILE'))
arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal (requires qrcode module)', action='store_true')
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
if qreader_available:
arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, metavar=('NUMBER'))
arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.ZBAR.name})', type=str, choices=[mode.name for mode in QRMode], default=QRMode.ZBAR.name)
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 == '-':
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: {qreader_available}")
if qreader_available:
if verbose >= LogLevel.VERBOSE: print(f"CV2 version: {cv2.__version__}")
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)
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, "Hit ESC to quit", 1, TextPosition.LEFT, FONT_COLOR, 17)
cv2_print_text(img, f"{len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} captured", 0, TextPosition.RIGHT, FONT_COLOR)
cv2_print_text(img, f"{len(otps)} otp{'s'[:len(otps) != 1]} extracted", 1, TextPosition.RIGHT, FONT_COLOR)
cv2.imshow(WINDOW_NAME, img)
quit, qr_mode = cv2_handle_pressed_keys(qr_mode)
if quit:
break
cam.release()
cv2.destroyAllWindows()
return otps
def get_color(new_otps_count: int, otp_url: str) -> ColorBGR:
if new_otps_count:
return SUCCESS_COLOR
else:
if otp_url:
return FAILURE_COLOR
else:
return NORMAL_COLOR
# TODO use cv2 types if available
def cv2_draw_box(img: Any, raw_pts: Any, color: ColorBGR) -> Any:
pts = np.array([raw_pts], np.int32)
pts = pts.reshape((-1, 1, 2))
cv2.polylines(img, [pts], True, color, BOX_THICKNESS)
return pts
# TODO use cv2 types if available
def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition, color: ColorBGR, opposite_len: Optional[int] = None) -> None:
window_dim = cv2.getWindowImageRect(WINDOW_NAME)
out_text = text
if opposite_len:
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
actual_width = text_dim[TEXT_WIDTH] + opposite_len * CHAR_DX + 4 * BORDER
if actual_width >= window_dim[WINDOW_WIDTH]:
out_text = out_text[:(window_dim[WINDOW_WIDTH] - actual_width) // CHAR_DX] + '.'
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
if position == TextPosition.LEFT:
pos = BORDER, START_Y + line_number * FONT_DY
else:
pos = window_dim[WINDOW_WIDTH] - text_dim[TEXT_WIDTH] - BORDER, START_Y + line_number * FONT_DY
cv2.putText(img, out_text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE)
def cv2_handle_pressed_keys(qr_mode: QRMode) -> Tuple[bool, QRMode]:
key = cv2.waitKey(1) & 0xFF
quit = False
if key == 27 or key == ord('q') or key == 13:
# ESC or Enter or q pressed
quit = True
elif key == 32:
qr_mode = next_qr_mode(qr_mode)
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 qreader_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) -> None:
print(f"Name: {otp['name']}")
print(f"Secret: {otp['secret']}")
if otp['issuer']: print(f"Issuer: {otp['issuer']}")
print(f"Type: {otp['type']}")
if otp['type'] == 'hotp':
print(f"Counter: {otp['counter']}")
if verbose:
print(otp['url'])
def save_qr(otp: Otp, args: Args, j: int) -> str:
dir = args.saveqr
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_file(args, otp['url'], f"{dir}/{j}-{file_otp_name}{'-' + file_otp_issuer if file_otp_issuer else ''}.png")
return file_otp_name
def save_qr_file(args: Args, 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(args: Args, otp_url: str) -> None:
qr = QRCode()
qr.add_data(otp_url)
qr.print_ascii()
def write_csv(args: Args, otps: Otps) -> None:
if args.csv and len(otps) > 0:
with open_file_or_stdout_for_csv(args.csv) as outfile:
writer = csv.DictWriter(outfile, otps[0].keys())
writer.writeheader()
writer.writerows(otps)
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to csv {args.csv}")
def write_keepass_csv(args: Args, otps: Otps) -> None:
if args.keepass and len(otps) > 0:
has_totp = has_otp_type(otps, 'totp')
has_hotp = has_otp_type(otps, 'hotp')
if args.keepass != '-':
otp_filename_totp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "totp")
otp_filename_hotp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "hotp")
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 count_totp_entries: print(f"Exported {count_totp_entries} totp entrie{'s'[:count_totp_entries != 1]} to keepass csv file {otp_filename_totp}")
if count_hotp_entries: print(f"Exported {count_hotp_entries} hotp entrie{'s'[:count_hotp_entries != 1]} to keepass csv file {otp_filename_hotp}")
def write_keepass_totp_csv(otp_filename: str, otps: Otps) -> int:
count_entries = 0
with open_file_or_stdout_for_csv(otp_filename) 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(otp_filename: str, otps: Otps) -> int:
count_entries = 0
with open_file_or_stdout_for_csv(otp_filename) 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(args: Args, otps: Otps) -> None:
if args.json:
with open_file_or_stdout(args.json) as outfile:
json.dump(otps, outfile, indent=4)
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to json {args.json}")
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 is_binary(line: str) -> bool:
try:
line.startswith('#')
return False
except (UnicodeDecodeError, AttributeError, TypeError):
return True
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
print(color('\nDebug checks passed', colorama.Fore.GREEN))
return True
class PrintVersionAction(argparse.Action):
def __init__(self, option_strings: Sequence[str], dest: str, nargs: int = 0, **kwargs: Any) -> None:
super().__init__(option_strings, dest, nargs, **kwargs)
def __call__(self, parser: argparse.ArgumentParser, namespace: Args, values: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None:
print_version()
parser.exit()
def print_version() -> None:
print(get_full_version())
def get_full_version() -> str:
version = get_raw_version()
meta = [
platform.python_implementation()
]
if executable: meta.append('exe')
meta.append(f"called as {'package' if __package__ else 'script'}")
return (
f"extract_otp_secrets {version} {platform.system()} {platform.machine()}"
f" Python {platform.python_version()}"
f" ({'/'.join(meta)})"
)
# https://setuptools-git-versioning.readthedocs.io/en/stable/runtime_version.html
def get_raw_version() -> str:
global __version__
try:
__version__ = version("extract_otp_secrets")
return __version__
except PackageNotFoundError:
# package is not installed
pass
# In some cases importlib cannot properly detect package version, for example it was compiled into executable file, so it uses some custom import mechanism.
# Instead, use pkg_resources which is included in setuptools (but has a significant runtime cost)
try:
__version__ = get_distribution("package-name").version
return __version__
except DistributionNotFound:
# package is not installed
pass
return ''
# workaround for PYTHON <= 3.9 use: BaseException | None
def log_debug(*values: object, sep: Optional[str] = ' ') -> None:
if colored:
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()

View 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): # noqa: F821
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): # noqa: F821
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

View File

@@ -1,291 +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_json, remove_files, remove_dir_with_files, read_file_to_str, file_exits
from os import path
from pytest import raises
import extract_otp_secret_keys
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_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_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_stdout(capsys):
# Act
extract_otp_secret_keys.main(['example_export.txt'])
# Assert
captured = capsys.readouterr()
expected_stdout = '''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
'''
assert captured.out == expected_stdout
assert captured.err == ''
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()
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_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.out) > 0
assert 'The arguments --verbose and --quiet are mutually exclusive.' in captured.out
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/')

21
tests/conftest.py Normal file
View 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])

View File

View File

@@ -9,3 +9,7 @@ otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXB
# 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://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

BIN
tests/data/lena_std.tif Normal file

Binary file not shown.

View File

@@ -0,0 +1,3 @@
# comment 1
# comment 2

View 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

View File

@@ -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,24 +50,22 @@ 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
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
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

View 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

View 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

View 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

View File

@@ -0,0 +1,71 @@
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. Secret
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
2. Secret
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
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

# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
5. Secret
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
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

View 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

View 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

View File

@@ -132,3 +132,32 @@ Counter: 4
                                             
                                             
Name: encoding: ¿äÄéÉ? (demo)
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: totp
                                                 
                                                 
    █▀▀▀▀▀█ ▄▀▀  ▀  ▄▀█ ▄▄▄██▀ ▄▀█▄█▀ █▀▀▀▀▀█    
    █ ███ █ █▄▀█▄▄ ▀  ▄▀█  █  █  ▀▀▀▄ █ ███ █    
    █ ▀▀▀ █  ▄ ▀ ▀▄▀▄ ▄▄▀▄▄█▄ ▀▄ █▀▀█ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ █ █ █▄█▄▀ ▀▄▀ █▄█ ▀ █ ▀ █ ▀▀▀▀▀▀▀    
    ▀   ▄ ▀ █▀▀▄▀ ▄▄▀▀▄█▄ █▄▀▀▄█▀██▄▄█▀ ▄█▀▀▄    
    ▄▀█▄█ ▀ ▀ █▄█▄▄    ▄███▄▄▀▀▀▄▄▀▄    ▄█▀▄▄    
     ▀█▀ ▄▀▄▄█ ▄▀███▀ ▄▀█▀▄▀▄  ▀██▄▄ ▄█▀█  ▀▄    
     ▄▀ █▄▀▀ █▀▄▄▄  ▄█▄█ ▀▄ ▄▄ ▄  ▀▀█▄▄ ▀█▄▄▄    
    ▀ ▄▄▄▀▀▄▄█▀▄ ▀▀▀█▄ █▄ ▀ ▄█▄▄▀▄▀▀▀▄▄█▄ ▀ ▀    
    █ ▄   ▀█ ▄▀ ██ █ ▄▄▀▀▀███▄     ▄▄██ ██▀█▀    
    █▀█▀██▀▀███▄  ▀▀▄▄▄▄█▀ █ ▄█▄█▄▀ ▄▄▀  ▄▄ ▀    
    ▄██▄▄ ▀ ▀   ▀▀  ██▄▄▄▀▀▄█▀█▄ ▀ █▄▀▄   ▀▄▄    
      ███▄▀█▄█▄▄█ ▀█▄ ▀▄█  ▄▀▄ █▄  ▄ █▄  ▄▀▄▀    
    █  ▄ ▄▀▀▄▄█▄▄█▄█ ▄▄▄ █▄▄▀█ █▀█▄ ▄▀▀█▄▄▄▀▀    
    ▄  ▀▄▀▀ ▄▄▀██  ▀▀▄█▀▀▄ ▀▀█   ▄ ████ █▀█▀█    
    █▀▄▀█ ▀▄▄ ▄ ▀▀▀ ▄▀ ▀ █▀▄▀▀█▀▀█▄▀█ ▀▄▀▄ █     
    ▀ ▀  ▀▀ ▄ █▄▀█▀▀▄▀█ ▀▄▄█▄▀ ██ ▀██▀▀▀█▀▄▄     
    █▀▀▀▀▀█  ▀█ ▄▄██▀ ▀██▄▀██ ▄▄██ ▀█ ▀ █ ▀█     
    █ ███ █  █▄ █▀▀█▀▀▀█▀█ ▀ ▀█ █▀▀ ██▀▀▀███▀    
    █ ▀▀▀ █  ▄ ▄▀█▄▄ ▀█ ▀▀  ▄ ▀█▀ ▄▀ █▀▀██ ▀▄    
    ▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀▀   ▀  ▀ ▀▀▀▀ ▀  ▀   ▀ ▀      
                                                 
                                                 

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 B

View File

@@ -0,0 +1 @@
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.

View File

@@ -0,0 +1 @@
otpauth-migration://offline?data=XXXX

View File

@@ -0,0 +1 @@
QR-Code:otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 KiB

View File

@@ -0,0 +1 @@
This is just a text file masquerading as an image file.

View 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()

File diff suppressed because it is too large Load Diff

View 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,36 +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 __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',
@@ -72,15 +80,19 @@ class TestExtract(unittest.TestCase):
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
'Type: hotp',
'Counter: 4',
'',
'Name: encoding: ¿äÄéÉ? (demo)',
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
'Type: totp',
''
]
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
@@ -106,13 +118,17 @@ Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: hotp
Counter: 4
Name: encoding: ¿äÄéÉ? (demo)
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
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
@@ -138,65 +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):
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
View 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))])

View File

@@ -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 -U -r requirements.txt"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="$PIP install -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

View File

@@ -1,84 +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") as infile:
lines = []
reader = csv.reader(infile)
for line in reader:
lines.append(line)
return lines
def read_json(filename):
"""Returns a list or a dictionary."""
with open(filename, "r") as infile:
return json.load(infile)
def read_file_to_list(filename):
"""Returns a list of lines."""
with open(filename, "r") as infile:
return infile.readlines()
def read_file_to_str(filename):
"""Returns a str."""
return "".join(read_file_to_list(filename))