andotp2Freeotp/andotp2freeotp/andotp2freeotp.py

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__()