diff --git a/README.md b/README.md index a1bcf58..37068d9 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ cd extract_otp_secret_keys ## Program help: arguments and options -
usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv FILE] [--keepass FILE] [--printqr] [--saveqr DIR] [--verbose | --quiet] infile
+usage: extract_otp_secret_keys.py [-h] [--json FILE] [--csv FILE] [--keepass FILE] [--printqr] [--saveqr DIR] [--verbose | --quiet] infile [infile ...]
positional arguments:
infile 1) file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored; 2) image file containing a QR code or = for stdin for an image containing a QR code
diff --git a/extract_otp_secret_keys.py b/extract_otp_secret_keys.py
index 5dfa313..02d62be 100644
--- a/extract_otp_secret_keys.py
+++ b/extract_otp_secret_keys.py
@@ -79,7 +79,7 @@ def main(sys_args):
def parse_args(sys_args):
formatter = lambda prog: argparse.HelpFormatter(prog, max_help_position=52)
arg_parser = argparse.ArgumentParser(formatter_class=formatter)
- arg_parser.add_argument('infile', help='1) file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored; 2) image file containing a QR code or = for stdin for an image containing a QR code')
+ arg_parser.add_argument('infile', help='1) file or - for stdin with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored; 2) image file containing a QR code or = for stdin for an image containing a QR code', nargs='+')
arg_parser.add_argument('--json', '-j', help='export json file or - for stdout', metavar=('FILE'))
arg_parser.add_argument('--csv', '-c', help='export csv file or - for stdout', metavar=('FILE'))
arg_parser.add_argument('--keepass', '-k', help='export totp/hotp csv file(s) for KeePass, - for stdout', metavar=('FILE'))
@@ -101,39 +101,39 @@ def extract_otps(args):
otps = []
i = j = 0
+ for infile in args.infile:
+ for line in get_lines_from_file(infile):
+ if verbose: print(line)
+ if line.startswith('#') or line == '': continue
+ i += 1
+ payload = get_payload_from_line(line, i, infile)
- for line in get_lines_from_file(args.infile):
- if verbose: print(line)
- if line.startswith('#') or line == '': continue
- i += 1
- payload = get_payload_from_line(line, i, args)
+ # pylint: disable=no-member
+ for raw_otp in payload.otp_parameters:
+ j += 1
+ if verbose: print('\n{}. Secret Key'.format(j))
+ secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
+ otp_type_enum = get_enum_name_by_number(raw_otp, 'type')
+ otp_type = get_otp_type_str_from_code(raw_otp.type)
+ otp_url = build_otp_url(secret, raw_otp)
+ otp = {
+ "name": raw_otp.name,
+ "secret": secret,
+ "issuer": raw_otp.issuer,
+ "type": otp_type,
+ "counter": raw_otp.counter if raw_otp.type == 1 else None,
+ "url": otp_url
+ }
+ if not quiet:
+ print_otp(otp)
+ if args.printqr:
+ print_qr(args, otp_url)
+ if args.saveqr:
+ save_qr(otp, args, j)
+ if not quiet:
+ print()
- # pylint: disable=no-member
- for raw_otp in payload.otp_parameters:
- j += 1
- if verbose: print('\n{}. Secret Key'.format(j))
- secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
- otp_type_enum = get_enum_name_by_number(raw_otp, 'type')
- otp_type = get_otp_type_str_from_code(raw_otp.type)
- otp_url = build_otp_url(secret, raw_otp)
- otp = {
- "name": raw_otp.name,
- "secret": secret,
- "issuer": raw_otp.issuer,
- "type": otp_type,
- "counter": raw_otp.counter if raw_otp.type == 1 else None,
- "url": otp_url
- }
- if not quiet:
- print_otp(otp)
- if args.printqr:
- print_qr(args, otp_url)
- if args.saveqr:
- save_qr(otp, args, j)
- if not quiet:
- print()
-
- otps.append(otp)
+ otps.append(otp)
return otps
@@ -209,10 +209,10 @@ def convert_img_to_line(filename):
abort('\nERROR: Encountered exception "{}".\ninput file: {}'.format(str(e), filename))
-def get_payload_from_line(line, i, args):
+def get_payload_from_line(line, i, infile):
global verbose
if not line.startswith('otpauth-migration://'):
- eprint( '\nWARN: line is not a otpauth-migration:// URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format( args.infile, line))
+ eprint( '\nWARN: line is not a otpauth-migration:// URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(infile, line))
parsed_url = urlparse(line)
if verbose > 1: print('\nDEBUG: parsed_url={}'.format(parsed_url))
try:
@@ -221,7 +221,7 @@ def get_payload_from_line(line, i, args):
params = []
if verbose > 1: print('\nDEBUG: querystring params={}'.format(params))
if 'data' not in params:
- abort('\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format( args.infile, line))
+ abort('\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(infile, line))
data_base64 = params['data'][0]
if verbose > 1: print('\nDEBUG: data_base64={}'.format(data_base64))
data_base64_fixed = data_base64.replace(' ', '+')
diff --git a/test_extract_otp_secret_keys_pytest.py b/test_extract_otp_secret_keys_pytest.py
index 8d0e561..58a9ec8 100644
--- a/test_extract_otp_secret_keys_pytest.py
+++ b/test_extract_otp_secret_keys_pytest.py
@@ -38,6 +38,21 @@ def test_extract_stdout(capsys):
assert captured.err == ''
+def test_extract_multiple_files_and_mixed(capsys):
+ # Act
+ extract_otp_secret_keys.main([
+ 'example_export.txt',
+ 'test/test_googleauth_export.png',
+ 'example_export.txt',
+ 'test/test_googleauth_export.png'])
+
+ # Assert
+ captured = capsys.readouterr()
+
+ assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT + EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG + EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT + EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG
+ assert captured.err == ''
+
+
def test_extract_non_existent_file(capsys):
# Act
with raises(SystemExit) as e:
@@ -498,25 +513,7 @@ def test_img_qr_reader_from_file_happy_path(capsys):
# Assert
captured = capsys.readouterr()
- expected_stdout =\
-'''Name: Test1:test1@example1.com
-Secret: JBSWY3DPEHPK3PXP
-Issuer: Test1
-Type: totp
-
-Name: Test2:test2@example2.com
-Secret: JBSWY3DPEHPK3PXQ
-Issuer: Test2
-Type: totp
-
-Name: Test3:test3@example3.com
-Secret: JBSWY3DPEHPK3PXR
-Issuer: Test3
-Type: totp
-
-'''
-
- assert captured.out == expected_stdout
+ assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG
assert captured.err == ''
@@ -664,3 +661,21 @@ Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
Type: totp
'''
+
+EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG =\
+'''Name: Test1:test1@example1.com
+Secret: JBSWY3DPEHPK3PXP
+Issuer: Test1
+Type: totp
+
+Name: Test2:test2@example2.com
+Secret: JBSWY3DPEHPK3PXQ
+Issuer: Test2
+Type: totp
+
+Name: Test3:test3@example3.com
+Secret: JBSWY3DPEHPK3PXR
+Issuer: Test3
+Type: totp
+
+'''