commit 750d063a886ef1561878077ed0bbc98472b01386 Author: scito Date: Sat May 23 08:51:41 2020 +0200 Initial diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9d6e0f0 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,8 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true \ No newline at end of file diff --git a/example_export.txt b/example_export.txt new file mode 100644 index 0000000..bdc6619 --- /dev/null +++ b/example_export.txt @@ -0,0 +1,4 @@ +# 2FA example from https://www.raspberrypi.org/blog/setting-up-two-factor-authentication-on-your-raspberry-pi/ +# Secret key: 7KSQL2JTUDIS5EF65KLMRQIIGY +# otpauth://totp/pi@raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi +otpauth-migration://offline?data=CjUKEPqlBekzoNEukL7qlsjBCDYSDnBpQHJhc3BiZXJyeXBpGgtyYXNwYmVycnlwaSABKAEwAhABGAEgACjr4JKK%2B%2F%2F%2F%2F%2F8B diff --git a/extract_otp_secret_keys.py b/extract_otp_secret_keys.py new file mode 100644 index 0000000..d66d33f --- /dev/null +++ b/extract_otp_secret_keys.py @@ -0,0 +1,100 @@ +# Extract two-factor autentication (2FA, TFA) secret keys from export QR codes of "Google Authenticator" app +# +# Usage: +# 1. Export the QR codes from "Google Authenticator" app +# 2. Read QR codes with QR code reader +# 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: +# python extract_otp_secret_keys.py -q example_export.txt +# +# 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). +# +# Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message defintion): +# 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 . + +import argparse +import base64 +import fileinput +import sys +from urllib.parse import parse_qs, urlencode, urlparse + +import generated_python.google_auth_pb2 + +arg_parser = argparse.ArgumentParser() +arg_parser.add_argument('--verbose', '-v', help='verbose output', action='store_true') +arg_parser.add_argument('--qr', '-q', help='print QR codes (otpauth://...)', action='store_true') +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() + +verbose = args.verbose +if args.qr: + from qrcode import QRCode + +# 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('=', '') + + +def print_qr(data): + qr = QRCode() + qr.add_data(data) + qr.print_tty() + +for line in (line.strip() for line in fileinput.input(args.infile)): + if verbose: print(line) + if line.startswith('#'): continue + parsed_url = urlparse(line) + params = parse_qs(parsed_url.query) + data_encoded = params['data'][0] + data = base64.b64decode(data_encoded) + payload = generated_python.google_auth_pb2.MigrationPayload() + payload.ParseFromString(data) + if verbose: print(payload) + + # pylint: disable=no-member + for otp in payload.otp_parameters: + print('\nName: {}'.format(otp.name)) + secret = convert_secret_from_bytes_to_base32_str(otp.secret) + print('Secret: {}'.format(secret)) + if otp.issuer: print('Issuer: {}'.format(otp.issuer)) + print('Type: {}'.format(get_enum_name_by_number(otp, 'type'))) + url_params = { 'secret': secret } + if otp.type == 1: url_params['counter'] = otp.counter + if otp.issuer: url_params['issuer'] = otp.issuer + otp_url = 'otpauth://{}/{}?'.format('totp' if otp.type == 2 else 'hotp', otp.name) + urlencode(url_params) + if args.qr: + if verbose: print(otp_url) + print_qr(otp_url) diff --git a/generated_python/google_auth_pb2.py b/generated_python/google_auth_pb2.py new file mode 100644 index 0000000..f850ce7 --- /dev/null +++ b/generated_python/google_auth_pb2.py @@ -0,0 +1,242 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google_auth.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='google_auth.proto', + package='', + syntax='proto3', + serialized_options=None, + create_key=_descriptor._internal_create_key, + serialized_pb=b'\n\x11google_auth.proto\"\xb7\x03\n\x10MigrationPayload\x12\x37\n\x0eotp_parameters\x18\x01 \x03(\x0b\x32\x1f.MigrationPayload.OtpParameters\x12\x0f\n\x07version\x18\x02 \x01(\x05\x12\x12\n\nbatch_size\x18\x03 \x01(\x05\x12\x13\n\x0b\x62\x61tch_index\x18\x04 \x01(\x05\x12\x10\n\x08\x62\x61tch_id\x18\x05 \x01(\x05\x1a\xb7\x01\n\rOtpParameters\x12\x0e\n\x06secret\x18\x01 \x01(\x0c\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x0e\n\x06issuer\x18\x03 \x01(\t\x12.\n\talgorithm\x18\x04 \x01(\x0e\x32\x1b.MigrationPayload.Algorithm\x12\x0e\n\x06\x64igits\x18\x05 \x01(\x05\x12\'\n\x04type\x18\x06 \x01(\x0e\x32\x19.MigrationPayload.OtpType\x12\x0f\n\x07\x63ounter\x18\x07 \x01(\x03\",\n\tAlgorithm\x12\x10\n\x0c\x41LGO_INVALID\x10\x00\x12\r\n\tALGO_SHA1\x10\x01\"6\n\x07OtpType\x12\x0f\n\x0bOTP_INVALID\x10\x00\x12\x0c\n\x08OTP_HOTP\x10\x01\x12\x0c\n\x08OTP_TOTP\x10\x02\x62\x06proto3' +) + + + +_MIGRATIONPAYLOAD_ALGORITHM = _descriptor.EnumDescriptor( + name='Algorithm', + full_name='MigrationPayload.Algorithm', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='ALGO_INVALID', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='ALGO_SHA1', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=361, + serialized_end=405, +) +_sym_db.RegisterEnumDescriptor(_MIGRATIONPAYLOAD_ALGORITHM) + +_MIGRATIONPAYLOAD_OTPTYPE = _descriptor.EnumDescriptor( + name='OtpType', + full_name='MigrationPayload.OtpType', + filename=None, + file=DESCRIPTOR, + create_key=_descriptor._internal_create_key, + values=[ + _descriptor.EnumValueDescriptor( + name='OTP_INVALID', index=0, number=0, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='OTP_HOTP', index=1, number=1, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + _descriptor.EnumValueDescriptor( + name='OTP_TOTP', index=2, number=2, + serialized_options=None, + type=None, + create_key=_descriptor._internal_create_key), + ], + containing_type=None, + serialized_options=None, + serialized_start=407, + serialized_end=461, +) +_sym_db.RegisterEnumDescriptor(_MIGRATIONPAYLOAD_OTPTYPE) + + +_MIGRATIONPAYLOAD_OTPPARAMETERS = _descriptor.Descriptor( + name='OtpParameters', + full_name='MigrationPayload.OtpParameters', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='secret', full_name='MigrationPayload.OtpParameters.secret', index=0, + number=1, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='name', full_name='MigrationPayload.OtpParameters.name', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='issuer', full_name='MigrationPayload.OtpParameters.issuer', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='algorithm', full_name='MigrationPayload.OtpParameters.algorithm', index=3, + number=4, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='digits', full_name='MigrationPayload.OtpParameters.digits', index=4, + number=5, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='type', full_name='MigrationPayload.OtpParameters.type', index=5, + number=6, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='counter', full_name='MigrationPayload.OtpParameters.counter', index=6, + number=7, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=176, + serialized_end=359, +) + +_MIGRATIONPAYLOAD = _descriptor.Descriptor( + name='MigrationPayload', + full_name='MigrationPayload', + filename=None, + file=DESCRIPTOR, + containing_type=None, + create_key=_descriptor._internal_create_key, + fields=[ + _descriptor.FieldDescriptor( + name='otp_parameters', full_name='MigrationPayload.otp_parameters', index=0, + number=1, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='version', full_name='MigrationPayload.version', index=1, + number=2, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='batch_size', full_name='MigrationPayload.batch_size', index=2, + number=3, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='batch_index', full_name='MigrationPayload.batch_index', index=3, + number=4, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + _descriptor.FieldDescriptor( + name='batch_id', full_name='MigrationPayload.batch_id', index=4, + number=5, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR, create_key=_descriptor._internal_create_key), + ], + extensions=[ + ], + nested_types=[_MIGRATIONPAYLOAD_OTPPARAMETERS, ], + enum_types=[ + _MIGRATIONPAYLOAD_ALGORITHM, + _MIGRATIONPAYLOAD_OTPTYPE, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=22, + serialized_end=461, +) + +_MIGRATIONPAYLOAD_OTPPARAMETERS.fields_by_name['algorithm'].enum_type = _MIGRATIONPAYLOAD_ALGORITHM +_MIGRATIONPAYLOAD_OTPPARAMETERS.fields_by_name['type'].enum_type = _MIGRATIONPAYLOAD_OTPTYPE +_MIGRATIONPAYLOAD_OTPPARAMETERS.containing_type = _MIGRATIONPAYLOAD +_MIGRATIONPAYLOAD.fields_by_name['otp_parameters'].message_type = _MIGRATIONPAYLOAD_OTPPARAMETERS +_MIGRATIONPAYLOAD_ALGORITHM.containing_type = _MIGRATIONPAYLOAD +_MIGRATIONPAYLOAD_OTPTYPE.containing_type = _MIGRATIONPAYLOAD +DESCRIPTOR.message_types_by_name['MigrationPayload'] = _MIGRATIONPAYLOAD +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +MigrationPayload = _reflection.GeneratedProtocolMessageType('MigrationPayload', (_message.Message,), { + + 'OtpParameters' : _reflection.GeneratedProtocolMessageType('OtpParameters', (_message.Message,), { + 'DESCRIPTOR' : _MIGRATIONPAYLOAD_OTPPARAMETERS, + '__module__' : 'google_auth_pb2' + # @@protoc_insertion_point(class_scope:MigrationPayload.OtpParameters) + }) + , + 'DESCRIPTOR' : _MIGRATIONPAYLOAD, + '__module__' : 'google_auth_pb2' + # @@protoc_insertion_point(class_scope:MigrationPayload) + }) +_sym_db.RegisterMessage(MigrationPayload) +_sym_db.RegisterMessage(MigrationPayload.OtpParameters) + + +# @@protoc_insertion_point(module_scope) diff --git a/google_auth.proto b/google_auth.proto new file mode 100644 index 0000000..1203075 --- /dev/null +++ b/google_auth.proto @@ -0,0 +1,32 @@ +syntax = "proto3"; + +// Copied from: https://github.com/beemdevelopment/Aegis/blob/master/app/src/main/proto/google_auth.proto + +message MigrationPayload { + enum Algorithm { + ALGO_INVALID = 0; + ALGO_SHA1 = 1; + } + + enum OtpType { + OTP_INVALID = 0; + OTP_HOTP = 1; + OTP_TOTP = 2; + } + + message OtpParameters { + bytes secret = 1; + string name = 2; + string issuer = 3; + Algorithm algorithm = 4; + int32 digits = 5; + OtpType type = 6; + int64 counter = 7; + } + + repeated OtpParameters otp_parameters = 1; + int32 version = 2; + int32 batch_size = 3; + int32 batch_index = 4; + int32 batch_id = 5; +}