mirror of
https://github.com/scito/extract_otp_secret_keys.git
synced 2025-12-11 09:06:38 +01:00
add ignore duplicate entries option and add quiet test; fixed -k stdout
This commit is contained in:
parent
722009b172
commit
b86c4f9a61
4 changed files with 102 additions and 27 deletions
5
build.sh
5
build.sh
|
|
@ -385,12 +385,13 @@ if $run_gui; then
|
|||
eval "$cmd"
|
||||
fi
|
||||
|
||||
echo -e "\n${blueBold}#### Results ####${reset}"
|
||||
line=$(printf '#%.0s' $(eval echo {1..$(( ($COLUMNS - 10) / 2))}))
|
||||
echo -e "\n${blueBold}$line RESULTS $line${reset}"
|
||||
|
||||
cmd="cat $TYPE_CHECK_OUT_FILE $LINT_OUT_FILE $COVERAGE_OUT_FILE"
|
||||
if $interactive ; then askContinueYn "$cmd"; else echo -e "${cyan}$cmd${reset}";fi
|
||||
eval "$cmd"
|
||||
|
||||
echo -e "\n${greenBold}Sucessful${reset}"
|
||||
echo -e "\n${greenBold}SUCCESS${reset}"
|
||||
|
||||
quit
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ def main(sys_args: list[str]) -> None:
|
|||
|
||||
def parse_args(sys_args: list[str]) -> Args:
|
||||
global verbose, quiet, colored
|
||||
description_text = "Extracts one time password (OTP) secrets from export QR codes from two-factor authentication (2FA) apps"
|
||||
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
|
||||
if qreader_available:
|
||||
description_text += "\nIf no infiles are provided, a GUI window starts and QR codes are captured from the camera."
|
||||
example_text = """examples:
|
||||
|
|
@ -186,18 +186,19 @@ python extract_otp_secrets.py = < example_export.png"""
|
|||
epilog=example_text)
|
||||
arg_parser.add_argument('infile', help="""a) file or - for stdin with 'otpauth-migration://...' URLs separated by newlines, lines starting with # are ignored;
|
||||
b) image file containing a QR code or = for stdin for an image containing a QR code""", nargs='*' if qreader_available else '+')
|
||||
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'))
|
||||
arg_parser.add_argument('--json', '-j', help='export json file or - 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('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
|
||||
if qreader_available:
|
||||
arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, metavar=('NUMBER'))
|
||||
arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.ZBAR.name})', type=str, choices=[mode.name for mode in QRMode], default=QRMode.ZBAR.name)
|
||||
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'))
|
||||
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('-i', '--ignore', help='ignore duplicate otps', action='store_true')
|
||||
arg_parser.add_argument('--no-color', '-n', help='do not use ANSI colors in console output', action='store_true')
|
||||
output_group = arg_parser.add_mutually_exclusive_group()
|
||||
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')
|
||||
output_group.add_argument('-v', '--verbose', help='verbose output', action='count')
|
||||
output_group.add_argument('-q', '--quiet', help='no stdout output, except output set by -', action='store_true')
|
||||
args = arg_parser.parse_args(sys_args)
|
||||
if args.csv == '-' or args.json == '-' or args.keepass == '-':
|
||||
args.quiet = args.q = True
|
||||
|
|
@ -421,7 +422,6 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count:
|
|||
new_otps_count = 0
|
||||
# pylint: disable=no-member
|
||||
for raw_otp in payload.otp_parameters:
|
||||
new_otps_count += 1
|
||||
if verbose: print(f"\n{len(otps) + 1}. Secret")
|
||||
secret = convert_secret_from_bytes_to_base32_str(raw_otp.secret)
|
||||
if verbose >= LogLevel.DEBUG: log_debug('OTP enum type:', get_enum_name_by_number(raw_otp, 'type'))
|
||||
|
|
@ -435,7 +435,9 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count:
|
|||
"counter": raw_otp.counter if raw_otp.type == 1 else None,
|
||||
"url": otp_url
|
||||
}
|
||||
if otp not in otps or not args.ignore:
|
||||
otps.append(otp)
|
||||
new_otps_count += 1
|
||||
if not quiet:
|
||||
print_otp(otp)
|
||||
if args.printqr:
|
||||
|
|
@ -444,6 +446,8 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count:
|
|||
save_qr(otp, args, len(otps))
|
||||
if not quiet:
|
||||
print()
|
||||
elif args.ignore and not quiet:
|
||||
eprint(f"Ignored duplicate otp: {otp['name']}", f" / {otp['issuer']}\n" if otp['issuer'] else '\n', sep='')
|
||||
|
||||
return new_otps_count
|
||||
|
||||
|
|
@ -613,8 +617,11 @@ def write_keepass_csv(args: Args, otps: Otps) -> None:
|
|||
if args.keepass and len(otps) > 0:
|
||||
has_totp = has_otp_type(otps, 'totp')
|
||||
has_hotp = has_otp_type(otps, 'hotp')
|
||||
if args.keepass != '-':
|
||||
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")
|
||||
else:
|
||||
otp_filename_totp = otp_filename_hotp = '-'
|
||||
if has_totp:
|
||||
count_totp_entries = write_keepass_totp_csv(otp_filename_totp, otps)
|
||||
if has_hotp:
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import pathlib
|
|||
import re
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import colorama
|
||||
import pytest
|
||||
|
|
@ -354,6 +355,43 @@ def test_extract_saveqr(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Pa
|
|||
assert count_files_in_dir(tmp_path) == 6
|
||||
|
||||
|
||||
def test_extract_ignored_duplicates(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
# Act
|
||||
extract_otp_secrets.main(['-i', 'example_export.txt'])
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
expected_stdout = '''Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Issuer: raspberrypi
|
||||
Type: totp
|
||||
|
||||
Name: pi@raspberrypi
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
Name: hotp demo
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: hotp
|
||||
Counter: 4
|
||||
|
||||
Name: encoding: ¿äÄéÉ? (demo)
|
||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||
Type: totp
|
||||
|
||||
'''
|
||||
|
||||
expected_stderr = '''Ignored duplicate otp: pi@raspberrypi
|
||||
|
||||
Ignored duplicate otp: pi@raspberrypi / raspberrypi
|
||||
|
||||
'''
|
||||
|
||||
assert captured.out == expected_stdout
|
||||
assert captured.err == expected_stderr
|
||||
|
||||
|
||||
def test_normalize_bytes() -> None:
|
||||
assert replace_escaped_octal_utf8_bytes_with_str(
|
||||
'Before\\\\302\\\\277\\\\303\nname: enc: \\302\\277\\303\\244\\303\\204\\303\\251\\303\\211?\nAfter') == 'Before\\\\302\\\\277\\\\303\nname: enc: ¿äÄéÉ?\nAfter'
|
||||
|
|
@ -421,7 +459,7 @@ def test_extract_help(capsys: pytest.CaptureFixture[str]) -> None:
|
|||
captured = capsys.readouterr()
|
||||
|
||||
assert len(captured.out) > 0
|
||||
assert "-h, --help" in captured.out and "--verbose, -v" in captured.out
|
||||
assert "-h, --help" in captured.out and "-v, --verbose" in captured.out
|
||||
assert captured.err == ''
|
||||
assert e.type == SystemExit
|
||||
assert e.value.code == 0
|
||||
|
|
@ -469,12 +507,41 @@ def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
|
|||
captured = capsys.readouterr()
|
||||
|
||||
assert len(captured.err) > 0
|
||||
assert 'error: argument --quiet/-q: not allowed with argument --verbose/-v' in captured.err
|
||||
assert 'error: argument -q/--quiet: not allowed with argument -v/--verbose' in captured.err
|
||||
assert captured.out == ''
|
||||
assert e.value.code == 2
|
||||
assert e.type == SystemExit
|
||||
|
||||
|
||||
@pytest.mark.parametrize("parameter,parameter_value,stdout_expected,stderr_expected", [
|
||||
('-c', 'outfile', False, False),
|
||||
('-c', '-', True, False),
|
||||
('-k', 'outfile', False, False),
|
||||
('-k', '-', True, False),
|
||||
('-j', 'outfile', False, False),
|
||||
('-s', 'outfile', False, False),
|
||||
('-j', '-', True, False),
|
||||
('-i', None, False, False),
|
||||
('-p', None, True, False),
|
||||
('-Q', 'CV2', False, False),
|
||||
('-n', None, False, False),
|
||||
])
|
||||
def test_quiet(parameter: str, parameter_value: Optional[str], stdout_expected: bool, stderr_expected: bool, capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
|
||||
args = ['-q', 'example_export.txt', 'example_export.png', parameter]
|
||||
if parameter_value == 'outfile':
|
||||
args.append(str(tmp_path / parameter_value))
|
||||
elif parameter_value:
|
||||
args.append(parameter_value)
|
||||
# Act
|
||||
extract_otp_secrets.main(args)
|
||||
|
||||
# Assert
|
||||
captured = capsys.readouterr()
|
||||
|
||||
assert (captured.out == '' and not stdout_expected) or (len(captured.out) > 0 and stdout_expected)
|
||||
assert (captured.err == '' and not stderr_expected) or (len(captured.err) > 0 and stderr_expected)
|
||||
|
||||
|
||||
def test_wrong_data(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
with pytest.raises(SystemExit) as e:
|
||||
# Act
|
||||
|
|
|
|||
|
|
@ -198,7 +198,7 @@ Type: totp
|
|||
actual_output = out.getvalue()
|
||||
|
||||
self.assertGreater(len(actual_output), 0)
|
||||
self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output)
|
||||
self.assertTrue("-h, --help" in actual_output and "-v, --verbose" in actual_output)
|
||||
|
||||
def test_extract_help_2(self) -> None:
|
||||
out = io.StringIO()
|
||||
|
|
@ -209,7 +209,7 @@ Type: totp
|
|||
actual_output = out.getvalue()
|
||||
|
||||
self.assertGreater(len(actual_output), 0)
|
||||
self.assertTrue("-h, --help" in actual_output and "--verbose, -v" in actual_output)
|
||||
self.assertTrue("-h, --help" in actual_output and "-v, --verbose" in actual_output)
|
||||
self.assertEqual(context.exception.code, 0)
|
||||
|
||||
def test_extract_help_3(self) -> None:
|
||||
|
|
@ -218,7 +218,7 @@ Type: totp
|
|||
extract_otp_secrets.main(['-h'])
|
||||
|
||||
self.assertGreater(len(actual_output), 0)
|
||||
self.assertTrue("-h, --help" in "\n".join(actual_output) and "--verbose, -v" in "\n".join(actual_output))
|
||||
self.assertTrue("-h, --help" in "\n".join(actual_output) and "-v, --verbose" in "\n".join(actual_output))
|
||||
self.assertEqual(context.exception.code, 0)
|
||||
|
||||
def setUp(self) -> None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue