@ -65,11 +65,20 @@ else:
debug_mode = ' -d ' in sys . argv [ 1 : ] or ' --debug ' in sys . argv [ 1 : ]
headless : bool = False
try :
import cv2 # type: ignore # TODO use cv2 types if available
import numpy as np # TODO use numpy types if available
try :
import tkinter
import tkinter . filedialog
import tkinter . messagebox
except ImportError :
headless = True
try :
import pyzbar . pyzbar as zbar # type: ignore
from qreader import QReader # type: ignore
@ -143,6 +152,7 @@ quiet: bool = False
colored : bool = True
executable : bool = False
__version__ : str
tk_root : tkinter . Tk
def sys_main ( ) - > None :
@ -150,7 +160,7 @@ def sys_main() -> None:
def main ( sys_args : list [ str ] ) - > None :
global executable
global executable , tk_root , headless
# allow to use sys.stdout with with (avoid closing)
sys . stdout . close = lambda : None # type: ignore
# set encoding to utf-8, needed for Windows
@ -164,6 +174,13 @@ def main(sys_args: list[str]) -> None:
# https://pyinstaller.org/en/stable/runtime-information.html#run-time-information
executable = getattr ( sys , ' frozen ' , False ) and hasattr ( sys , ' _MEIPASS ' )
if qreader_available and not headless :
try :
tk_root = tkinter . Tk ( )
tk_root . withdraw ( )
except tkinter . TclError :
headless = True
args = parse_args ( sys_args )
if colored :
@ -174,9 +191,10 @@ def main(sys_args: list[str]) -> None:
sys . exit ( 0 if do_debug_checks ( ) else 1 )
otps = extract_otps ( args )
write_csv ( args , otps )
write_keepass_csv ( args , otps )
write_json ( args , otps )
write_csv ( args . csv , otps )
write_keepass_csv ( args . keepass , otps )
write_json ( args . json , otps )
# workaround for PYTHON <= 3.9 use: pb.MigrationPayload | None
@ -362,15 +380,16 @@ def extract_otps_from_camera(args: Args) -> Otps:
qr_mode = next_qr_mode ( qr_mode )
continue
cv2_print_text ( img , f " Mode: { qr_mode . name } (Hit space to change) " , 0 , TextPosition . LEFT , FONT_COLOR , 20 )
cv2_print_text ( img , " Hit ESC to quit " , 1 , TextPosition . LEFT , FONT_COLOR , 17 )
cv2_print_text ( img , f " Mode: { qr_mode . name } (Hit SPACE to change) " , 0 , TextPosition . LEFT , FONT_COLOR , 20 )
cv2_print_text ( img , " Press ESC to quit " , 1 , TextPosition . LEFT , FONT_COLOR , 17 )
cv2_print_text ( img , " Press C/J/K to save as csv/json/keepass file " , 2 , TextPosition . LEFT , FONT_COLOR , None )
cv2_print_text ( img , f " { len ( otp_urls ) } QR code { ' s ' [ : len ( otp_urls ) != 1 ] } captured " , 0 , TextPosition . RIGHT , FONT_COLOR )
cv2_print_text ( img , f " { len ( otps ) } otp { ' s ' [ : len ( otps ) != 1 ] } extracted " , 1 , TextPosition . RIGHT , FONT_COLOR )
cv2 . imshow ( WINDOW_NAME , img )
quit , qr_mode = cv2_handle_pressed_keys ( qr_mode )
quit , qr_mode = cv2_handle_pressed_keys ( qr_mode , otps )
if quit :
break
@ -416,12 +435,48 @@ def cv2_print_text(img: Any, text: str, line_number: int, position: TextPosition
cv2 . putText ( img , out_text , pos , FONT , FONT_SCALE , color , FONT_THICKNESS , FONT_LINE_STYLE )
def cv2_handle_pressed_keys ( qr_mode : QRMode ) - > Tuple [ bool , QRMode ] :
def cv2_handle_pressed_keys ( qr_mode : QRMode , otps : Otps ) - > Tuple [ bool , QRMode ] :
key = cv2 . waitKey ( 1 ) & 0xFF
quit = False
if key == 27 or key == ord ( ' q ' ) or key == 13 :
if key == 27 or key == ord ( ' q ' ) or key == ord ( ' Q ' ) or key == 13 :
# ESC or Enter or q pressed
quit = True
elif ( key == ord ( ' c ' ) or key == ord ( ' C ' ) ) and is_not_headless ( ) :
if has_otps_or_show_warning ( otps ) :
pass
else :
file_name = tkinter . filedialog . asksaveasfilename (
title = " Save extracted otp secrets as CSV " ,
defaultextension = ' .csv ' ,
filetypes = [ ( ' CSV ' , ' *.csv ' ) , ( ' All ' , ' *.* ' ) ]
)
tk_root . update ( )
if len ( file_name ) > 0 :
write_csv ( file_name , otps )
elif ( key == ord ( ' j ' ) or key == ord ( ' J ' ) ) and is_not_headless ( ) :
if has_otps_or_show_warning ( otps ) :
pass
else :
file_name = tkinter . filedialog . asksaveasfilename (
title = " Save extracted otp secrets as JSON " ,
defaultextension = ' .json ' ,
filetypes = [ ( ' JSON ' , ' *.json ' ) , ( ' All ' , ' *.* ' ) ]
)
tk_root . update ( )
if len ( file_name ) > 0 :
write_json ( file_name , otps )
elif ( key == ord ( ' k ' ) or key == ord ( ' K ' ) ) and is_not_headless ( ) :
if has_otps_or_show_warning ( otps ) :
pass
else :
file_name = tkinter . filedialog . asksaveasfilename (
title = " Save extracted otp secrets as KeePass CSV file(s) " ,
defaultextension = ' .csv ' ,
filetypes = [ ( ' CSV ' , ' *.csv ' ) , ( ' All ' , ' *.* ' ) ]
)
tk_root . update ( )
if len ( file_name ) > 0 :
write_keepass_csv ( file_name , otps )
elif key == 32 :
qr_mode = next_qr_mode ( qr_mode )
if verbose > = LogLevel . MORE_VERBOSE : print ( f " QR reading mode: { qr_mode } " )
@ -626,22 +681,22 @@ def print_qr(args: Args, otp_url: str) -> None:
qr . print_ascii ( )
def write_csv ( args: Args , otps : Otps ) - > None :
if args. csv and len ( otps ) > 0 :
with open_file_or_stdout_for_csv ( args. csv ) as outfile :
def write_csv ( file: str , otps : Otps ) - > None :
if file and len ( file ) > 0 and len ( otps ) > 0 :
with open_file_or_stdout_for_csv ( file ) as outfile :
writer = csv . DictWriter ( outfile , otps [ 0 ] . keys ( ) )
writer . writeheader ( )
writer . writerows ( otps )
if not quiet : print ( f " Exported { len ( otps ) } otp { ' s ' [ : len ( otps ) != 1 ] } to csv { args. csv } " )
if not quiet : print ( f " Exported { len ( otps ) } otp { ' s ' [ : len ( otps ) != 1 ] } to csv { file } " )
def write_keepass_csv ( args: Args , otps : Otps ) - > None :
if args. keepass and len ( otps ) > 0 :
def write_keepass_csv ( file: str , otps : Otps ) - > None :
if file and len ( file ) > 0 and len ( otps ) > 0 :
has_totp = has_otp_type ( otps , ' totp ' )
has_hotp = has_otp_type ( otps , ' hotp ' )
if args. keepass != ' - ' :
otp_filename_totp = args. keepass if has_totp != has_hotp else add_pre_suffix ( args. keepass , " totp " )
otp_filename_hotp = args. keepass if has_totp != has_hotp else add_pre_suffix ( args. keepass , " hotp " )
if file != ' - ' :
otp_filename_totp = file if has_totp != has_hotp else add_pre_suffix ( file , " totp " )
otp_filename_hotp = file if has_totp != has_hotp else add_pre_suffix ( file , " hotp " )
else :
otp_filename_totp = otp_filename_hotp = ' - '
if has_totp :
@ -653,9 +708,9 @@ def write_keepass_csv(args: Args, otps: Otps) -> None:
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_ filenam e: str , otps : Otps ) - > int :
def write_keepass_totp_csv ( file: str , otps : Otps ) - > int :
count_entries = 0
with open_file_or_stdout_for_csv ( otp_ filenam e) as outfile :
with open_file_or_stdout_for_csv ( file) as outfile :
writer = csv . DictWriter ( outfile , [ " Title " , " User Name " , " TimeOtp-Secret-Base32 " , " Group " ] )
writer . writeheader ( )
for otp in otps :
@ -670,9 +725,9 @@ def write_keepass_totp_csv(otp_filename: str, otps: Otps) -> int:
return count_entries
def write_keepass_htop_csv ( otp_ filenam e: str , otps : Otps ) - > int :
def write_keepass_htop_csv ( file: str , otps : Otps ) - > int :
count_entries = 0
with open_file_or_stdout_for_csv ( otp_ filenam e) as outfile :
with open_file_or_stdout_for_csv ( file) as outfile :
writer = csv . DictWriter ( outfile , [ " Title " , " User Name " , " HmacOtp-Secret-Base32 " , " HmacOtp-Counter " , " Group " ] )
writer . writeheader ( )
for otp in otps :
@ -688,11 +743,11 @@ def write_keepass_htop_csv(otp_filename: str, otps: Otps) -> int:
return count_entries
def write_json ( args: Args , otps : Otps ) - > None :
if args. json :
with open_file_or_stdout ( args. json ) as outfile :
def write_json ( file: str , otps : Otps ) - > None :
if file and len ( file ) > 0 :
with open_file_or_stdout ( file ) as outfile :
json . dump ( otps , outfile , indent = 4 )
if not quiet : print ( f " Exported { len ( otps ) } otp { ' s ' [ : len ( otps ) != 1 ] } to json { args. json } " )
if not quiet : print ( f " Exported { len ( otps ) } otp { ' s ' [ : len ( otps ) != 1 ] } to json { file } " )
def has_otp_type ( otps : Otps , otp_type : str ) - > bool :
@ -729,6 +784,14 @@ def check_file_exists(filename: str) -> None:
f " \n input file: { filename } " )
def has_otps_or_show_warning ( otps : Otps ) - > bool :
if len ( otps ) == 0 :
tkinter . messagebox . showinfo ( title = " No data " , message = " There are no otp secrets to write " )
tk_root . update ( ) # dispose dialog
return len ( otps ) > 0
def is_binary ( line : str ) - > bool :
try :
line . startswith ( ' # ' )
@ -751,6 +814,12 @@ def do_debug_checks() -> bool:
return True
def is_not_headless ( ) - > bool :
if headless :
log_warn ( f " Cannot open dialog in headless mode " )
return not headless
class PrintVersionAction ( argparse . Action ) :
def __init__ ( self , option_strings : Sequence [ str ] , dest : str , nargs : int = 0 , * * kwargs : Any ) - > None :
super ( ) . __init__ ( option_strings , dest , nargs , * * kwargs )