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:
2021-02-13 16:58:30 +01:00
# python extract_otp_secret_keys.py -p 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/>.
import argparse
import base64
import fileinput
import sys
2022-06-29 06:18:51 +02:00
import csv
import json
2020-11-14 13:15:06 +01:00
from urllib.parse import parse_qs , urlencode , urlparse , quote
2020-12-21 12:19:13 -06:00
from os import path , mkdir
from re import sub , compile as rcompile
2020-05-23 08:51:41 +02:00
import generated_python.google_auth_pb2
arg_parser = argparse . ArgumentParser ()
arg_parser . add_argument ( '--verbose' , '-v' , help = 'verbose output' , action = 'store_true' )
2020-12-21 12:19:13 -06:00
arg_parser . add_argument ( '--saveqr' , '-s' , help = 'save QR code(s) as images to the "qr" subfolder' , action = 'store_true' )
arg_parser . add_argument ( '--printqr' , '-p' , help = 'print QR code(s) as text to the terminal' , action = 'store_true' )
2022-06-29 06:18:51 +02:00
arg_parser . add_argument ( '--json' , '-j' , help = 'export to json file' )
arg_parser . add_argument ( '--csv' , '-c' , help = 'export to csv file' )
2020-05-23 08:51:41 +02:00
arg_parser . add_argument ( 'infile' , help = 'file or - for stdin (default: -) with "otpauth-migration://..." URLs separated by newlines, lines starting with # are ignored' )
args = arg_parser . parse_args ()
2021-02-13 16:58:30 +01:00
if args . saveqr or args . printqr : from qrcode import QRCode
2020-05-23 08:51:41 +02:00
verbose = args . verbose
# https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf
def get_enum_name_by_number ( parent , field_name ):
field_value = getattr ( parent , field_name )
return parent . DESCRIPTOR . fields_by_name [ field_name ] . enum_type . values_by_number . get ( field_value ) . name
def convert_secret_from_bytes_to_base32_str ( bytes ):
return str ( base64 . b32encode ( otp . secret ), 'utf-8' ) . replace ( '=' , '' )
2020-12-21 12:19:13 -06:00
def save_qr ( data , name ):
qr = QRCode ()
qr . add_data ( data )
2020-12-21 19:52:55 +01:00
img = qr . make_image ( fill_color = 'black' , back_color = 'white' )
if verbose : print ( 'Saving to {} ' . format ( name ))
2020-12-21 12:19:13 -06:00
img . save ( name )
2020-05-23 08:51:41 +02:00
def print_qr ( data ):
qr = QRCode ()
qr . add_data ( data )
2021-12-29 21:30:58 +01:00
qr . print_ascii ()
2020-05-23 08:51:41 +02:00
2022-06-29 06:18:51 +02:00
otps = []
2021-02-13 16:58:30 +01:00
i = j = 0
2020-05-23 08:51:41 +02:00
for line in ( line . strip () for line in fileinput . input ( args . infile )):
if verbose : print ( line )
2021-02-13 16:45:43 +01:00
if line . startswith ( '#' ) or line == '' : continue
2021-02-13 16:54:12 +01:00
if not line . startswith ( 'otpauth-migration://' ): print ( ' \n WARN: line is not a otpauth-migration:// URL \n input file: {} \n line " {} " \n Probably a wrong file was given' . format ( args . infile , line ))
2020-05-23 08:51:41 +02:00
parsed_url = urlparse ( line )
params = parse_qs ( parsed_url . query )
2021-02-13 16:54:12 +01:00
if not 'data' in params :
print ( ' \n ERROR: no data query parameter in input URL \n input file: {} \n line " {} " \n Probably a wrong file was given' . format ( args . infile , line ))
sys . exit ( 1 )
2020-05-23 08:51:41 +02:00
data_encoded = params [ 'data' ][ 0 ]
data = base64 . b64decode ( data_encoded )
payload = generated_python . google_auth_pb2 . MigrationPayload ()
payload . ParseFromString ( data )
2021-02-13 16:58:30 +01:00
i += 1
if verbose : print ( ' \n {} . Payload Line' . format ( i ), payload , sep = ' \n ' )
2020-05-23 08:51:41 +02:00
# pylint: disable=no-member
for otp in payload . otp_parameters :
2021-02-13 16:58:30 +01:00
j += 1
if verbose : print ( ' \n {} . Secret Key' . format ( j ))
else : print ()
print ( 'Name: {} ' . format ( otp . name ))
2020-05-23 08:51:41 +02:00
secret = convert_secret_from_bytes_to_base32_str ( otp . secret )
print ( 'Secret: {} ' . format ( secret ))
if otp . issuer : print ( 'Issuer: {} ' . format ( otp . issuer ))
2022-06-29 06:18:51 +02:00
otp_type = get_enum_name_by_number ( otp , 'type' )
print ( 'Type: {} ' . format ( otp_type ))
2020-05-23 08:51:41 +02:00
url_params = { 'secret' : secret }
if otp . type == 1 : url_params [ 'counter' ] = otp . counter
if otp . issuer : url_params [ 'issuer' ] = otp . issuer
2020-11-14 13:15:06 +01:00
otp_url = 'otpauth:// {} / {} ?' . format ( 'totp' if otp . type == 2 else 'hotp' , quote ( otp . name )) + urlencode ( url_params )
2021-02-13 16:58:30 +01:00
if verbose : print ( otp_url )
if args . printqr :
print_qr ( otp_url )
if args . saveqr :
2020-12-21 19:52:55 +01:00
if not ( path . exists ( 'qr' )): mkdir ( 'qr' )
pattern = rcompile ( r '[\W_]+' )
2020-12-21 12:19:13 -06:00
file_otp_name = pattern . sub ( '' , otp . name )
file_otp_issuer = pattern . sub ( '' , otp . issuer )
2021-02-13 16:58:30 +01:00
save_qr ( otp_url , 'qr/ {} - {}{} .png' . format ( j , file_otp_name , '-' + file_otp_issuer if file_otp_issuer else '' ))
2022-06-29 06:18:51 +02:00
otps . append ({
"name" : otp . name ,
"secret" : secret ,
"issuer" : otp . issuer ,
"type" : otp_type ,
"url" : otp_url
})
if args . csv and len ( otps ) > 0 :
with open ( args . csv , "w" ) as outfile :
writer = csv . DictWriter ( outfile , otps [ 0 ] . keys ())
writer . writeheader ()
writer . writerows ( otps )
print ( "Exported {} otps to csv" . format ( len ( otps )))
if args . json :
with open ( args . json , "w" ) as outfile :
json . dump ( otps , outfile , indent = 4 )
print ( "Exported {} otp entries to json" . format ( len ( otps )))