mirror of
https://github.com/scito/extract_otp_secret_keys.git
synced 2025-12-07 23:35:07 +01:00
make zbar lib optional
- refactor: global variable renaming
This commit is contained in:
parent
4fc5559e15
commit
78118c73e8
2 changed files with 51 additions and 24 deletions
|
|
@ -65,6 +65,7 @@ else:
|
||||||
|
|
||||||
|
|
||||||
debug_mode = '-d' in sys.argv[1:] or '--debug' in sys.argv[1:]
|
debug_mode = '-d' in sys.argv[1:] or '--debug' in sys.argv[1:]
|
||||||
|
quiet = '-q' in sys.argv[1:] or '--quiet' in sys.argv[1:]
|
||||||
headless: bool = False
|
headless: bool = False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -82,12 +83,16 @@ try:
|
||||||
try:
|
try:
|
||||||
import pyzbar.pyzbar as zbar # type: ignore
|
import pyzbar.pyzbar as zbar # type: ignore
|
||||||
from qreader import QReader # type: ignore
|
from qreader import QReader # type: ignore
|
||||||
except ImportError as e:
|
zbar_available = True
|
||||||
|
except Exception as e:
|
||||||
|
if not quiet:
|
||||||
print(f"""
|
print(f"""
|
||||||
ERROR: Cannot import QReader module. This problem is probably due to the missing zbar shared library.
|
WARN: Cannot import pyzbar or qreader module. This problem is probably due to the missing zbar shared library.
|
||||||
On Linux and macOS libzbar0 must be installed.
|
On Linux and macOS libzbar0 must be installed.
|
||||||
See in README.md for the installation of the libzbar0.
|
See in README.md for the installation of the libzbar0.
|
||||||
Exception: {e}\n""", file=sys.stderr)
|
Exception: {e}\n""", file=sys.stderr)
|
||||||
|
zbar_available = False
|
||||||
|
if debug_mode:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
# Types
|
# Types
|
||||||
|
|
@ -121,9 +126,9 @@ Exception: {e}\n""", file=sys.stderr)
|
||||||
|
|
||||||
TextPosition = Enum('TextPosition', ['LEFT', 'RIGHT'])
|
TextPosition = Enum('TextPosition', ['LEFT', 'RIGHT'])
|
||||||
|
|
||||||
qreader_available = True
|
cv2_available = True
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
qreader_available = False
|
cv2_available = False
|
||||||
if debug_mode:
|
if debug_mode:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
|
@ -145,10 +150,10 @@ LogLevel = IntEnum('LogLevel', ['QUIET', 'NORMAL', 'VERBOSE', 'MORE_VERBOSE', 'D
|
||||||
|
|
||||||
# Constants
|
# Constants
|
||||||
CAMERA: Final[str] = 'camera'
|
CAMERA: Final[str] = 'camera'
|
||||||
|
CV2_QRMODES: List[str] = [QRMode.CV2.name, QRMode.CV2_WECHAT.name]
|
||||||
|
|
||||||
# Global variable declaration
|
# Global variable declaration
|
||||||
verbose: IntEnum = LogLevel.NORMAL
|
verbose: IntEnum = LogLevel.NORMAL
|
||||||
quiet: bool = False
|
|
||||||
colored: bool = True
|
colored: bool = True
|
||||||
executable: bool = False
|
executable: bool = False
|
||||||
__version__: str
|
__version__: str
|
||||||
|
|
@ -174,7 +179,7 @@ def main(sys_args: list[str]) -> None:
|
||||||
# https://pyinstaller.org/en/stable/runtime-information.html#run-time-information
|
# https://pyinstaller.org/en/stable/runtime-information.html#run-time-information
|
||||||
executable = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
|
executable = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS')
|
||||||
|
|
||||||
if qreader_available and not headless:
|
if cv2_available and not headless:
|
||||||
try:
|
try:
|
||||||
tk_root = tkinter.Tk()
|
tk_root = tkinter.Tk()
|
||||||
tk_root.withdraw()
|
tk_root.withdraw()
|
||||||
|
|
@ -275,7 +280,7 @@ def parse_args(sys_args: list[str]) -> Args:
|
||||||
name = os.path.basename(sys.argv[0])
|
name = os.path.basename(sys.argv[0])
|
||||||
cmd = f"python {name}" if name.endswith('.py') else f"{name}"
|
cmd = f"python {name}" if name.endswith('.py') else f"{name}"
|
||||||
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
|
description_text = "Extracts one time password (OTP) secrets from QR codes exported by two-factor authentication (2FA) apps"
|
||||||
if qreader_available:
|
if cv2_available:
|
||||||
description_text += "\nIf no infiles are provided, a GUI window starts and QR codes are captured from the camera."
|
description_text += "\nIf no infiles are provided, a GUI window starts and QR codes are captured from the camera."
|
||||||
example_text = f"""examples:
|
example_text = f"""examples:
|
||||||
{cmd}
|
{cmd}
|
||||||
|
|
@ -288,14 +293,17 @@ def parse_args(sys_args: list[str]) -> Args:
|
||||||
description=description_text,
|
description=description_text,
|
||||||
epilog=example_text)
|
epilog=example_text)
|
||||||
arg_parser.add_argument('infile', help="""a) file or - for stdin with 'otpauth-migration://...' URLs separated by newlines, lines starting with # are ignored;
|
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 cv2_available else '+')
|
||||||
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('--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('--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'))
|
||||||
if qreader_available:
|
if cv2_available:
|
||||||
arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, metavar=('NUMBER'))
|
arg_parser.add_argument('--camera', '-C', help='camera number of system (default camera: 0)', default=0, type=int, metavar=('NUMBER'))
|
||||||
|
if not zbar_available:
|
||||||
|
arg_parser.add_argument('--qr', '-Q', help=f'QR reader (default: {QRMode.CV2.name})', type=str, choices=[QRMode.CV2.name, QRMode.CV2_WECHAT.name], default=QRMode.CV2.name)
|
||||||
|
else:
|
||||||
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('-i', '--ignore', help='ignore duplicate otps', action='store_true')
|
arg_parser.add_argument('-i', '--ignore', help='ignore duplicate otps', action='store_true')
|
||||||
arg_parser.add_argument('--no-color', '-n', help='do not use ANSI colors in console output', action='store_true')
|
arg_parser.add_argument('--no-color', '-n', help='do not use ANSI colors in console output', action='store_true')
|
||||||
|
|
@ -314,8 +322,8 @@ b) image file containing a QR code or = for stdin for an image containing a QR c
|
||||||
verbose = LogLevel.DEBUG
|
verbose = LogLevel.DEBUG
|
||||||
log_debug('Debug mode start')
|
log_debug('Debug mode start')
|
||||||
quiet = True if args.quiet else False
|
quiet = True if args.quiet else False
|
||||||
if verbose: print(f"QReader installed: {qreader_available}")
|
if verbose: print(f"QReader installed: {cv2_available}")
|
||||||
if qreader_available:
|
if cv2_available:
|
||||||
if verbose >= LogLevel.VERBOSE: 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")
|
if verbose: print(f"QR reading mode: {args.qr}\n")
|
||||||
|
|
||||||
|
|
@ -339,6 +347,7 @@ def extract_otps_from_camera(args: Args) -> Otps:
|
||||||
cam = cv2.VideoCapture(args.camera)
|
cam = cv2.VideoCapture(args.camera)
|
||||||
cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_AUTOSIZE)
|
cv2.namedWindow(WINDOW_NAME, cv2.WINDOW_AUTOSIZE)
|
||||||
|
|
||||||
|
if zbar_available:
|
||||||
qreader = QReader()
|
qreader = QReader()
|
||||||
cv2_qr = cv2.QRCodeDetector()
|
cv2_qr = cv2.QRCodeDetector()
|
||||||
cv2_qr_wechat = cv2.wechat_qrcode.WeChatQRCode()
|
cv2_qr_wechat = cv2.wechat_qrcode.WeChatQRCode()
|
||||||
|
|
@ -478,7 +487,7 @@ def cv2_handle_pressed_keys(qr_mode: QRMode, otps: Otps) -> Tuple[bool, QRMode]:
|
||||||
if len(file_name) > 0:
|
if len(file_name) > 0:
|
||||||
write_keepass_csv(file_name, otps)
|
write_keepass_csv(file_name, otps)
|
||||||
elif key == 32:
|
elif key == 32:
|
||||||
qr_mode = next_qr_mode(qr_mode)
|
qr_mode = next_valid_qr_mode(qr_mode, zbar_available)
|
||||||
if verbose >= LogLevel.MORE_VERBOSE: print(f"QR reading 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:
|
if cv2.getWindowProperty(WINDOW_NAME, cv2.WND_PROP_VISIBLE) < 1:
|
||||||
# Window close clicked
|
# Window close clicked
|
||||||
|
|
@ -526,7 +535,7 @@ def get_otp_urls_from_file(filename: str, args: Args) -> OtpUrls:
|
||||||
return lines
|
return lines
|
||||||
|
|
||||||
# could not process text file, try reading as image
|
# could not process text file, try reading as image
|
||||||
if filename != '-' and qreader_available:
|
if filename != '-' and cv2_available:
|
||||||
return convert_img_to_otp_urls(filename, args)
|
return convert_img_to_otp_urls(filename, args)
|
||||||
|
|
||||||
return []
|
return []
|
||||||
|
|
@ -799,6 +808,14 @@ def is_binary(line: str) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def next_valid_qr_mode(qr_mode: QRMode, with_zbar: bool = True) -> QRMode:
|
||||||
|
ok = False
|
||||||
|
while not ok:
|
||||||
|
qr_mode = next_qr_mode(qr_mode)
|
||||||
|
ok = True if with_zbar else qr_mode.name in CV2_QRMODES
|
||||||
|
return qr_mode
|
||||||
|
|
||||||
|
|
||||||
def next_qr_mode(qr_mode: QRMode) -> QRMode:
|
def next_qr_mode(qr_mode: QRMode) -> QRMode:
|
||||||
return QRMode((qr_mode.value + 1) % len(QRMode))
|
return QRMode((qr_mode.value + 1) % len(QRMode))
|
||||||
|
|
||||||
|
|
@ -809,6 +826,10 @@ def do_debug_checks() -> bool:
|
||||||
import cv2 # noqa: F401 # This is only a debug import
|
import cv2 # noqa: F401 # This is only a debug import
|
||||||
log_debug('Try: import numpy as np')
|
log_debug('Try: import numpy as np')
|
||||||
import numpy as np # noqa: F401 # This is only a debug import
|
import numpy as np # noqa: F401 # This is only a debug import
|
||||||
|
log_debug('Try: import pyzbar.pyzbar as zbar')
|
||||||
|
import pyzbar.pyzbar as zbar # noqa: F401 # This is only a debug import
|
||||||
|
log_debug('Try: from qreader import QReader')
|
||||||
|
from qreader import QReader # noqa: F401 # This is only a debug import
|
||||||
print(color('\nDebug checks passed', colorama.Fore.GREEN))
|
print(color('\nDebug checks passed', colorama.Fore.GREEN))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -45,11 +45,11 @@ except ImportError:
|
||||||
# ignore
|
# ignore
|
||||||
pass
|
pass
|
||||||
|
|
||||||
qreader_available: bool = extract_otp_secrets.qreader_available
|
cv2_available: bool = extract_otp_secrets.cv2_available
|
||||||
|
|
||||||
|
|
||||||
# Quickfix comment
|
# Quickfix comment
|
||||||
# @pytest.mark.skipif(sys.platform.startswith("win") or not qreader_available or sys.implementation.name == 'pypy' or sys.version_info >= (3, 10), reason="Quickfix")
|
# @pytest.mark.skipif(sys.platform.startswith("win") or not cv2 or sys.implementation.name == 'pypy' or sys.version_info >= (3, 10), reason="Quickfix")
|
||||||
|
|
||||||
|
|
||||||
def test_extract_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
def test_extract_stdout(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
|
@ -122,7 +122,7 @@ def test_extract_stdin_only_comments(capsys: pytest.CaptureFixture[str], monkeyp
|
||||||
|
|
||||||
|
|
||||||
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 cv2_available:
|
||||||
# Act
|
# Act
|
||||||
with pytest.raises(SystemExit) as e:
|
with pytest.raises(SystemExit) as e:
|
||||||
extract_otp_secrets.main(['-n', 'tests/data/empty_file.txt'])
|
extract_otp_secrets.main(['-n', 'tests/data/empty_file.txt'])
|
||||||
|
|
@ -510,7 +510,7 @@ def test_extract_verbose(verbose_level: str, color: str, capsys: pytest.CaptureF
|
||||||
|
|
||||||
def normalize_verbose_text(text: str, relaxed: bool) -> str:
|
def normalize_verbose_text(text: str, relaxed: bool) -> str:
|
||||||
normalized = re.sub('^.*version: .+$', '', text, flags=re.MULTILINE | re.IGNORECASE)
|
normalized = re.sub('^.*version: .+$', '', text, flags=re.MULTILINE | re.IGNORECASE)
|
||||||
if not qreader_available:
|
if not cv2_available:
|
||||||
normalized = normalized \
|
normalized = normalized \
|
||||||
.replace('QReader installed: True', 'QReader installed: False') \
|
.replace('QReader installed: True', 'QReader installed: False') \
|
||||||
.replace('\nQR reading mode: ZBAR\n\n', '')
|
.replace('\nQR reading mode: ZBAR\n\n', '')
|
||||||
|
|
@ -564,7 +564,7 @@ def test_extract_version(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
|
|
||||||
|
|
||||||
def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
|
def test_extract_no_arguments(capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
|
||||||
if qreader_available:
|
if cv2_available:
|
||||||
# Arrange
|
# Arrange
|
||||||
otps = read_json('example_output.json')
|
otps = read_json('example_output.json')
|
||||||
mocker.patch('extract_otp_secrets.extract_otps_from_camera', return_value=otps)
|
mocker.patch('extract_otp_secrets.extract_otps_from_camera', return_value=otps)
|
||||||
|
|
@ -648,7 +648,7 @@ class MockCam:
|
||||||
('CV2_WECHAT', 'tests/data/lena_std.tif', None),
|
('CV2_WECHAT', 'tests/data/lena_std.tif', None),
|
||||||
])
|
])
|
||||||
def test_extract_otps_from_camera(qr_reader: Optional[str], file: str, success: bool, capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
|
def test_extract_otps_from_camera(qr_reader: Optional[str], file: str, success: bool, capsys: pytest.CaptureFixture[str], mocker: MockerFixture) -> None:
|
||||||
if qreader_available:
|
if cv2_available:
|
||||||
# Arrange
|
# Arrange
|
||||||
mockCam = MockCam([file])
|
mockCam = MockCam([file])
|
||||||
mocker.patch('cv2.VideoCapture', return_value=mockCam)
|
mocker.patch('cv2.VideoCapture', return_value=mockCam)
|
||||||
|
|
@ -733,7 +733,7 @@ def test_verbose_and_quiet(capsys: pytest.CaptureFixture[str]) -> None:
|
||||||
('-n', None, False, False),
|
('-n', None, False, False),
|
||||||
])
|
])
|
||||||
def test_quiet(parameter: str, parameter_value: Optional[str], stdout_expected: bool, stderr_expected: bool, capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
|
def test_quiet(parameter: str, parameter_value: Optional[str], stdout_expected: bool, stderr_expected: bool, capsys: pytest.CaptureFixture[str], tmp_path: pathlib.Path) -> None:
|
||||||
if parameter in ['-Q', '-C'] and not qreader_available:
|
if parameter in ['-Q', '-C'] and not cv2_available:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Arrange
|
# Arrange
|
||||||
|
|
@ -1076,6 +1076,12 @@ url: This is just a text file masquerading as an image file.
|
||||||
assert captured.out == ''
|
assert captured.out == ''
|
||||||
|
|
||||||
|
|
||||||
|
def test_next_valid_qr_mode() -> None:
|
||||||
|
assert extract_otp_secrets.next_valid_qr_mode(extract_otp_secrets.QRMode.CV2, True) == extract_otp_secrets.QRMode.CV2_WECHAT
|
||||||
|
assert extract_otp_secrets.next_valid_qr_mode(extract_otp_secrets.QRMode.CV2_WECHAT, True) == extract_otp_secrets.QRMode.ZBAR
|
||||||
|
assert extract_otp_secrets.next_valid_qr_mode(extract_otp_secrets.QRMode.CV2_WECHAT, False) == extract_otp_secrets.QRMode.CV2
|
||||||
|
|
||||||
|
|
||||||
EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT = '''Name: pi@raspberrypi
|
EXPECTED_STDOUT_FROM_EXAMPLE_EXPORT = '''Name: pi@raspberrypi
|
||||||
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
Secret: 7KSQL2JTUDIS5EF65KLMRQIIGY
|
||||||
Issuer: raspberrypi
|
Issuer: raspberrypi
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue