File: //usr/syno/bin/synokmip
#!/usr/bin/python3
from kmip.core import enums
from kmip.core.enums import KeyFormatType
from kmip.core.enums import ObjectType
from kmip.core.enums import Operation
from kmip.core.enums import ResultStatus
from kmip.core.factories.attributes import AttributeFactory
from kmip.core.objects import TemplateAttribute
from kmip.synology import utils
from kmip.synology import get
from kmip.synology import deactivate
from kmip.synology import destroy
from kmip.synology import activate
from kmip.synology import register_opaque_object
from kmip.synology import register_secret_data
from kmip.synology import get_attributes
from kmip.synology import modify_name
from kmip.synology import get_attributes
from kmip.synology import locate
from kmip.synology import query
import logging
import sys
import argparse
import json
import time
import datetime
import subprocess
import xml.etree.ElementTree as xmlET
import fcntl
import hashlib
import random
import string
from os import listdir
from os.path import isfile, join, exists
from pathlib import Path
SZF_KMIP_CLIENT_ENABLED = "/etc/pykmip/kmip_client_enabled"
SZF_KMIP_RETRIVE_REC = "/etc/pykmip/retrive_rec.json"
SZF_SYNOKMIP_LOCK = "/run/pykmip/synokmip.lock"
lock_file = None
def parse_action_argument(input_arg):
parser = argparse.ArgumentParser(description='Synology KMIP Tool')
parser.add_argument(
'--reg_opaque_obj',
dest='reg_opaque_obj',
action='store_true',
help='register opaque object'
)
parser.add_argument(
'--reg_sec_data',
dest='reg_sec_data',
action='store_true',
help='register secret data'
)
parser.add_argument(
'--get',
dest='get',
action='store_true',
help='get'
)
parser.add_argument(
'--deactivate',
dest='deactivate',
action='store_true',help='deactivate'
)
parser.add_argument(
'--destroy',
dest='destroy',
action='store_true',
help='destroy'
)
parser.add_argument(
'--activate',
dest='activate',
action='store_true',
help='activate'
)
parser.add_argument(
'--get_attributes',
dest='get_attributes',
action='store_true',
help='get_attributes'
)
parser.add_argument(
'--modify_name',
dest='modify_name',
action='store_true',
help='modify_name'
)
parser.add_argument(
'--locate',
dest='locate',
action='store_true',
help='locate'
)
parser.add_argument(
'--get_with_name',
dest='get_with_name',
action='store_true',
help='get with name'
)
parser.add_argument(
'--activate_with_name',
dest='activate_with_name',
action='store_true',
help='activate with name'
)
parser.add_argument(
'--deactivate_with_name',
dest='deactivate_with_name',
action='store_true',
help='deactivate with name'
)
parser.add_argument(
'--destroy_with_name',
dest='destroy_with_name',
action='store_true',
help='destroy with name'
)
parser.add_argument(
'--destroy_all_in_vault',
dest='destroy_all_in_vault',
action='store_true',
help='destroy all in vault'
)
parser.add_argument(
'--query',
dest='query',
action='store_true',
help='query'
)
parser.add_argument(
'--get_key_attr_list',
dest='get_key_attr_list',
action='store_true',
help='get_key_attr_list'
)
parser.add_argument(
'--get_server_hostname',
dest='get_server_hostname',
action='store_true',
help='get_server_hostname'
)
opts, left = parser.parse_known_args(input_arg)
sys.argv = sys.argv[:1]+left
return opts
def parse_secondary_argument(input_arg, action):
parser = argparse.ArgumentParser(description='Synology KMIP Tool')
if (action.get or
action.activate or
action.deactivate or
action.get_attributes or
action.modify_name or
action.destroy):
parser.add_argument(
'--uuid',
dest='uuid',
type=str,
help='uuid for get/get_attributes/activate/deactivate/modify'
)
if (action.locate):
parser.add_argument(
'--name',
dest='key_name',
type=str,
help='(optional) key name for locate'
)
if (action.get_with_name or
action.reg_opaque_obj or
action.reg_sec_data or
action.modify_name or
action.activate_with_name or
action.deactivate_with_name or
action.destroy_with_name):
parser.add_argument(
'--name',
dest='key_name',
type=str,
help='key name for register/modify_name/get_with_name'
)
if (action.reg_opaque_obj or
action.reg_sec_data):
parser.add_argument(
'--infile',
dest='infile',
type=str,
help='data file path for register'
)
if (action.get or
action.get_with_name or
action.reg_opaque_obj or
action.reg_sec_data or
action.get_key_attr_list or
action.get_server_hostname):
parser.add_argument(
'--outfile',
dest='outfile',
type=str,
help='(optional) data file path for get/register, output to the file if specified'
)
if (action.destroy_all_in_vault):
parser.add_argument(
'--vault_path',
dest='vault_path',
type=str,
help='vault path for destroy'
)
if (len(input_arg) == 0 and
not action.locate and
not action.query):
input_arg = ['-h']
opts, left = parser.parse_known_args(input_arg)
sys.argv = sys.argv[:1]+left
return opts
def read_json(file):
if exists(file):
try:
with open(file, 'r') as f:
obj = json.load(f)
except Exception:
raise Exception("Load {0} failed.".format(file))
return obj
else:
Path(file).touch()
return {}
def write_json(obj, file):
try:
with open(file, 'w') as f:
json.dump(obj, f)
except Exception:
raise Exception("Write {0} failed.".format(file))
def getVolumeInfo():
spaceMappingTree = xmlET.parse('/run/space/space_mapping.xml')
spaceMappingRoot = spaceMappingTree.getroot()
vols = []
webapi_ret = subprocess.Popen("synowebapi --exec api=SYNO.Core.Storage.Volume method=list outfile=apitest limit=-1 offset=0 location=\"internal\" option=\"include_cold_storage\"", shell=True, stdout=subprocess.PIPE).stdout.read()
webapi_ret_json = json.loads(webapi_ret)
for volume in spaceMappingRoot.iter('volume'):
new_vol = {}
new_vol['path'] = volume.attrib['path']
new_vol['uuid'] = volume.attrib['uuid']
new_vol['dev_path'] = volume.attrib['dev_path']
new_vol['luks_uuid'] = subprocess.Popen(["/usr/sbin/cryptsetup", "luksUUID", new_vol['dev_path']], stdout=subprocess.PIPE).stdout.read().strip().decode("utf-8")
for volinfo in webapi_ret_json['data']['volumes']:
if new_vol['path'] == volinfo['volume_path']:
new_vol['display_name'] = volinfo['display_name']
break;
else:
new_vol['display_name'] = ""
vols.append(new_vol)
return vols
def acquire_lock():
wait_count = 0
locked = True
if not exists(SZF_SYNOKMIP_LOCK):
Path(SZF_SYNOKMIP_LOCK).touch()
lock_file = open(SZF_SYNOKMIP_LOCK, 'w')
while locked:
try:
fcntl.lockf(lock_file, fcntl.LOCK_EX|fcntl.LOCK_NB)
locked = False
except IOError:
if wait_count > 100:
raise Exception("Get lock timeout.")
time.sleep(0.1)
wait_count += 1
return True
def release_lock():
if lock_file is not None:
fcntl.lockf(lock_file, fcntl.LOCK_UN)
lock_file.close()
def query_server_hostname(logger):
ret = {"hostname": "", "is_synology_server": False}
vendor_id = query.query(logger, 'VENDOR_ID')
if vendor_id.startswith('Synology KMIP Server'):
ret['hostname'] = vendor_id.split(':')[1].strip()
ret['is_synology_server'] = True
ret['vendor'] = 'Synology'
else:
ret['hostname'] = vendor_id
ret['is_synology_server'] = False
ret['vendor'] = vendor_id
return ret
def check_is_synology_server(logger):
return query_server_hostname(logger)['is_synology_server']
def get_policy_name_for_reg_operation(logger):
if check_is_synology_server(logger) is True:
return 'synology'
else:
return 'default'
def get_random_string(length):
return ''.join(random.choice(string.ascii_letters) for i in range(length))
def get_key_name_for_reg_operation(logger, key_name):
if check_is_synology_server(logger) is True:
return key_name
else:
seed_str = get_random_string(1024)
return hashlib.sha512(seed_str.encode()).hexdigest()
def check_same_name_key(logger, key_name):
if check_is_synology_server(logger) is not True:
uuids = locate.locate(logger, key_name)
if len(uuids) > 0:
for id in uuids:
seed_str = get_random_string(1024)
name_postfix = hashlib.sha512(seed_str.encode()).hexdigest()[:8]
modify_name.modify_name(logger, name=key_name + '-deprecated-' + name_postfix, uuid=id)
def main():
action_arg = parse_action_argument(sys.argv[1:])
arg = parse_secondary_argument(sys.argv[1:], action_arg)
logger = utils.build_console_logger(logging.INFO)
acquire_lock()
if action_arg.reg_opaque_obj:
with open(arg.infile, 'rb') as f:
data = f.read()
policy_name = get_policy_name_for_reg_operation(logger)
check_same_name_key(logger, arg.key_name)
key_name = get_key_name_for_reg_operation(logger, arg.key_name)
uuid = register_opaque_object.register_opaque_object(logger, data, key_name, policy_name)
if isinstance(arg.outfile, str):
with open(arg.outfile, 'w') as f:
f.write(uuid)
logger.info("uuid write to {0}".format(arg.outfile))
if action_arg.reg_sec_data:
with open(arg.infile, 'rb') as f:
data = f.read()
policy_name = get_policy_name_for_reg_operation(logger)
check_same_name_key(logger, arg.key_name)
key_name = get_key_name_for_reg_operation(logger, arg.key_name)
uuid = register_secret_data.register_secret_data(logger, data, key_name, policy_name)
if key_name != arg.key_name:
modify_name.modify_name(logger, name=arg.key_name, uuid=uuid)
if isinstance(arg.outfile, str):
with open(arg.outfile, 'w') as f:
f.write(uuid)
logger.info("uuid write to {0}".format(arg.outfile))
elif action_arg.get:
data = get.get(logger, arg.uuid)
if isinstance(arg.outfile, str):
with open(arg.outfile, 'wb') as f:
f.write(data.value)
logger.info("data write to {0}".format(arg.outfile))
elif action_arg.destroy:
destroy.destroy(logger, arg.uuid)
elif action_arg.deactivate:
deactivate.deactivate(logger, arg.uuid)
elif action_arg.activate:
activate.activate(logger, arg.uuid)
elif action_arg.get_attributes:
get_attributes.get_attributes(logger, arg.uuid)
elif action_arg.modify_name:
modify_name.modify_name(logger, name=arg.key_name, uuid=arg.uuid)
elif action_arg.locate:
locate.locate(logger, arg.key_name)
elif action_arg.query:
query.query(logger)
elif (action_arg.get_with_name or
action_arg.activate_with_name or
action_arg.deactivate_with_name or
action_arg.destroy_with_name):
if not isinstance(arg.key_name, str):
logger.info("key name not provided")
sys.exit(-1)
uuid = locate.locate(logger, arg.key_name)
if len(uuid) == 0:
logger.info("cannot find key with name '{0}'".format(arg.key_name))
sys.exit(-1)
if len(uuid) > 1:
logger.warning("multiple key with name '{0}' found, action on the latest result".format(arg.key_name))
if action_arg.get_with_name:
data = get.get(logger, uuid[0])
if isinstance(arg.outfile, str):
with open(arg.outfile, 'wb') as f:
f.write(data.value)
logger.info("data write to {0}".format(arg.outfile))
rec_obj = read_json(SZF_KMIP_RETRIVE_REC)
time = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
rec_obj[arg.key_name] = time
write_json(rec_obj, SZF_KMIP_RETRIVE_REC)
elif action_arg.activate_with_name:
activate.activate(logger, uuid[0])
elif action_arg.deactivate_with_name:
deactivate.deactivate(logger, uuid[0])
elif action_arg.destroy_with_name:
destroy.destroy(logger, uuid[0])
elif action_arg.destroy_all_in_vault:
files = listdir(arg.vault_path)
for f in files:
fullpath = join(arg.vault_path, f)
if isfile(fullpath) and f.endswith(".wkey.shadow"):
key_name = f.split('.')[0]
uuid = locate.locate(logger, key_name)
if len(uuid) == 0:
logger.info("cannot find key with name '{0}'".format(key_name))
sys.exit(-1)
if len(uuid) > 1:
logger.warning("multiple key with name '{0}' found, action on the latest result".format(key_name))
destroy.destroy(logger, uuid[0])
elif action_arg.get_key_attr_list:
uuids = locate.locate(logger, None)
output_data = []
volume_info = getVolumeInfo()
for uid in uuids:
data = get_attributes.get_attributes(logger, uid)
dict = {}
rec_obj = read_json(SZF_KMIP_RETRIVE_REC)
for attr in data:
if "Unique Identifier" == str(attr.attribute_name):
dict['uuid'] = str(attr.attribute_value)
elif "Name" == str(attr.attribute_name):
dict['name'] = str(attr.attribute_value)
elif "Object Type" == str(attr.attribute_name):
dict['obj_type'] = str(attr.attribute_value)
elif "Operation Policy Name" == str(attr.attribute_name):
dict['policy_name'] = str(attr.attribute_value)
elif "Cryptographic Usage Mask" == str(attr.attribute_name):
dict['crypto_usage_mask'] = str(attr.attribute_value)
elif "State" == str(attr.attribute_name):
dict['state'] = str(attr.attribute_value)
elif "Initial Date" == str(attr.attribute_name):
dict['init_date'] = str(attr.attribute_value)
if dict['name'] in rec_obj:
dict['last_retrive'] = rec_obj[dict['name']]
else:
dict['last_retrive'] = ''
if 0 < len(volume_info):
for i in range(len(volume_info)):
if dict['name'] == volume_info[i]['luks_uuid']:
dict['vol_path'] = volume_info[i]['path']
dict['luks_uuid'] = volume_info[i]['luks_uuid']
dict['display_name'] = volume_info[i]['display_name']
break;
else:
dict['vol_path'] = ""
dict['luks_uuid'] = ""
dict['display_name'] = ""
init_time = datetime.datetime.strptime(dict['init_date'], "%a %b %d %H:%M:%S %Y")
dict['init_date'] = init_time.strftime("%Y-%m-%d %H:%M:%S")
output_data.append(dict)
if isinstance(arg.outfile, str):
with open(arg.outfile, 'w') as f:
f.write(json.dumps(output_data))
logger.info("data write to {0}".format(arg.outfile))
elif action_arg.get_server_hostname:
query_ret = query_server_hostname(logger)
if isinstance(arg.outfile, str):
with open(arg.outfile, 'w') as f:
f.write("server_hostname=\"{0}\"\n".format(query_ret['hostname']))
f.write("vendor=\"{0}\"\n".format(query_ret['vendor']))
logger.info("data write to {0}".format(arg.outfile))
else:
logger.info("invalid action")
release_lock()
if __name__ == '__main__':
Path("/run/pykmip").mkdir(parents=True, exist_ok=True)
try:
main()
except Exception:
with open("/etc/pykmip/client_conn_record", 'w') as f:
f.write("status=\"{0}\"\n".format("failed"))
f.write("time=\"{0}\"\n".format(time.ctime()))
sys.exit(1)
else:
with open("/etc/pykmip/client_conn_record", 'w') as f:
f.write("status=\"{0}\"\n".format("success"))
f.write("time=\"{0}\"\n".format(time.ctime()))
sys.exit(0)