mirror of
https://github.com/scito/extract_otp_secret_keys.git
synced 2025-12-15 02:56:37 +01:00
colored warn and error messages
- add log_warn() and log_error() - adapt tests
This commit is contained in:
parent
fc1619d9c7
commit
b3fc854078
9 changed files with 105 additions and 54 deletions
1
Pipfile
1
Pipfile
|
|
@ -9,6 +9,7 @@ qrcode = "*"
|
||||||
pillow = "*"
|
pillow = "*"
|
||||||
qreader = "*"
|
qreader = "*"
|
||||||
opencv-contrib-python = "*"
|
opencv-contrib-python = "*"
|
||||||
|
colorama = "*"
|
||||||
# for macOS: opencv-contrib-python = "<=4.7.0"
|
# for macOS: opencv-contrib-python = "<=4.7.0"
|
||||||
# for PYTHON <= 3.7: typing_extensions = "*"
|
# for PYTHON <= 3.7: typing_extensions = "*"
|
||||||
|
|
||||||
|
|
|
||||||
10
Pipfile.lock
generated
10
Pipfile.lock
generated
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"hash": {
|
"hash": {
|
||||||
"sha256": "beffcba766af29a6a313c019cc98ab27e61c6dd433d02df0917fdb3808b90379"
|
"sha256": "4cc62fa4427b3a8821438db9164bd5c7c42d4e5f08d3f950c40da381ba7e063e"
|
||||||
},
|
},
|
||||||
"pipfile-spec": 6,
|
"pipfile-spec": 6,
|
||||||
"requires": {
|
"requires": {
|
||||||
|
|
@ -16,6 +16,14 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
|
"colorama": {
|
||||||
|
"hashes": [
|
||||||
|
"sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44",
|
||||||
|
"sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"
|
||||||
|
],
|
||||||
|
"index": "pypi",
|
||||||
|
"version": "==0.4.6"
|
||||||
|
},
|
||||||
"numpy": {
|
"numpy": {
|
||||||
"hashes": [
|
"hashes": [
|
||||||
"sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9",
|
"sha256:0044f7d944ee882400890f9ae955220d29b33d809a038923d88e4e01d652acd9",
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ dependencies = [
|
||||||
"opencv-contrib-python<=4.7.0; sys_platform == 'darwin'",
|
"opencv-contrib-python<=4.7.0; sys_platform == 'darwin'",
|
||||||
"opencv-contrib-python; sys_platform != 'darwin'",
|
"opencv-contrib-python; sys_platform != 'darwin'",
|
||||||
"typing_extensions; python_version<='3.7'",
|
"typing_extensions; python_version<='3.7'",
|
||||||
|
"colorama",
|
||||||
]
|
]
|
||||||
description = "Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of 'Google Authenticator' app"
|
description = "Extract two-factor authentication (2FA, TFA, OTP) secret keys from export QR codes of 'Google Authenticator' app"
|
||||||
dynamic = ["version"]
|
dynamic = ["version"]
|
||||||
|
|
|
||||||
|
|
@ -6,3 +6,4 @@ opencv-contrib-python<=4.7.0; sys_platform == 'darwin'
|
||||||
opencv-contrib-python; sys_platform != 'darwin'
|
opencv-contrib-python; sys_platform != 'darwin'
|
||||||
pyzbar
|
pyzbar
|
||||||
typing_extensions; python_version<='3.7'
|
typing_extensions; python_version<='3.7'
|
||||||
|
colorama
|
||||||
|
|
|
||||||
|
|
@ -65,6 +65,7 @@ else:
|
||||||
from qrcode import QRCode # type: ignore
|
from qrcode import QRCode # type: ignore
|
||||||
|
|
||||||
import protobuf_generated_python.google_auth_pb2 as pb
|
import protobuf_generated_python.google_auth_pb2 as pb
|
||||||
|
import colorama
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import cv2 # type: ignore
|
import cv2 # type: ignore
|
||||||
|
|
@ -123,6 +124,7 @@ CAMERA: Final[str] = 'camera'
|
||||||
# Global variable declaration
|
# Global variable declaration
|
||||||
verbose: int = 0
|
verbose: int = 0
|
||||||
quiet: bool = False
|
quiet: bool = False
|
||||||
|
colored: bool = True
|
||||||
|
|
||||||
|
|
||||||
def sys_main() -> None:
|
def sys_main() -> None:
|
||||||
|
|
@ -134,13 +136,17 @@ def main(sys_args: list[str]) -> None:
|
||||||
sys.stdout.close = lambda: None # type: ignore
|
sys.stdout.close = lambda: None # type: ignore
|
||||||
# set encoding to utf-8, needed for Windows
|
# set encoding to utf-8, needed for Windows
|
||||||
try:
|
try:
|
||||||
sys.stdout.reconfigure(encoding='utf-8') # type: ignore
|
sys.stdout.reconfigure(encoding='utf-8')
|
||||||
|
sys.stderr.reconfigure(encoding='utf-8')
|
||||||
except AttributeError: # '_io.StringIO' object has no attribute 'reconfigure'
|
except AttributeError: # '_io.StringIO' object has no attribute 'reconfigure'
|
||||||
# StringIO in tests do not have all attributes, ignore it
|
# StringIO in tests do not have all attributes, ignore it
|
||||||
pass
|
pass
|
||||||
|
|
||||||
args = parse_args(sys_args)
|
args = parse_args(sys_args)
|
||||||
|
|
||||||
|
if colored:
|
||||||
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
otps = extract_otps(args)
|
otps = extract_otps(args)
|
||||||
write_csv(args, otps)
|
write_csv(args, otps)
|
||||||
write_keepass_csv(args, otps)
|
write_keepass_csv(args, otps)
|
||||||
|
|
@ -148,7 +154,7 @@ def main(sys_args: list[str]) -> None:
|
||||||
|
|
||||||
|
|
||||||
def parse_args(sys_args: list[str]) -> Args:
|
def parse_args(sys_args: list[str]) -> Args:
|
||||||
global verbose, quiet
|
global verbose, quiet, colored
|
||||||
description_text = "Extracts one time password (OTP) secret keys from QR codes, e.g. from Google Authenticator app."
|
description_text = "Extracts one time password (OTP) secret keys from QR codes, e.g. from Google Authenticator app."
|
||||||
if qreader_available:
|
if qreader_available:
|
||||||
description_text += "\nIf no infiles are provided, the QR codes are interactively captured from the camera."
|
description_text += "\nIf no infiles are provided, the QR codes are interactively captured from the camera."
|
||||||
|
|
@ -165,13 +171,14 @@ python extract_otp_secrets.py = < example_export.png"""
|
||||||
arg_parser.add_argument('infile', help="""a) file or - for stdin with 'otpauth-migration://...' URLs separated by newlines, lines starting with # are ignored;
|
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 '+')
|
b) image file containing a QR code or = for stdin for an image containing a QR code""", nargs='*' if qreader_available else '+')
|
||||||
if qreader_available:
|
if qreader_available:
|
||||||
arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, nargs=1, metavar=('NUMBER'))
|
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('--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('--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('--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('--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('--printqr', '-p', help='print QR code(s) as text to the terminal (requires qrcode module)', action='store_true')
|
||||||
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
|
arg_parser.add_argument('--saveqr', '-s', help='save QR code(s) as images to the given folder (requires qrcode module)', metavar=('DIR'))
|
||||||
|
arg_parser.add_argument('--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 = arg_parser.add_mutually_exclusive_group()
|
||||||
output_group.add_argument('--verbose', '-v', help='verbose output', action='count')
|
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('--quiet', '-q', help='no stdout output, except output set by -', action='store_true')
|
||||||
|
|
@ -181,6 +188,7 @@ b) image file containing a QR code or = for stdin for an image containing a QR c
|
||||||
|
|
||||||
verbose = args.verbose if args.verbose else 0
|
verbose = args.verbose if args.verbose else 0
|
||||||
quiet = True if args.quiet else False
|
quiet = True if args.quiet else False
|
||||||
|
colored = not args.no_color
|
||||||
if verbose: print(f"QReader installed: {qreader_available}")
|
if verbose: print(f"QReader installed: {qreader_available}")
|
||||||
if qreader_available:
|
if qreader_available:
|
||||||
if verbose > 1: print(f"CV2 version: {cv2.__version__}")
|
if verbose > 1: print(f"CV2 version: {cv2.__version__}")
|
||||||
|
|
@ -213,7 +221,7 @@ def extract_otps_from_camera(args: Args) -> Otps:
|
||||||
|
|
||||||
qr_mode = QRMode[args.qr]
|
qr_mode = QRMode[args.qr]
|
||||||
|
|
||||||
cam = cv2.VideoCapture(args.camera[0])
|
cam = cv2.VideoCapture(args.camera)
|
||||||
window_name = "Extract OTP Secrets: Capture QR Codes from Camera"
|
window_name = "Extract OTP Secrets: Capture QR Codes from Camera"
|
||||||
cv2.namedWindow(window_name, cv2.WINDOW_AUTOSIZE)
|
cv2.namedWindow(window_name, cv2.WINDOW_AUTOSIZE)
|
||||||
|
|
||||||
|
|
@ -224,7 +232,7 @@ def extract_otps_from_camera(args: Args) -> Otps:
|
||||||
success, img = cam.read()
|
success, img = cam.read()
|
||||||
new_otps_count = 0
|
new_otps_count = 0
|
||||||
if not success:
|
if not success:
|
||||||
eprint("ERROR: Failed to capture image")
|
log_error("Failed to capture image")
|
||||||
break
|
break
|
||||||
if qr_mode in [QRMode.QREADER, QRMode.DEEP_QREADER]:
|
if qr_mode in [QRMode.QREADER, QRMode.DEEP_QREADER]:
|
||||||
bbox, found = qreader.detect(img)
|
bbox, found = qreader.detect(img)
|
||||||
|
|
@ -255,7 +263,7 @@ def extract_otps_from_camera(args: Args) -> Otps:
|
||||||
pts = pts.reshape((-1, 1, 2))
|
pts = pts.reshape((-1, 1, 2))
|
||||||
cv2.polylines(img, [pts], True, get_color(new_otps_count, otp_url), RECT_THICKNESS)
|
cv2.polylines(img, [pts], True, get_color(new_otps_count, otp_url), RECT_THICKNESS)
|
||||||
else:
|
else:
|
||||||
assert False, f"ERROR: Wrong QReader mode {qr_mode.name}"
|
assert False, f"Wrong QReader mode {qr_mode.name}"
|
||||||
|
|
||||||
cv2.putText(img, f"Mode: {qr_mode.name} (Hit space to change)", START_POS_TEXT, FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
cv2.putText(img, f"Mode: {qr_mode.name} (Hit space to change)", START_POS_TEXT, FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||||
cv2.putText(img, "Hit ESC to quit", tuple(map(add, START_POS_TEXT, FONT_DY)), FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
cv2.putText(img, "Hit ESC to quit", tuple(map(add, START_POS_TEXT, FONT_DY)), FONT, FONT_SCALE, NORMAL_COLOR, FONT_THICKNESS, FONT_LINE_STYLE)
|
||||||
|
|
@ -341,14 +349,14 @@ def read_lines_from_text_file(filename: str) -> list[str]:
|
||||||
for line in (line.strip() for line in finput):
|
for line in (line.strip() for line in finput):
|
||||||
if verbose: print(line)
|
if verbose: print(line)
|
||||||
if is_binary(line):
|
if is_binary(line):
|
||||||
abort("\nBinary input was given in stdin, please use = instead of - as infile argument for images.")
|
abort("Binary input was given in stdin, please use = instead of - as infile argument for images.")
|
||||||
# unfortunately yield line leads to random test fails
|
# unfortunately yield line leads to random test fails
|
||||||
lines.append(line)
|
lines.append(line)
|
||||||
if not lines:
|
if not lines:
|
||||||
eprint(f"WARN: {filename.replace('-', 'stdin')} is empty")
|
log_warn(f"{filename.replace('-', 'stdin')} is empty")
|
||||||
except UnicodeDecodeError:
|
except UnicodeDecodeError:
|
||||||
if filename == '-':
|
if filename == '-':
|
||||||
abort("\nERROR: Unable to open text file form stdin. "
|
abort("Unable to open text file form stdin. "
|
||||||
"In case you want read an image file from stdin, you must use '=' instead of '-'.")
|
"In case you want read an image file from stdin, you must use '=' instead of '-'.")
|
||||||
else: # The file is probably an image, process below
|
else: # The file is probably an image, process below
|
||||||
return []
|
return []
|
||||||
|
|
@ -367,7 +375,7 @@ def extract_otp_from_otp_url(otpauth_migration_url: str, otps: Otps, urls_count:
|
||||||
# pylint: disable=no-member
|
# pylint: disable=no-member
|
||||||
for raw_otp in payload.otp_parameters:
|
for raw_otp in payload.otp_parameters:
|
||||||
new_otps_count += 1
|
new_otps_count += 1
|
||||||
if verbose: print(f"\n{len(otps) + 1}. Secret Key")
|
if verbose: print(f"\n{len(otps) + 1}. Secret")
|
||||||
secret = convert_secret_from_bytes_to_base32_str(raw_otp.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: print('OTP enum type:', get_enum_name_by_number(raw_otp, 'type'))
|
||||||
otp_type = get_otp_type_str_from_code(raw_otp.type)
|
otp_type = get_otp_type_str_from_code(raw_otp.type)
|
||||||
|
|
@ -405,17 +413,17 @@ def convert_img_to_otp_url(filename: str, args: Args) -> OtpUrls:
|
||||||
# Workaround for pytest, since pytest cannot monkeypatch sys.stdin.buffer
|
# Workaround for pytest, since pytest cannot monkeypatch sys.stdin.buffer
|
||||||
stdin = sys.stdin.read() # type: ignore # Workaround for pytest fixtures
|
stdin = sys.stdin.read() # type: ignore # Workaround for pytest fixtures
|
||||||
if not stdin:
|
if not stdin:
|
||||||
eprint("WARN: stdin is empty")
|
log_warn("stdin is empty")
|
||||||
try:
|
try:
|
||||||
img_array = numpy.frombuffer(stdin, dtype='uint8')
|
img_array = numpy.frombuffer(stdin, dtype='uint8')
|
||||||
except TypeError as e:
|
except TypeError as e:
|
||||||
abort(f"\nERROR: Cannot read binary stdin buffer. Exception: {e}")
|
abort(f"Cannot read binary stdin buffer. Exception: {e}")
|
||||||
if not img_array.size:
|
if not img_array.size:
|
||||||
return []
|
return []
|
||||||
img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
|
img = cv2.imdecode(img_array, cv2.IMREAD_UNCHANGED)
|
||||||
|
|
||||||
if img is None:
|
if img is None:
|
||||||
abort(f"\nERROR: Unable to open file for reading.\ninput file: {filename}")
|
abort(f"Unable to open file for reading.\ninput file: {filename}")
|
||||||
|
|
||||||
qr_mode = QRMode[args.qr]
|
qr_mode = QRMode[args.qr]
|
||||||
otp_urls: OtpUrls = []
|
otp_urls: OtpUrls = []
|
||||||
|
|
@ -432,12 +440,12 @@ def convert_img_to_otp_url(filename: str, args: Args) -> OtpUrls:
|
||||||
qrcodes = zbar.decode(img)
|
qrcodes = zbar.decode(img)
|
||||||
otp_urls += [qrcode.data.decode('utf-8') for qrcode in qrcodes]
|
otp_urls += [qrcode.data.decode('utf-8') for qrcode in qrcodes]
|
||||||
else:
|
else:
|
||||||
assert False, f"ERROR: Wrong QReader mode {qr_mode.name}"
|
assert False, f"Wrong QReader mode {qr_mode.name}"
|
||||||
|
|
||||||
if len(otp_urls) == 0:
|
if len(otp_urls) == 0:
|
||||||
abort(f"\nERROR: Unable to read QR Code from file.\ninput file: {filename}")
|
abort(f"Unable to read QR Code from file.\ninput file: {filename}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
abort(f"\nERROR: Encountered exception '{e}'.\ninput file: {filename}")
|
abort(f"Encountered exception '{e}'.\ninput file: {filename}")
|
||||||
return otp_urls
|
return otp_urls
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -446,10 +454,10 @@ def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.M
|
||||||
if not otp_url.startswith('otpauth-migration://'):
|
if not otp_url.startswith('otpauth-migration://'):
|
||||||
msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}"
|
msg = f"input is not a otpauth-migration:// url\nsource: {source}\ninput: {otp_url}"
|
||||||
if source == CAMERA:
|
if source == CAMERA:
|
||||||
eprint(f"\nERROR: {msg}")
|
log_error(f"{msg}")
|
||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
eprint(f"\nWARN: {msg}\nMaybe a wrong file was given")
|
log_warn(f"{msg}\nMaybe a wrong file was given")
|
||||||
parsed_url = urlparse.urlparse(otp_url)
|
parsed_url = urlparse.urlparse(otp_url)
|
||||||
if verbose > 2: print(f"\nDEBUG: parsed_url={parsed_url}")
|
if verbose > 2: print(f"\nDEBUG: parsed_url={parsed_url}")
|
||||||
try:
|
try:
|
||||||
|
|
@ -458,7 +466,7 @@ def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.M
|
||||||
params = {}
|
params = {}
|
||||||
if verbose > 2: print(f"\nDEBUG: querystring params={params}")
|
if verbose > 2: print(f"\nDEBUG: querystring params={params}")
|
||||||
if 'data' not in params:
|
if 'data' not in params:
|
||||||
eprint(f"\nERROR: could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}")
|
log_error(f"could not parse query parameter in input url\nsource: {source}\nurl: {otp_url}")
|
||||||
return None
|
return None
|
||||||
data_base64 = params['data'][0]
|
data_base64 = params['data'][0]
|
||||||
if verbose > 2: print(f"\nDEBUG: data_base64={data_base64}")
|
if verbose > 2: print(f"\nDEBUG: data_base64={data_base64}")
|
||||||
|
|
@ -469,7 +477,7 @@ def get_payload_from_otp_url(otp_url: str, i: int, source: str) -> Optional[pb.M
|
||||||
try:
|
try:
|
||||||
payload.ParseFromString(data)
|
payload.ParseFromString(data)
|
||||||
except Exception:
|
except Exception:
|
||||||
abort(f"\nERROR: Cannot decode otpauth-migration migration payload.\n"
|
abort(f"Cannot decode otpauth-migration migration payload.\n"
|
||||||
f"data={data_base64}")
|
f"data={data_base64}")
|
||||||
if verbose:
|
if verbose:
|
||||||
print(f"\n{i}. Payload Line", payload, sep='\n')
|
print(f"\n{i}. Payload Line", payload, sep='\n')
|
||||||
|
|
@ -620,7 +628,7 @@ def open_file_or_stdout_for_csv(filename: str) -> TextIO:
|
||||||
|
|
||||||
def check_file_exists(filename: str) -> None:
|
def check_file_exists(filename: str) -> None:
|
||||||
if filename != '-' and not os.path.isfile(filename):
|
if filename != '-' and not os.path.isfile(filename):
|
||||||
abort(f"\nERROR: Input file provided is non-existent or not a file."
|
abort(f"Input file provided is non-existent or not a file."
|
||||||
f"\ninput file: {filename}")
|
f"\ninput file: {filename}")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -632,13 +640,21 @@ def is_binary(line: str) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def log_warn(msg: str) -> None:
|
||||||
|
eprint(f"{colorama.Fore.RED if colored else ''}\nWARN: {msg}{colorama.Fore.RESET if colored else ''}")
|
||||||
|
|
||||||
|
|
||||||
|
def log_error(msg: str) -> None:
|
||||||
|
eprint(f"{colorama.Fore.RED if colored else ''}\nERROR: {msg}{colorama.Fore.RESET if colored else ''}")
|
||||||
|
|
||||||
|
|
||||||
def eprint(*args: Any, **kwargs: Any) -> None:
|
def eprint(*args: Any, **kwargs: Any) -> None:
|
||||||
'''Print to stderr.'''
|
'''Print to stderr.'''
|
||||||
print(*args, file=sys.stderr, **kwargs)
|
print(*args, file=sys.stderr, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def abort(*args: Any, **kwargs: Any) -> None:
|
def abort(msg: str) -> None:
|
||||||
eprint(*args, **kwargs)
|
log_error(msg)
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ batch_size: 1
|
||||||
batch_id: -1320898453
|
batch_id: -1320898453
|
||||||
|
|
||||||
|
|
||||||
1. Secret Key
|
1. Secret
|
||||||
OTP enum type: OTP_TOTP
|
OTP enum type: OTP_TOTP
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
|
@ -68,7 +68,7 @@ batch_size: 1
|
||||||
batch_id: -2094403140
|
batch_id: -2094403140
|
||||||
|
|
||||||
|
|
||||||
2. Secret Key
|
2. Secret
|
||||||
OTP enum type: OTP_TOTP
|
OTP enum type: OTP_TOTP
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
|
@ -101,7 +101,7 @@ batch_size: 1
|
||||||
batch_id: -1822886384
|
batch_id: -1822886384
|
||||||
|
|
||||||
|
|
||||||
3. Secret Key
|
3. Secret
|
||||||
OTP enum type: OTP_TOTP
|
OTP enum type: OTP_TOTP
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
|
@ -109,7 +109,7 @@ Type: totp
|
||||||
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
|
||||||
|
|
||||||
4. Secret Key
|
4. Secret
|
||||||
OTP enum type: OTP_TOTP
|
OTP enum type: OTP_TOTP
|
||||||
Name: pi@raspberrypi
|
Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
|
@ -135,7 +135,7 @@ batch_size: 1
|
||||||
batch_id: -1558849573
|
batch_id: -1558849573
|
||||||
|
|
||||||
|
|
||||||
5. Secret Key
|
5. Secret
|
||||||
OTP enum type: OTP_HOTP
|
OTP enum type: OTP_HOTP
|
||||||
Name: hotp demo
|
Name: hotp demo
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
|
@ -161,7 +161,7 @@ batch_size: 1
|
||||||
batch_id: -171198419
|
batch_id: -171198419
|
||||||
|
|
||||||
|
|
||||||
6. Secret Key
|
6. Secret
|
||||||
OTP enum type: OTP_TOTP
|
OTP enum type: OTP_TOTP
|
||||||
Name: encoding: ¿äÄéÉ? (demo)
|
Name: encoding: ¿äÄéÉ? (demo)
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class TestQRImageExtract(unittest.TestCase):
|
||||||
def test_img_qr_reader_no_qr_code_in_image(self) -> None:
|
def test_img_qr_reader_no_qr_code_in_image(self) -> None:
|
||||||
with Capturing() as actual_output:
|
with Capturing() as actual_output:
|
||||||
with self.assertRaises(SystemExit) as context:
|
with self.assertRaises(SystemExit) as context:
|
||||||
extract_otp_secrets.main(['tests/data/lena_std.tif'])
|
extract_otp_secrets.main(['-n', 'tests/data/lena_std.tif'])
|
||||||
|
|
||||||
expected_output = ['', 'ERROR: Unable to read QR Code from file.', 'input file: tests/data/lena_std.tif']
|
expected_output = ['', 'ERROR: Unable to read QR Code from file.', 'input file: tests/data/lena_std.tif']
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@ class TestQRImageExtract(unittest.TestCase):
|
||||||
def test_img_qr_reader_nonexistent_file(self) -> None:
|
def test_img_qr_reader_nonexistent_file(self) -> None:
|
||||||
with Capturing() as actual_output:
|
with Capturing() as actual_output:
|
||||||
with self.assertRaises(SystemExit) as context:
|
with self.assertRaises(SystemExit) as context:
|
||||||
extract_otp_secrets.main(['nonexistent.bmp'])
|
extract_otp_secrets.main(['-n', 'nonexistent.bmp'])
|
||||||
|
|
||||||
expected_output = ['', 'ERROR: Input file provided is non-existent or not a file.', 'input file: nonexistent.bmp']
|
expected_output = ['', 'ERROR: Input file provided is non-existent or not a file.', 'input file: nonexistent.bmp']
|
||||||
|
|
||||||
|
|
@ -59,7 +59,7 @@ class TestQRImageExtract(unittest.TestCase):
|
||||||
|
|
||||||
def test_img_qr_reader_non_image_file(self) -> None:
|
def test_img_qr_reader_non_image_file(self) -> None:
|
||||||
with Capturing() as actual_output:
|
with Capturing() as actual_output:
|
||||||
extract_otp_secrets.main(['tests/data/text_masquerading_as_image.jpeg'])
|
extract_otp_secrets.main(['-n', 'tests/data/text_masquerading_as_image.jpeg'])
|
||||||
|
|
||||||
expected_output = [
|
expected_output = [
|
||||||
'',
|
'',
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import io
|
||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import sys
|
import sys
|
||||||
|
import colorama
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from pytest_mock import MockerFixture
|
from pytest_mock import MockerFixture
|
||||||
|
|
@ -54,7 +55,7 @@ def test_extract_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
def test_extract_non_existent_file(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_extract_non_existent_file(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
# Act
|
# Act
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
extract_otp_secrets.main(['non_existent_file.txt'])
|
extract_otp_secrets.main(['-n', 'non_existent_file.txt'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -86,25 +87,25 @@ def test_extract_stdin_empty(capsys: pytest.CaptureFixture[str], monkeypatch: py
|
||||||
monkeypatch.setattr('sys.stdin', io.StringIO())
|
monkeypatch.setattr('sys.stdin', io.StringIO())
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secrets.main(['-'])
|
extract_otp_secrets.main(['-n', '-'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
assert captured.out == ''
|
assert captured.out == ''
|
||||||
assert captured.err == 'WARN: stdin is empty\n'
|
assert captured.err == '\nWARN: stdin is empty\n'
|
||||||
|
|
||||||
|
|
||||||
def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_extract_empty_file_no_qreader(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
if qreader_available:
|
if qreader_available:
|
||||||
# Act
|
# Act
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
extract_otp_secrets.main(['tests/data/empty_file.txt'])
|
extract_otp_secrets.main(['-n', 'tests/data/empty_file.txt'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
expected_stderr = 'WARN: tests/data/empty_file.txt is empty\n\nERROR: Unable to open file for reading.\ninput file: tests/data/empty_file.txt\n'
|
expected_stderr = '\nWARN: tests/data/empty_file.txt is empty\n\nERROR: Unable to open file for reading.\ninput file: tests/data/empty_file.txt\n'
|
||||||
|
|
||||||
assert captured.err == expected_stderr
|
assert captured.err == expected_stderr
|
||||||
assert captured.out == ''
|
assert captured.out == ''
|
||||||
|
|
@ -127,13 +128,13 @@ def test_extract_stdin_img_empty(capsys: pytest.CaptureFixture[str], monkeypatch
|
||||||
monkeypatch.setattr('sys.stdin', io.BytesIO())
|
monkeypatch.setattr('sys.stdin', io.BytesIO())
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secrets.main(['='])
|
extract_otp_secrets.main(['-n', '='])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
assert captured.out == ''
|
assert captured.out == ''
|
||||||
assert captured.err == 'WARN: stdin is empty\n'
|
assert captured.err == '\nWARN: stdin is empty\n'
|
||||||
|
|
||||||
|
|
||||||
def test_extract_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
|
def test_extract_csv(capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
|
||||||
|
|
@ -354,9 +355,10 @@ 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'
|
'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
|
||||||
def test_extract_verbose(capsys: pytest.CaptureFixture[str], relaxed: bool) -> None:
|
def test_extract_verbose(capsys: pytest.CaptureFixture[str], relaxed: bool) -> None:
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secrets.main(['-v', 'example_export.txt'])
|
extract_otp_secrets.main(['-n', '-v', 'example_export.txt'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -442,7 +444,7 @@ def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: Mocker
|
||||||
def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secrets.main(['-v', '-q', 'example_export.txt'])
|
extract_otp_secrets.main(['-n', '-v', '-q', 'example_export.txt'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -457,7 +459,7 @@ def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
def test_wrong_data(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_wrong_data(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secrets.main(['tests/data/test_export_wrong_data.txt'])
|
extract_otp_secrets.main(['-n', 'tests/data/test_export_wrong_data.txt'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -475,7 +477,7 @@ data=XXXX
|
||||||
|
|
||||||
def test_wrong_content(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_wrong_content(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secrets.main(['tests/data/test_export_wrong_content.txt'])
|
extract_otp_secrets.main(['-n', 'tests/data/test_export_wrong_content.txt'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -497,7 +499,7 @@ url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy ei
|
||||||
|
|
||||||
def test_one_wrong_file(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_one_wrong_file(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secrets.main(['tests/data/test_export_wrong_content.txt', 'example_export.txt'])
|
extract_otp_secrets.main(['-n', 'tests/data/test_export_wrong_content.txt', 'example_export.txt'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -517,13 +519,35 @@ url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy ei
|
||||||
assert captured.err == expected_stderr
|
assert captured.err == expected_stderr
|
||||||
|
|
||||||
|
|
||||||
|
def test_one_wrong_file_colored(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
# Act
|
||||||
|
extract_otp_secrets.main(['tests/data/test_export_wrong_content.txt', 'example_export.txt'])
|
||||||
|
|
||||||
|
# Assert
|
||||||
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
|
expected_stderr = f'''{colorama.Fore.RED}
|
||||||
|
WARN: input is not a otpauth-migration:// url
|
||||||
|
source: tests/data/test_export_wrong_content.txt
|
||||||
|
input: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
|
||||||
|
Maybe a wrong file was given{colorama.Fore.RESET}
|
||||||
|
{colorama.Fore.RED}
|
||||||
|
ERROR: could not parse query parameter in input url
|
||||||
|
source: tests/data/test_export_wrong_content.txt
|
||||||
|
url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.{colorama.Fore.RESET}
|
||||||
|
'''
|
||||||
|
|
||||||
|
assert captured.out == EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT
|
||||||
|
assert captured.err == expected_stderr
|
||||||
|
|
||||||
|
|
||||||
def test_one_wrong_line(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
def test_one_wrong_line(capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch) -> None:
|
||||||
# Arrange
|
# Arrange
|
||||||
monkeypatch.setattr('sys.stdin',
|
monkeypatch.setattr('sys.stdin',
|
||||||
io.StringIO(read_file_to_str('tests/data/test_export_wrong_content.txt') + read_file_to_str('example_export.txt')))
|
io.StringIO(read_file_to_str('tests/data/test_export_wrong_content.txt') + read_file_to_str('example_export.txt')))
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secrets.main(['-'])
|
extract_otp_secrets.main(['-n', '-'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -545,7 +569,7 @@ url: Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy ei
|
||||||
|
|
||||||
def test_wrong_prefix(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_wrong_prefix(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secrets.main(['tests/data/test_export_wrong_prefix.txt'])
|
extract_otp_secrets.main(['-n', 'tests/data/test_export_wrong_prefix.txt'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -655,12 +679,12 @@ def test_img_qr_reader_from_stdin_wrong_symbol(capsys: pytest.CaptureFixture[str
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
extract_otp_secrets.main(['-'])
|
extract_otp_secrets.main(['-n', '-'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
||||||
expected_stderr = '\nBinary input was given in stdin, please use = instead of - as infile argument for images.\n'
|
expected_stderr = '\nERROR: Binary input was given in stdin, please use = instead of - as infile argument for images.\n'
|
||||||
|
|
||||||
assert captured.err == expected_stderr
|
assert captured.err == expected_stderr
|
||||||
assert captured.out == ''
|
assert captured.out == ''
|
||||||
|
|
@ -675,7 +699,7 @@ def test_extract_stdin_stdout_wrong_symbol(capsys: pytest.CaptureFixture[str], m
|
||||||
|
|
||||||
# Act
|
# Act
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
extract_otp_secrets.main(['='])
|
extract_otp_secrets.main(['-n', '='])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -692,7 +716,7 @@ def test_extract_stdin_stdout_wrong_symbol(capsys: pytest.CaptureFixture[str], m
|
||||||
def test_img_qr_reader_no_qr_code_in_image(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_img_qr_reader_no_qr_code_in_image(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
# Act
|
# Act
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
extract_otp_secrets.main(['tests/data/lena_std.tif'])
|
extract_otp_secrets.main(['-n', 'tests/data/lena_std.tif'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -709,7 +733,7 @@ def test_img_qr_reader_no_qr_code_in_image(capsys: pytest.CaptureFixture[str]) -
|
||||||
def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
# Act
|
# Act
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
extract_otp_secrets.main(['nonexistent.bmp'])
|
extract_otp_secrets.main(['-n', 'nonexistent.bmp'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
@ -724,7 +748,7 @@ def test_img_qr_reader_nonexistent_file(capsys: pytest.CaptureFixture[str]) -> N
|
||||||
|
|
||||||
def test_non_image_file(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_non_image_file(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
# Act
|
# Act
|
||||||
extract_otp_secrets.main(['tests/data/text_masquerading_as_image.jpeg'])
|
extract_otp_secrets.main(['-n', 'tests/data/text_masquerading_as_image.jpeg'])
|
||||||
|
|
||||||
# Assert
|
# Assert
|
||||||
captured = capsys.readouterr()
|
captured = capsys.readouterr()
|
||||||
|
|
|
||||||
|
|
@ -175,7 +175,7 @@ Type: totp
|
||||||
self.skipTest("Avoid encoding problems")
|
self.skipTest("Avoid encoding problems")
|
||||||
out = io.StringIO()
|
out = io.StringIO()
|
||||||
with redirect_stdout(out):
|
with redirect_stdout(out):
|
||||||
extract_otp_secrets.main(['-v', 'example_export.txt'])
|
extract_otp_secrets.main(['-n', '-v', 'example_export.txt'])
|
||||||
actual_output = out.getvalue()
|
actual_output = out.getvalue()
|
||||||
|
|
||||||
expected_output = read_file_to_str('tests/data/print_verbose_output.txt')
|
expected_output = read_file_to_str('tests/data/print_verbose_output.txt')
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue