Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
65c52f4d81 | ||
|
|
8ba4439305 | ||
|
|
10bc6959a3 | ||
|
|
a51507b701 | ||
|
|
7af4017910 | ||
|
|
7af631ff1e | ||
|
|
ca4a0bc7d2 | ||
|
|
1be4c7e0ef | ||
|
|
fd1841f8dd | ||
|
|
81c2cb498a | ||
|
|
21c16ed44e | ||
|
|
30638041d8 | ||
|
|
892f4f92ae | ||
|
|
bda0186d10 | ||
|
|
96c8836a98 | ||
|
|
c44a3f45de | ||
|
|
5783d086ad |
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -1,14 +1,23 @@
|
|||||||
name: tests
|
name: tests
|
||||||
|
|
||||||
on: [push]
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '47 3 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.x"]
|
python-version: ["3.x", "3.11", "3.10", "3.9", "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:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -20,7 +29,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install flake8 pytest
|
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
|
- name: Lint with flake8
|
||||||
run: |
|
run: |
|
||||||
# stop the build if there are Python syntax errors or undefined names
|
# stop the build if there are Python syntax errors or undefined names
|
||||||
@@ -28,5 +37,4 @@ jobs:
|
|||||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
# 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
|
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=200 --statistics
|
||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: pytest
|
||||||
pytest
|
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -14,3 +14,5 @@ venv/
|
|||||||
!.devcontainer/
|
!.devcontainer/
|
||||||
!.devcontainer/*.json
|
!.devcontainer/*.json
|
||||||
*.whl
|
*.whl
|
||||||
|
build/
|
||||||
|
extract_otp_secret_keys.egg-info/
|
||||||
|
|||||||
5
.vscode/extensions.json
vendored
Normal file
5
.vscode/extensions.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"recommendations": [
|
||||||
|
"ms-python.python"
|
||||||
|
]
|
||||||
|
}
|
||||||
68
Pipfile.lock
generated
68
Pipfile.lock
generated
@@ -85,23 +85,23 @@
|
|||||||
},
|
},
|
||||||
"protobuf": {
|
"protobuf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0413addc126c40a5440ee59be098de1007183d68e9f5f20ed5fbc44848f417ca",
|
"sha256:1f22ac0ca65bb70a876060d96d914dae09ac98d114294f77584b0d2644fa9c30",
|
||||||
"sha256:05cbcb9a25cd781fd949f93f6f98a911883868c0360c6d2264fc99a903c8f0d7",
|
"sha256:237216c3326d46808a9f7c26fd1bd4b20015fb6867dc5d263a493ef9a539293b",
|
||||||
"sha256:0c968753028cb14b1d24cc839723f7e9505b305fc588a37a9e0f7d270cb59d89",
|
"sha256:27f4d15021da6d2b706ddc3860fac0a5ddaba34ab679dc182b60a8bb4e1121cc",
|
||||||
"sha256:2a172741b5b041a896b621cef4277077afd571e0d3a6e524e7171f1c70e33200",
|
"sha256:299ea899484ee6f44604deb71f424234f654606b983cb496ea2a53e3c63ab791",
|
||||||
"sha256:3f08f04b4f101dd469efbcc1731fbf48068eccd8a42f4e2ea530aa012a5f56f8",
|
"sha256:3d164928ff0727d97022957c2b849250ca0e64777ee31efd7d6de2e07c494717",
|
||||||
"sha256:4d97c16c0d11155b3714a29245461f0eb60cace294455077f3a3b8a629afa383",
|
"sha256:6ab80df09e3208f742c98443b6166bcb70d65f52cfeb67357d52032ea1ae9bec",
|
||||||
"sha256:5096b3922b45e4b7a04d3d3cb855d13bb5ccd4d5e44b129e706232ebf0ffb870",
|
"sha256:78a28c9fa223998472886c77042e9b9afb6fe4242bd2a2a5aced88e3f4422aa7",
|
||||||
"sha256:5efa8a8162ada7e10847140308fbf84fdc5b89dc21655d12ec04aed87284fe07",
|
"sha256:7cd532c4566d0e6feafecc1059d04c7915aec8e182d1cf7adee8b24ef1e2e6ab",
|
||||||
"sha256:6b809f20923b6ef49dc1755cb50bdb21be179b4a3c7ffcab1fe5d3f139b58a51",
|
"sha256:89f9149e4a0169cddfc44c74f230d7743002e3aa0b9472d8c28f0388102fc4c2",
|
||||||
"sha256:81b233a06c62387ea5c9be2cd9aedd2ba09940e91da53b920e9ff5bd98e48e7f",
|
"sha256:a53fd3f03e578553623272dc46ac2f189de23862e68565e83dde203d41b76fc5",
|
||||||
"sha256:a5e89eabaa0ca72ce1b7c8104a740d44cdb67942cbbed00c69a4c0541de17107",
|
"sha256:b135410244ebe777db80298297a97fbb4c862c881b4403b71bac9d4107d61fd1",
|
||||||
"sha256:b78d7c2c36b51c0041b9bf000be4adb09f4112bfc40bc7a9d48ac0b0dfad139e",
|
"sha256:b98d0148f84e3a3c569e19f52103ca1feacdac0d2df8d6533cf983d1fda28462",
|
||||||
"sha256:e53165dd14d19abc7f50733f365de431e51d1d262db40c0ee22e271a074fac59",
|
"sha256:d1736130bce8cf131ac7957fa26880ca19227d4ad68b4888b3be0dea1f95df97",
|
||||||
"sha256:e92768d17473657c87e98b79a4c7724b0ddfa23211b05ce137bfdc55e734e36f"
|
"sha256:f45460f9ee70a0ec1b6694c6e4e348ad2019275680bd68a1d9314b8c7e01e574"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.21.10"
|
"version": "==4.21.12"
|
||||||
},
|
},
|
||||||
"qrcode": {
|
"qrcode": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -133,7 +133,7 @@
|
|||||||
"sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0",
|
"sha256:a07ffd2351b8c678dfc4a856a3005f8067aea51d6ba6c700796a4d9e280f39f0",
|
||||||
"sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"
|
"sha256:e5db55f3687856d8fbdab002ed78544e1c4559a130302693d839dfe8f93f2373"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.11'",
|
||||||
"version": "==0.3.6"
|
"version": "==0.3.6"
|
||||||
},
|
},
|
||||||
"flake8": {
|
"flake8": {
|
||||||
@@ -153,11 +153,11 @@
|
|||||||
},
|
},
|
||||||
"isort": {
|
"isort": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7",
|
"sha256:83155ffa936239d986b0f190347a3f2285f42a9b9e1725c89d865b27dd0627e5",
|
||||||
"sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"
|
"sha256:a8ca25fbfad0f7d5d8447a4314837298d9f6b23aed8618584c894574f626b64b"
|
||||||
],
|
],
|
||||||
"markers": "python_version < '4.0' and python_full_version >= '3.6.1'",
|
"markers": "python_full_version >= '3.7.0'",
|
||||||
"version": "==5.10.1"
|
"version": "==5.11.3"
|
||||||
},
|
},
|
||||||
"lazy-object-proxy": {
|
"lazy-object-proxy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -194,19 +194,19 @@
|
|||||||
},
|
},
|
||||||
"packaging": {
|
"packaging": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb",
|
"sha256:2198ec20bd4c017b8f9717e00f0c8714076fc2fd93816750ab48e2c41de2cfd3",
|
||||||
"sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"
|
"sha256:957e2148ba0e1a3b282772e791ef1d8083648bc131c8ab0c1feba110ce1146c3"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.6'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==21.3"
|
"version": "==22.0"
|
||||||
},
|
},
|
||||||
"platformdirs": {
|
"platformdirs": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7",
|
"sha256:1a89a12377800c81983db6be069ec068eee989748799b946cce2a6e80dcc54ca",
|
||||||
"sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"
|
"sha256:b46ffafa316e6b83b47489d240ce17173f123a9b9c83282141c3daf26ad9ac2e"
|
||||||
],
|
],
|
||||||
"markers": "python_version >= '3.7'",
|
"markers": "python_version >= '3.7'",
|
||||||
"version": "==2.5.4"
|
"version": "==2.6.0"
|
||||||
},
|
},
|
||||||
"pluggy": {
|
"pluggy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
@@ -234,19 +234,11 @@
|
|||||||
},
|
},
|
||||||
"pylint": {
|
"pylint": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:1d561d1d3e8be9dd880edc685162fbdaa0409c88b9b7400873c0cf345602e326",
|
"sha256:18783cca3cfee5b83c6c5d10b3cdb66c6594520ffae61890858fe8d932e1c6b4",
|
||||||
"sha256:91e4776dbcb4b4d921a3e4b6fec669551107ba11f29d9199154a01622e460a57"
|
"sha256:349c8cd36aede4d50a0754a8c0218b43323d13d5d88f4b2952ddfe3e169681eb"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.15.7"
|
"version": "==2.15.9"
|
||||||
},
|
|
||||||
"pyparsing": {
|
|
||||||
"hashes": [
|
|
||||||
"sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb",
|
|
||||||
"sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"
|
|
||||||
],
|
|
||||||
"markers": "python_full_version >= '3.6.8'",
|
|
||||||
"version": "==3.0.9"
|
|
||||||
},
|
},
|
||||||
"pytest": {
|
"pytest": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|||||||
79
README.md
79
README.md
@@ -3,16 +3,21 @@
|
|||||||
[](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml)
|
[](https://github.com/scito/extract_otp_secret_keys/actions/workflows/ci.yml)
|
||||||

|

|
||||||
[](https://github.com/scito/extract_otp_secret_keys/blob/master/Pipfile.lock)
|
[](https://github.com/scito/extract_otp_secret_keys/blob/master/Pipfile.lock)
|
||||||

|

|
||||||
[](https://github.com/scito/extract_otp_secret_keys/blob/master/LICENSE)
|
[](https://github.com/scito/extract_otp_secret_keys/blob/master/LICENSE)
|
||||||
[](https://github.com/scito/extract_otp_secret_keys/tags)
|
[](https://github.com/scito/extract_otp_secret_keys/tags)
|
||||||
[](https://stand-with-ukraine.pp.ua)
|
[](https://stand-with-ukraine.pp.ua)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Extract two-factor authentication (2FA, TFA, one time passwords, otp) secret keys from export QR codes of "Google Authenticator" app.
|
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.
|
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
|
## Usage
|
||||||
|
|
||||||
1. Open "Google Authenticator" app on the mobile phone
|
1. Open "Google Authenticator" app on the mobile phone
|
||||||
@@ -22,24 +27,24 @@ The secret and otp values can be printed and exported to json or csv. The QR cod
|
|||||||
5. Transfer the file to the computer where his script is installed.
|
5. Transfer the file to the computer where his script is installed.
|
||||||
6. Call this script with the file as input:
|
6. Call this script with the file as input:
|
||||||
|
|
||||||
python extract_otp_secret_keys.py example_export.txt
|
python extract_otp_secret_keys.py example_export.txt
|
||||||
|
|
||||||
## Program help: arguments and options
|
## Program help: arguments and options
|
||||||
|
|
||||||
<pre>usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv FILE] [--keepass FILE] [--printqr] [--saveqr DIR] [--verbose] [--quiet] infile
|
<pre>usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv FILE] [--keepass FILE] [--printqr] [--saveqr DIR] [--verbose | --quiet] infile
|
||||||
|
|
||||||
positional arguments:
|
positional arguments:
|
||||||
infile file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored
|
infile file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored
|
||||||
|
|
||||||
options:
|
options:
|
||||||
-h, --help show this help message and exit
|
-h, --help show this help message and exit
|
||||||
--json FILE, -j FILE export json file
|
--json FILE, -j FILE export json file or - for stdout
|
||||||
--csv FILE, -c FILE export csv file
|
--csv FILE, -c FILE export csv file or - for stdout
|
||||||
--keepass FILE, -k FILE export totp/hotp csv file(s) for KeePass
|
--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)
|
--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)
|
--saveqr DIR, -s DIR save QR code(s) as images to the given folder (requires qrcode module)
|
||||||
--verbose, -v verbose output
|
--verbose, -v verbose output
|
||||||
--quiet, -q no stdout output</pre>
|
--quiet, -q no stdout output, except output set by -</pre>
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@@ -48,7 +53,7 @@ options:
|
|||||||
Known to work with
|
Known to work with
|
||||||
|
|
||||||
* Python 3.10.8, protobuf 4.21.9, 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.0, protobuf 4.21.10, 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.
|
For protobuf versions 3.14.0 or similar or Python 3.6, use the extract_otp_secret_keys version 1.4.0.
|
||||||
|
|
||||||
@@ -58,6 +63,29 @@ For printing QR codes, the qrcode module is required, otherwise it can be omitte
|
|||||||
|
|
||||||
pip install qrcode[pil]
|
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
|
||||||
|
|
||||||
[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 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).
|
||||||
@@ -105,15 +133,38 @@ Command for regeneration of Python code from proto3 message definition file (onl
|
|||||||
|
|
||||||
protoc --python_out=protobuf_generated_python google_auth.proto
|
protoc --python_out=protobuf_generated_python google_auth.proto
|
||||||
|
|
||||||
The generated protobuf Python code was generated by protoc 21.10 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.10).
|
The generated protobuf Python code was generated by protoc 21.12 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.12).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
* Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
|
||||||
* Template code: https://github.com/beemdevelopment/Aegis/pull/406
|
* 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
|
## 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
|
### pipenv
|
||||||
|
|
||||||
You can you use [Pipenv](https://github.com/pypa/pipenv) for running extract_otp_secret_keys.
|
You can you use [Pipenv](https://github.com/pypa/pipenv) for running extract_otp_secret_keys.
|
||||||
@@ -212,6 +263,12 @@ Setup for running the tests in VSCode.
|
|||||||
pip install -U -r requirements.txt
|
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 🇺🇦
|
# #StandWithUkraine 🇺🇦
|
||||||
|
|||||||
@@ -12,3 +12,7 @@ otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXB
|
|||||||
|
|
||||||
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||||
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
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
|
||||||
|
|||||||
@@ -3,3 +3,4 @@ raspberrypi,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
|||||||
,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||||
,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||||
raspberrypi,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
raspberrypi,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||||
|
,encoding: ¿äÄéÉ? (demo),7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||||
|
|||||||
|
@@ -4,3 +4,4 @@ pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi
|
|||||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
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
|
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
|
||||||
|
|||||||
|
@@ -38,5 +38,13 @@
|
|||||||
"type": "hotp",
|
"type": "hotp",
|
||||||
"counter": 4,
|
"counter": 4,
|
||||||
"url": "otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -59,6 +59,10 @@ def sys_main():
|
|||||||
|
|
||||||
def main(sys_args):
|
def main(sys_args):
|
||||||
global verbose, quiet
|
global verbose, quiet
|
||||||
|
|
||||||
|
# allow to use sys.stdout with with (avoid closing)
|
||||||
|
sys.stdout.close = lambda: None
|
||||||
|
|
||||||
args = parse_args(sys_args)
|
args = parse_args(sys_args)
|
||||||
verbose = args.verbose if args.verbose else 0
|
verbose = args.verbose if args.verbose else 0
|
||||||
quiet = args.quiet
|
quiet = args.quiet
|
||||||
@@ -70,20 +74,20 @@ def main(sys_args):
|
|||||||
|
|
||||||
|
|
||||||
def parse_args(sys_args):
|
def parse_args(sys_args):
|
||||||
formatter = lambda prog: argparse.HelpFormatter(prog,max_help_position=52)
|
formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=52)
|
||||||
arg_parser = argparse.ArgumentParser(formatter_class=formatter)
|
arg_parser = argparse.ArgumentParser(formatter_class=formatter)
|
||||||
arg_parser.add_argument('infile', help='file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
|
arg_parser.add_argument('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', metavar=('FILE'))
|
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', 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', 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('--printqr', '-p', help='print QR code(s) as text to the terminal (requires qrcode module)', action='store_true')
|
||||||
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
|
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
|
||||||
arg_parser.add_argument('--verbose', '-v', help='verbose output', action='count')
|
output_group = arg_parser.add_mutually_exclusive_group()
|
||||||
arg_parser.add_argument('--quiet', '-q', help='no stdout output', action='store_true')
|
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)
|
args = arg_parser.parse_args(sys_args)
|
||||||
if args.verbose and args.quiet:
|
if args.csv == '-' or args.json == '-' or args.keepass == '-':
|
||||||
print("The arguments --verbose and --quiet are mutually exclusive.")
|
args.quiet = args.q = True
|
||||||
sys.exit(1)
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
@@ -94,51 +98,58 @@ def extract_otps(args):
|
|||||||
otps = []
|
otps = []
|
||||||
|
|
||||||
i = j = 0
|
i = j = 0
|
||||||
for line in (line.strip() for line in fileinput.input(args.infile)):
|
finput = fileinput.input(args.infile)
|
||||||
if verbose: print(line)
|
try:
|
||||||
if line.startswith('#') or line == '': continue
|
for line in (line.strip() for line in finput):
|
||||||
i += 1
|
if verbose: print(line)
|
||||||
payload = get_payload_from_line(line, i, args)
|
if line.startswith('#') or line == '': continue
|
||||||
|
i += 1
|
||||||
|
payload = get_payload_from_line(line, i, args)
|
||||||
|
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
for raw_otp in payload.otp_parameters:
|
for raw_otp in payload.otp_parameters:
|
||||||
j += 1
|
j += 1
|
||||||
if verbose: print('\n{}. Secret Key'.format(j))
|
if verbose: print('\n{}. Secret Key'.format(j))
|
||||||
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
||||||
otp_type_enum = get_enum_name_by_number(raw_otp, 'type')
|
otp_type_enum = get_enum_name_by_number(raw_otp, 'type')
|
||||||
otp_type = get_otp_type_str_from_code(raw_otp.type)
|
otp_type = get_otp_type_str_from_code(raw_otp.type)
|
||||||
otp_url = build_otp_url(secret, raw_otp)
|
otp_url = build_otp_url(secret, raw_otp)
|
||||||
otp = {
|
otp = {
|
||||||
"name": raw_otp.name,
|
"name": raw_otp.name,
|
||||||
"secret": secret,
|
"secret": secret,
|
||||||
"issuer": raw_otp.issuer,
|
"issuer": raw_otp.issuer,
|
||||||
"type": otp_type,
|
"type": otp_type,
|
||||||
"counter": raw_otp.counter if raw_otp.type == 1 else None,
|
"counter": raw_otp.counter if raw_otp.type == 1 else None,
|
||||||
"url": otp_url
|
"url": otp_url
|
||||||
}
|
}
|
||||||
if not quiet:
|
if not quiet:
|
||||||
print_otp(otp)
|
print_otp(otp)
|
||||||
if args.printqr:
|
if args.printqr:
|
||||||
print_qr(args, otp_url)
|
print_qr(args, otp_url)
|
||||||
if args.saveqr:
|
if args.saveqr:
|
||||||
save_qr(otp, args, j)
|
save_qr(otp, args, j)
|
||||||
if not quiet:
|
if not quiet:
|
||||||
print()
|
print()
|
||||||
|
|
||||||
otps.append(otp)
|
otps.append(otp)
|
||||||
|
finally:
|
||||||
|
finput.close()
|
||||||
return otps
|
return otps
|
||||||
|
|
||||||
|
|
||||||
def get_payload_from_line(line, i, args):
|
def get_payload_from_line(line, i, args):
|
||||||
global verbose
|
global verbose
|
||||||
if not line.startswith('otpauth-migration://'):
|
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))
|
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)
|
parsed_url = urlparse(line)
|
||||||
if verbose > 1: print('\nDEBUG: parsed_url={}'.format(parsed_url))
|
if verbose > 1: print('\nDEBUG: parsed_url={}'.format(parsed_url))
|
||||||
params = parse_qs(parsed_url.query, strict_parsing=True)
|
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 verbose > 1: print('\nDEBUG: querystring params={}'.format(params))
|
||||||
if 'data' not in params:
|
if 'data' not in params:
|
||||||
print('\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line))
|
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)
|
sys.exit(1)
|
||||||
data_base64 = params['data'][0]
|
data_base64 = params['data'][0]
|
||||||
if verbose > 1: print('\nDEBUG: data_base64={}'.format(data_base64))
|
if verbose > 1: print('\nDEBUG: data_base64={}'.format(data_base64))
|
||||||
@@ -146,7 +157,12 @@ def get_payload_from_line(line, i, args):
|
|||||||
if verbose > 1: print('\nDEBUG: data_base64_fixed={}'.format(data_base64))
|
if verbose > 1: print('\nDEBUG: data_base64_fixed={}'.format(data_base64))
|
||||||
data = base64.b64decode(data_base64_fixed, validate=True)
|
data = base64.b64decode(data_base64_fixed, validate=True)
|
||||||
payload = protobuf_generated_python.google_auth_pb2.MigrationPayload()
|
payload = protobuf_generated_python.google_auth_pb2.MigrationPayload()
|
||||||
payload.ParseFromString(data)
|
try:
|
||||||
|
payload.ParseFromString(data)
|
||||||
|
except:
|
||||||
|
eprint('\nERROR: Cannot decode otpauth-migration migration payload.')
|
||||||
|
eprint('data={}'.format(data_base64))
|
||||||
|
exit(1);
|
||||||
if verbose:
|
if verbose:
|
||||||
print('\n{}. Payload Line'.format(i), payload, sep='\n')
|
print('\n{}. Payload Line'.format(i), payload, sep='\n')
|
||||||
|
|
||||||
@@ -216,7 +232,7 @@ def print_qr(args, data):
|
|||||||
def write_csv(args, otps):
|
def write_csv(args, otps):
|
||||||
global verbose, quiet
|
global verbose, quiet
|
||||||
if args.csv and len(otps) > 0:
|
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 = csv.DictWriter(outfile, otps[0].keys())
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
writer.writerows(otps)
|
writer.writerows(otps)
|
||||||
@@ -233,7 +249,7 @@ def write_keepass_csv(args, otps):
|
|||||||
count_totp_entries = 0
|
count_totp_entries = 0
|
||||||
count_hotp_entries = 0
|
count_hotp_entries = 0
|
||||||
if has_totp:
|
if has_totp:
|
||||||
with open(otp_filename_totp, "w") as outfile:
|
with open_file_or_stdout_for_csv(otp_filename_totp) as outfile:
|
||||||
writer = csv.DictWriter(outfile, ["Title", "User Name", "TimeOtp-Secret-Base32", "Group"])
|
writer = csv.DictWriter(outfile, ["Title", "User Name", "TimeOtp-Secret-Base32", "Group"])
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
for otp in otps:
|
for otp in otps:
|
||||||
@@ -246,7 +262,7 @@ def write_keepass_csv(args, otps):
|
|||||||
})
|
})
|
||||||
count_totp_entries += 1
|
count_totp_entries += 1
|
||||||
if has_hotp:
|
if has_hotp:
|
||||||
with open(otp_filename_hotp, "w") as outfile:
|
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 = csv.DictWriter(outfile, ["Title", "User Name", "HmacOtp-Secret-Base32", "HmacOtp-Counter", "Group"])
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
for otp in otps:
|
for otp in otps:
|
||||||
@@ -267,7 +283,7 @@ def write_keepass_csv(args, otps):
|
|||||||
def write_json(args, otps):
|
def write_json(args, otps):
|
||||||
global verbose, quiet
|
global verbose, quiet
|
||||||
if args.json:
|
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)
|
json.dump(otps, outfile, indent=4)
|
||||||
if not quiet: print("Exported {} otp entries to json {}".format(len(otps), args.json))
|
if not quiet: print("Exported {} otp entries to json {}".format(len(otps), args.json))
|
||||||
|
|
||||||
@@ -285,5 +301,25 @@ def add_pre_suffix(file, pre_suffix):
|
|||||||
return name + "." + pre_suffix + (ext if ext else "")
|
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__':
|
if __name__ == '__main__':
|
||||||
sys_main()
|
sys_main()
|
||||||
|
|||||||
60
setup.py
Normal file
60
setup.py
Normal 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)',
|
||||||
|
)
|
||||||
@@ -9,3 +9,7 @@ otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXB
|
|||||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
|
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
|
||||||
|
|||||||
@@ -112,3 +112,27 @@ Type: hotp
|
|||||||
Counter: 4
|
Counter: 4
|
||||||
otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&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
|
||||||
|
|
||||||
|
|||||||
@@ -132,3 +132,32 @@ Counter: 4
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Name: encoding: ¿äÄéÉ? (demo)
|
||||||
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
Type: totp
|
||||||
|
|
||||||
|
|
||||||
|
█▀▀▀▀▀█ ▄▀▀ ▀ ▄▀█ ▄▄▄██▀ ▄▀█▄█▀ █▀▀▀▀▀█
|
||||||
|
█ ███ █ █▄▀█▄▄ ▀ ▄▀█ █ █ ▀▀▀▄ █ ███ █
|
||||||
|
█ ▀▀▀ █ ▄ ▀ ▀▄▀▄ ▄▄▀▄▄█▄ ▀▄ █▀▀█ █ ▀▀▀ █
|
||||||
|
▀▀▀▀▀▀▀ █ █ █▄█▄▀ ▀▄▀ █▄█ ▀ █ ▀ █ ▀▀▀▀▀▀▀
|
||||||
|
▀ ▄ ▀ █▀▀▄▀ ▄▄▀▀▄█▄ █▄▀▀▄█▀██▄▄█▀ ▄█▀▀▄
|
||||||
|
▄▀█▄█ ▀ ▀ █▄█▄▄ ▄███▄▄▀▀▀▄▄▀▄ ▄█▀▄▄
|
||||||
|
▀█▀ ▄▀▄▄█ ▄▀███▀ ▄▀█▀▄▀▄ ▀██▄▄ ▄█▀█ ▀▄
|
||||||
|
▄▀ █▄▀▀ █▀▄▄▄ ▄█▄█ ▀▄ ▄▄ ▄ ▀▀█▄▄ ▀█▄▄▄
|
||||||
|
▀ ▄▄▄▀▀▄▄█▀▄ ▀▀▀█▄ █▄ ▀ ▄█▄▄▀▄▀▀▀▄▄█▄ ▀ ▀
|
||||||
|
█ ▄ ▀█ ▄▀ ██ █ ▄▄▀▀▀███▄ ▄▄██ ██▀█▀
|
||||||
|
█▀█▀██▀▀███▄ ▀▀▄▄▄▄█▀ █ ▄█▄█▄▀ ▄▄▀ ▄▄ ▀
|
||||||
|
▄██▄▄ ▀ ▀ ▀▀ ██▄▄▄▀▀▄█▀█▄ ▀ █▄▀▄ ▀▄▄
|
||||||
|
███▄▀█▄█▄▄█ ▀█▄ ▀▄█ ▄▀▄ █▄ ▄ █▄ ▄▀▄▀
|
||||||
|
█ ▄ ▄▀▀▄▄█▄▄█▄█ ▄▄▄ █▄▄▀█ █▀█▄ ▄▀▀█▄▄▄▀▀
|
||||||
|
▄ ▀▄▀▀ ▄▄▀██ ▀▀▄█▀▀▄ ▀▀█ ▄ ████ █▀█▀█
|
||||||
|
█▀▄▀█ ▀▄▄ ▄ ▀▀▀ ▄▀ ▀ █▀▄▀▀█▀▀█▄▀█ ▀▄▀▄ █
|
||||||
|
▀ ▀ ▀▀ ▄ █▄▀█▀▀▄▀█ ▀▄▄█▄▀ ██ ▀██▀▀▀█▀▄▄
|
||||||
|
█▀▀▀▀▀█ ▀█ ▄▄██▀ ▀██▄▀██ ▄▄██ ▀█ ▀ █ ▀█
|
||||||
|
█ ███ █ █▄ █▀▀█▀▀▀█▀█ ▀ ▀█ █▀▀ ██▀▀▀███▀
|
||||||
|
█ ▀▀▀ █ ▄ ▄▀█▄▄ ▀█ ▀▀ ▄ ▀█▀ ▄▀ █▀▀██ ▀▄
|
||||||
|
▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀▀ ▀ ▀ ▀▀▀▀ ▀ ▀ ▀ ▀
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
1
test/test_export_wrong_content.txt
Normal file
1
test/test_export_wrong_content.txt
Normal 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.
|
||||||
1
test/test_export_wrong_data.txt
Normal file
1
test/test_export_wrong_data.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
otpauth-migration://offline?data=XXXX
|
||||||
1
test/test_export_wrong_prefix.txt
Normal file
1
test/test_export_wrong_prefix.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
QR-Code:otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
||||||
@@ -18,13 +18,40 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
from utils import read_csv, read_json, remove_files, remove_dir_with_files, read_file_to_str, file_exits
|
from 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 os import path
|
||||||
from pytest import raises
|
from pytest import raises, mark
|
||||||
|
from io import StringIO
|
||||||
|
from sys import implementation
|
||||||
|
|
||||||
import extract_otp_secret_keys
|
import extract_otp_secret_keys
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def test_extract_csv(capsys):
|
||||||
# Arrange
|
# Arrange
|
||||||
cleanup()
|
cleanup()
|
||||||
@@ -47,6 +74,51 @@ def test_extract_csv(capsys):
|
|||||||
cleanup()
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
def test_keepass_csv(capsys):
|
||||||
'''Two csv files .totp and .htop are generated.'''
|
'''Two csv files .totp and .htop are generated.'''
|
||||||
# Arrange
|
# Arrange
|
||||||
@@ -74,6 +146,31 @@ def test_keepass_csv(capsys):
|
|||||||
cleanup()
|
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):
|
def test_single_keepass_csv(capsys):
|
||||||
'''Does not add .totp or .hotp pre-suffix'''
|
'''Does not add .totp or .hotp pre-suffix'''
|
||||||
# Arrange
|
# Arrange
|
||||||
@@ -121,41 +218,25 @@ def test_extract_json(capsys):
|
|||||||
cleanup()
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
def test_extract_stdout(capsys):
|
def test_extract_json_stdout(capsys):
|
||||||
|
# Arrange
|
||||||
|
cleanup()
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secret_keys.main(['example_export.txt'])
|
extract_otp_secret_keys.main(['-j', '-', 'example_export.txt'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
|
expected_json = read_json('example_output.json')
|
||||||
|
assert not file_exits('test_example_output.json')
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
actual_json = read_json_str(captured.out)
|
||||||
|
|
||||||
expected_stdout = '''Name: pi@raspberrypi
|
assert actual_json == expected_json
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
|
||||||
Issuer: raspberrypi
|
|
||||||
Type: totp
|
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
|
||||||
Type: totp
|
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
|
||||||
Type: totp
|
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
|
||||||
Issuer: raspberrypi
|
|
||||||
Type: totp
|
|
||||||
|
|
||||||
Name: hotp demo
|
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
|
||||||
Type: hotp
|
|
||||||
Counter: 4
|
|
||||||
|
|
||||||
'''
|
|
||||||
|
|
||||||
assert captured.out == expected_stdout
|
|
||||||
assert captured.err == ''
|
assert captured.err == ''
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
def test_extract_not_encoded_plus(capsys):
|
def test_extract_not_encoded_plus(capsys):
|
||||||
# Act
|
# Act
|
||||||
@@ -225,6 +306,7 @@ def test_extract_saveqr(capsys):
|
|||||||
cleanup()
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
@mark.skipif(implementation.name == 'pypy', reason="Encoding problems in verbose mode in pypy.")
|
||||||
def test_extract_verbose(capsys):
|
def test_extract_verbose(capsys):
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
|
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
|
||||||
@@ -267,6 +349,20 @@ def test_extract_help(capsys):
|
|||||||
assert pytest_wrapped_e.value.code == 0
|
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):
|
def test_verbose_and_quiet(capsys):
|
||||||
with raises(SystemExit) as pytest_wrapped_e:
|
with raises(SystemExit) as pytest_wrapped_e:
|
||||||
# Act
|
# Act
|
||||||
@@ -275,8 +371,75 @@ def test_verbose_and_quiet(capsys):
|
|||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
assert len(captured.out) > 0
|
assert len(captured.err) > 0
|
||||||
assert 'The arguments --verbose and --quiet are mutually exclusive.' in captured.out
|
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):
|
def test_add_pre_suffix(capsys):
|
||||||
@@ -289,3 +452,33 @@ def cleanup():
|
|||||||
remove_files('test_example_*.csv')
|
remove_files('test_example_*.csv')
|
||||||
remove_files('test_example_*.json')
|
remove_files('test_example_*.json')
|
||||||
remove_dir_with_files('testout/')
|
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
|
||||||
|
|
||||||
|
'''
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import io
|
|||||||
from contextlib import redirect_stdout
|
from contextlib import redirect_stdout
|
||||||
from utils import read_csv, read_json, remove_file, remove_dir_with_files, 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 os import path
|
||||||
|
from sys import implementation
|
||||||
|
|
||||||
import extract_otp_secret_keys
|
import extract_otp_secret_keys
|
||||||
|
|
||||||
@@ -72,6 +73,10 @@ class TestExtract(unittest.TestCase):
|
|||||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||||
'Type: hotp',
|
'Type: hotp',
|
||||||
'Counter: 4',
|
'Counter: 4',
|
||||||
|
'',
|
||||||
|
'Name: encoding: ¿äÄéÉ? (demo)',
|
||||||
|
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||||
|
'Type: totp',
|
||||||
''
|
''
|
||||||
]
|
]
|
||||||
self.assertEqual(output, expected_output)
|
self.assertEqual(output, expected_output)
|
||||||
@@ -106,6 +111,10 @@ Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
|||||||
Type: hotp
|
Type: hotp
|
||||||
Counter: 4
|
Counter: 4
|
||||||
|
|
||||||
|
Name: encoding: ¿äÄéÉ? (demo)
|
||||||
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
Type: totp
|
||||||
|
|
||||||
'''
|
'''
|
||||||
self.assertEqual(actual_output, expected_output)
|
self.assertEqual(actual_output, expected_output)
|
||||||
|
|
||||||
@@ -157,6 +166,7 @@ Type: totp
|
|||||||
self.assertTrue(path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png'))
|
self.assertTrue(path.isfile('testout/qr/4-piraspberrypi-raspberrypi.png'))
|
||||||
|
|
||||||
def test_extract_verbose(self):
|
def test_extract_verbose(self):
|
||||||
|
if implementation.name == 'pypy': self.skipTest("Encoding problems in verbose mode in pypy.")
|
||||||
out = io.StringIO()
|
out = io.StringIO()
|
||||||
with redirect_stdout(out):
|
with redirect_stdout(out):
|
||||||
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
|
extract_otp_secret_keys.main(['-v', 'example_export.txt'])
|
||||||
|
|||||||
@@ -181,11 +181,11 @@ eval "$cmd"
|
|||||||
|
|
||||||
$PIP --version
|
$PIP --version
|
||||||
|
|
||||||
cmd="$PIP install -U -r requirements.txt"
|
cmd="$PIP install --use-pep517 -U -r requirements.txt"
|
||||||
if $interactive ; then askContinueYn "$cmd"; fi
|
if $interactive ; then askContinueYn "$cmd"; fi
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
|
|
||||||
cmd="$PIP install -U -r requirements-dev.txt"
|
cmd="$PIP install --use-pep517 -U -r requirements-dev.txt"
|
||||||
if $interactive ; then askContinueYn "$cmd"; fi
|
if $interactive ; then askContinueYn "$cmd"; fi
|
||||||
eval "$cmd"
|
eval "$cmd"
|
||||||
|
|
||||||
|
|||||||
20
utils.py
20
utils.py
@@ -59,7 +59,7 @@ def remove_dir_with_files(dir):
|
|||||||
|
|
||||||
def read_csv(filename):
|
def read_csv(filename):
|
||||||
"""Returns a list of lines."""
|
"""Returns a list of lines."""
|
||||||
with open(filename, "r") as infile:
|
with open(filename, "r", encoding="utf-8", newline='') as infile:
|
||||||
lines = []
|
lines = []
|
||||||
reader = csv.reader(infile)
|
reader = csv.reader(infile)
|
||||||
for line in reader:
|
for line in reader:
|
||||||
@@ -67,15 +67,29 @@ def read_csv(filename):
|
|||||||
return lines
|
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):
|
def read_json(filename):
|
||||||
"""Returns a list or a dictionary."""
|
"""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)
|
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):
|
def read_file_to_list(filename):
|
||||||
"""Returns a list of lines."""
|
"""Returns a list of lines."""
|
||||||
with open(filename, "r") as infile:
|
with open(filename, "r", encoding="utf-8") as infile:
|
||||||
return infile.readlines()
|
return infile.readlines()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user