2020-05-23 09:10:50 +02:00
# Extract two-factor authentication (2FA, TFA) secret keys 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-09-08 21:11:49 +02:00
# python extract_otp_secret_keys.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/>.
2022-12-29 22:12:07 +01:00
from __future__ import annotations # for compatibility with Python < 3.11
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
2022-12-28 22:28:54 +01:00
from enum import Enum
from operator import add
2022-12-30 01:44:11 +01:00
try :
from typing import Any , TextIO , TypedDict , Union , List
except ImportError :
from typing import Any , TextIO , Union , List
# PYTHON < 3.8: compatibility
from typing_extensions import TypedDict
2022-12-28 22:28:54 +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
2020-05-23 08:51:41 +02:00
2022-12-28 22:28:54 +01:00
try :
2022-12-29 15:52:17 +01:00
import cv2 # type: ignore
2022-12-29 21:29:20 +01:00
import numpy
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 } """ )
qreader_available = True
2022-12-29 15:52:17 +01:00
except ImportError :
2022-12-28 22:28:54 +01:00
qreader_available = False
2022-12-29 23:17:31 +01:00
# TODO Workaround for PYTHON < 3.10: Union[int, None] used instead of int | None
2022-12-29 21:29:20 +01:00
# Types
Args = argparse . Namespace
OtpUrl = str
2022-12-30 01:07:39 +01:00
# PYTHON 3.11: 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 } )
2022-12-30 01:07:39 +01:00
# PYTHON 3.11: Otps = list[Otp]
Otps = List [ Otp ]
# PYTHON 3.11: OtpUrls = list[OtpUrl]
OtpUrls = List [ OtpUrl ]
2022-12-29 21:29:20 +01:00
# Global variable declaration
verbose : int = 0
quiet : bool = False
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-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
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 :
2022-12-28 22:28:54 +01:00
global verbose , quiet
description_text = " Extracts one time password (OTP) secret keys from QR codes, e.g. from Google Authenticator app. "
if qreader_available :
description_text + = " \n If no infiles are provided, the QR codes are interactively captured from the camera. "
example_text = """ examples:
python extract_otp_secret_keys . py
2022-12-26 23:39:13 +01:00
python extract_otp_secret_keys . py example_ * . txt
python extract_otp_secret_keys . py - < example_export . txt
python extract_otp_secret_keys . py - - csv - example_ * . png | tail - n + 2
2022-12-28 22:28:54 +01:00
python extract_otp_secret_keys . py = < example_export . png """
2022-12-26 23:39:13 +01:00
2022-12-29 15:52:17 +01:00
arg_parser = argparse . ArgumentParser ( formatter_class = lambda prog : argparse . RawTextHelpFormatter ( prog , max_help_position = 52 ) ,
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 :
arg_parser . add_argument ( ' --camera ' , ' -C ' , help = ' camera number of system (default camera: 0) ' , default = 0 , nargs = 1 , metavar = ( ' NUMBER ' ) )
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 ' ) )
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
verbose = args . verbose if args . verbose else 0
quiet = True if args . quiet else False
2022-12-29 16:30:18 +01:00
if verbose : print ( f " QReader installed: { qreader_available } " )
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-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-29 15:52:17 +01:00
QRMode = Enum ( ' QRMode ' , [ ' QREADER ' , ' DEEP_QREADER ' , ' CV2 ' ] , start = 0 )
2022-12-28 22:28:54 +01:00
qr_mode = QRMode . QREADER
if verbose : print ( f " QR reading mode: { qr_mode } " )
cam = cv2 . VideoCapture ( args . camera )
window_name = " Extract OTP Secret Keys: Capture QR Codes from Camera "
cv2 . namedWindow ( window_name , cv2 . WINDOW_AUTOSIZE )
2022-12-29 11:18:57 +01:00
neutral_color = 255 , 0 , 255
sucess_color = 0 , 255 , 0
2022-12-28 22:28:54 +01:00
font = cv2 . FONT_HERSHEY_PLAIN
font_scale = 1
font_thickness = 1
pos_text = 5 , 20
font_dy = 0 , cv2 . getTextSize ( " M " , font , font_scale , font_thickness ) [ 0 ] [ 1 ] + 5
font_line = cv2 . LINE_AA
rect_thickness = 5
decoder = QReader ( )
while True :
success , img = cam . read ( )
if not success :
eprint ( " ERROR: Failed to capture image " )
break
if qr_mode in [ QRMode . QREADER , QRMode . DEEP_QREADER ] :
bbox , found = decoder . detect ( img )
if qr_mode == QRMode . DEEP_QREADER :
otp_url = decoder . detect_and_decode ( img )
elif qr_mode == QRMode . QREADER :
otp_url = decoder . decode ( img , bbox ) if found else None
if found :
2022-12-29 11:18:57 +01:00
cv2 . rectangle ( img , ( bbox [ 0 ] , bbox [ 1 ] ) , ( bbox [ 2 ] , bbox [ 3 ] ) , sucess_color if otp_url else neutral_color , rect_thickness )
2022-12-28 22:28:54 +01:00
if otp_url :
extract_otps_from_otp_url ( otp_url , otp_urls , otps , args )
elif qr_mode == QRMode . CV2 :
for qrcode in zbar . decode ( img ) :
otp_url = qrcode . data . decode ( ' utf-8 ' )
pts = numpy . array ( [ qrcode . polygon ] , numpy . int32 )
pts = pts . reshape ( ( - 1 , 1 , 2 ) )
2022-12-29 11:18:57 +01:00
cv2 . polylines ( img , [ pts ] , True , sucess_color if otp_url else neutral_color , rect_thickness )
2022-12-28 22:28:54 +01:00
extract_otps_from_otp_url ( otp_url , otp_urls , otps , args )
else :
2022-12-29 04:15:36 +01:00
assert False , f " ERROR: Wrong QReader mode { qr_mode . name } "
2022-12-28 22:28:54 +01:00
2022-12-29 11:18:57 +01:00
cv2 . putText ( img , f " Mode: { qr_mode . name } (Hit space to change) " , pos_text , font , font_scale , neutral_color , font_thickness , font_line )
cv2 . putText ( img , " Hit ESC to quit " , tuple ( map ( add , pos_text , font_dy ) ) , font , font_scale , neutral_color , font_thickness , font_line )
2022-12-28 22:28:54 +01:00
window_dim = cv2 . getWindowImageRect ( window_name )
qrcodes_text = f " { len ( otp_urls ) } QR code { ' s ' [ : len ( otp_urls ) != 1 ] } captured "
pos_qrcodes_text = window_dim [ 2 ] - cv2 . getTextSize ( qrcodes_text , font , font_scale , font_thickness ) [ 0 ] [ 0 ] - 5 , pos_text [ 1 ]
2022-12-29 11:18:57 +01:00
cv2 . putText ( img , qrcodes_text , pos_qrcodes_text , font , font_scale , neutral_color , font_thickness , font_line )
2022-12-28 22:28:54 +01:00
otps_text = f " { len ( otps ) } otp { ' s ' [ : len ( otps ) != 1 ] } extracted "
pos_otps_text = window_dim [ 2 ] - cv2 . getTextSize ( otps_text , font , font_scale , font_thickness ) [ 0 ] [ 0 ] - 5 , pos_text [ 1 ] + font_dy [ 1 ]
2022-12-29 11:18:57 +01:00
cv2 . putText ( img , otps_text , pos_otps_text , font , font_scale , neutral_color , font_thickness , font_line )
2022-12-28 22:28:54 +01:00
cv2 . imshow ( window_name , img )
key = cv2 . waitKey ( 1 ) & 0xFF
if key == 27 or key == ord ( ' q ' ) or key == 13 :
# ESC pressed
break
elif key == 32 :
qr_mode = QRMode ( ( qr_mode . value + 1 ) % len ( QRMode ) )
if verbose : print ( f " QR reading mode: { qr_mode } " )
2022-12-30 01:58:54 +01:00
if cv2 . getWindowProperty ( window_name , cv2 . WND_PROP_VISIBLE ) < 1 :
2022-12-30 01:22:12 +01:00
# Window close clicked
break
2022-12-28 22:28:54 +01:00
cam . release ( )
cv2 . destroyAllWindows ( )
return otps
2022-12-29 21:29:20 +01:00
def extract_otps_from_otp_url ( otp_url : str , otp_urls : OtpUrls , otps : Otps , args : Args ) - > None :
2022-12-28 22:28:54 +01:00
if otp_url and verbose : print ( otp_url )
if otp_url and otp_url not in otp_urls :
otp_urls . append ( otp_url )
extract_otp_from_otp_url ( otp_url , otps , len ( otp_urls ) , len ( otps ) , ' camera ' , args )
if verbose : print ( f " { len ( otps ) } otp { ' s ' [ : len ( otps ) != 1 ] } from { len ( otp_urls ) } QR code { ' s ' [ : len ( otp_urls ) != 1 ] } extracted " )
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-24 15:30:17 +01:00
i = j = k = 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 :
2022-12-28 22:28:54 +01:00
if verbose : print ( f " Processing infile { infile } " )
2022-12-24 15:30:17 +01:00
k + = 1
2022-12-28 22:28:54 +01:00
for line in get_otp_urls_from_file ( infile ) :
2022-12-24 04:48:12 +01:00
if verbose : print ( line )
if line . startswith ( ' # ' ) or line == ' ' : continue
i + = 1
2022-12-28 22:28:54 +01:00
j = extract_otp_from_otp_url ( line , otps , i , j , infile , args )
if verbose : print ( f " { k } infile { ' s ' [ : k != 1 ] } processed " )
2022-09-03 14:31:09 +02:00
return otps
2022-09-03 16:12:28 +02:00
2022-12-29 21:29:20 +01:00
def get_otp_urls_from_file ( filename : str ) - > 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 :
return convert_img_to_otp_url ( filename )
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 ] :
2022-12-28 22:28:54 +01:00
if verbose : print ( f " Reading lines of { filename } " )
2022-12-24 15:30:17 +01:00
finput = fileinput . input ( filename )
try :
lines = [ ]
for line in ( line . strip ( ) for line in finput ) :
if verbose : print ( line )
if is_binary ( line ) :
2022-12-28 22:28:54 +01:00
abort ( " \n 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 :
2022-12-28 22:28:54 +01:00
eprint ( f " WARN: { filename . replace ( ' - ' , ' stdin ' ) } is empty " )
2022-12-24 15:30:17 +01:00
except UnicodeDecodeError :
if filename == ' - ' :
2022-12-28 22:28:54 +01:00
abort ( " \n ERROR: Unable to open text file form stdin. "
2022-12-29 15:52:17 +01:00
" In case you want read an image file from stdin, you must use ' = ' instead of ' - ' . " )
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-29 21:29:20 +01:00
def extract_otp_from_otp_url ( otpauth_migration_url : str , otps : Otps , i : int , j : int , infile : str , args : Args ) - > int :
2022-12-28 22:28:54 +01:00
payload = get_payload_from_otp_url ( otpauth_migration_url , i , infile )
# pylint: disable=no-member
for raw_otp in payload . otp_parameters :
j + = 1
if verbose : print ( f " \n { j } . Secret Key " )
secret = convert_secret_from_bytes_to_base32_str ( raw_otp . secret )
2022-12-29 15:52:17 +01:00
if verbose : print ( ' 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 :
save_qr ( otp , args , j )
if not quiet :
print ( )
return j
2022-12-29 21:29:20 +01:00
def convert_img_to_otp_url ( filename : str ) - > 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 :
eprint ( " WARN: stdin is empty " )
2022-12-24 03:25:10 +01:00
try :
2022-12-26 18:31:09 +01:00
img_array = numpy . frombuffer ( stdin , dtype = ' uint8 ' )
2022-12-24 15:30:17 +01:00
except TypeError as e :
2022-12-28 22:28:54 +01:00
abort ( f " \n ERROR: Cannot read binary stdin buffer. Exception: { 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 :
abort ( f " \n ERROR: Unable to open file for reading. \n input file: { filename } " )
2022-12-24 15:30:17 +01:00
2022-12-28 22:28:54 +01:00
decoded_text = QReader ( ) . detect_and_decode ( img )
2022-12-24 15:30:17 +01:00
if decoded_text is None :
2022-12-28 22:28:54 +01:00
abort ( f " \n ERROR: Unable to read QR Code from file. \n input file: { filename } " )
2022-12-24 15:30:17 +01:00
except Exception as e :
2022-12-28 22:28:54 +01:00
abort ( f " \n ERROR: Encountered exception ' { e } ' . \n input file: { filename } " )
2022-12-29 21:29:20 +01:00
return [ decoded_text ]
2022-12-21 16:47:31 -08:00
2022-12-29 21:32:19 +01:00
def get_payload_from_otp_url ( otpauth_migration_url : str , i : int , input_source : str ) - > pb . MigrationPayload :
2022-12-28 22:28:54 +01:00
if not otpauth_migration_url . startswith ( ' otpauth-migration:// ' ) :
eprint ( f " \n WARN: line is not a otpauth-migration:// URL \n input: { input_source } \n line ' { otpauth_migration_url } ' \n Probably a wrong file was given " )
parsed_url = urlparse . urlparse ( otpauth_migration_url )
if verbose > 2 : print ( f " \n DEBUG: 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 )
2022-12-29 22:34:07 +01:00
except Exception : # Necessary for PYTHON < 3.11
2022-12-29 21:29:20 +01:00
params = { }
2022-12-28 22:28:54 +01:00
if verbose > 2 : print ( f " \n DEBUG: querystring params= { params } " )
2022-09-04 08:37:03 +02:00
if ' data ' not in params :
2022-12-28 22:28:54 +01:00
abort ( f " \n ERROR: no data query parameter in input URL \n input file: { input_source } \n line ' { otpauth_migration_url } ' \n Probably a wrong file was given " )
2022-09-07 21:58:03 +02:00
data_base64 = params [ ' data ' ] [ 0 ]
2022-12-28 22:28:54 +01:00
if verbose > 2 : print ( f " \n DEBUG: data_base64= { data_base64 } " )
2022-09-07 21:58:03 +02:00
data_base64_fixed = data_base64 . replace ( ' ' , ' + ' )
2022-12-28 22:28:54 +01:00
if verbose > 2 : print ( f " \n DEBUG: 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 )
2022-12-29 15:52:17 +01:00
except Exception :
2022-12-28 22:28:54 +01:00
abort ( f " \n ERROR: Cannot decode otpauth-migration migration payload. \n "
2022-12-29 15:52:17 +01:00
f " data= { data_base64 } " )
2022-09-04 08:37:03 +02:00
if verbose :
2022-12-28 22:28:54 +01:00
print ( f " \n { i } . Payload Line " , payload , sep = ' \n ' )
2022-09-04 08:37:03 +02:00
return payload
# 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-09-04 08:37:03 +02:00
return file_otp_issuer
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 " )
count_totp_entries = 0
count_hotp_entries = 0
if has_totp :
2022-12-18 19:24:07 +01:00
with open_file_or_stdout_for_csv ( otp_filename_totp ) as outfile :
2022-12-04 12:23:39 +01:00
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 ,
2022-12-28 22:28:54 +01:00
' Group ' : f " OTP/ { otp [ ' type ' ] . upper ( ) } "
2022-12-04 12:23:39 +01:00
} )
count_totp_entries + = 1
if has_hotp :
2022-12-18 19:24:07 +01:00
with open_file_or_stdout_for_csv ( otp_filename_hotp ) as outfile :
2022-12-24 01:59:35 +01:00
writer = csv . DictWriter ( outfile , [ " Title " , " User Name " , " HmacOtp-Secret-Base32 " , " HmacOtp-Counter " , " Group " ] )
2022-12-04 12:23:39 +01:00
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 ,
2022-12-28 22:28:54 +01:00
' Group ' : f " OTP/ { otp [ ' type ' ] . upper ( ) } "
2022-12-04 12:23:39 +01:00
} )
count_hotp_entries + = 1
if not quiet :
2022-12-28 22:28:54 +01:00
if count_totp_entries > 0 : print ( f " Exported { count_totp_entries } totp entrie { ' s ' [ : count_totp_entries != 1 ] } to keepass csv file { otp_filename_totp } " )
if count_hotp_entries > 0 : print ( f " Exported { count_hotp_entries } hotp entrie { ' s ' [ : count_hotp_entries != 1 ] } to keepass csv file { otp_filename_hotp } " )
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 ) :
2022-12-28 22:28:54 +01:00
abort ( f " \n ERROR: 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
2022-12-29 21:29:20 +01:00
def eprint ( * args : Any , * * kwargs : Any ) - > None :
2022-12-18 19:24:07 +01:00
''' Print to stderr. '''
print ( * args , file = sys . stderr , * * kwargs )
2022-12-29 21:29:20 +01:00
def abort ( * args : Any , * * kwargs : Any ) - > None :
2022-12-29 11:18:57 +01:00
eprint ( * args , * * kwargs )
sys . exit ( 1 )
2022-09-03 14:31:09 +02:00
if __name__ == ' __main__ ' :
sys_main ( )