mirror of
https://github.com/scito/extract_otp_secret_keys.git
synced 2025-12-08 07:45:01 +01:00
add keepass csv export; improve hotp
- export to dedicated totp and hotp csv files for KeePass - show Typ as totp/hotp instead of OTP_TOTP/OTP_HOTP (BREAKING CHANGE in csv, json and stdout, qr codes or urls are not affected) - add hotp example - add hotp tests - export counter for hotp to csv and json files - add section on KeePass to README - increase protobuf to 4.21.10 - show file names of exported csv or json files
This commit is contained in:
parent
eae01a07d5
commit
a77e775948
14 changed files with 433 additions and 157 deletions
36
Pipfile.lock
generated
36
Pipfile.lock
generated
|
|
@ -85,23 +85,23 @@
|
||||||
},
|
},
|
||||||
"protobuf": {
|
"protobuf": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:2c9c2ed7466ad565f18668aa4731c535511c5d9a40c6da39524bccf43e441719",
|
"sha256:0413addc126c40a5440ee59be098de1007183d68e9f5f20ed5fbc44848f417ca",
|
||||||
"sha256:48e2cd6b88c6ed3d5877a3ea40df79d08374088e89bedc32557348848dff250b",
|
"sha256:05cbcb9a25cd781fd949f93f6f98a911883868c0360c6d2264fc99a903c8f0d7",
|
||||||
"sha256:5b0834e61fb38f34ba8840d7dcb2e5a2f03de0c714e0293b3963b79db26de8ce",
|
"sha256:0c968753028cb14b1d24cc839723f7e9505b305fc588a37a9e0f7d270cb59d89",
|
||||||
"sha256:61f21493d96d2a77f9ca84fefa105872550ab5ef71d21c458eb80edcf4885a99",
|
"sha256:2a172741b5b041a896b621cef4277077afd571e0d3a6e524e7171f1c70e33200",
|
||||||
"sha256:6e0be9f09bf9b6cf497b27425487706fa48c6d1632ddd94dab1a5fe11a422392",
|
"sha256:3f08f04b4f101dd469efbcc1731fbf48068eccd8a42f4e2ea530aa012a5f56f8",
|
||||||
"sha256:6e312e280fbe3c74ea9e080d9e6080b636798b5e3939242298b591064470b06b",
|
"sha256:4d97c16c0d11155b3714a29245461f0eb60cace294455077f3a3b8a629afa383",
|
||||||
"sha256:7eb8f2cc41a34e9c956c256e3ac766cf4e1a4c9c925dc757a41a01be3e852965",
|
"sha256:5096b3922b45e4b7a04d3d3cb855d13bb5ccd4d5e44b129e706232ebf0ffb870",
|
||||||
"sha256:84ea107016244dfc1eecae7684f7ce13c788b9a644cd3fca5b77871366556444",
|
"sha256:5efa8a8162ada7e10847140308fbf84fdc5b89dc21655d12ec04aed87284fe07",
|
||||||
"sha256:9227c14010acd9ae7702d6467b4625b6fe853175a6b150e539b21d2b2f2b409c",
|
"sha256:6b809f20923b6ef49dc1755cb50bdb21be179b4a3c7ffcab1fe5d3f139b58a51",
|
||||||
"sha256:a419cc95fca8694804709b8c4f2326266d29659b126a93befe210f5bbc772536",
|
"sha256:81b233a06c62387ea5c9be2cd9aedd2ba09940e91da53b920e9ff5bd98e48e7f",
|
||||||
"sha256:a7d0ea43949d45b836234f4ebb5ba0b22e7432d065394b532cdca8f98415e3cf",
|
"sha256:a5e89eabaa0ca72ce1b7c8104a740d44cdb67942cbbed00c69a4c0541de17107",
|
||||||
"sha256:b5ab0b8918c136345ff045d4b3d5f719b505b7c8af45092d7f45e304f55e50a1",
|
"sha256:b78d7c2c36b51c0041b9bf000be4adb09f4112bfc40bc7a9d48ac0b0dfad139e",
|
||||||
"sha256:e575c57dc8b5b2b2caa436c16d44ef6981f2235eb7179bfc847557886376d740",
|
"sha256:e53165dd14d19abc7f50733f365de431e51d1d262db40c0ee22e271a074fac59",
|
||||||
"sha256:f9eae277dd240ae19bb06ff4e2346e771252b0e619421965504bd1b1bba7c5fa"
|
"sha256:e92768d17473657c87e98b79a4c7724b0ddfa23211b05ce137bfdc55e734e36f"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==4.21.9"
|
"version": "==4.21.10"
|
||||||
},
|
},
|
||||||
"qrcode": {
|
"qrcode": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
@ -234,11 +234,11 @@
|
||||||
},
|
},
|
||||||
"pylint": {
|
"pylint": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:15060cc22ed6830a4049cf40bc24977744df2e554d38da1b2657591de5bcd052",
|
"sha256:1d561d1d3e8be9dd880edc685162fbdaa0409c88b9b7400873c0cf345602e326",
|
||||||
"sha256:25b13ddcf5af7d112cf96935e21806c1da60e676f952efb650130f2a4483421c"
|
"sha256:91e4776dbcb4b4d921a3e4b6fec669551107ba11f29d9199154a01622e460a57"
|
||||||
],
|
],
|
||||||
"index": "pypi",
|
"index": "pypi",
|
||||||
"version": "==2.15.6"
|
"version": "==2.15.7"
|
||||||
},
|
},
|
||||||
"pyparsing": {
|
"pyparsing": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
|
|
|
||||||
53
README.md
53
README.md
|
|
@ -3,14 +3,14 @@
|
||||||
[](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) secret keys from export QR codes of "Google Authenticator" app.
|
Extract two-factor authentication (2FA, TFA, one time passwords, 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.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
@ -26,15 +26,16 @@ The secret and otp values can be printed and exported to json or csv. The QR cod
|
||||||
|
|
||||||
## Program help: arguments and options
|
## Program help: arguments and options
|
||||||
|
|
||||||
<pre>usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv 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 (default: -) 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 to json file
|
--json FILE, -j FILE export json file
|
||||||
--csv FILE, -c FILE export to csv file
|
--csv FILE, -c FILE export csv file
|
||||||
|
--keepass FILE, -k FILE export totp/hotp csv file(s) for KeePass
|
||||||
--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
|
||||||
|
|
@ -47,7 +48,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.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
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
|
|
@ -57,6 +58,44 @@ For printing QR codes, the qrcode module is required, otherwise it can be omitte
|
||||||
|
|
||||||
pip install qrcode[pil]
|
pip install qrcode[pil]
|
||||||
|
|
||||||
|
## KeePass
|
||||||
|
|
||||||
|
[KeePass 2.51](https://keepass.info/news/n220506_2.51.html) (released in May 2022) and newer [support the generation of OTPs (TOTP and HOTP)](https://keepass.info/help/base/placeholders.html#otp).
|
||||||
|
|
||||||
|
KeePass can generate the second factor password (2FA) if the OTP secret is stored in `TimeOtp-Secret-Base32` string field for TOTP or `HmacOtp-Secret-Base32` string field for HOTP. You view or edit them in entry dialog on the 'Advanced' tab page.
|
||||||
|
|
||||||
|
KeePass provides menu commands in the main window for generating one-time passwords ('Copy HMAC-Based OTP', 'Show HMAC-Based OTP', 'Copy Time-Based OTP', 'Show Time-Based OTP'). Furthermore, one-time passwords can be generated during auto-type using the {HMACOTP} and {TIMEOTP} placeholders.
|
||||||
|
|
||||||
|
In order to simplify the usage of the second factor password generation in KeePass a specific KeePass CSV export is available with option `-keepass` or `-k`. This KeePass CSV file can be imported by the ["Generic CSV Importer" of KeePass](https://keepass.info/help/kb/imp_csv.html).
|
||||||
|
|
||||||
|
If TOTP and HOTP entries have to be exported, then two files with an intermediate suffix .totp or .hotp will be added to the KeePass export filename.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
- Only TOTP entries to export and parameter --keepass example_keepass_output.csv<br>
|
||||||
|
→ example_keepass_output.csv with TOTP entries will be exported
|
||||||
|
- Only HOTP entries to export and parameter --keepass example_keepass_output.csv<br>
|
||||||
|
→ example_keepass_output.csv with HOTP entries will be exported
|
||||||
|
- If both TOTP and HOTP entries to export and parameter --keepass example_keepass_output.csv<br>
|
||||||
|
→ example_keepass_output.totp.csv with TOTP entries will be exported<br>
|
||||||
|
→ example_keepass_output.hotp.csv with HOTP entries will be exported
|
||||||
|
|
||||||
|
Import CSV with TOTP entries in KeePass as
|
||||||
|
|
||||||
|
- Title
|
||||||
|
- User Name
|
||||||
|
- String (TimeOtp-Secret-Base32)
|
||||||
|
- Group (/)
|
||||||
|
|
||||||
|
Import CSV with HOTP entries in KeePass as
|
||||||
|
|
||||||
|
- Title
|
||||||
|
- User Name
|
||||||
|
- String (HmacOtp-Secret-Base32)
|
||||||
|
- String (HmacOtp-Counter)
|
||||||
|
- Group (/)
|
||||||
|
|
||||||
|
KeePass can be used as a backup for one time passwords (second factor) from the mobile phone.
|
||||||
|
|
||||||
## Technical background
|
## Technical background
|
||||||
|
|
||||||
The export QR code of "Google Authenticator" contains the URL `otpauth-migration://offline?data=...`.
|
The export QR code of "Google Authenticator" contains the URL `otpauth-migration://offline?data=...`.
|
||||||
|
|
@ -66,7 +105,7 @@ 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.9 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.9).
|
The generated protobuf Python code was generated by protoc 21.10 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.10).
|
||||||
|
|
||||||
## References
|
## References
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,6 @@ 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://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||||
|
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
||||||
|
|
|
||||||
2
example_keepass_output.hotp.csv
Normal file
2
example_keepass_output.hotp.csv
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
Title,User Name,HmacOtp-Secret-Base32,HmacOtp-Counter,Group
|
||||||
|
,hotp demo,7KSQL2JTUDIS5EF65KLMRQIIGY,4,OTP/HOTP
|
||||||
|
5
example_keepass_output.totp.csv
Normal file
5
example_keepass_output.totp.csv
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
Title,User Name,TimeOtp-Secret-Base32,Group
|
||||||
|
raspberrypi,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||||
|
,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||||
|
,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||||
|
raspberrypi,pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,OTP/TOTP
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
name,secret,issuer,type,url
|
name,secret,issuer,type,counter,url
|
||||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,OTP_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
|
||||||
|
|
|
||||||
|
|
|
@ -3,28 +3,40 @@
|
||||||
"name": "pi@raspberrypi",
|
"name": "pi@raspberrypi",
|
||||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||||
"issuer": "raspberrypi",
|
"issuer": "raspberrypi",
|
||||||
"type": "OTP_TOTP",
|
"type": "totp",
|
||||||
|
"counter": null,
|
||||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi"
|
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pi@raspberrypi",
|
"name": "pi@raspberrypi",
|
||||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||||
"issuer": "",
|
"issuer": "",
|
||||||
"type": "OTP_TOTP",
|
"type": "totp",
|
||||||
|
"counter": null,
|
||||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY"
|
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pi@raspberrypi",
|
"name": "pi@raspberrypi",
|
||||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||||
"issuer": "",
|
"issuer": "",
|
||||||
"type": "OTP_TOTP",
|
"type": "totp",
|
||||||
|
"counter": null,
|
||||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY"
|
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "pi@raspberrypi",
|
"name": "pi@raspberrypi",
|
||||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||||
"issuer": "raspberrypi",
|
"issuer": "raspberrypi",
|
||||||
"type": "OTP_TOTP",
|
"type": "totp",
|
||||||
|
"counter": null,
|
||||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi"
|
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "hotp demo",
|
||||||
|
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||||
|
"issuer": "",
|
||||||
|
"type": "hotp",
|
||||||
|
"counter": 4,
|
||||||
|
"url": "otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -65,14 +65,17 @@ def main(sys_args):
|
||||||
|
|
||||||
otps = extract_otps(args)
|
otps = extract_otps(args)
|
||||||
write_csv(args, otps)
|
write_csv(args, otps)
|
||||||
|
write_keepass_csv(args, otps)
|
||||||
write_json(args, otps)
|
write_json(args, otps)
|
||||||
|
|
||||||
|
|
||||||
def parse_args(sys_args):
|
def parse_args(sys_args):
|
||||||
arg_parser = argparse.ArgumentParser()
|
formatter = lambda prog: argparse.HelpFormatter(prog,max_help_position=52)
|
||||||
|
arg_parser = argparse.ArgumentParser(formatter_class=formatter)
|
||||||
arg_parser.add_argument('infile', help='file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
|
arg_parser.add_argument('infile', help='file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored')
|
||||||
arg_parser.add_argument('--json', '-j', help='export to json file', metavar=('FILE'))
|
arg_parser.add_argument('--json', '-j', help='export json file', metavar=('FILE'))
|
||||||
arg_parser.add_argument('--csv', '-c', help='export to csv file', metavar=('FILE'))
|
arg_parser.add_argument('--csv', '-c', help='export csv file', metavar=('FILE'))
|
||||||
|
arg_parser.add_argument('--keepass', '-k', help='export totp/hotp csv file(s) for KeePass', 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')
|
arg_parser.add_argument('--verbose', '-v', help='verbose output', action='count')
|
||||||
|
|
@ -102,13 +105,15 @@ def extract_otps(args):
|
||||||
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 = 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_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,
|
||||||
"url": otp_url
|
"url": otp_url
|
||||||
}
|
}
|
||||||
if not quiet:
|
if not quiet:
|
||||||
|
|
@ -154,6 +159,10 @@ def get_enum_name_by_number(parent, field_name):
|
||||||
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name
|
return parent.DESCRIPTOR.fields_by_name[field_name].enum_type.values_by_number.get(field_value).name
|
||||||
|
|
||||||
|
|
||||||
|
def get_otp_type_str_from_code(otp_type):
|
||||||
|
return 'totp' if otp_type == 2 else 'hotp'
|
||||||
|
|
||||||
|
|
||||||
def convert_secret_from_bytes_to_base32_str(bytes):
|
def convert_secret_from_bytes_to_base32_str(bytes):
|
||||||
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
|
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
|
||||||
|
|
||||||
|
|
@ -162,7 +171,7 @@ def build_otp_url(secret, raw_otp):
|
||||||
url_params = {'secret': secret}
|
url_params = {'secret': secret}
|
||||||
if raw_otp.type == 1: url_params['counter'] = raw_otp.counter
|
if raw_otp.type == 1: url_params['counter'] = raw_otp.counter
|
||||||
if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
|
if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
|
||||||
otp_url = 'otpauth://{}/{}?'.format('totp' if raw_otp.type == 2 else 'hotp', quote(raw_otp.name)) + urlencode(url_params)
|
otp_url = 'otpauth://{}/{}?'.format(get_otp_type_str_from_code(raw_otp.type), quote(raw_otp.name)) + urlencode(url_params)
|
||||||
return otp_url
|
return otp_url
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -171,6 +180,8 @@ def print_otp(otp):
|
||||||
print('Secret: {}'.format(otp['secret']))
|
print('Secret: {}'.format(otp['secret']))
|
||||||
if otp['issuer']: print('Issuer: {}'.format(otp['issuer']))
|
if otp['issuer']: print('Issuer: {}'.format(otp['issuer']))
|
||||||
print('Type: {}'.format(otp['type']))
|
print('Type: {}'.format(otp['type']))
|
||||||
|
if otp['type'] == 'hotp':
|
||||||
|
print('Counter: {}'.format(otp['counter']))
|
||||||
if verbose:
|
if verbose:
|
||||||
print(otp['url'])
|
print(otp['url'])
|
||||||
|
|
||||||
|
|
@ -209,7 +220,48 @@ def write_csv(args, otps):
|
||||||
writer = csv.DictWriter(outfile, otps[0].keys())
|
writer = csv.DictWriter(outfile, otps[0].keys())
|
||||||
writer.writeheader()
|
writer.writeheader()
|
||||||
writer.writerows(otps)
|
writer.writerows(otps)
|
||||||
if not quiet: print("Exported {} otps to csv".format(len(otps)))
|
if not quiet: print("Exported {} otps to csv {}".format(len(otps), args.csv))
|
||||||
|
|
||||||
|
|
||||||
|
def write_keepass_csv(args, otps):
|
||||||
|
global verbose, quiet
|
||||||
|
if args.keepass and len(otps) > 0:
|
||||||
|
has_totp = has_otp_type(otps, 'totp')
|
||||||
|
has_hotp = has_otp_type(otps, 'hotp')
|
||||||
|
otp_filename_totp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "totp")
|
||||||
|
otp_filename_hotp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "hotp")
|
||||||
|
count_totp_entries = 0
|
||||||
|
count_hotp_entries = 0
|
||||||
|
if has_totp:
|
||||||
|
with open(otp_filename_totp, "w") as outfile:
|
||||||
|
writer = csv.DictWriter(outfile, ["Title", "User Name", "TimeOtp-Secret-Base32", "Group"])
|
||||||
|
writer.writeheader()
|
||||||
|
for otp in otps:
|
||||||
|
if otp['type'] == 'totp':
|
||||||
|
writer.writerow({
|
||||||
|
'Title': otp['issuer'],
|
||||||
|
'User Name': otp['name'],
|
||||||
|
'TimeOtp-Secret-Base32': otp['secret'] if otp['type'] == 'totp' else None,
|
||||||
|
'Group': "OTP/{}".format(otp['type'].upper())
|
||||||
|
})
|
||||||
|
count_totp_entries += 1
|
||||||
|
if has_hotp:
|
||||||
|
with open(otp_filename_hotp, "w") as outfile:
|
||||||
|
writer = csv.DictWriter(outfile, ["Title", "User Name", "HmacOtp-Secret-Base32", "HmacOtp-Counter", "Group"])
|
||||||
|
writer.writeheader()
|
||||||
|
for otp in otps:
|
||||||
|
if otp['type'] == 'hotp':
|
||||||
|
writer.writerow({
|
||||||
|
'Title': otp['issuer'],
|
||||||
|
'User Name': otp['name'],
|
||||||
|
'HmacOtp-Secret-Base32': otp['secret'] if otp['type'] == 'hotp' else None,
|
||||||
|
'HmacOtp-Counter': otp['counter'] if otp['type'] == 'hotp' else None,
|
||||||
|
'Group': "OTP/{}".format(otp['type'].upper())
|
||||||
|
})
|
||||||
|
count_hotp_entries += 1
|
||||||
|
if not quiet:
|
||||||
|
if count_totp_entries > 0: print("Exported {} totp entries to keepass csv file {}".format(count_totp_entries, otp_filename_totp))
|
||||||
|
if count_hotp_entries > 0: print("Exported {} hotp entries to keepass csv file {}".format(count_hotp_entries, otp_filename_hotp))
|
||||||
|
|
||||||
|
|
||||||
def write_json(args, otps):
|
def write_json(args, otps):
|
||||||
|
|
@ -217,7 +269,20 @@ def write_json(args, otps):
|
||||||
if args.json:
|
if args.json:
|
||||||
with open(args.json, "w") as outfile:
|
with open(args.json, "w") 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)))
|
if not quiet: print("Exported {} otp entries to json {}".format(len(otps), args.json))
|
||||||
|
|
||||||
|
|
||||||
|
def has_otp_type(otps, otp_type):
|
||||||
|
for otp in otps:
|
||||||
|
if otp['type'] == otp_type:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def add_pre_suffix(file, pre_suffix):
|
||||||
|
'''filename.ext, pre -> filename.pre.ext'''
|
||||||
|
name, ext = path.splitext(file)
|
||||||
|
return name + "." + pre_suffix + (ext if ext else "")
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
|
||||||
11
test/example_export_only_totp.txt
Normal file
11
test/example_export_only_totp.txt
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/
|
||||||
|
# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||||
|
otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B
|
||||||
|
|
||||||
|
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D
|
||||||
|
|
||||||
|
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||||
|
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
|
||||||
|
|
@ -21,7 +21,7 @@ batch_id: -1320898453
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Issuer: raspberrypi
|
Issuer: raspberrypi
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -44,7 +44,7 @@ batch_id: -2094403140
|
||||||
2. Secret Key
|
2. Secret Key
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -76,7 +76,7 @@ batch_id: -1822886384
|
||||||
3. Secret Key
|
3. Secret Key
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -84,6 +84,31 @@ otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Issuer: raspberrypi
|
Issuer: raspberrypi
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||||
|
|
||||||
|
|
||||||
|
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||||
|
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
||||||
|
|
||||||
|
4. Payload Line
|
||||||
|
otp_parameters {
|
||||||
|
secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106"
|
||||||
|
name: "hotp demo"
|
||||||
|
algorithm: ALGO_SHA1
|
||||||
|
digits: 1
|
||||||
|
type: OTP_HOTP
|
||||||
|
counter: 4
|
||||||
|
}
|
||||||
|
version: 1
|
||||||
|
batch_size: 1
|
||||||
|
batch_id: -1558849573
|
||||||
|
|
||||||
|
|
||||||
|
5. Secret Key
|
||||||
|
Name: hotp demo
|
||||||
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
Type: hotp
|
||||||
|
Counter: 4
|
||||||
|
otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Issuer: raspberrypi
|
Issuer: raspberrypi
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
|
|
||||||
█▀▀▀▀▀█ ▄▀▄▄█ █▀ ▀▀▀▀▀█ ▄▄ █▀▀▀▀▀█
|
█▀▀▀▀▀█ ▄▀▄▄█ █▀ ▀▀▀▀▀█ ▄▄ █▀▀▀▀▀█
|
||||||
|
|
@ -28,7 +28,7 @@ Type: OTP_TOTP
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
|
|
||||||
█▀▀▀▀▀█ ▀▀██ █▄▀█ ▀▄▄▀█▀▄ █▀▀▀▀▀█
|
█▀▀▀▀▀█ ▀▀██ █▄▀█ ▀▄▄▀█▀▄ █▀▀▀▀▀█
|
||||||
|
|
@ -53,7 +53,7 @@ Type: OTP_TOTP
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
|
|
||||||
█▀▀▀▀▀█ ▀▀██ █▄▀█ ▀▄▄▀█▀▄ █▀▀▀▀▀█
|
█▀▀▀▀▀█ ▀▀██ █▄▀█ ▀▄▄▀█▀▄ █▀▀▀▀▀█
|
||||||
|
|
@ -79,7 +79,7 @@ Type: OTP_TOTP
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Issuer: raspberrypi
|
Issuer: raspberrypi
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
|
|
||||||
█▀▀▀▀▀█ ▄▀▄▄█ █▀ ▀▀▀▀▀█ ▄▄ █▀▀▀▀▀█
|
█▀▀▀▀▀█ ▄▀▄▄█ █▀ ▀▀▀▀▀█ ▄▄ █▀▀▀▀▀█
|
||||||
|
|
@ -104,3 +104,31 @@ Type: OTP_TOTP
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Name: hotp demo
|
||||||
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
Type: hotp
|
||||||
|
Counter: 4
|
||||||
|
|
||||||
|
|
||||||
|
█▀▀▀▀▀█ ▄█▀▄▄ ▄▄▄██ █▄▄██ █▄ █▀▀▀▀▀█
|
||||||
|
█ ███ █ ▀▄ ▄▄▄ ▀▄ ▄▄▀ █▀ █ ███ █
|
||||||
|
█ ▀▀▀ █ ▀█ ▄▄█▄ ▄▀█▀▀██▄▄██▄ █ ▀▀▀ █
|
||||||
|
▀▀▀▀▀▀▀ ▀▄▀ █▄▀▄█▄█▄█ ▀▄█▄█ █ ▀▀▀▀▀▀▀
|
||||||
|
█▄█ █ ▀▄▄ ▀ ▄▄███▄█▄ ▄█▀ ▀█ ▄█▄▄▀▄
|
||||||
|
▄██▄▀▄▀██▄▀▄▀ ▀▀█ ▀▄ █▄▀██▄ ▀▄▀▀▀▄█▀
|
||||||
|
▄ ▄█▄▀▀▀▀█▄▄▄▀▄█▄ ▄ ▄██▀█▀▄ ▀▄█▄ █▀▀█
|
||||||
|
▄▀▄▀██▀█▀ ██▀▄ ▀▀ ▄▄▄█▄██ ▄▀█▄▄▄ ▀▄▀
|
||||||
|
█▄▀▀▀█▀█▄ ▄ ▀ ▀█ ▄ ▄█ █▄▀█▄ █▄█ ▀▄
|
||||||
|
▀▀██▄█▀ ▄█▄▀▀█▄ ▄█▀██▄▄█▄ █▀▄█ ▀▀▀█
|
||||||
|
█████▄▀▀█▀▀█▀▀▄ ▄ ▀█▀▄ ██▄ ▄███ ▄▀█
|
||||||
|
▄▄█▀▀▀█▀█▄█ ▄█▄▄█ ▀▀ ▄▀▄ ▄█▀▄▄█▀▀▄▄
|
||||||
|
██▄ █▀▄▀▀ █ ▀██ █▄ ▄ █ ▀▄█▀▄█▄██
|
||||||
|
▀▄▀ █ ▀▄▀▄██▄█ ▀█▀▄▄ ██▄▄▄▀ ▀▄ ▄█ ███
|
||||||
|
▀ ▀▀ ▀▀▄ ▄▄▄█▄██▀▀ ▄█ ▀ █▀▀█▀▀▀█▀ █
|
||||||
|
█▀▀▀▀▀█ █▄█▀█▀▀█▀ ▄█ ▀▄▄▀█ ▀ █▀▀ ▀
|
||||||
|
█ ███ █ ▀█▀▀ ▀ █ ▄ ▄█▄█ █▄ █▀▀██▀ ██
|
||||||
|
█ ▀▀▀ █ ▀▄▀▄█▀▀▄ ▀▀█▄▄ ▀▄▄█ █▀▀▀▄▀
|
||||||
|
▀▀▀▀▀▀▀ ▀▀▀▀ ▀ ▀ ▀▀▀ ▀▀ ▀ ▀▀▀▀ ▀▀
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
# 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_file, remove_dir_with_files, read_file_to_str
|
from utils import read_csv, read_json, 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
|
||||||
|
|
||||||
|
|
@ -47,6 +47,58 @@ def test_extract_csv(capsys):
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def test_keepass_csv(capsys):
|
||||||
|
'''Two csv files .totp and .htop are generated.'''
|
||||||
|
# Arrange
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
# Act
|
||||||
|
extract_otp_secret_keys.main(['-q', '-k', 'test_example_keepass_output.csv', 'example_export.txt'])
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
expected_totp_csv = read_csv('example_keepass_output.totp.csv')
|
||||||
|
expected_hotp_csv = read_csv('example_keepass_output.hotp.csv')
|
||||||
|
actual_totp_csv = read_csv('test_example_keepass_output.totp.csv')
|
||||||
|
actual_hotp_csv = read_csv('test_example_keepass_output.hotp.csv')
|
||||||
|
|
||||||
|
assert actual_totp_csv == expected_totp_csv
|
||||||
|
assert actual_hotp_csv == expected_hotp_csv
|
||||||
|
assert not file_exits('test_example_keepass_output.csv')
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
|
assert captured.out == ''
|
||||||
|
assert captured.err == ''
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
|
def test_single_keepass_csv(capsys):
|
||||||
|
'''Does not add .totp or .hotp pre-suffix'''
|
||||||
|
# Arrange
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
# Act
|
||||||
|
extract_otp_secret_keys.main(['-q', '-k', 'test_example_keepass_output.csv', 'test/example_export_only_totp.txt'])
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
expected_totp_csv = read_csv('example_keepass_output.totp.csv')
|
||||||
|
actual_totp_csv = read_csv('test_example_keepass_output.csv')
|
||||||
|
|
||||||
|
assert actual_totp_csv == expected_totp_csv
|
||||||
|
assert not file_exits('test_example_keepass_output.totp.csv')
|
||||||
|
assert not file_exits('test_example_keepass_output.hotp.csv')
|
||||||
|
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
|
assert captured.out == ''
|
||||||
|
assert captured.err == ''
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
cleanup()
|
||||||
|
|
||||||
|
|
||||||
def test_extract_json(capsys):
|
def test_extract_json(capsys):
|
||||||
# Arrange
|
# Arrange
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
@ -79,20 +131,25 @@ def test_extract_stdout(capsys):
|
||||||
expected_stdout = '''Name: pi@raspberrypi
|
expected_stdout = '''Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Issuer: raspberrypi
|
Issuer: raspberrypi
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Issuer: raspberrypi
|
Issuer: raspberrypi
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
|
Name: hotp demo
|
||||||
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
Type: hotp
|
||||||
|
Counter: 4
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
@ -110,22 +167,22 @@ def test_extract_not_encoded_plus(capsys):
|
||||||
expected_stdout = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
expected_stdout = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
||||||
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
||||||
Issuer: SerenityLabs
|
Issuer: SerenityLabs
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: SerenityLabs:test2@serenitylabs.co.uk
|
Name: SerenityLabs:test2@serenitylabs.co.uk
|
||||||
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
||||||
Issuer: SerenityLabs
|
Issuer: SerenityLabs
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: SerenityLabs:test3@serenitylabs.co.uk
|
Name: SerenityLabs:test3@serenitylabs.co.uk
|
||||||
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
||||||
Issuer: SerenityLabs
|
Issuer: SerenityLabs
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: SerenityLabs:test4@serenitylabs.co.uk
|
Name: SerenityLabs:test4@serenitylabs.co.uk
|
||||||
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
||||||
Issuer: SerenityLabs
|
Issuer: SerenityLabs
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
|
@ -209,6 +266,7 @@ def test_extract_help(capsys):
|
||||||
assert pytest_wrapped_e.type == SystemExit
|
assert pytest_wrapped_e.type == SystemExit
|
||||||
assert pytest_wrapped_e.value.code == 0
|
assert pytest_wrapped_e.value.code == 0
|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -220,7 +278,14 @@ def test_verbose_and_quiet(capsys):
|
||||||
assert len(captured.out) > 0
|
assert len(captured.out) > 0
|
||||||
assert 'The arguments --verbose and --quiet are mutually exclusive.' in captured.out
|
assert 'The arguments --verbose and --quiet are mutually exclusive.' in captured.out
|
||||||
|
|
||||||
|
|
||||||
|
def test_add_pre_suffix(capsys):
|
||||||
|
assert extract_otp_secret_keys.add_pre_suffix("name.csv", "totp") == "name.totp.csv"
|
||||||
|
assert extract_otp_secret_keys.add_pre_suffix("name.csv", "") == "name..csv"
|
||||||
|
assert extract_otp_secret_keys.add_pre_suffix("name", "totp") == "name.totp"
|
||||||
|
|
||||||
|
|
||||||
def cleanup():
|
def cleanup():
|
||||||
remove_file('test_example_output.csv')
|
remove_files('test_example_*.csv')
|
||||||
remove_file('test_example_output.json')
|
remove_files('test_example_*.json')
|
||||||
remove_dir_with_files('testout/')
|
remove_dir_with_files('testout/')
|
||||||
|
|
|
||||||
|
|
@ -53,20 +53,25 @@ class TestExtract(unittest.TestCase):
|
||||||
'Name: pi@raspberrypi',
|
'Name: pi@raspberrypi',
|
||||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||||
'Issuer: raspberrypi',
|
'Issuer: raspberrypi',
|
||||||
'Type: OTP_TOTP',
|
'Type: totp',
|
||||||
'',
|
'',
|
||||||
'Name: pi@raspberrypi',
|
'Name: pi@raspberrypi',
|
||||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||||
'Type: OTP_TOTP',
|
'Type: totp',
|
||||||
'',
|
'',
|
||||||
'Name: pi@raspberrypi',
|
'Name: pi@raspberrypi',
|
||||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||||
'Type: OTP_TOTP',
|
'Type: totp',
|
||||||
'',
|
'',
|
||||||
'Name: pi@raspberrypi',
|
'Name: pi@raspberrypi',
|
||||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||||
'Issuer: raspberrypi',
|
'Issuer: raspberrypi',
|
||||||
'Type: OTP_TOTP',
|
'Type: totp',
|
||||||
|
'',
|
||||||
|
'Name: hotp demo',
|
||||||
|
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||||
|
'Type: hotp',
|
||||||
|
'Counter: 4',
|
||||||
''
|
''
|
||||||
]
|
]
|
||||||
self.assertEqual(output, expected_output)
|
self.assertEqual(output, expected_output)
|
||||||
|
|
@ -81,20 +86,25 @@ class TestExtract(unittest.TestCase):
|
||||||
expected_output = '''Name: pi@raspberrypi
|
expected_output = '''Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Issuer: raspberrypi
|
Issuer: raspberrypi
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Issuer: raspberrypi
|
Issuer: raspberrypi
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
|
Name: hotp demo
|
||||||
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
Type: hotp
|
||||||
|
Counter: 4
|
||||||
|
|
||||||
'''
|
'''
|
||||||
self.assertEqual(actual_output, expected_output)
|
self.assertEqual(actual_output, expected_output)
|
||||||
|
|
@ -108,22 +118,22 @@ Type: OTP_TOTP
|
||||||
expected_output = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
expected_output = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
||||||
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
||||||
Issuer: SerenityLabs
|
Issuer: SerenityLabs
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: SerenityLabs:test2@serenitylabs.co.uk
|
Name: SerenityLabs:test2@serenitylabs.co.uk
|
||||||
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
||||||
Issuer: SerenityLabs
|
Issuer: SerenityLabs
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: SerenityLabs:test3@serenitylabs.co.uk
|
Name: SerenityLabs:test3@serenitylabs.co.uk
|
||||||
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
||||||
Issuer: SerenityLabs
|
Issuer: SerenityLabs
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
Name: SerenityLabs:test4@serenitylabs.co.uk
|
Name: SerenityLabs:test4@serenitylabs.co.uk
|
||||||
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
||||||
Issuer: SerenityLabs
|
Issuer: SerenityLabs
|
||||||
Type: OTP_TOTP
|
Type: totp
|
||||||
|
|
||||||
'''
|
'''
|
||||||
self.assertEqual(actual_output, expected_output)
|
self.assertEqual(actual_output, expected_output)
|
||||||
|
|
|
||||||
12
utils.py
12
utils.py
|
|
@ -19,6 +19,7 @@ import os
|
||||||
import shutil
|
import shutil
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
import sys
|
import sys
|
||||||
|
import glob
|
||||||
|
|
||||||
|
|
||||||
# Ref. https://stackoverflow.com/a/16571630
|
# Ref. https://stackoverflow.com/a/16571630
|
||||||
|
|
@ -39,8 +40,17 @@ with Capturing() as output:
|
||||||
sys.stdout = self._stdout
|
sys.stdout = self._stdout
|
||||||
|
|
||||||
|
|
||||||
|
def file_exits(file):
|
||||||
|
return os.path.isfile(file)
|
||||||
|
|
||||||
|
|
||||||
def remove_file(file):
|
def remove_file(file):
|
||||||
if os.path.isfile(file): os.remove(file)
|
if file_exits(file): os.remove(file)
|
||||||
|
|
||||||
|
|
||||||
|
def remove_files(glob_pattern):
|
||||||
|
for f in glob.glob(glob_pattern):
|
||||||
|
os.remove(f)
|
||||||
|
|
||||||
|
|
||||||
def remove_dir_with_files(dir):
|
def remove_dir_with_files(dir):
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue