mirror of
https://github.com/scito/extract_otp_secret_keys.git
synced 2025-12-05 22:34:57 +01:00
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
This commit is contained in:
parent
445d77783c
commit
6a7a7233a4
23 changed files with 877 additions and 93 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
|
@ -7,6 +7,8 @@ name: tests
|
|||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
# pull_request:
|
||||
schedule:
|
||||
# Run daily on default branch
|
||||
|
|
@ -29,6 +31,7 @@ jobs:
|
|||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: false
|
||||
- name: Install zbar shared lib for QReader (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
|
|
|
|||
104
.github/workflows/ci_docker.yml
vendored
104
.github/workflows/ci_docker.yml
vendored
|
|
@ -11,13 +11,19 @@ name: docker
|
|||
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:
|
||||
# define job to build and publish docker image
|
||||
build-and-push-docker-image:
|
||||
build-and-push-docker-debian-image:
|
||||
name: Build Docker image and push to repositories
|
||||
# run only when code is compiling and tests are passing
|
||||
runs-on: ubuntu-latest
|
||||
|
|
@ -57,9 +63,74 @@ jobs:
|
|||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_IO_TOKEN }}
|
||||
|
||||
- name: "no_qr_reader: Build image and push to Docker Hub and GitHub Container Registry"
|
||||
- name: "qr_reader: 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 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@v2
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
# relative path to the place where source code with Dockerfile is located
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
|
@ -67,30 +138,19 @@ jobs:
|
|||
file: Dockerfile_only_txt
|
||||
# builder: ${{ steps.buildx.outputs.name }}
|
||||
# Note: tags has to be all lower-case
|
||||
pull: true
|
||||
tags: |
|
||||
scit0/extract_otp_secrets:latest-only-txt
|
||||
ghcr.io/scito/extract_otp_secrets:latest-only-txt
|
||||
scit0/extract_otp_secrets:only-txt
|
||||
scit0/extract_otp_secrets:alpine
|
||||
ghcr.io/scito/extract_otp_secrets:only-txt
|
||||
ghcr.io/scito/extract_otp_secrets:alpine
|
||||
# build on feature branches, push only on master branch
|
||||
push: ${{ github.ref == 'refs/heads/master' }}
|
||||
build-args: |
|
||||
RUN_TESTS=true
|
||||
|
||||
- name: "qr_reader: Build image and push to Docker Hub and GitHub Container Registry"
|
||||
id: docker_build_qr_reader
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
# relative path to the place where source code with Dockerfile is located
|
||||
context: .
|
||||
# builder: ${{ steps.buildx.outputs.name }}
|
||||
# Note: tags has to be all lower-case
|
||||
tags: |
|
||||
scit0/extract_otp_secrets:latest
|
||||
ghcr.io/scito/extract_otp_secrets:latest
|
||||
# build on feature branches, push only on master branch
|
||||
push: ${{ github.ref == 'refs/heads/master' }}
|
||||
|
||||
- name: Image digest
|
||||
# TODO upload digests to assets
|
||||
run: |
|
||||
echo "extract_otp_secrets: ${{ steps.docker_build_qr_reader.outputs.digest }}"
|
||||
echo "extract_otp_secrets_only_txt: ${{ steps.docker_build_only_txt.outputs.digest }}"
|
||||
echo "extract_otp_secrets:only-txt: ${{ steps.docker_build_only_txt.outputs.digest }}"
|
||||
|
|
|
|||
281
.github/workflows/ci_release.yml
vendored
Normal file
281
.github/workflows/ci_release.yml
vendored
Normal 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: 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
|
||||
- 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: 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
|
||||
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=$(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
|
||||
- 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 }})
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -19,3 +19,9 @@ dist/
|
|||
*.xml
|
||||
pytest-coverage.txt
|
||||
tests/reports/
|
||||
dist_*/
|
||||
*.spec
|
||||
|
||||
file_version_info_python.txt
|
||||
file_version_info_explorer.txt
|
||||
file_version_info.txt
|
||||
|
|
|
|||
15
Dockerfile
15
Dockerfile
|
|
@ -1,4 +1,6 @@
|
|||
FROM python:3.11-slim-bullseye
|
||||
# --build-arg BASE_IMAGE=python:3.11-slim-buster
|
||||
ARG BASE_IMAGE=python:3.11-slim-bullseye
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
# https://docs.docker.com/engine/reference/builder/
|
||||
|
||||
|
|
@ -10,7 +12,7 @@ FROM python:3.11-slim-bullseye
|
|||
|
||||
WORKDIR /extract
|
||||
|
||||
COPY . .
|
||||
COPY requirements*.txt src/ run_pytest.sh pytest.ini tests/ example_*.txt example_*.png example_*.csv example*.json docker/.alias ./
|
||||
|
||||
ARG RUN_TESTS=true
|
||||
|
||||
|
|
@ -20,13 +22,14 @@ RUN apt-get update && apt-get install -y \
|
|||
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
|
||||
&& pip install --no-cache-dir -U -r requirements.txt \
|
||||
&& if [ "$RUN_TESTS" = "true" ]; then /extract/run_pytest.sh; else echo "Not running tests..."; fi \
|
||||
&& echo 'test -s /extract/.alias && . /extract/.alias || true' >> ~/.bashrc
|
||||
|
||||
WORKDIR /files
|
||||
|
||||
ENTRYPOINT ["python", "/extract/src/extract_otp_secrets.py"]
|
||||
ENTRYPOINT ["python", "/extract/extract_otp_secrets.py"]
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/scito/extract_otp_secrets
|
||||
LABEL org.opencontainers.image.license GPL-3.0+
|
||||
LABEL maintainer="Scito https://scito.ch, https://github.com/scito"
|
||||
|
|
|
|||
|
|
@ -1,16 +1,19 @@
|
|||
FROM python:3.11-alpine
|
||||
ARG BASE_IMAGE=python:3.11-alpine
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
# https://docs.docker.com/engine/reference/builder/
|
||||
|
||||
# For debugging
|
||||
# docker run --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt
|
||||
# docker build . -t extract_otp_secrets_only_txt -f Dockerfile_only_txt --pull --build-arg RUN_TESTS=false
|
||||
# docker run --entrypoint /bin/sh -it --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt
|
||||
# docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt tests/extract_otp_secrets_test.py -k "not qreader" --relaxed -vvv -s
|
||||
# docker run --entrypoint /bin/ash -it --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt -l
|
||||
# docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt extract_otp_secrets_test.py -k "not qreader" --relaxed -vvv -s
|
||||
|
||||
# https://github.com/pypa/manylinux/blob/main/docker/Dockerfile
|
||||
|
||||
WORKDIR /extract
|
||||
|
||||
COPY . .
|
||||
COPY requirements*.txt src/ run_pytest.sh pytest.ini tests/ example_*.txt example_*.png example_*.csv example*.json docker/.alias ./
|
||||
|
||||
ARG RUN_TESTS=true
|
||||
|
||||
|
|
@ -32,11 +35,13 @@ RUN apk add --no-cache \
|
|||
protobuf \
|
||||
qrcode \
|
||||
&& if [[ "$(apk --print-arch)" == "aarch64" ]]; then apk del .build-deps; fi \
|
||||
&& if [[ "$RUN_TESTS" == "true" ]]; then /extract/run_pytest.sh tests/extract_otp_secrets_test.py -k "not qreader" --relaxed; else echo "Not running tests..."; fi
|
||||
&& if [[ "$RUN_TESTS" == "true" ]]; then /extract/run_pytest.sh extract_otp_secrets_test.py -k "not qreader" --relaxed; else echo "Not running tests..."; fi \
|
||||
&& echo 'test -s /extract/.alias && . /extract/.alias || true' >> ~/.profile
|
||||
|
||||
WORKDIR /files
|
||||
|
||||
ENTRYPOINT ["python", "/extract/src/extract_otp_secrets.py"]
|
||||
ENTRYPOINT ["python", "/extract/extract_otp_secrets.py"]
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/scito/extract_otp_secrets
|
||||
LABEL org.opencontainers.image.license GPL-3.0+
|
||||
LABEL maintainer="Scito https://scito.ch, https://github.com/scito"
|
||||
|
|
|
|||
2
Pipfile
2
Pipfile
|
|
@ -16,12 +16,14 @@ qreader = "<2.0.0"
|
|||
[dev-packages]
|
||||
build = "*"
|
||||
flake8 = "*"
|
||||
gfm-toc = "*"
|
||||
mypy = "*"
|
||||
mypy-protobuf = "*"
|
||||
pylint = "*"
|
||||
pytest = "*"
|
||||
pytest-cov = "*"
|
||||
pytest-mock = "*"
|
||||
setuptools-git-versioning = "*"
|
||||
types-protobuf = "*"
|
||||
wheel = "*"
|
||||
|
||||
|
|
|
|||
38
Pipfile.lock
generated
38
Pipfile.lock
generated
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"_meta": {
|
||||
"hash": {
|
||||
"sha256": "9b56c9708e464fbb035e36b29b0c89f0250bc50ac37234f3948a366136a8e6c9"
|
||||
"sha256": "41edd4aebe075d6c39d035ec7cb10f0253a3ad21f9b4aa5b9c57deccca87874f"
|
||||
},
|
||||
"pipfile-spec": 6,
|
||||
"requires": {
|
||||
|
|
@ -220,11 +220,11 @@
|
|||
"develop": {
|
||||
"astroid": {
|
||||
"hashes": [
|
||||
"sha256:3bc7834720e1a24ca797fd785d77efb14f7a28ee8e635ef040b6e2d80ccb3303",
|
||||
"sha256:8f6a8d40c4ad161d6fc419545ae4b2f275ed86d1c989c97825772120842ee0d2"
|
||||
"sha256:14c1603c41cc61aae731cad1884a073c4645e26f126d13ac8346113c95577f3b",
|
||||
"sha256:6afc22718a48a689ca24a97981ad377ba7fb78c133f40335dfd16772f29bcfb1"
|
||||
],
|
||||
"markers": "python_full_version >= '3.7.2'",
|
||||
"version": "==2.13.2"
|
||||
"version": "==2.13.3"
|
||||
},
|
||||
"attrs": {
|
||||
"hashes": [
|
||||
|
|
@ -318,6 +318,14 @@
|
|||
"index": "pypi",
|
||||
"version": "==6.0.0"
|
||||
},
|
||||
"gfm-toc": {
|
||||
"hashes": [
|
||||
"sha256:247af7267a6cbbdd4213f8383157997bcb07e39e819db737bd2dbfbdb94ee7ae",
|
||||
"sha256:c53ed0e2cd400e89051377017ca98c11c9cef628b2effddf787db4fc19ff343d"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==0.0.7"
|
||||
},
|
||||
"iniconfig": {
|
||||
"hashes": [
|
||||
"sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3",
|
||||
|
|
@ -535,6 +543,22 @@
|
|||
"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": [
|
||||
"sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b",
|
||||
|
|
@ -545,11 +569,11 @@
|
|||
},
|
||||
"types-protobuf": {
|
||||
"hashes": [
|
||||
"sha256:7df483d34ad3fcb1fa7fff1073560d596c9ac1f419cfa851b220c9a93386c998",
|
||||
"sha256:aeefcf39d637016998b3c7b699750847071b555f7c2e0c9873d42ab6103d1a39"
|
||||
"sha256:6c87c7f8df61d57a53de8221777e4fcc3c7ed24419fbf43b8e9f50887f3773fa",
|
||||
"sha256:824109e0fe87525a9d2da4cc4eec36ca004f1a0f3d1c0838cfd2873a484cffdd"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.21.0.2"
|
||||
"version": "==4.21.0.3"
|
||||
},
|
||||
"typing-extensions": {
|
||||
"hashes": [
|
||||
|
|
|
|||
160
README.md
160
README.md
|
|
@ -1,13 +1,20 @@
|
|||
# Extract TOTP/HOTP secrets from QR codes exported by two-factor authentication apps
|
||||
|
||||
[](https://github.com/scito/extract_otp_secrets/actions/workflows/ci.yml)
|
||||

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

|
||||

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

|
||||
-->
|
||||

|
||||
[](https://github.com/scito/extract_otp_secrets/blob/master/LICENSE)
|
||||
[](https://github.com/scito/extract_otp_secrets/tags)
|
||||
[](https://github.com/scito/extract_otp_secrets/releases/latest)
|
||||
[](https://github.com/scito/extract_otp_secrets/releases/latest)
|
||||
[](https://github.com/scito/extract_otp_secrets/releases/latest)
|
||||
[](https://github.com/scito/extract_otp_secrets/releases/latest)
|
||||
[](https://github.com/scito/extract_otp_secrets/releases/latest)
|
||||
[](https://hub.docker.com/repository/docker/scit0/extract_otp_secrets/general)
|
||||
<!-- [](https://GitHub.com/scito/extract_otp_secrets/releases/) -->
|
||||
[](https://stand-with-ukraine.pp.ua)
|
||||
|
||||
---
|
||||
|
|
@ -23,6 +30,62 @@ The secrets can be exported to JSON or CSV, or printed as QR codes to console or
|
|||
|
||||
⚡ **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)
|
||||
|
|
@ -30,11 +93,16 @@ The secrets can be exported to JSON or CSV, or printed as QR codes to console or
|
|||
1. Open "Google Authenticator" app on the mobile phone
|
||||
2. Export the QR codes from "Google Authenticator" app (see [how to export](#how-to-export-otp-secrets-from-google-authenticator-app))
|
||||
3. Point the exported QR codes to the camera of your computer
|
||||
4. Call this script without infile parameters:
|
||||
|
||||
4. Run the program without infile parameters:
|
||||
```
|
||||
extract_otp_secrets
|
||||
```
|
||||
or
|
||||
```
|
||||
python src/extract_otp_secrets.py
|
||||
```
|
||||
|
||||

|
||||

|
||||
|
||||
Detected QR codes are surrounded with a frame. The color of the frame indicates the extracting result:
|
||||
|
||||
|
|
@ -48,13 +116,13 @@ The secrets are printed by default to the console. [Set program parameters](#pro
|
|||
|
||||
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))
|
||||
4. Save the QR code as image file, e.g. example_export.png
|
||||
5. Transfer the images files to the computer where his script is installed.
|
||||
6. Call this script with the file as input:
|
||||
|
||||
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
|
||||
|
||||
7. Remove unencrypted files with secrets from your computer and mobile.
|
||||
```
|
||||
6. Remove unencrypted files with secrets from your computer and mobile.
|
||||
|
||||
### With external QR decoder app from text files
|
||||
|
||||
|
|
@ -64,14 +132,25 @@ The secrets are printed by default to the console. [Set program parameters](#pro
|
|||
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.
|
||||
|
||||
## 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
|
||||
|
|
@ -120,7 +199,7 @@ OpenCV requires [Visual C++ redistributable 2015](https://www.microsoft.com/en-u
|
|||
|
||||
## Program help: arguments and options
|
||||
|
||||
<pre>usage: extract_otp_secrets.py [-h] [--csv FILE] [--keepass FILE] [--json FILE] [--printqr] [--saveqr DIR] [--camera NUMBER] [--qr {ZBAR,QREADER,QREADER_DEEP,CV2,CV2_WECHAT}] [-i] [--no-color] [-d | -v | -q] [infile ...]
|
||||
<pre>usage: extract_otp_secrets.py [-h] [--csv FILE] [--keepass FILE] [--json FILE] [--printqr] [--saveqr DIR] [--camera NUMBER] [--qr {ZBAR,QREADER,QREADER_DEEP,CV2,CV2_WECHAT}] [-i] [--no-color] [--version] [-d | -v | -q] [infile ...]
|
||||
|
||||
Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps
|
||||
If no infiles are provided, a GUI window starts and QR codes are captured from the camera.
|
||||
|
|
@ -141,6 +220,7 @@ options:
|
|||
QR reader (default: ZBAR)
|
||||
-i, --ignore ignore duplicate otps
|
||||
--no-color, -n do not use ANSI colors in console output
|
||||
--version, -V print version and quit
|
||||
-d, --debug enter debug mode, do checks and quit
|
||||
-v, --verbose verbose output
|
||||
-q, --quiet no stdout output, except output set by -
|
||||
|
|
@ -226,6 +306,11 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
|||
* Portable image format - *.pbm, *.pgm, *.ppm *.pxm, *.pnm
|
||||
* Prints errors and warnings to stderr (🆕 since v2.0)
|
||||
* Prints colored output (🆕 since v2.0)
|
||||
* Startable as executable (script, Python, and all dependencies packed in one executable) (🆕 since v2.1)
|
||||
* extract_otp_secrets_linux_x86_64 (requires glibc >= 2.28)
|
||||
* extract_otp_secrets_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
|
||||
|
|
@ -234,7 +319,6 @@ python extract_otp_secrets.py = < example_export.png</pre>
|
|||
* Docker
|
||||
* VSCode devcontainer
|
||||
* devbox
|
||||
* Prebuilt Docker images provided for amd64 and arm64 (🆕 since v2.0)
|
||||
* Compatible with major platforms:
|
||||
* Linux
|
||||
* macOS
|
||||
|
|
@ -308,10 +392,16 @@ KeePass can be used as a backup for one time passwords (second factor) from the
|
|||
|
||||
## Alternative installation methods
|
||||
|
||||
### pip
|
||||
### pip using github
|
||||
|
||||
```
|
||||
pip install -U git+https://github.com/scito/extract_otp_secrets
|
||||
extract_otp_secrets
|
||||
```
|
||||
|
||||
or run it
|
||||
|
||||
```
|
||||
python -m extract_otp_secrets
|
||||
```
|
||||
|
||||
|
|
@ -319,7 +409,7 @@ or from a specific tag
|
|||
|
||||
```
|
||||
pip install -U git+https://github.com/scito/extract_otp_secrets.git@v2.0.0
|
||||
python -m extract_otp_secrets
|
||||
extract_otp_secrets
|
||||
curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/example_export.txt | python -m extract_otp_secrets -
|
||||
```
|
||||
|
||||
|
|
@ -328,6 +418,12 @@ curl -s https://raw.githubusercontent.com/scito/extract_otp_secrets/master/examp
|
|||
```
|
||||
git clone https://github.com/scito/extract_otp_secrets.git
|
||||
pip install -U -e extract_otp_secrets
|
||||
extract_otp_secrets extract_otp_secrets/example_export.txt
|
||||
```
|
||||
|
||||
or run it
|
||||
|
||||
```
|
||||
python -m extract_otp_secrets extract_otp_secrets/example_export.txt
|
||||
```
|
||||
|
||||
|
|
@ -500,16 +596,34 @@ docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extrac
|
|||
#### Alpine (only text file processing)
|
||||
|
||||
```bash
|
||||
docker build . -t extract_otp_secrets_only_txt --pull -f Dockerfile_only_txt --build-arg RUN_TESTS=false
|
||||
docker build . -t extract_otp_secrets:only_txt --pull -f Dockerfile_only_txt --build-arg RUN_TESTS=false
|
||||
```
|
||||
|
||||
Run tests in docker container:
|
||||
|
||||
```bash
|
||||
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt tests/extract_otp_secrets_test.py -k "not qreader" --relaxed
|
||||
docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt extract_otp_secrets_test.py -k "not qreader" --relaxed
|
||||
```
|
||||
|
||||
### Full local build
|
||||
### Create executables with pyinstaller
|
||||
|
||||
#### Linux
|
||||
|
||||
```bash
|
||||
pyinstaller -y --add-data $pythonLocation/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile src/extract_otp_secrets.py
|
||||
```
|
||||
|
||||
Output is executable `dist/extract_otp_secrets`
|
||||
|
||||
#### Windows
|
||||
|
||||
```
|
||||
pyinstaller -y --add-data "%pythonLocation%\__yolo_v3_qr_detector;__yolo_v3_qr_detector" --add-binary "%pythonLocation%\pyzbar\libiconv.dll;pyzbar" --add-binary "%pythonLocation%\pyzbar\libzbar-64.dll;pyzbar" --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.
|
||||
|
||||
|
|
|
|||
95
build.sh
95
build.sh
|
|
@ -76,6 +76,7 @@ askContinueYn() {
|
|||
interactive=false
|
||||
ignore_version_check=true
|
||||
clean=false
|
||||
clean_flag=""
|
||||
build_docker=true
|
||||
run_gui=true
|
||||
generate_result_files=false
|
||||
|
|
@ -119,6 +120,7 @@ while test $# -gt 0; do
|
|||
;;
|
||||
-c)
|
||||
clean=true
|
||||
clean_flag="--clean"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
|
|
@ -160,7 +162,7 @@ if $clean; then
|
|||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="rm -r dist/ build/ *.whl pytest.xml pytest-coverage.txt .coverage tests/reports || true; find . -name '*.pyc' -type f -delete; find . -name '__pycache__' -type d -exec rm -r {} \; || true; find . -name '*.egg-info' -type d -exec rm -r {} \; || true; find . -name '*_cache' -type d -exec rm -r {} \; || true; mkdir -p tests/reports;"
|
||||
cmd="sudo rm -rf dist/ build/ dist_*/ *.whl extracted_*.csv extracted_*.json pytest.xml pytest-coverage.txt .coverage tests/reports || true; find . -name '*.pyc' -type f -delete; find . -name '__pycache__' -type d -exec rm -r {} \; || true; find . -name '*.egg-info' -type d -exec rm -r {} \; || true; find . -name '*_cache' -type d -exec rm -r {} \; || true; mkdir -p tests/reports;"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
|
|
@ -277,6 +279,15 @@ cmd="$MYPY --strict src/*.py tests/*.py | tee $TYPE_CHECK_OUT_FILE"
|
|||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
|
||||
# Generate results files
|
||||
|
||||
if $generate_result_files; then
|
||||
cmd="for color in '' '-n'; do for level in '' '-v' '-vv' '-vvv'; do $PYTHON src/extract_otp_secrets.py example_export.txt \$color \$level > tests/data/print_verbose_output\$color\$level.txt; done; done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# pip -e install
|
||||
|
||||
cmd="$PIP install -U -e ."
|
||||
|
|
@ -330,13 +341,26 @@ cmd="$PIP wheel ."
|
|||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Generate results files
|
||||
# Build executable
|
||||
|
||||
if $generate_result_files; then
|
||||
cmd="for color in '' '-n'; do for level in '' '-v' '-vv' '-vvv'; do $PYTHON src/extract_otp_secrets.py example_export.txt $color $level > tests/data/print_verbose_output$color$level.txt; done; done"
|
||||
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"
|
||||
fi
|
||||
|
||||
# Update Code Coverage in README.md
|
||||
|
||||
|
|
@ -349,7 +373,7 @@ if $build_docker; then
|
|||
# Build docker
|
||||
|
||||
# Build Dockerfile_only_txt (Alpine)
|
||||
cmd="docker build . -t extract_otp_secrets_only_txt -f Dockerfile_only_txt --pull --build-arg RUN_TESTS=false"
|
||||
cmd="docker build . -t extract_otp_secrets_only_txt -t extract_otp_secrets:only-txt -t extract_otp_secrets:alpine -f Dockerfile_only_txt --pull --build-arg RUN_TESTS=false"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
|
|
@ -361,13 +385,12 @@ if $build_docker; then
|
|||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secrets_only_txt tests/extract_otp_secrets_test.py -k 'not qreader' -vvv --relaxed"
|
||||
cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secrets_only_txt extract_otp_secrets_test.py -k 'not qreader' -vvv --relaxed"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
|
||||
# Build extract_otp_secrets (Debian)
|
||||
cmd="docker build . -t extract_otp_secrets --pull --build-arg RUN_TESTS=false"
|
||||
# Build extract_otp_secrets (Debian Bullseye)
|
||||
cmd="docker build . -t extract_otp_secrets -t extract_otp_secrets:bullseye --pull --build-arg RUN_TESTS=false"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
|
|
@ -387,6 +410,58 @@ if $build_docker; then
|
|||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build extract_otp_secrets (Debian Buster)
|
||||
cmd="docker build . -t extract_otp_secrets:buster --pull --build-arg RUN_TESTS=false --build-arg BASE_IMAGE=python:3.11-slim-buster"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -v \"$(pwd)\":/files:ro extract_otp_secrets:buster example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="cat example_export.txt | docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets:buster - -c - > example_output.csv"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets:buster = < example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secrets:buster"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build executable from Docker latest
|
||||
# sed "1!d" is workaround for head -n 1 since it head procduces exit code != 0
|
||||
BULLSEYE_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
|
||||
echo "Bullseye glibc: $BULLSEYE_GLIBC_VERSION"
|
||||
|
||||
cmd="docker run --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets -c 'apt-get update && apt-get -y install binutils && pip install -U -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
|
||||
|
|
|
|||
4
docker/.alias
Normal file
4
docker/.alias
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
alias ll='ls -lh'
|
||||
alias la='ls -lha'
|
||||
alias l='ls -alhF'
|
||||
alias ls-l='ls -lh'
|
||||
56
docs/README_TOC.md
Normal file
56
docs/README_TOC.md
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
Generate from file: README.md
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [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.
|
||||
|
Before Width: | Height: | Size: 528 KiB After Width: | Height: | Size: 528 KiB |
46
file_version_info_template.txt
Normal file
46
file_version_info_template.txt
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# UTF-8
|
||||
#
|
||||
# For more details about fixed file info 'ffi' see:
|
||||
# http://msdn.microsoft.com/en-us/library/ms646997.aspx
|
||||
VSVersionInfo(
|
||||
ffi=FixedFileInfo(
|
||||
# The elements of each tuple represent 16-bit values from most-significant to least-significant. For example the value (2, 0, 4, 0) resolves to 0002000000040000 in hex.
|
||||
# filevers and prodvers should be always a tuple with four items: (1, 2, 3, 4)
|
||||
# Set not needed items to zero 0.
|
||||
filevers=($VERSION_MAJOR, $VERSION_MINOR, $VERSION_PATCH, $VERSION_BUILD),
|
||||
prodvers=($VERSION_MAJOR, $VERSION_MINOR, $VERSION_PATCH, $VERSION_BUILD),
|
||||
# Contains a bitmask that specifies the valid bits 'flags'r
|
||||
mask=0x3f,
|
||||
# Contains a bitmask that specifies the Boolean attributes of the file.
|
||||
flags=0x0,
|
||||
# The operating system for which this file was designed.
|
||||
# 0x4 - NT and there is no need to change it.
|
||||
OS=0x4,
|
||||
# The general type of file.
|
||||
# 0x1 - the file is an application.
|
||||
fileType=0x1,
|
||||
# The function of the file.
|
||||
# 0x0 - the function is not defined for this fileType
|
||||
subtype=0x0,
|
||||
# Creation date and time stamp.
|
||||
date=(0, 0)
|
||||
),
|
||||
kids=[
|
||||
StringFileInfo(
|
||||
[
|
||||
StringTable(
|
||||
# 0x0409 (U.S. English) + 04B0 (1200 = Unicode), https://learn.microsoft.com/en-us/windows/win32/menurc/stringfileinfo-block
|
||||
'040904B0',
|
||||
[StringStruct('CompanyName', 'scito'),
|
||||
StringStruct('FileDescription', 'extract_otp_secrets'),
|
||||
StringStruct('FileVersion', '$VERSION_STR'),
|
||||
StringStruct('InternalName', 'extract_otp_secrets'),
|
||||
StringStruct('LegalCopyright', 'Copyright © $YEARS Scito.'),
|
||||
StringStruct('OriginalFilename', 'extract_otp_secrets.exe'),
|
||||
StringStruct('ProductName', 'extract_otp_secrets'),
|
||||
StringStruct('ProductVersion', '$VERSION_STR')])
|
||||
]),
|
||||
# 1033 (0x0409 = U.S. English), 1200 (Unicode)
|
||||
VarFileInfo([VarStruct('Translation', [0, 1200])])
|
||||
]
|
||||
)
|
||||
|
|
@ -27,6 +27,10 @@ classifiers = [
|
|||
"Intended Audience :: Developers",
|
||||
"Intended Audience :: System Administrators",
|
||||
"Programming Language :: Python",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: Microsoft :: Windows :: Windows 10",
|
||||
"Operating System :: Microsoft :: Windows :: Windows 11",
|
||||
"Operating System :: MacOS",
|
||||
"Natural Language :: English",
|
||||
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||
"Typing :: Typed",
|
||||
|
|
@ -40,7 +44,9 @@ dependencies = [
|
|||
"pyzbar",
|
||||
"qrcode",
|
||||
"qreader<2.0.0",
|
||||
# workaround for PYTHON <= 3.7: compatibility
|
||||
"typing_extensions; python_version<='3.7'",
|
||||
"importlib_metadata; python_version<='3.7'",
|
||||
]
|
||||
description = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as 'Google Authenticator'"
|
||||
dynamic = ["version"]
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
build
|
||||
flake8
|
||||
gfm-toc
|
||||
mypy
|
||||
mypy-protobuf
|
||||
pyinstaller
|
||||
pylint
|
||||
pytest
|
||||
pytest-cov
|
||||
pytest-mock
|
||||
setuptools
|
||||
setuptools-git-versioning
|
||||
types-protobuf
|
||||
wheel
|
||||
|
|
|
|||
|
|
@ -7,3 +7,4 @@ pyzbar
|
|||
qrcode
|
||||
qreader<2.0.0
|
||||
typing_extensions; python_version<='3.7'
|
||||
importlib_metadata; python_version<='3.7'
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
#!/bin/sh
|
||||
cd /extract
|
||||
pip install -U pytest pytest-mock && pip install --no-deps . && pytest "$@"
|
||||
mkdir -p tests
|
||||
ln -sf /extract/data tests/data
|
||||
pip install -U pytest pytest-mock && pytest "$@"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,10 @@ python_requires = >=3.7, <4
|
|||
py_modules = extract_otp_secrets, protobuf_generated_python.google_auth_pb2
|
||||
package_dir =
|
||||
=src
|
||||
platforms =
|
||||
Linux
|
||||
Windows
|
||||
MacOS
|
||||
# packages=find:
|
||||
|
||||
# [options.packages.find]
|
||||
|
|
|
|||
|
|
@ -38,11 +38,18 @@ import csv
|
|||
import fileinput
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse as urlparse
|
||||
from enum import Enum, IntEnum
|
||||
from typing import Any, List, Optional, TextIO, Tuple, Union
|
||||
from typing import Any, List, Optional, Sequence, TextIO, Tuple, Union
|
||||
|
||||
import colorama
|
||||
from pkg_resources import DistributionNotFound, get_distribution
|
||||
from qrcode import QRCode # type: ignore
|
||||
|
||||
import protobuf_generated_python.google_auth_pb2 as pb
|
||||
|
||||
# workaround for PYTHON <= 3.7: compatibility
|
||||
if sys.version_info >= (3, 8):
|
||||
|
|
@ -50,16 +57,17 @@ if sys.version_info >= (3, 8):
|
|||
else:
|
||||
from typing_extensions import Final, TypedDict
|
||||
|
||||
from qrcode import QRCode # type: ignore
|
||||
# workaround for PYTHON <= 3.7: compatibility
|
||||
if sys.version_info >= (3, 8):
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
else:
|
||||
from importlib_metadata import PackageNotFoundError, version
|
||||
|
||||
import protobuf_generated_python.google_auth_pb2 as pb
|
||||
import colorama
|
||||
|
||||
debug_mode = '-d' in sys.argv[1:] or '--debug' in sys.argv[1:]
|
||||
|
||||
try:
|
||||
import cv2 # type: ignore # TODO use cv2 types if available
|
||||
|
||||
import numpy as np # TODO use numpy types if available
|
||||
|
||||
try:
|
||||
|
|
@ -133,6 +141,8 @@ CAMERA: Final[str] = 'camera'
|
|||
verbose: IntEnum = LogLevel.NORMAL
|
||||
quiet: bool = False
|
||||
colored: bool = True
|
||||
executable: bool = False
|
||||
__version__: str
|
||||
|
||||
|
||||
def sys_main() -> None:
|
||||
|
|
@ -140,6 +150,7 @@ def sys_main() -> None:
|
|||
|
||||
|
||||
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
|
||||
|
|
@ -150,11 +161,15 @@ def main(sys_args: list[str]) -> None:
|
|||
# StringIO in tests do not have all attributes, ignore it
|
||||
pass
|
||||
|
||||
# https://pyinstaller.org/en/stable/runtime-information.html#run-time-information
|
||||
executable = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
|
||||
|
||||
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)
|
||||
|
||||
|
|
@ -237,15 +252,19 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count:
|
|||
|
||||
def parse_args(sys_args: list[str]) -> Args:
|
||||
global verbose, quiet, colored
|
||||
|
||||
# For PYTHON <= 3.7: Use :=
|
||||
name = os.path.basename(sys.argv[0])
|
||||
cmd = f"python {name}" if name.endswith('.py') else f"{name}"
|
||||
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
|
||||
if qreader_available:
|
||||
description_text += "\nIf no infiles are provided, a GUI window starts and QR codes are captured from the camera."
|
||||
example_text = """examples:
|
||||
python extract_otp_secrets.py
|
||||
python extract_otp_secrets.py example_*.txt
|
||||
python extract_otp_secrets.py - < example_export.txt
|
||||
python extract_otp_secrets.py --csv - example_*.png | tail -n+2
|
||||
python extract_otp_secrets.py = < example_export.png"""
|
||||
example_text = f"""examples:
|
||||
{cmd}
|
||||
{cmd} example_*.txt
|
||||
{cmd} - < example_export.txt
|
||||
{cmd} --csv - example_*.png | tail -n+2
|
||||
{cmd} = < example_export.png"""
|
||||
|
||||
arg_parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=32),
|
||||
description=description_text,
|
||||
|
|
@ -262,6 +281,7 @@ b) image file containing a QR code or = for stdin for an image containing a QR c
|
|||
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')
|
||||
|
|
@ -731,6 +751,57 @@ def do_debug_checks() -> bool:
|
|||
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:
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ QReader installed: True
|
|||
CV2 version: 4.7.0
|
||||
QR reading mode: ZBAR
|
||||
|
||||
Version: extract_otp_secrets 2.0.2.post50+git.158245dd.dirty Linux x86_64 Python 3.11.1 (CPython/called as script)
|
||||
|
||||
Input files: ['example_export.txt']
|
||||
Processing infile example_export.txt
|
||||
Reading lines of example_export.txt
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ QReader installed: True
|
|||
CV2 version: 4.7.0
|
||||
QR reading mode: ZBAR
|
||||
|
||||
Version: extract_otp_secrets 2.0.2.post50+git.158245dd.dirty Linux x86_64 Python 3.11.1 (CPython/called as script)
|
||||
|
||||
Input files: ['example_export.txt']
|
||||
[36mProcessing infile example_export.txt[39m
|
||||
Reading lines of example_export.txt
|
||||
|
|
|
|||
|
|
@ -509,7 +509,7 @@ def test_extract_verbose(verbose_level: str, color: str, capsys: pytest.CaptureF
|
|||
|
||||
|
||||
def normalize_verbose_text(text: str, relaxed: bool) -> str:
|
||||
normalized = re.sub('^.+ version: .+$', '', text, flags=re.MULTILINE | re.IGNORECASE)
|
||||
normalized = re.sub('^.*version: .+$', '', text, flags=re.MULTILINE | re.IGNORECASE)
|
||||
if not qreader_available:
|
||||
normalized = normalized \
|
||||
.replace('QReader installed: True', 'QReader installed: False') \
|
||||
|
|
@ -549,6 +549,20 @@ def test_extract_help(capsys: pytest.CaptureFixture[str]) -> None:
|
|||
assert e.value.code == 0
|
||||
|
||||
|
||||
def test_extract_version(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
with pytest.raises(SystemExit) as e:
|
||||
# Act
|
||||
extract_otp_secrets.main(['--version'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert captured.out.startswith('extract_otp_secrets ')
|
||||
assert captured.err == ''
|
||||
assert e.type == SystemExit
|
||||
assert e.value.code == 0
|
||||
|
||||
|
||||
def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
|
||||
if qreader_available:
|
||||
# Arrange
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue