Compare commits

...

51 Commits

Author SHA1 Message Date
scito
65c52f4d81 skip verbose tests for pypy 2022-12-19 22:37:23 +01:00
scito
8ba4439305 add utf-8 encoding tests 2022-12-19 22:37:23 +01:00
scito
10bc6959a3 add python to vscode recommended extensions 2022-12-19 17:48:17 +01:00
u231053
a51507b701 fix #30: enforce utf-8 encoding
since windows used non-utf8-encoding
2022-12-19 17:48:17 +01:00
scito
7af4017910 ci: daily tests and order python from new to old
- more recent version first
2022-12-19 00:11:30 +01:00
scito
7af631ff1e ci: setup testing for linux, macos and windows platforms
- fix ci for windows: remove bash if/fi
- exlucde failing test from windows
- enable scheduled tests
- use --use-pep517 for pip install: avoid deprecation message
- exlcude windows-latest and pypy-3.9 since there is a problem with installing (missing zlib.h)
2022-12-18 23:06:52 +01:00
scito
ca4a0bc7d2 improvements for - (stdout)
- - implies -q
- use add_mutually_exclusive_group for -v and -q
- adapt and improve tests
2022-12-18 21:34:24 +01:00
scito
1be4c7e0ef suppor writing csv and json to stdout; print errors to stderr
- add tests
2022-12-18 19:24:07 +01:00
scito
fd1841f8dd update Pipfile.lock 2022-12-18 17:42:42 +01:00
scito
81c2cb498a add stdin pytest 2022-12-18 17:41:35 +01:00
scito
21c16ed44e add pypy Python versions 3.7, 3.8, and 3.9 to GitHub CI testing 2022-12-18 13:26:17 +01:00
scito
30638041d8 upgrade to protobuf 4.21.12 2022-12-16 13:17:39 +01:00
scito
892f4f92ae fix compatibility for Python < 3.11 2022-12-16 13:10:22 +01:00
scito
bda0186d10 test wrong data and improve error handling 2022-12-16 12:43:32 +01:00
scito
96c8836a98 upgrade to protobuf 4.21.11 2022-12-10 12:25:15 +01:00
scito
c44a3f45de add pip installation; improve README
- improve README
    - add Installation section
    - add Features section
    - add Glossary
    - add related projects

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-05 17:36:39 +01:00
scito
a77e775948 add keepass csv export; improve hotp
- export to dedicated totp and hotp csv files for KeePass
- show Typ as totp/hotp instead of OTP_TOTP/OTP_HOTP
  (BREAKING CHANGE in csv, json and stdout, qr codes or urls are not affected)
