2023-01-01 22:47:12 +01:00
# TODO rewrite
2023-01-01 23:21:29 +01:00
# Extract two-factor authentication (2FA, TFA) secrets from export QR codes of "Google Authenticator" app
2020-05-23 08:51:41 +02:00
#
# Usage:
# 1. Export the QR codes from "Google Authenticator" app
2021-02-13 16:58:30 +01:00
# 2. Read QR codes with QR code reader (e.g. with a second device)
2020-05-23 08:51:41 +02:00
# 3. Save the captured QR codes in a text file. Save each QR code on a new line. (The captured QR codes look like "otpauth-migration://offline?data=...")
# 4. Call this script with the file as input:
2022-12-30 20:37:38 +01:00
# python extract_otp_secrets.py example_export.txt
2020-05-23 08:51:41 +02:00
#
# Requirement:
# The protobuf package of Google for proto3 is required for running this script.
# pip install protobuf
#
# Optional:
# For printing QR codes, the qrcode module is required
# pip install qrcode
#
# Technical background:
# The export QR code of "Google Authenticator" contains the URL "otpauth-migration://offline?data=...".
# The data parameter is a base64 encoded proto3 message (Google Protocol Buffers).
#
2020-05-23 09:31:59 +02:00
# Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition):
2020-05-23 08:51:41 +02:00
# protoc --python_out=generated_python google_auth.proto
#
# References:
# Proto3 documentation: https://developers.google.com/protocol-buffers/docs/pythontutorial
# Template code: https://github.com/beemdevelopment/Aegis/pull/406
# Author: Scito (https://scito.ch)
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
2023-01-01 17:25:24 +01:00
from __future__ import annotations # workaround for PYTHON <= 3.10
2022-12-31 11:32:07 +01:00
2020-05-23 08:51:41 +02:00
import argparse
import base64
2022-06-29 06:18:51 +02:00
import csv
2022-12-25 11:00:15 +01:00
import fileinput
2022-06-29 06:18:51 +02:00
import json
2022-12-25 11:00:15 +01:00
import os
import re
import sys
import urllib . parse as urlparse
2023-01-02 13:40:03 +01:00
from enum import Enum , IntEnum
2022-12-31 19:11:37 +01:00
from typing import Any , List , Optional , TextIO , Tuple , Union
2022-12-30 01:44:11 +01:00
2023-01-01 17:25:24 +01:00
# workaround for PYTHON <= 3.7: compatibility
2022-12-31 19:42:53 +01:00
if sys . version_info > = ( 3 , 8 ) :
from typing import Final , TypedDict
else :
from typing_extensions import Final , TypedDict
2022-12-30 01:44:11 +01:00
2022-12-29 15:52:17 +01:00
from qrcode import QRCode # type: ignore
2022-12-25 11:00:15 +01:00
2022-12-29 21:32:19 +01:00
import protobuf_generated_python . google_auth_pb2 as pb
2023-01-01 00:13:34 +01:00
import colorama
2020-05-23 08:51:41 +02:00
2022-12-28 22:28:54 +01:00
try :
2023-01-01 19:22:46 +01:00
import cv2 # type: ignore # TODO use cv2 types if available
import numpy as np # TODO use numpy types if available
2022-12-28 22:28:54 +01:00
try :
2022-12-29 15:52:17 +01:00
import pyzbar . pyzbar as zbar # type: ignore
from qreader import QReader # type: ignore
2022-12-28 22:28:54 +01:00
except ImportError as e :
2022-12-29 11:18:57 +01:00
raise SystemExit ( f """
2022-12-28 22:28:54 +01:00
ERROR : Cannot import QReader module . This problem is probably due to the missing zbar shared library .
On Linux and macOS libzbar0 must be installed .
See in README . md for the installation of the libzbar0 .
Exception : { e } """ )
2022-12-31 11:32:07 +01:00
# Types
2023-01-01 17:25:24 +01:00
# workaround for PYTHON <= 3.9: Final[tuple[int]]
2022-12-31 11:32:07 +01:00
ColorBGR = Tuple [ int , int , int ] # RGB Color specified as Blue, Green, Red
Point = Tuple [ int , int ]
# CV2 camera capture constants
FONT : Final [ int ] = cv2 . FONT_HERSHEY_PLAIN
2023-01-02 13:40:03 +01:00
FONT_SCALE : Final [ float ] = 1.3
2022-12-31 11:32:07 +01:00
FONT_THICKNESS : Final [ int ] = 1
FONT_LINE_STYLE : Final [ int ] = cv2 . LINE_AA
2023-01-02 13:40:03 +01:00
FONT_COLOR : Final [ ColorBGR ] = ( 255 , 0 , 0 )
2023-01-01 19:22:46 +01:00
BOX_THICKNESS : Final [ int ] = 5
2023-01-01 17:25:24 +01:00
# workaround for PYTHON <= 3.7: must use () for assignments
2023-01-02 13:40:03 +01:00
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 )
2022-12-31 19:20:01 +01:00
NORMAL_COLOR : Final [ ColorBGR ] = ( 255 , 0 , 255 )
SUCCESS_COLOR : Final [ ColorBGR ] = ( 0 , 255 , 0 )
FAILURE_COLOR : Final [ ColorBGR ] = ( 0 , 0 , 255 )
2023-01-02 13:40:03 +01:00
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
2023-01-01 19:22:46 +01:00
WINDOW_NAME : Final [ str ] = " Extract OTP Secrets: Capture QR Codes from Camera "
TextPosition = Enum ( ' TextPosition ' , [ ' LEFT ' , ' RIGHT ' ] )
2022-12-31 11:32:07 +01:00
2022-12-28 22:28:54 +01:00
qreader_available = True
2022-12-29 15:52:17 +01:00
except ImportError :
2022-12-28 22:28:54 +01:00
qreader_available = False
2023-01-01 17:25:24 +01:00
# Workaround for PYTHON <= 3.9: Union[int, None] used instead of int | None
2022-12-29 23:17:31 +01:00
2022-12-29 21:29:20 +01:00
# Types
Args = argparse . Namespace
OtpUrl = str
2023-01-01 17:25:24 +01:00
# workaround for PYTHON <= 3.7: Otp = TypedDict('Otp', {'name': str, 'secret': str, 'issuer': str, 'type': str, 'counter': int | None, 'url': OtpUrl})
2022-12-29 22:34:07 +01:00
Otp = TypedDict ( ' Otp ' , { ' name ' : str , ' secret ' : str , ' issuer ' : str , ' type ' : str , ' counter ' : Union [ int , None ] , ' url ' : OtpUrl } )
2023-01-01 17:25:24 +01:00
# workaround for PYTHON <= 3.9: Otps = list[Otp]
2022-12-30 01:07:39 +01:00
Otps = List [ Otp ]
2023-01-01 17:25:24 +01:00
# workaround for PYTHON <= 3.9: OtpUrls = list[OtpUrl]
2022-12-30 01:07:39 +01:00
OtpUrls = List [ OtpUrl ]
2022-12-29 21:29:20 +01:00
2023-01-01 22:47:12 +01:00
QRMode = Enum ( ' QRMode ' , [ ' ZBAR ' , ' QREADER ' , ' QREADER_DEEP ' , ' CV2 ' , ' CV2_WECHAT ' ] , start = 0 )
2023-01-02 13:40:03 +01:00
LogLevel = IntEnum ( ' LogLevel ' , [ ' QUIET ' , ' NORMAL ' , ' VERBOSE ' , ' MORE_VERBOSE ' , ' DEBUG ' ] , start = - 1 )
2022-12-31 12:43:17 +01:00
2022-12-29 21:29:20 +01:00
2022-12-31 11:32:07 +01:00
# Constants
CAMERA : Final [ str ] = ' camera '
2022-12-29 21:29:20 +01:00
# Global variable declaration
2023-01-02 13:40:03 +01:00
verbose : IntEnum = LogLevel . NORMAL
2022-12-29 21:29:20 +01:00
quiet : bool = False
2023-01-01 00:13:34 +01:00
colored : bool = True
2022-09-03 16:12:28 +02:00
2022-12-29 16:30:18 +01:00
def sys_main ( ) - > None :
2022-09-04 08:37:03 +02:00
main ( sys . argv [ 1 : ] )
2020-05-23 08:51:41 +02:00
2022-09-03 16:12:28 +02:00
2022-12-29 16:30:18 +01:00
def main ( sys_args : list [ str ] ) - > None :
2022-12-18 19:24:07 +01:00
# allow to use sys.stdout with with (avoid closing)
2022-12-29 16:30:18 +01:00
sys . stdout . close = lambda : None # type: ignore
2022-12-31 19:52:09 +01:00
# set encoding to utf-8, needed for Windows
2022-12-31 20:19:21 +01:00
try :
2023-01-01 00:17:27 +01:00
sys . stdout . reconfigure ( encoding = ' utf-8 ' ) # type: ignore
sys . stderr . reconfigure ( encoding = ' utf-8 ' ) # type: ignore
2022-12-31 20:19:21 +01:00
except AttributeError : # '_io.StringIO' object has no attribute 'reconfigure'
# StringIO in tests do not have all attributes, ignore it
pass
2022-12-26 23:39:13 +01:00
2022-09-04 08:37:03 +02:00
args = parse_args ( sys_args )
2022-09-03 16:12:28 +02:00
2023-01-01 00:13:34 +01:00
if colored :
colorama . just_fix_windows_console ( )
2022-09-04 08:37:03 +02:00
otps = extract_otps ( args )
write_csv ( args , otps )
2022-12-04 12:23:39 +01:00
write_keepass_csv ( args , otps )
2022-09-04 08:37:03 +02:00
write_json ( args , otps )
2020-05-23 08:51:41 +02:00
2022-09-03 16:12:28 +02:00
2022-12-29 21:29:20 +01:00
def parse_args ( sys_args : list [ str ] ) - > Args :
2023-01-01 00:13:34 +01:00
global verbose , quiet , colored
2023-01-02 13:40:03 +01:00
description_text = " Extracts one time password (OTP) secrets from export QR codes from two-factor authentication (2FA) apps "
2022-12-28 22:28:54 +01:00
if qreader_available :
2023-01-02 13:40:03 +01:00
description_text + = " \n If no infiles are provided, a GUI window starts and QR codes are captured from the camera. "
2022-12-28 22:28:54 +01:00
example_text = """ examples:
2022-12-30 20:37:38 +01:00
python extract_otp_secrets . py
python extract_otp_secrets . py example_ * . txt
python extract_otp_secrets . py - < example_export . txt
python extract_otp_secrets . py - - csv - example_ * . png | tail - n + 2
python extract_otp_secrets . py = < example_export . png """
2022-12-26 23:39:13 +01:00
2022-12-31 18:00:49 +01:00
arg_parser = argparse . ArgumentParser ( formatter_class = lambda prog : argparse . RawTextHelpFormatter ( prog , max_help_position = 32 ) ,
2022-12-28 22:28:54 +01:00
description = description_text ,
2022-12-26 23:39:13 +01:00
epilog = example_text )
2022-12-28 22:28:54 +01:00
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 ' + ' )
if qreader_available :
2023-01-01 00:13:34 +01:00
arg_parser . add_argument ( ' --camera ' , ' -C ' , help = ' camera number of system (default camera: 0) ' , default = 0 , type = int , metavar = ( ' NUMBER ' ) )
2022-12-31 15:41:37 +01:00
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 )
2022-12-18 19:24:07 +01:00
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 ' ) )
2022-12-24 01:59:35 +01:00
arg_parser . add_argument ( ' --keepass ' , ' -k ' , help = ' export totp/hotp csv file(s) for KeePass, - for stdout ' , metavar = ( ' FILE ' ) )
arg_parser . add_argument ( ' --printqr ' , ' -p ' , help = ' print QR code(s) as text to the terminal (requires qrcode module) ' , action = ' store_true ' )
arg_parser . add_argument ( ' --saveqr ' , ' -s ' , help = ' save QR code(s) as images to the given folder (requires qrcode module) ' , metavar = ( ' DIR ' ) )
2023-01-01 00:13:34 +01:00
arg_parser . add_argument ( ' --no-color ' , ' -n ' , help = ' do not use ANSI colors in console output ' , action = ' store_true ' )
2022-12-18 21:34:24 +01:00
output_group = arg_parser . add_mutually_exclusive_group ( )
output_group . add_argument ( ' --verbose ' , ' -v ' , help = ' verbose output ' , action = ' count ' )
output_group . add_argument ( ' --quiet ' , ' -q ' , help = ' no stdout output, except output set by - ' , action = ' store_true ' )
2022-09-03 14:31:09 +02:00
args = arg_parser . parse_args ( sys_args )
2022-12-18 21:34:24 +01:00
if args . csv == ' - ' or args . json == ' - ' or args . keepass == ' - ' :
args . quiet = args . q = True
2022-12-28 22:28:54 +01:00
2023-01-02 13:40:03 +01:00
verbose = args . verbose if args . verbose else LogLevel . NORMAL
2022-12-28 22:28:54 +01:00
quiet = True if args . quiet else False
2023-01-01 00:13:34 +01:00
colored = not args . no_color
2022-12-29 16:30:18 +01:00
if verbose : print ( f " QReader installed: { qreader_available } " )
2022-12-31 15:41:37 +01:00
if qreader_available :
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . VERBOSE : print ( f " CV2 version: { cv2 . __version__ } " )
2022-12-31 15:41:37 +01:00
if verbose : print ( f " QR reading mode: { args . qr } \n " )
2022-12-28 22:28:54 +01:00
2022-09-03 14:31:09 +02:00
return args
2022-09-03 16:12:28 +02:00
2022-12-29 21:29:20 +01:00
def extract_otps ( args : Args ) - > Otps :
2022-12-28 22:28:54 +01:00
if not args . infile :
return extract_otps_from_camera ( args )
else :
return extract_otps_from_files ( args )
2022-09-03 14:31:09 +02:00
2022-12-28 22:28:54 +01:00
2022-12-31 11:32:07 +01:00
def get_color ( new_otps_count : int , otp_url : str ) - > ColorBGR :
if new_otps_count :
return SUCCESS_COLOR
else :
if otp_url :
return FAILURE_COLOR
else :
return NORMAL_COLOR
2023-01-01 19:22:46 +01:00
# TODO use cv2 types if available
def cv2_draw_box ( img : Any , raw_pts : Any , color : ColorBGR ) - > Any :
pts = np . array ( [ raw_pts ] , np . int32 )
pts = pts . reshape ( ( - 1 , 1 , 2 ) )
cv2 . polylines ( img , [ pts ] , True , color , BOX_THICKNESS )
return pts
# TODO use cv2 types if available
2023-01-02 13:40:03 +01:00
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 )
2023-01-02 14:16:30 +01:00
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 ] + ' . '
2023-01-02 13:40:03 +01:00
text_dim , _ = cv2 . getTextSize ( out_text , FONT , FONT_SCALE , FONT_THICKNESS )
2023-01-01 19:22:46 +01:00
if position == TextPosition . LEFT :
2023-01-02 13:40:03 +01:00
pos = BORDER , START_Y + line_number * FONT_DY
2023-01-01 19:22:46 +01:00
else :
2023-01-02 13:40:03 +01:00
pos = window_dim [ WINDOW_WIDTH ] - text_dim [ TEXT_WIDTH ] - BORDER , START_Y + line_number * FONT_DY
2023-01-01 19:22:46 +01:00
2023-01-02 13:40:03 +01:00
cv2 . putText ( img , out_text , pos , FONT , FONT_SCALE , color , FONT_THICKNESS , FONT_LINE_STYLE )
2023-01-01 19:22:46 +01:00
2022-12-29 21:29:20 +01:00
def extract_otps_from_camera ( args : Args ) - > Otps :
2022-12-28 22:28:54 +01:00
if verbose : print ( " Capture QR codes from camera " )
2022-12-29 21:29:20 +01:00
otp_urls : OtpUrls = [ ]
otps : Otps = [ ]
2022-12-28 22:28:54 +01:00
2022-12-31 12:43:17 +01:00
qr_mode = QRMode [ args . qr ]
2022-12-28 22:28:54 +01:00
2023-01-01 00:13:34 +01:00
cam = cv2 . VideoCapture ( args . camera )
2023-01-01 19:22:46 +01:00
cv2 . namedWindow ( WINDOW_NAME , cv2 . WINDOW_AUTOSIZE )
2022-12-28 22:28:54 +01:00
2022-12-31 15:41:37 +01:00
qreader = QReader ( )
cv2_qr = cv2 . QRCodeDetector ( )
cv2_qr_wechat = cv2 . wechat_qrcode . WeChatQRCode ( )
2022-12-28 22:28:54 +01:00
while True :
success , img = cam . read ( )
2022-12-31 15:41:37 +01:00
new_otps_count = 0
2022-12-28 22:28:54 +01:00
if not success :
2023-01-01 19:55:28 +01:00
log_error ( " Failed to capture image from camera " )
2022-12-28 22:28:54 +01:00
break
2023-01-01 15:12:24 +01:00
try :
2023-01-01 22:47:12 +01:00
if qr_mode in [ QRMode . QREADER , QRMode . QREADER_DEEP ] :
2023-01-01 19:22:46 +01:00
found , bbox = qreader . detect ( img )
2023-01-01 22:47:12 +01:00
if qr_mode == QRMode . QREADER_DEEP :
2023-01-01 15:12:24 +01:00
otp_url = qreader . detect_and_decode ( img , True )
elif qr_mode == QRMode . QREADER :
otp_url = qreader . decode ( img , bbox ) if found else None
2022-12-31 15:41:37 +01:00
if otp_url :
new_otps_count = extract_otps_from_otp_url ( otp_url , otp_urls , otps , args )
2023-01-01 15:12:24 +01:00
if found :
2023-01-01 19:22:46 +01:00
cv2 . rectangle ( img , ( bbox [ 0 ] , bbox [ 1 ] ) , ( bbox [ 2 ] , bbox [ 3 ] ) , get_color ( new_otps_count , otp_url ) , BOX_THICKNESS )
2023-01-01 15:12:24 +01:00
elif qr_mode == QRMode . ZBAR :
for qrcode in zbar . decode ( img ) :
otp_url = qrcode . data . decode ( ' utf-8 ' )
new_otps_count = extract_otps_from_otp_url ( otp_url , otp_urls , otps , args )
2023-01-01 19:22:46 +01:00
cv2_draw_box ( img , [ qrcode . polygon ] , get_color ( new_otps_count , otp_url ) )
elif qr_mode in [ QRMode . CV2 , QRMode . CV2_WECHAT ] :
2023-01-01 15:12:24 +01:00
if QRMode . CV2 :
otp_url , raw_pts , _ = cv2_qr . detectAndDecode ( img )
else :
otp_url , raw_pts = cv2_qr_wechat . detectAndDecode ( img )
if raw_pts is not None :
if otp_url :
new_otps_count = extract_otps_from_otp_url ( otp_url , otp_urls , otps , args )
2023-01-01 19:22:46 +01:00
cv2_draw_box ( img , raw_pts , get_color ( new_otps_count , otp_url ) )
2023-01-01 15:12:24 +01:00
else :
2023-01-01 19:55:28 +01:00
abort ( f " Invalid QReader mode: { qr_mode . name } " )
2023-01-01 15:12:24 +01:00
except Exception as e :
log_error ( f ' An error occured during QR detection and decoding for QR reader { qr_mode } . Changed to the next QR reader. ' , e )
qr_mode = next_qr_mode ( qr_mode )
2023-01-01 15:21:18 +01:00
continue
2022-12-28 22:28:54 +01:00
2023-01-02 13:40:03 +01:00
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 )
2022-12-28 22:28:54 +01:00
2023-01-02 13:40:03 +01:00
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 )
2022-12-28 22:28:54 +01:00
2023-01-01 19:22:46 +01:00
cv2 . imshow ( WINDOW_NAME , img )
2022-12-28 22:28:54 +01:00
2023-01-02 13:40:03 +01:00
quit , qr_mode = cv2_handle_pressed_keys ( qr_mode )
if quit :
2022-12-30 01:22:12 +01:00
break
2022-12-28 22:28:54 +01:00
cam . release ( )
cv2 . destroyAllWindows ( )
return otps
2023-01-02 13:40:03 +01:00
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
2022-12-31 11:32:07 +01:00
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. '''
2023-01-02 13:40:03 +01:00
if otp_url and verbose > = LogLevel . VERBOSE : print ( otp_url )
2022-12-31 11:32:07 +01:00
if not otp_url :
return 0
if otp_url not in otp_urls :
new_otps_count = extract_otp_from_otp_url ( otp_url , otps , len ( otp_urls ) , CAMERA , args )
if new_otps_count :
otp_urls . append ( otp_url )
if verbose : print ( f " Extracted { new_otps_count } otp { ' s ' [ : len ( otps ) != 1 ] } . { len ( otps ) } otp { ' s ' [ : len ( otps ) != 1 ] } from { len ( otp_urls ) } QR code { ' s ' [ : len ( otp_urls ) != 1 ] } extracted " )
return new_otps_count
return - 1
2022-12-28 22:28:54 +01:00
2022-12-29 21:29:20 +01:00
def extract_otps_from_files ( args : Args ) - > Otps :
otps : Otps = [ ]
2022-09-03 14:31:09 +02:00
2022-12-31 11:32:07 +01:00
files_count = urls_count = otps_count = 0
2022-12-28 22:28:54 +01:00
if verbose : print ( f " Input files: { args . infile } " )
2022-12-24 04:48:12 +01:00
for infile in args . infile :
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . MORE_VERBOSE : log_verbose ( f " Processing infile { infile } " )
2022-12-31 11:32:07 +01:00
files_count + = 1
2022-12-31 15:41:37 +01:00
for line in get_otp_urls_from_file ( infile , args ) :
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . MORE_VERBOSE : log_verbose ( line )
2022-12-24 04:48:12 +01:00
if line . startswith ( ' # ' ) or line == ' ' : continue
2022-12-31 11:32:07 +01:00
urls_count + = 1
otps_count + = extract_otp_from_otp_url ( line , otps , urls_count , infile , args )
2023-01-02 13:40:03 +01:00
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 ] } " )
2022-09-03 14:31:09 +02:00
return otps
2022-09-03 16:12:28 +02:00
2022-12-31 15:41:37 +01:00
def get_otp_urls_from_file ( filename : str , args : Args ) - > OtpUrls :
2022-12-24 15:30:17 +01:00
# stdin stream cannot be rewinded, thus distinguish, use - for utf-8 stdin and = for binary image stdin
if filename != ' = ' :
check_file_exists ( filename )
lines = read_lines_from_text_file ( filename )
2022-12-26 18:31:09 +01:00
if lines or filename == ' - ' :
2022-12-24 15:30:17 +01:00
return lines
2022-12-21 16:47:31 -08:00
2022-12-24 02:56:40 +01:00
# could not process text file, try reading as image
2022-12-28 22:28:54 +01:00
if filename != ' - ' and qreader_available :
2023-01-02 13:40:03 +01:00
return convert_img_to_otp_urls ( filename , args )
2022-12-28 22:28:54 +01:00
return [ ]
2022-12-24 02:56:40 +01:00
2022-12-24 04:19:43 +01:00
2022-12-29 21:29:20 +01:00
def read_lines_from_text_file ( filename : str ) - > list [ str ] :
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . DEBUG : print ( f " Reading lines of { filename } " )
2023-01-01 17:25:24 +01:00
# workaround for PYTHON <= 3.9 support encoding
2023-01-01 00:41:11 +01:00
if sys . version_info > = ( 3 , 10 ) :
finput = fileinput . input ( filename , encoding = ' utf-8 ' )
else :
finput = fileinput . input ( filename )
2022-12-24 15:30:17 +01:00
try :
lines = [ ]
for line in ( line . strip ( ) for line in finput ) :
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . DEBUG : log_verbose ( line )
2022-12-24 15:30:17 +01:00
if is_binary ( line ) :
2023-01-01 00:13:34 +01:00
abort ( " Binary input was given in stdin, please use = instead of - as infile argument for images. " )
2022-12-24 15:30:17 +01:00
# unfortunately yield line leads to random test fails
lines . append ( line )
2022-12-26 18:31:09 +01:00
if not lines :
2023-01-01 00:13:34 +01:00
log_warn ( f " { filename . replace ( ' - ' , ' stdin ' ) } is empty " )
2023-01-01 15:12:24 +01:00
except UnicodeDecodeError as e :
2022-12-24 15:30:17 +01:00
if filename == ' - ' :
2023-01-01 00:13:34 +01:00
abort ( " Unable to open text file form stdin. "
2023-01-01 15:12:24 +01:00
" In case you want read an image file from stdin, you must use ' = ' instead of ' - ' . " , e )
2022-12-29 15:52:17 +01:00
else : # The file is probably an image, process below
2022-12-29 21:29:20 +01:00
return [ ]
2022-12-24 15:30:17 +01:00
finally :
finput . close ( )
2022-12-29 21:29:20 +01:00
return lines
2022-12-21 16:47:31 -08:00
2022-12-24 04:19:43 +01:00
2022-12-31 11:32:07 +01:00
def extract_otp_from_otp_url ( otpauth_migration_url : str , otps : Otps , urls_count : int , infile : str , args : Args ) - > int :
payload = get_payload_from_otp_url ( otpauth_migration_url , urls_count , infile )
if not payload :
return 0
2022-12-28 22:28:54 +01:00
2022-12-31 11:32:07 +01:00
new_otps_count = 0
2022-12-28 22:28:54 +01:00
# pylint: disable=no-member
for raw_otp in payload . otp_parameters :
2022-12-31 11:32:07 +01:00
new_otps_count + = 1
2023-01-01 00:13:34 +01:00
if verbose : print ( f " \n { len ( otps ) + 1 } . Secret " )
2022-12-28 22:28:54 +01:00
secret = convert_secret_from_bytes_to_base32_str ( raw_otp . secret )
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . DEBUG : log_debug ( ' OTP enum type: ' , get_enum_name_by_number ( raw_otp , ' type ' ) )
2022-12-28 22:28:54 +01:00
otp_type = get_otp_type_str_from_code ( raw_otp . type )
otp_url = build_otp_url ( secret , raw_otp )
2022-12-29 21:29:20 +01:00
otp : Otp = {
2022-12-28 22:28:54 +01:00
" name " : raw_otp . name ,
" secret " : secret ,
" issuer " : raw_otp . issuer ,
" type " : otp_type ,
" counter " : raw_otp . counter if raw_otp . type == 1 else None ,
" url " : otp_url
}
otps . append ( otp )
if not quiet :
print_otp ( otp )
if args . printqr :
print_qr ( args , otp_url )
if args . saveqr :
2022-12-31 11:32:07 +01:00
save_qr ( otp , args , len ( otps ) )
2022-12-28 22:28:54 +01:00
if not quiet :
print ( )
2022-12-31 11:32:07 +01:00
return new_otps_count
2022-12-28 22:28:54 +01:00
2023-01-02 13:40:03 +01:00
def convert_img_to_otp_urls ( filename : str , args : Args ) - > OtpUrls :
2022-12-28 22:28:54 +01:00
if verbose : print ( f " Reading image { filename } " )
2022-12-24 15:30:17 +01:00
try :
if filename != ' = ' :
2022-12-28 22:28:54 +01:00
img = cv2 . imread ( filename )
2022-12-24 15:30:17 +01:00
else :
try :
stdin = sys . stdin . buffer . read ( )
except AttributeError :
# Workaround for pytest, since pytest cannot monkeypatch sys.stdin.buffer
2022-12-29 21:29:20 +01:00
stdin = sys . stdin . read ( ) # type: ignore # Workaround for pytest fixtures
2022-12-26 18:31:09 +01:00
if not stdin :
2023-01-01 00:13:34 +01:00
log_warn ( " stdin is empty " )
2022-12-24 03:25:10 +01:00
try :
2023-01-01 19:22:46 +01:00
img_array = np . frombuffer ( stdin , dtype = ' uint8 ' )
2022-12-24 15:30:17 +01:00
except TypeError as e :
2023-01-01 19:22:46 +01:00
abort ( " Cannot read binary stdin buffer. " , e )
2022-12-26 18:31:09 +01:00
if not img_array . size :
return [ ]
2022-12-28 22:28:54 +01:00
img = cv2 . imdecode ( img_array , cv2 . IMREAD_UNCHANGED )
2022-12-21 16:47:31 -08:00
2022-12-28 22:28:54 +01:00
if img is None :
2023-01-01 00:13:34 +01:00
abort ( f " Unable to open file for reading. \n input file: { filename } " )
2022-12-24 15:30:17 +01:00
2022-12-31 15:41:37 +01:00
qr_mode = QRMode [ args . qr ]
2023-01-02 13:40:03 +01:00
otp_urls = decode_qr_img_otp_urls ( img , qr_mode )
2022-12-31 15:41:37 +01:00
if len ( otp_urls ) == 0 :
2023-01-01 00:13:34 +01:00
abort ( f " Unable to read QR Code from file. \n input file: { filename } " )
2022-12-24 15:30:17 +01:00
except Exception as e :
2023-01-01 15:12:24 +01:00
abort ( f " Encountered exception \n input file: { filename } " , e )
2022-12-31 15:41:37 +01:00
return otp_urls
2022-12-21 16:47:31 -08:00
2023-01-02 13:40:03 +01:00
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
2023-01-01 17:25:24 +01:00
# workaround for PYTHON <= 3.9 use: pb.MigrationPayload | None
2022-12-31 11:32:07 +01:00
def get_payload_from_otp_url ( otp_url : str , i : int , source : str ) - > Optional [ pb . MigrationPayload ] :
2023-01-02 13:40:03 +01:00
''' 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
2022-12-31 11:32:07 +01:00
parsed_url = urlparse . urlparse ( otp_url )
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . DEBUG : log_debug ( f " parsed_url= { parsed_url } " )
2022-12-16 13:10:22 +01:00
try :
2022-12-25 11:00:15 +01:00
params = urlparse . parse_qs ( parsed_url . query , strict_parsing = True )
2023-01-01 17:25:24 +01:00
except Exception : # workaround for PYTHON <= 3.10
2022-12-29 21:29:20 +01:00
params = { }
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . DEBUG : log_debug ( f " querystring params= { params } " )
2022-09-04 08:37:03 +02:00
if ' data ' not in params :
2023-01-01 00:13:34 +01:00
log_error ( f " could not parse query parameter in input url \n source: { source } \n url: { otp_url } " )
2022-12-31 11:32:07 +01:00
return None
2022-09-07 21:58:03 +02:00
data_base64 = params [ ' data ' ] [ 0 ]
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . DEBUG : log_debug ( f " data_base64= { data_base64 } " )
2022-09-07 21:58:03 +02:00
data_base64_fixed = data_base64 . replace ( ' ' , ' + ' )
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . DEBUG : log_debug ( f " data_base64_fixed= { data_base64_fixed } " )
2022-09-07 21:58:03 +02:00
data = base64 . b64decode ( data_base64_fixed , validate = True )
2022-12-29 21:32:19 +01:00
payload = pb . MigrationPayload ( )
2022-12-16 12:43:32 +01:00
try :
payload . ParseFromString ( data )
2023-01-01 15:12:24 +01:00
except Exception as e :
2023-01-01 00:13:34 +01:00
abort ( f " Cannot decode otpauth-migration migration payload. \n "
2023-01-01 15:12:24 +01:00
f " data= { data_base64 } " , e )
2023-01-02 13:40:03 +01:00
if verbose > = LogLevel . DEBUG : log_debug ( f " \n { i } . Payload Line " , payload , sep = ' \n ' )
2022-09-04 08:37:03 +02:00
return payload
2023-01-02 13:40:03 +01:00
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 \n source: { source } \n input: { otp_url } "
if source == CAMERA :
log_warn ( f " { msg } " )
return False
else :
log_warn ( f " { msg } \n Maybe a wrong file was given " )
return True
2022-09-04 08:37:03 +02:00
# https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf
2022-12-29 21:29:20 +01:00
def get_enum_name_by_number ( parent : Any , field_name : str ) - > str :
2022-09-04 08:37:03 +02:00
field_value = getattr ( parent , field_name )
2022-12-29 21:29:20 +01:00
return parent . DESCRIPTOR . fields_by_name [ field_name ] . enum_type . values_by_number . get ( field_value ) . name # type: ignore # generic code
2022-09-04 08:37:03 +02:00
2022-12-29 21:29:20 +01:00
def get_otp_type_str_from_code ( otp_type : int ) - > str :
2022-12-04 12:23:39 +01:00
return ' totp ' if otp_type == 2 else ' hotp '
2022-12-29 21:29:20 +01:00
def convert_secret_from_bytes_to_base32_str ( bytes : bytes ) - > str :
2022-09-04 08:37:03 +02:00
return str ( base64 . b32encode ( bytes ) , ' utf-8 ' ) . replace ( ' = ' , ' ' )
2022-12-29 21:32:19 +01:00
def build_otp_url ( secret : str , raw_otp : pb . MigrationPayload . OtpParameters ) - > str :
2022-09-04 08:37:03 +02:00
url_params = { ' secret ' : secret }
2022-12-29 21:29:20 +01:00
if raw_otp . type == 1 : url_params [ ' counter ' ] = str ( raw_otp . counter )
2022-09-04 08:37:03 +02:00
if raw_otp . issuer : url_params [ ' issuer ' ] = raw_otp . issuer
2022-12-28 22:28:54 +01:00
otp_url = f " otpauth:// { get_otp_type_str_from_code ( raw_otp . type ) } / { urlparse . quote ( raw_otp . name ) } ? " + urlparse . urlencode ( url_params )
2022-09-04 08:37:03 +02:00
return otp_url
2022-12-29 21:29:20 +01:00
def print_otp ( otp : Otp ) - > None :
2022-12-28 22:28:54 +01:00
print ( f " Name: { otp [ ' name ' ] } " )
print ( f " Secret: { otp [ ' secret ' ] } " )
if otp [ ' issuer ' ] : print ( f " Issuer: { otp [ ' issuer ' ] } " )
print ( f " Type: { otp [ ' type ' ] } " )
2022-12-04 12:23:39 +01:00
if otp [ ' type ' ] == ' hotp ' :
2022-12-28 22:28:54 +01:00
print ( f " Counter: { otp [ ' counter ' ] } " )
2022-09-04 08:37:03 +02:00
if verbose :
print ( otp [ ' url ' ] )
2022-12-29 21:29:20 +01:00
def save_qr ( otp : Otp , args : Args , j : int ) - > str :
2022-09-09 13:13:13 +02:00
dir = args . saveqr
2022-12-25 11:00:15 +01:00
if not ( os . path . exists ( dir ) ) : os . makedirs ( dir , exist_ok = True )
pattern = re . compile ( r ' [ \ W_]+ ' )
2022-09-09 13:08:35 +02:00
file_otp_name = pattern . sub ( ' ' , otp [ ' name ' ] )
file_otp_issuer = pattern . sub ( ' ' , otp [ ' issuer ' ] )
2022-12-28 22:28:54 +01:00
save_qr_file ( args , otp [ ' url ' ] , f " { dir } / { j } - { file_otp_name } { ' - ' + file_otp_issuer if file_otp_issuer else ' ' } .png " )
2022-12-31 11:32:07 +01:00
return file_otp_name
2022-09-04 08:37:03 +02:00
2022-12-29 21:29:20 +01:00
def save_qr_file ( args : Args , otp_url : OtpUrl , name : str ) - > None :
2022-09-04 08:37:03 +02:00
qr = QRCode ( )
2022-12-29 21:29:20 +01:00
qr . add_data ( otp_url )
2022-09-04 08:37:03 +02:00
img = qr . make_image ( fill_color = ' black ' , back_color = ' white ' )
2022-12-28 22:28:54 +01:00
if verbose : print ( f " Saving to { name } " )
2022-09-04 08:37:03 +02:00
img . save ( name )
2022-12-29 21:29:20 +01:00
def print_qr ( args : Args , otp_url : str ) - > None :
2022-09-04 08:37:03 +02:00
qr = QRCode ( )
2022-12-29 21:29:20 +01:00
qr . add_data ( otp_url )
2022-09-04 08:37:03 +02:00
qr . print_ascii ( )
2022-12-29 21:29:20 +01:00
def write_csv ( args : Args , otps : Otps ) - > None :
2022-09-03 14:31:09 +02:00
if args . csv and len ( otps ) > 0 :
2022-12-18 19:24:07 +01:00
with open_file_or_stdout_for_csv ( args . csv ) as outfile :
2022-09-03 14:31:09 +02:00
writer = csv . DictWriter ( outfile , otps [ 0 ] . keys ( ) )
writer . writeheader ( )
writer . writerows ( otps )
2022-12-28 22:28:54 +01:00
if not quiet : print ( f " Exported { len ( otps ) } otp { ' s ' [ : len ( otps ) != 1 ] } to csv { args . csv } " )
2022-12-04 12:23:39 +01:00
2022-12-29 21:29:20 +01:00
def write_keepass_csv ( args : Args , otps : Otps ) - > None :
2022-12-04 12:23:39 +01:00
if args . keepass and len ( otps ) > 0 :
has_totp = has_otp_type ( otps , ' totp ' )
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 " )
if has_totp :
2023-01-02 13:40:03 +01:00
count_totp_entries = write_keepass_totp_csv ( otp_filename_totp , otps )
2022-12-04 12:23:39 +01:00
if has_hotp :
2023-01-02 13:40:03 +01:00
count_hotp_entries = write_keepass_htop_csv ( otp_filename_hotp , otps )
2022-12-04 12:23:39 +01:00
if not quiet :
2023-01-02 13:40:03 +01:00
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
2022-09-03 14:31:09 +02:00
2022-09-03 16:12:28 +02:00
2022-12-29 21:29:20 +01:00
def write_json ( args : Args , otps : Otps ) - > None :
2022-09-03 14:31:09 +02:00
if args . json :
2022-12-18 19:24:07 +01:00
with open_file_or_stdout ( args . json ) as outfile :
2022-09-03 16:12:28 +02:00
json . dump ( otps , outfile , indent = 4 )
2022-12-28 22:28:54 +01:00
if not quiet : print ( f " Exported { len ( otps ) } otp { ' s ' [ : len ( otps ) != 1 ] } to json { args . json } " )
2022-12-04 12:23:39 +01:00
2022-12-29 21:29:20 +01:00
def has_otp_type ( otps : Otps , otp_type : str ) - > bool :
2022-12-04 12:23:39 +01:00
for otp in otps :
if otp [ ' type ' ] == otp_type :
return True
return False
2022-12-29 21:29:20 +01:00
def add_pre_suffix ( file : str , pre_suffix : str ) - > str :
2022-12-04 12:23:39 +01:00
''' filename.ext, pre -> filename.pre.ext '''
2022-12-25 11:00:15 +01:00
name , ext = os . path . splitext ( file )
2022-12-04 12:23:39 +01:00
return name + " . " + pre_suffix + ( ext if ext else " " )
2022-09-03 14:31:09 +02:00
2022-09-03 16:12:28 +02:00
2022-12-29 21:29:20 +01:00
def open_file_or_stdout ( filename : str ) - > TextIO :
2022-12-18 19:24:07 +01:00
''' stdout is denoted as " - " .
Note : Set before the following line :
sys . stdout . close = lambda : None '''
2022-12-19 16:39:28 +01:00
return open ( filename , " w " , encoding = ' utf-8 ' ) if filename != ' - ' else sys . stdout
2022-12-18 19:24:07 +01:00
2022-12-29 21:29:20 +01:00
def open_file_or_stdout_for_csv ( filename : str ) - > TextIO :
2022-12-18 19:24:07 +01:00
''' stdout is denoted as " - " .
newline = ' '
Note : Set before the following line :
sys . stdout . close = lambda : None '''
2022-12-19 16:39:28 +01:00
return open ( filename , " w " , encoding = ' utf-8 ' , newline = ' ' ) if filename != ' - ' else sys . stdout
2022-12-18 19:24:07 +01:00
2022-12-29 21:29:20 +01:00
def check_file_exists ( filename : str ) - > None :
2022-12-25 11:00:15 +01:00
if filename != ' - ' and not os . path . isfile ( filename ) :
2023-01-01 00:13:34 +01:00
abort ( f " Input file provided is non-existent or not a file. "
2022-12-29 15:52:17 +01:00
f " \n input file: { filename } " )
2022-12-24 01:59:35 +01:00
2022-12-24 04:19:43 +01:00
2022-12-29 21:29:20 +01:00
def is_binary ( line : str ) - > bool :
2022-12-24 15:30:17 +01:00
try :
line . startswith ( ' # ' )
return False
except ( UnicodeDecodeError , AttributeError , TypeError ) :
return True
2022-12-24 04:19:43 +01:00
2023-01-01 15:12:24 +01:00
def next_qr_mode ( qr_mode : QRMode ) - > QRMode :
return QRMode ( ( qr_mode . value + 1 ) % len ( QRMode ) )
2023-01-02 13:40:03 +01:00
# workaround for PYTHON <= 3.9 use: BaseException | None
def log_debug ( * values : object , sep : Optional [ str ] = ' ' ) - > None :
if colored :
print ( f " { colorama . Fore . CYAN } \n DEBUG: { str ( values [ 0 ] ) } " , * values [ 1 : ] , colorama . Fore . RESET , sep )
else :
print ( f " \n DEBUG: { 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 ) )
2023-01-01 17:45:50 +01:00
# workaround for PYTHON <= 3.9 use: BaseException | None
def log_warn ( msg : str , exception : Optional [ BaseException ] = None ) - > None :
exception_text = " \n Exception: "
2023-01-02 13:40:03 +01:00
eprint ( color ( f " \n WARN: { msg } { ( exception_text + str ( exception ) ) if exception else ' ' } " , colorama . Fore . RED ) )
2023-01-01 15:12:24 +01:00
2023-01-01 00:13:34 +01:00
2023-01-01 17:45:50 +01:00
# workaround for PYTHON <= 3.9 use: BaseException | None
def log_error ( msg : str , exception : Optional [ BaseException ] = None ) - > None :
exception_text = " \n Exception: "
2023-01-02 13:40:03 +01:00
eprint ( color ( f " \n ERROR: { 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 ' ' } "
2023-01-01 00:13:34 +01:00
2023-01-02 13:40:03 +01:00
def eprint ( * values : object , * * kwargs : Any ) - > None :
2022-12-18 19:24:07 +01:00
''' Print to stderr. '''
2023-01-02 13:40:03 +01:00
print ( * values , file = sys . stderr , * * kwargs )
2022-12-18 19:24:07 +01:00
2023-01-01 19:22:46 +01:00
2023-01-01 17:45:50 +01:00
# workaround for PYTHON <= 3.9 use: BaseException | None
def abort ( msg : str , exception : Optional [ BaseException ] = None ) - > None :
2023-01-01 15:12:24 +01:00
log_error ( msg , exception )
2022-12-29 11:18:57 +01:00
sys . exit ( 1 )
2022-09-03 14:31:09 +02:00
if __name__ == ' __main__ ' :
sys_main ( )