196 lines
6 KiB
Python
196 lines
6 KiB
Python
#!/usr/bin/env python3
|
|
# -*- encoding: utf-8 -*-
|
|
#
|
|
# This script is licensed under GNU GPL version 2.0 or above
|
|
# (c) 2023 Antonio J. Delgado
|
|
"""Convert andOTP json backups to FreeOTP format"""
|
|
|
|
import sys
|
|
import os
|
|
import logging
|
|
from logging.handlers import SysLogHandler
|
|
import click
|
|
import click_config_file
|
|
import json
|
|
# import base64
|
|
from urllib.parse import quote
|
|
|
|
|
|
class andotp2freeotp:
|
|
|
|
def __init__(self, **kwargs):
|
|
self.config = kwargs
|
|
if 'log_file' not in kwargs or kwargs['log_file'] is None:
|
|
self.config['log_file'] = os.path.join(
|
|
os.environ.get(
|
|
'HOME',
|
|
os.environ.get(
|
|
'USERPROFILE',
|
|
os.getcwd()
|
|
)
|
|
),
|
|
'log',
|
|
'andotp2freeotp.log'
|
|
)
|
|
self._init_log()
|
|
|
|
self._read_file()
|
|
if self.config['format'] == 'json':
|
|
self._log.error(
|
|
"""Sorry JSON format is not implemented yet. Pending on
|
|
https://github.com/freeotp/freeotp-android/issues/368"""
|
|
)
|
|
self._convert_to_json_data()
|
|
self._write_json_file()
|
|
elif self.config['format'] == 'uris':
|
|
self._convert_to_uris_data()
|
|
self._write_uris_file()
|
|
|
|
def _read_file(self):
|
|
with open(self.config['input'], 'r') as filepointer:
|
|
self.data = json.load(filepointer)
|
|
|
|
def _convert_to_json_data(self):
|
|
self.new_data = {
|
|
"tokenOrder": [],
|
|
"tokens": []
|
|
}
|
|
for item in self.data:
|
|
new_item = {
|
|
"algo": item['algorithm'],
|
|
"counter": 0,
|
|
"digits": item['digits'],
|
|
"issuerExt": item['issuer'],
|
|
"label": item['label'],
|
|
"period": item['period'],
|
|
# "secret": [
|
|
# -32,
|
|
# -17,
|
|
# -127,
|
|
# -72,
|
|
# -1,
|
|
# -57,
|
|
# -105,
|
|
# 22,
|
|
# 53,
|
|
# -54
|
|
# ],
|
|
# 73G5LA12XF6IYUQF
|
|
"type": item['type'],
|
|
# This is the way around
|
|
# "secret": base64.b32encode(
|
|
# bytes(x & 0xff for x in item["secret"])
|
|
# ).decode("utf8")
|
|
}
|
|
self.new_data['tokens'].append(new_item)
|
|
self.new_data['tokenOrder'].append(
|
|
f"{item['issuer']}:{item['label']}"
|
|
)
|
|
|
|
def _write_json_file(self):
|
|
if self.config['output'] == '-':
|
|
json.dumps(self.new_data, indent=2)
|
|
else:
|
|
with open(self.config['output'], 'w') as filepointer:
|
|
json.dump(self.new_data, filepointer)
|
|
|
|
def _convert_to_uris_data(self):
|
|
self.uris = []
|
|
for item in self.data:
|
|
uri_type = item['type'].lower()
|
|
label = quote(f"{item['label']}@{item['issuer']}")
|
|
uri = f"otpauth://{uri_type}/{label}?secret={item['secret']}"
|
|
other_fields = [
|
|
'algorithm', 'digits', 'period', 'issuer', 'counter'
|
|
]
|
|
for field in other_fields:
|
|
if field in item and item[field] != '':
|
|
uri += f"&{field}={item[field]}"
|
|
self.uris.append(uri)
|
|
|
|
def _write_uris_file(self):
|
|
if self.config['output'] == '-':
|
|
for uri in self.uris:
|
|
print(uri)
|
|
else:
|
|
with open(self.config['output'], 'w') as filepointer:
|
|
for uri in self.uris:
|
|
filepointer.write(f"{uri}\n")
|
|
|
|
def _init_log(self):
|
|
''' Initialize log object '''
|
|
self._log = logging.getLogger("andotp2freeotp")
|
|
self._log.setLevel(logging.DEBUG)
|
|
|
|
sysloghandler = SysLogHandler()
|
|
sysloghandler.setLevel(logging.DEBUG)
|
|
self._log.addHandler(sysloghandler)
|
|
|
|
streamhandler = logging.StreamHandler(sys.stdout)
|
|
streamhandler.setLevel(
|
|
logging.getLevelName(self.config.get("debug_level", 'INFO'))
|
|
)
|
|
self._log.addHandler(streamhandler)
|
|
|
|
if 'log_file' in self.config:
|
|
log_file = self.config['log_file']
|
|
else:
|
|
home_folder = os.environ.get(
|
|
'HOME', os.environ.get('USERPROFILE', '')
|
|
)
|
|
log_folder = os.path.join(home_folder, "log")
|
|
log_file = os.path.join(log_folder, "andotp2freeotp.log")
|
|
|
|
if not os.path.exists(os.path.dirname(log_file)):
|
|
os.mkdir(os.path.dirname(log_file))
|
|
|
|
filehandler = logging.handlers.RotatingFileHandler(
|
|
log_file, maxBytes=102400000
|
|
)
|
|
# create formatter
|
|
formatter = logging.Formatter(
|
|
'%(asctime)s %(name)-12s %(levelname)-8s %(message)s'
|
|
)
|
|
filehandler.setFormatter(formatter)
|
|
filehandler.setLevel(logging.DEBUG)
|
|
self._log.addHandler(filehandler)
|
|
return True
|
|
|
|
|
|
@click.command()
|
|
@click.option(
|
|
"--debug-level",
|
|
"-d",
|
|
default="INFO",
|
|
type=click.Choice(
|
|
["CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG", "NOTSET"],
|
|
case_sensitive=False,
|
|
),
|
|
help='Set the debug level for the standard output.'
|
|
)
|
|
@click.option('--log-file', '-l', help="File to store all debug messages.")
|
|
# @click.option("--dummy","-n", is_flag=True,
|
|
# help="Don't do anything, just show what would be done.")
|
|
@click.option(
|
|
'--input', '-i', required=True, help='JSON file from andOTP to convert'
|
|
)
|
|
@click.option(
|
|
'--output', '-o', required=True, help='JSON/URI file to write for FreeOTP'
|
|
)
|
|
@click.option(
|
|
'--format',
|
|
'-f',
|
|
default='uris',
|
|
type=click.Choice(
|
|
['uris', 'json'],
|
|
case_sensitive=False,
|
|
),
|
|
help='Format for the output',
|
|
)
|
|
@click_config_file.configuration_option()
|
|
def __main__(**kwargs):
|
|
return andotp2freeotp(**kwargs)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
__main__()
|