- add hotp example
- add hotp tests
- export counter for hotp to csv and json files
- add section on KeePass to README
- increase protobuf to 4.21.10
- show file names of exported csv or json files
2022-12-04 16:19:30 +01:00
Wu Tingfeng
eae01a07d5 Test mutually exclusive arguments verbose and quiet (#25) 2022-11-28 21:19:35 +01:00
scito
10fefacd2d add clean pipenv to README 2022-11-27 21:47:34 +01:00
scito
b562ceb00a use Python 3.11 for pipenv 2022-11-27 18:33:21 +01:00
scito
3e1818619e add dev deps to Pipfile 2022-11-19 10:17:44 +01:00
Ilya Kaznacheev
d08195507e Add docker as installation option (#23)
* Add docker as installation option

* Add newline to Dockerfile

* Fix example typo

* Add code review fixes
2022-11-19 09:18:24 +01:00
scito
a95a0d1325 upgrade devcontainer to Python 3.11 2022-10-31 22:11:55 +01:00
scito
302c45be99 generalize to upgrade_deps.sh and update Pipfile.lock 2022-10-31 21:50:16 +01:00
scito
354a4bdada improve steps in README.md 2022-10-30 14:46:43 +01:00
scito
13c4b6c7d4 enable Python 3.11 ci 2022-10-25 22:36:58 +02:00
dependabot[bot]
397534d5ef Bump protobuf from 4.21.7 to 4.21.8
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 4.21.7 to 4.21.8.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-25 22:28:16 +02:00
scito
52d5c56890 use protobuf 4.21.8 2022-10-22 10:31:25 +02:00
dependabot[bot]
8bc7d8f035 Bump protobuf from 4.21.6 to 4.21.7
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 4.21.6 to 4.21.7.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-10-21 21:46:11 +02:00
scito
a60cbbb7bb ignore wheel files 2022-09-25 11:59:46 +02:00
scito
4546655cc5 add protoc upgrade script and update to protoc 21.6/protobuf 4.21.6 2022-09-25 11:54:22 +02:00
dependabot[bot]
39af5ab077 Bump protobuf from 4.21.5 to 4.21.6
Bumps [protobuf](https://github.com/protocolbuffers/protobuf) from 4.21.5 to 4.21.6.
- [Release notes](https://github.com/protocolbuffers/protobuf/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf/blob/main/generate_changelog.py)
- [Commits](https://github.com/protocolbuffers/protobuf/commits)

---
updated-dependencies:
- dependency-name: protobuf
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-09-25 09:27:43 +02:00
Roland Kurmann
be4d0d37db add default codeql-analysis.yml
This file enables security scans on GitHub.
2022-09-17 11:24:11 +02:00
scito
3933e6ed8a add #StandWithUkraine 2022-09-09 18:50:10 +02:00
scito
dbfd3464f2 save qr code to specific dir, improve help, add tests
- use metavar for files and dirs in help
- support several recursive dirs in saveqr
- add saveqr and debug tests
2022-09-09 13:15:22 +02:00
scito
fbefb3474c fix save_qr: dict notation is needed 2022-09-09 13:08:35 +02:00
scito
4baf406211 improve docu
- add help page to README.txt
- remove -p
- mention optional qrcode module
2022-09-08 21:26:19 +02:00
scito
cd2d3258d3 handle not encoded + in query params, fixes #15
- add debug level, by givein parameter -vv
- if the base64 string is not urlencoded, then + will be replaced by a space,
  what cannot be decoded anymore
  --> replace spaces back to plus
- add test
2022-09-07 21:58:03 +02:00
scito
df8b99dce4 add debug launch config for folder and workspace 2022-09-07 21:49:29 +02:00
scito
801c0e42d0 add technical spelling words, like TOTP 2022-09-07 19:59:38 +02:00
scito
d7f4533c99 enable base64 decode validation
Enable validation for analyzing #15
2022-09-07 19:36:10 +02:00
scito
9beb98693c README: link badges, add protobuf 2022-09-04 22:07:39 +02:00
scito
fbde835601 enable pytest in vscode and mention it in README 2022-09-04 19:02:36 +00:00
scito
fb4cee14da add VSCode devcontainer setup 2022-09-04 13:58:58 +00:00
scito
4027677b38 add vscode settings.json 2022-09-04 13:44:46 +02:00
scito
acab230436 improve README.md 2022-09-04 08:57:12 +02:00
scito
7fc69ea415 refactor to reduce complexity of extract_otps (main loop)
flake8 is happy now
2022-09-04 08:37:03 +02:00
scito
16d9fffc4a add test for verbose output 2022-09-04 08:16:20 +02:00
scito
33bba8848a add dependabot.yml 2022-09-04 01:30:45 +02:00
scito
2eaab5e3b5 update README and CI 2022-09-04 01:01:53 +02:00
33 changed files with 2014 additions and 299 deletions

37
.devcontainer/Dockerfile Normal file
View File

@@ -0,0 +1,37 @@
# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.134.0/containers/python-3/.devcontainer/base.Dockerfile
# [Choice] Python version: 3, 3.10, ...
ARG VARIANT=3
FROM mcr.microsoft.com/vscode/devcontainers/python:${VARIANT}
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# Remove imagemagick due to https://security-tracker.debian.org/tracker/CVE-2019-10131
&& apt-get purge -y imagemagick imagemagick-6-common \
# Install common packages, non-root user
# && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" \
# Clean up
&& apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts
# [Option] Install Node.js
# ARG INSTALL_NODE="false"
# ARG NODE_VERSION="lts/*"
# RUN if [ "${INSTALL_NODE}" = "true" ]; then su vscode -c "source /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] Allow the vscode user to pip install globally w/o sudo
# ENV PIP_TARGET=/usr/local/pip-global
# ENV PYTHONPATH=${PIP_TARGET}:${PYTHONPATH}
# ENV PATH=${PIP_TARGET}/bin:${PATH}
# RUN mkdir -p ${PIP_TARGET} \
# && chown vscode:root ${PIP_TARGET} \
# && echo "if [ \"\$(stat -c '%U' ${PIP_TARGET})\" != \"vscode\" ]; then chown -R vscode:root ${PIP_TARGET}; fi" \
# | tee -a /root/.bashrc /home/vscode/.bashrc /root/.zshrc >> /home/vscode/.zshrc
# [Optional] If your pip requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
# && rm -rf /tmp/pip-tmp
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update \
# && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>

View File

@@ -0,0 +1,22 @@
// Docu: https://containers.dev/implementors/json_reference/
{
"name": "Python 3",
"build": {
"dockerfile": "Dockerfile",
"context": "..",
//Update 'VARIANT' to pick a Python version: 3, 3.10, ...
"args": {
"VARIANT": "3.11"
}
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.python"
],
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand":
"python -m pip install --upgrade pip; pip install -r requirements-dev.txt; pip install -r requirements.txt",
"postStartCommand": "echo 'Happy coding'"
// Comment out to connect as root instead.
// "remoteUser": "vscode"
}

11
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,11 @@
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "pip" # See documentation for possible values
directory: "/" # Location of package manifests
schedule:
interval: "weekly"

View File

@@ -1,14 +1,23 @@
name: extract_otp_secret_keys
name: tests
on: [push]
on:
push:
pull_request:
schedule:
- cron: '47 3 * * *'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10", "3.x"]
python-version: ["3.x", "3.11", "3.10", "3.9", "pypy-3.9", "3.8", "pypy-3.8", "3.7", "pypy-3.7"]
platform: [ubuntu-latest, macos-latest, windows-latest]
exclude:
- platform: windows-latest
- python-version: [pypy-3.9]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v3
@@ -20,7 +29,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install --use-pep517 -r requirements.txt
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
@@ -28,8 +37,4 @@ jobs:
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics
- name: Test with pytest
run: |
pytest
- name: Test with unittest
run: |
python -m unittest
run: pytest

74
.github/workflows/codeql-analysis.yml vendored Normal file
View File

@@ -0,0 +1,74 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ "master" ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ "master" ]
schedule:
- cron: '25 19 * * 0'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'python' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
steps:
- name: Checkout repository
uses: actions/checkout@v3
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
# queries: security-extended,security-and-quality
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v2
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
# If the Autobuild fails above, remove it and uncomment the following three lines.
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
# - run: |
# echo "Run, Build Application using script"
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
with:
category: "/language:${{matrix.language}}"

13
.gitignore vendored
View File

@@ -1,11 +1,18 @@
generated_python/__pycache__/
__pycache__/
qr/
venv/
*.csv
*.json
/*.csv
/*.json
!devbox.json
!example_output.json
!example_output.csv
!.github/
!.flake8
.vscode
!.vscode/settings.json
!.devcontainer/
!.devcontainer/*.json
*.whl
build/
extract_otp_secret_keys.egg-info/

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

@@ -0,0 +1,5 @@
{
"recommendations": [
"ms-python.python"
]
}

20
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,20 @@
{
"python.testing.pytestArgs": [
"."
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true,
"cSpell.words": [
"devbox",
"HOTP",
"otpauth",
"pipenv",
"proto",
"protobuf",
"protoc",
"pytest",
"qrcode",
"TOTP",
"venv"
]
}

11
Dockerfile Normal file
View File

@@ -0,0 +1,11 @@
FROM python:3.11-alpine
WORKDIR /extract
COPY . .
RUN pip install -r requirements.txt
WORKDIR /files
ENTRYPOINT [ "python", "/extract/extract_otp_secret_keys.py" ]

View File

@@ -10,6 +10,9 @@ pillow = "*"
[dev-packages]
pytest = "*"
wheel = "*"
flake8 = "*"
pylint = "*"
[requires]
python_version = "3.10"
python_version = "3.11"

358
Pipfile.lock generated
View File

@@ -1,11 +1,11 @@
{
"_meta": {
"hash": {
"sha256": "360af7adfda239c3869ca72b8c07e4d53b66dc3c83c38dc304a175fe408f6737"
"sha256": "38a1c4d86c2546c0ac33f77c1a20dc325b50be44b8c4b310050b63033558507d"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.10"
"python_version": "3.11"
},
"sources": [
{
@@ -18,87 +18,90 @@
"default": {
"pillow": {
"hashes": [
"sha256:0030fdbd926fb85844b8b92e2f9449ba89607231d3dd597a21ae72dc7fe26927",
"sha256:030e3460861488e249731c3e7ab59b07c7853838ff3b8e16aac9561bb345da14",
"sha256:0ed2c4ef2451de908c90436d6e8092e13a43992f1860275b4d8082667fbb2ffc",
"sha256:136659638f61a251e8ed3b331fc6ccd124590eeff539de57c5f80ef3a9594e58",
"sha256:13b725463f32df1bfeacbf3dd197fb358ae8ebcd8c5548faa75126ea425ccb60",
"sha256:1536ad017a9f789430fb6b8be8bf99d2f214c76502becc196c6f2d9a75b01b76",
"sha256:15928f824870535c85dbf949c09d6ae7d3d6ac2d6efec80f3227f73eefba741c",
"sha256:17d4cafe22f050b46d983b71c707162d63d796a1235cdf8b9d7a112e97b15bac",
"sha256:1802f34298f5ba11d55e5bb09c31997dc0c6aed919658dfdf0198a2fe75d5490",
"sha256:1cc1d2451e8a3b4bfdb9caf745b58e6c7a77d2e469159b0d527a4554d73694d1",
"sha256:1fd6f5e3c0e4697fa7eb45b6e93996299f3feee73a3175fa451f49a74d092b9f",
"sha256:254164c57bab4b459f14c64e93df11eff5ded575192c294a0c49270f22c5d93d",
"sha256:2ad0d4df0f5ef2247e27fc790d5c9b5a0af8ade9ba340db4a73bb1a4a3e5fb4f",
"sha256:2c58b24e3a63efd22554c676d81b0e57f80e0a7d3a5874a7e14ce90ec40d3069",
"sha256:2d33a11f601213dcd5718109c09a52c2a1c893e7461f0be2d6febc2879ec2402",
"sha256:336b9036127eab855beec9662ac3ea13a4544a523ae273cbf108b228ecac8437",
"sha256:337a74fd2f291c607d220c793a8135273c4c2ab001b03e601c36766005f36885",
"sha256:37ff6b522a26d0538b753f0b4e8e164fdada12db6c6f00f62145d732d8a3152e",
"sha256:3d1f14f5f691f55e1b47f824ca4fdcb4b19b4323fe43cc7bb105988cad7496be",
"sha256:4134d3f1ba5f15027ff5c04296f13328fecd46921424084516bdb1b2548e66ff",
"sha256:4ad2f835e0ad81d1689f1b7e3fbac7b01bb8777d5a985c8962bedee0cc6d43da",
"sha256:50dff9cc21826d2977ef2d2a205504034e3a4563ca6f5db739b0d1026658e004",
"sha256:510cef4a3f401c246cfd8227b300828715dd055463cdca6176c2e4036df8bd4f",
"sha256:5aed7dde98403cd91d86a1115c78d8145c83078e864c1de1064f52e6feb61b20",
"sha256:69bd1a15d7ba3694631e00df8de65a8cb031911ca11f44929c97fe05eb9b6c1d",
"sha256:6bf088c1ce160f50ea40764f825ec9b72ed9da25346216b91361eef8ad1b8f8c",
"sha256:6e8c66f70fb539301e064f6478d7453e820d8a2c631da948a23384865cd95544",
"sha256:74a04183e6e64930b667d321524e3c5361094bb4af9083db5c301db64cd341f3",
"sha256:75e636fd3e0fb872693f23ccb8a5ff2cd578801251f3a4f6854c6a5d437d3c04",
"sha256:7761afe0126d046974a01e030ae7529ed0ca6a196de3ec6937c11df0df1bc91c",
"sha256:7888310f6214f19ab2b6df90f3f06afa3df7ef7355fc025e78a3044737fab1f5",
"sha256:7b0554af24df2bf96618dac71ddada02420f946be943b181108cac55a7a2dcd4",
"sha256:7c7b502bc34f6e32ba022b4a209638f9e097d7a9098104ae420eb8186217ebbb",
"sha256:808add66ea764ed97d44dda1ac4f2cfec4c1867d9efb16a33d158be79f32b8a4",
"sha256:831e648102c82f152e14c1a0938689dbb22480c548c8d4b8b248b3e50967b88c",
"sha256:93689632949aff41199090eff5474f3990b6823404e45d66a5d44304e9cdc467",
"sha256:96b5e6874431df16aee0c1ba237574cb6dff1dcb173798faa6a9d8b399a05d0e",
"sha256:9a54614049a18a2d6fe156e68e188da02a046a4a93cf24f373bffd977e943421",
"sha256:a138441e95562b3c078746a22f8fca8ff1c22c014f856278bdbdd89ca36cff1b",
"sha256:a647c0d4478b995c5e54615a2e5360ccedd2f85e70ab57fbe817ca613d5e63b8",
"sha256:a9c9bc489f8ab30906d7a85afac4b4944a572a7432e00698a7239f44a44e6efb",
"sha256:ad2277b185ebce47a63f4dc6302e30f05762b688f8dc3de55dbae4651872cdf3",
"sha256:adabc0bce035467fb537ef3e5e74f2847c8af217ee0be0455d4fec8adc0462fc",
"sha256:b6d5e92df2b77665e07ddb2e4dbd6d644b78e4c0d2e9272a852627cdba0d75cf",
"sha256:bc431b065722a5ad1dfb4df354fb9333b7a582a5ee39a90e6ffff688d72f27a1",
"sha256:bdd0de2d64688ecae88dd8935012c4a72681e5df632af903a1dca8c5e7aa871a",
"sha256:c79698d4cd9318d9481d89a77e2d3fcaeff5486be641e60a4b49f3d2ecca4e28",
"sha256:cb6259196a589123d755380b65127ddc60f4c64b21fc3bb46ce3a6ea663659b0",
"sha256:d5b87da55a08acb586bad5c3aa3b86505f559b84f39035b233d5bf844b0834b1",
"sha256:dcd7b9c7139dc8258d164b55696ecd16c04607f1cc33ba7af86613881ffe4ac8",
"sha256:dfe4c1fedfde4e2fbc009d5ad420647f7730d719786388b7de0999bf32c0d9fd",
"sha256:ea98f633d45f7e815db648fd7ff0f19e328302ac36427343e4432c84432e7ff4",
"sha256:ec52c351b35ca269cb1f8069d610fc45c5bd38c3e91f9ab4cbbf0aebc136d9c8",
"sha256:eef7592281f7c174d3d6cbfbb7ee5984a671fcd77e3fc78e973d492e9bf0eb3f",
"sha256:f07f1f00e22b231dd3d9b9208692042e29792d6bd4f6639415d2f23158a80013",
"sha256:f3fac744f9b540148fa7715a435d2283b71f68bfb6d4aae24482a890aed18b59",
"sha256:fa768eff5f9f958270b081bb33581b4b569faabf8774726b283edb06617101dc",
"sha256:fac2d65901fb0fdf20363fbd345c01958a742f2dc62a8dd4495af66e3ff502a4"
"sha256:03150abd92771742d4a8cd6f2fa6246d847dcd2e332a18d0c15cc75bf6703040",
"sha256:073adb2ae23431d3b9bcbcff3fe698b62ed47211d0716b067385538a1b0f28b8",
"sha256:0b07fffc13f474264c336298d1b4ce01d9c5a011415b79d4ee5527bb69ae6f65",
"sha256:0b7257127d646ff8676ec8a15520013a698d1fdc48bc2a79ba4e53df792526f2",
"sha256:12ce4932caf2ddf3e41d17fc9c02d67126935a44b86df6a206cf0d7161548627",
"sha256:15c42fb9dea42465dfd902fb0ecf584b8848ceb28b41ee2b58f866411be33f07",
"sha256:18498994b29e1cf86d505edcb7edbe814d133d2232d256db8c7a8ceb34d18cef",
"sha256:1c7c8ae3864846fc95f4611c78129301e203aaa2af813b703c55d10cc1628535",
"sha256:22b012ea2d065fd163ca096f4e37e47cd8b59cf4b0fd47bfca6abb93df70b34c",
"sha256:276a5ca930c913f714e372b2591a22c4bd3b81a418c0f6635ba832daec1cbcfc",
"sha256:2e0918e03aa0c72ea56edbb00d4d664294815aa11291a11504a377ea018330d3",
"sha256:3033fbe1feb1b59394615a1cafaee85e49d01b51d54de0cbf6aa8e64182518a1",
"sha256:3168434d303babf495d4ba58fc22d6604f6e2afb97adc6a423e917dab828939c",
"sha256:32a44128c4bdca7f31de5be641187367fe2a450ad83b833ef78910397db491aa",
"sha256:3dd6caf940756101205dffc5367babf288a30043d35f80936f9bfb37f8355b32",
"sha256:40e1ce476a7804b0fb74bcfa80b0a2206ea6a882938eaba917f7a0f004b42502",
"sha256:41e0051336807468be450d52b8edd12ac60bebaa97fe10c8b660f116e50b30e4",
"sha256:4390e9ce199fc1951fcfa65795f239a8a4944117b5935a9317fb320e7767b40f",
"sha256:502526a2cbfa431d9fc2a079bdd9061a2397b842bb6bc4239bb176da00993812",
"sha256:51e0e543a33ed92db9f5ef69a0356e0b1a7a6b6a71b80df99f1d181ae5875636",
"sha256:57751894f6618fd4308ed8e0c36c333e2f5469744c34729a27532b3db106ee20",
"sha256:5d77adcd56a42d00cc1be30843d3426aa4e660cab4a61021dc84467123f7a00c",
"sha256:655a83b0058ba47c7c52e4e2df5ecf484c1b0b0349805896dd350cbc416bdd91",
"sha256:68943d632f1f9e3dce98908e873b3a090f6cba1cbb1b892a9e8d97c938871fbe",
"sha256:6c738585d7a9961d8c2821a1eb3dcb978d14e238be3d70f0a706f7fa9316946b",
"sha256:73bd195e43f3fadecfc50c682f5055ec32ee2c933243cafbfdec69ab1aa87cad",
"sha256:772a91fc0e03eaf922c63badeca75e91baa80fe2f5f87bdaed4280662aad25c9",
"sha256:77ec3e7be99629898c9a6d24a09de089fa5356ee408cdffffe62d67bb75fdd72",
"sha256:7db8b751ad307d7cf238f02101e8e36a128a6cb199326e867d1398067381bff4",
"sha256:801ec82e4188e935c7f5e22e006d01611d6b41661bba9fe45b60e7ac1a8f84de",
"sha256:82409ffe29d70fd733ff3c1025a602abb3e67405d41b9403b00b01debc4c9a29",
"sha256:828989c45c245518065a110434246c44a56a8b2b2f6347d1409c787e6e4651ee",
"sha256:829f97c8e258593b9daa80638aee3789b7df9da5cf1336035016d76f03b8860c",
"sha256:871b72c3643e516db4ecf20efe735deb27fe30ca17800e661d769faab45a18d7",
"sha256:89dca0ce00a2b49024df6325925555d406b14aa3efc2f752dbb5940c52c56b11",
"sha256:90fb88843d3902fe7c9586d439d1e8c05258f41da473952aa8b328d8b907498c",
"sha256:97aabc5c50312afa5e0a2b07c17d4ac5e865b250986f8afe2b02d772567a380c",
"sha256:9aaa107275d8527e9d6e7670b64aabaaa36e5b6bd71a1015ddd21da0d4e06448",
"sha256:9f47eabcd2ded7698106b05c2c338672d16a6f2a485e74481f524e2a23c2794b",
"sha256:a0a06a052c5f37b4ed81c613a455a81f9a3a69429b4fd7bb913c3fa98abefc20",
"sha256:ab388aaa3f6ce52ac1cb8e122c4bd46657c15905904b3120a6248b5b8b0bc228",
"sha256:ad58d27a5b0262c0c19b47d54c5802db9b34d38bbf886665b626aff83c74bacd",
"sha256:ae5331c23ce118c53b172fa64a4c037eb83c9165aba3a7ba9ddd3ec9fa64a699",
"sha256:af0372acb5d3598f36ec0914deed2a63f6bcdb7b606da04dc19a88d31bf0c05b",
"sha256:afa4107d1b306cdf8953edde0534562607fe8811b6c4d9a486298ad31de733b2",
"sha256:b03ae6f1a1878233ac620c98f3459f79fd77c7e3c2b20d460284e1fb370557d4",
"sha256:b0915e734b33a474d76c28e07292f196cdf2a590a0d25bcc06e64e545f2d146c",
"sha256:b4012d06c846dc2b80651b120e2cdd787b013deb39c09f407727ba90015c684f",
"sha256:b472b5ea442148d1c3e2209f20f1e0bb0eb556538690fa70b5e1f79fa0ba8dc2",
"sha256:b59430236b8e58840a0dfb4099a0e8717ffb779c952426a69ae435ca1f57210c",
"sha256:b90f7616ea170e92820775ed47e136208e04c967271c9ef615b6fbd08d9af0e3",
"sha256:b9a65733d103311331875c1dca05cb4606997fd33d6acfed695b1232ba1df193",
"sha256:bac18ab8d2d1e6b4ce25e3424f709aceef668347db8637c2296bcf41acb7cf48",
"sha256:bca31dd6014cb8b0b2db1e46081b0ca7d936f856da3b39744aef499db5d84d02",
"sha256:be55f8457cd1eac957af0c3f5ece7bc3f033f89b114ef30f710882717670b2a8",
"sha256:c7025dce65566eb6e89f56c9509d4f628fddcedb131d9465cacd3d8bac337e7e",
"sha256:c935a22a557a560108d780f9a0fc426dd7459940dc54faa49d83249c8d3e760f",
"sha256:dbb8e7f2abee51cef77673be97760abff1674ed32847ce04b4af90f610144c7b",
"sha256:e6ea6b856a74d560d9326c0f5895ef8050126acfdc7ca08ad703eb0081e82b74",
"sha256:ebf2029c1f464c59b8bdbe5143c79fa2045a581ac53679733d3a91d400ff9efb",
"sha256:f1ff2ee69f10f13a9596480335f406dd1f70c3650349e2be67ca3139280cade0"
],
"index": "pypi",
"version": "==9.2.0"
"version": "==9.3.0"
},
"protobuf": {
"hashes": [
"sha256:011c0f267e85f5d73750b6c25f0155d5db1e9443cd3590ab669a6221dd8fcdb0",
"sha256:3ec6f5b37935406bb9df9b277e79f8ed81d697146e07ef2ba8a5a272fb24b2c9",
"sha256:5310cbe761e87f0c1decce019d23f2101521d4dfff46034f8a12a53546036ec7",
"sha256:5e0b272217aad8971763960238c1a1e6a65d50ef7824e23300da97569a251c55",
"sha256:5e0ce02418ef03d7657a420ae8fd6fec4995ac713a3cb09164e95f694dbcf085",
"sha256:5eb0724615e90075f1d763983e708e1cef08e66b1891d8b8b6c33bc3b2f1a02b",
"sha256:7b6f22463e2d1053d03058b7b4ceca6e4ed4c14f8c286c32824df751137bf8e7",
"sha256:a7faa62b183d6a928e3daffd06af843b4287d16ef6e40f331575ecd236a7974d",
"sha256:b04484d6f42f48c57dd2737a72692f4c6987529cdd148fb5b8e5f616862a2e37",
"sha256:b52e7a522911a40445a5f588bd5b5e584291bfc5545e09b7060685e4b2ff814f",
"sha256:bf711b451212dc5b0fa45ae7dada07d8e71a4b0ff0bc8e4783ee145f47ac4f82",
"sha256:e5c5a2886ae48d22a9d32fbb9b6636a089af3cd26b706750258ce1ca96cc0116",
"sha256:eb1106e87e095628e96884a877a51cdb90087106ee693925ec0a300468a9be3a",
"sha256:ee04f5823ed98bb9a8c3b1dc503c49515e0172650875c3f76e225b223793a1f2"
"sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30",
"sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b",
"sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc",
"sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791",
"sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717",
"sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec",
"sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7",
"sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab",
"sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2",
"sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5",
"sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1",
"sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462",
"sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97",
"sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"
],
"index": "pypi",
"version": "==4.21.5"
"version": "==4.21.12"
},
"qrcode": {
"hashes": [
@@ -109,6 +112,14 @@
}
},
"develop": {
"astroid": {
"hashes": [
"sha256:10e0ad5f7b79c435179d0d0f0df69998c4eef4597534aae44910db060baeb907",
"sha256:1493fe8bd3dfd73dc35bd53c9d5b6e49ead98497c47b2307662556a5692d29d7"
],
"markers": "python_full_version >= '3.7.2'",
"version": "==2.12.13"
},
"attrs": {
"hashes": [
"sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6",
@@ -117,6 +128,22 @@
"markers": "python_version >= '3.5'",
"version": "==22.1.0"
},
"dill": {
"hashes": [
"sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0",
"sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"
],
"markers": "python_version >= '3.11'",
"version": "==0.3.6"
},
"flake8": {
"hashes": [
"sha256:3833794e27ff64ea4e9cf5d410082a8b97ff1a06c16aa3d2027339cd0f1195c7",
"sha256:c61007e76655af75e6785a931f452915b371dc48f56efd765247c8fe68f2b181"
],
"index": "pypi",
"version": "==6.0.0"
},
"iniconfig": {
"hashes": [
"sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
@@ -124,13 +151,62 @@
],
"version": "==1.1.1"
},
"packaging": {
"isort": {
"hashes": [
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
"sha256:83155ffa936239d986b0f190347a3f2285f42a9b9e1725c89d865b27dd0627e5",
"sha256:a8ca25fbfad0f7d5d8447a4314837298d9f6b23aed8618584c894574f626b64b"
],
"markers": "python_full_version >= '3.7.0'",
"version": "==5.11.3"
},
"lazy-object-proxy": {
"hashes": [
"sha256:0c1c7c0433154bb7c54185714c6929acc0ba04ee1b167314a779b9025517eada",
"sha256:14010b49a2f56ec4943b6cf925f597b534ee2fe1f0738c84b3bce0c1a11ff10d",
"sha256:4e2d9f764f1befd8bdc97673261b8bb888764dfdbd7a4d8f55e4fbcabb8c3fb7",
"sha256:4fd031589121ad46e293629b39604031d354043bb5cdf83da4e93c2d7f3389fe",
"sha256:5b51d6f3bfeb289dfd4e95de2ecd464cd51982fe6f00e2be1d0bf94864d58acd",
"sha256:6850e4aeca6d0df35bb06e05c8b934ff7c533734eb51d0ceb2d63696f1e6030c",
"sha256:6f593f26c470a379cf7f5bc6db6b5f1722353e7bf937b8d0d0b3fba911998858",
"sha256:71d9ae8a82203511a6f60ca5a1b9f8ad201cac0fc75038b2dc5fa519589c9288",
"sha256:7e1561626c49cb394268edd00501b289053a652ed762c58e1081224c8d881cec",
"sha256:8f6ce2118a90efa7f62dd38c7dbfffd42f468b180287b748626293bf12ed468f",
"sha256:ae032743794fba4d171b5b67310d69176287b5bf82a21f588282406a79498891",
"sha256:afcaa24e48bb23b3be31e329deb3f1858f1f1df86aea3d70cb5c8578bfe5261c",
"sha256:b70d6e7a332eb0217e7872a73926ad4fdc14f846e85ad6749ad111084e76df25",
"sha256:c219a00245af0f6fa4e95901ed28044544f50152840c5b6a3e7b2568db34d156",
"sha256:ce58b2b3734c73e68f0e30e4e725264d4d6be95818ec0a0be4bb6bf9a7e79aa8",
"sha256:d176f392dbbdaacccf15919c77f526edf11a34aece58b55ab58539807b85436f",
"sha256:e20bfa6db17a39c706d24f82df8352488d2943a3b7ce7d4c22579cb89ca8896e",
"sha256:eac3a9a5ef13b332c059772fd40b4b1c3d45a3a2b05e33a361dee48e54a4dad0",
"sha256:eb329f8d8145379bf5dbe722182410fe8863d186e51bf034d2075eb8d85ee25b"
],
"markers": "python_version >= '3.7'",
"version": "==1.8.0"
},
"mccabe": {
"hashes": [
"sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325",
"sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"
],
"markers": "python_version >= '3.6'",
"version": "==21.3"
"version": "==0.7.0"
},
"packaging": {
"hashes": [
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
"sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"
],
"markers": "python_version >= '3.7'",
"version": "==22.0"
},
"platformdirs": {
"hashes": [
"sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca",
"sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"
],
"markers": "python_version >= '3.7'",
"version": "==2.6.0"
},
"pluggy": {
"hashes": [
@@ -140,37 +216,123 @@
"markers": "python_version >= '3.6'",
"version": "==1.0.0"
},
"py": {
"pycodestyle": {
"hashes": [
"sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719",
"sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"
"sha256:347187bdb476329d98f695c213d7295a846d1152ff4fe9bacb8a9590b8ee7053",
"sha256:8a4eaf0d0495c7395bdab3589ac2db602797d76207242c17d470186815706610"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==1.11.0"
"markers": "python_version >= '3.6'",
"version": "==2.10.0"
},
"pyparsing": {
"pyflakes": {
"hashes": [
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
"sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
"sha256:ec55bf7fe21fff7f1ad2f7da62363d749e2a470500eab1b555334b67aa1ef8cf",
"sha256:ec8b276a6b60bd80defed25add7e439881c19e64850afd9b346283d4165fd0fd"
],
"markers": "python_full_version >= '3.6.8'",
"version": "==3.0.9"
"markers": "python_version >= '3.6'",
"version": "==3.0.1"
},
"pylint": {
"hashes": [
"sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4",
"sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb"
],
"index": "pypi",
"version": "==2.15.9"
},
"pytest": {
"hashes": [
"sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7",
"sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"
"sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71",
"sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"
],
"index": "pypi",
"version": "==7.1.3"
"version": "==7.2.0"
},
"tomli": {
"tomlkit": {
"hashes": [
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
"sha256:07de26b0d8cfc18f871aec595fda24d95b08fef89d147caa861939f37230bf4b",
"sha256:71b952e5721688937fb02cf9d354dbcf0785066149d2855e44531ebdd2b65d73"
],
"markers": "python_version >= '3.7'",
"version": "==2.0.1"
"markers": "python_version >= '3.6'",
"version": "==0.11.6"
},
"wheel": {
"hashes": [
"sha256:965f5259b566725405b05e7cf774052044b1ed30119b5d586b2703aafe8719ac",
"sha256:b60533f3f5d530e971d6737ca6d58681ee434818fab630c83a734bb10c083ce8"
],
"index": "pypi",
"version": "==0.38.4"
},
"wrapt": {
"hashes": [
"sha256:00b6d4ea20a906c0ca56d84f93065b398ab74b927a7a3dbd470f6fc503f95dc3",
"sha256:01c205616a89d09827986bc4e859bcabd64f5a0662a7fe95e0d359424e0e071b",
"sha256:02b41b633c6261feff8ddd8d11c711df6842aba629fdd3da10249a53211a72c4",
"sha256:07f7a7d0f388028b2df1d916e94bbb40624c59b48ecc6cbc232546706fac74c2",
"sha256:11871514607b15cfeb87c547a49bca19fde402f32e2b1c24a632506c0a756656",
"sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3",
"sha256:21ac0156c4b089b330b7666db40feee30a5d52634cc4560e1905d6529a3897ff",
"sha256:257fd78c513e0fb5cdbe058c27a0624c9884e735bbd131935fd49e9fe719d310",
"sha256:2b39d38039a1fdad98c87279b48bc5dce2c0ca0d73483b12cb72aa9609278e8a",
"sha256:2cf71233a0ed05ccdabe209c606fe0bac7379fdcf687f39b944420d2a09fdb57",
"sha256:2fe803deacd09a233e4762a1adcea5db5d31e6be577a43352936179d14d90069",
"sha256:3232822c7d98d23895ccc443bbdf57c7412c5a65996c30442ebe6ed3df335383",
"sha256:34aa51c45f28ba7f12accd624225e2b1e5a3a45206aa191f6f9aac931d9d56fe",
"sha256:36f582d0c6bc99d5f39cd3ac2a9062e57f3cf606ade29a0a0d6b323462f4dd87",
"sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d",
"sha256:40e7bc81c9e2b2734ea4bc1aceb8a8f0ceaac7c5299bc5d69e37c44d9081d43b",
"sha256:43ca3bbbe97af00f49efb06e352eae40434ca9d915906f77def219b88e85d907",
"sha256:4fcc4649dc762cddacd193e6b55bc02edca674067f5f98166d7713b193932b7f",
"sha256:5a0f54ce2c092aaf439813735584b9537cad479575a09892b8352fea5e988dc0",
"sha256:5a9a0d155deafd9448baff28c08e150d9b24ff010e899311ddd63c45c2445e28",
"sha256:5b02d65b9ccf0ef6c34cba6cf5bf2aab1bb2f49c6090bafeecc9cd81ad4ea1c1",
"sha256:60db23fa423575eeb65ea430cee741acb7c26a1365d103f7b0f6ec412b893853",
"sha256:642c2e7a804fcf18c222e1060df25fc210b9c58db7c91416fb055897fc27e8cc",
"sha256:6a9a25751acb379b466ff6be78a315e2b439d4c94c1e99cb7266d40a537995d3",
"sha256:6b1a564e6cb69922c7fe3a678b9f9a3c54e72b469875aa8018f18b4d1dd1adf3",
"sha256:6d323e1554b3d22cfc03cd3243b5bb815a51f5249fdcbb86fda4bf62bab9e164",
"sha256:6e743de5e9c3d1b7185870f480587b75b1cb604832e380d64f9504a0535912d1",
"sha256:709fe01086a55cf79d20f741f39325018f4df051ef39fe921b1ebe780a66184c",
"sha256:7b7c050ae976e286906dd3f26009e117eb000fb2cf3533398c5ad9ccc86867b1",
"sha256:7d2872609603cb35ca513d7404a94d6d608fc13211563571117046c9d2bcc3d7",
"sha256:7ef58fb89674095bfc57c4069e95d7a31cfdc0939e2a579882ac7d55aadfd2a1",
"sha256:80bb5c256f1415f747011dc3604b59bc1f91c6e7150bd7db03b19170ee06b320",
"sha256:81b19725065dcb43df02b37e03278c011a09e49757287dca60c5aecdd5a0b8ed",
"sha256:833b58d5d0b7e5b9832869f039203389ac7cbf01765639c7309fd50ef619e0b1",
"sha256:88bd7b6bd70a5b6803c1abf6bca012f7ed963e58c68d76ee20b9d751c74a3248",
"sha256:8ad85f7f4e20964db4daadcab70b47ab05c7c1cf2a7c1e51087bfaa83831854c",
"sha256:8c0ce1e99116d5ab21355d8ebe53d9460366704ea38ae4d9f6933188f327b456",
"sha256:8d649d616e5c6a678b26d15ece345354f7c2286acd6db868e65fcc5ff7c24a77",
"sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef",
"sha256:9736af4641846491aedb3c3f56b9bc5568d92b0692303b5a305301a95dfd38b1",
"sha256:988635d122aaf2bdcef9e795435662bcd65b02f4f4c1ae37fbee7401c440b3a7",
"sha256:9cca3c2cdadb362116235fdbd411735de4328c61425b0aa9f872fd76d02c4e86",
"sha256:9e0fd32e0148dd5dea6af5fee42beb949098564cc23211a88d799e434255a1f4",
"sha256:9f3e6f9e05148ff90002b884fbc2a86bd303ae847e472f44ecc06c2cd2fcdb2d",
"sha256:a85d2b46be66a71bedde836d9e41859879cc54a2a04fad1191eb50c2066f6e9d",
"sha256:a9a52172be0b5aae932bef82a79ec0a0ce87288c7d132946d645eba03f0ad8a8",
"sha256:aa31fdcc33fef9eb2552cbcbfee7773d5a6792c137b359e82879c101e98584c5",
"sha256:b014c23646a467558be7da3d6b9fa409b2c567d2110599b7cf9a0c5992b3b471",
"sha256:b21bb4c09ffabfa0e85e3a6b623e19b80e7acd709b9f91452b8297ace2a8ab00",
"sha256:b5901a312f4d14c59918c221323068fad0540e34324925c8475263841dbdfe68",
"sha256:b9b7a708dd92306328117d8c4b62e2194d00c365f18eff11a9b53c6f923b01e3",
"sha256:d1967f46ea8f2db647c786e78d8cc7e4313dbd1b0aca360592d8027b8508e24d",
"sha256:d52a25136894c63de15a35bc0bdc5adb4b0e173b9c0d07a2be9d3ca64a332735",
"sha256:d77c85fedff92cf788face9bfa3ebaa364448ebb1d765302e9af11bf449ca36d",
"sha256:d79d7d5dc8a32b7093e81e97dad755127ff77bcc899e845f41bf71747af0c569",
"sha256:dbcda74c67263139358f4d188ae5faae95c30929281bc6866d00573783c422b7",
"sha256:ddaea91abf8b0d13443f6dac52e89051a5063c7d014710dcb4d4abb2ff811a59",
"sha256:dee0ce50c6a2dd9056c20db781e9c1cfd33e77d2d569f5d1d9321c641bb903d5",
"sha256:dee60e1de1898bde3b238f18340eec6148986da0455d8ba7848d50470a7a32fb",
"sha256:e2f83e18fe2f4c9e7db597e988f72712c0c3676d337d8b101f6758107c42425b",
"sha256:e3fb1677c720409d5f671e39bac6c9e0e422584e5f518bfd50aa4cbbea02433f",
"sha256:ee2b1b1769f6707a8a445162ea16dddf74285c3964f605877a20e38545c3c462",
"sha256:ee6acae74a2b91865910eef5e7de37dc6895ad96fa23603d1d27ea69df545015",
"sha256:ef3f72c9666bba2bab70d2a8b79f2c6d2c1a42a7f7e2b0ec83bb2f9e383950af"
],
"markers": "python_version >= '3.11'",
"version": "==1.14.1"
}
}
}

193
README.md
View File

@@ -2,21 +2,49 @@
[![CI Status](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml/badge.svg)](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml)
![PyPI - Python Version](https://img.shields.io/pypi/pyversions/protobuf)
![GitHub Pipenv locked Python version](https://img.shields.io/github/pipenv/locked/python-version/scito/extract_otp_secret_keys)
![License](https://img.shields.io/github/license/scito/extract_otp_secret_keys)
[![GitHub Pipenv locked Python version](https://img.shields.io/github/pipenv/locked/python-version/scito/extract_otp_secret_keys)](https://github.com/scito/extract_otp_secret_keys/blob/master/Pipfile.lock)
![protobuf version](https://img.shields.io/badge/protobuf-4.21.12-informational)
[![License](https://img.shields.io/github/license/scito/extract_otp_secret_keys)](https://github.com/scito/extract_otp_secret_keys/blob/master/LICENSE)
[![GitHub tag (latest SemVer)](https://img.shields.io/github/v/tag/scito/extract_otp_secret_keys?sort=semver&label=version)](https://github.com/scito/extract_otp_secret_keys/tags)
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/badges/StandWithUkraine.svg)](https://stand-with-ukraine.pp.ua)
---
Extract two-factor authentication (2FA, TFA) secret keys from export QR codes of "Google Authenticator" app
Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of "Google Authenticator" app.
The secret and otp values can be printed and exported to json or csv. The QR codes can be printed or saved as PNG images.
## Installation
git clone https://github.com/scito/extract_otp_secret_keys.git
cd extract_otp_secret_keys
## Usage
1. Export the QR codes from "Google Authenticator" app
2. Read QR codes with QR code reader
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:
1. Open "Google Authenticator" app on the mobile phone
2. Export the QR codes from "Google Authenticator" app
3. Read QR codes with a QR code reader (e.g. from another phone)
4. Save the captured QR codes in the QR code reader to a text file, e.g. example_export.txt. Save each QR code on a new line. (The captured QR codes look like `otpauth-migration://offline?data=...`)
5. Transfer the file to the computer where his script is installed.
6. Call this script with the file as input:
python extract_otp_secret_keys.py -p example_export.txt
python extract_otp_secret_keys.py example_export.txt
## Program help: arguments and options
<pre>usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv FILE] [--keepass FILE] [--printqr] [--saveqr DIR] [--verbose | --quiet] infile
positional arguments:
infile file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored
options:
-h, --help show this help message and exit
--json FILE, -j FILE export json file or - for stdout
--csv FILE, -c FILE export csv file or - for stdout
--keepass FILE, -k FILE export totp/hotp csv file(s) for KeePass, - for stdout
--printqr, -p print QR code(s) as text to the terminal (requires qrcode module)
--saveqr DIR, -s DIR save QR code(s) as images to the given folder (requires qrcode module)
--verbose, -v verbose output
--quiet, -q no stdout output, except output set by -</pre>
## Dependencies
@@ -24,7 +52,10 @@ Extract two-factor authentication (2FA, TFA) secret keys from export QR codes of
Known to work with
* Python 3.10.6, protobuf 4.21.5, qrcode 7.3.1, and pillow 9.2
* Python 3.10.8, protobuf 4.21.9, qrcode 7.3.1, and pillow 9.2
* Python 3.11.1, protobuf 4.21.12, qrcode 7.3.1, and pillow 9.2
For protobuf versions 3.14.0 or similar or Python 3.6, use the extract_otp_secret_keys version 1.4.0.
### Optional
@@ -32,6 +63,67 @@ For printing QR codes, the qrcode module is required, otherwise it can be omitte
pip install qrcode[pil]
## Features
* Free and open source
* Supports Google Authenticator export
* All functionality in one Python script: extract_otp_secret_keys.py (except protobuf generated code in protobuf_generated_python)
* Supports TOTP and HOTP
* Generates QR codes
* Various export formats:
* CSV
* JSON
* Dedicated CSV for KeePass
* QR code images
* Supports reading from stdin and writing to stdout by specifying '-'
* Errors and warnings are written to stderr
* Many ways to run the script:
* Native Python
* pipenv
* venv
* Docker
* VSCode devcontainer
* devbox
* pip
## KeePass
[KeePass 2.51](https://keepass.info/news/n220506_2.51.html) (released in May 2022) and newer [support the generation of OTPs (TOTP and HOTP)](https://keepass.info/help/base/placeholders.html#otp).
KeePass can generate the second factor password (2FA) if the OTP secret is stored in `TimeOtp-Secret-Base32` string field for TOTP or `HmacOtp-Secret-Base32` string field for HOTP. You view or edit them in entry dialog on the 'Advanced' tab page.
KeePass provides menu commands in the main window for generating one-time passwords ('Copy HMAC-Based OTP', 'Show HMAC-Based OTP', 'Copy Time-Based OTP', 'Show Time-Based OTP'). Furthermore, one-time passwords can be generated during auto-type using the {HMACOTP} and {TIMEOTP} placeholders.
In order to simplify the usage of the second factor password generation in KeePass a specific KeePass CSV export is available with option `-keepass` or `-k`. This KeePass CSV file can be imported by the ["Generic CSV Importer" of KeePass](https://keepass.info/help/kb/imp_csv.html).
If TOTP and HOTP entries have to be exported, then two files with an intermediate suffix .totp or .hotp will be added to the KeePass export filename.
Example:
- Only TOTP entries to export and parameter --keepass example_keepass_output.csv<br>
→ example_keepass_output.csv with TOTP entries will be exported
- Only HOTP entries to export and parameter --keepass example_keepass_output.csv<br>
→ example_keepass_output.csv with HOTP entries will be exported
- If both TOTP and HOTP entries to export and parameter --keepass example_keepass_output.csv<br>
→ example_keepass_output.totp.csv with TOTP entries will be exported<br>
→ example_keepass_output.hotp.csv with HOTP entries will be exported
Import CSV with TOTP entries in KeePass as
- Title
- User Name
- String (TimeOtp-Secret-Base32)
- Group (/)
Import CSV with HOTP entries in KeePass as
- Title
- User Name
- String (HmacOtp-Secret-Base32)
- String (HmacOtp-Counter)
- Group (/)
KeePass can be used as a backup for one time passwords (second factor) from the mobile phone.
## Technical background
The export QR code of "Google Authenticator" contains the URL `otpauth-migration://offline?data=...`.
@@ -41,32 +133,69 @@ Command for regeneration of Python code from proto3 message definition file (onl
protoc --python_out=protobuf_generated_python google_auth.proto
The generated protobuf Python code was generated by protoc 21.5 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.5).
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
## References
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
## Glossary
* OTP = One-time password
* TOTP = Time-based one-time password
* HOTP = HMAC-based one-time password (using a counter)
* 2FA = Second factor authentication
* TFA = Two factor authentication
* QR code = Quick response code
## Alternative installation methods
### pip
```
pip install git+https://github.com/scito/extract_otp_secret_keys
python -m extract_otp_secret_keys
```
#### Example
```
wget https://raw.githubusercontent.com/scito/extract_otp_secret_keys/master/example_export.txt
python -m extract_otp_secret_keys example_export.txt
```
### pipenv
You can you use [Pipenv](https://github.com/pypa/pipenv) for running extract_otp_secret_keys.
```
pipenv --rm
pipenv install
pipenv shell
python extract_otp_secret_keys.py example_export.txt
```
### Visual Studio Code Remote - Containers / VSCode devcontainer
You can you use [VSCode devcontainer](https://code.visualstudio.com/docs/remote/containers-tutorial) for running extract_otp_secret_keys.
Requirement: Docker
1. Start VSCode
2. Open extract_otp_secret_keys.code-workspace
3. Open VSCode command palette (Ctrl-Shift-P)
4. Type command "Remote-Containers: Reopen in Container"
5. Open integrated bash terminal in VSCode
6. Execute: python extract_otp_secret_keys.py example_export.txt
### venv
Alternatively, you can use a python virtual env for the dependencies:
python -m venv venv
. venv/bin/activate
pip install -r requirements-buildenv.txt
pip install -r requirements-dev.txt
pip install -r requirements.txt
The requirements\*.txt files contain all the dependencies (also the optional ones).
@@ -80,6 +209,17 @@ Install [devbox](https://github.com/jetpack-io/devbox), which is a wrapper for n
devbox shell
```
### Docker
Install [Docker](https://docs.docker.com/get-docker/).
Build and run the app within the container:
```bash
docker build . -t extract_otp
docker run --rm -v "$(pwd)":/files:ro extract_otp -p example_export.txt
```
## Tests
### PyTest
@@ -105,3 +245,34 @@ Run tests:
```
python -m unittest
```
### VSCode Setup
Setup for running the tests in VSCode.
1. Open VSCode command palette (Ctrl-Shift-P)
2. Type command "Python: Configure Tests"
3. Choose unittest or pytest. (pytest is recommended, both are supported)
4. Set ". Root" directory
## Maintenance
### Upgrade pip Packages
```
pip install -U -r requirements.txt
```
## Related projects
* [ZBar](https://github.com/mchehab/zbar) is an open source software suite for reading bar codes from various sources, including webcams.
* [Aegis Authenticator](https://github.com/beemdevelopment/Aegis) is a free, secure and open source 2FA app for Android.
* [Android OTP Extractor](https://github.com/puddly/android-otp-extractor) can extract your tokens from popular Android OTP apps and export them in a standard format or just display them as QR codes for easy importing. [Requires a _rooted_ Android phone.]
***
# #StandWithUkraine 🇺🇦
I have Ukrainian relatives and friends.
#RussiaInvadedUkraine on 24 of February 2022, at 05:00 the armed forces of the Russian Federation attacked Ukraine. Please, stand with Ukraine, stay tuned for updates on Ukraine's official sources and channels in English and support Ukraine in its fight for freedom and democracy in Europe.

View File

@@ -9,3 +9,10 @@ otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXB
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
# Name: "encoding: ¿äÄéÉ? (demo)"
otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D

View File

@@ -0,0 +1,2 @@
Title,User Name,HmacOtp-Secret-Base32,HmacOtp-Counter,Group
,hotp demo,7KSQL2JTUDIS5EF65KLMRQIIGY,4,OTP/HOTP
1 Title User Name HmacOtp-Secret-Base32 HmacOtp-Counter Group
2 hotp demo 7KSQL2JTUDIS5EF65KLMRQIIGY 4 OTP/HOTP

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,25 @@
{
"folders": [
{
"path": "."
}
],
"settings": {
"python.testing.pytestEnabled": true
},
"launch": {
"version": "0.2.0",
"configurations": [
{
"name": "Python: extract_otp_secret_keys.py",
"type": "python",
"request": "launch",
"program": "extract_otp_secret_keys.py",
"args": [
"example_export.txt"
],
"console": "integratedTerminal"
},
]
}
}

View File

@@ -5,7 +5,7 @@
# 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 -p example_export.txt
# python extract_otp_secret_keys.py example_export.txt
#
# Requirement:
# The protobuf package of Google for proto3 is required for running this script.
@@ -48,22 +48,171 @@ import sys
import csv
import json
from urllib.parse import parse_qs, urlencode, urlparse, quote
from os import path, mkdir
from os import path, makedirs
from re import compile as rcompile
import protobuf_generated_python.google_auth_pb2
def sys_main():
main(sys.argv[1:])
def main(sys_args):
global verbose, quiet
# allow to use sys.stdout with with (avoid closing)
sys.stdout.close = lambda: None
args = parse_args(sys_args)
verbose = args.verbose if args.verbose else 0
quiet = args.quiet
otps = extract_otps(args)
write_csv(args, otps)
write_keepass_csv(args, otps)
write_json(args, otps)
def parse_args(sys_args):
formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=52)
arg_parser = argparse.ArgumentParser(formatter_class=formatter)
arg_parser.add_argument('infile', help='file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
arg_parser.add_argument('--json', '-j', help='export json file or - for stdout', metavar=('FILE'))
arg_parser.add_argument('--csv', '-c', help='export csv file or - for stdout', metavar=('FILE'))
arg_parser.add_argument('--keepass', '-k', help='export totp/hotp csv file(s) for KeePass, - for stdout', metavar=('FILE'))
arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal (requires qrcode module)', action='store_true')
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
output_group = arg_parser.add_mutually_exclusive_group()
output_group.add_argument('--verbose', '-v', help='verbose output', action='count')
output_group.add_argument('--quiet', '-q', help='no stdout output, except output set by -', action='store_true')
args = arg_parser.parse_args(sys_args)
if args.csv == '-' or args.json == '-' or args.keepass == '-':
args.quiet = args.q = True
return args
def extract_otps(args):
global verbose, quiet
quiet = args.quiet
otps = []
i = j = 0
finput = fileinput.input(args.infile)
try:
for line in (line.strip() for line in finput):
if verbose: print(line)
if line.startswith('#') or line == '': continue
i += 1
payload = get_payload_from_line(line, i, args)
# pylint: disable=no-member
for raw_otp in payload.otp_parameters:
j += 1
if verbose: print('\n{}. Secret Key'.format(j))
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
otp_type_enum = get_enum_name_by_number(raw_otp, 'type')
otp_type = get_otp_type_str_from_code(raw_otp.type)
otp_url = build_otp_url(secret, raw_otp)
otp = {
"name": raw_otp.name,
"secret": secret,
"issuer": raw_otp.issuer,
"type": otp_type,
"counter": raw_otp.counter if raw_otp.type == 1 else None,
"url": otp_url
}
if not quiet:
print_otp(otp)
if args.printqr:
print_qr(args, otp_url)
if args.saveqr:
save_qr(otp, args, j)
if not quiet:
print()
otps.append(otp)
finally:
finput.close()
return otps
def get_payload_from_line(line, i, args):
global verbose
if not line.startswith('otpauth-migration://'):
eprint('\nWARN: line is not a otpauth-migration:// URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line))
parsed_url = urlparse(line)
if verbose > 1: print('\nDEBUG: parsed_url={}'.format(parsed_url))
try:
params = parse_qs(parsed_url.query, strict_parsing=True)
except: # Not necessary for Python >= 3.11
params = []
if verbose > 1: print('\nDEBUG: querystring params={}'.format(params))
if 'data' not in params:
eprint('\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line))
sys.exit(1)
data_base64 = params['data'][0]
if verbose > 1: print('\nDEBUG: data_base64={}'.format(data_base64))
data_base64_fixed = data_base64.replace(' ', '+')
if verbose > 1: print('\nDEBUG: data_base64_fixed={}'.format(data_base64))
data = base64.b64decode(data_base64_fixed, validate=True)
payload = protobuf_generated_python.google_auth_pb2.MigrationPayload()
try:
payload.ParseFromString(data)
except:
eprint('\nERROR: Cannot decode otpauth-migration migration payload.')
eprint('data={}'.format(data_base64))
exit(1);
if verbose:
print('\n{}. Payload Line'.format(i), payload, sep='\n')
return payload
# https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf
def get_enum_name_by_number(parent, field_name):
field_value = getattr(parent, field_name)
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name
def get_otp_type_str_from_code(otp_type):
return 'totp' if otp_type == 2 else 'hotp'
def convert_secret_from_bytes_to_base32_str(bytes):
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
def save_qr(args, data, name):
def build_otp_url(secret, raw_otp):
url_params = {'secret': secret}
if raw_otp.type == 1: url_params['counter'] = raw_otp.counter
if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
otp_url = 'otpauth://{}/{}?'.format(get_otp_type_str_from_code(raw_otp.type), quote(raw_otp.name)) + urlencode(url_params)
return otp_url
def print_otp(otp):
print('Name: {}'.format(otp['name']))
print('Secret: {}'.format(otp['secret']))
if otp['issuer']: print('Issuer: {}'.format(otp['issuer']))
print('Type: {}'.format(otp['type']))
if otp['type'] == 'hotp':
print('Counter: {}'.format(otp['counter']))
if verbose:
print(otp['url'])
def save_qr(otp, args, j):
dir = args.saveqr
if not (path.exists(dir)): makedirs(dir, exist_ok=True)
pattern = rcompile(r'[\W_]+')
file_otp_name = pattern.sub('', otp['name'])
file_otp_issuer = pattern.sub('', otp['issuer'])
save_qr_file(args, otp['url'], '{}/{}-{}{}.png'.format(dir, j, file_otp_name, '-' + file_otp_issuer if file_otp_issuer else ''))
return file_otp_issuer
def save_qr_file(args, data, name):
from qrcode import QRCode
global verbose
qr = QRCode()
@@ -80,111 +229,96 @@ def print_qr(args, data):
qr.print_ascii()
def parse_args(sys_args):
arg_parser = argparse.ArgumentParser()
arg_parser.add_argument('--verbose', '-v', help='verbose output', action='store_true')
arg_parser.add_argument('--quiet', '-q', help='no stdout output', action='store_true')
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the "qr" subfolder', action='store_true')
arg_parser.add_argument('--printqr', '-p', help='print QR code(s) as text to the terminal', action='store_true')
arg_parser.add_argument('--json', '-j', help='export to json file')
arg_parser.add_argument('--csv', '-c', help='export to csv file')
arg_parser.add_argument('infile', help='file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
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 sys_main():
main(sys.argv[1:])
def main(sys_args):
global verbose, quiet
args = parse_args(sys_args)
verbose = args.verbose
quiet = args.quiet
otps = extract_otps(args)
write_csv(args, otps)
write_json(args, otps)
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
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)
params = parse_qs(parsed_url.query)
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_encoded = params['data'][0]
data = base64.b64decode(data_encoded)
payload = protobuf_generated_python.google_auth_pb2.MigrationPayload()
payload.ParseFromString(data)
i += 1
if verbose: print('\n{}. Payload Line'.format(i), payload, sep='\n')
# pylint: disable=no-member
for otp in payload.otp_parameters:
j += 1
if verbose: print('\n{}. Secret Key'.format(j))
if not quiet: print('Name: {}'.format(otp.name))
secret = convert_secret_from_bytes_to_base32_str(otp.secret)
if not quiet: print('Secret: {}'.format(secret))
if otp.issuer and not quiet: print('Issuer: {}'.format(otp.issuer))
otp_type = get_enum_name_by_number(otp, 'type')
if not quiet: print('Type: {}'.format(otp_type))
url_params = {'secret': secret}
if otp.type == 1: url_params['counter'] = otp.counter
if otp.issuer: url_params['issuer'] = otp.issuer
otp_url = 'otpauth://{}/{}?'.format('totp' if otp.type == 2 else 'hotp', quote(otp.name)) + urlencode(url_params)
if verbose: print(otp_url)
if args.printqr:
print_qr(args, otp_url)
if args.saveqr:
if not (path.exists('qr')): mkdir('qr')
pattern = rcompile(r'[\W_]+')
file_otp_name = pattern.sub('', otp.name)
file_otp_issuer = pattern.sub('', otp.issuer)
save_qr(args, otp_url, 'qr/{}-{}{}.png'.format(j, file_otp_name, '-' + file_otp_issuer if file_otp_issuer else ''))
if not quiet: print()
otps.append({
"name": otp.name,
"secret": secret,
"issuer": otp.issuer,
"type": otp_type,
"url": otp_url
})
return otps
def write_csv(args, otps):
global verbose, quiet
if args.csv and len(otps) > 0:
with open(args.csv, "w") as outfile:
with open_file_or_stdout_for_csv(args.csv) as outfile:
writer = csv.DictWriter(outfile, otps[0].keys())
writer.writeheader()
writer.writerows(otps)
if not quiet: print("Exported {} otps to csv".format(len(otps)))
if not quiet: print("Exported {} otps to csv {}".format(len(otps), args.csv))
def write_keepass_csv(args, otps):
global verbose, quiet
if args.keepass and len(otps) > 0:
has_totp = has_otp_type(otps, 'totp')
has_hotp = has_otp_type(otps, 'hotp')
otp_filename_totp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "totp")
otp_filename_hotp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "hotp")
count_totp_entries = 0
count_hotp_entries = 0
if has_totp:
with open_file_or_stdout_for_csv(otp_filename_totp) as outfile:
writer = csv.DictWriter(outfile, ["Title", "User Name", "TimeOtp-Secret-Base32", "Group"])
writer.writeheader()
for otp in otps:
if otp['type'] == 'totp':
writer.writerow({
'Title': otp['issuer'],
'User Name': otp['name'],
'TimeOtp-Secret-Base32': otp['secret'] if otp['type'] == 'totp' else None,
'Group': "OTP/{}".format(otp['type'].upper())
})
count_totp_entries += 1
if has_hotp:
with open_file_or_stdout_for_csv(otp_filename_hotp) as outfile:
writer = csv.DictWriter(outfile, ["Title", "User Name", "HmacOtp-Secret-Base32", "HmacOtp-Counter", "Group"])
writer.writeheader()
for otp in otps:
if otp['type'] == 'hotp':
writer.writerow({
'Title': otp['issuer'],
'User Name': otp['name'],
'HmacOtp-Secret-Base32': otp['secret'] if otp['type'] == 'hotp' else None,
'HmacOtp-Counter': otp['counter'] if otp['type'] == 'hotp' else None,
'Group': "OTP/{}".format(otp['type'].upper())
})
count_hotp_entries += 1
if not quiet:
if count_totp_entries > 0: print("Exported {} totp entries to keepass csv file {}".format(count_totp_entries, otp_filename_totp))
if count_hotp_entries > 0: print("Exported {} hotp entries to keepass csv file {}".format(count_hotp_entries, otp_filename_hotp))
def write_json(args, otps):
global verbose, quiet
if args.json:
with open(args.json, "w") as outfile:
with open_file_or_stdout(args.json) as outfile:
json.dump(otps, outfile, indent=4)
if not quiet: print("Exported {} otp entries to json".format(len(otps)))
if not quiet: print("Exported {} otp entries to json {}".format(len(otps), args.json))
def has_otp_type(otps, otp_type):
for otp in otps:
if otp['type'] == otp_type:
return True
return False
def add_pre_suffix(file, pre_suffix):
'''filename.ext, pre -> filename.pre.ext'''
name, ext = path.splitext(file)
return name + "." + pre_suffix + (ext if ext else "")
def open_file_or_stdout(filename):
'''stdout is denoted as "-".
Note: Set before the following line:
sys.stdout.close = lambda: None'''
return open(filename, "w", encoding='utf-8') if filename != '-' else sys.stdout
def open_file_or_stdout_for_csv(filename):
'''stdout is denoted as "-".
newline=''
Note: Set before the following line:
sys.stdout.close = lambda: None'''
return open(filename, "w", encoding='utf-8', newline='') if filename != '-' else sys.stdout
def eprint(*args, **kwargs):
'''Print to stderr.'''
print(*args, file=sys.stderr, **kwargs)
if __name__ == '__main__':

View File

@@ -1 +0,0 @@
wheel

4
requirements-dev.txt Normal file
View File

@@ -0,0 +1,4 @@
wheel
pytest
flake8
pylint

60
setup.py Normal file
View File

@@ -0,0 +1,60 @@
import pathlib
from setuptools import setup
setup(
name='extract_otp_secret_keys',
version='1.6.0',
description='Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of "Google Authenticator" app',
long_description=(pathlib.Path(__file__).parent / 'README.md').read_text(),
long_description_content_type='text/markdown',
url='https://github.com/scito/extract_otp_secret_keys',
author='scito',
author_email='info@scito.ch',
classifiers=[
'Development Status :: 5 - Production/Stable',
'Environment :: Console',
'Topic :: System :: Archiving :: Backup',
'Topic :: Utilities',
'Programming Language :: Python :: 3.7',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Intended Audience :: End Users/Desktop',
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Programming Language :: Python'
'Natural Language :: English',
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
],
keywords='python security json otp csv protobuf qrcode two-factor totp google-authenticator recovery proto3 mfa two-factor-authentication tfa qr-codes otpauth 2fa security-tools',
py_modules=['extract_otp_secret_keys', 'protobuf_generated_python.google_auth_pb2'],
entry_points={
'console_scripts': [
'extract_otp_secret_keys = extract_otp_secret_keys:sys_main',
]
},
python_requires='>=3.7, <4',
install_requires=[
'protobuf',
'qrcode',
'Pillow'
],
project_urls={
'Bug Reports': 'https://github.com/scito/extract_otp_secret_keys/issues',
'Source': 'https://github.com/scito/extract_otp_secret_keys',
},
license='GNU General Public License v3 (GPLv3)',
)

View File

@@ -0,0 +1,15 @@
# 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

View File

@@ -0,0 +1,138 @@
# 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: 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: 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: totp
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
4. Secret Key
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
4. Payload Line
otp_parameters {
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
name: "hotp demo"
algorithm: ALGO_SHA1
digits: 1
type: OTP_HOTP
counter: 4
}
version: 1
batch_size: 1
batch_id: -1558849573
5. Secret Key
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
5. Payload Line
otp_parameters {
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
name: "encoding: ¿äÄéÉ? (demo)"
algorithm: ALGO_SHA1
digits: 1
type: OTP_TOTP
}
version: 1
batch_size: 1
batch_id: -171198419
6. Secret Key
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

View File

@@ -1,7 +1,7 @@
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: OTP_TOTP
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: totp
                                             
                                             
    █▀▀▀▀▀█  ▄▀▄▄█ █▀  ▀▀▀▀▀█  ▄▄ █▀▀▀▀▀█    
@@ -26,9 +26,9 @@ Type: OTP_TOTP
                                             
                                             
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: OTP_TOTP
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: totp
                                         
                                         
    █▀▀▀▀▀█ ▀▀██ █▄▀█ ▀▄▄▀█▀▄ █▀▀▀▀▀█    
@@ -51,9 +51,9 @@ Type: OTP_TOTP
                                         
                                         
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: OTP_TOTP
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: totp
                                         
                                         
    █▀▀▀▀▀█ ▀▀██ █▄▀█ ▀▄▄▀█▀▄ █▀▀▀▀▀█    
@@ -76,10 +76,10 @@ Type: OTP_TOTP
                                         
                                         
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: OTP_TOTP
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: totp
                                             
                                             
    █▀▀▀▀▀█  ▄▀▄▄█ █▀  ▀▀▀▀▀█  ▄▄ █▀▀▀▀▀█    
@@ -104,3 +104,60 @@ Type: OTP_TOTP
                                             
                                             
Name: hotp demo
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: hotp
Counter: 4
                                             
                                             
    █▀▀▀▀▀█ ▄█▀▄▄ ▄▄▄██  █▄▄██ █▄ █▀▀▀▀▀█    
    █ ███ █  ▀▄   ▄▄▄  ▀▄ ▄▄▀ █▀  █ ███ █    
    █ ▀▀▀ █ ▀█ ▄▄█▄  ▄▀█▀▀██▄▄██▄ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ ▀▄▀ █▄▀▄█▄█▄█ ▀▄█▄█ █ ▀▀▀▀▀▀▀    
    █▄█ █ ▀▄▄ ▀ ▄▄███▄█▄   ▄█▀ ▀█  ▄█▄▄▀▄    
    ▄██▄▀▄▀██▄▀▄▀ ▀▀█ ▀▄ █▄▀██▄  ▀▄▀▀▀▄█▀    
    ▄ ▄█▄▀▀▀▀█▄▄▄▀▄█▄ ▄ ▄██▀█▀▄ ▀▄█▄ █▀▀█    
    ▄▀▄▀██▀█▀  ██▀▄ ▀▀ ▄▄▄█▄██ ▄▀█▄▄▄ ▀▄▀    
    █▄▀▀▀█▀█▄ ▄ ▀ ▀█ ▄ ▄█ █▄▀█▄ █▄█    ▀▄    
    ▀▀██▄█▀  ▄█▄▀▀█▄ ▄█▀██▄▄█▄  █▀▄█ ▀▀▀█    
    █████▄▀▀█▀▀█▀▀▄  ▄ ▀█▀▄ ██▄ ▄███ ▄▀█     
     ▄▄█▀▀▀█▀█▄█  ▄█▄▄█ ▀▀ ▄▀▄ ▄█▀▄▄█▀▀▄▄    
     ██▄ █▀▄▀▀ █  ▀██ █▄ ▄   █  ▀▄█▀▄█▄██    
    ▀▄▀ █ ▀▄▀▄██▄█ ▀█▀▄▄ ██▄▄▄▀ ▀▄ ▄█ ███    
    ▀  ▀▀ ▀▀▄ ▄▄▄█▄██▀▀ ▄█ ▀ █▀▀█▀▀▀█▀  █    
    █▀▀▀▀▀█  █▄█▀█▀▀█▀   ▄█ ▀▄▄▀█ ▀ █▀▀ ▀    
    █ ███ █ ▀█▀▀ ▀ █ ▄ ▄█▄█  █▄ █▀▀██▀ ██    
    █ ▀▀▀ █ ▀▄▀▄█▀▀▄  ▀▀█▄▄ ▀▄▄█   █▀▀▀▄▀    
    ▀▀▀▀▀▀▀ ▀▀▀▀  ▀ ▀  ▀▀▀   ▀▀ ▀ ▀▀▀▀ ▀▀    
                                             
                                             
Name: encoding: ¿äÄéÉ? (demo)
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: totp
                                                 
                                                 
    █▀▀▀▀▀█ ▄▀▀  ▀  ▄▀█ ▄▄▄██▀ ▄▀█▄█▀ █▀▀▀▀▀█    
    █ ███ █ █▄▀█▄▄ ▀  ▄▀█  █  █  ▀▀▀▄ █ ███ █    
    █ ▀▀▀ █  ▄ ▀ ▀▄▀▄ ▄▄▀▄▄█▄ ▀▄ █▀▀█ █ ▀▀▀ █    
    ▀▀▀▀▀▀▀ █ █ █▄█▄▀ ▀▄▀ █▄█ ▀ █ ▀ █ ▀▀▀▀▀▀▀    
    ▀   ▄ ▀ █▀▀▄▀ ▄▄▀▀▄█▄ █▄▀▀▄█▀██▄▄█▀ ▄█▀▀▄    
    ▄▀█▄█ ▀ ▀ █▄█▄▄    ▄███▄▄▀▀▀▄▄▀▄    ▄█▀▄▄    
     ▀█▀ ▄▀▄▄█ ▄▀███▀ ▄▀█▀▄▀▄  ▀██▄▄ ▄█▀█  ▀▄    
     ▄▀ █▄▀▀ █▀▄▄▄  ▄█▄█ ▀▄ ▄▄ ▄  ▀▀█▄▄ ▀█▄▄▄    
    ▀ ▄▄▄▀▀▄▄█▀▄ ▀▀▀█▄ █▄ ▀ ▄█▄▄▀▄▀▀▀▄▄█▄ ▀ ▀    
    █ ▄   ▀█ ▄▀ ██ █ ▄▄▀▀▀███▄     ▄▄██ ██▀█▀    
    █▀█▀██▀▀███▄  ▀▀▄▄▄▄█▀ █ ▄█▄█▄▀ ▄▄▀  ▄▄ ▀    
    ▄██▄▄ ▀ ▀   ▀▀  ██▄▄▄▀▀▄█▀█▄ ▀ █▄▀▄   ▀▄▄    
      ███▄▀█▄█▄▄█ ▀█▄ ▀▄█  ▄▀▄ █▄  ▄ █▄  ▄▀▄▀    
    █  ▄ ▄▀▀▄▄█▄▄█▄█ ▄▄▄ █▄▄▀█ █▀█▄ ▄▀▀█▄▄▄▀▀    
    ▄  ▀▄▀▀ ▄▄▀██  ▀▀▄█▀▀▄ ▀▀█   ▄ ████ █▀█▀█    
    █▀▄▀█ ▀▄▄ ▄ ▀▀▀ ▄▀ ▀ █▀▄▀▀█▀▀█▄▀█ ▀▄▀▄ █     
    ▀ ▀  ▀▀ ▄ █▄▀█▀▀▄▀█ ▀▄▄█▄▀ ██ ▀██▀▀▀█▀▄▄     
    █▀▀▀▀▀█  ▀█ ▄▄██▀ ▀██▄▀██ ▄▄██ ▀█ ▀ █ ▀█     
    █ ███ █  █▄ █▀▀█▀▀▀█▀█ ▀ ▀█ █▀▀ ██▀▀▀███▀    
    █ ▀▀▀ █  ▄ ▄▀█▄▄ ▀█ ▀▀  ▄ ▀█▀ ▄▀ █▀▀██ ▀▄    
    ▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀▀   ▀  ▀ ▀▀▀▀ ▀  ▀   ▀ ▀      
                                                 
                                                 

View File

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

View File

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

View File

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

View File

@@ -0,0 +1 @@
otpauth-migration://offline?data=ClEKFAciUeGF4aS6IDCvMv99ySZ1ekKsEiVTZXJlbml0eUxhYnM6dGVzdDFAc2VyZW5pdHlsYWJzLmNvLnVrGgxTZXJlbml0eUxhYnMgASgBMAIKUQoUkIY8/fbrHZWTb4CBln18lvqt0HcSJVNlcmVuaXR5TGFiczp0ZXN0MkBzZXJlbml0eWxhYnMuY28udWsaDFNlcmVuaXR5TGFicyABKAEwAgpRChScf+1/Ua4d4gCY0W/7fj9VBkM9PBIlU2VyZW5pdHlMYWJzOnRlc3QzQHNlcmVuaXR5bGFicy5jby51axoMU2VyZW5pdHlMYWJzIAEoATACClEKFG6Qu0ryTSFA/l5rmvTIXtNeb5LtEiVTZXJlbml0eUxhYnM6dGVzdDRAc2VyZW5pdHlsYWJzLmNvLnVrGgxTZXJlbml0eUxhYnMgASgBMAIQARgBIAAogtTa1vz/////AQ==

View File

@@ -18,12 +18,41 @@
# 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, read_file_to_str
from utils import read_csv, read_csv_str, read_json, read_json_str, remove_files, remove_dir_with_files, read_file_to_str, file_exits
from os import path
from pytest import raises, mark
from io import StringIO
from sys import implementation
import extract_otp_secret_keys
def test_extract_csv():
def test_extract_stdout(capsys):
# Act
extract_otp_secret_keys.main(['example_export.txt'])
# Assert
captured = capsys.readouterr()
assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT
assert captured.err == ''
def test_extract_stdin_stdout(capsys, monkeypatch):
# Arrange
monkeypatch.setattr('sys.stdin', StringIO(read_file_to_str('example_export.txt')))
# Act
extract_otp_secret_keys.main(['-'])
# Assert
captured = capsys.readouterr()
assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT
assert captured.err == ''
def test_extract_csv(capsys):
# Arrange
cleanup()
@@ -36,11 +65,138 @@ def test_extract_csv():
assert actual_csv == expected_csv
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
# Clean up
cleanup()
def test_extract_json():
def test_extract_csv_stdout(capsys):
# Arrange
cleanup()
# Act
extract_otp_secret_keys.main(['-c', '-', 'example_export.txt'])
# Assert
assert not file_exits('test_example_output.csv')
captured = capsys.readouterr()
expected_csv = read_csv('example_output.csv')
actual_csv = read_csv_str(captured.out)
assert actual_csv == expected_csv
assert captured.err == ''
# Clean up
cleanup()
def test_extract_stdin_and_csv_stdout(capsys, monkeypatch):
# Arrange
cleanup()
monkeypatch.setattr('sys.stdin', StringIO(read_file_to_str('example_export.txt')))
# Act
extract_otp_secret_keys.main(['-c', '-', '-'])
# Assert
assert not file_exits('test_example_output.csv')
captured = capsys.readouterr()
expected_csv = read_csv('example_output.csv')
actual_csv = read_csv_str(captured.out)
assert actual_csv == expected_csv
assert captured.err == ''
# Clean up
cleanup()
def test_keepass_csv(capsys):
'''Two csv files .totp and .htop are generated.'''
# Arrange
cleanup()
# Act
extract_otp_secret_keys.main(['-q', '-k', 'test_example_keepass_output.csv', 'example_export.txt'])
# Assert
expected_totp_csv = read_csv('example_keepass_output.totp.csv')
expected_hotp_csv = read_csv('example_keepass_output.hotp.csv')
actual_totp_csv = read_csv('test_example_keepass_output.totp.csv')
actual_hotp_csv = read_csv('test_example_keepass_output.hotp.csv')
assert actual_totp_csv == expected_totp_csv
assert actual_hotp_csv == expected_hotp_csv
assert not file_exits('test_example_keepass_output.csv')
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
# Clean up
cleanup()
def test_keepass_csv_stdout(capsys):
'''Two csv files .totp and .htop are generated.'''
# Arrange
cleanup()
# Act
extract_otp_secret_keys.main(['-k', '-', 'test/example_export_only_totp.txt'])
# Assert
expected_totp_csv = read_csv('example_keepass_output.totp.csv')
expected_hotp_csv = read_csv('example_keepass_output.hotp.csv')
assert not file_exits('test_example_keepass_output.totp.csv')
assert not file_exits('test_example_keepass_output.hotp.csv')
assert not file_exits('test_example_keepass_output.csv')
captured = capsys.readouterr()
actual_totp_csv = read_csv_str(captured.out)
assert actual_totp_csv == expected_totp_csv
assert captured.err == ''
# Clean up
cleanup()
def test_single_keepass_csv(capsys):
'''Does not add .totp or .hotp pre-suffix'''
# Arrange
cleanup()
# Act
extract_otp_secret_keys.main(['-q', '-k', 'test_example_keepass_output.csv', 'test/example_export_only_totp.txt'])
# Assert
expected_totp_csv = read_csv('example_keepass_output.totp.csv')
actual_totp_csv = read_csv('test_example_keepass_output.csv')
assert actual_totp_csv == expected_totp_csv
assert not file_exits('test_example_keepass_output.totp.csv')
assert not file_exits('test_example_keepass_output.hotp.csv')
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
# Clean up
cleanup()
def test_extract_json(capsys):
# Arrange
cleanup()
@@ -53,34 +209,61 @@ def test_extract_json():
assert actual_json == expected_json
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
# Clean up
cleanup()
def test_extract_stdout(capsys):
def test_extract_json_stdout(capsys):
# Arrange
cleanup()
# Act
extract_otp_secret_keys.main(['example_export.txt'])
extract_otp_secret_keys.main(['-j', '-', 'example_export.txt'])
# Assert
expected_json = read_json('example_output.json')
assert not file_exits('test_example_output.json')
captured = capsys.readouterr()
actual_json = read_json_str(captured.out)
assert actual_json == expected_json
assert captured.err == ''
# Clean up
cleanup()
def test_extract_not_encoded_plus(capsys):
# Act
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
# Assert
captured = capsys.readouterr()
expected_stdout = '''Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: OTP_TOTP
expected_stdout = '''Name: SerenityLabs:test1@serenitylabs.co.uk
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
Issuer: SerenityLabs
Type: totp
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: OTP_TOTP
Name: SerenityLabs:test2@serenitylabs.co.uk
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
Issuer: SerenityLabs
Type: totp
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: OTP_TOTP
Name: SerenityLabs:test3@serenitylabs.co.uk
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
Issuer: SerenityLabs
Type: totp
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: OTP_TOTP
Name: SerenityLabs:test4@serenitylabs.co.uk
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
Issuer: SerenityLabs
Type: totp
'''
@@ -101,6 +284,201 @@ def test_extract_printqr(capsys):
assert captured.err == ''
def test_extract_saveqr(capsys):
# Arrange
cleanup()
# Act
extract_otp_secret_keys.main(['-q', '-s', 'testout/qr/', 'example_export.txt'])
# Assert
captured = capsys.readouterr()
assert captured.out == ''
assert captured.err == ''
assert path.isfile('testout/qr/1-piraspberrypi-raspberrypi.png')
assert path.isfile('testout/qr/2-piraspberrypi.png')
assert path.isfile('testout/qr/3-piraspberrypi.png')
assert path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png')
# Clean up
cleanup()
@mark.skipif(implementation.name == 'pypy', reason="Encoding problems in verbose mode in pypy.")
def test_extract_verbose(capsys):
# Act
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
# Assert
captured = capsys.readouterr()
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
assert captured.out == expected_stdout
assert captured.err == ''
def test_extract_debug(capsys):
# Act
extract_otp_secret_keys.main(['-vv', 'example_export.txt'])
# Assert
captured = capsys.readouterr()
expected_stdout = read_file_to_str('test/print_verbose_output.txt')
assert len(captured.out) > len(expected_stdout)
assert "DEBUG: " in captured.out
assert captured.err == ''
def test_extract_help(capsys):
with raises(SystemExit) as pytest_wrapped_e:
# Act
extract_otp_secret_keys.main(['-h'])
# Assert
captured = capsys.readouterr()
assert len(captured.out) > 0
assert "-h, --help" in captured.out and "--verbose, -v" in captured.out
assert captured.err == ''
assert pytest_wrapped_e.type == SystemExit
assert pytest_wrapped_e.value.code == 0
def test_extract_no_arguments(capsys):
# Act
with raises(SystemExit) as pytest_wrapped_e:
extract_otp_secret_keys.main([])
# Assert
captured = capsys.readouterr()
expected_err_msg = 'error: the following arguments are required: infile'
assert expected_err_msg in captured.err
assert captured.out == ''
def test_verbose_and_quiet(capsys):
with raises(SystemExit) as pytest_wrapped_e:
# Act
extract_otp_secret_keys.main(['-v', '-q', 'example_export.txt'])
# Assert
captured = capsys.readouterr()
assert len(captured.err) > 0
assert 'error: argument --quiet/-q: not allowed with argument --verbose/-v' in captured.err
assert captured.out == ''
def test_wrong_data(capsys):
with raises(SystemExit) as pytest_wrapped_e:
# Act
extract_otp_secret_keys.main(['test/test_export_wrong_data.txt'])
# Assert
captured = capsys.readouterr()
expected_stderr = '''
ERROR: Cannot decode otpauth-migration migration payload.
data=XXXX
'''
assert captured.err == expected_stderr
assert captured.out == ''
def test_wrong_content(capsys):
with raises(SystemExit) as pytest_wrapped_e:
# Act
extract_otp_secret_keys.main(['test/test_export_wrong_content.txt'])
# Assert
captured = capsys.readouterr()
expected_stderr = '''
WARN: line is not a otpauth-migration:// URL
input file: test/test_export_wrong_content.txt
line "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."
Probably a wrong file was given
ERROR: no data query parameter in input URL
input file: test/test_export_wrong_content.txt
line "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua."
Probably a wrong file was given
'''
assert captured.out == ''
assert captured.err == expected_stderr
def test_wrong_prefix(capsys):
# Act
extract_otp_secret_keys.main(['test/test_export_wrong_prefix.txt'])
# Assert
captured = capsys.readouterr()
expected_stderr = '''
WARN: line is not a otpauth-migration:// URL
input file: test/test_export_wrong_prefix.txt
line "QR-Code:otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B"
Probably a wrong file was given
'''
expected_stdout = '''Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: totp
'''
assert captured.out == expected_stdout
assert captured.err == expected_stderr
def test_add_pre_suffix(capsys):
assert extract_otp_secret_keys.add_pre_suffix("name.csv", "totp") == "name.totp.csv"
assert extract_otp_secret_keys.add_pre_suffix("name.csv", "") == "name..csv"
assert extract_otp_secret_keys.add_pre_suffix("name", "totp") == "name.totp"
def cleanup():
remove_file('test_example_output.csv')
remove_file('test_example_output.json')
remove_files('test_example_*.csv')
remove_files('test_example_*.json')
remove_dir_with_files('testout/')
EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT = '''Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: totp
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: totp
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: totp
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: totp
Name: hotp demo
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: hotp
Counter: 4
Name: encoding: ¿äÄéÉ? (demo)
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: totp
'''

View File

@@ -21,7 +21,9 @@
import unittest
import io
from contextlib import redirect_stdout
from utils import read_csv, read_json, remove_file, Capturing, read_file_to_str
from utils import read_csv, read_json, remove_file, remove_dir_with_files, Capturing, read_file_to_str
from os import path
from sys import implementation
import extract_otp_secret_keys
@@ -49,23 +51,32 @@ class TestExtract(unittest.TestCase):
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',
'Issuer: raspberrypi',
'Type: totp',
'',
'Name: pi@raspberrypi',
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
'Type: OTP_TOTP',
'Name: pi@raspberrypi',
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
'Type: totp',
'',
'Name: pi@raspberrypi',
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
'Type: OTP_TOTP',
'Name: pi@raspberrypi',
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
'Type: totp',
'',
'Name: pi@raspberrypi',
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
'Issuer: raspberrypi',
'Type: OTP_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)
@@ -77,23 +88,61 @@ class TestExtract(unittest.TestCase):
extract_otp_secret_keys.main(['example_export.txt'])
actual_output = out.getvalue()
expected_output = '''Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: OTP_TOTP
expected_output = '''Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: totp
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: OTP_TOTP
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: totp
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: OTP_TOTP
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: totp
Name: pi@raspberrypi
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Issuer: raspberrypi
Type: OTP_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):
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: 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)
@@ -108,6 +157,49 @@ Type: OTP_TOTP
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):
if implementation.name == 'pypy': self.skipTest("Encoding problems in verbose mode in pypy.")
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()
@@ -117,6 +209,7 @@ Type: OTP_TOTP
def cleanup(self):
remove_file('test_example_output.csv')
remove_file('test_example_output.json')
remove_dir_with_files('testout/')
if __name__ == '__main__':

214
upgrade_deps.sh Executable file
View File

@@ -0,0 +1,214 @@
#!/bin/bash
# Upgrades Protoc from https://github.com/protocolbuffers/protobuf/releases
black='\e[0;30m'
blackBold='\e[1;30m'
blackBackground='\e[1;40m'
red='\e[0;31m'
redBold='\e[1;31m'
redBackground='\e[0;41m'
green='\e[0;32m'
greenBold='\e[1;32m'
greenBackground='\e[0;42m'
yellow='\e[0;33m'
yellowBold='\e[1;33m'
yellowBackground='\e[0;43m'
blue='\e[0;34m'
blueBold='\e[1;34m'
blueBackground='\e[0;44m'
magenta='\e[0;35m'
magentaBold='\e[1;35m'
magentaBackground='\e[0;45m'
cyan='\e[0;36m'
cyanBold='\e[1;36m'
cyanBackground='\e[0;46m'
white='\e[0;37m'
whiteBold='\e[1;37m'
whiteBackground='\e[0;47m'
reset='\e[0m'
abort() {
echo '
***************
*** ABORTED ***
***************
' >&2
echo "An error occurred on line $1. Exiting..." >&2
date -Iseconds >&2
exit 1
}
trap 'abort $LINENO' ERR
set -e -o pipefail
quit() {
trap : 0
exit 0
}
# Asks if [Yn] if script shoud continue, otherwise exit 1
# $1: msg or nothing
# Example call 1: askContinueYn
# Example call 1: askContinueYn "Backup DB?"
askContinueYn() {
if [[ $1 ]]; then
msg="$1 "
else
msg=""
fi
# http://stackoverflow.com/questions/3231804/in-bash-how-to-add-are-you-sure-y-n-to-any-command-or-alias
read -e -p "${msg}Continue? [Y/n] " response
response=${response,,} # tolower
if [[ $response =~ ^(yes|y|)$ ]] ; then
# echo ""
# OK
:
else
echo "Aborted"
exit 1
fi
}
# Reference: https://gist.github.com/steinwaywhw/a4cd19cda655b8249d908261a62687f8
echo "Checking Protoc version..."
VERSION=$(curl -sL https://github.com/protocolbuffers/protobuf/releases/latest | grep -E "<title>" | perl -pe's%.*Protocol Buffers v(\d+\.\d+(\.\d+)?).*%\1%')
BASEVERSION=4
echo
interactive=true
check_version=true
while test $# -gt 0; do
case $1 in
-h|--help)
echo "Upgrade Protoc"
echo
echo "$0 [options]"
echo
echo "Options:"
echo "-a Automatic mode"
echo "-C Ignore version check"
echo "-h, --help Help"
quit
;;
-a)
interactive=false
shift
;;
-C)
check_version=false
shift
;;
esac
done
BIN="$HOME/bin"
DOWNLOADS="$HOME/downloads"
PIP="pip3.11"
PIPENV="python3.11 -m pipenv"
# Upgrade protoc
DEST="protoc"
OLDVERSION=$(cat $BIN/$DEST/.VERSION.txt || echo "")
echo -e "\nProtoc remote version $VERSION\n"
echo -e "Protoc local version: $OLDVERSION\n"
if [ "$OLDVERSION" != "$VERSION" ]; then
echo "Upgrade protoc from $OLDVERSION to $VERSION"
NAME="protoc-$VERSION"
ARCHIVE="$NAME.zip"
mkdir -p $DOWNLOADS
# https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protoc-21.6-linux-x86_64.zip
cmd="wget --trust-server-names https://github.com/protocolbuffers/protobuf/releases/download/v$VERSION/protoc-$VERSION-linux-x86_64.zip -O $DOWNLOADS/$ARCHIVE"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="echo -e '\nSize [Byte]'; stat --printf='%s\n' $DOWNLOADS/$ARCHIVE; echo -e '\nMD5'; md5sum $DOWNLOADS/$ARCHIVE; echo -e '\nSHA256'; sha256sum $DOWNLOADS/$ARCHIVE;"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="mkdir -p $BIN/$NAME; unzip $DOWNLOADS/$ARCHIVE -d $BIN/$NAME"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="echo $VERSION > $BIN/$NAME/.VERSION.txt; echo $VERSION > $BIN/$NAME/.VERSION_$VERSION.txt"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="[ -d $BIN/$DEST.old ] && rm -rf $BIN/$DEST.old || echo 'No old dir to delete'"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="[ -d $BIN/$DEST ] && mv -iT $BIN/$DEST $BIN/$DEST.old || echo 'No previous dir to keep'"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="mv -iT $BIN/$NAME $BIN/$DEST"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="rm $DOWNLOADS/$ARCHIVE"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="$BIN/$DEST/bin/protoc --python_out=protobuf_generated_python google_auth.proto"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
# Update README.md
cmd="perl -i -pe 's%proto(buf|c)([- ])(\d\.)?$OLDVERSION%proto\$1\$2\${3}$VERSION%g' README.md && perl -i -pe 's%(protobuf/releases/tag/v)$OLDVERSION%\${1}$VERSION%g' README.md"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
else
echo -e "\nVersion has not changed. Quit"
fi
# Upgrade pip requirements
cmd="sudo pip install --upgrade pip"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
$PIP --version
cmd="$PIP install --use-pep517 -U -r requirements.txt"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="$PIP install -U pipenv"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
$PIPENV --version
cmd="$PIPENV update && $PIPENV --rm && $PIPENV install"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
$PIPENV run python --version
# Test
cmd="pytest"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
cmd="$PIPENV run pytest"
if $interactive ; then askContinueYn "$cmd"; fi
eval "$cmd"
quit

View File

@@ -16,8 +16,10 @@
import csv
import json
import os
import shutil
from io import StringIO
import sys
import glob
# Ref. https://stackoverflow.com/a/16571630
@@ -38,13 +40,26 @@ with Capturing() as output:
sys.stdout = self._stdout
def remove_file(filename):
if os.path.exists(filename): os.remove(filename)
def file_exits(file):
return os.path.isfile(file)
def remove_file(file):
if file_exits(file): os.remove(file)
def remove_files(glob_pattern):
for f in glob.glob(glob_pattern):
os.remove(f)
def remove_dir_with_files(dir):
if os.path.exists(dir): shutil.rmtree(dir)
def read_csv(filename):
"""Returns a list of lines."""
with open(filename, "r") as infile:
with open(filename, "r", encoding="utf-8", newline='') as infile:
lines = []
reader = csv.reader(infile)
for line in reader:
@@ -52,15 +67,29 @@ def read_csv(filename):
return lines
def read_csv_str(str):
"""Returns a list of lines."""
lines = []
reader = csv.reader(str.splitlines())
for line in reader:
lines.append(line)
return lines
def read_json(filename):
"""Returns a list or a dictionary."""
with open(filename, "r") as infile:
with open(filename, "r", encoding="utf-8") as infile:
return json.load(infile)
def read_json_str(str):
"""Returns a list or a dictionary."""
return json.loads(str)
def read_file_to_list(filename):
"""Returns a list of lines."""
with open(filename, "r") as infile:
with open(filename, "r", encoding="utf-8") as infile:
return infile.readlines()