diff --git a/src/extract_otp_secrets.py b/src/extract_otp_secrets.py index 400281d..95434c0 100644 --- a/src/extract_otp_secrets.py +++ b/src/extract_otp_secrets.py @@ -54,7 +54,7 @@ import os import re import sys import urllib.parse as urlparse -from enum import Enum +from enum import Enum, IntEnum from typing import Any, List, Optional, TextIO, Tuple, Union # workaround for PYTHON <= 3.7: compatibility @@ -90,16 +90,26 @@ Exception: {e}""") # CV2 camera capture constants FONT: Final[int] = cv2.FONT_HERSHEY_PLAIN - FONT_SCALE: Final[int] = 1 + FONT_SCALE: Final[float] = 1.3 FONT_THICKNESS: Final[int] = 1 FONT_LINE_STYLE: Final[int] = cv2.LINE_AA + FONT_COLOR: Final[ColorBGR] = (255, 0, 0) BOX_THICKNESS: Final[int] = 5 # workaround for PYTHON <= 3.7: must use () for assignments - START_POS_TEXT: Final[Point] = (5, 20) + WINDOW_X: Final[int] = 0 + WINDOW_Y: Final[int] = 1 + WINDOW_WIDTH: Final[int] = 2 + WINDOW_HEIGHT: Final[int] = 3 + TEXT_WIDTH: Final[int] = 0 + TEXT_HEIGHT: Final[int] = 1 + BORDER: Final[int] = 5 + START_Y: Final[int] = 20 + START_POS_TEXT: Final[Point] = (BORDER, START_Y) NORMAL_COLOR: Final[ColorBGR] = (255, 0, 255) SUCCESS_COLOR: Final[ColorBGR] = (0, 255, 0) FAILURE_COLOR: Final[ColorBGR] = (0, 0, 255) - FONT_DY: Final[int] = cv2.getTextSize("M", FONT, FONT_SCALE, FONT_THICKNESS)[0][1] + 5 + CHAR_DX: Final[int] = (lambda text: cv2.getTextSize(text, FONT, FONT_SCALE, FONT_THICKNESS)[0][TEXT_WIDTH] // len(text))("28 QR codes capturedMMM") + FONT_DY: Final[int] = cv2.getTextSize("M", FONT, FONT_SCALE, FONT_THICKNESS)[0][TEXT_HEIGHT] + 5 WINDOW_NAME: Final[str] = "Extract OTP Secrets: Capture QR Codes from Camera" TextPosition = Enum('TextPosition', ['LEFT', 'RIGHT']) @@ -121,13 +131,14 @@ Otps = List[Otp] OtpUrls = List[OtpUrl] QRMode = Enum('QRMode', ['ZBAR', 'QREADER', 'QREADER_DEEP', 'CV2', 'CV2_WECHAT'], start=0) +LogLevel = IntEnum('LogLevel', ['QUIET', 'NORMAL', 'VERBOSE', 'MORE_VERBOSE', 'DEBUG'], start=-1) # Constants CAMERA: Final[str] = 'camera' # Global variable declaration -verbose: int = 0 +verbose: IntEnum = LogLevel.NORMAL quiet: bool = False colored: bool = True @@ -160,9 +171,9 @@ 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) / two-factor authentication (2FA) secrets from export QR codes, e.g. from Google Authenticator app." + description_text = "Extracts one time password (OTP) secrets from export QR codes from two-factor authentication (2FA) apps" if qreader_available: - description_text += "\nIf no infiles are provided, the QR codes a GUI window starts and QR codes can interactively be captured from the system camera." + description_text += "\nIf no infiles are provided, a GUI window starts and QR codes are captured from the camera." example_text = """examples: python extract_otp_secrets.py python extract_otp_secrets.py example_*.txt @@ -191,12 +202,12 @@ b) image file containing a QR code or = for stdin for an image containing a QR c if args.csv == '-' or args.json == '-' or args.keepass == '-': args.quiet = args.q = True - verbose = args.verbose if args.verbose else 0 + verbose = args.verbose if args.verbose else LogLevel.NORMAL quiet = True if args.quiet else False colored = not args.no_color if verbose: print(f"QReader installed: {qreader_available}") if qreader_available: - if verbose > 1: print(f"CV2 version: {cv2.__version__}") + if verbose >= LogLevel.VERBOSE: print(f"CV2 version: {cv2.__version__}") if verbose: print(f"QR reading mode: {args.qr}\n") return args @@ -228,14 +239,17 @@ def cv2_draw_box(img: Any, raw_pts: Any, color: ColorBGR) -> Any: # TODO use cv2 types if available -def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition, color: ColorBGR) -> None: +def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition, color: ColorBGR, opposite_len: Optional[int] = None) -> None: + text_dim, _ = cv2.getTextSize(text, FONT, FONT_SCALE, FONT_THICKNESS) + window_dim = cv2.getWindowImageRect(WINDOW_NAME) + out_text = text if not opposite_len or (actual_width := text_dim[TEXT_WIDTH] + opposite_len * CHAR_DX + 4 * BORDER) <= window_dim[WINDOW_WIDTH] else text[:(window_dim[WINDOW_WIDTH] - actual_width) // CHAR_DX] + '.' + text_dim, _ = cv2.getTextSize(out_text, FONT, FONT_SCALE, FONT_THICKNESS) if position == TextPosition.LEFT: - pos = START_POS_TEXT[0], START_POS_TEXT[1] + line_number * FONT_DY + pos = BORDER, START_Y + line_number * FONT_DY else: - window_dim = cv2.getWindowImageRect(WINDOW_NAME) - pos = window_dim[2] - cv2.getTextSize(text, FONT, FONT_SCALE, FONT_THICKNESS)[0][0] - 5, START_POS_TEXT[1] + line_number * FONT_DY + pos = window_dim[WINDOW_WIDTH] - text_dim[TEXT_WIDTH] - BORDER, START_Y + line_number * FONT_DY - cv2.putText(img, text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE) + cv2.putText(img, out_text, pos, FONT, FONT_SCALE, color, FONT_THICKNESS, FONT_LINE_STYLE) def extract_otps_from_camera(args: Args) -> Otps: @@ -289,23 +303,16 @@ def extract_otps_from_camera(args: Args) -> Otps: qr_mode = next_qr_mode(qr_mode) continue - cv2_print_text(img, f"Mode: {qr_mode.name} (Hit space to change)", 0, TextPosition.LEFT, NORMAL_COLOR) - cv2_print_text(img, "Hit ESC to quit", 1, TextPosition.LEFT, NORMAL_COLOR) + cv2_print_text(img, f"Mode: {qr_mode.name} (Hit space to change)", 0, TextPosition.LEFT, FONT_COLOR, 20) + cv2_print_text(img, "Hit ESC to quit", 1, TextPosition.LEFT, FONT_COLOR, 17) - cv2_print_text(img, f"{len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} captured", 0, TextPosition.RIGHT, NORMAL_COLOR) - cv2_print_text(img, f"{len(otps)} otp{'s'[:len(otps) != 1]} extracted", 1, TextPosition.RIGHT, NORMAL_COLOR) + cv2_print_text(img, f"{len(otp_urls)} QR code{'s'[:len(otp_urls) != 1]} captured", 0, TextPosition.RIGHT, FONT_COLOR) + cv2_print_text(img, f"{len(otps)} otp{'s'[:len(otps) != 1]} extracted", 1, TextPosition.RIGHT, FONT_COLOR) cv2.imshow(WINDOW_NAME, img) - key = cv2.waitKey(1) & 0xFF - if key == 27 or key == ord('q') or key == 13: - # ESC or Enter or q pressed - break - elif key == 32: - qr_mode = next_qr_mode(qr_mode) - if verbose: print(f"QR reading mode: {qr_mode}") - if cv2.getWindowProperty(WINDOW_NAME, cv2.WND_PROP_VISIBLE) < 1: - # Window close clicked + quit, qr_mode = cv2_handle_pressed_keys(qr_mode) + if quit: break cam.release() @@ -314,9 +321,24 @@ def extract_otps_from_camera(args: Args) -> Otps: return otps +def cv2_handle_pressed_keys(qr_mode: QRMode) -> Tuple[bool, QRMode]: + key = cv2.waitKey(1) & 0xFF + quit = False + if key == 27 or key == ord('q') or key == 13: + # ESC or Enter or q pressed + quit = True + elif key == 32: + qr_mode = next_qr_mode(qr_mode) + if verbose >= LogLevel.MORE_VERBOSE: print(f"QR reading mode: {qr_mode}") + if cv2.getWindowProperty(WINDOW_NAME, cv2.WND_PROP_VISIBLE) < 1: + # Window close clicked + quit = True + return quit, qr_mode + + def extract_otps_from_otp_url(otp_url: str, otp_urls: OtpUrls, otps: Otps, args: Args) -> int: '''Returns -1 if opt_url was already added.''' - if otp_url and verbose: print(otp_url) + if otp_url and verbose >= LogLevel.VERBOSE: print(otp_url) if not otp_url: return 0 if otp_url not in otp_urls: @@ -334,14 +356,14 @@ def extract_otps_from_files(args: Args) -> Otps: files_count = urls_count = otps_count = 0 if verbose: print(f"Input files: {args.infile}") for infile in args.infile: - if verbose: print(f"Processing infile {infile}") + if verbose >= LogLevel.MORE_VERBOSE: log_verbose(f"Processing infile {infile}") files_count += 1 for line in get_otp_urls_from_file(infile, args): - if verbose: print(line) + if verbose >= LogLevel.MORE_VERBOSE: log_verbose(line) if line.startswith('#') or line == '': continue urls_count += 1 otps_count += extract_otp_from_otp_url(line, otps, urls_count, infile, args) - if verbose: print(f"{files_count} infile{'s'[:files_count != 1]} processed") + if verbose: print(f"Extracted {otps_count} otp{'s'[:otps_count != 1]} from {urls_count} otp url{'s'[:urls_count != 1]} by reading {files_count} infile{'s'[:files_count != 1]}") return otps @@ -355,13 +377,13 @@ def get_otp_urls_from_file(filename: str, args: Args) -> OtpUrls: # could not process text file, try reading as image if filename != '-' and qreader_available: - return convert_img_to_otp_url(filename, args) + return convert_img_to_otp_urls(filename, args) return [] def read_lines_from_text_file(filename: str) -> list[str]: - if verbose: print(f"Reading lines of {filename}") + if verbose >= LogLevel.DEBUG: print(f"Reading lines of {filename}") # workaround for PYTHON <= 3.9 support encoding if sys.version_info >= (3, 10): finput = fileinput.input(filename, encoding='utf-8') @@ -370,7 +392,7 @@ def read_lines_from_text_file(filename: str) -> list[str]: try: lines = [] for line in (line.strip() for line in finput): - if verbose: print(line) + if verbose >= LogLevel.DEBUG: log_verbose(line) if is_binary(line): abort("Binary input was given in stdin, please use = instead of - as infile argument for images.") # unfortunately yield line leads to random test fails @@ -400,7 +422,7 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: 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: print('OTP enum type:', get_enum_name_by_number(raw_otp, 'type')) + if verbose >= LogLevel.DEBUG: log_debug('OTP enum type:', 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: Otp = { @@ -424,7 +446,7 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count: return new_otps_count -def convert_img_to_otp_url(filename: str, args: Args) -> OtpUrls: +def convert_img_to_otp_urls(filename: str, args: Args) -> OtpUrls: if verbose: print(f"Reading image {filename}") try: if filename != '=': @@ -449,22 +471,7 @@ def convert_img_to_otp_url(filename: str, args: Args) -> OtpUrls: abort(f"Unable to open file for reading.\ninput file: {filename}") qr_mode = QRMode[args.qr] - otp_urls: OtpUrls = [] - if qr_mode in [QRMode.QREADER, QRMode.QREADER_DEEP]: - otp_url = QReader().detect_and_decode(img, qr_mode == QRMode.QREADER_DEEP) - otp_urls.append(otp_url) - elif qr_mode == QRMode.CV2: - otp_url, _, _ = cv2.QRCodeDetector().detectAndDecode(img) - otp_urls.append(otp_url) - elif qr_mode == QRMode.CV2_WECHAT: - otp_url, _ = cv2.wechat_qrcode.WeChatQRCode().detectAndDecode(img) - otp_urls += list(otp_url) - elif qr_mode == QRMode.ZBAR: - qrcodes = zbar.decode(img) - otp_urls += [qrcode.data.decode('utf-8') for qrcode in qrcodes] - else: - assert False, f"Wrong QReader mode {qr_mode.name}" - + otp_urls = decode_qr_img_otp_urls(img, qr_mode) if len(otp_urls) == 0: abort(f"Unable to read QR Code from file.\ninput file: {filename}") except Exception as e: @@ -472,29 +479,45 @@ def convert_img_to_otp_url(filename: str, args: Args) -> OtpUrls: return otp_urls +def decode_qr_img_otp_urls(img: Any, qr_mode: QRMode) -> OtpUrls: + otp_urls: OtpUrls = [] + if qr_mode in [QRMode.QREADER, QRMode.QREADER_DEEP]: + otp_url = QReader().detect_and_decode(img, qr_mode == QRMode.QREADER_DEEP) + otp_urls.append(otp_url) + elif qr_mode == QRMode.CV2: + otp_url, _, _ = cv2.QRCodeDetector().detectAndDecode(img) + otp_urls.append(otp_url) + elif qr_mode == QRMode.CV2_WECHAT: + otp_url, _ = cv2.wechat_qrcode.WeChatQRCode().detectAndDecode(img) + otp_urls += list(otp_url) + elif qr_mode == QRMode.ZBAR: + qrcodes = zbar.decode(img) + otp_urls += [qrcode.data.decode('utf-8') for qrcode in qrcodes] + else: + assert False, f"Wrong QReader mode {qr_mode.name}" + + return otp_urls + + # workaround for PYTHON <= 3.9 use: pb.MigrationPayload | None def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.MigrationPayload]: - if not otp_url.startswith('otpauth-migration://'): - msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}" - if source == CAMERA: - log_error(f"{msg}") - return None - else: - log_warn(f"{msg}\nMaybe a wrong file was given") + '''Extracts the otp migration payload from an otp url. This function is the core of the this appliation.''' + if not is_opt_url(otp_url, source): + return None parsed_url = urlparse.urlparse(otp_url) - if verbose > 2: print(f"\nDEBUG: parsed_url={parsed_url}") + if verbose >= LogLevel.DEBUG: log_debug(f"parsed_url={parsed_url}") try: params = urlparse.parse_qs(parsed_url.query, strict_parsing=True) except Exception: # workaround for PYTHON <= 3.10 params = {} - if verbose > 2: print(f"\nDEBUG: querystring params={params}") + if verbose >= LogLevel.DEBUG: log_debug(f"querystring params={params}") if 'data' not in params: log_error(f"could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}") return None data_base64 = params['data'][0] - if verbose > 2: print(f"\nDEBUG: data_base64={data_base64}") + if verbose >= LogLevel.DEBUG: log_debug(f"data_base64={data_base64}") data_base64_fixed = data_base64.replace(' ', '+') - if verbose > 2: print(f"\nDEBUG: data_base64_fixed={data_base64_fixed}") + if verbose >= LogLevel.DEBUG: log_debug(f"data_base64_fixed={data_base64_fixed}") data = base64.b64decode(data_base64_fixed, validate=True) payload = pb.MigrationPayload() try: @@ -502,12 +525,22 @@ def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.M except Exception as e: abort(f"Cannot decode otpauth-migration migration payload.\n" f"data={data_base64}", e) - if verbose: - print(f"\n{i}. Payload Line", payload, sep='\n') + if verbose >= LogLevel.DEBUG: log_debug(f"\n{i}. Payload Line", payload, sep='\n') return payload +def is_opt_url(otp_url: str, source: str) -> bool: + if not otp_url.startswith('otpauth-migration://'): + msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}" + if source == CAMERA: + log_warn(f"{msg}") + return False + else: + log_warn(f"{msg}\nMaybe a wrong file was given") + return True + + # https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf def get_enum_name_by_number(parent: Any, field_name: str) -> str: field_value = getattr(parent, field_name) @@ -580,38 +613,48 @@ def write_keepass_csv(args: Args, otps: Otps) -> None: 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_file_or_stdout_for_csv(otp_filename_totp) 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': f"OTP/{otp['type'].upper()}" - }) - count_totp_entries += 1 + count_totp_entries = write_keepass_totp_csv(otp_filename_totp, otps) if has_hotp: - with open_file_or_stdout_for_csv(otp_filename_hotp) as outfile: - writer = csv.DictWriter(outfile, ["Title", "User Name", "HmacOtp-Secret-Base32", "HmacOtp-Counter", "Group"]) - writer.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': f"OTP/{otp['type'].upper()}" - }) - count_hotp_entries += 1 + count_hotp_entries = write_keepass_htop_csv(otp_filename_hotp, otps) if not quiet: - if count_totp_entries > 0: print(f"Exported {count_totp_entries} totp entrie{'s'[:count_totp_entries != 1]} to keepass csv file {otp_filename_totp}") - if count_hotp_entries > 0: print(f"Exported {count_hotp_entries} hotp entrie{'s'[:count_hotp_entries != 1]} to keepass csv file {otp_filename_hotp}") + if count_totp_entries: print(f"Exported {count_totp_entries} totp entrie{'s'[:count_totp_entries != 1]} to keepass csv file {otp_filename_totp}") + if count_hotp_entries: print(f"Exported {count_hotp_entries} hotp entrie{'s'[:count_hotp_entries != 1]} to keepass csv file {otp_filename_hotp}") + + +def write_keepass_totp_csv(otp_filename: str, otps: Otps) -> int: + count_entries = 0 + with open_file_or_stdout_for_csv(otp_filename) 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': f"OTP/{otp['type'].upper()}" + }) + count_entries += 1 + return count_entries + + +def write_keepass_htop_csv(otp_filename: str, otps: Otps) -> int: + count_entries = 0 + with open_file_or_stdout_for_csv(otp_filename) 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': f"OTP/{otp['type'].upper()}" + }) + count_entries += 1 + return count_entries def write_json(args: Args, otps: Otps) -> None: @@ -667,21 +710,38 @@ def next_qr_mode(qr_mode: QRMode) -> QRMode: return QRMode((qr_mode.value + 1) % len(QRMode)) +# workaround for PYTHON <= 3.9 use: BaseException | None +def log_debug(*values: object, sep: Optional[str] = ' ') -> None: + if colored: + print(f"{colorama.Fore.CYAN}\nDEBUG: {str(values[0])}", *values[1:], colorama.Fore.RESET, sep) + else: + print(f"\nDEBUG: {str(values[0])}", *values[1:], sep) + + +# workaround for PYTHON <= 3.9 use: BaseException | None +def log_verbose(msg: str) -> None: + print(color(msg, colorama.Fore.CYAN)) + + # workaround for PYTHON <= 3.9 use: BaseException | None def log_warn(msg: str, exception: Optional[BaseException] = None) -> None: exception_text = "\nException: " - eprint(f"{colorama.Fore.RED if colored else ''}\nWARN: {msg}{(exception_text + str(exception)) if exception else ''}{colorama.Fore.RESET if colored else ''}") + eprint(color(f"\nWARN: {msg}{(exception_text + str(exception)) if exception else ''}", colorama.Fore.RED)) # workaround for PYTHON <= 3.9 use: BaseException | None def log_error(msg: str, exception: Optional[BaseException] = None) -> None: exception_text = "\nException: " - eprint(f"{colorama.Fore.RED if colored else ''}\nERROR: {msg}{(exception_text + str(exception)) if exception else ''}{colorama.Fore.RESET if colored else ''}") + eprint(color(f"\nERROR: {msg}{(exception_text + str(exception)) if exception else ''}", colorama.Fore.RED)) + + +def color(msg: str, color: Optional[str] = None) -> str: + return f"{color if colored and color else ''}{msg}{colorama.Fore.RESET if colored and color else ''}" -def eprint(*args: Any, **kwargs: Any) -> None: +def eprint(*values: object, **kwargs: Any) -> None: '''Print to stderr.''' - print(*args, file=sys.stderr, **kwargs) + print(*values, file=sys.stderr, **kwargs) # workaround for PYTHON <= 3.9 use: BaseException | None diff --git a/tests/data/print_verbose_output-n-v.txt b/tests/data/print_verbose_output-n-v.txt new file mode 100644 index 0000000..e32c422 --- /dev/null +++ b/tests/data/print_verbose_output-n-v.txt @@ -0,0 +1,51 @@ +QReader installed: True +CV2 version: 4.7.0 +QR reading mode: ZBAR + +Input files: ['example_export.txt'] + +1. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi + + +2. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +3. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +4. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi + + +5. Secret +Name: hotp demo +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: hotp +Counter: 4 +otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4 + + +6. Secret +Name: encoding: ¿äÄéÉ? (demo) +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + +Extracted 6 otps from 5 otp urls by reading 1 infile diff --git a/tests/data/print_verbose_output-n-vv.txt b/tests/data/print_verbose_output-n-vv.txt new file mode 100644 index 0000000..544f975 --- /dev/null +++ b/tests/data/print_verbose_output-n-vv.txt @@ -0,0 +1,71 @@ +QReader installed: True +CV2 version: 4.7.0 +QR reading mode: ZBAR + +Input files: ['example_export.txt'] +Processing infile example_export.txt +# 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 + +1. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi + + +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D + +2. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B + +3. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +4. Secret +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 + +5. Secret +Name: hotp demo +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: hotp +Counter: 4 +otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4 + + +# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +# Name: "encoding: ¿äÄéÉ? (demo)" +otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D + +6. Secret +Name: encoding: ¿äÄéÉ? (demo) +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + +Extracted 6 otps from 5 otp urls by reading 1 infile diff --git a/tests/data/print_verbose_output-n-vvv.txt b/tests/data/print_verbose_output-n-vvv.txt new file mode 100644 index 0000000..4013a77 --- /dev/null +++ b/tests/data/print_verbose_output-n-vvv.txt @@ -0,0 +1,223 @@ +QReader installed: True +CV2 version: 4.7.0 +QR reading mode: ZBAR + +Input files: ['example_export.txt'] +Processing infile example_export.txt +Reading lines of example_export.txt +# 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 + +# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4 +otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D + +# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +# Name: "encoding: ¿äÄéÉ? (demo)" +otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D +# 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 + +DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B', fragment='') + +DEBUG: querystring params={'data': ['CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B']} + +DEBUG: data_base64=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B + +DEBUG: data_base64_fixed=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B + +DEBUG: +1. Payload Line otp_parameters { + secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" + name: "pi@raspberrypi" + issuer: "raspberrypi" + algorithm: ALGO_SHA1 + digits: 1 + type: OTP_TOTP +} +version: 1 +batch_size: 1 +batch_id: -1320898453 + + + +1. Secret + +DEBUG: OTP enum type: OTP_TOTP +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi + + +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D + +DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D', fragment='') + +DEBUG: querystring params={'data': ['CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE=']} + +DEBUG: data_base64=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE= + +DEBUG: data_base64_fixed=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE= + +DEBUG: +2. Payload Line otp_parameters { + secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" + name: "pi@raspberrypi" + algorithm: ALGO_SHA1 + digits: 1 + type: OTP_TOTP +} +version: 1 +batch_size: 1 +batch_id: -2094403140 + + + +2. Secret + +DEBUG: OTP enum type: OTP_TOTP +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B + +DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B', fragment='') + +DEBUG: querystring params={'data': ['CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B']} + +DEBUG: data_base64=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B + +DEBUG: data_base64_fixed=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B + +DEBUG: +3. Payload Line otp_parameters { + secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" + name: "pi@raspberrypi" + algorithm: ALGO_SHA1 + digits: 1 + type: OTP_TOTP +} +otp_parameters { + secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" + name: "pi@raspberrypi" + issuer: "raspberrypi" + algorithm: ALGO_SHA1 + digits: 1 + type: OTP_TOTP +} +version: 1 +batch_size: 1 +batch_id: -1822886384 + + + +3. Secret + +DEBUG: OTP enum type: OTP_TOTP +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +4. Secret + +DEBUG: OTP enum 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 + +DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D', fragment='') + +DEBUG: querystring params={'data': ['CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE=']} + +DEBUG: data_base64=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE= + +DEBUG: data_base64_fixed=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE= + +DEBUG: +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 + +DEBUG: OTP enum type: OTP_HOTP +Name: hotp demo +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: hotp +Counter: 4 +otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4 + + +# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +# Name: "encoding: ¿äÄéÉ? (demo)" +otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D + +DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D', fragment='') + +DEBUG: querystring params={'data': ['CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ==']} + +DEBUG: data_base64=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ== + +DEBUG: data_base64_fixed=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ== + +DEBUG: +5. Payload Line otp_parameters { + secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" + name: "encoding: ¿äÄéÉ? (demo)" + algorithm: ALGO_SHA1 + digits: 1 + type: OTP_TOTP +} +version: 1 +batch_size: 1 +batch_id: -171198419 + + + +6. Secret + +DEBUG: OTP enum type: OTP_TOTP +Name: encoding: ¿äÄéÉ? (demo) +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + +Extracted 6 otps from 5 otp urls by reading 1 infile diff --git a/tests/data/print_verbose_output-n.txt b/tests/data/print_verbose_output-n.txt new file mode 100644 index 0000000..7e1bceb --- /dev/null +++ b/tests/data/print_verbose_output-n.txt @@ -0,0 +1,27 @@ +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: totp + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp + +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: totp + +Name: hotp demo +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: hotp +Counter: 4 + +Name: encoding: ¿äÄéÉ? (demo) +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp + diff --git a/tests/data/print_verbose_output-v.txt b/tests/data/print_verbose_output-v.txt new file mode 100644 index 0000000..e32c422 --- /dev/null +++ b/tests/data/print_verbose_output-v.txt @@ -0,0 +1,51 @@ +QReader installed: True +CV2 version: 4.7.0 +QR reading mode: ZBAR + +Input files: ['example_export.txt'] + +1. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi + + +2. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +3. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +4. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi + + +5. Secret +Name: hotp demo +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: hotp +Counter: 4 +otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4 + + +6. Secret +Name: encoding: ¿äÄéÉ? (demo) +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + +Extracted 6 otps from 5 otp urls by reading 1 infile diff --git a/tests/data/print_verbose_output-vv.txt b/tests/data/print_verbose_output-vv.txt new file mode 100644 index 0000000..3819f4a --- /dev/null +++ b/tests/data/print_verbose_output-vv.txt @@ -0,0 +1,71 @@ +QReader installed: True +CV2 version: 4.7.0 +QR reading mode: ZBAR + +Input files: ['example_export.txt'] +Processing infile example_export.txt +# 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 + +1. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi + + +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D + +2. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B + +3. Secret +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +4. Secret +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 + +5. Secret +Name: hotp demo +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: hotp +Counter: 4 +otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4 + + +# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +# Name: "encoding: ¿äÄéÉ? (demo)" +otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D + +6. Secret +Name: encoding: ¿äÄéÉ? (demo) +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + +Extracted 6 otps from 5 otp urls by reading 1 infile diff --git a/tests/data/print_verbose_output-vvv.txt b/tests/data/print_verbose_output-vvv.txt new file mode 100644 index 0000000..e81eb24 --- /dev/null +++ b/tests/data/print_verbose_output-vvv.txt @@ -0,0 +1,223 @@ +QReader installed: True +CV2 version: 4.7.0 +QR reading mode: ZBAR + +Input files: ['example_export.txt'] +Processing infile example_export.txt +Reading lines of example_export.txt +# 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 + +# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4 +otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D + +# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +# Name: "encoding: ¿äÄéÉ? (demo)" +otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D +# 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 + +DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B', fragment='')  + +DEBUG: querystring params={'data': ['CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B']}  + +DEBUG: data_base64=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B  + +DEBUG: data_base64_fixed=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK+/////8B  + +DEBUG: +1. Payload Line otp_parameters { + secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" + name: "pi@raspberrypi" + issuer: "raspberrypi" + algorithm: ALGO_SHA1 + digits: 1 + type: OTP_TOTP +} +version: 1 +batch_size: 1 +batch_id: -1320898453 +  + + +1. Secret + +DEBUG: OTP enum type: OTP_TOTP  +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Issuer: raspberrypi +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi + + +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D + +DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D', fragment='')  + +DEBUG: querystring params={'data': ['CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE=']}  + +DEBUG: data_base64=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE=  + +DEBUG: data_base64_fixed=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4/////wE=  + +DEBUG: +2. Payload Line otp_parameters { + secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" + name: "pi@raspberrypi" + algorithm: ALGO_SHA1 + digits: 1 + type: OTP_TOTP +} +version: 1 +batch_size: 1 +batch_id: -2094403140 +  + + +2. Secret + +DEBUG: OTP enum type: OTP_TOTP  +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B + +DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B', fragment='')  + +DEBUG: querystring params={'data': ['CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B']}  + +DEBUG: data_base64=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B  + +DEBUG: data_base64_fixed=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa+f////8B  + +DEBUG: +3. Payload Line otp_parameters { + secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" + name: "pi@raspberrypi" + algorithm: ALGO_SHA1 + digits: 1 + type: OTP_TOTP +} +otp_parameters { + secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" + name: "pi@raspberrypi" + issuer: "raspberrypi" + algorithm: ALGO_SHA1 + digits: 1 + type: OTP_TOTP +} +version: 1 +batch_size: 1 +batch_id: -1822886384 +  + + +3. Secret + +DEBUG: OTP enum type: OTP_TOTP  +Name: pi@raspberrypi +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + + +4. Secret + +DEBUG: OTP enum 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 + +DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D', fragment='')  + +DEBUG: querystring params={'data': ['CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE=']}  + +DEBUG: data_base64=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE=  + +DEBUG: data_base64_fixed=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6/////wE=  + +DEBUG: +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 + +DEBUG: OTP enum type: OTP_HOTP  +Name: hotp demo +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: hotp +Counter: 4 +otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4 + + +# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +# Name: "encoding: ¿äÄéÉ? (demo)" +otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D + +DEBUG: parsed_url=ParseResult(scheme='otpauth-migration', netloc='offline', path='', params='', query='data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D', fragment='')  + +DEBUG: querystring params={'data': ['CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ==']}  + +DEBUG: data_base64=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ==  + +DEBUG: data_base64_fixed=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv//////AQ==  + +DEBUG: +5. Payload Line otp_parameters { + secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" + name: "encoding: ¿äÄéÉ? (demo)" + algorithm: ALGO_SHA1 + digits: 1 + type: OTP_TOTP +} +version: 1 +batch_size: 1 +batch_id: -171198419 +  + + +6. Secret + +DEBUG: OTP enum type: OTP_TOTP  +Name: encoding: ¿äÄéÉ? (demo) +Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY +Type: totp +otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY + +Extracted 6 otps from 5 otp urls by reading 1 infile diff --git a/tests/data/print_verbose_output.txt b/tests/data/print_verbose_output.txt index 5fc53bf..7e1bceb 100644 --- a/tests/data/print_verbose_output.txt +++ b/tests/data/print_verbose_output.txt @@ -1,171 +1,27 @@ -QReader installed: True -QR reading mode: ZBAR - -Input files: ['example_export.txt'] -Processing infile example_export.txt -Reading lines of example_export.txt -# 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 - -# otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4 -otpauth-migration://offline?data=CiUKEPqlBekzoNEukL7qlsjBCDYSCWhvdHAgZGVtbyABKAEwATgEEAEYASAAKNuv15j6%2F%2F%2F%2F%2FwE%3D - -# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY -# Name: "encoding: ¿äÄéÉ? (demo)" -otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D -# 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 - -1. Payload Line -otp_parameters { - secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" - name: "pi@raspberrypi" - issuer: "raspberrypi" - algorithm: ALGO_SHA1 - digits: 1 - type: OTP_TOTP -} -version: 1 -batch_size: 1 -batch_id: -1320898453 - - -1. Secret -OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Issuer: raspberrypi Type: totp -otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi - - -# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY -otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACEAEYASAAKLzjp5n4%2F%2F%2F%2F%2FwE%3D - -2. Payload Line -otp_parameters { - secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" - name: "pi@raspberrypi" - algorithm: ALGO_SHA1 - digits: 1 - type: OTP_TOTP -} -version: 1 -batch_size: 1 -batch_id: -2094403140 - -2. Secret -OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Type: totp -otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY - -# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi -# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY -otpauth-migration://offline?data=CigKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpIAEoATACCjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACiQ7OOa%2Bf%2F%2F%2F%2F8B - -3. Payload Line -otp_parameters { - secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" - name: "pi@raspberrypi" - algorithm: ALGO_SHA1 - digits: 1 - type: OTP_TOTP -} -otp_parameters { - secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" - name: "pi@raspberrypi" - issuer: "raspberrypi" - algorithm: ALGO_SHA1 - digits: 1 - type: OTP_TOTP -} -version: 1 -batch_size: 1 -batch_id: -1822886384 - - -3. Secret -OTP enum type: OTP_TOTP Name: pi@raspberrypi Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Type: totp -otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY - -4. Secret -OTP enum 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 -OTP enum type: OTP_HOTP Name: hotp demo Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Type: hotp Counter: 4 -otpauth://hotp/hotp%20demo?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&counter=4 - - -# otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY -# Name: "encoding: ¿äÄéÉ? (demo)" -otpauth-migration://offline?data=CjYKEPqlBekzoNEukL7qlsjBCDYSHGVuY29kaW5nOiDCv8Okw4TDqcOJPyAoZGVtbykgASgBMAIQARgBIAAorfCurv%2F%2F%2F%2F%2F%2FAQ%3D%3D - -5. Payload Line -otp_parameters { - secret: "\372\245\005\3513\240\321.\220\276\352\226\310\301\0106" - name: "encoding: ¿äÄéÉ? (demo)" - algorithm: ALGO_SHA1 - digits: 1 - type: OTP_TOTP -} -version: 1 -batch_size: 1 -batch_id: -171198419 - -6. Secret -OTP enum type: OTP_TOTP Name: encoding: ¿äÄéÉ? (demo) Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY Type: totp -otpauth://totp/encoding%3A%20%C2%BF%C3%A4%C3%84%C3%A9%C3%89%3F%20%28demo%29?secret=7KSQL2JTUDIS5EF65KLMRQIIGY -1 infile processed diff --git a/tests/extract_otp_secrets_test.py b/tests/extract_otp_secrets_test.py index 5532c38..1e86d61 100644 --- a/tests/extract_otp_secrets_test.py +++ b/tests/extract_otp_secrets_test.py @@ -19,20 +19,24 @@ # along with this program. If not, see . from __future__ import annotations # workaround for PYTHON <= 3.10 + import io import os import pathlib +import re import sys -import colorama +import time +import colorama import pytest from pytest_mock import MockerFixture - -import extract_otp_secrets -from utils import (file_exits, quick_and_dirty_workaround_encoding_problem, +from utils import (count_files_in_dir, file_exits, + quick_and_dirty_workaround_encoding_problem, read_binary_file_as_stream, read_csv, read_csv_str, read_file_to_str, read_json, read_json_str, - replace_escaped_octal_utf8_bytes_with_str, count_files_in_dir) + replace_escaped_octal_utf8_bytes_with_str) + +import extract_otp_secrets qreader_available: bool = extract_otp_secrets.qreader_available @@ -355,17 +359,26 @@ def test_normalize_bytes() -> None: 'Before\\\\302\\\\277\\\\303\nname: enc: \\302\\277\\303\\244\\303\\204\\303\\251\\303\\211?\nAfter') == 'Before\\\\302\\\\277\\\\303\nname: enc: ¿äÄéÉ?\nAfter' -# Generate verbose output: python3.11 src/extract_otp_secrets.py example_export.txt -v -n > tests/data/print_verbose_output.txt +# Generate verbose output: +# for color in '' '-n'; do for level in '' '-v' '-vv' '-vvv'; do python3.11 src/extract_otp_secrets.py example_export.txt $color $level > tests/data/print_verbose_output$color$level.txt; done; done # workaround for PYTHON <= 3.10 @pytest.mark.skipif(sys.version_info < (3, 10), reason="fileinput.input encoding exists since PYTHON 3.10") -def test_extract_verbose(capsys: pytest.CaptureFixture[str], relaxed: bool) -> None: +@pytest.mark.parametrize("verbose_level", ['', '-v', '-vv', '-vvv']) +@pytest.mark.parametrize("color", ['', '-n']) +def test_extract_verbose(verbose_level: str, color: str, capsys: pytest.CaptureFixture[str], relaxed: bool) -> None: + args = ['example_export.txt'] + if verbose_level: + args.append(verbose_level) + if color: + args.append(color) # Act - extract_otp_secrets.main(['-n', '-v', 'example_export.txt']) + extract_otp_secrets.main(args) # Assert captured = capsys.readouterr() - expected_stdout = read_file_to_str('tests/data/print_verbose_output.txt') + expected_stdout = normalize_verbose_text(read_file_to_str(f'tests/data/print_verbose_output{color}{verbose_level}.txt')) + actual_stdout = normalize_verbose_text(captured.out) if not qreader_available: expected_stdout = expected_stdout.replace('QReader installed: True', 'QReader installed: False') @@ -374,12 +387,15 @@ def test_extract_verbose(capsys: pytest.CaptureFixture[str], relaxed: bool) -> N if relaxed or sys.implementation.name == 'pypy': print('\nRelaxed mode\n') - assert replace_escaped_octal_utf8_bytes_with_str(captured.out) == replace_escaped_octal_utf8_bytes_with_str(expected_stdout) - assert quick_and_dirty_workaround_encoding_problem(captured.out) == quick_and_dirty_workaround_encoding_problem(expected_stdout) + assert replace_escaped_octal_utf8_bytes_with_str(actual_stdout) == replace_escaped_octal_utf8_bytes_with_str(expected_stdout) + assert quick_and_dirty_workaround_encoding_problem(actual_stdout) == quick_and_dirty_workaround_encoding_problem(expected_stdout) else: - assert captured.out == expected_stdout + assert actual_stdout == expected_stdout assert captured.err == '' +def normalize_verbose_text(text: str) -> str: + return re.sub('^.+ version: .+$', '', text, flags=re.MULTILINE | re.IGNORECASE) + def test_extract_debug(capsys: pytest.CaptureFixture[str]) -> None: # Act @@ -623,7 +639,9 @@ def test_img_qr_reader_from_file_happy_path(capsys: pytest.CaptureFixture[str]) @pytest.mark.qreader def test_img_qr_reader_by_parameter(capsys: pytest.CaptureFixture[str], qr_mode: str) -> None: # Act + start_s = time.process_time() extract_otp_secrets.main(['--qr', qr_mode, 'tests/data/test_googleauth_export.png']) + elapsed_s = time.process_time() - start_s # Assert captured = capsys.readouterr() @@ -631,6 +649,8 @@ def test_img_qr_reader_by_parameter(capsys: pytest.CaptureFixture[str], qr_mode: assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT_PNG assert captured.err == '' + print(f"Elapsed time for {qr_mode}: {elapsed_s:.2f}s") + @pytest.mark.qreader def test_extract_multiple_files_and_mixed(capsys: pytest.CaptureFixture[str]) -> None: diff --git a/tests/extract_otp_secrets_txt_unit_test.py b/tests/extract_otp_secrets_txt_unit_test.py index 03b213b..f5545d5 100644 --- a/tests/extract_otp_secrets_txt_unit_test.py +++ b/tests/extract_otp_secrets_txt_unit_test.py @@ -30,6 +30,10 @@ from utils import (Capturing, read_csv, read_file_to_str, read_json, remove_dir_with_files, remove_file, count_files_in_dir) +# Conditional skip example +# if sys.implementation.name == 'pypy' or sys.platform.startswith("win") or sys.version_info < (3, 10): +# self.skipTest("Avoid encoding problems") + class TestExtract(unittest.TestCase): def test_extract_csv(self) -> None: @@ -170,18 +174,6 @@ Type: totp self.assertTrue(os.path.isfile('testout/qr/6-encodingäÄéÉdemo.png')) self.assertEqual(count_files_in_dir('testout/qr'), 6) - def test_extract_verbose(self) -> None: - if sys.implementation.name == 'pypy' or sys.platform.startswith("win") or sys.version_info < (3, 10): - self.skipTest("Avoid encoding problems") - out = io.StringIO() - with redirect_stdout(out): - extract_otp_secrets.main(['-n', '-v', 'example_export.txt']) - actual_output = out.getvalue() - - expected_output = read_file_to_str('tests/data/print_verbose_output.txt') - - self.assertEqual(actual_output, expected_output) - def test_extract_debug(self) -> None: out = io.StringIO() with redirect_stdout(out):