Compare commits
No commits in common. 'master' and 'v1.5.4' have entirely different histories.
@ -1,76 +1,32 @@
|
||||
name: tests
|
||||
|
||||
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
# https://docs.github.com/en/actions/using-workflows
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
# https://docs.github.com/en/actions/learn-github-actions/expressions
|
||||
|
||||
on:
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
# pull_request:
|
||||
schedule:
|
||||
# Run daily on default branch
|
||||
- cron: '37 3 * * *'
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12", "3.11", "3.10", "3.9", "3.8"]
|
||||
platform: [ubuntu-latest, macos-latest, windows-latest]
|
||||
# exclude:
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.x"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v5
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
check-latest: ${{ github.event_name == 'schedule' }}
|
||||
- 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 -U -r requirements-dev.txt
|
||||
pip install -U .
|
||||
pip install flake8 pytest
|
||||
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
|
||||
- name: Lint with flake8
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics
|
||||
if: matrix.python-version != '3.7'
|
||||
- name: Type checking with mypy
|
||||
run: |
|
||||
mypy --install-types --non-interactive src/*.py tests/*.py
|
||||
mypy --strict src/*.py tests/*.py
|
||||
if: matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||
- name: Test with pytest
|
||||
run: pytest
|
||||
if: (matrix.python-version != '3.x' || matrix.platform != 'ubuntu-latest') && (matrix.python-version != '3.10' && matrix.platform != 'macos-latest')
|
||||
- name: Test with pytest (with code coverage)
|
||||
run: pytest --cov=extract_otp_secrets_test --junitxml=pytest.xml --cov-report=term-missing | tee pytest-coverage.txt
|
||||
if: matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||
# https://github.com/marketplace/actions/pytest-coverage-comment
|
||||
- name: Pytest coverage comment
|
||||
uses: MishaKav/pytest-coverage-comment@main
|
||||
with:
|
||||
pytest-coverage-path: ./pytest-coverage.txt
|
||||
junitxml-path: ./pytest.xml
|
||||
if: |
|
||||
false && matrix.python-version == '3.x' && matrix.platform == 'ubuntu-latest'
|
||||
&& !contains(github.ref, 'refs/tags/')
|
||||
|
||||
run: |
|
||||
pytest
|
||||
|
@ -1,259 +0,0 @@
|
||||
name: docker
|
||||
|
||||
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
# https://docs.github.com/en/actions/using-workflows
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
# https://docs.github.com/en/actions/learn-github-actions/expressions
|
||||
|
||||
# How to setup: https://event-driven.io/en/how_to_buid_and_push_docker_image_with_github_actions/
|
||||
# How to run: https://aschmelyun.com/blog/using-docker-run-inside-of-github-actions/
|
||||
|
||||
on:
|
||||
# run it on push to the default repository branch
|
||||
push:
|
||||
paths-ignore:
|
||||
- 'docs/**'
|
||||
- '**.md'
|
||||
tags-ignore:
|
||||
- '**'
|
||||
# branches is needed if tags-ignore is used
|
||||
branches:
|
||||
- '**'
|
||||
schedule:
|
||||
# Run weekly on default branch
|
||||
- cron: '47 3 * * 6'
|
||||
|
||||
jobs:
|
||||
build-and-push-docker-debian-image:
|
||||
name: Build Docker Bookworm image and push to repositories
|
||||
# run only when code is compiling and tests are passing
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# steps to perform in job
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 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@v3
|
||||
|
||||
# setup Docker build action
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# Workaround for failing builds: https://github.com/docker/build-push-action/issues/761#issuecomment-1383822381
|
||||
# TODO remove workaround when fixed
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_IO_TOKEN }}
|
||||
|
||||
- name: "Build image and push to Docker Hub and GitHub Container Registry"
|
||||
id: docker_build_qr_reader_latest
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
# relative path to the place where source code with Dockerfile is located
|
||||
# TODO file:, move to docker/
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
# builder: ${{ steps.buildx.outputs.name }}
|
||||
# Note: tags has to be all lower-case
|
||||
build-args: |
|
||||
BASE_IMAGE=python:3.12-slim-bookworm
|
||||
pull: true
|
||||
tags: |
|
||||
scit0/extract_otp_secrets:latest
|
||||
scit0/extract_otp_secrets:bookworm
|
||||
ghcr.io/scito/extract_otp_secrets:latest
|
||||
ghcr.io/scito/extract_otp_secrets:bookworm
|
||||
# build on feature branches, push only on master branch
|
||||
push: ${{ github.ref == 'refs/heads/master' && github.secret_source == 'Actions'}}
|
||||
|
||||
- name: Image digest
|
||||
# TODO upload digests to assets
|
||||
run: |
|
||||
echo "extract_otp_secrets digests: ${{ steps.docker_build_qr_reader_latest.outputs.digest }}"
|
||||
echo "${{ steps.docker_build_qr_reader_latest.outputs.digest }}" > digests.txt
|
||||
- name: Save docker digests as artifacts
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debian_digests
|
||||
path: digests.txt
|
||||
|
||||
build-and-push-docker-alpine-image:
|
||||
name: Build Docker Alpine image and push to repositories
|
||||
# run only when code is compiling and tests are passing
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# steps to perform in job
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 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@v3
|
||||
|
||||
# setup Docker build action
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_IO_TOKEN }}
|
||||
|
||||
- name: "only_txt: Build image and push to Docker Hub and GitHub Container Registry"
|
||||
id: docker_build_only_txt
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
# relative path to the place where source code with Dockerfile is located
|
||||
platforms: linux/amd64,linux/arm64
|
||||
context: .
|
||||
file: docker/Dockerfile_only_txt
|
||||
# builder: ${{ steps.buildx.outputs.name }}
|
||||
# Note: tags has to be all lower-case
|
||||
pull: true
|
||||
tags: |
|
||||
scit0/extract_otp_secrets:only-txt
|
||||
scit0/extract_otp_secrets:alpine
|
||||
ghcr.io/scito/extract_otp_secrets:only-txt
|
||||
ghcr.io/scito/extract_otp_secrets:alpine
|
||||
# build on feature branches, push only on master branch
|
||||
push: ${{ github.ref == 'refs/heads/master' && github.secret_source == 'Actions'}}
|
||||
build-args: |
|
||||
RUN_TESTS=true
|
||||
|
||||
- name: Image digest
|
||||
# TODO upload digests to assets
|
||||
run: |
|
||||
echo "extract_otp_secrets:only-txt digests: ${{ steps.docker_build_only_txt.outputs.digest }}"
|
||||
echo "${{ steps.docker_build_qr_reader_latest.outputs.digest }}" > digests.txt
|
||||
|
||||
- name: Save docker digests as artifacts
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: alpine_digests
|
||||
path: digests.txt
|
||||
|
||||
build-and-push-docker-bullseye-image:
|
||||
name: Build Docker Bullseye image (for PyInstsaller) and push to repositories
|
||||
# run only when code is compiling and tests are passing
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# steps to perform in job
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 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@v3
|
||||
|
||||
# setup Docker build action
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# Workaround for failing builds: https://github.com/docker/build-push-action/issues/761#issuecomment-1383822381
|
||||
# TODO remove workaround when fixed
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_IO_TOKEN }}
|
||||
|
||||
- name: "Build image from Bullseye and push to GitHub Container Registry"
|
||||
id: docker_build_bullseye
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
platforms: linux/amd64,linux/arm64
|
||||
# relative path to the place where source code with Dockerfile is located
|
||||
context: .
|
||||
file: docker/Dockerfile
|
||||
# builder: ${{ steps.buildx.outputs.name }}
|
||||
build-args: |
|
||||
BASE_IMAGE=python:3.12-slim-bullseye
|
||||
# Note: tags has to be all lower-case
|
||||
pull: true
|
||||
tags: |
|
||||
scit0/extract_otp_secrets:bullseye
|
||||
push: ${{ github.secret_source == 'Actions' }}
|
||||
|
||||
- name: Image digest
|
||||
# TODO upload digests to assets
|
||||
run: |
|
||||
echo "extract_otp_secrets digests: ${{ steps.docker_build_qr_reader_latest.outputs.digest }}"
|
||||
echo "${{ steps.docker_build_qr_reader_latest.outputs.digest }}" > digests.txt
|
||||
- name: Save docker digests as artifacts
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bullseye_digests
|
||||
path: digests.txt
|
@ -1,410 +0,0 @@
|
||||
name: release
|
||||
|
||||
# https://data-dive.com/multi-os-deployment-in-cloud-using-pyinstaller-and-github-actions
|
||||
# https://github.com/actions/upload-artifact
|
||||
# https://github.com/actions/download-artifact
|
||||
# https://github.com/docker/metadata-action
|
||||
# https://github.com/marketplace/actions/generate-release-hashes
|
||||
|
||||
# https://github.com/oleksis/pyinstaller-manylinux
|
||||
# https://github.com/pypa/manylinux
|
||||
# https://github.com/batonogov/docker-pyinstaller
|
||||
|
||||
# https://docs.github.com/de/actions/using-workflows/workflow-syntax-for-github-actions
|
||||
# https://docs.github.com/en/actions/using-workflows
|
||||
# https://docs.github.com/en/actions/learn-github-actions/contexts
|
||||
# https://docs.github.com/en/actions/learn-github-actions/expressions
|
||||
|
||||
# https://docs.github.com/en/rest/releases/releases
|
||||
|
||||
# https://peps.python.org/pep-0440/
|
||||
# https://semver.org/
|
||||
|
||||
# macOS:
|
||||
# https://pyinstaller.org/en/stable/usage.html#building-macos-app-bundles
|
||||
# https://github.com/pyinstaller/pyinstaller/wiki/Recipe-OSX-Code-Signing
|
||||
|
||||
# Build matrix:
|
||||
# - Linux x86_64 glibc 2.35: ubuntu-latest
|
||||
# - Linux x86_64 glibc 2.31: extract_otp_secrets:bullseye
|
||||
# - Linux x86_64 glibc 2.36: extract_otp_secrets:bookworm
|
||||
# - Windows x86_64: windows-latest
|
||||
# - MacOS x86_64: macos-11
|
||||
# - Linux x86_64 glibc 2.28: extract_otp_secrets:buster
|
||||
# - Linux aarch64 glibc 2.28: extract_otp_secrets:buster
|
||||
# - MacOS universal2: macos-11
|
||||
# - Windows arm64: [buildx + https://github.com/batonogov/docker-pyinstaller]
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
pull_request:
|
||||
schedule:
|
||||
# Run weekly on default branch
|
||||
- cron: '47 4 * * 6'
|
||||
|
||||
jobs:
|
||||
|
||||
create-release:
|
||||
name: Create Release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
date: ${{ steps.meta.outputs.date }}
|
||||
version: ${{ steps.meta.outputs.version }}
|
||||
inline_version: ${{ steps.meta.outputs.inline_version }}
|
||||
tag_name: ${{ steps.meta.outputs.tag_name }}
|
||||
tag_message: ${{ steps.meta.outputs.tag_message }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set meta data
|
||||
id: meta
|
||||
# Writing to env with >> $GITHUB_ENV is an alternative
|
||||
run: |
|
||||
echo "date=$(TZ=Europe/Zurich date +'%d.%m.%Y')" >> $GITHUB_OUTPUT
|
||||
echo "version=${TAG_NAME/v/}" >> $GITHUB_OUTPUT
|
||||
echo "inline_version=${TAG_NAME/v/_}" >> $GITHUB_OUTPUT
|
||||
echo "tag_name=${{ github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
echo "tag_message=$(git tag -l --format='%(contents:subject)' ${{ github.ref_name }})" >> $GITHUB_OUTPUT
|
||||
env:
|
||||
TAG_NAME: ${{ github.ref_name }}
|
||||
PYTHONHASHSEED: 31
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
# https://docs.github.com/en/rest/releases/releases?apiVersion=2022-11-28#create-a-release
|
||||
run: |
|
||||
echo "date: ${{ steps.meta.outputs.date }}"
|
||||
echo "version: ${{ steps.meta.outputs.version }}"
|
||||
echo "inline_version: ${{ steps.meta.outputs.inline_version }}"
|
||||
echo "tag_name: ${{ steps.meta.outputs.tag_name }}"
|
||||
echo "tag_message: ${{ steps.meta.outputs.tag_message }}"
|
||||
response=$(curl \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/scito/extract_otp_secrets/releases \
|
||||
--silent \
|
||||
--show-error \
|
||||
-d '{"tag_name":"${{ github.ref }}","target_commitish":"master","name":"${{ steps.meta.outputs.version }} - ${{ steps.meta.outputs.date }}","body":"${{ steps.meta.outputs.tag_message }}\n\n## Executables\n\nDownload the executable for your platform and execute it, see [README.md](https://github.com/scito/extract_otp_secrets#readme)\n\n | Executable | Description |\n | --- | --- |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_linux_x86_64 | Linux x86_64/amd64 (glibc >= 2.31) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_linux_arm64 | Linux arm64 (glibc >= 2.31) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_win_x86_64.exe | Windows x86_64/amd64/x64 |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_win_arm64.exe | N/A |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_macos_x86_64.dmg | N/A, see [README.md](https://github.com/scito/extract_otp_secrets#readme) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_macos_x86_64.pkg | N/A, see [README.md](https://github.com/scito/extract_otp_secrets#readme) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_macos_x86_64 | MacOS x86_64/amd64 (bare executable, see [README.md](https://github.com/scito/extract_otp_secrets#readme); optional libzbar must be installed manually, see [README.md](https://github.com/scito/extract_otp_secrets#readme)) |\n | extract_otp_secrets${{ steps.meta.outputs.inline_version }}_macos_arm64 | N/A |\n","draft":true,"prerelease":false,"generate_release_notes":true}')
|
||||
echo upload_url=$(jq '.upload_url' <<< "$response") >> $GITHUB_OUTPUT
|
||||
echo $(jq -r '.upload_url' <<< "$response") > release_url.txt
|
||||
echo $(jq -r '.id' <<< "$response") > release_id.txt
|
||||
- name: Save Release URL File for publish
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release_url
|
||||
path: release_url.txt
|
||||
- name: Save asset upload id for publish
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release_id
|
||||
path: release_id.txt
|
||||
|
||||
build-linux-executable-in-docker:
|
||||
name: Build ${{ matrix.PLATFORM }} release in docker container
|
||||
# run only when code is compiling and tests are passing
|
||||
runs-on: ubuntu-latest
|
||||
needs: create-release
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- PLATFORM: linux/amd64
|
||||
EXE: extract_otp_secrets_linux_x86_64
|
||||
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_linux_x86_64
|
||||
- PLATFORM: linux/arm64
|
||||
EXE: extract_otp_secrets_linux_arm64
|
||||
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_linux_arm64
|
||||
|
||||
# steps to perform in job
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# 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@v3
|
||||
|
||||
# setup Docker build action
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
# Workaround for failing builds: https://github.com/docker/build-push-action/issues/761#issuecomment-1383822381
|
||||
# TODO remove workaround when fixed
|
||||
with:
|
||||
driver-opts: |
|
||||
image=moby/buildkit:v0.10.6
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Github Packages
|
||||
uses: docker/login-action@v3
|
||||
if: github.secret_source == 'Actions'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GHCR_IO_TOKEN }}
|
||||
|
||||
- name: Image digest
|
||||
# TODO upload digests to assets
|
||||
run: |
|
||||
echo "extract_otp_secrets: ${{ steps.docker_build_bullseye.outputs.digest }}"
|
||||
|
||||
# TODO use local docker image https://stackoverflow.com/a/61155718/1663871
|
||||
# https://github.com/multiarch/qemu-user-static
|
||||
# https://hub.docker.com/r/multiarch/qemu-user-static/
|
||||
- name: Run Pyinstaller in container for ${{ matrix.EXE }}
|
||||
run: |
|
||||
docker run --pull always --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
docker run --platform ${{ matrix.PLATFORM }} --pull always --entrypoint /bin/bash --rm -v "$(pwd)":/files -w /files scit0/extract_otp_secrets:bullseye -c 'apt-get update && apt-get -y install binutils && pip install -U -r /files/requirements.txt && pip install pyinstaller && PYTHONHASHSEED=31 && pyinstaller -y --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name ${{ matrix.EXE }} --distpath /files/dist/ /files/src/extract_otp_secrets.py'
|
||||
|
||||
- name: Smoke tests linux/amd64
|
||||
if: matrix.PLATFORM == 'linux/amd64'
|
||||
run: |
|
||||
dist/${{ matrix.EXE }} -V
|
||||
dist/${{ matrix.EXE }} -h
|
||||
dist/${{ matrix.EXE }} --debug
|
||||
dist/${{ matrix.EXE }} example_export.png
|
||||
dist/${{ matrix.EXE }} - < example_export.txt
|
||||
dist/${{ matrix.EXE }} --qr ZBAR example_export.png
|
||||
dist/${{ matrix.EXE }} --qr QREADER example_export.png
|
||||
dist/${{ matrix.EXE }} --qr QREADER_DEEP example_export.png
|
||||
dist/${{ matrix.EXE }} --qr CV2 example_export.png
|
||||
dist/${{ matrix.EXE }} --qr CV2_WECHAT example_export.png
|
||||
- name: Smoke tests linux/arm64
|
||||
if: matrix.PLATFORM == 'linux/arm64'
|
||||
run: |
|
||||
docker run --platform ${{ matrix.PLATFORM }} --pull always --entrypoint /bin/bash --rm -v "$(pwd)":/files -w /files scit0/extract_otp_secrets -c 'dist/${{ matrix.EXE }} -V && dist/${{ matrix.EXE }} -h && dist/${{ matrix.EXE }} example_export.png && dist/${{ matrix.EXE }} - < example_export.txt && dist/${{ matrix.EXE }} --qr ZBAR example_export.png && dist/${{ matrix.EXE }} --qr QREADER example_export.png && dist/${{ matrix.EXE }} --qr QREADER_DEEP example_export.png && dist/${{ matrix.EXE }} --qr CV2 example_export.png && dist/${{ matrix.EXE }} --qr CV2_WECHAT example_export.png'
|
||||
- name: Load Release URL File from release job
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release_url
|
||||
- name: Display structure of files
|
||||
run: ls -R
|
||||
- name: Upload EXE to artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.EXE }}
|
||||
path: dist/${{ matrix.EXE }}
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
response=$(curl \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Content-Type: application/x-executable" \
|
||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}"\
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--silent \
|
||||
--show-error \
|
||||
--data-binary @dist/${{ matrix.EXE }} \
|
||||
$(cat release_url.txt)=${{ matrix.ASSET_NAME }})
|
||||
|
||||
build-native-executables:
|
||||
name: Build native packages
|
||||
needs: create-release
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#choosing-github-hosted-runners
|
||||
include:
|
||||
- os: windows-latest
|
||||
TARGET: windows
|
||||
# TODO add --icon
|
||||
# TODO add --manifest
|
||||
# TODO find more elegant solution for pyzbar\libiconv.dll and pyzbar\libzbar-64.dll
|
||||
# Files of Visual C++ 2013 Redistributable Package: https://support.microsoft.com/en-us/topic/update-for-visual-c-2013-redistributable-package-d8ccd6a5-4e26-c290-517b-8da6cfdf4f10
|
||||
EXE: extract_otp_secrets.exe
|
||||
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_win_x86_64.exe
|
||||
ASSET_MIME: application/vnd.microsoft.portable-executable
|
||||
UPLOAD: true
|
||||
CMD_BUILD: |
|
||||
pyinstaller -y --add-data "$($Env:pythonLocation)\__yolo_v3_qr_detector:__yolo_v3_qr_detector" --add-binary "$($Env:pythonLocation)\Lib\site-packages\pyzbar\libiconv.dll:pyzbar" --add-binary "$($Env:pythonLocation)\Lib\site-packages\pyzbar\libzbar-64.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\msvcr120.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\msvcp120.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\vcamp120.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\vcomp120.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\vccorlib120.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120u.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120chs.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120cht.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120deu.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120enu.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120esn.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120fra.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120ita.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120jpn.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120kor.dll:pyzbar" --add-binary "$($Env:WinDir)\system32\mfc120rus.dll:pyzbar" --onefile --version-file build\win_file_version_info.txt --name extract_otp_secrets.exe src\extract_otp_secrets.py
|
||||
- os: macos-12
|
||||
TARGET: macos
|
||||
# https://pyinstaller.org/en/stable/spec-files.html#spec-file-options-for-a-macos-bundle
|
||||
EXE: extract_otp_secrets
|
||||
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_macos_x86_64
|
||||
DMG: extract_otp_secrets.dmg
|
||||
ASSET_NAME_DMG: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_macos_x86_64.dmg
|
||||
ASSET_MIME: application/octet-stream
|
||||
UPLOAD: true
|
||||
CMD_BUILD: |
|
||||
VERSION_STR=$(setuptools-git-versioning) COPYRIGHT_YEARS='2020-2023' envsubst < installer/extract_otp_secrets_macos_template.spec > extract_otp_secrets_macos.spec
|
||||
pyinstaller -y extract_otp_secrets_macos.spec
|
||||
installer/build_dmg.sh
|
||||
- os: ubuntu-latest
|
||||
TARGET: linux
|
||||
EXE: extract_otp_secrets_ubuntu
|
||||
ASSET_NAME: extract_otp_secrets${{ needs.create-release.outputs.inline_version }}_linux_x86_64_ubuntu_latest
|
||||
ASSET_MIME: application/x-executable
|
||||
UPLOAD: false
|
||||
CMD_BUILD: |
|
||||
pyinstaller -y --add-data $pythonLocation/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_ubuntu src/extract_otp_secrets.py
|
||||
steps:
|
||||
- name: Output path
|
||||
if: runner.os == 'Windows'
|
||||
run: echo "$($Env:Path)"
|
||||
- name: List Windir
|
||||
if: runner.os == 'Windows'
|
||||
run: ls "$($Env:WinDir)\system32"
|
||||
- uses: actions/checkout@v4
|
||||
- name: Set macos macos_python_path
|
||||
# TODO use variable for Python version
|
||||
run: echo "macos_python_path=/Library/Frameworks/Python.framework/Versions/3.12" >> $GITHUB_ENV
|
||||
- name: Set up Python 3.12
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.12
|
||||
check-latest: true
|
||||
- name: Install zbar shared lib for QReader (Linux)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get install -y libzbar0
|
||||
- name: Install zbar shared lib for QReader (macOS)
|
||||
if: runner.os == 'macOS'
|
||||
run: |
|
||||
brew install zbar create-dmg
|
||||
- name: Install dependencies
|
||||
# TODO fix --use-pep517
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -U -r requirements-dev.txt
|
||||
pip install -U .
|
||||
- name: Create Windows win_file_version_info.txt
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p build/
|
||||
VERSION_STR=$(setuptools-git-versioning) VERSION_MAJOR=$(cut -d '.' -f 1 <<< "$(setuptools-git-versioning)") VERSION_MINOR=$(cut -d '.' -f 2 <<< "$(setuptools-git-versioning)") VERSION_PATCH=$(echo $(cut -d '.' -f 3 <<< "$(setuptools-git-versioning)") | sed -E -n "s/^([0-9]+).*/\1/p") VERSION_BUILD=$(echo $(cut -d '.' -f 3 <<< "$(setuptools-git-versioning)") | sed -E -n -e"s/^[0-9]+.+/99/p")$(($(git rev-list --count $(git tag | sort -V -r | sed '1!d')..HEAD))) COPYRIGHT_YEARS='2020-2023' envsubst < installer/win_file_version_info_template.txt > build/win_file_version_info.txt
|
||||
- name: Build with pyinstaller for ${{ matrix.TARGET }}
|
||||
env:
|
||||
# Reproducible build: https://pyinstaller.org/en/stable/advanced-topics.html#creating-a-reproducible-build
|
||||
PYTHONHASHSEED: 31
|
||||
run: ${{ matrix.CMD_BUILD }}
|
||||
- name: Smoke tests for generated exe (general)
|
||||
run: |
|
||||
dist/${{ matrix.EXE }} -V
|
||||
dist/${{ matrix.EXE }} -h
|
||||
dist/${{ matrix.EXE }} --debug
|
||||
dist/${{ matrix.EXE }} example_export.png
|
||||
dist/${{ matrix.EXE }} --qr ZBAR example_export.png
|
||||
dist/${{ matrix.EXE }} --qr QREADER example_export.png
|
||||
dist/${{ matrix.EXE }} --qr QREADER_DEEP example_export.png
|
||||
dist/${{ matrix.EXE }} --qr CV2 example_export.png
|
||||
dist/${{ matrix.EXE }} --qr CV2_WECHAT example_export.png
|
||||
- name: Smoke tests for generated exe (stdin)
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
dist/${{ matrix.EXE }} - < example_export.txt
|
||||
- name: Load Release URL File from release job
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release_url
|
||||
- name: Load Release Id File from release job
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release_id
|
||||
- name: Display structure of files
|
||||
run: ls -R
|
||||
- name: Set meta data
|
||||
id: meta
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
shell: bash
|
||||
run: |
|
||||
echo "release_id=$(cat release_id.txt)" >> $GITHUB_OUTPUT
|
||||
echo "upload_url=https://uploads.github.com/repos/scito/extract_otp_secrets/releases/$(cat release_id.txt)/assets?name=" >> $GITHUB_OUTPUT
|
||||
- name: Upload EXE to artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ${{ matrix.EXE }}
|
||||
path: dist/${{ matrix.EXE }}
|
||||
- name: Upload DMG to artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
if: runner.os == 'macOS'
|
||||
with:
|
||||
name: ${{ matrix.DMG }}
|
||||
path: dist/${{ matrix.DMG }}
|
||||
- name: Upload Release Asset
|
||||
id: upload-release-asset
|
||||
if: matrix.UPLOAD && startsWith(github.ref, 'refs/tags/v')
|
||||
run: |
|
||||
curl -X POST -H "Accept: application/vnd.github+json" -H "Content-Type: ${{ matrix.ASSET_MIME }}" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" --show-error --data-binary @dist/${{ matrix.EXE }} ${{ steps.meta.outputs.upload_url }}=${{ matrix.ASSET_NAME }}
|
||||
- name: Upload Release Asset DMG (macOS)
|
||||
id: upload-release-asset-dmg
|
||||
if: false && matrix.UPLOAD && startsWith(github.ref, 'refs/tags/v') && runner.os == 'macOS'
|
||||
run: |
|
||||
curl -X POST -H "Accept: application/vnd.github+json" -H "Content-Type: ${{ matrix.ASSET_MIME }}" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" --show-error --data-binary @dist/${{ matrix.DMG }} ${{ steps.meta.outputs.upload_url }}=${{ matrix.ASSET_NAME_DMG }}
|
||||
|
||||
upload-hashes:
|
||||
name: Upload hashes
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs:
|
||||
- build-linux-executable-in-docker
|
||||
- build-native-executables
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Load Release Id File from release job
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: release_id
|
||||
- name: Set meta data
|
||||
id: meta
|
||||
run: |
|
||||
echo "release_id=$(cat release_id.txt)" >> $GITHUB_OUTPUT
|
||||
echo "upload_url=https://uploads.github.com/repos/scito/extract_otp_secrets/releases/$(cat release_id.txt)/assets?name=" >> $GITHUB_OUTPUT
|
||||
- name: Calculate and upload hashes from assets
|
||||
run: |
|
||||
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }}
|
||||
for asset_url in $(curl \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN"\
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--silent \
|
||||
--show-error \
|
||||
https://api.github.com/repos/scito/extract_otp_secrets/releases/${{ steps.meta.outputs.release_id }}/assets |
|
||||
jq -r '.[].url'); do
|
||||
echo "Download $asset_url"
|
||||
name=$(curl \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN"\
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--output-dir assets \
|
||||
-L \
|
||||
$asset_url |
|
||||
jq -r '.name')
|
||||
curl \
|
||||
-H "Accept: application/octet-stream" \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN"\
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--create-dirs \
|
||||
--output-dir assets \
|
||||
-L \
|
||||
-o $name \
|
||||
$asset_url
|
||||
done
|
||||
(cd assets/ && sha256sum * > ../sha256_hashes.txt)
|
||||
curl -X POST -H "Accept: application/vnd.github+json" -H "Content-Type: text/plain" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" --show-error --data-binary @sha256_hashes.txt ${{ steps.meta.outputs.upload_url }}=sha256_hashes.txt
|
||||
|
||||
(cd assets/ && sha512sum * > ../sha512_hashes.txt)
|
||||
curl -X POST -H "Accept: application/vnd.github+json" -H "Content-Type: text/plain" -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" -H "X-GitHub-Api-Version: 2022-11-28" --show-error --data-binary @sha512_hashes.txt ${{ steps.meta.outputs.upload_url }}=sha512_hashes.txt
|
@ -1,7 +0,0 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"ms-python.python",
|
||||
"ms-python.isort",
|
||||
"tamasfe.even-better-toml",
|
||||
]
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
{
|
||||
// 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"
|
||||
},
|
||||
]
|
||||
}
|
@ -1,675 +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
|
||||
|
||||
interactive=false
|
||||
ignore_version_check=true
|
||||
clean=false
|
||||
clean_flag=""
|
||||
build_base=true
|
||||
build_arm=false
|
||||
build_x86_64=true
|
||||
build_docker=false
|
||||
build_local=true
|
||||
build_exe=false
|
||||
build_nuitka_exe=false
|
||||
run_pipenv=true
|
||||
run_gui=false
|
||||
generate_result_files=false
|
||||
PYTHONHASHSEED=31
|
||||
|
||||
while test $# -gt 0; do
|
||||
case $1 in
|
||||
-h|--help)
|
||||
echo "Build extract_otp_secrets project"
|
||||
echo
|
||||
echo "$0 [options]"
|
||||
echo
|
||||
echo "Options:"
|
||||
echo "-i Interactive mode, all steps must be confirmed"
|
||||
echo "-C Ignore version check of protobuf/protoc"
|
||||
echo "-e Build exe"
|
||||
echo "-n Build nuitka exe"
|
||||
echo "-L Do not run protoc and base build locally incl. exes"
|
||||
echo "-d Build docker"
|
||||
echo "-a Build arm"
|
||||
echo "-X Do not build x86_64"
|
||||
echo "-B Do not build base"
|
||||
echo "-V Do not run pipenv"
|
||||
echo "-g Start extract_otp_secrets.py in GUI mode"
|
||||
echo "-c Clean everything"
|
||||
echo "-r Generate result files"
|
||||
echo "-h, --help Show help and quit"
|
||||
quit
|
||||
;;
|
||||
-i)
|
||||
interactive=true
|
||||
shift
|
||||
;;
|
||||
-C)
|
||||
ignore_version_check=false
|
||||
shift
|
||||
;;
|
||||
-B)
|
||||
build_base=false
|
||||
shift
|
||||
;;
|
||||
-L)
|
||||
build_local=false
|
||||
build_base=false
|
||||
shift
|
||||
;;
|
||||
-a)
|
||||
build_arm=true
|
||||
shift
|
||||
;;
|
||||
-X)
|
||||
build_x86_64=false
|
||||
shift
|
||||
;;
|
||||
-e)
|
||||
build_exe=true
|
||||
shift
|
||||
;;
|
||||
-n)
|
||||
build_nuitka_exe=true
|
||||
shift
|
||||
;;
|
||||
-d)
|
||||
build_docker=true
|
||||
shift
|
||||
;;
|
||||
-V)
|
||||
run_pipenv=false
|
||||
shift
|
||||
;;
|
||||
-g)
|
||||
run_gui=true
|
||||
shift
|
||||
;;
|
||||
-r)
|
||||
generate_result_files=true
|
||||
shift
|
||||
;;
|
||||
-c)
|
||||
clean=true
|
||||
clean_flag="--clean"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
BIN="$HOME/bin"
|
||||
DOWNLOADS="$HOME/downloads"
|
||||
|
||||
PYTHON="python3.11"
|
||||
PIP="pip3.11"
|
||||
PIPENV="$PYTHON -m pipenv"
|
||||
FLAKE8="$PYTHON -m flake8"
|
||||
MYPY="$PYTHON -m mypy"
|
||||
|
||||
# sudo ln -s /usr/bin/python3.11 /usr/bin/python
|
||||
|
||||
# Upgrade protoc
|
||||
|
||||
DEST="protoc"
|
||||
|
||||
if $clean; then
|
||||
cmd="docker image prune -f || echo 'No docker image pruned'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP uninstall -y extract-otp-secrets || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze | grep -v -E '^-e|^#' | xargs sudo $PIP uninstall -y || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze --user | grep -v -E '^-e|^#' | xargs $PIP uninstall -y || echo nothing done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PIP freeze | cut -d \"@\" -f1 | xargs pip uninstall -y || echo Nothing to do"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="sudo rm -rf dist/ build/ dist_*/ *.whl extracted_*.csv extracted_*.json pytest.xml pytest-coverage.txt .coverage tests/reports || true; find . -name '*.pyc' -type f -delete; find . -name '__pycache__' -type d -exec rm -r {} \; || true; find . -name '*.egg-info' -type d -exec rm -r {} \; || true; find . -name '*_cache' -type d -exec rm -r {} \; || true; mkdir -p tests/reports;"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="pipenv --rm || true"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="sudo pipenv --rm || true"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="mkdir -p dist"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_local; then
|
||||
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
echo -e "\n\nChecking Protoc version..."
|
||||
VERSION=$(curl -sL https://github.com/protocolbuffers/protobuf/releases/latest | grep -E "<title>" | perl -pe's%.*Protocol Buffers v(\d+\.\d+(\.\d+)?).*%\1%')
|
||||
BASEVERSION=4
|
||||
echo
|
||||
|
||||
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
|
||||
echo -e "\nProtoc remote version $VERSION\n"
|
||||
echo -e "Protoc local version: $OLDVERSION\n"
|
||||
|
||||
if [ "$OLDVERSION" != "$VERSION" ] || ! $ignore_version_check; then
|
||||
echo "Upgrade protoc from $OLDVERSION to $VERSION"
|
||||
|
||||
NAME="protoc-$VERSION"
|
||||
ARCHIVE="$NAME.zip"
|
||||
|
||||
mkdir -p $DOWNLOADS
|
||||
# https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protoc-21.6-linux-x86_64.zip
|
||||
cmd="wget --trust-server-names https://github.com/protocolbuffers/protobuf/releases/download/v$VERSION/protoc-$VERSION-linux-x86_64.zip -O $DOWNLOADS/$ARCHIVE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="echo -e '\nSize [Byte]'; stat --printf='%s\n' $DOWNLOADS/$ARCHIVE; echo -e '\nMD5'; md5sum $DOWNLOADS/$ARCHIVE; echo -e '\nSHA256'; sha256sum $DOWNLOADS/$ARCHIVE;"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="mkdir -p $BIN/$NAME; unzip $DOWNLOADS/$ARCHIVE -d $BIN/$NAME"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="echo $VERSION > $BIN/$NAME/.VERSION.txt; echo $VERSION > $BIN/$NAME/.VERSION_$VERSION.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="[ -d $BIN/$DEST.old ] && rm -rf $BIN/$DEST.old || echo 'No old dir to delete'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="[ -d $BIN/$DEST ] && mv -iT $BIN/$DEST $BIN/$DEST.old || echo 'No previous dir to keep'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="mv -iT $BIN/$NAME $BIN/$DEST"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="rm $DOWNLOADS/$ARCHIVE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$BIN/$DEST/bin/protoc --plugin=protoc-gen-mypy=$HOME/.local/bin/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python --proto_path=src google_auth.proto"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Update README.md
|
||||
|
||||
cmd="perl -i -pe 's%proto(buf|c)([- ])(\d\.)?$OLDVERSION%proto\$1\$2\${3}$VERSION%g' README.md && perl -i -pe 's%(protobuf/releases/tag/v)$OLDVERSION%\${1}$VERSION%g' README.md"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
else
|
||||
echo -e "\nVersion has not changed. Quit"
|
||||
fi
|
||||
|
||||
# Upgrade pip requirements
|
||||
|
||||
cmd="pip install -U pip"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIP --version
|
||||
|
||||
cmd="$PIP install --use-pep517 -U -r requirements.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
if $build_base; then
|
||||
# Lint
|
||||
|
||||
LINT_OUT_FILE="tests/reports/flake8_results.txt"
|
||||
cmd="$FLAKE8 . --count --select=E9,F63,F7,F82 --show-source --statistics | tee $LINT_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$FLAKE8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics --exclude=.git,__pycache__,docs/source/conf.py,old,build,dist,protobuf_generated_python | tee -a $LINT_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Type checking
|
||||
|
||||
TYPE_CHECK_OUT_FILE="tests/reports/mypy_results.txt"
|
||||
cmd="$MYPY --install-types --non-interactive src/*.py tests/*.py"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# change to src as python -m mypy adds the current dir Python sys.path
|
||||
# execute in a subshell in order not to loose the exit code and not to change the dir in the currrent shell
|
||||
cmd="$MYPY --strict src/*.py tests/*.py | tee $TYPE_CHECK_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
|
||||
# Generate results files
|
||||
|
||||
if $generate_result_files; then
|
||||
cmd="for color in '' '-n'; do for level in '' '-v' '-vv' '-vvv'; do $PYTHON src/extract_otp_secrets.py example_export.txt \$color \$level > tests/data/print_verbose_output\$color\$level.txt; done; done"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# pip -e install
|
||||
|
||||
cmd="$PIP install -U -e ."
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="extract_otp_secrets example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="extract_otp_secrets - < example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Test (needs module)
|
||||
|
||||
cmd="$PYTHON src/extract_otp_secrets.py example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="$PYTHON src/extract_otp_secrets.py - < example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
COVERAGE_OUT_FILE="tests/reports/pytest-coverage.txt"
|
||||
cmd="pytest --cov=extract_otp_secrets_test --junitxml=tests/reports/pytest.xml --cov-report html:tests/reports/html --cov-report=term-missing tests/ | tee $COVERAGE_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# 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
|
||||
|
||||
# Pipenv
|
||||
|
||||
if $run_pipenv; then
|
||||
cmd="$PIP install -U pipenv"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIPENV --version
|
||||
|
||||
cmd="$PIPENV --rm && $PIPENV update && $PIPENV install"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
$PIPENV run python --version
|
||||
|
||||
cmd="$PIPENV run pytest --cov=extract_otp_secrets_test tests/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# Build wheel
|
||||
|
||||
cmd="$PIP wheel ."
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# Build executable
|
||||
if $build_exe; then
|
||||
cmd="LOCAL_GLIBC_VERSION=$(ldd --version | sed '1!d' | sed -E 's/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/')"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
echo "local glibc: $LOCAL_GLIBC_VERSION"
|
||||
|
||||
cmd="time pyinstaller -y --specpath installer --add-data $HOME/.local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile $clean_flag src/extract_otp_secrets.py"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets -h"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets --qr ZBAR example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# Build compiled executable
|
||||
|
||||
if $build_nuitka_exe; then
|
||||
cmd="LOCAL_GLIBC_VERSION=$(ldd --version | sed '1!d' | sed -E 's/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/')"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
echo "local glibc: $LOCAL_GLIBC_VERSION"
|
||||
|
||||
cmd="$PIP install -U pyqt5"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="time $PYTHON -m nuitka --enable-plugin=tk-inter --enable-plugin=pyqt5 --noinclude-default-mode=nofollow --clang --include-data-dir=$HOME/.local/__yolo_v3_qr_detector/=__yolo_v3_qr_detector/ --onefile --output-dir=build/nuitka --output-filename=extract_otp_secrets_compiled src/extract_otp_secrets.py"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="cp build/nuitka/extract_otp_secrets_compiled dist/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets_compiled -h"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets_compiled --qr ZBAR example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
# Generate README.md TOC
|
||||
|
||||
cmd="gfm-toc -s 2 -e 3 -t -o README.md > docs/README_TOC.md"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# create Windows win_file_version_info.txt
|
||||
cmd="VERSION_STR=$(setuptools-git-versioning) VERSION_MAJOR=$(cut -d '.' -f 1 <<< "$(setuptools-git-versioning)") VERSION_MINOR=$(cut -d '.' -f 2 <<< "$(setuptools-git-versioning)") VERSION_PATCH=$(cut -d '.' -f 3 <<< "$(setuptools-git-versioning)") VERSION_BUILD=$(($(git rev-list --count $(git tag | sort -V -r | sed '1!d')..HEAD))) COPYRIGHT_YEARS='2020-2023' envsubst < installer/win_file_version_info_template.txt > build/win_file_version_info.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# create macOS extract_otp_secrets_macos.spec from extract_otp_secrets_macos_template.spec
|
||||
cmd="VERSION_STR=$(setuptools-git-versioning) COPYRIGHT_YEARS='2020-2023' envsubst < installer/extract_otp_secrets_macos_template.spec > build/extract_otp_secrets_macos.spec"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_docker; then
|
||||
# Build docker
|
||||
|
||||
if $build_x86_64; then
|
||||
# Build Dockerfile_only_txt (Alpine)
|
||||
cmd="docker build . -t extract_otp_secrets_only_txt -t extract_otp_secrets:only-txt -t extract_otp_secrets:alpine -f docker/Dockerfile_only_txt --pull --build-arg RUN_TESTS=false"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -v \"$(pwd)\":/files:ro extract_otp_secrets_only_txt example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets_only_txt - < example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secrets_only_txt extract_otp_secrets_test.py -k 'not qreader' -vvv --relaxed"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build extract_otp_secrets (Debian Bookworm)
|
||||
cmd="docker build . -t extract_otp_secrets -t extract_otp_secrets:bookworm --pull -f docker/Dockerfile --build-arg RUN_TESTS=false"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -v \"$(pwd)\":/files:ro extract_otp_secrets example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="cat example_export.txt | docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets - -c - > example_output.csv"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets = < example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secrets"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build extract_otp_secrets (Debian Bullseye)
|
||||
cmd="docker build . -t extract_otp_secrets:bullseye -t extract_otp_secrets:bullseye-x86_64 --pull -f docker/Dockerfile --build-arg RUN_TESTS=false --build-arg BASE_IMAGE=python:3.12-slim-bullseye"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -v \"$(pwd)\":/files:ro extract_otp_secrets:bullseye example_export.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="cat example_export.txt | docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets:bullseye - -c - > example_output.csv"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --rm -i -v \"$(pwd)\":/files:ro extract_otp_secrets:bullseye = < example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="docker run --entrypoint /extract/run_pytest.sh --rm -v \"$(pwd)\":/files:ro extract_otp_secrets:bullseye"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build executable from Docker latest
|
||||
# sed "1!d" is workaround for head -n 1 since it head procduces exit code != 0
|
||||
BOOKWORM_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
|
||||
echo "Bookworm glibc: $BOOKWORM_GLIBC_VERSION"
|
||||
fi
|
||||
|
||||
if $build_arm; then
|
||||
# build linux/arm64
|
||||
cmd="docker run --pull always --rm --privileged multiarch/qemu-user-static --reset -p yes"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build extract_otp_secrets (Debian Bullseye)
|
||||
cmd="docker buildx build --platform=linux/arm64 . -t extract_otp_secrets:bullseye-arm64 --pull -f docker/Dockerfile --build-arg RUN_TESTS=false --build-arg BASE_IMAGE=python:3.12-slim-bullseye"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_exe; then
|
||||
if $build_x86_64; then
|
||||
cmd="docker run --platform linux/amd64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets -c 'apt-get update && apt-get -y install binutils && pip install -U pip && pip install -U -r /files/requirements.txt && pip install pyinstaller && PYTHONHASHSEED=31 && pyinstaller -y --specpath installer --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_linux_x86_64_bookworm --distpath /files/dist/ /files/src/extract_otp_secrets.py'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets_linux_x86_64_bookworm -h || echo 'Could not run exe; see error message above'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets_linux_x86_64_bookworm --qr ZBAR example_export.png || echo 'Could not run exe; see error message above'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build executable from Docker bullseye
|
||||
BULLSEYE_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets:bullseye -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
|
||||
echo "Bullseye glibc: $BULLSEYE_GLIBC_VERSION"
|
||||
|
||||
cmd="docker run --platform linux/amd64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye -c 'apt-get update && apt-get -y install binutils && pip install -U pip && pip install -U -r /files/requirements.txt && pip install pyinstaller && PYTHONHASHSEED=31 && pyinstaller -y --specpath installer --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_linux_x86_64 --distpath /files/dist/ /files/src/extract_otp_secrets.py'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets_linux_x86_64 -h"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="dist/extract_otp_secrets_linux_x86_64 --qr ZBAR example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_arm; then
|
||||
# build linux/arm64
|
||||
cmd="docker run --platform linux/arm64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye-arm64 -c 'apt-get update && apt-get -y install binutils && pip install -U pip && pip install -U -r /files/requirements.txt && pip install pyinstaller && PYTHONHASHSEED=31 && pyinstaller --specpath installer -y --add-data /usr/local/__yolo_v3_qr_detector/:__yolo_v3_qr_detector/ --onefile --name extract_otp_secrets_linux_arm64 --distpath /files/dist/ /files/src/extract_otp_secrets.py'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="PLATFORM='linux/arm64' && EXE='dist/extract_otp_secrets_linux_arm64' && docker run --platform \"\$PLATFORM\" --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye-arm64 -c \"\$EXE -V && \$EXE -h && \$EXE example_export.png && \$EXE - < example_export.txt && \$EXE --qr ZBAR example_export.png && \$EXE --qr QREADER example_export.png && \$EXE --qr QREADER_DEEP example_export.png && \$EXE --qr CV2 example_export.png && \$EXE --qr CV2_WECHAT example_export.png\""
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $build_nuitka_exe; then
|
||||
if $build_x86_64; then
|
||||
cmd="docker run --platform linux/amd64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets -c 'apt-get update && apt-get -y install binutils build-essential patchelf && pip install -U pip && pip install -U -r /files/requirements.txt && pip install nuitka pyqt5 && PYTHONHASHSEED=31 && python -m nuitka --enable-plugin=tk-inter --enable-plugin=pyqt5 --include-data-dir=/usr/local/__yolo_v3_qr_detector/=__yolo_v3_qr_detector/ --onefile --output-dir=/files/build/docker/nuitka --output-filename=extract_otp_secrets_linux_x86_64_bookworm_compiled /files/src/extract_otp_secrets.py'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="build/docker/nuitka/extract_otp_secrets_linux_x86_64_bookworm_compiled -h"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="build/docker/nuitka/extract_otp_secrets_linux_x86_64_bookworm_compiled --qr ZBAR example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="cp build/docker/nuitka/extract_otp_secrets_linux_x86_64_bookworm_compiled dist/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
# Build executable from Docker bullseye
|
||||
BULLSEYE_GLIBC_VERSION=$(docker run --entrypoint /bin/bash --rm extract_otp_secrets:bullseye -c 'ldd --version | sed "1!d" | sed -E "s/.* ([[:digit:]]+\.[[:digit:]]+)$/\1/"')
|
||||
echo "Bookworm glibc: $BULLSEYE_GLIBC_VERSION"
|
||||
|
||||
cmd="docker run --platform linux/amd64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye -c 'apt-get update && apt-get -y install binutils build-essential patchelf && pip install -U pip && pip install -U -r /files/requirements.txt && pip install nuitka pyqt5 && PYTHONHASHSEED=31 && python -m nuitka --enable-plugin=tk-inter --enable-plugin=pyqt5 --include-data-dir=/usr/local/__yolo_v3_qr_detector/=__yolo_v3_qr_detector/ --onefile --output-dir=/files/build/docker/nuitka --output-filename=extract_otp_secrets_linux_x86_64_bullseye_compiled /files/src/extract_otp_secrets.py'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="build/docker/nuitka/extract_otp_secrets_linux_x86_64_bullseye_compiled -h"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="build/docker/nuitka/extract_otp_secrets_linux_x86_64_bullseye_compiled --qr ZBAR example_export.png"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="cp build/docker/nuitka/extract_otp_secrets_linux_x86_64_bullseye_compiled dist/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_arm; then
|
||||
# build linux/arm64
|
||||
cmd="docker run --platform linux/arm64 --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye-arm64 -c 'apt-get update && apt-get -y install binutils build-essential patchelf qt5-default && pip install -U pip && pip install -U -r /files/requirements.txt && pip install nuitka pyqt5 && PYTHONHASHSEED=31 && python -m nuitka --enable-plugin=tk-inter --enable-plugin=pyqt5 --include-data-dir=/usr/local/__yolo_v3_qr_detector/=__yolo_v3_qr_detector/ --onefile --output-dir=/files/build/docker/nuitka --output-filename=extract_otp_secrets_linux_arm64_compiled /files/src/extract_otp_secrets.py'"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="PLATFORM='linux/arm64' && EXE='build/docker/nuitka/extract_otp_secrets_linux_arm64_compiled' && docker run --platform \"\$PLATFORM\" --entrypoint /bin/bash --rm -v \"$(pwd)\":/files -w /files extract_otp_secrets:bullseye-arm64 -c \"\$EXE -V && \$EXE -h && \$EXE example_export.png && \$EXE - < example_export.txt && \$EXE --qr ZBAR example_export.png && \$EXE --qr QREADER example_export.png && \$EXE --qr QREADER_DEEP example_export.png && \$EXE --qr CV2 example_export.png && \$EXE --qr CV2_WECHAT example_export.png\""
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="cp build/docker/nuitka/extract_otp_secrets_linux_arm64_compiled dist/"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Run GUI from Docker
|
||||
if $build_x86_64 && $run_gui; then
|
||||
cmd="docker run --rm -v "$(pwd)":/files:ro --device=\"/dev/video0:/dev/video0\" --env=\"DISPLAY\" -v /tmp/.X11-unix:/tmp/.X11-unix:ro extract_otp_secrets &"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
fi
|
||||
|
||||
if $run_gui; then
|
||||
cmd="$PYTHON src/extract_otp_secrets.py &"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
if $build_base; then
|
||||
line=$(printf '#%.0s' $(eval echo {1..$(( ($COLUMNS - 10) / 2))}))
|
||||
echo -e "\n${blueBold}$line RESULTS $line${reset}"
|
||||
|
||||
cmd="cat $TYPE_CHECK_OUT_FILE $LINT_OUT_FILE $COVERAGE_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
fi
|
||||
|
||||
line=$(printf '#%.0s' $(eval echo {1..$(( ($COLUMNS - 10) / 2))}))
|
||||
echo -e "\n${greenBold}$line SUCCESS $line${reset}"
|
||||
|
||||
git status
|
||||
|
||||
quit
|
@ -1,4 +0,0 @@
|
||||
alias ll='ls -lh'
|
||||
alias la='ls -lha'
|
||||
alias l='ls -alhF'
|
||||
alias ls-l='ls -lh'
|
@ -1,36 +0,0 @@
|
||||
# --build-arg BASE_IMAGE=python:3.11-slim-buster
|
||||
ARG BASE_IMAGE=python:3.12-slim-bookworm
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
# https://docs.docker.com/engine/reference/builder/
|
||||
|
||||
# For debugging
|
||||
# docker build . -t extract_otp_secrets --pull --build-arg RUN_TESTS=false
|
||||
# docker run --rm -v "$(pwd)":/files:ro extract_otp_secrets
|
||||
# docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets
|
||||
# docker run --entrypoint /bin/bash -it --rm -v "$(pwd)":/files:ro --device="/dev/video0:/dev/video0" --env="DISPLAY" -v /tmp/.X11-unix:/tmp/.X11-unix:ro extract_otp_secrets
|
||||
|
||||
WORKDIR /extract
|
||||
|
||||
COPY requirements*.txt src/ run_pytest.sh pytest.ini tests/ example_*.txt example_*.png example_*.csv example*.json docker/.alias ./
|
||||
|
||||
ARG RUN_TESTS=true
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libgl1 \
|
||||
libglib2.0-0 \
|
||||
libsm6 \
|
||||
libzbar0 \
|
||||
python3-tk \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& pip install --no-cache-dir -U pip -r requirements.txt \
|
||||
&& if [ "$RUN_TESTS" = "true" ]; then /extract/run_pytest.sh; else echo "Not running tests..."; fi \
|
||||
&& echo 'test -s /extract/.alias && . /extract/.alias || true' >> ~/.bashrc
|
||||
|
||||
WORKDIR /files
|
||||
|
||||
ENTRYPOINT ["python", "/extract/extract_otp_secrets.py"]
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/scito/extract_otp_secrets
|
||||
LABEL org.opencontainers.image.license GPL-3.0+
|
||||
LABEL maintainer="Scito https://scito.ch, https://github.com/scito"
|
@ -1,48 +0,0 @@
|
||||
ARG BASE_IMAGE=python:3.12-alpine
|
||||
FROM $BASE_IMAGE
|
||||
|
||||
# https://docs.docker.com/engine/reference/builder/
|
||||
|
||||
# For debugging
|
||||
# docker run --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt
|
||||
# docker build . -t extract_otp_secrets_only_txt -f Dockerfile_only_txt --pull --build-arg RUN_TESTS=false
|
||||
# docker run --entrypoint /bin/ash -it --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt -l
|
||||
# docker run --entrypoint /extract/run_pytest.sh --rm -v "$(pwd)":/files:ro extract_otp_secrets_only_txt extract_otp_secrets_test.py -k "not qreader" --relaxed -vvv -s
|
||||
|
||||
# https://github.com/pypa/manylinux/blob/main/docker/Dockerfile
|
||||
|
||||
WORKDIR /extract
|
||||
|
||||
COPY requirements*.txt src/ run_pytest.sh pytest.ini tests/ example_*.txt example_*.png example_*.csv example*.json docker/.alias ./
|
||||
|
||||
ARG RUN_TESTS=true
|
||||
|
||||
RUN apk add --no-cache \
|
||||
jpeg \
|
||||
zlib \
|
||||
&& echo "Arch: $(apk --print-arch)" \
|
||||
&& if [[ "$(apk --print-arch)" == "aarch64" ]]; then apk add --no-cache --virtual .build-deps \
|
||||
gcc \
|
||||
jpeg-dev \
|
||||
libc-dev \
|
||||
py3-setuptools \
|
||||
python3-dev \
|
||||
zlib-dev \
|
||||
; fi \
|
||||
&& pip install --no-cache-dir -U \
|
||||
pip \
|
||||
colorama \
|
||||
Pillow \
|
||||
protobuf \
|
||||
qrcode \
|
||||
&& if [[ "$(apk --print-arch)" == "aarch64" ]]; then apk del .build-deps; fi \
|
||||
&& if [[ "$RUN_TESTS" == "true" ]]; then /extract/run_pytest.sh extract_otp_secrets_test.py -k "not qreader" --relaxed; else echo "Not running tests..."; fi \
|
||||
&& echo 'test -s /extract/.alias && . /extract/.alias || true' >> ~/.profile
|
||||
|
||||
WORKDIR /files
|
||||
|
||||
ENTRYPOINT ["python", "/extract/extract_otp_secrets.py"]
|
||||
|
||||
LABEL org.opencontainers.image.source https://github.com/scito/extract_otp_secrets
|
||||
LABEL org.opencontainers.image.license GPL-3.0+
|
||||
LABEL maintainer="Scito https://scito.ch, https://github.com/scito"
|
Before Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 38 KiB |
@ -1,56 +0,0 @@
|
||||
Generate from file: README.md
|
||||
|
||||
## Table of contents
|
||||
|
||||
- [Table of contents](#table-of-contents)
|
||||
- [Download and run binary executable (🆕 since v2.1)](#download-and-run-binary-executable--since-v21)
|
||||
- [MacOS](#macos)
|
||||
- [Usage](#usage)
|
||||
- [Capture QR codes from camera (🆕 since version 2.0)](#capture-qr-codes-from-camera--since-version-20)
|
||||
- [With builtin QR decoder from image files (🆕 since version 2.0)](#with-builtin-qr-decoder-from-image-files--since-version-20)
|
||||
- [With external QR decoder app from text files](#with-external-qr-decoder-app-from-text-files)
|
||||
- [Installation of Python script (recommended for developers or advanced users)](#installation-of-python-script-recommended-for-developers-or-advanced-users)
|
||||
- [Installation of optional shared system libraries (recommended)](#installation-of-optional-shared-system-libraries-recommended)
|
||||
- [Program help: arguments and options](#program-help-arguments-and-options)
|
||||
- [Examples](#examples)
|
||||
- [Printing otp secrets form text file](#printing-otp-secrets-form-text-file)
|
||||
- [Printing otp secrets from image file](#printing-otp-secrets-from-image-file)
|
||||
- [Writing otp secrets to csv file](#writing-otp-secrets-to-csv-file)
|
||||
- [Writing otp secrets to json file](#writing-otp-secrets-to-json-file)
|
||||
- [Printing otp secrets multiple files](#printing-otp-secrets-multiple-files)
|
||||
- [Printing otp secrets from stdin (text)](#printing-otp-secrets-from-stdin-text)
|
||||
- [Printing otp secrets from stdin (image)](#printing-otp-secrets-from-stdin-image)
|
||||
- [Printing otp secrets csv to stdout](#printing-otp-secrets-csv-to-stdout)
|
||||
- [Printing otp secrets csv to stdout without header line](#printing-otp-secrets-csv-to-stdout-without-header-line)
|
||||
- [Reading from stdin and printing to stdout](#reading-from-stdin-and-printing-to-stdout)
|
||||
- [Features](#features)
|
||||
- [KeePass](#keepass)
|
||||
- [How to export otp secrets from Google Authenticator app](#how-to-export-otp-secrets-from-google-authenticator-app)
|
||||
- [Glossary](#glossary)
|
||||
- [Alternative installation methods](#alternative-installation-methods)
|
||||
- [pip using github](#pip-using-github)
|
||||
- [local pip](#local-pip)
|
||||
- [pipenv](#pipenv)
|
||||
- [Visual Studio Code Remote - Containers / VSCode devcontainer](#visual-studio-code-remote---containers--vscode-devcontainer)
|
||||
- [venv](#venv)
|
||||
- [devbox](#devbox)
|
||||
- [docker](#docker)
|
||||
- [More docker examples](#more-docker-examples)
|
||||
- [Tests](#tests)
|
||||
- [PyTest](#pytest)
|
||||
- [unittest](#unittest)
|
||||
- [VSCode Setup](#vscode-setup)
|
||||
- [Development](#development)
|
||||
- [Build](#build)
|
||||
- [Upgrade pip Packages](#upgrade-pip-packages)
|
||||
- [Build docker images](#build-docker-images)
|
||||
- [Create executables with pyinstaller](#create-executables-with-pyinstaller)
|
||||
- [Full local build (bash)](#full-local-build-bash)
|
||||
- [Technical background](#technical-background)
|
||||
- [References](#references)
|
||||
- [Issues](#issues)
|
||||
- [Problems and Troubleshooting](#problems-and-troubleshooting)
|
||||
- [Windows error message](#windows-error-message)
|
||||
- [Related projects](#related-projects)
|
||||
|
||||
Table of contents generated.
|
Before Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 8.1 KiB |
Before Width: | Height: | Size: 528 KiB |
@ -1,3 +0,0 @@
|
||||
## Star History
|
||||
|
||||
[![Star History Chart](https://api.star-history.com/svg?repos=scito/extract_otp_secrets&type=Date)](https://star-history.com/#scito/extract_otp_secrets&Date)
|
Before Width: | Height: | Size: 653 KiB |
@ -1,2 +0,0 @@
|
||||
Title,User Name,HmacOtp-Secret-Base32,HmacOtp-Counter,Group
|
||||
,hotp demo,7KSQL2JTUDIS5EF65KLMRQIIGY,4,OTP/HOTP
|
|
@ -1,6 +0,0 @@
|
||||
Title,User Name,TimeOtp-Secret-Base32,Group
|
||||
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,7 +1,5 @@
|
||||
name,secret,issuer,type,counter,url
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
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
|
||||
name,secret,issuer,type,url
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
|
@ -0,0 +1,224 @@
|
||||
# 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_json(args, otps)
|
||||
|
||||
|
||||
def parse_args(sys_args):
|
||||
arg_parser = argparse.ArgumentParser()
|
||||
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 to json file', metavar=('FILE'))
|
||||
arg_parser.add_argument('--csv', '-c', help='export to csv file', 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 --quite are mutual 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 = get_enum_name_by_number(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,
|
||||
"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 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('totp' if raw_otp.type == 2 else 'hotp', 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 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)))
|
||||
|
||||
|
||||
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)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys_main()
|
@ -1,22 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Create a folder (named dmg) to prepare our DMG in (if it doesn't already exist).
|
||||
|
||||
# https://www.pythonguis.com/tutorials/packaging-pyqt5-applications-pyinstaller-macos-dmg/
|
||||
|
||||
mkdir -p dist/dmg
|
||||
# Empty the dmg folder.
|
||||
rm -r dist/dmg/*
|
||||
# Copy the app bundle to the dmg folder.
|
||||
cp -r "dist/extract_otp_secrets.app" dist/dmg
|
||||
# If the DMG already exists, delete it.
|
||||
test -f "dist/extract_otp_secrets.dmg" && rm "dist/extract_otp_secrets.dmg"
|
||||
create-dmg \
|
||||
--volname "Extract OTP Secrets" \
|
||||
--window-pos 200 120 \
|
||||
--window-size 600 300 \
|
||||
--icon-size 100 \
|
||||
--hide-extension "extract_otp_secrets.app" \
|
||||
--app-drop-link 425 120 \
|
||||
"dist/extract_otp_secrets.dmg" \
|
||||
"dist/dmg/"
|
@ -1,111 +0,0 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
# https://www.pythonguis.com/tutorials/packaging-pyqt5-applications-pyinstaller-macos-dmg/
|
||||
# https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFBundles/BundleTypes/BundleTypes.html
|
||||
|
||||
block_cipher = None
|
||||
|
||||
a = Analysis(
|
||||
['src/extract_otp_secrets.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=[('$macos_python_path/__yolo_v3_qr_detector/', '__yolo_v3_qr_detector/')],
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
[],
|
||||
name='extract_otp_secrets',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=True,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
app = BUNDLE(
|
||||
exe,
|
||||
name='extract_otp_secrets.app',
|
||||
icon=None,
|
||||
bundle_identifier='ch.scito.tools.extract_otp_secrets',
|
||||
version='$VERSION_STR',
|
||||
info_plist={
|
||||
'NSPrincipalClass': 'NSApplication',
|
||||
'NSAppleScriptEnabled': False,
|
||||
'NSHumanReadableCopyright': 'Copyright © $COPYRIGHT_YEARS Scito.',
|
||||
'CFBundleDocumentTypes': [
|
||||
# Reference: https://chromium.googlesource.com/chromium/src/+/lkgr/chrome/app/app-Info.plist
|
||||
# https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/understanding_utis/understand_utis_conc/understand_utis_conc.html#//apple_ref/doc/uid/TP40001319-CH202-SW6
|
||||
# https://developer.apple.com/documentation/uniformtypeidentifiers/system-declared_uniform_type_identifiers
|
||||
{
|
||||
'CFBundleTypeName': 'GIF image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['com.compuserve.gif'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'JPEG image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['public.jpeg'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'PNG image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['public.png'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'WebP image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['org.webmproject.webp'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'Tiff image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['public.tiff'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'Bmp image',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['com.microsoft.bmp'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
{
|
||||
'CFBundleTypeName': 'Plain text document',
|
||||
'CFBundleTypeIconFile': 'document.icns',
|
||||
'LSItemContentTypes': ['public.plain-text'],
|
||||
'LSHandlerRank': 'Alternate',
|
||||
'NSExportableTypes': ['public.json','public.comma-separated-values-text','public.plain-text'],
|
||||
},
|
||||
],
|
||||
},
|
||||
)
|
@ -1,46 +0,0 @@
|
||||
# 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])])
|
||||
]
|
||||
)
|
@ -1,74 +0,0 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"pip",
|
||||
"nuitka",
|
||||
# https://setuptools-git-versioning.readthedocs.io/en/latest/differences.html
|
||||
"setuptools>=64.0.0",
|
||||
"setuptools-git-versioning",
|
||||
"wheel>=0.37.0",
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "extract_otp_secrets"
|
||||
# https://pypi.org/classifiers/
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Environment :: Console",
|
||||
"Environment :: X11 Applications :: Qt",
|
||||
"Environment :: Win32 (MS Windows)",
|
||||
"Topic :: System :: Archiving :: Backup",
|
||||
"Topic :: Utilities",
|
||||
"Topic :: Security",
|
||||
"Topic :: Multimedia :: Graphics :: Capture :: Digital Camera",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"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",
|
||||
"Pillow",
|
||||
"protobuf",
|
||||
"pyzbar",
|
||||
"qrcode",
|
||||
"qreader<2.0.0",
|
||||
]
|
||||
description = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as 'Google Authenticator'"
|
||||
dynamic = ["version"]
|
||||
keywords = ["python", "security", "json", "otp", "csv", "protobuf", "qrcode", "two-factor", "totp", "google-authenticator", "recovery", "proto3", "mfa", "two-factor-authentication", "tfa", "qr-codes", "otpauth", "2fa", "security-tools", "cv2"]
|
||||
license = {text = "GNU General Public License v3 (GPLv3)"}
|
||||
readme = "README.md"
|
||||
authors = [{name = "scito", email = "info@scito.ch"}]
|
||||
maintainers = [{name = "scito", email = "info@scito.ch"}]
|
||||
requires-python = ">=3.8, <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", ]
|
@ -1,3 +0,0 @@
|
||||
[pytest]
|
||||
markers =
|
||||
qreader: QR image reader tests
|
@ -1,7 +1,3 @@
|
||||
colorama>=0.4.6
|
||||
opencv-contrib-python
|
||||
Pillow
|
||||
protobuf
|
||||
pyzbar
|
||||
qrcode
|
||||
qreader<2.0.0
|
||||
Pillow
|
||||
|
@ -1,5 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd /extract
|
||||
mkdir -p tests
|
||||
ln -sf /extract/data tests/data
|
||||
pip install -U pytest pytest-mock && pytest "$@"
|
@ -1,16 +0,0 @@
|
||||
[metadata]
|
||||
name = extract_otp_secrets
|
||||
|
||||
[options]
|
||||
python_requires = >=3.8, <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
|
@ -1,957 +0,0 @@
|
||||
# Extract one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps such as "Google Authenticator"
|
||||
#
|
||||
# For more information, see README.md
|
||||
#
|
||||
# Source code available on https://github.com/scito/extract_otp_secrets
|
||||
#
|
||||
# Technical background:
|
||||
# The export QR code from "Google Authenticator" contains the URL "otpauth-migration://offline?data=…".
|
||||
# The data parameter is a base64 encoded proto3 message (Google Protocol Buffers).
|
||||
#
|
||||
# Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition):
|
||||
# protoc --plugin=protoc-gen-mypy=path/to/protoc-gen-mypy --python_out=src/protobuf_generated_python --mypy_out=src/protobuf_generated_python --proto_path=src google_auth.proto
|
||||
#
|
||||
# References:
|
||||
# Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
||||
# Template code: https://github.com/beemdevelopment/Aegis/pull/406
|
||||
|
||||
# Author: Scito (https://scito.ch)
|
||||
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
from __future__ import annotations # workaround for PYTHON <= 3.10
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import csv
|
||||
import fileinput
|
||||
import json
|
||||
import os
|
||||
import platform
|
||||
import re
|
||||
import sys
|
||||
import urllib.parse as urlparse
|
||||
from enum import Enum, IntEnum
|
||||
from importlib.metadata import PackageNotFoundError, version
|
||||
from typing import (Any, Final, List, Optional, Sequence, TextIO, Tuple,
|
||||
TypedDict, Union)
|
||||
|
||||
import colorama
|
||||
from qrcode import QRCode # type: ignore
|
||||
|
||||
import protobuf_generated_python.google_auth_pb2 as pb
|
||||
|
||||
debug_mode = '-d' in sys.argv[1:] or '--debug' in sys.argv[1:]
|
||||
quiet = '-q' in sys.argv[1:] or '--quiet' in sys.argv[1:]
|
||||
headless: bool = False
|
||||
|
||||
|
||||
try:
|
||||
import cv2
|
||||
import numpy as np
|
||||
import cv2.typing
|
||||
|
||||
try:
|
||||
import tkinter
|
||||
import tkinter.filedialog
|
||||
import tkinter.messagebox
|
||||
except ImportError:
|
||||
headless = True
|
||||
|
||||
try:
|
||||
import pyzbar.pyzbar as zbar # type: ignore
|
||||
from qreader import QReader # type: ignore
|
||||
zbar_available = True
|
||||
except Exception as e:
|
||||
if not quiet:
|
||||
print(f"""
|
||||
WARN: Cannot import pyzbar module. This problem is probably due to the missing zbar shared library. (The zbar library is optional.)
|
||||
See in README.md for the installation of the zbar shared library.
|
||||
Exception: {e}\n""", file=sys.stderr)
|
||||
zbar_available = False
|
||||
if debug_mode:
|
||||
raise e
|
||||
|
||||
# Types
|
||||
# workaround for PYTHON <= 3.9: Final[tuple[int]]
|
||||
ColorBGR = Tuple[int, int, int] # RGB Color specified as Blue, Green, Red
|
||||
Point = Tuple[int, int]
|
||||
|
||||
# CV2 camera capture constants
|
||||
FONT: Final[int] = cv2.FONT_HERSHEY_PLAIN
|
||||
FONT_SCALE: Final[float] = 1.3
|
||||
FONT_THICKNESS: Final[int] = 1
|
||||
FONT_LINE_STYLE: Final[int] = cv2.LINE_AA
|
||||
FONT_COLOR: Final[ColorBGR] = 255, 0, 0
|
||||
BOX_THICKNESS: Final[int] = 5
|
||||
WINDOW_X: Final[int] = 0
|
||||
WINDOW_Y: Final[int] = 1
|
||||
WINDOW_WIDTH: Final[int] = 2
|
||||
WINDOW_HEIGHT: Final[int] = 3
|
||||
TEXT_WIDTH: Final[int] = 0
|
||||
TEXT_HEIGHT: Final[int] = 1
|
||||
BORDER: Final[int] = 5
|
||||
START_Y: Final[int] = 20
|
||||
START_POS_TEXT: Final[Point] = BORDER, START_Y
|
||||
NORMAL_COLOR: Final[ColorBGR] = 255, 0, 255
|
||||
SUCCESS_COLOR: Final[ColorBGR] = 0, 255, 0
|
||||
FAILURE_COLOR: Final[ColorBGR] = 0, 0, 255
|
||||
CHAR_DX: Final[int] = (lambda text: cv2.getTextSize(text, FONT, FONT_SCALE, FONT_THICKNESS)[0][TEXT_WIDTH] // len(text))("28 QR codes capturedMMM")
|
||||
FONT_DY: Final[int] = cv2.getTextSize("M", FONT, FONT_SCALE, FONT_THICKNESS)[0][TEXT_HEIGHT] + 5
|
||||
WINDOW_NAME: Final[str] = "Extract OTP Secrets: Capture QR Codes from Camera"
|
||||
|
||||
TextPosition = Enum('TextPosition', ['LEFT', 'RIGHT'])
|
||||
|
||||
cv2_available = True
|
||||
except ImportError as e:
|
||||
cv2_available = False
|
||||
if debug_mode:
|
||||
raise e
|
||||
|
||||
# Workaround for PYTHON <= 3.9: Generally Union[int, None] used instead of int | None
|
||||
|
||||
# Types
|
||||
Args = argparse.Namespace
|
||||
OtpUrl = str
|
||||
# Workaround for PYTHON <= 3.9: Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': int | None, 'url': OtpUrl})
|
||||
Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': Union[int, None], 'url': OtpUrl})
|
||||
# workaround for PYTHON <= 3.9: Otps = list[Otp]
|
||||
Otps = List[Otp]
|
||||
# workaround for PYTHON <= 3.9: OtpUrls = list[OtpUrl]
|
||||
OtpUrls = List[OtpUrl]
|
||||
|
||||
QRMode = Enum('QRMode', ['ZBAR', 'QREADER', 'QREADER_DEEP', 'CV2', 'CV2_WECHAT'], start=0)
|
||||
LogLevel = IntEnum('LogLevel', ['QUIET', 'NORMAL', 'VERBOSE', 'MORE_VERBOSE', 'DEBUG'], start=-1)
|
||||
|
||||
|
||||
# Constants
|
||||
CAMERA: Final[str] = 'camera'
|
||||
CV2_QRMODES: List[str] = [QRMode.CV2.name, QRMode.CV2_WECHAT.name]
|
||||
|
||||
# Global variable declaration
|
||||
verbose: IntEnum = LogLevel.NORMAL
|
||||
colored: bool = True
|
||||
executable: bool = False
|
||||
__version__: str
|
||||
tk_root: tkinter.Tk
|
||||
|
||||
|
||||
def sys_main() -> None:
|
||||
main(sys.argv[1:])
|
||||
|
||||
|
||||
def main(sys_args: list[str]) -> None:
|
||||
global executable, tk_root, headless
|
||||
# allow to use sys.stdout with with (avoid closing)
|
||||
sys.stdout.close = lambda: None # type: ignore
|
||||
# set encoding to utf-8, needed for Windows
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding='utf-8') # type: ignore
|
||||
sys.stderr.reconfigure(encoding='utf-8') # type: ignore
|
||||
except AttributeError: # '_io.StringIO' object has no attribute 'reconfigure'
|
||||
# StringIO in tests do not have all attributes, ignore it
|
||||
pass
|
||||
|
||||
# https://pyinstaller.org/en/stable/runtime-information.html#run-time-information
|
||||
executable = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
|
||||
|
||||
if cv2_available and not headless:
|
||||
try:
|
||||
tk_root = tkinter.Tk()
|
||||
tk_root.withdraw()
|
||||
except tkinter.TclError:
|
||||
headless = True
|
||||
|
||||
args = parse_args(sys_args)
|
||||
|
||||
if colored:
|
||||
colorama.just_fix_windows_console()
|
||||
if verbose >= LogLevel.DEBUG:
|
||||
print(f"Version: {get_full_version()}\n")
|
||||
if args.debug:
|
||||
sys.exit(0 if do_debug_checks() else 1)
|
||||
|
||||
otps = extract_otps(args)
|
||||
|
||||
write_csv(args.csv, otps)
|
||||
write_keepass_csv(args.keepass, otps)
|
||||
write_json(args.json, otps)
|
||||
write_txt(args.txt, otps, True)
|
||||
write_urls(args.urls, 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(otp_url)
|
||||
if args.saveqr:
|
||||
save_qr_image(otp, args.saveqr, len(otps))
|
||||
if not quiet:
|
||||
print()
|
||||
elif args.ignore and not quiet:
|
||||
eprint(f"Ignored duplicate otp: {otp['name']}", f" / {otp['issuer']}\n" if otp['issuer'] else '\n', sep='')
|
||||
|
||||
return new_otps_count
|
||||
|
||||
|
||||
def parse_args(sys_args: list[str]) -> Args:
|
||||
global verbose, quiet, colored
|
||||
|
||||
cmd = f"python {name}" if (name := os.path.basename(sys.argv[0])).endswith('.py') else f"{name}"
|
||||
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
|
||||
if cv2_available:
|
||||
description_text += "\nIf no infiles are provided, a GUI window starts and QR codes are captured from the camera."
|
||||
example_text = f"""examples:
|
||||
{cmd}
|
||||
{cmd} example_*.txt
|
||||
{cmd} - < example_export.txt
|
||||
{cmd} --csv - example_*.png | tail -n+2
|
||||
{cmd} = < example_export.png"""
|
||||
|
||||
arg_parser = argparse.ArgumentParser(formatter_class=lambda prog: argparse.RawTextHelpFormatter(prog, max_help_position=32),
|
||||
description=description_text,
|
||||
epilog=example_text)
|
||||
arg_parser.add_argument('infile', help="""a) file or - for stdin with 'otpauth-migration://...' URLs separated by newlines, lines starting with # are ignored;
|
||||
b) image file containing a QR code or = for stdin for an image containing a QR code""", nargs='*' if cv2_available else '+')
|
||||
arg_parser.add_argument('--csv', '-c', help='export csv file, or - for stdout', metavar=('FILE'))
|
||||
arg_parser.add_argument('--keepass', '-k', help='export totp/hotp csv file(s) for KeePass, - for stdout', metavar=('FILE'))
|
||||
arg_parser.add_argument('--json', '-j', help='export json file or - for stdout', metavar=('FILE'))
|
||||
arg_parser.add_argument('--txt', '-t', help='export txt file or - for stdout', metavar=('FILE'))
|
||||
arg_parser.add_argument('--urls', '-u', help='export file with list of otpauth urls, or - for stdout', metavar=('FILE'))
|
||||
arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal', action='store_true')
|
||||
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to directory', metavar=('DIR'))
|
||||
if cv2_available:
|
||||
arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, metavar=('NUMBER'))
|
||||
if not zbar_available:
|
||||
arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.CV2.name})', type=str, choices=[QRMode.CV2.name, QRMode.CV2_WECHAT.name], default=QRMode.CV2.name)
|
||||
else:
|
||||
arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.ZBAR.name})', type=str, choices=[mode.name for mode in QRMode], default=QRMode.ZBAR.name)
|
||||
arg_parser.add_argument('-i', '--ignore', help='ignore duplicate otps', action='store_true')
|
||||
arg_parser.add_argument('--no-color', '-n', help='do not use ANSI colors in console output', action='store_true')
|
||||
arg_parser.add_argument('--version', '-V', help='print version and quit', action=PrintVersionAction)
|
||||
output_group = arg_parser.add_mutually_exclusive_group()
|
||||
output_group.add_argument('-d', '--debug', help='enter debug mode, do checks and quit', action='count')
|
||||
output_group.add_argument('-v', '--verbose', help='verbose output', action='count')
|
||||
output_group.add_argument('-q', '--quiet', help='no stdout output, except output set by -', action='store_true')
|
||||
args = arg_parser.parse_args(sys_args)
|
||||
colored = not args.no_color
|
||||
if args.csv == '-' or args.json == '-' or args.keepass == '-' or args.txt == '-' or args.urls == '-':
|
||||
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')
|
||||
log_debug(args)
|
||||
quiet = True if args.quiet else False
|
||||
if verbose: print(f"QReader installed: {cv2_available}")
|
||||
if cv2_available:
|
||||
if verbose >= LogLevel.VERBOSE: print(f"CV2 version: {cv2.__version__}")
|
||||
if verbose: print(f"QR reading mode: {args.qr}\n")
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def extract_otps(args: Args) -> Otps:
|
||||
if not args.infile:
|
||||
return extract_otps_from_camera(args)
|
||||
else:
|
||||
return extract_otps_from_files(args)
|
||||
|
||||
|
||||
def extract_otps_from_camera(args: Args) -> Otps:
|
||||
if verbose: print("Capture QR codes from camera")
|
||||
otp_urls: OtpUrls = []
|
||||
otps: Otps = []
|
||||
|
||||
qr_mode = QRMode[args.qr]
|
||||
|
||||
cam = cv2.VideoCapture(args.camera)
|
||||
cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_AUTOSIZE)
|
||||
|
||||
if zbar_available:
|
||||
qreader = QReader()
|
||||
cv2_qr = cv2.QRCodeDetector()
|
||||
cv2_qr_wechat = cv2.wechat_qrcode.WeChatQRCode()
|
||||
while True:
|
||||
success, img = cam.read()
|
||||
new_otps_count = 0
|
||||
if not success:
|
||||
log_error("Failed to capture image from camera")
|
||||
break
|
||||
try:
|
||||
if qr_mode in [QRMode.QREADER, QRMode.QREADER_DEEP]:
|
||||
found, bbox = qreader.detect(img)
|
||||
if qr_mode == QRMode.QREADER_DEEP:
|
||||
otp_url = qreader.detect_and_decode(img, True)
|
||||
elif qr_mode == QRMode.QREADER:
|
||||
otp_url = qreader.decode(img, bbox) if found else None
|
||||
if otp_url:
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
if found:
|
||||
cv2_draw_box(img, [(bbox[0], bbox[1]), (bbox[2], bbox[1]), (bbox[2], bbox[3]), (bbox[0], bbox[3])], get_color(new_otps_count, otp_url))
|
||||
elif qr_mode == QRMode.ZBAR:
|
||||
for qrcode in zbar.decode(img, symbols=[zbar.ZBarSymbol.QRCODE]):
|
||||
otp_url = qrcode.data.decode('utf-8')
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
cv2_draw_box(img, [qrcode.polygon], get_color(new_otps_count, otp_url))
|
||||
elif qr_mode in [QRMode.CV2, QRMode.CV2_WECHAT]:
|
||||
if QRMode.CV2:
|
||||
otp_url, raw_pts, _ = cv2_qr.detectAndDecode(img)
|
||||
else:
|
||||
otp_url, raw_pts = cv2_qr_wechat.detectAndDecode(img) # type: ignore # use proper cv2 types
|
||||
if raw_pts is not None:
|
||||
if otp_url:
|
||||
new_otps_count = extract_otps_from_otp_url(otp_url, otp_urls, otps, args)
|
||||
cv2_draw_box(img, raw_pts, get_color(new_otps_count, otp_url))
|
||||
else:
|
||||
abort(f"Invalid QReader mode: {qr_mode.name}")
|
||||
except Exception as e:
|
||||
log_error(f'An error occured during QR detection and decoding for QR reader {qr_mode}. Changed to the next QR reader.', e)
|
||||
qr_mode = next_qr_mode(qr_mode)
|
||||
continue
|
||||
|
||||
cv2_print_text(img, f"Mode: {qr_mode.name} (Hit SPACE to change)", 0, TextPosition.LEFT, FONT_COLOR, 20)
|
||||
cv2_print_text(img, "Press ESC to quit", 1, TextPosition.LEFT, FONT_COLOR, 17)
|
||||
cv2_print_text(img, "Press c,j,k,t,u to save as csv/json/keepass/txt/urls file", 2, TextPosition.LEFT, FONT_COLOR, None)
|
||||
|
||||
cv2_print_text(img, f"{len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} captured", 0, TextPosition.RIGHT, FONT_COLOR)
|
||||
cv2_print_text(img, f"{len(otps)} otp{'s'[:len(otps) != 1]} extracted", 1, TextPosition.RIGHT, FONT_COLOR)
|
||||
|
||||
cv2.imshow(WINDOW_NAME, img)
|
||||
|
||||
quit, qr_mode = cv2_handle_pressed_keys(qr_mode, otps)
|
||||
if quit:
|
||||
break
|
||||
|
||||
cam.release()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
return otps
|
||||
|
||||
|
||||
def get_color(new_otps_count: int, otp_url: str) -> ColorBGR:
|
||||
if new_otps_count:
|
||||
return SUCCESS_COLOR
|
||||
else:
|
||||
if otp_url:
|
||||
return FAILURE_COLOR
|
||||
else:
|
||||
return NORMAL_COLOR
|
||||
|
||||
|
||||
# TODO use proper cv2 types if available
|
||||
def cv2_draw_box(img: cv2.typing.MatLike, raw_pts: cv2.typing.MatLike | list[tuple[Any, Any]], color: ColorBGR) -> np.ndarray[Any, np.dtype[np.int32]]:
|
||||
pts = np.array([raw_pts], np.int32)
|
||||
pts = pts.reshape((-1, 1, 2))
|
||||
cv2.polylines(img, [pts], True, color, BOX_THICKNESS)
|
||||
return pts
|
||||
|
||||
|
||||
def cv2_print_text(img: cv2.typing.MatLike, text: str, line_number: int, position: TextPosition, color: ColorBGR, opposite_len: Optional[int] = None) -> None:
|
||||
window_dim = cv2.getWindowImageRect(WINDOW_NAME)
|
||||
out_text = text
|
||||
if opposite_len:
|
||||
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
|
||||
actual_width = text_dim[TEXT_WIDTH] + opposite_len * CHAR_DX + 4 * BORDER
|
||||
if actual_width >= window_dim[WINDOW_WIDTH]:
|
||||
out_text = out_text[:(window_dim[WINDOW_WIDTH] - actual_width) // CHAR_DX] + '.'
|
||||
text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS)
|
||||
if position == TextPosition.LEFT:
|
||||
pos = BORDER, START_Y + line_number * FONT_DY
|
||||
else:
|
||||
pos = window_dim[WINDOW_WIDTH] - text_dim[TEXT_WIDTH] - BORDER, START_Y + line_number * FONT_DY
|
||||
|
||||
cv2.putText(img, out_text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||
|
||||
|
||||
def cv2_handle_pressed_keys(qr_mode: QRMode, otps: Otps) -> Tuple[bool, QRMode]:
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
quit = False
|
||||
if key == 27 or key == ord('q') or key == ord('Q') or key == 13:
|
||||
# ESC or Enter or q pressed
|
||||
quit = True
|
||||
elif (key == ord('c') or key == ord('C')) and is_not_headless():
|
||||
if has_no_otps_show_warning(otps):
|
||||
pass
|
||||
else:
|
||||
file_name = tkinter.filedialog.asksaveasfilename(
|
||||
title="Save extracted otp secrets as CSV",
|
||||
defaultextension='.csv',
|
||||
filetypes=[('CSV', '*.csv'), ('All', '*.*')]
|
||||
)
|
||||
tk_root.update()
|
||||
if len(file_name) > 0:
|
||||
write_csv(file_name, otps)
|
||||
elif (key == ord('j') or key == ord('J')) and is_not_headless():
|
||||
if has_no_otps_show_warning(otps):
|
||||
pass
|
||||
else:
|
||||
file_name = tkinter.filedialog.asksaveasfilename(
|
||||
title="Save extracted otp secrets as JSON",
|
||||
defaultextension='.json',
|
||||
filetypes=[('JSON', '*.json'), ('All', '*.*')]
|
||||
)
|
||||
tk_root.update()
|
||||
if len(file_name) > 0:
|
||||
write_json(file_name, otps)
|
||||
elif (key == ord('k') or key == ord('K')) and is_not_headless():
|
||||
if has_no_otps_show_warning(otps):
|
||||
pass
|
||||
else:
|
||||
file_name = tkinter.filedialog.asksaveasfilename(
|
||||
title="Save extracted otp secrets as KeePass CSV file(s)",
|
||||
defaultextension='.csv',
|
||||
filetypes=[('CSV', '*.csv'), ('All', '*.*')]
|
||||
)
|
||||
tk_root.update()
|
||||
if len(file_name) > 0:
|
||||
write_keepass_csv(file_name, otps)
|
||||
elif (key == ord('t') or key == ord('T')) and is_not_headless():
|
||||
if has_no_otps_show_warning(otps):
|
||||
pass
|
||||
else:
|
||||
file_name = tkinter.filedialog.asksaveasfilename(
|
||||
title="Save extracted otp secrets as text",
|
||||
defaultextension='.txt',
|
||||
filetypes=[('Text', '*.txt'), ('All', '*.*')]
|
||||
)
|
||||
tk_root.update()
|
||||
if len(file_name) > 0:
|
||||
write_txt(file_name, otps, True)
|
||||
elif (key == ord('u') or key == ord('U')) and is_not_headless():
|
||||
if has_no_otps_show_warning(otps):
|
||||
pass
|
||||
else:
|
||||
file_name = tkinter.filedialog.asksaveasfilename(
|
||||
title="Save extracted otp secrets as list of urls",
|
||||
defaultextension='.txt',
|
||||
filetypes=[('Text', '*.txt'), ('All', '*.*')]
|
||||
)
|
||||
tk_root.update()
|
||||
if len(file_name) > 0:
|
||||
write_urls(file_name, otps)
|
||||
elif key == 32:
|
||||
qr_mode = next_valid_qr_mode(qr_mode, zbar_available)
|
||||
if verbose >= LogLevel.MORE_VERBOSE: print(f"QR reading mode: {qr_mode}")
|
||||
if cv2.getWindowProperty(WINDOW_NAME, cv2.WND_PROP_VISIBLE) < 1:
|
||||
# Window close clicked
|
||||
quit = True
|
||||
return quit, qr_mode
|
||||
|
||||
|
||||
def extract_otps_from_otp_url(otp_url: str, otp_urls: OtpUrls, otps: Otps, args: Args) -> int:
|
||||
'''Returns -1 if opt_url was already added.'''
|
||||
if otp_url and verbose >= LogLevel.VERBOSE: print(otp_url)
|
||||
if not otp_url:
|
||||
return 0
|
||||
if otp_url not in otp_urls:
|
||||
new_otps_count = extract_otp_from_otp_url(otp_url, otps, len(otp_urls), CAMERA, args)
|
||||
if new_otps_count:
|
||||
otp_urls.append(otp_url)
|
||||
if verbose: print(f"Extracted {new_otps_count} otp{'s'[:len(otps) != 1]}. {len(otps)} otp{'s'[:len(otps) != 1]} from {len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} extracted")
|
||||
return new_otps_count
|
||||
return -1
|
||||
|
||||
|
||||
def extract_otps_from_files(args: Args) -> Otps:
|
||||
otps: Otps = []
|
||||
|
||||
files_count = urls_count = otps_count = 0
|
||||
if verbose: print(f"Input files: {args.infile}")
|
||||
for infile in args.infile:
|
||||
if verbose >= LogLevel.MORE_VERBOSE: log_verbose(f"Processing infile {infile}")
|
||||
files_count += 1
|
||||
for line in get_otp_urls_from_file(infile, args):
|
||||
if verbose >= LogLevel.MORE_VERBOSE: log_verbose(line)
|
||||
if line.startswith('#') or line == '': continue
|
||||
urls_count += 1
|
||||
otps_count += extract_otp_from_otp_url(line, otps, urls_count, infile, args)
|
||||
if verbose: print(f"Extracted {otps_count} otp{'s'[:otps_count != 1]} from {urls_count} otp url{'s'[:urls_count != 1]} by reading {files_count} infile{'s'[:files_count != 1]}")
|
||||
return otps
|
||||
|
||||
|
||||
def get_otp_urls_from_file(filename: str, args: Args) -> OtpUrls:
|
||||
# stdin stream cannot be rewinded, thus distinguish, use - for utf-8 stdin and = for binary image stdin
|
||||
if filename != '=':
|
||||
check_file_exists(filename)
|
||||
lines = read_lines_from_text_file(filename)
|
||||
if lines or filename == '-':
|
||||
return lines
|
||||
|
||||
# could not process text file, try reading as image
|
||||
if filename != '-' and cv2_available:
|
||||
return convert_img_to_otp_urls(filename, args)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def read_lines_from_text_file(filename: str) -> list[str]:
|
||||
if verbose >= LogLevel.DEBUG: print(f"Reading lines of {filename}")
|
||||
# workaround for PYTHON <= 3.9 support encoding
|
||||
if sys.version_info >= (3, 10):
|
||||
finput = fileinput.input(filename, encoding='utf-8')
|
||||
else:
|
||||
finput = fileinput.input(filename)
|
||||
try:
|
||||
lines = []
|
||||
for line in (line.strip() for line in finput):
|
||||
if verbose >= LogLevel.DEBUG: log_verbose(line)
|
||||
if is_binary(line):
|
||||
abort("Binary input was given in stdin, please use = instead of - as infile argument for images.")
|
||||
# unfortunately yield line leads to random test fails
|
||||
lines.append(line)
|
||||
if not lines:
|
||||
log_warn(f"{filename.replace('-', 'stdin')} is empty")
|
||||
except UnicodeDecodeError as e:
|
||||
if filename == '-':
|
||||
abort("Unable to open text file form stdin. "
|
||||
"In case you want read an image file from stdin, you must use '=' instead of '-'.", e)
|
||||
else: # The file is probably an image, process below
|
||||
return []
|
||||
finally:
|
||||
finput.close()
|
||||
return lines
|
||||
|
||||
|
||||
def convert_img_to_otp_urls(filename: str, args: Args) -> OtpUrls:
|
||||
if verbose: print(f"Reading image {filename}")
|
||||
try:
|
||||
if filename != '=':
|
||||
img = cv2.imread(filename)
|
||||
else:
|
||||
try:
|
||||
stdin = sys.stdin.buffer.read()
|
||||
except AttributeError:
|
||||
# Workaround for pytest, since pytest cannot monkeypatch sys.stdin.buffer
|
||||
stdin = sys.stdin.read() # type: ignore # Workaround for pytest fixtures
|
||||
if not stdin:
|
||||
log_warn("stdin is empty")
|
||||
try:
|
||||
img_array = np.frombuffer(stdin, dtype='uint8')
|
||||
except TypeError as e:
|
||||
abort("Cannot read binary stdin buffer.", e)
|
||||
if not img_array.size:
|
||||
return []
|
||||
img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
|
||||
|
||||
if img is None:
|
||||
abort(f"Unable to open file for reading.\ninput file: {filename}")
|
||||
|
||||
qr_mode = QRMode[args.qr]
|
||||
otp_urls = decode_qr_img_otp_urls(img, qr_mode)
|
||||
if len(otp_urls) == 0:
|
||||
abort(f"Unable to read QR Code from file.\ninput file: {filename}")
|
||||
except Exception as e:
|
||||
abort(f"Encountered exception\ninput file: {filename}", e)
|
||||
return otp_urls
|
||||
|
||||
|
||||
def decode_qr_img_otp_urls(img: Any, qr_mode: QRMode) -> OtpUrls:
|
||||
otp_urls: OtpUrls = []
|
||||
if qr_mode in [QRMode.QREADER, QRMode.QREADER_DEEP]:
|
||||
otp_url = QReader().detect_and_decode(img, qr_mode == QRMode.QREADER_DEEP)
|
||||
otp_urls.append(otp_url)
|
||||
elif qr_mode == QRMode.CV2:
|
||||
otp_url, _, _ = cv2.QRCodeDetector().detectAndDecode(img)
|
||||
otp_urls.append(otp_url)
|
||||
elif qr_mode == QRMode.CV2_WECHAT:
|
||||
otp_url, _ = cv2.wechat_qrcode.WeChatQRCode().detectAndDecode(img)
|
||||
otp_urls += list(otp_url)
|
||||
elif qr_mode == QRMode.ZBAR:
|
||||
qrcodes = zbar.decode(img, symbols=[zbar.ZBarSymbol.QRCODE])
|
||||
otp_urls += [qrcode.data.decode('utf-8') for qrcode in qrcodes]
|
||||
else:
|
||||
assert False, f"Wrong QReader mode {qr_mode.name}"
|
||||
|
||||
return otp_urls
|
||||
|
||||
|
||||
def is_opt_url(otp_url: str, source: str) -> bool:
|
||||
if not otp_url.startswith('otpauth-migration://'):
|
||||
msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}"
|
||||
if source == CAMERA:
|
||||
log_warn(f"{msg}")
|
||||
return False
|
||||
else:
|
||||
log_warn(f"{msg}\nMaybe a wrong file was given")
|
||||
return True
|
||||
|
||||
|
||||
# https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf
|
||||
def get_enum_name_by_number(parent: Any, field_name: str) -> str:
|
||||
field_value = getattr(parent, field_name)
|
||||
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name # type: ignore # generic code
|
||||
|
||||
|
||||
def get_otp_type_str_from_code(otp_type: int) -> str:
|
||||
return 'totp' if otp_type == 2 else 'hotp'
|
||||
|
||||
|
||||
def convert_secret_from_bytes_to_base32_str(bytes: bytes) -> str:
|
||||
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
|
||||
|
||||
|
||||
def build_otp_url(secret: str, raw_otp: pb.MigrationPayload.OtpParameters) -> str:
|
||||
url_params = {'secret': secret}
|
||||
if raw_otp.type == 1: url_params['counter'] = str(raw_otp.counter)
|
||||
if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
|
||||
otp_url = f"otpauth://{get_otp_type_str_from_code(raw_otp.type)}/{urlparse.quote(raw_otp.name)}?" + urlparse.urlencode(url_params)
|
||||
return otp_url
|
||||
|
||||
|
||||
def print_otp(otp: Otp, out: Optional[TextIO] = None) -> None:
|
||||
print(f"Name: {otp['name']}", file=out)
|
||||
print(f"Secret: {otp['secret']}", file=out)
|
||||
if otp['issuer']: print(f"Issuer: {otp['issuer']}", file=out)
|
||||
print(f"Type: {otp['type']}", file=out)
|
||||
if otp['type'] == 'hotp':
|
||||
print(f"Counter: {otp['counter']}", file=out)
|
||||
if verbose:
|
||||
print(otp['url'], file=out)
|
||||
|
||||
|
||||
def write_url(otp: Otp, out: Optional[TextIO] = None) -> None:
|
||||
print(otp['url'], file=out)
|
||||
|
||||
|
||||
def save_qr_image(otp: Otp, dir: str, j: int) -> str:
|
||||
if not (os.path.exists(dir)): os.makedirs(dir, exist_ok=True)
|
||||
pattern = re.compile(r'[\W_]+')
|
||||
file_otp_name = pattern.sub('', otp['name'])
|
||||
file_otp_issuer = pattern.sub('', otp['issuer'])
|
||||
save_qr_image_file(otp['url'], f"{dir}/{j}-{file_otp_name}{'-' + file_otp_issuer if file_otp_issuer else ''}.png")
|
||||
return file_otp_name
|
||||
|
||||
|
||||
def save_qr_image_file(otp_url: OtpUrl, name: str) -> None:
|
||||
qr = QRCode()
|
||||
qr.add_data(otp_url)
|
||||
img = qr.make_image(fill_color='black', back_color='white')
|
||||
if verbose: print(f"Saving to {name}")
|
||||
img.save(name)
|
||||
|
||||
|
||||
def print_qr(otp_url: str, out: Optional[TextIO] = None) -> None:
|
||||
qr = QRCode()
|
||||
qr.add_data(otp_url)
|
||||
qr.print_ascii(out)
|
||||
|
||||
|
||||
def write_txt(file: str, otps: Otps, write_qr: bool = False) -> None:
|
||||
if file and len(file) > 0 and len(otps) > 0:
|
||||
with open_file_or_stdout(file) as outfile:
|
||||
for otp in otps:
|
||||
print_otp(otp, outfile)
|
||||
if write_qr:
|
||||
print_qr(otp['url'], outfile)
|
||||
print(file=outfile)
|
||||
|
||||
|
||||
def write_urls(file: str, otps: Otps) -> None:
|
||||
if file and len(file) > 0 and len(otps) > 0:
|
||||
with open_file_or_stdout(file) as outfile:
|
||||
for otp in otps:
|
||||
write_url(otp, outfile)
|
||||
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to otpauth url list file {file}")
|
||||
|
||||
|
||||
def write_csv(file: str, otps: Otps) -> None:
|
||||
if file and len(file) > 0 and len(otps) > 0:
|
||||
with open_file_or_stdout_for_csv(file) as outfile:
|
||||
writer = csv.DictWriter(outfile, otps[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(otps)
|
||||
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to csv {file}")
|
||||
|
||||
|
||||
def write_keepass_csv(file: str, otps: Otps) -> None:
|
||||
if file and len(file) > 0 and len(otps) > 0:
|
||||
has_totp = has_otp_type(otps, 'totp')
|
||||
has_hotp = has_otp_type(otps, 'hotp')
|
||||
if file != '-':
|
||||
otp_filename_totp = file if has_totp != has_hotp else add_pre_suffix(file, "totp")
|
||||
otp_filename_hotp = file if has_totp != has_hotp else add_pre_suffix(file, "hotp")
|
||||
else:
|
||||
otp_filename_totp = otp_filename_hotp = '-'
|
||||
if has_totp:
|
||||
count_totp_entries = write_keepass_totp_csv(otp_filename_totp, otps)
|
||||
if has_hotp:
|
||||
count_hotp_entries = write_keepass_htop_csv(otp_filename_hotp, otps)
|
||||
if not quiet:
|
||||
if has_totp and count_totp_entries: print(f"Exported {count_totp_entries} totp entrie{'s'[:count_totp_entries != 1]} to keepass csv file {otp_filename_totp}")
|
||||
if has_hotp and count_hotp_entries: print(f"Exported {count_hotp_entries} hotp entrie{'s'[:count_hotp_entries != 1]} to keepass csv file {otp_filename_hotp}")
|
||||
|
||||
|
||||
def write_keepass_totp_csv(file: str, otps: Otps) -> int:
|
||||
count_entries = 0
|
||||
with open_file_or_stdout_for_csv(file) as outfile:
|
||||
writer = csv.DictWriter(outfile, ["Title", "User Name", "TimeOtp-Secret-Base32", "Group"])
|
||||
writer.writeheader()
|
||||
for otp in otps:
|
||||
if otp['type'] == 'totp':
|
||||
writer.writerow({
|
||||
'Title': otp['issuer'],
|
||||
'User Name': otp['name'],
|
||||
'TimeOtp-Secret-Base32': otp['secret'] if otp['type'] == 'totp' else None,
|
||||
'Group': f"OTP/{otp['type'].upper()}"
|
||||
})
|
||||
count_entries += 1
|
||||
return count_entries
|
||||
|
||||
|
||||
def write_keepass_htop_csv(file: str, otps: Otps) -> int:
|
||||
count_entries = 0
|
||||
with open_file_or_stdout_for_csv(file) as outfile:
|
||||
writer = csv.DictWriter(outfile, ["Title", "User Name", "HmacOtp-Secret-Base32", "HmacOtp-Counter", "Group"])
|
||||
writer.writeheader()
|
||||
for otp in otps:
|
||||
if otp['type'] == 'hotp':
|
||||
writer.writerow({
|
||||
'Title': otp['issuer'],
|
||||
'User Name': otp['name'],
|
||||
'HmacOtp-Secret-Base32': otp['secret'] if otp['type'] == 'hotp' else None,
|
||||
'HmacOtp-Counter': otp['counter'] if otp['type'] == 'hotp' else None,
|
||||
'Group': f"OTP/{otp['type'].upper()}"
|
||||
})
|
||||
count_entries += 1
|
||||
return count_entries
|
||||
|
||||
|
||||
def write_json(file: str, otps: Otps) -> None:
|
||||
if file and len(file) > 0:
|
||||
with open_file_or_stdout(file) as outfile:
|
||||
json.dump(otps, outfile, indent=4)
|
||||
if not quiet: print(f"Exported {len(otps)} otp{'s'[:len(otps) != 1]} to json {file}")
|
||||
|
||||
|
||||
def has_otp_type(otps: Otps, otp_type: str) -> bool:
|
||||
for otp in otps:
|
||||
if otp['type'] == otp_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def add_pre_suffix(file: str, pre_suffix: str) -> str:
|
||||
'''filename.ext, pre -> filename.pre.ext'''
|
||||
name, ext = os.path.splitext(file)
|
||||
return name + "." + pre_suffix + (ext if ext else "")
|
||||
|
||||
|
||||
def open_file_or_stdout(filename: str) -> TextIO:
|
||||
'''stdout is denoted as "-".
|
||||
Note: Set before the following line:
|
||||
sys.stdout.close = lambda: None'''
|
||||
return open(filename, "w", encoding='utf-8') if filename != '-' else sys.stdout
|
||||
|
||||
|
||||
def open_file_or_stdout_for_csv(filename: str) -> TextIO:
|
||||
'''stdout is denoted as "-".
|
||||
newline=''
|
||||
Note: Set before the following line:
|
||||
sys.stdout.close = lambda: None'''
|
||||
return open(filename, "w", encoding='utf-8', newline='') if filename != '-' else sys.stdout
|
||||
|
||||
|
||||
def check_file_exists(filename: str) -> None:
|
||||
if filename != '-' and not os.path.isfile(filename):
|
||||
abort(f"Input file provided is non-existent or not a file."
|
||||
f"\ninput file: {filename}")
|
||||
|
||||
|
||||
def has_no_otps_show_warning(otps: Otps) -> bool:
|
||||
if len(otps) == 0:
|
||||
tkinter.messagebox.showinfo(title="No data", message="There are no otp secrets to write")
|
||||
tk_root.update() # dispose dialog
|
||||
return len(otps) == 0
|
||||
|
||||
|
||||
def is_binary(line: str) -> bool:
|
||||
try:
|
||||
line.startswith('#')
|
||||
return False
|
||||
except (UnicodeDecodeError, AttributeError, TypeError):
|
||||
return True
|
||||
|
||||
|
||||
def next_valid_qr_mode(qr_mode: QRMode, with_zbar: bool = True) -> QRMode:
|
||||
ok = False
|
||||
while not ok:
|
||||
qr_mode = next_qr_mode(qr_mode)
|
||||
ok = True if with_zbar else qr_mode.name in CV2_QRMODES
|
||||
return qr_mode
|
||||
|
||||
|
||||
def next_qr_mode(qr_mode: QRMode) -> QRMode:
|
||||
return QRMode((qr_mode.value + 1) % len(QRMode))
|
||||
|
||||
|
||||
def do_debug_checks() -> bool:
|
||||
log_debug('Do debug checks')
|
||||
log_debug('Try: import cv2')
|
||||
import cv2 # noqa: F401 # This is only a debug import
|
||||
log_debug('Try: import numpy as np')
|
||||
import numpy as np # noqa: F401 # This is only a debug import
|
||||
log_debug('Try: import pyzbar.pyzbar as zbar')
|
||||
import pyzbar.pyzbar as zbar # noqa: F401 # This is only a debug import
|
||||
log_debug('Try: from qreader import QReader')
|
||||
from qreader import QReader # noqa: F401 # This is only a debug import
|
||||
print(color('\nDebug checks passed', colorama.Fore.GREEN))
|
||||
return True
|
||||
|
||||
|
||||
def is_not_headless() -> bool:
|
||||
if headless:
|
||||
log_warn("Cannot open dialog in headless mode")
|
||||
return not headless
|
||||
|
||||
|
||||
class PrintVersionAction(argparse.Action):
|
||||
def __init__(self, option_strings: Sequence[str], dest: str, nargs: int = 0, **kwargs: Any) -> None:
|
||||
super().__init__(option_strings, dest, nargs, **kwargs)
|
||||
|
||||
def __call__(self, parser: argparse.ArgumentParser, namespace: Args, values: Union[str, Sequence[Any], None], option_string: Optional[str] = None) -> None:
|
||||
print_version()
|
||||
parser.exit()
|
||||
|
||||
|
||||
def print_version() -> None:
|
||||
print(get_full_version())
|
||||
|
||||
|
||||
def get_full_version() -> str:
|
||||
version = get_raw_version()
|
||||
meta = [
|
||||
platform.python_implementation()
|
||||
]
|
||||
if executable: meta.append('exe')
|
||||
meta.append(f"called as {'package' if __package__ else 'script'}")
|
||||
return (
|
||||
f"extract_otp_secrets {version} {platform.system()} {platform.machine()}"
|
||||
f" Python {platform.python_version()}"
|
||||
f" ({'/'.join(meta)})"
|
||||
)
|
||||
|
||||
|
||||
# https://setuptools-git-versioning.readthedocs.io/en/stable/runtime_version.html
|
||||
def get_raw_version() -> str:
|
||||
global __version__
|
||||
|
||||
try:
|
||||
__version__ = version("extract_otp_secrets")
|
||||
return __version__
|
||||
except PackageNotFoundError:
|
||||
# package is not installed
|
||||
return ''
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def log_debug(*values: object, sep: Optional[str] = ' ') -> None:
|
||||
if colored:
|
||||
print(f"{colorama.Fore.CYAN}\nDEBUG: {str(values[0])}", *values[1:], colorama.Fore.RESET, sep)
|
||||
else:
|
||||
print(f"\nDEBUG: {str(values[0])}", *values[1:], sep)
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def log_verbose(msg: str) -> None:
|
||||
print(color(msg, colorama.Fore.CYAN))
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def log_warn(msg: str, exception: Optional[BaseException] = None) -> None:
|
||||
exception_text = "\nException: "
|
||||
eprint(color(f"\nWARN: {msg}{(exception_text + str(exception)) if exception else ''}", colorama.Fore.RED))
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def log_error(msg: str, exception: Optional[BaseException] = None) -> None:
|
||||
exception_text = "\nException: "
|
||||
eprint(color(f"\nERROR: {msg}{(exception_text + str(exception)) if exception else ''}", colorama.Fore.RED))
|
||||
|
||||
|
||||
def color(msg: str, color: Optional[str] = None) -> str:
|
||||
return f"{color if colored and color else ''}{msg}{colorama.Fore.RESET if colored and color else ''}"
|
||||
|
||||
|
||||
def eprint(*values: object, **kwargs: Any) -> None:
|
||||
'''Print to stderr.'''
|
||||
print(*values, file=sys.stderr, **kwargs)
|
||||
|
||||
|
||||
# workaround for PYTHON <= 3.9 use: BaseException | None
|
||||
def abort(msg: str, exception: Optional[BaseException] = None) -> None:
|
||||
log_error(msg, exception)
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys_main()
|
@ -1,109 +0,0 @@
|
||||
"""
|
||||
@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.final
|
||||
class MigrationPayload(google.protobuf.message.Message):
|
||||
"""Copied from: https://github.com/beemdevelopment/Aegis/blob/master/app/src/main/proto/google_auth.proto"""
|
||||
|
||||
DESCRIPTOR: google.protobuf.descriptor.Descriptor
|
||||
|
||||
class _Algorithm:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _AlgorithmEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[MigrationPayload._Algorithm.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
ALGO_INVALID: MigrationPayload._Algorithm.ValueType # 0
|
||||
ALGO_SHA1: MigrationPayload._Algorithm.ValueType # 1
|
||||
|
||||
class Algorithm(_Algorithm, metaclass=_AlgorithmEnumTypeWrapper): ...
|
||||
ALGO_INVALID: MigrationPayload.Algorithm.ValueType # 0
|
||||
ALGO_SHA1: MigrationPayload.Algorithm.ValueType # 1
|
||||
|
||||
class _OtpType:
|
||||
ValueType = typing.NewType("ValueType", builtins.int)
|
||||
V: typing_extensions.TypeAlias = ValueType
|
||||
|
||||
class _OtpTypeEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[MigrationPayload._OtpType.ValueType], builtins.type):
|
||||
DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor
|
||||
OTP_INVALID: MigrationPayload._OtpType.ValueType # 0
|
||||
OTP_HOTP: MigrationPayload._OtpType.ValueType # 1
|
||||
OTP_TOTP: MigrationPayload._OtpType.ValueType # 2
|
||||
|
||||
class OtpType(_OtpType, metaclass=_OtpTypeEnumTypeWrapper): ...
|
||||
OTP_INVALID: MigrationPayload.OtpType.ValueType # 0
|
||||
OTP_HOTP: MigrationPayload.OtpType.ValueType # 1
|
||||
OTP_TOTP: MigrationPayload.OtpType.ValueType # 2
|
||||
|
||||
@typing.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.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
|
||||
version: builtins.int
|
||||
batch_size: builtins.int
|
||||
batch_index: builtins.int
|
||||
batch_id: builtins.int
|
||||
@property
|
||||
def otp_parameters(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___MigrationPayload.OtpParameters]: ...
|
||||
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.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
|
@ -0,0 +1,89 @@
|
||||
# 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
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_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. 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
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_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. 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
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret Key
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
@ -0,0 +1,216 @@
|
||||
# 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_file, remove_dir_with_files, read_file_to_str
|
||||
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_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: OTP_TOTP
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
|
||||
'''
|
||||
|
||||
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: OTP_TOTP
|
||||
|
||||
Name: SerenityLabs:test2@serenitylabs.co.uk
|
||||
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
|
||||
Name: SerenityLabs:test3@serenitylabs.co.uk
|
||||
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
|
||||
Name: SerenityLabs:test4@serenitylabs.co.uk
|
||||
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_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 cleanup():
|
||||
remove_file('test_example_output.csv')
|
||||
remove_file('test_example_output.json')
|
||||
remove_dir_with_files('testout/')
|
@ -0,0 +1,196 @@
|
||||
# Unit test for extract_otp_secret_keys.py
|
||||
|
||||
# Run tests:
|
||||
# python -m unittest
|
||||
|
||||
# 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 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
|
||||
|
||||
import extract_otp_secret_keys
|
||||
|
||||
|
||||
class TestExtract(unittest.TestCase):
|
||||
|
||||
def test_extract_csv(self):
|
||||
extract_otp_secret_keys.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'])
|
||||
|
||||
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):
|
||||
with Capturing() as output:
|
||||
extract_otp_secret_keys.main(['example_export.txt'])
|
||||
|
||||
expected_output = [
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Issuer: raspberrypi',
|
||||
'Type: OTP_TOTP',
|
||||
'',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: OTP_TOTP',
|
||||
'',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: OTP_TOTP',
|
||||
'',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Issuer: raspberrypi',
|
||||
'Type: OTP_TOTP',
|
||||
''
|
||||
]
|
||||
self.assertEqual(output, expected_output)
|
||||
|
||||
# Ref for capturing https://stackoverflow.com/a/40984270
|
||||
def test_extract_stdout_2(self):
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secret_keys.main(['example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = '''Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
|
||||
'''
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_not_encoded_plus(self):
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
||||
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
|
||||
Name: SerenityLabs:test2@serenitylabs.co.uk
|
||||
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
|
||||
Name: SerenityLabs:test3@serenitylabs.co.uk
|
||||
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
|
||||
Name: SerenityLabs:test4@serenitylabs.co.uk
|
||||
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
|
||||
'''
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_printqr(self):
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secret_keys.main(['-p', 'example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = read_file_to_str('test/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'])
|
||||
|
||||
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'))
|
||||
|
||||
def test_extract_verbose(self):
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secret_keys.main(['-v', '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')
|
||||
|
||||
self.assertGreater(len(actual_output), len(expected_stdout))
|
||||
self.assertTrue("DEBUG: " in actual_output)
|
||||
|
||||
def test_extract_help(self):
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
try:
|
||||
extract_otp_secret_keys.main(['-h'])
|
||||
except SystemExit:
|
||||
pass
|
||||
|
||||
actual_output = out.getvalue()
|
||||
|
||||
self.assertGreater(len(actual_output), 0)
|
||||
self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output)
|
||||
|
||||
def setUp(self):
|
||||
self.cleanup()
|
||||
|
||||
def tearDown(self):
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self):
|
||||
remove_file('test_example_output.csv')
|
||||
remove_file('test_example_output.json')
|
||||
remove_dir_with_files('testout/')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,21 +0,0 @@
|
||||
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])
|
@ -1,15 +0,0 @@
|
||||
# 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://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
|
@ -1,3 +0,0 @@
|
||||
# comment 1
|
||||
|
||||
# comment 2
|
@ -1,51 +0,0 @@
|
||||
QReader installed: True
|
||||
CV2 version: 4.7.0
|
||||
QR reading mode: ZBAR
|
||||
|
||||
Input files: ['example_export.txt']
|
||||
|
||||
1. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
2. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
3. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
5. Secret
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
|
||||
|
||||
6. Secret
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
Extracted 6 otps from 5 otp urls by reading 1 infile
|
@ -1,71 +0,0 @@
|
||||
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
|
@ -1,225 +0,0 @@
|
||||
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
|
@ -1,27 +0,0 @@
|
||||
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
|
||||
|
@ -1,51 +0,0 @@
|
||||
QReader installed: True
|
||||
CV2 version: 4.7.0
|
||||
QR reading mode: ZBAR
|
||||
|
||||
Input files: ['example_export.txt']
|
||||
|
||||
1. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
2. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
3. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
5. Secret
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
|
||||
|
||||
6. Secret
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
Extracted 6 otps from 5 otp urls by reading 1 infile
|
@ -1,27 +0,0 @@
|
||||
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
|
||||
|
Before Width: | Height: | Size: 478 B |
@ -1 +0,0 @@
|
||||
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
|
@ -1 +0,0 @@
|
||||
otpauth-migration://offline?data=XXXX
|
@ -1 +0,0 @@
|
||||
QR-Code:otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
Before Width: | Height: | Size: 653 KiB |
@ -1 +0,0 @@
|
||||
This is just a text file masquerading as an image file.
|
@ -1,6 +0,0 @@
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
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
|
@ -1,89 +0,0 @@
|
||||
# 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()
|
@ -1,237 +0,0 @@
|
||||
# Unit test for extract_otp_secrets.py
|
||||
|
||||
# Run tests:
|
||||
# python -m unittest
|
||||
|
||||
# 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 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) -> 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) -> 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) -> None:
|
||||
with Capturing() as output:
|
||||
extract_otp_secrets.main(['example_export.txt'])
|
||||
|
||||
expected_output = [
|
||||
'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',
|
||||
''
|
||||
]
|
||||
self.assertEqual(output, expected_output)
|
||||
|
||||
# Ref for capturing https://stackoverflow.com/a/40984270
|
||||
def test_extract_stdout_2(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secrets.main(['example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = '''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
|
||||
|
||||
'''
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_not_encoded_plus(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secrets.main(['tests/data/test_plus_problem_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = '''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
|
||||
|
||||
'''
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_printqr(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secrets.main(['-p', 'example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = read_file_to_str('tests/data/printqr_output.txt')
|
||||
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
||||
def test_extract_saveqr(self) -> None:
|
||||
extract_otp_secrets.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
|
||||
|
||||
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_debug(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
extract_otp_secrets.main(['-vvv', 'example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
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_1(self) -> None:
|
||||
out = io.StringIO()
|
||||
with redirect_stdout(out):
|
||||
try:
|
||||
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 "-v, --verbose" in actual_output)
|
||||
|
||||
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) -> None:
|
||||
self.cleanup()
|
||||
|
||||
def cleanup(self) -> None:
|
||||
remove_file('test_example_output.csv')
|
||||
remove_file('test_example_output.json')
|
||||
remove_dir_with_files('testout/')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
@ -1,140 +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/>.
|
||||
|
||||
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))])
|
@ -0,0 +1,174 @@
|
||||
#!/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"
|
||||
DEST="protoc"
|
||||
|
||||
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
|
||||
echo -e "\nUpgrade Protoc $VERSION\n"
|
||||
echo -e "Current version: $OLDVERSION\n"
|
||||
|
||||
if [ "$OLDVERSION" = "$VERSION" ] && $check_version; then
|
||||
echo -e "\nVersion has not changed. Quit"
|
||||
quit
|
||||
fi
|
||||
|
||||
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"
|
||||
|
||||
cmd="pip install -U -r requirements.txt"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
cmd="pytest"
|
||||
if $interactive ; then askContinueYn "$cmd"; fi
|
||||
eval "$cmd"
|
||||
|
||||
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"
|
||||
|
||||
quit
|
@ -0,0 +1,74 @@
|
||||
# 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
|
||||
|
||||
|
||||
# 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 remove_file(file):
|
||||
if os.path.isfile(file): os.remove(file)
|
||||
|
||||
|
||||
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))
|