diff --git a/.gitignore b/.gitignore index 797ad06..1c93df5 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ venv/ *.csv *.json !devbox.json +!example_output.json +!example_output.csv diff --git a/README.md b/README.md index 1425e4d..0186428 100644 --- a/README.md +++ b/README.md @@ -13,18 +13,15 @@ Extract two-factor authentication (2FA, TFA) secret keys from export QR codes of ## Dependencies -The protobuf package of Google for proto3 is required for running this script. protobuf >= 3.14 is recommended. - - pip install protobuf + pip install -r requirements.txt Known to work with -* Python 3.6.12 and protobuf 3.14.0 -* Python 3.8.5 and protobuf 3.14.0 +* Python 3.10.6, protobuf 4.21.5, qrcode 7.3.1, and pillow 9.2 ### Optional -For printing QR codes, the qrcode module is required +For printing QR codes, the qrcode module is required, otherwise it can be omitted. pip install qrcode[pil] @@ -33,9 +30,11 @@ For printing QR codes, the qrcode module is required 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 definition): +Command for regeneration of Python code from proto3 message definition file (only necessary in case of changes of the proto3 message definition or new protobuf versions): + + protoc --python_out=protobuf_generated_python google_auth.proto - protoc --python_out=generated_python google_auth.proto +The generated protobuf Python code was generated by protoc 21.5 (https://github.com/protocolbuffers/protobuf/releases/tag/v21.5). ## References @@ -63,3 +62,13 @@ Install [devbox](https://github.com/jetpack-io/devbox), which is a wrapper for n ``` devbox shell ``` + +## Unit Tests + +There are basic unit tests, see `unittest_extract_otp_secret_keys.py`. + +Run unit tests: + +``` +python -m unittest +``` diff --git a/example_output.csv b/example_output.csv new file mode 100644 index 0000000..7815773 --- /dev/null +++ b/example_output.csv @@ -0,0 +1,5 @@ +name,secret,issuer,type,url +pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi +pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY +pi@raspberrypi,7KSQL2JTUDIS5EF65KLMRQIIGY,raspberrypi,OTP_TOTP,otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi diff --git a/example_output.json b/example_output.json new file mode 100644 index 0000000..3e85fd0 --- /dev/null +++ b/example_output.json @@ -0,0 +1,30 @@ +[ + { + "name": "pi@raspberrypi", + "secret": "7KSQL2JTUDIS5EF65KLMRQIIGY", + "issuer": "raspberrypi", + "type": "OTP_TOTP", + "url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi" + }, + { + "name": "pi@raspberrypi", + "secret": "7KSQL2JTUDIS5EF65KLMRQIIGY", + "issuer": "", + "type": "OTP_TOTP", + "url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY" + }, + { + "name": "pi@raspberrypi", + "secret": "7KSQL2JTUDIS5EF65KLMRQIIGY", + "issuer": "", + "type": "OTP_TOTP", + "url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY" + }, + { + "name": "pi@raspberrypi", + "secret": "7KSQL2JTUDIS5EF65KLMRQIIGY", + "issuer": "raspberrypi", + "type": "OTP_TOTP", + "url": "otpauth://totp/pi%40raspberrypi?secret=7KSQL2JTUDIS5EF65KLMRQIIGY&issuer=raspberrypi" + } +] \ No newline at end of file diff --git a/extract_otp_secret_keys.py b/extract_otp_secret_keys.py index 94ad8db..788e91f 100644 --- a/extract_otp_secret_keys.py +++ b/extract_otp_secret_keys.py @@ -50,19 +50,7 @@ import json from urllib.parse import parse_qs, urlencode, urlparse, quote from os import path, mkdir from re import sub, compile as rcompile -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('--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') -arg_parser.add_argument('--json', '-j', help='export to json file') -arg_parser.add_argument('--csv', '-c', help='export to csv file') -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() - -if args.saveqr or args.printqr: from qrcode import QRCode -verbose = args.verbose +import protobuf_generated_python.google_auth_pb2 # https://stackoverflow.com/questions/40226049/find-enums-listed-in-python-descriptor-for-protobuf def get_enum_name_by_number(parent, field_name): @@ -73,6 +61,7 @@ def convert_secret_from_bytes_to_base32_str(bytes): return str(base64.b32encode(bytes), 'utf-8').replace('=', '') def save_qr(data, name): + global verbose qr = QRCode() qr.add_data(data) img = qr.make_image(fill_color='black', back_color='white') @@ -84,66 +73,107 @@ def print_qr(data): qr.add_data(data) qr.print_ascii() -otps = [] - -i = j = 0 -for line in (line.strip() for line in fileinput.input(args.infile)): - if verbose: print(line) - if line.startswith('#') or line == '': continue - if not line.startswith('otpauth-migration://'): print('\nWARN: line is not a otpauth-migration:// URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line)) - parsed_url = urlparse(line) - params = parse_qs(parsed_url.query) - if not 'data' in params: - print('\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line)) +def parse_args(sys_args): + arg_parser = argparse.ArgumentParser() + arg_parser.add_argument('--verbose', '-v', help='verbose output', action='store_true') + arg_parser.add_argument('--quiet', '-q', help='no stdout output', action='store_true') + 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') + arg_parser.add_argument('--json', '-j', help='export to json file') + arg_parser.add_argument('--csv', '-c', help='export to csv file') + 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(sys_args) + if args.verbose and args.quiet: + print("The arguments --verbose and --quite are mutual exclusive.") sys.exit(1) - data_encoded = params['data'][0] - data = base64.b64decode(data_encoded) - payload = generated_python.google_auth_pb2.MigrationPayload() - payload.ParseFromString(data) - i += 1 - if verbose: print('\n{}. Payload Line'.format(i), payload, sep='\n') - - # pylint: disable=no-member - for otp in payload.otp_parameters: - j += 1 - if verbose: print('\n{}. Secret Key'.format(j)) - else: print() - print('Name: {}'.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)) - otp_type = get_enum_name_by_number(otp, 'type') - print('Type: {}'.format(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', quote(otp.name)) + urlencode(url_params) - if verbose: print(otp_url) - if args.printqr: - print_qr(otp_url) - if args.saveqr: - if not(path.exists('qr')): mkdir('qr') - pattern = rcompile(r'[\W_]+') - file_otp_name = pattern.sub('', otp.name) - file_otp_issuer = pattern.sub('', otp.issuer) - save_qr(otp_url, 'qr/{}-{}{}.png'.format(j, file_otp_name, '-' + file_otp_issuer if file_otp_issuer else '')) - - 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))) + return args + +def sys_main(): + main(sys.argv[1:]) + +def main(sys_args): + global verbose, quiet + args = parse_args(sys_args) + if args.saveqr or args.printqr: from qrcode import QRCode + verbose = args.verbose + quiet = args.quiet + + otps = extract_otps(args) + write_csv(args, otps) + write_json(args, otps) + +def extract_otps(args): + global verbose, quiet + quiet = args.quiet + + otps = [] + + i = j = 0 + for line in (line.strip() for line in fileinput.input(args.infile)): + if verbose: print(line) + if line.startswith('#') or line == '': continue + if not line.startswith('otpauth-migration://'): print('\nWARN: line is not a otpauth-migration:// URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line)) + parsed_url = urlparse(line) + params = parse_qs(parsed_url.query) + if not 'data' in params: + print('\nERROR: no data query parameter in input URL\ninput file: {}\nline "{}"\nProbably a wrong file was given'.format(args.infile, line)) + sys.exit(1) + data_encoded = params['data'][0] + data = base64.b64decode(data_encoded) + payload = protobuf_generated_python.google_auth_pb2.MigrationPayload() + payload.ParseFromString(data) + i += 1 + if verbose: print('\n{}. Payload Line'.format(i), payload, sep='\n') + + # pylint: disable=no-member + for otp in payload.otp_parameters: + j += 1 + if verbose: print('\n{}. Secret Key'.format(j)) + if not quiet: print('Name: {}'.format(otp.name)) + secret = convert_secret_from_bytes_to_base32_str(otp.secret) + if not quiet: print('Secret: {}'.format(secret)) + if otp.issuer and not quiet: print('Issuer: {}'.format(otp.issuer)) + otp_type = get_enum_name_by_number(otp, 'type') + if not quiet: print('Type: {}'.format(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', quote(otp.name)) + urlencode(url_params) + if verbose: print(otp_url) + if args.printqr: + print_qr(otp_url) + if args.saveqr: + if not(path.exists('qr')): mkdir('qr') + pattern = rcompile(r'[\W_]+') + file_otp_name = pattern.sub('', otp.name) + file_otp_issuer = pattern.sub('', otp.issuer) + save_qr(otp_url, 'qr/{}-{}{}.png'.format(j, file_otp_name, '-' + file_otp_issuer if file_otp_issuer else '')) + if not quiet: print() + + otps.append({ + "name": otp.name, + "secret": secret, + "issuer": otp.issuer, + "type": otp_type, + "url": otp_url + }) + return otps + +def write_csv(args, otps): + global verbose, quiet + 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) + if not quiet: print("Exported {} otps to csv".format(len(otps))) + +def write_json(args, otps): + global verbose, quiet + if args.json: + with open(args.json, "w") as outfile: + json.dump(otps, outfile, indent = 4) + if not quiet: print("Exported {} otp entries to json".format(len(otps))) + +if __name__ == '__main__': + sys_main() diff --git a/generated_python/google_auth_pb2.py b/generated_python/google_auth_pb2.py deleted file mode 100644 index 62ecd04..0000000 --- a/generated_python/google_auth_pb2.py +++ /dev/null @@ -1,242 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: google_auth.proto -"""Generated protocol buffer code.""" -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/protobuf_generated_python/google_auth_pb2.py b/protobuf_generated_python/google_auth_pb2.py new file mode 100644 index 0000000..7099ee4 --- /dev/null +++ b/protobuf_generated_python/google_auth_pb2.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: google_auth.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(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') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'google_auth_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _MIGRATIONPAYLOAD._serialized_start=22 + _MIGRATIONPAYLOAD._serialized_end=461 + _MIGRATIONPAYLOAD_OTPPARAMETERS._serialized_start=176 + _MIGRATIONPAYLOAD_OTPPARAMETERS._serialized_end=359 + _MIGRATIONPAYLOAD_ALGORITHM._serialized_start=361 + _MIGRATIONPAYLOAD_ALGORITHM._serialized_end=405 + _MIGRATIONPAYLOAD_OTPTYPE._serialized_start=407 + _MIGRATIONPAYLOAD_OTPTYPE._serialized_end=461 +# @@protoc_insertion_point(module_scope) diff --git a/unittest_extract_otp_secret_keys.py b/unittest_extract_otp_secret_keys.py new file mode 100644 index 0000000..e101118 --- /dev/null +++ b/unittest_extract_otp_secret_keys.py @@ -0,0 +1,75 @@ +# Unit test for extract_otp_secret_keys.py + +# Run tests: +# python -m unittest + +# 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 unittest +import csv +import json +import os + +import extract_otp_secret_keys + +class TestExtract(unittest.TestCase): + + def test_extract_csv(self): + extract_otp_secret_keys.main(['-q', '-c', 'test_example_output.csv', 'example_export.txt']) + + expected_csv = read_csv('example_output.csv') + actual_csv = read_csv('test_example_output.csv') + + self.assertEqual(actual_csv, expected_csv) + + def test_extract_json(self): + extract_otp_secret_keys.main(['-q', '-j', 'test_example_output.json', 'example_export.txt']) + + expected_json = read_json('example_output.json') + actual_json = read_json('test_example_output.json') + + self.assertEqual(actual_json, expected_json) + + def setUp(self): + self.cleanup() + + def tearDown(self): + self.cleanup() + + def cleanup(self): + remove_file('test_example_output.csv') + remove_file('test_example_output.json') + +def remove_file(filename): + if os.path.exists(filename): os.remove(filename) + +def read_csv(filename): + """Returns a list of lines.""" + with open(filename, "r") as infile: + lines = [] + reader = csv.reader(infile) + for line in reader: + lines.append(line) + return lines + +def read_json(filename): + """Returns a list or a dictionary.""" + with open(filename, "r") as infile: + return json.load(infile) + + +if __name__ == '__main__': + unittest.main()