commit 7f899a57a83b60356f43807ba4c7b1878909adec Author: Samuel Pua Date: Thu Nov 25 17:55:49 2021 +0800 First working version diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fcb9767 --- /dev/null +++ b/.gitignore @@ -0,0 +1,209 @@ + +# Created by https://www.toptal.com/developers/gitignore/api/python,macos,linux,vim +# Edit at https://www.toptal.com/developers/gitignore?templates=python,macos,linux,vim + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +### Vim ### +# Swap +[._]*.s[a-v][a-z] +!*.svg # comment out if you don't need vector files +[._]*.sw[a-p] +[._]s[a-rt-v][a-z] +[._]ss[a-gi-z] +[._]sw[a-p] + +# Session +Session.vim +Sessionx.vim + +# Temporary +.netrwhist +# Auto-generated tag files +tags +# Persistent undo +[._]*.un~ + +# End of https://www.toptal.com/developers/gitignore/api/python,macos,linux,vim diff --git a/README.md b/README.md new file mode 100644 index 0000000..e2722b6 --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# GAuth QR Reencoder + +This tool is used to conver image files from Google Authenticator's QR code from both migration (export) and new TOTP. + +## Usage + +The script would take a screenshot of the screen in 2 seconds, then parse the first eligible QR code shown on the screen. + +Execute the following to start the Python script: +``` +python3 gauth_reencoder.py +``` + +## References + +This project utilises code from digitalduke (https://github.com/digitalduke/otpauth-migration-decoder) diff --git a/gauth_reencoder.py b/gauth_reencoder.py new file mode 100644 index 0000000..a9fd488 --- /dev/null +++ b/gauth_reencoder.py @@ -0,0 +1,98 @@ +from PIL import Image +import PIL +import pyzbar.pyzbar as pyzbar +import otpauth_migration_decoder.src.decoder +import urllib.parse +import time +import qrcode + +def decode_qr_file(img_file: str) -> str: + img = Image.open(img_file) + decoded = pyzbar.decode(img) + return decoded[0].data.decode("utf8") + +def decode_screenshot_img() -> str: + from PIL import ImageGrab + img = ImageGrab.grab() + decoded = pyzbar.decode(img) + return decoded[0].data.decode("utf8") + +def decode_totp(totp_uri: str) -> list: + issuer_start = totp_uri.split("&issuer=")[1] + issuer = issuer_start.split("&")[0] + + secret_start = totp_uri.split("&secret=")[1] + secret = secret_start.split("&")[0] + + algorithm_start = totp_uri.split("?algorithm=")[1] + algorithm = algorithm_start.split("&")[0] + + account_start = totp_uri.split("totp/")[1] + account = account_start.split("?")[0] + + digits_start = totp_uri.split("&digits=")[1] + digits = digits_start.split("&")[0] + + issuer = urllib.parse.unquote_plus(issuer) + secret = urllib.parse.unquote_plus(secret) + algorithm = urllib.parse.unquote_plus(algorithm) + account = urllib.parse.unquote_plus(account) + digits = urllib.parse.unquote_plus(digits) + + return {"issuer": issuer, "account": account, "algorithm": algorithm, "secret": secret, "digits": digits} + +def generate_qr_code(totp_details: dict): + issuer = urllib.parse.quote(totp_details["issuer"]) + account = urllib.parse.quote(totp_details["account"]) + algorithm = urllib.parse.quote(totp_details["algorithm"]) + secret = urllib.parse.quote(totp_details["secret"]) + digits = urllib.parse.quote(totp_details["digits"]) + + generated_uri = "otpauth://totp/%s?algorithm=%s&digits=%s&issuer=%s&secret=%s" % ( + account, + algorithm, + digits, + issuer, + secret + ) + img = qrcode.make(generated_uri) + print(generated_uri) + img.show() + + +def main(): + print("Grabbing screenshot in 2 seconds...") + time.sleep(2) + decoded_data = decode_screenshot_img() + print(decoded_data) + + totp_uri_list: list = [] + if "otpauth-migration" in decoded_data: + totp_uri_list = otpauth_migration_decoder.src.decoder.start_decode_migration(decoded_data) + elif "otpauth://totp/" in decoded_data: + totp_uri_list.append(decoded_data) + else: + print("Error in QR code. Please check if it is proper Google Authenticator QR code.") + return + + for uri in totp_uri_list: + try: + curr_totp_details = decode_totp(uri) + print(curr_totp_details) + except: + print("Error in parsing the following URI: %s" % uri) + + # Checking for each item + print() + print() + print("Do you want to change: (separator is \":::\")") + print("%s:::%s ... " % (curr_totp_details["issuer"], curr_totp_details["account"]), end="") + updated_data = input() + updated_data_splitted = updated_data.split(":::") + if updated_data != "": + curr_totp_details["issuer"] = updated_data_splitted[0] + curr_totp_details["account"] = updated_data_splitted[1] + generate_qr_code(curr_totp_details) + +if __name__ == "__main__": + main() diff --git a/otpauth_migration_decoder/.editorconfig b/otpauth_migration_decoder/.editorconfig new file mode 100644 index 0000000..e5c71fe --- /dev/null +++ b/otpauth_migration_decoder/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{json,yml}] +indent_size = 2 diff --git a/otpauth_migration_decoder/.envrc.example b/otpauth_migration_decoder/.envrc.example new file mode 100644 index 0000000..d23567b --- /dev/null +++ b/otpauth_migration_decoder/.envrc.example @@ -0,0 +1 @@ +layout python python3.9 diff --git a/otpauth_migration_decoder/.gitignore b/otpauth_migration_decoder/.gitignore new file mode 100644 index 0000000..03a1c8b --- /dev/null +++ b/otpauth_migration_decoder/.gitignore @@ -0,0 +1,2 @@ +decoder.build/ +decoder.dist/ diff --git a/otpauth_migration_decoder/README.md b/otpauth_migration_decoder/README.md new file mode 100644 index 0000000..48096f6 --- /dev/null +++ b/otpauth_migration_decoder/README.md @@ -0,0 +1,31 @@ +# otpauth migration decoder + +Convert Google Authenticator data to plain otpauth links + + +## usage + +1. get QR code in "Google Authenticator" app (Menu → Transfer accounts → Export accounts → Select accounts → Next) +1. extract link from QR code with your preferred [QR codes reading software](https://play.google.com/store/search?q=qr%20code%20reader) +1. pass migration link (`otpauth-migration://offline?data=...`) to this tool + +## requirements + +The protobuf package is required to running this script: + +```.shell +$ pip install protobuf +``` + +## example + +``` +$ python decoder.py "otpauth-migration://offline?data=CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC" +``` + + +## references + +1. [otpauth:// URI format](https://github.com/google/google-authenticator/wiki/Key-Uri-Format) +1. [Protocol Buffer Basics: Python](https://developers.google.com/protocol-buffers/docs/pythontutorial) +1. [Authenticator live demo](https://rootprojects.org/authenticator/) diff --git a/otpauth_migration_decoder/poetry.lock b/otpauth_migration_decoder/poetry.lock new file mode 100644 index 0000000..124ebe3 --- /dev/null +++ b/otpauth_migration_decoder/poetry.lock @@ -0,0 +1,526 @@ +[[package]] +name = "appdirs" +version = "1.4.4" +description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "atomicwrites" +version = "1.4.0" +description = "Atomic file writes." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "attrs" +version = "21.2.0" +description = "Classes Without Boilerplate" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.extras] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit"] +docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins"] + +[[package]] +name = "click" +version = "8.0.1" +description = "Composable command line interface toolkit" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.4" +description = "Cross-platform colored terminal text." +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[[package]] +name = "coverage" +version = "5.5" +description = "Code coverage measurement for Python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4" + +[package.extras] +toml = ["toml"] + +[[package]] +name = "distlib" +version = "0.3.2" +description = "Distribution utilities" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "filelock" +version = "3.0.12" +description = "A platform independent file lock." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "iniconfig" +version = "1.1.1" +description = "iniconfig: brain-dead simple config-ini parsing" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "isort" +version = "5.9.1" +description = "A Python utility / library to sort Python imports." +category = "dev" +optional = false +python-versions = ">=3.6.1,<4.0" + +[package.extras] +pipfile_deprecated_finder = ["pipreqs", "requirementslib"] +requirements_deprecated_finder = ["pipreqs", "pip-api"] +colors = ["colorama (>=0.4.3,<0.5.0)"] +plugins = ["setuptools"] + +[[package]] +name = "mypy" +version = "0.910" +description = "Optional static typing for Python" +category = "dev" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +mypy-extensions = ">=0.4.3,<0.5.0" +toml = "*" +typing-extensions = ">=3.7.4" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +python2 = ["typed-ast (>=1.4.0,<1.5.0)"] + +[[package]] +name = "mypy-extensions" +version = "0.4.3" +description = "Experimental type system extensions for programs checked with the mypy typechecker." +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "nuitka" +version = "0.6.16" +description = "Python compiler with full language support and CPython compatibility" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "packaging" +version = "20.9" +description = "Core utilities for Python packages" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.dependencies] +pyparsing = ">=2.0.2" + +[[package]] +name = "pluggy" +version = "0.13.1" +description = "plugin and hook calling mechanisms for python" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[package.extras] +dev = ["pre-commit", "tox"] + +[[package]] +name = "protobuf" +version = "3.17.3" +description = "Protocol Buffers" +category = "main" +optional = false +python-versions = "*" + +[package.dependencies] +six = ">=1.9" + +[[package]] +name = "py" +version = "1.10.0" +description = "library with cross-python path, ini-parsing, io, code, log facilities" +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" + +[[package]] +name = "pyparsing" +version = "2.4.7" +description = "Python parsing module" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "pytest" +version = "6.2.4" +description = "pytest: simple powerful testing with Python" +category = "dev" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} +attrs = ">=19.2.0" +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<1.0.0a1" +py = ">=1.8.2" +toml = "*" + +[package.extras] +testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] + +[[package]] +name = "pytest-cov" +version = "2.12.1" +description = "Pytest plugin for measuring coverage." +category = "dev" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" + +[package.dependencies] +coverage = ">=5.2.1" +pytest = ">=4.6" +toml = "*" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +category = "main" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "toml" +version = "0.10.2" +description = "Python Library for Tom's Obvious, Minimal Language" +category = "dev" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" + +[[package]] +name = "tox" +version = "3.23.1" +description = "tox is a generic virtualenv management and test command line tool" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" + +[package.dependencies] +colorama = {version = ">=0.4.1", markers = "platform_system == \"Windows\""} +filelock = ">=3.0.0" +packaging = ">=14" +pluggy = ">=0.12.0" +py = ">=1.4.17" +six = ">=1.14.0" +toml = ">=0.9.4" +virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" + +[package.extras] +docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "pytest-xdist (>=1.22.2)", "pathlib2 (>=2.3.3)"] + +[[package]] +name = "tox-poetry" +version = "0.4.0" +description = "Tox poetry plugin" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +pluggy = "*" +toml = "*" +tox = {version = ">=3.7.0", markers = "python_version >= \"3\""} + +[package.extras] +test = ["coverage", "pytest", "pycodestyle", "pylint"] + +[[package]] +name = "types-futures" +version = "0.1.6" +description = "Typing stubs for futures" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "types-protobuf" +version = "0.1.13" +description = "Typing stubs for protobuf" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +types-futures = "*" + +[[package]] +name = "typing-extensions" +version = "3.10.0.0" +description = "Backported and Experimental Type Hints for Python 3.5+" +category = "dev" +optional = false +python-versions = "*" + +[[package]] +name = "virtualenv" +version = "20.4.7" +description = "Virtual Python Environment builder" +category = "dev" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7" + +[package.dependencies] +appdirs = ">=1.4.3,<2" +distlib = ">=0.3.1,<1" +filelock = ">=3.0.0,<4" +six = ">=1.9.0,<2" + +[package.extras] +docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=19.9.0rc1)"] +testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)", "xonsh (>=0.9.16)"] + +[metadata] +lock-version = "1.1" +python-versions = "^3.9" +content-hash = "55529582d8d2d5c532c77d89b7d15fde600d866bfc8f9651d1d44a69e328e501" + +[metadata.files] +appdirs = [ + {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, + {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, +] +atomicwrites = [ + {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, + {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, +] +attrs = [ + {file = "attrs-21.2.0-py2.py3-none-any.whl", hash = "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1"}, + {file = "attrs-21.2.0.tar.gz", hash = "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"}, +] +click = [ + {file = "click-8.0.1-py3-none-any.whl", hash = "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"}, + {file = "click-8.0.1.tar.gz", hash = "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a"}, +] +colorama = [ + {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, + {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, +] +coverage = [ + {file = "coverage-5.5-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:b6d534e4b2ab35c9f93f46229363e17f63c53ad01330df9f2d6bd1187e5eaacf"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:b7895207b4c843c76a25ab8c1e866261bcfe27bfaa20c192de5190121770672b"}, + {file = "coverage-5.5-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:c2723d347ab06e7ddad1a58b2a821218239249a9e4365eaff6649d31180c1669"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:900fbf7759501bc7807fd6638c947d7a831fc9fdf742dc10f02956ff7220fa90"}, + {file = "coverage-5.5-cp27-cp27m-manylinux2010_x86_64.whl", hash = "sha256:004d1880bed2d97151facef49f08e255a20ceb6f9432df75f4eef018fdd5a78c"}, + {file = "coverage-5.5-cp27-cp27m-win32.whl", hash = "sha256:06191eb60f8d8a5bc046f3799f8a07a2d7aefb9504b0209aff0b47298333302a"}, + {file = "coverage-5.5-cp27-cp27m-win_amd64.whl", hash = "sha256:7501140f755b725495941b43347ba8a2777407fc7f250d4f5a7d2a1050ba8e82"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:372da284cfd642d8e08ef606917846fa2ee350f64994bebfbd3afb0040436905"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:8963a499849a1fc54b35b1c9f162f4108017b2e6db2c46c1bed93a72262ed083"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_i686.whl", hash = "sha256:869a64f53488f40fa5b5b9dcb9e9b2962a66a87dab37790f3fcfb5144b996ef5"}, + {file = "coverage-5.5-cp27-cp27mu-manylinux2010_x86_64.whl", hash = "sha256:4a7697d8cb0f27399b0e393c0b90f0f1e40c82023ea4d45d22bce7032a5d7b81"}, + {file = "coverage-5.5-cp310-cp310-macosx_10_14_x86_64.whl", hash = "sha256:8d0a0725ad7c1a0bcd8d1b437e191107d457e2ec1084b9f190630a4fb1af78e6"}, + {file = "coverage-5.5-cp310-cp310-manylinux1_x86_64.whl", hash = "sha256:51cb9476a3987c8967ebab3f0fe144819781fca264f57f89760037a2ea191cb0"}, + {file = "coverage-5.5-cp310-cp310-win_amd64.whl", hash = "sha256:c0891a6a97b09c1f3e073a890514d5012eb256845c451bd48f7968ef939bf4ae"}, + {file = "coverage-5.5-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:3487286bc29a5aa4b93a072e9592f22254291ce96a9fbc5251f566b6b7343cdb"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_i686.whl", hash = "sha256:deee1077aae10d8fa88cb02c845cfba9b62c55e1183f52f6ae6a2df6a2187160"}, + {file = "coverage-5.5-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:f11642dddbb0253cc8853254301b51390ba0081750a8ac03f20ea8103f0c56b6"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_i686.whl", hash = "sha256:6c90e11318f0d3c436a42409f2749ee1a115cd8b067d7f14c148f1ce5574d701"}, + {file = "coverage-5.5-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:30c77c1dc9f253283e34c27935fded5015f7d1abe83bc7821680ac444eaf7793"}, + {file = "coverage-5.5-cp35-cp35m-win32.whl", hash = "sha256:9a1ef3b66e38ef8618ce5fdc7bea3d9f45f3624e2a66295eea5e57966c85909e"}, + {file = "coverage-5.5-cp35-cp35m-win_amd64.whl", hash = "sha256:972c85d205b51e30e59525694670de6a8a89691186012535f9d7dbaa230e42c3"}, + {file = "coverage-5.5-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:af0e781009aaf59e25c5a678122391cb0f345ac0ec272c7961dc5455e1c40066"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:74d881fc777ebb11c63736622b60cb9e4aee5cace591ce274fb69e582a12a61a"}, + {file = "coverage-5.5-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:92b017ce34b68a7d67bd6d117e6d443a9bf63a2ecf8567bb3d8c6c7bc5014465"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_i686.whl", hash = "sha256:d636598c8305e1f90b439dbf4f66437de4a5e3c31fdf47ad29542478c8508bbb"}, + {file = "coverage-5.5-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:41179b8a845742d1eb60449bdb2992196e211341818565abded11cfa90efb821"}, + {file = "coverage-5.5-cp36-cp36m-win32.whl", hash = "sha256:040af6c32813fa3eae5305d53f18875bedd079960822ef8ec067a66dd8afcd45"}, + {file = "coverage-5.5-cp36-cp36m-win_amd64.whl", hash = "sha256:5fec2d43a2cc6965edc0bb9e83e1e4b557f76f843a77a2496cbe719583ce8184"}, + {file = "coverage-5.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:18ba8bbede96a2c3dde7b868de9dcbd55670690af0988713f0603f037848418a"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:2910f4d36a6a9b4214bb7038d537f015346f413a975d57ca6b43bf23d6563b53"}, + {file = "coverage-5.5-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:f0b278ce10936db1a37e6954e15a3730bea96a0997c26d7fee88e6c396c2086d"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_i686.whl", hash = "sha256:796c9c3c79747146ebd278dbe1e5c5c05dd6b10cc3bcb8389dfdf844f3ead638"}, + {file = "coverage-5.5-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:53194af30d5bad77fcba80e23a1441c71abfb3e01192034f8246e0d8f99528f3"}, + {file = "coverage-5.5-cp37-cp37m-win32.whl", hash = "sha256:184a47bbe0aa6400ed2d41d8e9ed868b8205046518c52464fde713ea06e3a74a"}, + {file = "coverage-5.5-cp37-cp37m-win_amd64.whl", hash = "sha256:2949cad1c5208b8298d5686d5a85b66aae46d73eec2c3e08c817dd3513e5848a"}, + {file = "coverage-5.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:217658ec7187497e3f3ebd901afdca1af062b42cfe3e0dafea4cced3983739f6"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_i686.whl", hash = "sha256:1aa846f56c3d49205c952d8318e76ccc2ae23303351d9270ab220004c580cfe2"}, + {file = "coverage-5.5-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:24d4a7de75446be83244eabbff746d66b9240ae020ced65d060815fac3423759"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_i686.whl", hash = "sha256:d1f8bf7b90ba55699b3a5e44930e93ff0189aa27186e96071fac7dd0d06a1873"}, + {file = "coverage-5.5-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:970284a88b99673ccb2e4e334cfb38a10aab7cd44f7457564d11898a74b62d0a"}, + {file = "coverage-5.5-cp38-cp38-win32.whl", hash = "sha256:01d84219b5cdbfc8122223b39a954820929497a1cb1422824bb86b07b74594b6"}, + {file = "coverage-5.5-cp38-cp38-win_amd64.whl", hash = "sha256:2e0d881ad471768bf6e6c2bf905d183543f10098e3b3640fc029509530091502"}, + {file = "coverage-5.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d1f9ce122f83b2305592c11d64f181b87153fc2c2bbd3bb4a3dde8303cfb1a6b"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_i686.whl", hash = "sha256:13c4ee887eca0f4c5a247b75398d4114c37882658300e153113dafb1d76de529"}, + {file = "coverage-5.5-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:52596d3d0e8bdf3af43db3e9ba8dcdaac724ba7b5ca3f6358529d56f7a166f8b"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_i686.whl", hash = "sha256:2cafbbb3af0733db200c9b5f798d18953b1a304d3f86a938367de1567f4b5bff"}, + {file = "coverage-5.5-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:44d654437b8ddd9eee7d1eaee28b7219bec228520ff809af170488fd2fed3e2b"}, + {file = "coverage-5.5-cp39-cp39-win32.whl", hash = "sha256:d314ed732c25d29775e84a960c3c60808b682c08d86602ec2c3008e1202e3bb6"}, + {file = "coverage-5.5-cp39-cp39-win_amd64.whl", hash = "sha256:13034c4409db851670bc9acd836243aeee299949bd5673e11844befcb0149f03"}, + {file = "coverage-5.5-pp36-none-any.whl", hash = "sha256:f030f8873312a16414c0d8e1a1ddff2d3235655a2174e3648b4fa66b3f2f1079"}, + {file = "coverage-5.5-pp37-none-any.whl", hash = "sha256:2a3859cb82dcbda1cfd3e6f71c27081d18aa251d20a17d87d26d4cd216fb0af4"}, + {file = "coverage-5.5.tar.gz", hash = "sha256:ebe78fe9a0e874362175b02371bdfbee64d8edc42a044253ddf4ee7d3c15212c"}, +] +distlib = [ + {file = "distlib-0.3.2-py2.py3-none-any.whl", hash = "sha256:23e223426b28491b1ced97dc3bbe183027419dfc7982b4fa2f05d5f3ff10711c"}, + {file = "distlib-0.3.2.zip", hash = "sha256:106fef6dc37dd8c0e2c0a60d3fca3e77460a48907f335fa28420463a6f799736"}, +] +filelock = [ + {file = "filelock-3.0.12-py3-none-any.whl", hash = "sha256:929b7d63ec5b7d6b71b0fa5ac14e030b3f70b75747cef1b10da9b879fef15836"}, + {file = "filelock-3.0.12.tar.gz", hash = "sha256:18d82244ee114f543149c66a6e0c14e9c4f8a1044b5cdaadd0f82159d6a6ff59"}, +] +iniconfig = [ + {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, + {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, +] +isort = [ + {file = "isort-5.9.1-py3-none-any.whl", hash = "sha256:8e2c107091cfec7286bc0f68a547d0ba4c094d460b732075b6fba674f1035c0c"}, + {file = "isort-5.9.1.tar.gz", hash = "sha256:83510593e07e433b77bd5bff0f6f607dbafa06d1a89022616f02d8b699cfcd56"}, +] +mypy = [ + {file = "mypy-0.910-cp35-cp35m-macosx_10_9_x86_64.whl", hash = "sha256:a155d80ea6cee511a3694b108c4494a39f42de11ee4e61e72bc424c490e46457"}, + {file = "mypy-0.910-cp35-cp35m-manylinux1_x86_64.whl", hash = "sha256:b94e4b785e304a04ea0828759172a15add27088520dc7e49ceade7834275bedb"}, + {file = "mypy-0.910-cp35-cp35m-manylinux2010_x86_64.whl", hash = "sha256:088cd9c7904b4ad80bec811053272986611b84221835e079be5bcad029e79dd9"}, + {file = "mypy-0.910-cp35-cp35m-win_amd64.whl", hash = "sha256:adaeee09bfde366d2c13fe6093a7df5df83c9a2ba98638c7d76b010694db760e"}, + {file = "mypy-0.910-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:ecd2c3fe726758037234c93df7e98deb257fd15c24c9180dacf1ef829da5f921"}, + {file = "mypy-0.910-cp36-cp36m-manylinux1_x86_64.whl", hash = "sha256:d9dd839eb0dc1bbe866a288ba3c1afc33a202015d2ad83b31e875b5905a079b6"}, + {file = "mypy-0.910-cp36-cp36m-manylinux2010_x86_64.whl", hash = "sha256:3e382b29f8e0ccf19a2df2b29a167591245df90c0b5a2542249873b5c1d78212"}, + {file = "mypy-0.910-cp36-cp36m-win_amd64.whl", hash = "sha256:53fd2eb27a8ee2892614370896956af2ff61254c275aaee4c230ae771cadd885"}, + {file = "mypy-0.910-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b6fb13123aeef4a3abbcfd7e71773ff3ff1526a7d3dc538f3929a49b42be03f0"}, + {file = "mypy-0.910-cp37-cp37m-manylinux1_x86_64.whl", hash = "sha256:e4dab234478e3bd3ce83bac4193b2ecd9cf94e720ddd95ce69840273bf44f6de"}, + {file = "mypy-0.910-cp37-cp37m-manylinux2010_x86_64.whl", hash = "sha256:7df1ead20c81371ccd6091fa3e2878559b5c4d4caadaf1a484cf88d93ca06703"}, + {file = "mypy-0.910-cp37-cp37m-win_amd64.whl", hash = "sha256:0aadfb2d3935988ec3815952e44058a3100499f5be5b28c34ac9d79f002a4a9a"}, + {file = "mypy-0.910-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ec4e0cd079db280b6bdabdc807047ff3e199f334050db5cbb91ba3e959a67504"}, + {file = "mypy-0.910-cp38-cp38-manylinux1_x86_64.whl", hash = "sha256:119bed3832d961f3a880787bf621634ba042cb8dc850a7429f643508eeac97b9"}, + {file = "mypy-0.910-cp38-cp38-manylinux2010_x86_64.whl", hash = "sha256:866c41f28cee548475f146aa4d39a51cf3b6a84246969f3759cb3e9c742fc072"}, + {file = "mypy-0.910-cp38-cp38-win_amd64.whl", hash = "sha256:ceb6e0a6e27fb364fb3853389607cf7eb3a126ad335790fa1e14ed02fba50811"}, + {file = "mypy-0.910-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1a85e280d4d217150ce8cb1a6dddffd14e753a4e0c3cf90baabb32cefa41b59e"}, + {file = "mypy-0.910-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42c266ced41b65ed40a282c575705325fa7991af370036d3f134518336636f5b"}, + {file = "mypy-0.910-cp39-cp39-manylinux1_x86_64.whl", hash = "sha256:3c4b8ca36877fc75339253721f69603a9c7fdb5d4d5a95a1a1b899d8b86a4de2"}, + {file = "mypy-0.910-cp39-cp39-manylinux2010_x86_64.whl", hash = "sha256:c0df2d30ed496a08de5daed2a9ea807d07c21ae0ab23acf541ab88c24b26ab97"}, + {file = "mypy-0.910-cp39-cp39-win_amd64.whl", hash = "sha256:c6c2602dffb74867498f86e6129fd52a2770c48b7cd3ece77ada4fa38f94eba8"}, + {file = "mypy-0.910-py3-none-any.whl", hash = "sha256:ef565033fa5a958e62796867b1df10c40263ea9ded87164d67572834e57a174d"}, + {file = "mypy-0.910.tar.gz", hash = "sha256:704098302473cb31a218f1775a873b376b30b4c18229421e9e9dc8916fd16150"}, +] +mypy-extensions = [ + {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, + {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"}, +] +nuitka = [ + {file = "Nuitka-0.6.16.tar.gz", hash = "sha256:f194af0eecacda5b44c7bd26a29ddf3a9165dfda02731466ce17ed1ebd151b8b"}, +] +packaging = [ + {file = "packaging-20.9-py2.py3-none-any.whl", hash = "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"}, + {file = "packaging-20.9.tar.gz", hash = "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5"}, +] +pluggy = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] +protobuf = [ + {file = "protobuf-3.17.3-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:ab6bb0e270c6c58e7ff4345b3a803cc59dbee19ddf77a4719c5b635f1d547aa8"}, + {file = "protobuf-3.17.3-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:13ee7be3c2d9a5d2b42a1030976f760f28755fcf5863c55b1460fd205e6cd637"}, + {file = "protobuf-3.17.3-cp35-cp35m-macosx_10_9_intel.whl", hash = "sha256:1556a1049ccec58c7855a78d27e5c6e70e95103b32de9142bae0576e9200a1b0"}, + {file = "protobuf-3.17.3-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:f0e59430ee953184a703a324b8ec52f571c6c4259d496a19d1cabcdc19dabc62"}, + {file = "protobuf-3.17.3-cp35-cp35m-win32.whl", hash = "sha256:a981222367fb4210a10a929ad5983ae93bd5a050a0824fc35d6371c07b78caf6"}, + {file = "protobuf-3.17.3-cp35-cp35m-win_amd64.whl", hash = "sha256:6d847c59963c03fd7a0cd7c488cadfa10cda4fff34d8bc8cba92935a91b7a037"}, + {file = "protobuf-3.17.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:145ce0af55c4259ca74993ddab3479c78af064002ec8227beb3d944405123c71"}, + {file = "protobuf-3.17.3-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6ce4d8bf0321e7b2d4395e253f8002a1a5ffbcfd7bcc0a6ba46712c07d47d0b4"}, + {file = "protobuf-3.17.3-cp36-cp36m-win32.whl", hash = "sha256:7a4c97961e9e5b03a56f9a6c82742ed55375c4a25f2692b625d4087d02ed31b9"}, + {file = "protobuf-3.17.3-cp36-cp36m-win_amd64.whl", hash = "sha256:a22b3a0dbac6544dacbafd4c5f6a29e389a50e3b193e2c70dae6bbf7930f651d"}, + {file = "protobuf-3.17.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ffea251f5cd3c0b9b43c7a7a912777e0bc86263436a87c2555242a348817221b"}, + {file = "protobuf-3.17.3-cp37-cp37m-manylinux2014_aarch64.whl", hash = "sha256:9b7a5c1022e0fa0dbde7fd03682d07d14624ad870ae52054849d8960f04bc764"}, + {file = "protobuf-3.17.3-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:8727ee027157516e2c311f218ebf2260a18088ffb2d29473e82add217d196b1c"}, + {file = "protobuf-3.17.3-cp37-cp37m-win32.whl", hash = "sha256:14c1c9377a7ffbeaccd4722ab0aa900091f52b516ad89c4b0c3bb0a4af903ba5"}, + {file = "protobuf-3.17.3-cp37-cp37m-win_amd64.whl", hash = "sha256:c56c050a947186ba51de4f94ab441d7f04fcd44c56df6e922369cc2e1a92d683"}, + {file = "protobuf-3.17.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:2ae692bb6d1992afb6b74348e7bb648a75bb0d3565a3f5eea5bec8f62bd06d87"}, + {file = "protobuf-3.17.3-cp38-cp38-manylinux2014_aarch64.whl", hash = "sha256:99938f2a2d7ca6563c0ade0c5ca8982264c484fdecf418bd68e880a7ab5730b1"}, + {file = "protobuf-3.17.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6902a1e4b7a319ec611a7345ff81b6b004b36b0d2196ce7a748b3493da3d226d"}, + {file = "protobuf-3.17.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ffbd23640bb7403574f7aff8368e2aeb2ec9a5c6306580be48ac59a6bac8bde"}, + {file = "protobuf-3.17.3-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:26010f693b675ff5a1d0e1bdb17689b8b716a18709113288fead438703d45539"}, + {file = "protobuf-3.17.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e76d9686e088fece2450dbc7ee905f9be904e427341d289acbe9ad00b78ebd47"}, + {file = "protobuf-3.17.3-py2.py3-none-any.whl", hash = "sha256:2bfb815216a9cd9faec52b16fd2bfa68437a44b67c56bee59bc3926522ecb04e"}, + {file = "protobuf-3.17.3.tar.gz", hash = "sha256:72804ea5eaa9c22a090d2803813e280fb273b62d5ae497aaf3553d141c4fdd7b"}, +] +py = [ + {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, + {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, +] +pyparsing = [ + {file = "pyparsing-2.4.7-py2.py3-none-any.whl", hash = "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"}, + {file = "pyparsing-2.4.7.tar.gz", hash = "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1"}, +] +pytest = [ + {file = "pytest-6.2.4-py3-none-any.whl", hash = "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"}, + {file = "pytest-6.2.4.tar.gz", hash = "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b"}, +] +pytest-cov = [ + {file = "pytest-cov-2.12.1.tar.gz", hash = "sha256:261ceeb8c227b726249b376b8526b600f38667ee314f910353fa318caa01f4d7"}, + {file = "pytest_cov-2.12.1-py2.py3-none-any.whl", hash = "sha256:261bb9e47e65bd099c89c3edf92972865210c36813f80ede5277dceb77a4a62a"}, +] +six = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] +toml = [ + {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, + {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, +] +tox = [ + {file = "tox-3.23.1-py2.py3-none-any.whl", hash = "sha256:b0b5818049a1c1997599d42012a637a33f24c62ab8187223fdd318fa8522637b"}, + {file = "tox-3.23.1.tar.gz", hash = "sha256:307a81ddb82bd463971a273f33e9533a24ed22185f27db8ce3386bff27d324e3"}, +] +tox-poetry = [ + {file = "tox-poetry-0.4.0.tar.gz", hash = "sha256:b926723cb1dea87902299dd8cad458d0b80ab7345bbf7983e6dab1cbf951090d"}, + {file = "tox_poetry-0.4.0-py2.py3-none-any.whl", hash = "sha256:b529f3b534b351b7cf9506caa66e2f230dded38b2b74245559c3d662685f4494"}, +] +types-futures = [ + {file = "types-futures-0.1.6.tar.gz", hash = "sha256:da372dd55dc08c257de1e3dfd56273e44af9668e077047b0509adcfc43dd2838"}, + {file = "types_futures-0.1.6-py3-none-any.whl", hash = "sha256:7cb32c3fb4885089d78873a28ad33db3d5300661eac8b3ad327f4f5616fdf742"}, +] +types-protobuf = [ + {file = "types-protobuf-0.1.13.tar.gz", hash = "sha256:1c5800d7b6bb5ccf27d5256903fd32d96745a54b3a32179fc1606e8179a035e9"}, + {file = "types_protobuf-0.1.13-py2.py3-none-any.whl", hash = "sha256:771c272572be5396a1946f035db730d8d6be03ad21ff6885e0e7d1b18549af89"}, +] +typing-extensions = [ + {file = "typing_extensions-3.10.0.0-py2-none-any.whl", hash = "sha256:0ac0f89795dd19de6b97debb0c6af1c70987fd80a2d62d1958f7e56fcc31b497"}, + {file = "typing_extensions-3.10.0.0-py3-none-any.whl", hash = "sha256:779383f6086d90c99ae41cf0ff39aac8a7937a9283ce0a414e5dd782f4c94a84"}, + {file = "typing_extensions-3.10.0.0.tar.gz", hash = "sha256:50b6f157849174217d0656f99dc82fe932884fb250826c18350e159ec6cdf342"}, +] +virtualenv = [ + {file = "virtualenv-20.4.7-py2.py3-none-any.whl", hash = "sha256:2b0126166ea7c9c3661f5b8e06773d28f83322de7a3ff7d06f0aed18c9de6a76"}, + {file = "virtualenv-20.4.7.tar.gz", hash = "sha256:14fdf849f80dbb29a4eb6caa9875d476ee2a5cf76a5f5415fa2f1606010ab467"}, +] diff --git a/otpauth_migration_decoder/pyproject.toml b/otpauth_migration_decoder/pyproject.toml new file mode 100644 index 0000000..bd68d4a --- /dev/null +++ b/otpauth_migration_decoder/pyproject.toml @@ -0,0 +1,24 @@ +[tool.poetry] +name = "otpauth-migration-decoder" +version = "0.1.1" +description = "Convert otpauth-migration to plain link" +authors = ["digitalduke "] + +[tool.poetry.dependencies] +python = "^3.9" +protobuf = "^3.17.3" +click = "^8.0.1" + +[tool.poetry.dev-dependencies] +Nuitka = "^0.6.15" +isort = "^5.9.1" +pytest = "*" +pytest-cov = "*" +tox = "*" +tox-poetry = "*" +mypy = "^0.910" +types-protobuf = "*" + +[build-system] +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" diff --git a/otpauth_migration_decoder/setup.cfg b/otpauth_migration_decoder/setup.cfg new file mode 100644 index 0000000..466b423 --- /dev/null +++ b/otpauth_migration_decoder/setup.cfg @@ -0,0 +1,12 @@ +[tool:isort] +line_length = 120 +indent = ' ' +multi_line_output = 3 +no_lines_before = LOCALFOLDER +use_parentheses = true +lines_after_imports = 2 +skip_glob = .tox,.direnv,*_pb2.py +include_trailing_comma = true +case_sensitive = true +force_grid_wrap = 2 +combine_as_imports = true diff --git a/otpauth_migration_decoder/src/decoder.py b/otpauth_migration_decoder/src/decoder.py new file mode 100644 index 0000000..063d9bf --- /dev/null +++ b/otpauth_migration_decoder/src/decoder.py @@ -0,0 +1,109 @@ +from base64 import ( + b32encode, + b64decode, +) +from collections.abc import Generator +from typing import ( + Any, + Dict, + List, + Union, +) +from urllib.parse import ( + ParseResult, + parse_qs, + quote, + urlencode, + urlparse, +) + +from .otpauth_enums import ( + Algorithm, + DigitCount, + OtpType, +) +from .otpauth_migration_pb2 import Payload + + +SCHEME = 'otpauth-migration' +HOSTNAME = 'offline' +PAYLOAD_MARK = 'data' +EXAMPLE_PAYLOAD = 'CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC' +EXAMPLE_MIGRATION = f'{SCHEME}://{HOSTNAME}?{PAYLOAD_MARK}={EXAMPLE_PAYLOAD}' + + +def is_migration_incorrect( + *, + parsed_url: ParseResult, + parsed_qs: Dict[str, Any], +) -> bool: + return ( + parsed_url.scheme != SCHEME + or parsed_url.hostname != HOSTNAME + or PAYLOAD_MARK not in parsed_qs + or not isinstance(parsed_qs[PAYLOAD_MARK], list) + ) + + +def decoded_data(data: List[str]) -> Generator: + for data_item in data: + yield b64decode(data_item) + + +def decode_secret(secret: bytes) -> str: + return str(b32encode(secret), 'utf-8').replace('=', '') + + +def get_url_params(otp: Payload.OtpParameters) -> str: + params: dict[str, Union[str, int]] = {} + + if otp.algorithm: + params.update(algorithm=Algorithm.get(otp.algorithm, '')) + if otp.digits: + params.update(digits=DigitCount.get(otp.digits, '')) + if otp.issuer: + params.update(issuer=otp.issuer) + if otp.secret: + otp_secret = decode_secret(otp.secret) + params.update(secret=otp_secret) + + return urlencode(params) + + +def get_otpauth_url(otp: Payload.OtpParameters) -> str: + otp_type = OtpType.get(otp.type, '') + otp_name = quote(otp.name) + otp_params = get_url_params(otp) + + return f'otpauth://{otp_type}/{otp_name}?{otp_params}' + + +def validate_migration(migration: str) -> list: + url: ParseResult = urlparse(migration) + qs: Dict[str, Any] = parse_qs(url.query) + + + return qs[PAYLOAD_MARK] + + +def decoder(migration_data: list) -> list: + """Convert Google Authenticator data to plain otpauth links""" + result = [] + + for payload in decoded_data(data=migration_data): + migration_payload = Payload() + migration_payload.ParseFromString(payload) + + for otp_item in migration_payload.otp_parameters: + result.append(get_otpauth_url(otp_item)) + + return result + +def start_decode_migration(migration_code: str) -> list: + payload_list = validate_migration(migration_code) + results = decoder(payload_list) + return results + +if __name__ == "__main__": + start_decode_migration("otpauth-migration://offline?data=CkAKFFUlyp7lQLXJnL8Oc5%2BjVRVCEIMPEhpHb29nbGU6cHVha2Foa2luQGdtYWlsLmNvbRoGR29vZ2xlIAEoATACCj0KFEMSL8awHj%2BEFzX2cODi9rNgfvPFEhdHb29nbGU6a2Foa2luQGdtYWlsLmNvbRoGR29vZ2xlIAEoATACCi8KEBdHlsAmppdFyWD18FyPcQkSDXNhbXVlbEBhcG9sbG8aBmFwb2xsbyABKAEwAgouChQpi9Btg%2BHoQJj6tilYEsOef%2BSkiRIQc2FtdWVsQGFwaHJvZGl0ZSABKAEwAgo1ChDnUjGxj%2BJmsJJxdcrbBNpqEhBzYW11ZWxAc2FtdWVscHVhGglzYW11ZWxwdWEgASgBMAIKUAoUihAclMtkwYi3NfvbkHcHmgAi2yESJmdpdGxhYi5jb206Z2l0bGFiLmNvbV9rYWhraW5AZ21haWwuY29tGgpnaXRsYWIuY29tIAEoATACCioKCsaExzWrYHtcsLoSDkdpdEh1Yjp0ZWxib29uGgZHaXRIdWIgASgBMAIKbwooyfRfWMXsY%2B%2B%2FVvZKl9ypsl1VLAZrOPmyr3AsGxeCOuTHmGdGpt4OyBIoQW1hem9uIFdlYiBTZXJ2aWNlczp0ZWxib29uQDI2Nzk5NDAyOTgxMBoTQW1hem9uIFdlYiBTZXJ2aWNlcyABKAEwAgp%2FCiieutAtJJdKrtOMVznsVaMbvrpM%2FNvwURQSChMpPzgI2PlVshsoRk3QEjhBbWF6b24gV2ViIFNlcnZpY2VzOnJvb3QtYWNjb3VudC1tZmEtZGV2aWNlQDI2Nzk5NDAyOTgxMBoTQW1hem9uIFdlYiBTZXJ2aWNlcyABKAEwAgozCgr2b4%2BKdfJzdo%2ByEhRzYW11ZWxAd2F0Y2h0b3dyLmNvbRoJTWljcm9zb2Z0IAEoATACEAEYAiAAKKessZb6%2F%2F%2F%2F%2FwE%3D") + diff --git a/otpauth_migration_decoder/src/otpauth-migration.proto b/otpauth_migration_decoder/src/otpauth-migration.proto new file mode 100644 index 0000000..4105dba --- /dev/null +++ b/otpauth_migration_decoder/src/otpauth-migration.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package otpauth_migration; + +message Payload { + enum Algorithm { + ALGORITHM_UNSPECIFIED = 0; + ALGORITHM_SHA1 = 1; + ALGORITHM_SHA256 = 2; + ALGORITHM_SHA512 = 3; + ALGORITHM_MD5 = 4; + } + enum DigitCount { + DIGIT_COUNT_UNSPECIFIED = 0; + DIGIT_COUNT_SIX = 1; + DIGIT_COUNT_EIGHT = 2; + } + enum OtpType { + OTP_TYPE_UNSPECIFIED = 0; + OTP_TYPE_HOTP = 1; + OTP_TYPE_TOTP = 2; + } + message OtpParameters { + bytes secret = 1; + string name = 2; + string issuer = 3; + Algorithm algorithm = 4; + DigitCount digits = 5; + OtpType type = 6; + int64 counter = 7; + } + repeated OtpParameters otp_parameters = 1; + int32 version = 2; + int32 batch_size = 3; + int32 batch_index = 4; + int32 batch_id = 5; +} \ No newline at end of file diff --git a/otpauth_migration_decoder/src/otpauth_enums.py b/otpauth_migration_decoder/src/otpauth_enums.py new file mode 100644 index 0000000..2c27206 --- /dev/null +++ b/otpauth_migration_decoder/src/otpauth_enums.py @@ -0,0 +1,19 @@ +from typing import Mapping + + +Algorithm: Mapping[int, str] = { + 1: 'SHA1', + 2: 'SHA256', + 3: 'SHA512', + 4: 'MD5', +} + +DigitCount: Mapping[int, str] = { + 1: '6', + 2: '8', +} + +OtpType: Mapping[int, str] = { + 1: 'hotp', + 2: 'totp', +} diff --git a/otpauth_migration_decoder/src/otpauth_migration_pb2.py b/otpauth_migration_decoder/src/otpauth_migration_pb2.py new file mode 100644 index 0000000..e7e3c31 --- /dev/null +++ b/otpauth_migration_decoder/src/otpauth_migration_pb2.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: otpauth-migration.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='otpauth-migration.proto', + package='otpauth_migration', + syntax='proto3', + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x17otpauth-migration.proto\x12\x11otpauth_migration\"\xa7\x05\n\x07Payload\x12@\n\x0eotp_parameters\x18\x01 \x03(\x0b\x32(.otpauth_migration.Payload.OtpParameters\x12\x0f\n\x07version\x18\x02 \x01(\x05\x12\x12\n\nbatch_size\x18\x03 \x01(\x05\x12\x13\n\x0b\x62\x61tch_index\x18\x04 \x01(\x05\x12\x10\n\x08\x62\x61tch_id\x18\x05 \x01(\x05\x1a\xf0\x01\n\rOtpParameters\x12\x0e\n\x06secret\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06issuer\x18\x03 \x01(\t\x12\x37\n\talgorithm\x18\x04 \x01(\x0e\x32$.otpauth_migration.Payload.Algorithm\x12\x35\n\x06\x64igits\x18\x05 \x01(\x0e\x32%.otpauth_migration.Payload.DigitCount\x12\x30\n\x04type\x18\x06 \x01(\x0e\x32\".otpauth_migration.Payload.OtpType\x12\x0f\n\x07\x63ounter\x18\x07 \x01(\x03\"y\n\tAlgorithm\x12\x19\n\x15\x41LGORITHM_UNSPECIFIED\x10\x00\x12\x12\n\x0e\x41LGORITHM_SHA1\x10\x01\x12\x14\n\x10\x41LGORITHM_SHA256\x10\x02\x12\x14\n\x10\x41LGORITHM_SHA512\x10\x03\x12\x11\n\rALGORITHM_MD5\x10\x04\"U\n\nDigitCount\x12\x1b\n\x17\x44IGIT_COUNT_UNSPECIFIED\x10\x00\x12\x13\n\x0f\x44IGIT_COUNT_SIX\x10\x01\x12\x15\n\x11\x44IGIT_COUNT_EIGHT\x10\x02\"I\n\x07OtpType\x12\x18\n\x14OTP_TYPE_UNSPECIFIED\x10\x00\x12\x11\n\rOTP_TYPE_HOTP\x10\x01\x12\x11\n\rOTP_TYPE_TOTP\x10\x02\x62\x06proto3' +) + + + +_PAYLOAD_ALGORITHM = _descriptor.EnumDescriptor( + name='Algorithm', + full_name='otpauth_migration.Payload.Algorithm', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='ALGORITHM_UNSPECIFIED', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ALGORITHM_SHA1', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ALGORITHM_SHA256', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ALGORITHM_SHA512', index=3, number=3, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ALGORITHM_MD5', index=4, number=4, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=447, + serialized_end=568, +) +_sym_db.RegisterEnumDescriptor(_PAYLOAD_ALGORITHM) + +_PAYLOAD_DIGITCOUNT = _descriptor.EnumDescriptor( + name='DigitCount', + full_name='otpauth_migration.Payload.DigitCount', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='DIGIT_COUNT_UNSPECIFIED', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DIGIT_COUNT_SIX', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='DIGIT_COUNT_EIGHT', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=570, + serialized_end=655, +) +_sym_db.RegisterEnumDescriptor(_PAYLOAD_DIGITCOUNT) + +_PAYLOAD_OTPTYPE = _descriptor.EnumDescriptor( + name='OtpType', + full_name='otpauth_migration.Payload.OtpType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='OTP_TYPE_UNSPECIFIED', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='OTP_TYPE_HOTP', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='OTP_TYPE_TOTP', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=657, + serialized_end=730, +) +_sym_db.RegisterEnumDescriptor(_PAYLOAD_OTPTYPE) + + +_PAYLOAD_OTPPARAMETERS = _descriptor.Descriptor( + name='OtpParameters', + full_name='otpauth_migration.Payload.OtpParameters', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='secret', full_name='otpauth_migration.Payload.OtpParameters.secret', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='name', full_name='otpauth_migration.Payload.OtpParameters.name', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='issuer', full_name='otpauth_migration.Payload.OtpParameters.issuer', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='algorithm', full_name='otpauth_migration.Payload.OtpParameters.algorithm', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='digits', full_name='otpauth_migration.Payload.OtpParameters.digits', index=4, + number=5, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type', full_name='otpauth_migration.Payload.OtpParameters.type', index=5, + number=6, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='counter', full_name='otpauth_migration.Payload.OtpParameters.counter', index=6, + number=7, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=205, + serialized_end=445, +) + +_PAYLOAD = _descriptor.Descriptor( + name='Payload', + full_name='otpauth_migration.Payload', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='otp_parameters', full_name='otpauth_migration.Payload.otp_parameters', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='version', full_name='otpauth_migration.Payload.version', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='batch_size', full_name='otpauth_migration.Payload.batch_size', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='batch_index', full_name='otpauth_migration.Payload.batch_index', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='batch_id', full_name='otpauth_migration.Payload.batch_id', index=4, + number=5, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_PAYLOAD_OTPPARAMETERS, ], + enum_types=[ + _PAYLOAD_ALGORITHM, + _PAYLOAD_DIGITCOUNT, + _PAYLOAD_OTPTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=51, + serialized_end=730, +) + +_PAYLOAD_OTPPARAMETERS.fields_by_name['algorithm'].enum_type = _PAYLOAD_ALGORITHM +_PAYLOAD_OTPPARAMETERS.fields_by_name['digits'].enum_type = _PAYLOAD_DIGITCOUNT +_PAYLOAD_OTPPARAMETERS.fields_by_name['type'].enum_type = _PAYLOAD_OTPTYPE +_PAYLOAD_OTPPARAMETERS.containing_type = _PAYLOAD +_PAYLOAD.fields_by_name['otp_parameters'].message_type = _PAYLOAD_OTPPARAMETERS +_PAYLOAD_ALGORITHM.containing_type = _PAYLOAD +_PAYLOAD_DIGITCOUNT.containing_type = _PAYLOAD +_PAYLOAD_OTPTYPE.containing_type = _PAYLOAD +DESCRIPTOR.message_types_by_name['Payload'] = _PAYLOAD +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Payload = _reflection.GeneratedProtocolMessageType('Payload', (_message.Message,), { + + 'OtpParameters' : _reflection.GeneratedProtocolMessageType('OtpParameters', (_message.Message,), { + 'DESCRIPTOR' : _PAYLOAD_OTPPARAMETERS, + '__module__' : 'otpauth_migration_pb2' + # @@protoc_insertion_point(class_scope:otpauth_migration.Payload.OtpParameters) + }) + , + 'DESCRIPTOR' : _PAYLOAD, + '__module__' : 'otpauth_migration_pb2' + # @@protoc_insertion_point(class_scope:otpauth_migration.Payload) + }) +_sym_db.RegisterMessage(Payload) +_sym_db.RegisterMessage(Payload.OtpParameters) + + +# @@protoc_insertion_point(module_scope) diff --git a/otpauth_migration_decoder/src/otpauth_migration_pb2.pyi b/otpauth_migration_decoder/src/otpauth_migration_pb2.pyi new file mode 100644 index 0000000..07016a8 --- /dev/null +++ b/otpauth_migration_decoder/src/otpauth_migration_pb2.pyi @@ -0,0 +1,111 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper +import google.protobuf.message +import typing +import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor = ... + +class Payload(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + class Algorithm(metaclass=_Algorithm): + V = typing.NewType('V', builtins.int) + + ALGORITHM_UNSPECIFIED = Payload.Algorithm.V(0) + ALGORITHM_SHA1 = Payload.Algorithm.V(1) + ALGORITHM_SHA256 = Payload.Algorithm.V(2) + ALGORITHM_SHA512 = Payload.Algorithm.V(3) + ALGORITHM_MD5 = Payload.Algorithm.V(4) + + class _Algorithm(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[Algorithm.V], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + ALGORITHM_UNSPECIFIED = Payload.Algorithm.V(0) + ALGORITHM_SHA1 = Payload.Algorithm.V(1) + ALGORITHM_SHA256 = Payload.Algorithm.V(2) + ALGORITHM_SHA512 = Payload.Algorithm.V(3) + ALGORITHM_MD5 = Payload.Algorithm.V(4) + + class DigitCount(metaclass=_DigitCount): + V = typing.NewType('V', builtins.int) + + DIGIT_COUNT_UNSPECIFIED = Payload.DigitCount.V(0) + DIGIT_COUNT_SIX = Payload.DigitCount.V(1) + DIGIT_COUNT_EIGHT = Payload.DigitCount.V(2) + + class _DigitCount(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[DigitCount.V], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + DIGIT_COUNT_UNSPECIFIED = Payload.DigitCount.V(0) + DIGIT_COUNT_SIX = Payload.DigitCount.V(1) + DIGIT_COUNT_EIGHT = Payload.DigitCount.V(2) + + class OtpType(metaclass=_OtpType): + V = typing.NewType('V', builtins.int) + + OTP_TYPE_UNSPECIFIED = Payload.OtpType.V(0) + OTP_TYPE_HOTP = Payload.OtpType.V(1) + OTP_TYPE_TOTP = Payload.OtpType.V(2) + + class _OtpType(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[OtpType.V], builtins.type): + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor = ... + OTP_TYPE_UNSPECIFIED = Payload.OtpType.V(0) + OTP_TYPE_HOTP = Payload.OtpType.V(1) + OTP_TYPE_TOTP = Payload.OtpType.V(2) + + class OtpParameters(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor = ... + SECRET_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + ISSUER_FIELD_NUMBER: builtins.int + ALGORITHM_FIELD_NUMBER: builtins.int + DIGITS_FIELD_NUMBER: builtins.int + TYPE_FIELD_NUMBER: builtins.int + COUNTER_FIELD_NUMBER: builtins.int + secret: builtins.bytes = ... + name: typing.Text = ... + issuer: typing.Text = ... + algorithm: global___Payload.Algorithm.V = ... + digits: global___Payload.DigitCount.V = ... + type: global___Payload.OtpType.V = ... + counter: builtins.int = ... + + def __init__(self, + *, + secret : builtins.bytes = ..., + name : typing.Text = ..., + issuer : typing.Text = ..., + algorithm : global___Payload.Algorithm.V = ..., + digits : global___Payload.DigitCount.V = ..., + type : global___Payload.OtpType.V = ..., + counter : builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"algorithm",b"algorithm",u"counter",b"counter",u"digits",b"digits",u"issuer",b"issuer",u"name",b"name",u"secret",b"secret",u"type",b"type"]) -> None: ... + + OTP_PARAMETERS_FIELD_NUMBER: builtins.int + VERSION_FIELD_NUMBER: builtins.int + BATCH_SIZE_FIELD_NUMBER: builtins.int + BATCH_INDEX_FIELD_NUMBER: builtins.int + BATCH_ID_FIELD_NUMBER: builtins.int + version: builtins.int = ... + batch_size: builtins.int = ... + batch_index: builtins.int = ... + batch_id: builtins.int = ... + + @property + def otp_parameters(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Payload.OtpParameters]: ... + + def __init__(self, + *, + otp_parameters : typing.Optional[typing.Iterable[global___Payload.OtpParameters]] = ..., + version : builtins.int = ..., + batch_size : builtins.int = ..., + batch_index : builtins.int = ..., + batch_id : builtins.int = ..., + ) -> None: ... + def ClearField(self, field_name: typing_extensions.Literal[u"batch_id",b"batch_id",u"batch_index",b"batch_index",u"batch_size",b"batch_size",u"otp_parameters",b"otp_parameters",u"version",b"version"]) -> None: ... +global___Payload = Payload diff --git a/otpauth_migration_decoder/tests/__init__.py b/otpauth_migration_decoder/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/otpauth_migration_decoder/tests/test_decode_secret.py b/otpauth_migration_decoder/tests/test_decode_secret.py new file mode 100644 index 0000000..1b8e903 --- /dev/null +++ b/otpauth_migration_decoder/tests/test_decode_secret.py @@ -0,0 +1,19 @@ +import pytest + +from src.decoder import decode_secret + + +@pytest.mark.parametrize( + 'secret,expected_result', + [ + (b'Hello!\xde\xad\xbe\xef', 'JBSWY3DPEHPK3PXP', ), + (b'Hello!', 'JBSWY3DPEE',), + (b'\xde\xad\xbe\xef', '32W353Y',), + ], +) +def test_decode_secret(secret, expected_result): + # act + result = decode_secret(secret) + + # assert + assert result == expected_result diff --git a/otpauth_migration_decoder/tests/test_decoded_data.py b/otpauth_migration_decoder/tests/test_decoded_data.py new file mode 100644 index 0000000..c62563c --- /dev/null +++ b/otpauth_migration_decoder/tests/test_decoded_data.py @@ -0,0 +1,31 @@ +from src.decoder import decoded_data + + +def test_decoded_data__expected(): + # arrange + data = ['CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC', ] + + # act + generator = decoded_data(data) + result = list(generator) + + # assert + assert result == [b'\n1\n\nHello!\xde\xad\xbe\xef\x12\x18Example:alice@google.com\x1a\x07Example0\x02', ] + + +def test_decoded_data__list__expected(): + # arrange + data = [ + 'CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC', + 'CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC', + ] + + # act + generator = decoded_data(data) + result = list(generator) + + # assert + assert result == [ + b'\n1\n\nHello!\xde\xad\xbe\xef\x12\x18Example:alice@google.com\x1a\x07Example0\x02', + b'\n1\n\nHello!\xde\xad\xbe\xef\x12\x18Example:alice@google.com\x1a\x07Example0\x02', + ] diff --git a/otpauth_migration_decoder/tests/test_get_otpauth_url.py b/otpauth_migration_decoder/tests/test_get_otpauth_url.py new file mode 100644 index 0000000..544196b --- /dev/null +++ b/otpauth_migration_decoder/tests/test_get_otpauth_url.py @@ -0,0 +1,18 @@ +from src.decoder import get_otpauth_url +from src.otpauth_migration_pb2 import Payload + + +def test_get_otpauth_url(): + # arrange + otp = Payload.OtpParameters( + secret=b'Hello!\336\255\276\357', + name='Example:alice@google.com', + issuer='Example', + type=2 + ) + + # act + result = get_otpauth_url(otp) + + # assert + assert result == 'otpauth://totp/Example%3Aalice%40google.com?issuer=Example&secret=JBSWY3DPEHPK3PXP' diff --git a/otpauth_migration_decoder/tests/test_get_url_params.py b/otpauth_migration_decoder/tests/test_get_url_params.py new file mode 100644 index 0000000..524b077 --- /dev/null +++ b/otpauth_migration_decoder/tests/test_get_url_params.py @@ -0,0 +1,18 @@ +from src.decoder import get_url_params +from src.otpauth_migration_pb2 import Payload + + +def test_get_url_params(): + # arrange + otp = Payload.OtpParameters( + secret=b'Hello!\336\255\276\357', + name='Example:alice@google.com', + issuer='Example', + type=2 + ) + + # act + result = get_url_params(otp) + + # assert + assert result == 'issuer=Example&secret=JBSWY3DPEHPK3PXP' diff --git a/otpauth_migration_decoder/tests/test_validate_migration.py b/otpauth_migration_decoder/tests/test_validate_migration.py new file mode 100644 index 0000000..6259425 --- /dev/null +++ b/otpauth_migration_decoder/tests/test_validate_migration.py @@ -0,0 +1,31 @@ +import pytest +from click import BadParameter + +from src.decoder import validate_migration + + +def test_validate_migration__migration__ok(): + # arrange + migration = 'otpauth-migration://offline?data=CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC' + + # act + result = validate_migration(None, None, migration) + + # assert + assert result == ['CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC'] + + +@pytest.mark.parametrize( + 'broken_migration', + [ + 'otpauth-migration://online?data=CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC', + 'CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC', + 'offline?data=CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC', + 'otpauth-migration://online?data=Cu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC', + 'data=CjEKCkhlbGxvId6tvu8SGEV4YW1wbGU6YWxpY2VAZ29vZ2xlLmNvbRoHRXhhbXBsZTAC', + ] +) +def test_validate_migration__broken_migration__raise(broken_migration): + # act & assert + with pytest.raises(BadParameter): + validate_migration(None, None, broken_migration) diff --git a/otpauth_migration_decoder/tox.ini b/otpauth_migration_decoder/tox.ini new file mode 100644 index 0000000..19e9d8e --- /dev/null +++ b/otpauth_migration_decoder/tox.ini @@ -0,0 +1,12 @@ +[tox] +envlist = py39 + +[testenv] +setenv = + COVERAGE_FILE = {envlogdir}/.coverage + PYTHONPATH = src +commands = + coverage run --source=src --module pytest --verbose tests + coverage report --show-missing + isort . + mypy src --pretty diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f9fcc45 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +pillow +pyzbar +qrcode \ No newline at end of file