mirror of
https://github.com/scito/extract_otp_secret_keys.git
synced 2025-12-06 23:05:06 +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": {
|
||||
"hashes": [
|
||||
"sha256:2c9c2ed7466ad565f18668aa4731c535511c5d9a40c6da39524bccf43e441719",
|
||||
"sha256:48e2cd6b88c6ed3d5877a3ea40df79d08374088e89bedc32557348848dff250b",
|
||||
"sha256:5b0834e61fb38f34ba8840d7dcb2e5a2f03de0c714e0293b3963b79db26de8ce",
|
||||
"sha256:61f21493d96d2a77f9ca84fefa105872550ab5ef71d21c458eb80edcf4885a99",
|
||||
"sha256:6e0be9f09bf9b6cf497b27425487706fa48c6d1632ddd94dab1a5fe11a422392",
|
||||
"sha256:6e312e280fbe3c74ea9e080d9e6080b636798b5e3939242298b591064470b06b",
|
||||
"sha256:7eb8f2cc41a34e9c956c256e3ac766cf4e1a4c9c925dc757a41a01be3e852965",
|
||||
"sha256:84ea107016244dfc1eecae7684f7ce13c788b9a644cd3fca5b77871366556444",
|
||||
"sha256:9227c14010acd9ae7702d6467b4625b6fe853175a6b150e539b21d2b2f2b409c",
|
||||
"sha256:a419cc95fca8694804709b8c4f2326266d29659b126a93befe210f5bbc772536",
|
||||
"sha256:a7d0ea43949d45b836234f4ebb5ba0b22e7432d065394b532cdca8f98415e3cf",
|
||||
"sha256:b5ab0b8918c136345ff045d4b3d5f719b505b7c8af45092d7f45e304f55e50a1",
|
||||
"sha256:e575c57dc8b5b2b2caa436c16d44ef6981f2235eb7179bfc847557886376d740",
|
||||
"sha256:f9eae277dd240ae19bb06ff4e2346e771252b0e619421965504bd1b1bba7c5fa"
|
||||
"sha256:0413addc126c40a5440ee59be098de1007183d68e9f5f20ed5fbc44848f417ca",
|
||||
"sha256:05cbcb9a25cd781fd949f93f6f98a911883868c0360c6d2264fc99a903c8f0d7",
|
||||
"sha256:0c968753028cb14b1d24cc839723f7e9505b305fc588a37a9e0f7d270cb59d89",
|
||||
"sha256:2a172741b5b041a896b621cef4277077afd571e0d3a6e524e7171f1c70e33200",
|
||||
"sha256:3f08f04b4f101dd469efbcc1731fbf48068eccd8a42f4e2ea530aa012a5f56f8",
|
||||
"sha256:4d97c16c0d11155b3714a29245461f0eb60cace294455077f3a3b8a629afa383",
|
||||
"sha256:5096b3922b45e4b7a04d3d3cb855d13bb5ccd4d5e44b129e706232ebf0ffb870",
|
||||
"sha256:5efa8a8162ada7e10847140308fbf84fdc5b89dc21655d12ec04aed87284fe07",
|
||||
"sha256:6b809f20923b6ef49dc1755cb50bdb21be179b4a3c7ffcab1fe5d3f139b58a51",
|
||||
"sha256:81b233a06c62387ea5c9be2cd9aedd2ba09940e91da53b920e9ff5bd98e48e7f",
|
||||
"sha256:a5e89eabaa0ca72ce1b7c8104a740d44cdb67942cbbed00c69a4c0541de17107",
|
||||
"sha256:b78d7c2c36b51c0041b9bf000be4adb09f4112bfc40bc7a9d48ac0b0dfad139e",
|
||||
"sha256:e53165dd14d19abc7f50733f365de431e51d1d262db40c0ee22e271a074fac59",
|
||||
"sha256:e92768d17473657c87e98b79a4c7724b0ddfa23211b05ce137bfdc55e734e36f"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==4.21.9"
|
||||
"version": "==4.21.10"
|
||||
},
|
||||
"qrcode": {
|
||||
"hashes": [
|
||||
|
|
@ -234,11 +234,11 @@
|
|||
},
|
||||
"pylint": {
|
||||
"hashes": [
|
||||
"sha256:15060cc22ed6830a4049cf40bc24977744df2e554d38da1b2657591de5bcd052",
|
||||
"sha256:25b13ddcf5af7d112cf96935e21806c1da60e676f952efb650130f2a4483421c"
|
||||
"sha256:1d561d1d3e8be9dd880edc685162fbdaa0409c88b9b7400873c0cf345602e326",
|
||||
"sha256:91e4776dbcb4b4d921a3e4b6fec669551107ba11f29d9199154a01622e460a57"
|
||||
],
|
||||
"index": "pypi",
|
||||
"version": "==2.15.6"
|
||||
"version": "==2.15.7"
|
||||
},
|
||||
"pyparsing": {
|
||||
"hashes": [
|
||||
|
|
|
|||
65
README.md
65
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/blob/master/Pipfile.lock)
|
||||

|
||||

|
||||
[](https://github.com/scito/extract_otp_secret_keys/blob/master/LICENSE)
|
||||
[](https://github.com/scito/extract_otp_secret_keys/tags)
|
||||
[](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.
|
||||
|
||||
## Usage
|
||||
|
|
@ -26,19 +26,20 @@ The secret and otp values can be printed and exported to json or csv. The QR cod
|
|||
|
||||
## 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:
|
||||
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:
|
||||
-h, --help show this help message and exit
|
||||
--json FILE, -j FILE export to json file
|
||||
--csv FILE, -c FILE export to csv file
|
||||
--printqr, -p print QR code(s) as text to the terminal (requires qrcode module)
|
||||
--saveqr DIR, -s DIR save QR code(s) as images to the given folder (requires qrcode module)
|
||||
--verbose, -v verbose output
|
||||
--quiet, -q no stdout output</pre>
|
||||
-h, --help show this help message and exit
|
||||
--json FILE, -j FILE export json 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)
|
||||
--saveqr DIR, -s DIR save QR code(s) as images to the given folder (requires qrcode module)
|
||||
--verbose, -v verbose output
|
||||
--quiet, -q no stdout output</pre>
|
||||
|
||||
## Dependencies
|
||||
|
||||
|
|
@ -47,7 +48,7 @@ options:
|
|||
Known to work with
|
||||
|
||||
* 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.
|
||||
|
||||
|
|
@ -57,6 +58,44 @@ For printing QR codes, the qrcode module is required, otherwise it can be omitte
|
|||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -9,3 +9,6 @@ otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXB
|
|||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B
|
||||
|
||||
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
||||
|
|
|
|||
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
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
name,secret,issuer,type,counter,url
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,totp,,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
hotp demo,7KSQL2JTUDIS5EF65KLMRQIIGY,,hotp,4,otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
|
|
|
|||
|
|
|
@ -3,28 +3,40 @@
|
|||
"name": "pi@raspberrypi",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "raspberrypi",
|
||||
"type": "OTP_TOTP",
|
||||
"type": "totp",
|
||||
"counter": null,
|
||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi"
|
||||
},
|
||||
{
|
||||
"name": "pi@raspberrypi",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "",
|
||||
"type": "OTP_TOTP",
|
||||
"type": "totp",
|
||||
"counter": null,
|
||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY"
|
||||
},
|
||||
{
|
||||
"name": "pi@raspberrypi",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "",
|
||||
"type": "OTP_TOTP",
|
||||
"type": "totp",
|
||||
"counter": null,
|
||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY"
|
||||
},
|
||||
{
|
||||
"name": "pi@raspberrypi",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "raspberrypi",
|
||||
"type": "OTP_TOTP",
|
||||
"type": "totp",
|
||||
"counter": null,
|
||||
"url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi"
|
||||
},
|
||||
{
|
||||
"name": "hotp demo",
|
||||
"secret": "7KSQL2JTUDIS5EF65KLMRQIIGY",
|
||||
"issuer": "",
|
||||
"type": "hotp",
|
||||
"counter": 4,
|
||||
"url": "otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4"
|
||||
}
|
||||
]
|
||||
|
|
@ -65,14 +65,17 @@ def main(sys_args):
|
|||
|
||||
otps = extract_otps(args)
|
||||
write_csv(args, otps)
|
||||
write_keepass_csv(args, otps)
|
||||
write_json(args, otps)
|
||||
|
||||
|
||||
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('--json', '-j', help='export to json file', metavar=('FILE'))
|
||||
arg_parser.add_argument('--csv', '-c', help='export to csv file', metavar=('FILE'))
|
||||
arg_parser.add_argument('--json', '-j', help='export json 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('--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')
|
||||
|
|
@ -102,13 +105,15 @@ def extract_otps(args):
|
|||
j += 1
|
||||
if verbose: print('\n{}. Secret Key'.format(j))
|
||||
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
||||
otp_type = get_enum_name_by_number(raw_otp, 'type')
|
||||
otp_type_enum = get_enum_name_by_number(raw_otp, 'type')
|
||||
otp_type = get_otp_type_str_from_code(raw_otp.type)
|
||||
otp_url = build_otp_url(secret, raw_otp)
|
||||
otp = {
|
||||
"name": raw_otp.name,
|
||||
"secret": secret,
|
||||
"issuer": raw_otp.issuer,
|
||||
"type": otp_type,
|
||||
"counter": raw_otp.counter if raw_otp.type == 1 else None,
|
||||
"url": otp_url
|
||||
}
|
||||
if not quiet:
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
def get_otp_type_str_from_code(otp_type):
|
||||
return 'totp' if otp_type == 2 else 'hotp'
|
||||
|
||||
|
||||
def convert_secret_from_bytes_to_base32_str(bytes):
|
||||
return str(base64.b32encode(bytes), 'utf-8').replace('=', '')
|
||||
|
||||
|
|
@ -162,15 +171,17 @@ def build_otp_url(secret, raw_otp):
|
|||
url_params = {'secret': secret}
|
||||
if raw_otp.type == 1: url_params['counter'] = raw_otp.counter
|
||||
if raw_otp.issuer: url_params['issuer'] = raw_otp.issuer
|
||||
otp_url = 'otpauth://{}/{}?'.format('totp' if raw_otp.type == 2 else 'hotp', quote(raw_otp.name)) + urlencode(url_params)
|
||||
otp_url = 'otpauth://{}/{}?'.format(get_otp_type_str_from_code(raw_otp.type), quote(raw_otp.name)) + urlencode(url_params)
|
||||
return otp_url
|
||||
|
||||
|
||||
def print_otp(otp):
|
||||
print('Name: {}'.format(otp['name']))
|
||||
print('Secret: {}'.format(otp['secret']))
|
||||
if otp['issuer']: print('Issuer: {}'.format(otp['issuer']))
|
||||
print('Type: {}'.format(otp['type']))
|
||||
print('Name: {}'.format(otp['name']))
|
||||
print('Secret: {}'.format(otp['secret']))
|
||||
if otp['issuer']: print('Issuer: {}'.format(otp['issuer']))
|
||||
print('Type: {}'.format(otp['type']))
|
||||
if otp['type'] == 'hotp':
|
||||
print('Counter: {}'.format(otp['counter']))
|
||||
if verbose:
|
||||
print(otp['url'])
|
||||
|
||||
|
|
@ -209,7 +220,48 @@ def write_csv(args, otps):
|
|||
writer = csv.DictWriter(outfile, otps[0].keys())
|
||||
writer.writeheader()
|
||||
writer.writerows(otps)
|
||||
if not quiet: print("Exported {} otps to csv".format(len(otps)))
|
||||
if not quiet: print("Exported {} otps to csv {}".format(len(otps), args.csv))
|
||||
|
||||
|
||||
def write_keepass_csv(args, otps):
|
||||
global verbose, quiet
|
||||
if args.keepass and len(otps) > 0:
|
||||
has_totp = has_otp_type(otps, 'totp')
|
||||
has_hotp = has_otp_type(otps, 'hotp')
|
||||
otp_filename_totp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "totp")
|
||||
otp_filename_hotp = args.keepass if has_totp != has_hotp else add_pre_suffix(args.keepass, "hotp")
|
||||
count_totp_entries = 0
|
||||
count_hotp_entries = 0
|
||||
if has_totp:
|
||||
with open(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):
|
||||
|
|
@ -217,7 +269,20 @@ def write_json(args, otps):
|
|||
if args.json:
|
||||
with open(args.json, "w") as outfile:
|
||||
json.dump(otps, outfile, indent=4)
|
||||
if not quiet: print("Exported {} otp entries to json".format(len(otps)))
|
||||
if 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__':
|
||||
|
|
|
|||
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
|
||||
|
|
@ -18,10 +18,10 @@ batch_id: -1320898453
|
|||
|
||||
|
||||
1. Secret Key
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
|
|
@ -42,9 +42,9 @@ batch_id: -2094403140
|
|||
|
||||
|
||||
2. Secret Key
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
|
|
@ -74,16 +74,41 @@ batch_id: -1822886384
|
|||
|
||||
|
||||
3. Secret Key
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
|
||||
|
||||
4. Secret Key
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi
|
||||
|
||||
|
||||
# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4
|
||||
otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D
|
||||
|
||||
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
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
|
||||
█▀▀▀▀▀█ ▄▀▄▄█ █▀ ▀▀▀▀▀█ ▄▄ █▀▀▀▀▀█
|
||||
|
|
@ -26,9 +26,9 @@ Type: OTP_TOTP
|
|||
|
||||
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
|
||||
█▀▀▀▀▀█ ▀▀██ █▄▀█ ▀▄▄▀█▀▄ █▀▀▀▀▀█
|
||||
|
|
@ -51,9 +51,9 @@ Type: OTP_TOTP
|
|||
|
||||
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
|
||||
█▀▀▀▀▀█ ▀▀██ █▄▀█ ▀▄▄▀█▀▄ █▀▀▀▀▀█
|
||||
|
|
@ -76,10 +76,10 @@ Type: OTP_TOTP
|
|||
|
||||
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
|
||||
█▀▀▀▀▀█ ▄▀▄▄█ █▀ ▀▀▀▀▀█ ▄▄ █▀▀▀▀▀█
|
||||
|
|
@ -104,3 +104,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
|
||||
# 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 pytest import raises
|
||||
|
||||
|
|
@ -47,6 +47,58 @@ def test_extract_csv(capsys):
|
|||
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):
|
||||
# Arrange
|
||||
cleanup()
|
||||
|
|
@ -76,23 +128,28 @@ def test_extract_stdout(capsys):
|
|||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_stdout = '''Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
expected_stdout = '''Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
|
||||
'''
|
||||
|
||||
|
|
@ -107,25 +164,25 @@ def test_extract_not_encoded_plus(capsys):
|
|||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_stdout = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
||||
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
expected_stdout = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
||||
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test2@serenitylabs.co.uk
|
||||
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
Name: SerenityLabs:test2@serenitylabs.co.uk
|
||||
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test3@serenitylabs.co.uk
|
||||
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
Name: SerenityLabs:test3@serenitylabs.co.uk
|
||||
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test4@serenitylabs.co.uk
|
||||
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
Name: SerenityLabs:test4@serenitylabs.co.uk
|
||||
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
'''
|
||||
|
||||
|
|
@ -209,6 +266,7 @@ def test_extract_help(capsys):
|
|||
assert pytest_wrapped_e.type == SystemExit
|
||||
assert pytest_wrapped_e.value.code == 0
|
||||
|
||||
|
||||
def test_verbose_and_quiet(capsys):
|
||||
with raises(SystemExit) as pytest_wrapped_e:
|
||||
# Act
|
||||
|
|
@ -220,7 +278,14 @@ def test_verbose_and_quiet(capsys):
|
|||
assert len(captured.out) > 0
|
||||
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():
|
||||
remove_file('test_example_output.csv')
|
||||
remove_file('test_example_output.json')
|
||||
remove_files('test_example_*.csv')
|
||||
remove_files('test_example_*.json')
|
||||
remove_dir_with_files('testout/')
|
||||
|
|
|
|||
|
|
@ -50,23 +50,28 @@ class TestExtract(unittest.TestCase):
|
|||
extract_otp_secret_keys.main(['example_export.txt'])
|
||||
|
||||
expected_output = [
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Issuer: raspberrypi',
|
||||
'Type: OTP_TOTP',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Issuer: raspberrypi',
|
||||
'Type: totp',
|
||||
'',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: OTP_TOTP',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: totp',
|
||||
'',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: OTP_TOTP',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: totp',
|
||||
'',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Issuer: raspberrypi',
|
||||
'Type: OTP_TOTP',
|
||||
'Name: pi@raspberrypi',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Issuer: raspberrypi',
|
||||
'Type: totp',
|
||||
'',
|
||||
'Name: hotp demo',
|
||||
'Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY',
|
||||
'Type: hotp',
|
||||
'Counter: 4',
|
||||
''
|
||||
]
|
||||
self.assertEqual(output, expected_output)
|
||||
|
|
@ -78,23 +83,28 @@ class TestExtract(unittest.TestCase):
|
|||
extract_otp_secret_keys.main(['example_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = '''Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
expected_output = '''Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: OTP_TOTP
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
|
||||
'''
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
|
@ -105,25 +115,25 @@ Type: OTP_TOTP
|
|||
extract_otp_secret_keys.main(['test/test_plus_problem_export.txt'])
|
||||
actual_output = out.getvalue()
|
||||
|
||||
expected_output = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
||||
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
expected_output = '''Name: SerenityLabs:test1@serenitylabs.co.uk
|
||||
Secret: A4RFDYMF4GSLUIBQV4ZP67OJEZ2XUQVM
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test2@serenitylabs.co.uk
|
||||
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
Name: SerenityLabs:test2@serenitylabs.co.uk
|
||||
Secret: SCDDZ7PW5MOZLE3PQCAZM7L4S35K3UDX
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test3@serenitylabs.co.uk
|
||||
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
Name: SerenityLabs:test3@serenitylabs.co.uk
|
||||
Secret: TR76272RVYO6EAEY2FX7W7R7KUDEGPJ4
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
Name: SerenityLabs:test4@serenitylabs.co.uk
|
||||
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
||||
Issuer: SerenityLabs
|
||||
Type: OTP_TOTP
|
||||
Name: SerenityLabs:test4@serenitylabs.co.uk
|
||||
Secret: N2ILWSXSJUQUB7S6NONPJSC62NPG7EXN
|
||||
Issuer: SerenityLabs
|
||||
Type: totp
|
||||
|
||||
'''
|
||||
self.assertEqual(actual_output, expected_output)
|
||||
|
|
|
|||
12
utils.py
12
utils.py
|
|
@ -19,6 +19,7 @@ import os
|
|||
import shutil
|
||||
from io import StringIO
|
||||
import sys
|
||||
import glob
|
||||
|
||||
|
||||
# Ref. https://stackoverflow.com/a/16571630
|
||||
|
|
@ -39,8 +40,17 @@ with Capturing() as output:
|
|||
sys.stdout = self._stdout
|
||||
|
||||
|
||||
def file_exits(file):
|
||||
return os.path.isfile(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):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue