From ffeedeeeed12fbd2c3075238248a8a04cec159e1 Mon Sep 17 00:00:00 2001 From: fxiang21 <fxiang21@126.com> Date: Thu, 26 Oct 2023 09:43:58 +0800 Subject: [PATCH 1/7] add secrets,for test --- cmdb-api/api/lib/secrets/__init__.py | 1 + cmdb-api/api/lib/secrets/inner.py | 96 ++++++++++++++++++++++++++++ cmdb-api/requirements.txt | 2 + 3 files changed, 99 insertions(+) create mode 100644 cmdb-api/api/lib/secrets/__init__.py create mode 100644 cmdb-api/api/lib/secrets/inner.py diff --git a/cmdb-api/api/lib/secrets/__init__.py b/cmdb-api/api/lib/secrets/__init__.py new file mode 100644 index 0000000..380474e --- /dev/null +++ b/cmdb-api/api/lib/secrets/__init__.py @@ -0,0 +1 @@ +# -*- coding:utf-8 -*- diff --git a/cmdb-api/api/lib/secrets/inner.py b/cmdb-api/api/lib/secrets/inner.py new file mode 100644 index 0000000..bbb3bea --- /dev/null +++ b/cmdb-api/api/lib/secrets/inner.py @@ -0,0 +1,96 @@ +from base64 import b64encode, b64decode +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives import padding +import os +import sys + +# global_root_key just for test here +global_root_key = "4OIzj9ztvfu/qUbzUkjvH54jVC0xGyVaWlemotx6PC0=" +global_iv_length = 16 + + +def string_to_bytes(value): + if sys.version_info.major == 2: + byte_string = value + else: + byte_string = value.encode("utf-8") + return byte_string + + +class KeyMange: + + def __init__(self): + pass + + @staticmethod + def generate_unseal_keys(): + root_key = AESGCM.generate_key(256) + return root_key + + @staticmethod + def generate_key(): + return AESGCM.generate_key(256) + + def _acquire(self): + """ + get encryption key from backend storage + :return: + """ + return + + def init(self): + """ + init the master key, unseal key. + :return: + """ + @staticmethod + def is_seal(): + return global_root_key == b'' + + +class InnerCrypt: + def __init__(self): + self.encrypt_key = b64decode(global_root_key.encode("utf-8")) + + def encrypt(self, plaintext): + status = True + encrypt_value = self.aes_encrypt(plaintext) + return encrypt_value, status + + def decrypt(self, ciphertext): + status = True + decrypt_value = self.aes_decrypt(ciphertext) + return decrypt_value, status + + def aes_encrypt(self, plaintext): + if isinstance(plaintext, str): + plaintext = string_to_bytes(plaintext) + iv = os.urandom(global_iv_length) + cipher = Cipher(algorithms.AES(self.encrypt_key), modes.CBC(iv), backend=default_backend()) + encryptor = cipher.encryptor() + padder = padding.PKCS7(algorithms.AES.block_size).padder() + padded_plaintext = padder.update(plaintext) + padder.finalize() + ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize() + return b64encode(iv+ciphertext).decode('utf-8') + + def aes_decrypt(self, ciphertext): + s = b64decode(ciphertext.encode("utf-8")) + iv = s[:global_iv_length] + ciphertext = s[global_iv_length:] + cipher = Cipher(algorithms.AES(self.encrypt_key), modes.CBC(iv), backend=default_backend()) + decryptor = cipher.decryptor() + decrypted_padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize() + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() + plaintext = unpadder.update(decrypted_padded_plaintext) + unpadder.finalize() + return plaintext.decode('utf-8') + + +if __name__ == "__main__": + t_plaintext = "Hello, World!" # The plaintext to encrypt + c = InnerCrypt() + t_ciphertext = c.aes_encrypt(t_plaintext) + print("Ciphertext:", t_ciphertext) + decrypted_plaintext = c.aes_decrypt(t_ciphertext) + print("Decrypted plaintext:", decrypted_plaintext) diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index 8f92c05..f232175 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -47,3 +47,5 @@ toposort==1.10 treelib==1.6.1 Werkzeug==2.3.6 WTForms==3.0.0 +shamir~=17.12.0 +hvac~=2.0.0 From c4f2a89be15a7b262e940fadce00c73fb133fee3 Mon Sep 17 00:00:00 2001 From: Mimo <osatmnzn@gmail.com> Date: Thu, 26 Oct 2023 16:56:30 +0800 Subject: [PATCH 2/7] feat: vault SDK (#238) * feat: vault SDK * docs: i18n --- cmdb-api/Pipfile | 1 + cmdb-api/api/lib/secrets/vault.py | 139 ++++++++++++++++++++++++++++++ cmdb-api/requirements.txt | 1 + 3 files changed, 141 insertions(+) create mode 100644 cmdb-api/api/lib/secrets/vault.py diff --git a/cmdb-api/Pipfile b/cmdb-api/Pipfile index 20e831a..61ed9c2 100644 --- a/cmdb-api/Pipfile +++ b/cmdb-api/Pipfile @@ -59,6 +59,7 @@ Jinja2 = "==3.1.2" jinja2schema = "==0.1.4" msgpack-python = "==0.5.6" alembic = "==1.7.7" +hvac = "==2.0.0" [dev-packages] # Testing diff --git a/cmdb-api/api/lib/secrets/vault.py b/cmdb-api/api/lib/secrets/vault.py new file mode 100644 index 0000000..66ce43d --- /dev/null +++ b/cmdb-api/api/lib/secrets/vault.py @@ -0,0 +1,139 @@ +import hvac +from base64 import b64encode, b64decode + + +class VaultSDK: + def __init__(self, base_url, token, mount_path): + self.client = hvac.Client(url=base_url, token=token) + self.mount_path = mount_path + + def create_approle(self, role_name, policies): + resp = self.client.create_approle(role_name, policies=policies) + return resp == 200 + + def delete_approle(self, role_name): + resp = self.client.delete_approle(role_name) + return resp == 204 + + def update_approle_policies(self, role_name, policies): + resp = self.client.update_approle_role(role_name, policies=policies) + return resp == 204 + + def get_approle(self, role_name): + resp = self.client.get_approle(role_name) + resp.json() + if resp.status_code == 200: + return resp.json + else: + return {} + + def enable_secrets_engine(self): + resp = self.client.sys.enable_secrets_engine('kv', path=self.mount_path) + resp_01 = self.client.sys.enable_secrets_engine('transit') + + if resp.status_code == 200 and resp_01.status_code == 200: + return resp.json + else: + return {} + + def encrypt_data(self, plaintext): + response = self.client.secrets.transit.encrypt_data(name='transit-key', plaintext=plaintext) + ciphertext = response['data']['ciphertext'] + return ciphertext + + # decrypt data + def decrypt_data(self, ciphertext): + response = self.client.secrets.transit.decrypt_data(name='transit-key', ciphertext=ciphertext) + plaintext = response['data']['plaintext'] + return plaintext + + def write_data(self, path, data, encrypt=None): + if encrypt: + for k, v in data.items(): + data[k] = self.encrypt_data(self.encode_base64(v)) + response = self.client.secrets.kv.v2.create_or_update_secret( + path=path, + secret=data, + mount_point=self.mount_path + ) + + return response + + # read data + def read_data(self, path, decrypt=None): + try: + response = self.client.secrets.kv.v2.read_secret_version( + path=path, raise_on_deleted_version=False, mount_point=self.mount_path + ) + except Exception as e: + return str(e), False + data = response['data']['data'] + if decrypt: + try: + for k, v in data.items(): + data[k] = self.decode_base64(self.decrypt_data(v)) + except Exception: + return data, True + return data, True + + # update data + def update_data(self, path, data, overwrite=None, encrypt=None): + if encrypt: + for k, v in data.items(): + data[k] = self.encrypt_data(self.encode_base64(v)) + if overwrite: + response = self.client.secrets.kv.v2.create_or_update_secret( + path=path, + secret=data, + mount_point=self.mount_path + ) + else: + response = self.client.secrets.kv.v2.patch(path=path, secret=data, mount_point=self.mount_path) + return response + + # delete data + def delete_data(self, path): + response = self.client.secrets.kv.v2.delete_metadata_and_all_versions( + path=path, + mount_point=self.mount_path + ) + return response + + # Base64 encode + @classmethod + def encode_base64(cls, data): + encoded_bytes = b64encode(data.encode()) + encoded_string = encoded_bytes.decode() + return encoded_string + + # Base64 decode + @classmethod + def decode_base64(cls, encoded_string): + decoded_bytes = b64decode(encoded_string) + decoded_string = decoded_bytes.decode() + return decoded_string + + +class VaultAppRoleSDK(VaultSDK): + def __int__(self, base_url, role_id, secret_id): + self.base_url = base_url + self.role_id = role_id + self.secret_id = secret_id + self.token = self.get_token() + super(VaultAppRoleSDK, self).__int__(base_url, self.token) + + +if __name__ == "__main__": + base_url = "http://localhost:8200" + token = "hvs.OALuhK2wToxsn2Z1WFnDaKRw" + mount_path = "cmdb" + + path = "test001" + # Example + sdk = VaultSDK(base_url, token, mount_path) + # sdk.enable_secrets_engine() + data = {"key1": "value1", "key2": "value2", "key3": "value3"} + data = sdk.update_data(path, data, overwrite=True, encrypt=True) + print(data.status_code) + data = sdk.read_data(path, decrypt=True) + print(data) diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index f232175..6ad5a2c 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -18,6 +18,7 @@ Flask-RESTful==0.3.10 Flask-SQLAlchemy==2.5.0 future==0.18.3 gunicorn==21.0.1 +hvac==2.0.0 itsdangerous==2.1.2 Jinja2==3.1.2 jinja2schema==0.1.4 From 5bede8ad73304ffa06c2fb82b78014d06744cd40 Mon Sep 17 00:00:00 2001 From: pycook <pycook@126.com> Date: Thu, 26 Oct 2023 17:39:50 +0800 Subject: [PATCH 3/7] perf(vault): format code --- cmdb-api/api/lib/secrets/vault.py | 74 ++++++++++++++++--------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/cmdb-api/api/lib/secrets/vault.py b/cmdb-api/api/lib/secrets/vault.py index 66ce43d..85bc6db 100644 --- a/cmdb-api/api/lib/secrets/vault.py +++ b/cmdb-api/api/lib/secrets/vault.py @@ -1,25 +1,30 @@ +from base64 import b64decode +from base64 import b64encode + import hvac -from base64 import b64encode, b64decode -class VaultSDK: - def __init__(self, base_url, token, mount_path): +class VaultClient: + def __init__(self, base_url, token, mount_path='cmdb'): self.client = hvac.Client(url=base_url, token=token) self.mount_path = mount_path - def create_approle(self, role_name, policies): + def create_app_role(self, role_name, policies): resp = self.client.create_approle(role_name, policies=policies) + return resp == 200 - def delete_approle(self, role_name): + def delete_app_role(self, role_name): resp = self.client.delete_approle(role_name) + return resp == 204 - def update_approle_policies(self, role_name, policies): + def update_app_role_policies(self, role_name, policies): resp = self.client.update_approle_role(role_name, policies=policies) + return resp == 204 - def get_approle(self, role_name): + def get_app_role(self, role_name): resp = self.client.get_approle(role_name) resp.json() if resp.status_code == 200: @@ -36,21 +41,23 @@ class VaultSDK: else: return {} - def encrypt_data(self, plaintext): + def encrypt(self, plaintext): response = self.client.secrets.transit.encrypt_data(name='transit-key', plaintext=plaintext) ciphertext = response['data']['ciphertext'] + return ciphertext # decrypt data - def decrypt_data(self, ciphertext): + def decrypt(self, ciphertext): response = self.client.secrets.transit.decrypt_data(name='transit-key', ciphertext=ciphertext) plaintext = response['data']['plaintext'] + return plaintext - def write_data(self, path, data, encrypt=None): + def write(self, path, data, encrypt=None): if encrypt: for k, v in data.items(): - data[k] = self.encrypt_data(self.encode_base64(v)) + data[k] = self.encrypt(self.encode_base64(v)) response = self.client.secrets.kv.v2.create_or_update_secret( path=path, secret=data, @@ -60,7 +67,7 @@ class VaultSDK: return response # read data - def read_data(self, path, decrypt=None): + def read(self, path, decrypt=True): try: response = self.client.secrets.kv.v2.read_secret_version( path=path, raise_on_deleted_version=False, mount_point=self.mount_path @@ -71,16 +78,17 @@ class VaultSDK: if decrypt: try: for k, v in data.items(): - data[k] = self.decode_base64(self.decrypt_data(v)) - except Exception: + data[k] = self.decode_base64(self.decrypt(v)) + except: return data, True + return data, True # update data - def update_data(self, path, data, overwrite=None, encrypt=None): + def update(self, path, data, overwrite=True, encrypt=True): if encrypt: for k, v in data.items(): - data[k] = self.encrypt_data(self.encode_base64(v)) + data[k] = self.encrypt(self.encode_base64(v)) if overwrite: response = self.client.secrets.kv.v2.create_or_update_secret( path=path, @@ -89,14 +97,16 @@ class VaultSDK: ) else: response = self.client.secrets.kv.v2.patch(path=path, secret=data, mount_point=self.mount_path) + return response # delete data - def delete_data(self, path): + def delete(self, path): response = self.client.secrets.kv.v2.delete_metadata_and_all_versions( path=path, mount_point=self.mount_path ) + return response # Base64 encode @@ -104,6 +114,7 @@ class VaultSDK: def encode_base64(cls, data): encoded_bytes = b64encode(data.encode()) encoded_string = encoded_bytes.decode() + return encoded_string # Base64 decode @@ -111,29 +122,20 @@ class VaultSDK: def decode_base64(cls, encoded_string): decoded_bytes = b64decode(encoded_string) decoded_string = decoded_bytes.decode() + return decoded_string -class VaultAppRoleSDK(VaultSDK): - def __int__(self, base_url, role_id, secret_id): - self.base_url = base_url - self.role_id = role_id - self.secret_id = secret_id - self.token = self.get_token() - super(VaultAppRoleSDK, self).__int__(base_url, self.token) - - if __name__ == "__main__": - base_url = "http://localhost:8200" - token = "hvs.OALuhK2wToxsn2Z1WFnDaKRw" - mount_path = "cmdb" + _base_url = "http://localhost:8200" + _token = "your token" - path = "test001" + _path = "test001" # Example - sdk = VaultSDK(base_url, token, mount_path) + sdk = VaultClient(_base_url, _token) # sdk.enable_secrets_engine() - data = {"key1": "value1", "key2": "value2", "key3": "value3"} - data = sdk.update_data(path, data, overwrite=True, encrypt=True) - print(data.status_code) - data = sdk.read_data(path, decrypt=True) - print(data) + _data = {"key1": "value1", "key2": "value2", "key3": "value3"} + _data = sdk.update(_path, _data, overwrite=True, encrypt=True) + print(_data.status_code) + _data = sdk.read(_path, decrypt=True) + print(_data) From c808b2cf4b99a3ca84cf5545c716345579113aee Mon Sep 17 00:00:00 2001 From: pycook <pycook@126.com> Date: Thu, 26 Oct 2023 18:21:44 +0800 Subject: [PATCH 4/7] feat(secrets): support vault --- cmdb-api/api/lib/cmdb/ci.py | 122 +++++++++++++++++++++++++-- cmdb-api/api/lib/cmdb/const.py | 2 + cmdb-api/api/lib/cmdb/resp_format.py | 3 + cmdb-api/api/lib/cmdb/utils.py | 16 +++- cmdb-api/api/lib/cmdb/value.py | 8 +- cmdb-api/api/views/cmdb/ci.py | 18 ++-- cmdb-api/settings.example.py | 5 ++ 7 files changed, 154 insertions(+), 20 deletions(-) diff --git a/cmdb-api/api/lib/cmdb/ci.py b/cmdb-api/api/lib/cmdb/ci.py index 3951426..785a2db 100644 --- a/cmdb-api/api/lib/cmdb/ci.py +++ b/cmdb-api/api/lib/cmdb/ci.py @@ -29,6 +29,7 @@ from api.lib.cmdb.const import PermEnum from api.lib.cmdb.const import REDIS_PREFIX_CI from api.lib.cmdb.const import ResourceTypeEnum from api.lib.cmdb.const import RetKey +from api.lib.cmdb.const import ValueTypeEnum from api.lib.cmdb.history import AttributeHistoryManger from api.lib.cmdb.history import CIRelationHistoryManager from api.lib.cmdb.history import CITriggerHistoryManager @@ -42,6 +43,8 @@ from api.lib.notify import notify_send from api.lib.perm.acl.acl import ACLManager from api.lib.perm.acl.acl import is_app_admin from api.lib.perm.acl.acl import validate_permission +from api.lib.secrets.inner import InnerCrypt +from api.lib.secrets.vault import VaultClient from api.lib.utils import Lock from api.lib.utils import handle_arg_list from api.lib.webhook import webhook_request @@ -323,6 +326,8 @@ class CIManager(object): ci_attr2type_attr = {type_attr.attr_id: type_attr for type_attr, _ in attrs} ci = None + record_id = None + password_dict = {} need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS) with Lock(ci_type_name, need_lock=need_lock): existed = cls.ci_is_exist(unique_key, unique_value, ci_type.id) @@ -351,14 +356,23 @@ class CIManager(object): ci_dict.get(attr.name) is None and ci_dict.get(attr.alias) is None)): ci_dict[attr.name] = attr.default.get('default') - if type_attr.is_required and (attr.name not in ci_dict and attr.alias not in ci_dict): + if (type_attr.is_required and not attr.is_computed and + (attr.name not in ci_dict and attr.alias not in ci_dict)): return abort(400, ErrFormat.attribute_value_required.format(attr.name)) else: for type_attr, attr in attrs: if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT: ci_dict[attr.name] = now - computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None + computed_attrs = [] + for _, attr in attrs: + if attr.is_computed: + computed_attrs.append(attr.to_dict()) + elif attr.is_password: + if attr.name in ci_dict: + password_dict[attr.id] = ci_dict.pop(attr.name) + elif attr.alias in ci_dict: + password_dict[attr.id] = ci_dict.pop(attr.alias) value_manager = AttributeValueManager() @@ -395,6 +409,10 @@ class CIManager(object): cls.delete(ci.id) raise e + if password_dict: + for attr_id in password_dict: + record_id = cls.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci_type.id) + if record_id: # has change ci_cache.apply_async(args=(ci.id, operate_type, record_id), queue=CMDB_QUEUE) @@ -414,7 +432,16 @@ class CIManager(object): if attr.default and attr.default.get('default') == AttributeDefaultValueEnum.UPDATED_AT: ci_dict[attr.name] = now - computed_attrs = [attr.to_dict() for _, attr in attrs if attr.is_computed] or None + password_dict = dict() + computed_attrs = list() + for _, attr in attrs: + if attr.is_computed: + computed_attrs.append(attr.to_dict()) + elif attr.is_password: + if attr.name in ci_dict: + password_dict[attr.id] = ci_dict.pop(attr.name) + elif attr.alias in ci_dict: + password_dict[attr.id] = ci_dict.pop(attr.alias) value_manager = AttributeValueManager() @@ -423,6 +450,7 @@ class CIManager(object): limit_attrs = self._valid_ci_for_no_read(ci) if not _is_admin else {} + record_id = None need_lock = current_user.username not in current_app.config.get('PRIVILEGED_USERS', PRIVILEGED_USERS) with Lock(ci.ci_type.name, need_lock=need_lock): self._valid_unique_constraint(ci.type_id, ci_dict, ci_id) @@ -440,6 +468,10 @@ class CIManager(object): except BadRequest as e: raise e + if password_dict: + for attr_id in password_dict: + record_id = self.save_password(ci.id, attr_id, password_dict[attr_id], record_id, ci.type_id) + if record_id: # has change ci_cache.apply_async(args=(ci_id, OperateType.UPDATE, record_id), queue=CMDB_QUEUE) @@ -602,7 +634,7 @@ class CIManager(object): _fields = list() for field in fields: attr = AttributeCache.get(field) - if attr is not None: + if attr is not None and not attr.is_password: _fields.append(str(attr.id)) filter_fields_sql = "WHERE A.attr_id in ({0})".format(",".join(_fields)) @@ -620,7 +652,7 @@ class CIManager(object): ci_dict = dict() unique_id2obj = dict() excludes = excludes and set(excludes) - for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list in cis: + for ci_id, type_id, attr_id, attr_name, attr_alias, value, value_type, is_list, is_password in cis: if not fields and excludes and (attr_name in excludes or attr_alias in excludes): continue @@ -647,11 +679,14 @@ class CIManager(object): else: return abort(400, ErrFormat.argument_invalid.format("ret_key")) - value = ValueTypeMap.serialize2[value_type](value) - if is_list: - ci_dict.setdefault(attr_key, []).append(value) + if is_password and value: + ci_dict[attr_key] = '******' else: - ci_dict[attr_key] = value + value = ValueTypeMap.serialize2[value_type](value) + if is_list: + ci_dict.setdefault(attr_key, []).append(value) + else: + ci_dict[attr_key] = value return res @@ -683,6 +718,75 @@ class CIManager(object): return cls._get_cis_from_db(ci_ids, ret_key, fields, value_tables, excludes=excludes) + @classmethod + def save_password(cls, ci_id, attr_id, value, record_id, type_id): + if not value: + return + + changed = None + + value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD] + if current_app.config.get('SECRETS_ENGINE') == 'inner': + encrypt_value, status = InnerCrypt().encrypt(value) + if not status: + current_app.logger.error('save password failed: {}'.format(encrypt_value)) + return abort(400, ErrFormat.password_save_failed.format(encrypt_value)) + else: + encrypt_value = '******' + + existed = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False) + if existed is None: + value_table.create(ci_id=ci_id, attr_id=attr_id, value=encrypt_value) + changed = [(ci_id, attr_id, OperateType.ADD, '', '******', type_id)] + elif existed.value != encrypt_value: + existed.update(ci_id=ci_id, attr_id=attr_id, value=encrypt_value) + changed = [(ci_id, attr_id, OperateType.UPDATE, '******', '******', type_id)] + + if current_app.config.get('SECRETS_ENGINE') == 'vault': + vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN')) + try: + vault.update("/{}/{}".format(ci_id, attr_id), dict(v=value)) + except Exception as e: + current_app.logger.error('save password to vault failed: {}'.format(e)) + return abort(400, ErrFormat.password_save_failed.format('write vault failed')) + + if changed is not None: + AttributeValueManager.write_change2(changed, record_id) + + @classmethod + def load_password(cls, ci_id, attr_id): + ci = CI.get_by_id(ci_id) or abort(404, ErrFormat.ci_not_found.format(ci_id)) + + limit_attrs = cls._valid_ci_for_no_read(ci, ci.ci_type) + if limit_attrs: + attr = AttributeCache.get(attr_id) + if attr and attr.name not in limit_attrs: + return abort(403, ErrFormat.no_permission2) + + if current_app.config.get('SECRETS_ENGINE', 'inner') == 'inner': + value_table = ValueTypeMap.table[ValueTypeEnum.PASSWORD] + v = value_table.get_by(ci_id=ci_id, attr_id=attr_id, first=True, to_dict=False) + + v = v and v.value + if not v: + return + + decrypt_value, status = InnerCrypt().decrypt(v) + if not status: + current_app.logger.error('load password failed: {}'.format(decrypt_value)) + return abort(400, ErrFormat.password_load_failed.format(decrypt_value)) + + return decrypt_value + + elif current_app.config.get('SECRETS_ENGINE') == 'vault': + vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN')) + data, status = vault.read("/{}/{}".format(ci_id, attr_id)) + if not status: + current_app.logger.error('read password from vault failed: {}'.format(data)) + return abort(400, ErrFormat.password_load_failed.format(data)) + + return data.get('v') + class CIRelationManager(object): """ diff --git a/cmdb-api/api/lib/cmdb/const.py b/cmdb-api/api/lib/cmdb/const.py index e36bba0..dc9497a 100644 --- a/cmdb-api/api/lib/cmdb/const.py +++ b/cmdb-api/api/lib/cmdb/const.py @@ -12,6 +12,8 @@ class ValueTypeEnum(BaseEnum): DATE = "4" TIME = "5" JSON = "6" + PASSWORD = TEXT + LINK = TEXT class ConstraintEnum(BaseEnum): diff --git a/cmdb-api/api/lib/cmdb/resp_format.py b/cmdb-api/api/lib/cmdb/resp_format.py index 5c64abb..ef040be 100644 --- a/cmdb-api/api/lib/cmdb/resp_format.py +++ b/cmdb-api/api/lib/cmdb/resp_format.py @@ -95,3 +95,6 @@ class ErrFormat(CommonErrFormat): ci_filter_perm_cannot_or_query = "CI过滤授权 暂时不支持 或 查询" ci_filter_perm_attr_no_permission = "您没有属性 {} 的操作权限!" ci_filter_perm_ci_no_permission = "您没有该CI的操作权限!" + + password_save_failed = "保存密码失败: {}" + password_load_failed = "获取密码失败: {}" diff --git a/cmdb-api/api/lib/cmdb/utils.py b/cmdb-api/api/lib/cmdb/utils.py index e529670..cf05466 100644 --- a/cmdb-api/api/lib/cmdb/utils.py +++ b/cmdb-api/api/lib/cmdb/utils.py @@ -37,6 +37,8 @@ class ValueTypeMap(object): ValueTypeEnum.DATETIME: str2datetime, ValueTypeEnum.DATE: str2datetime, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, + ValueTypeEnum.PASSWORD: lambda x: x, + ValueTypeEnum.LINK: lambda x: x, } serialize = { @@ -47,6 +49,8 @@ class ValueTypeMap(object): ValueTypeEnum.DATE: lambda x: x.strftime("%Y-%m-%d") if not isinstance(x, six.string_types) else x, ValueTypeEnum.DATETIME: lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if not isinstance(x, six.string_types) else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, + ValueTypeEnum.PASSWORD: lambda x: x if isinstance(x, six.string_types) else str(x), + ValueTypeEnum.LINK: lambda x: x if isinstance(x, six.string_types) else str(x), } serialize2 = { @@ -57,6 +61,8 @@ class ValueTypeMap(object): ValueTypeEnum.DATE: lambda x: (x.decode() if not isinstance(x, six.string_types) else x).split()[0], ValueTypeEnum.DATETIME: lambda x: x.decode() if not isinstance(x, six.string_types) else x, ValueTypeEnum.JSON: lambda x: json.loads(x) if isinstance(x, six.string_types) and x else x, + ValueTypeEnum.PASSWORD: lambda x: x.decode() if not isinstance(x, six.string_types) else x, + ValueTypeEnum.LINK: lambda x: x.decode() if not isinstance(x, six.string_types) else x, } choice = { @@ -71,6 +77,8 @@ class ValueTypeMap(object): table = { ValueTypeEnum.TEXT: model.CIValueText, ValueTypeEnum.JSON: model.CIValueJson, + ValueTypeEnum.PASSWORD: model.CIValueText, + ValueTypeEnum.LINK: model.CIValueText, 'index_{0}'.format(ValueTypeEnum.INT): model.CIIndexValueInteger, 'index_{0}'.format(ValueTypeEnum.TEXT): model.CIIndexValueText, 'index_{0}'.format(ValueTypeEnum.DATETIME): model.CIIndexValueDateTime, @@ -83,6 +91,8 @@ class ValueTypeMap(object): table_name = { ValueTypeEnum.TEXT: 'c_value_texts', ValueTypeEnum.JSON: 'c_value_json', + ValueTypeEnum.PASSWORD: 'c_value_texts', + ValueTypeEnum.LINK: 'c_value_texts', 'index_{0}'.format(ValueTypeEnum.INT): 'c_value_index_integers', 'index_{0}'.format(ValueTypeEnum.TEXT): 'c_value_index_texts', 'index_{0}'.format(ValueTypeEnum.DATETIME): 'c_value_index_datetime', @@ -100,6 +110,8 @@ class ValueTypeMap(object): ValueTypeEnum.TIME: 'text', ValueTypeEnum.FLOAT: 'float', ValueTypeEnum.JSON: 'object', + ValueTypeEnum.PASSWORD: 'text', + ValueTypeEnum.LINK: 'text', } @@ -112,7 +124,7 @@ class TableMap(object): @property def table(self): attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr - if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON: + if attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON, ValueTypeEnum.PASSWORD, ValueTypeEnum.LINK}: self.is_index = True elif self.is_index is None: self.is_index = attr.is_index @@ -124,7 +136,7 @@ class TableMap(object): @property def table_name(self): attr = AttributeCache.get(self.attr_name) if not self.attr else self.attr - if attr.value_type != ValueTypeEnum.TEXT and attr.value_type != ValueTypeEnum.JSON: + if attr.value_type not in {ValueTypeEnum.TEXT, ValueTypeEnum.JSON, ValueTypeEnum.PASSWORD, ValueTypeEnum.LINK}: self.is_index = True elif self.is_index is None: self.is_index = attr.is_index diff --git a/cmdb-api/api/lib/cmdb/value.py b/cmdb-api/api/lib/cmdb/value.py index 43327f0..3da3fbb 100644 --- a/cmdb-api/api/lib/cmdb/value.py +++ b/cmdb-api/api/lib/cmdb/value.py @@ -66,9 +66,10 @@ class AttributeValueManager(object): use_master=use_master, to_dict=False) field_name = getattr(attr, ret_key) - if attr.is_list: res[field_name] = [ValueTypeMap.serialize[attr.value_type](i.value) for i in rs] + elif attr.is_password and rs: + res[field_name] = '******' else: res[field_name] = ValueTypeMap.serialize[attr.value_type](rs[0].value) if rs else None @@ -131,8 +132,7 @@ class AttributeValueManager(object): return AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id) @staticmethod - def _write_change2(changed): - record_id = None + def write_change2(changed, record_id=None): for ci_id, attr_id, operate_type, old, new, type_id in changed: record_id = AttributeHistoryManger.add(record_id, ci_id, [(attr_id, operate_type, old, new)], type_id, commit=False, flush=False) @@ -286,7 +286,7 @@ class AttributeValueManager(object): current_app.logger.warning(str(e)) return abort(400, ErrFormat.attribute_value_unknown_error.format(e.args[0])) - return self._write_change2(changed) + return self.write_change2(changed) @staticmethod def delete_attr_value(attr_id, ci_id): diff --git a/cmdb-api/api/views/cmdb/ci.py b/cmdb-api/api/views/cmdb/ci.py index d287b86..ce39962 100644 --- a/cmdb-api/api/views/cmdb/ci.py +++ b/cmdb-api/api/views/cmdb/ci.py @@ -84,11 +84,10 @@ class CIView(APIView): ci_dict = self._wrap_ci_dict() manager = CIManager() - current_app.logger.debug(ci_dict) ci_id = manager.add(ci_type, exist_policy=exist_policy or ExistPolicy.REJECT, _no_attribute_policy=_no_attribute_policy, - _is_admin=request.values.pop('__is_admin', False), + _is_admin=request.values.pop('__is_admin', None) or False, **ci_dict) return self.jsonify(ci_id=ci_id) @@ -96,7 +95,6 @@ class CIView(APIView): @has_perm_for_ci("ci_id", ResourceTypeEnum.CI, PermEnum.UPDATE, CIManager.get_type) def put(self, ci_id=None): args = request.values - current_app.logger.info(args) ci_type = args.get("ci_type") _no_attribute_policy = args.get("no_attribute_policy", ExistPolicy.IGNORE) @@ -104,14 +102,14 @@ class CIView(APIView): manager = CIManager() if ci_id is not None: manager.update(ci_id, - _is_admin=request.values.pop('__is_admin', False), + _is_admin=request.values.pop('__is_admin', None) or False, **ci_dict) else: request.values.pop('exist_policy', None) ci_id = manager.add(ci_type, exist_policy=ExistPolicy.REPLACE, _no_attribute_policy=_no_attribute_policy, - _is_admin=request.values.pop('__is_admin', False), + _is_admin=request.values.pop('__is_admin', None) or False, **ci_dict) return self.jsonify(ci_id=ci_id) @@ -242,3 +240,13 @@ class CIAutoDiscoveryStatisticsView(APIView): def get(self): return self.jsonify(CIManager.get_ad_statistics()) + + +class CIPasswordView(APIView): + url_prefix = "/ci/<int:ci_id>/attributes/<int:attr_id>/password" + + def get(self, ci_id, attr_id): + return self.jsonify(ci_id=ci_id, attr_id=attr_id, value=CIManager.load_password(ci_id, attr_id)) + + def post(self, ci_id, attr_id): + return self.get(ci_id, attr_id) diff --git a/cmdb-api/settings.example.py b/cmdb-api/settings.example.py index ed6b6ce..307b929 100644 --- a/cmdb-api/settings.example.py +++ b/cmdb-api/settings.example.py @@ -97,3 +97,8 @@ BOOL_TRUE = ['true', 'TRUE', 'True', True, '1', 1, "Yes", "YES", "yes", 'Y', 'y' # # messenger USE_MESSENGER = True + +# # secrets +SECRETS_ENGINE = 'inner' # 'inner' or 'vault' +VAULT_URL = '' +VAULT_TOKEN = '' From 6fff2fe9df32a959716f571dcc5fc116005de725 Mon Sep 17 00:00:00 2001 From: fxiang21 <fxiang21@126.com> Date: Fri, 27 Oct 2023 16:43:18 +0800 Subject: [PATCH 5/7] feat: add inner password storage --- cmdb-api/Pipfile | 3 +- cmdb-api/api/commands/click_cmdb.py | 61 ++++ cmdb-api/api/lib/secrets/inner.py | 420 ++++++++++++++++++++++++---- cmdb-api/api/lib/secrets/secrets.py | 21 ++ cmdb-api/api/models/cmdb.py | 7 + cmdb-api/api/views/cmdb/secrets.py | 27 ++ cmdb-api/requirements.txt | 3 +- cmdb-api/settings.example.py | 1 + 8 files changed, 492 insertions(+), 51 deletions(-) create mode 100644 cmdb-api/api/lib/secrets/secrets.py create mode 100644 cmdb-api/api/views/cmdb/secrets.py diff --git a/cmdb-api/Pipfile b/cmdb-api/Pipfile index 61ed9c2..38ebd94 100644 --- a/cmdb-api/Pipfile +++ b/cmdb-api/Pipfile @@ -60,6 +60,8 @@ jinja2schema = "==0.1.4" msgpack-python = "==0.5.6" alembic = "==1.7.7" hvac = "==2.0.0" +colorama = ">=0.4.6" +pycryptodomex = ">=3.19.0" [dev-packages] # Testing @@ -76,4 +78,3 @@ flake8-isort = "==2.7.0" isort = "==4.3.21" pep8-naming = "==0.8.2" pydocstyle = "==3.0.0" - diff --git a/cmdb-api/api/commands/click_cmdb.py b/cmdb-api/api/commands/click_cmdb.py index a191c00..a44965e 100644 --- a/cmdb-api/api/commands/click_cmdb.py +++ b/cmdb-api/api/commands/click_cmdb.py @@ -29,6 +29,10 @@ from api.lib.perm.acl.resource import ResourceCRUD from api.lib.perm.acl.resource import ResourceTypeCRUD from api.lib.perm.acl.role import RoleCRUD from api.lib.perm.acl.user import UserCRUD +from api.lib.secrets.inner import KeyMange +from api.lib.secrets.secrets import InnerKVManger +from api.lib.secrets.inner import global_key_threshold + from api.models.acl import App from api.models.acl import ResourceType from api.models.cmdb import Attribute @@ -311,3 +315,60 @@ def cmdb_index_table_upgrade(): CIIndexValueDateTime.create(ci_id=i.ci_id, attr_id=i.attr_id, value=i.value, commit=False) i.delete(commit=False) db.session.commit() + + +@click.command() +@with_appcontext +def cmdb_inner_secrets_init(): + """ + init inner secrets for password feature + """ + KeyMange(backend=InnerKVManger).init() + + +@click.command() +@click.option( + '-k', + '--token', + help='root token', +) +@with_appcontext +def cmdb_inner_secrets_unseal(token): + """ + unseal the secrets feature + """ + for i in range(global_key_threshold): + token = click.prompt(f'Enter token {i+1}', hide_input=True, confirmation_prompt=False) + assert token is not None + res = KeyMange(backend=InnerKVManger).unseal(token) + KeyMange.print_response(res) + + +@click.command() +@click.option( + '-k', + '--token', + help='root token', + prompt=True, + hide_input=True, +) +@with_appcontext +def cmdb_inner_secrets_seal(token): + """ + seal the secrets feature + """ + assert token is not None + res = KeyMange(backend=InnerKVManger()).seal(token) + KeyMange.print_response(res) + + +@click.command() +@with_appcontext +def cmdb_inner_secrets_auto_seal(): + """ + auto seal the secrets feature + """ + res = KeyMange(current_app.config.get("INNER_TRIGGER_TOKEN"), backend=InnerKVManger()).auto_unseal() + KeyMange.print_response(res) + + diff --git a/cmdb-api/api/lib/secrets/inner.py b/cmdb-api/api/lib/secrets/inner.py index bbb3bea..d5453d6 100644 --- a/cmdb-api/api/lib/secrets/inner.py +++ b/cmdb-api/api/lib/secrets/inner.py @@ -1,17 +1,42 @@ from base64 import b64encode, b64decode -from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from colorama import Back +from colorama import Fore +from colorama import Style +from colorama import init as colorama_init from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher +from cryptography.hazmat.primitives.ciphers import algorithms +from cryptography.hazmat.primitives.ciphers import modes from cryptography.hazmat.primitives.ciphers.aead import AESGCM +from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC + import os +import secrets import sys +from Cryptodome.Protocol.SecretSharing import Shamir # global_root_key just for test here -global_root_key = "4OIzj9ztvfu/qUbzUkjvH54jVC0xGyVaWlemotx6PC0=" +global_root_key = "" +global_encrypt_key = "" global_iv_length = 16 +global_key_shares = 5 # Number of generated key shares +global_key_threshold = 3 # Minimum number of shares required to rebuild the key +global_shares = [] + +backend_root_key_name = "root_key" +backend_encrypt_key_name = "encrypt_key" +backend_root_key_salt_name = "root_key_salt" +backend_encrypt_key_salt_name = "encrypt_key_salt" +success = "success" +seal_status = True +cache = {} def string_to_bytes(value): + if isinstance(value, bytes): + return value if sys.version_info.major == 2: byte_string = value else: @@ -19,78 +44,375 @@ def string_to_bytes(value): return byte_string -class KeyMange: - +class cache_backend: def __init__(self): pass - @staticmethod - def generate_unseal_keys(): - root_key = AESGCM.generate_key(256) - return root_key + @classmethod + def get(cls, key): + global cache + return cache.get(key) - @staticmethod - def generate_key(): - return AESGCM.generate_key(256) + @classmethod + def add(cls, key, value): + cache[key] = value + return success, True - def _acquire(self): - """ - get encryption key from backend storage - :return: - """ - return + +class Backend: + def __init__(self, backend=None): + if not backend: + self.backend = cache_backend + else: + self.backend = backend + + def get(self, key): + return self.backend.get(key) + + def add(self, key, value): + return self.backend.add(key, value) + + +class KeyMange: + + def __init__(self, trigger=None, backend=None): + self.trigger = trigger + self.backend = backend + if backend: + self.backend = Backend(backend) + pass + + def hash_root_key(self, value): + algorithm = hashes.SHA256() + salt = self.backend.get(backend_root_key_salt_name) + if not salt: + salt = secrets.token_hex(16) + msg, ok = self.backend.add(backend_root_key_salt_name, salt) + if not ok: + return msg, ok + kdf = PBKDF2HMAC( + algorithm=algorithm, + length=32, + salt=string_to_bytes(salt), + iterations=100000, + ) + key = kdf.derive(string_to_bytes(value)) + return b64encode(key).decode('utf-8'), True + + def generate_encrypt_key(self, key): + algorithm = hashes.SHA256() + salt = self.backend.get(backend_encrypt_key_salt_name) + if not salt: + salt = secrets.token_hex(32) + kdf = PBKDF2HMAC( + algorithm=algorithm, + length=32, + salt=string_to_bytes(salt), + iterations=100000, + backend=default_backend() + ) + key = kdf.derive(string_to_bytes(key)) + msg, ok = self.backend.add(backend_encrypt_key_salt_name, salt) + if ok: + return b64encode(key).decode('utf-8'), ok + else: + return msg, ok + + @classmethod + def generate_keys(cls, secret): + shares = Shamir.split(global_key_threshold, global_key_shares, secret) + new_shares = [] + for share in shares: + t = [i for i in share[1]] + [ord(i) for i in "{:0>2}".format(share[0])] + new_shares.append(b64encode(bytes(t))) + return new_shares + + def auth_root_secret(self, root_key): + # root_key_hash, ok = self.hash_root_key(b64encode(root_key)) + root_key_hash, ok = self.hash_root_key(root_key) + if not ok: + return { + "message": root_key_hash, + "status": "failed" + } + backend_root_key_hash = self.backend.get(backend_root_key_name) + print(root_key, root_key_hash, backend_root_key_hash) + if not backend_root_key_hash: + return { + "message": "should init firstly", + "status": "failed" + } + elif backend_root_key_hash != root_key_hash: + return { + "message": "invalid root key", + "status": "failed" + } + encrypt_key_aes = self.backend.get(backend_encrypt_key_name) + if not encrypt_key_aes: + return { + "message": "encrypt key is empty", + "status": "failed" + } + global global_encrypt_key + global global_root_key + global global_shares + global_encrypt_key = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes) + global_root_key = root_key + global_shares = [] + return { + "message": success, + "status": success + } + + def unseal(self, key): + if not self.is_seal(): + return { + "message": "current status is unseal, skip", + "status": "skip" + } + global global_shares, global_root_key, global_encrypt_key + try: + t = [i for i in b64decode(key)] + v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2])) + if v not in global_shares: + global_shares.append(v) + if len(global_shares) >= global_key_threshold: + recovered_secret = Shamir.combine(global_shares[:global_key_threshold]) + return self.auth_root_secret(b64encode(recovered_secret)) + else: + return { + "message": "waiting for inputting other unseal key {0}/{1}".format(len(global_shares), + global_key_threshold), + "status": "waiting" + } + except Exception as e: + return { + "message": "invalid token: " + str(e), + "status": "failed" + } + + def generate_unseal_keys(self): + info = self.backend.get(backend_root_key_name) + if info: + return "already exist", [], False + secret = AESGCM.generate_key(128) + shares = self.generate_keys(secret) + return b64encode(secret), shares, True def init(self): """ - init the master key, unseal key. + init the master key, unseal key and store in backend :return: """ - @staticmethod - def is_seal(): - return global_root_key == b'' + root_key = self.backend.get(backend_root_key_name) + if root_key: + return {"message": "already init, skip"}, False + else: + root_key, shares, status = self.generate_unseal_keys() + if not status: + return {"message": root_key}, False + # hash root key and store in backend + root_key_hash, ok = self.hash_root_key(root_key) + if not ok: + return {"message": root_key_hash}, False + msg, ok = self.backend.add(backend_root_key_name, root_key_hash) + if not ok: + return {"message": msg}, False + # generate encrypt key from root_key and store in backend + encrypt_key, ok = self.generate_encrypt_key(root_key) + if not ok: + return {"message": encrypt_key} + encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key) + if not status: + return {"message": encrypt_key_aes} + msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes) + if not ok: + return {"message": msg}, False + # + global global_root_key, global_encrypt_key + global_root_key = root_key + global_encrypt_key = encrypt_key + self.print_token(shares, root_token=root_key) + return {"message": "OK", + "details": { + "root_token": root_key, + "seal_tokens": shares, + }}, True + def auto_unseal(self): + if not self.trigger: + return { + "message": "trigger config is empty, skip", + "status": "skip" + } + if self.trigger.startswith("http"): + return { + "message": "todo in next step, skip", + "status": "skip" + } + # TODO + elif len(self.trigger.strip()) == 24: + res = self.auth_root_secret(self.trigger.encode()) + if res.get("status") == success: + return { + "message": success, + "status": success + } + else: + return { + "message": res.get("message"), + "status": "failed" + } + else: + return { + "message": "trigger config is invalid, skip", + "status": "skip" + } + + def seal(self, root_key): + root_key = root_key.encode() + root_key_hash, ok = self.hash_root_key(root_key) + if not ok: + return { + "message": root_key_hash, + "status": "failed" + } + backend_root_key_hash = self.backend.get(backend_root_key_name) + if not backend_root_key_hash: + return { + "message": "not init, seal skip", + "status": "skip" + } + elif root_key_hash != backend_root_key_hash: + return { + "message": "invalid root key", + "status": "failed" + } + else: + global global_root_key + global global_encrypt_key + global_root_key = "" + global_encrypt_key = "" + return { + "message": success, + "status": success + } + + def is_seal(self): + """ + If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state. + :return: + """ + root_key = self.backend.get(backend_root_key_name) + if root_key == "" or root_key != global_root_key: + return "invalid root key", True + return "", False + + @classmethod + def print_token(cls, shares, root_token): + """ + data: {"message": "OK", + "details": { + "root_token": root_key, + "seal_tokens": shares, + }} + """ + colorama_init() + print(Style.BRIGHT, "Please be sure to store the Unseal Key in a secure location and avoid losing it." + " The Unseal Key is required to unseal the system every time when it restarts." + " Successful unsealing is necessary to enable the password feature." + Style.RESET_ALL) + for i, v in enumerate(shares): + print( + "unseal token " + str(i + 1) + ": " + Fore.RED + Back.CYAN + v.decode("utf-8") + Style.RESET_ALL) + print() + print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL) + + @classmethod + def print_response(cls, data): + status = data.get("status", "") + message = data.get("message", "") + if status == "skip": + print(Style.BRIGHT, message) + elif status == "failed": + print(Fore.RED, message) + elif status == "waiting": + print(Fore.YELLOW, message) + else: + print(Fore.GREEN, message) class InnerCrypt: - def __init__(self): - self.encrypt_key = b64decode(global_root_key.encode("utf-8")) + def __init__(self, trigger=None): + self.encrypt_key = b64decode(global_encrypt_key.encode("utf-8")) def encrypt(self, plaintext): - status = True - encrypt_value = self.aes_encrypt(plaintext) - return encrypt_value, status + """ + encrypt method contain aes currently + """ + return self.aes_encrypt(self.encrypt_key, plaintext) def decrypt(self, ciphertext): - status = True - decrypt_value = self.aes_decrypt(ciphertext) - return decrypt_value, status + """ + decrypt method contain aes currently + """ + return self.aes_decrypt(self.encrypt_key, ciphertext) - def aes_encrypt(self, plaintext): + @classmethod + def aes_encrypt(cls, key, plaintext): if isinstance(plaintext, str): plaintext = string_to_bytes(plaintext) iv = os.urandom(global_iv_length) - cipher = Cipher(algorithms.AES(self.encrypt_key), modes.CBC(iv), backend=default_backend()) - encryptor = cipher.encryptor() - padder = padding.PKCS7(algorithms.AES.block_size).padder() - padded_plaintext = padder.update(plaintext) + padder.finalize() - ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize() - return b64encode(iv+ciphertext).decode('utf-8') + try: + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + encryptor = cipher.encryptor() + v_padder = padding.PKCS7(algorithms.AES.block_size).padder() + padded_plaintext = v_padder.update(plaintext) + v_padder.finalize() + ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize() + return b64encode(iv + ciphertext).decode("utf-8"), True + except Exception as e: + return str(e), False - def aes_decrypt(self, ciphertext): - s = b64decode(ciphertext.encode("utf-8")) - iv = s[:global_iv_length] - ciphertext = s[global_iv_length:] - cipher = Cipher(algorithms.AES(self.encrypt_key), modes.CBC(iv), backend=default_backend()) - decryptor = cipher.decryptor() - decrypted_padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize() - unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() - plaintext = unpadder.update(decrypted_padded_plaintext) + unpadder.finalize() - return plaintext.decode('utf-8') + @classmethod + def aes_decrypt(cls, key, ciphertext): + try: + s = b64decode(ciphertext.encode("utf-8")) + iv = s[:global_iv_length] + ciphertext = s[global_iv_length:] + cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend()) + decrypter = cipher.decryptor() + decrypted_padded_plaintext = decrypter.update(ciphertext) + decrypter.finalize() + unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() + plaintext = unpadder.update(decrypted_padded_plaintext) + unpadder.finalize() + return plaintext.decode('utf-8'), True + except Exception as e: + return str(e), False if __name__ == "__main__": - t_plaintext = "Hello, World!" # The plaintext to encrypt + + print(global_encrypt_key) + km = KeyMange() + # info, shares, status = km.generate_unseal_keys() + # print(info, shares, status) + # print("..................") + # for i in shares: + # print(b64encode(i[1]).decode()) + + res1, ok1 = km.init() + if not ok1: + print(res1) + # for j in res["details"]["seal_tokens"]: + # r = km.unseal(j) + # if r["status"] != "waiting": + # if r["status"] != "success": + # print("r........", r) + # else: + # print(r) + # break + + t_plaintext = b"Hello, World!" # The plaintext to encrypt c = InnerCrypt() - t_ciphertext = c.aes_encrypt(t_plaintext) + t_ciphertext, status1 = c.encrypt(t_plaintext) print("Ciphertext:", t_ciphertext) - decrypted_plaintext = c.aes_decrypt(t_ciphertext) + decrypted_plaintext, status2 = c.decrypt(t_ciphertext) print("Decrypted plaintext:", decrypted_plaintext) + print(global_encrypt_key) diff --git a/cmdb-api/api/lib/secrets/secrets.py b/cmdb-api/api/lib/secrets/secrets.py new file mode 100644 index 0000000..5f541f3 --- /dev/null +++ b/cmdb-api/api/lib/secrets/secrets.py @@ -0,0 +1,21 @@ +from api.models.cmdb import InnerKV + + +class InnerKVManger(object): + def __init__(self): + pass + + @classmethod + def add(cls, key, value): + data = {"key": key, "value": value} + res = InnerKV.create(**data) + if res.key == key: + return "success", True + return "add failed", False + + @classmethod + def get(cls, key): + res = InnerKV().get_by(first=True, to_dict=False, **{"key": key}) + if not res: + return None + return res.value diff --git a/cmdb-api/api/models/cmdb.py b/cmdb-api/api/models/cmdb.py index ae15db0..d6b5491 100644 --- a/cmdb-api/api/models/cmdb.py +++ b/cmdb-api/api/models/cmdb.py @@ -504,3 +504,10 @@ class CIFilterPerms(Model): attr_filter = db.Column(db.Text) rid = db.Column(db.Integer, index=True) + + +class InnerKV(Model): + __tablename__ = "c_kv" + + key = db.Column(db.String(128), index=True) + value = db.Column(db.Text) diff --git a/cmdb-api/api/views/cmdb/secrets.py b/cmdb-api/api/views/cmdb/secrets.py new file mode 100644 index 0000000..d2e76fd --- /dev/null +++ b/cmdb-api/api/views/cmdb/secrets.py @@ -0,0 +1,27 @@ +from api.resource import APIView +from api.models.cmdb import InnerKV +from api.lib.secrets.inner import KeyMange + +from flask import request, abort + + +class InnerSecretUnSealView(APIView): + url_prefix = "/secrets/unseal" + + def post(self): + unseal_key = request.headers.get("Inner-Token") + res = KeyMange(InnerKV()).unseal(unseal_key) + if res.get("status") == "failed": + return abort(400, res.get("message")) + return self.jsonify(**res) + + +class InnerSecretSealView(APIView): + url_prefix = "/secrets/seal" + + def post(self): + unseal_key = request.headers.get("Inner-Token") + res = KeyMange(InnerKV()).seal(unseal_key) + if res.get("status") == "failed": + return abort(400, res.get("message")) + return self.jsonify(**res) diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index 6ad5a2c..ab06a17 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -30,7 +30,6 @@ marshmallow==2.20.2 more-itertools==5.0.0 msgpack-python==0.5.6 Pillow==9.3.0 -pycryptodome==3.12.0 cryptography==41.0.2 PyJWT==2.4.0 PyMySQL==1.1.0 @@ -50,3 +49,5 @@ Werkzeug==2.3.6 WTForms==3.0.0 shamir~=17.12.0 hvac~=2.0.0 +pycryptodomex>=3.19.0 +colorama>=0.4.6 diff --git a/cmdb-api/settings.example.py b/cmdb-api/settings.example.py index 307b929..373e6b7 100644 --- a/cmdb-api/settings.example.py +++ b/cmdb-api/settings.example.py @@ -102,3 +102,4 @@ USE_MESSENGER = True SECRETS_ENGINE = 'inner' # 'inner' or 'vault' VAULT_URL = '' VAULT_TOKEN = '' +INNER_TRIGGER_TOKEN = '' From 27a1c75a2543ad7c70164ea4a37eb19f3ec630b6 Mon Sep 17 00:00:00 2001 From: pycook <pycook@126.com> Date: Fri, 27 Oct 2023 17:42:49 +0800 Subject: [PATCH 6/7] feat: secrets --- cmdb-api/Pipfile | 4 +- cmdb-api/api/commands/click_cmdb.py | 71 ++++++++++++++----- .../api/lib/cmdb/search/ci/db/query_sql.py | 6 +- cmdb-api/api/lib/secrets/inner.py | 67 +++++++++-------- cmdb-api/api/lib/secrets/vault.py | 2 +- cmdb-api/api/views/cmdb/secrets.py | 6 +- cmdb-api/requirements.txt | 2 +- 7 files changed, 98 insertions(+), 60 deletions(-) diff --git a/cmdb-api/Pipfile b/cmdb-api/Pipfile index 38ebd94..5f071d1 100644 --- a/cmdb-api/Pipfile +++ b/cmdb-api/Pipfile @@ -26,14 +26,14 @@ Flask-Bcrypt = "==1.0.1" Flask-Cors = ">=3.0.8" ldap3 = "==2.9.1" pycryptodome = "==3.12.0" -cryptography = "==41.0.2" +cryptography = ">=41.0.2" # Caching Flask-Caching = ">=1.0.0" # Environment variable parsing environs = "==4.2.0" marshmallow = "==2.20.2" # async tasks -celery = "==5.3.1" +celery = ">=5.3.1" celery_once = "==3.0.1" more-itertools = "==5.0.0" kombu = "==5.3.1" diff --git a/cmdb-api/api/commands/click_cmdb.py b/cmdb-api/api/commands/click_cmdb.py index a44965e..08c84e8 100644 --- a/cmdb-api/api/commands/click_cmdb.py +++ b/cmdb-api/api/commands/click_cmdb.py @@ -29,10 +29,9 @@ from api.lib.perm.acl.resource import ResourceCRUD from api.lib.perm.acl.resource import ResourceTypeCRUD from api.lib.perm.acl.role import RoleCRUD from api.lib.perm.acl.user import UserCRUD -from api.lib.secrets.inner import KeyMange -from api.lib.secrets.secrets import InnerKVManger +from api.lib.secrets.inner import KeyManage from api.lib.secrets.inner import global_key_threshold - +from api.lib.secrets.secrets import InnerKVManger from api.models.acl import App from api.models.acl import ResourceType from api.models.cmdb import Attribute @@ -57,6 +56,7 @@ def cmdb_init_cache(): if relations: rd.create_or_update(relations, REDIS_PREFIX_CI_RELATION) + es = None if current_app.config.get("USE_ES"): from api.extensions import es from api.models.cmdb import Attribute @@ -323,25 +323,20 @@ def cmdb_inner_secrets_init(): """ init inner secrets for password feature """ - KeyMange(backend=InnerKVManger).init() + KeyManage(backend=InnerKVManger).init() @click.command() -@click.option( - '-k', - '--token', - help='root token', -) @with_appcontext -def cmdb_inner_secrets_unseal(token): +def cmdb_inner_secrets_unseal(): """ unseal the secrets feature """ for i in range(global_key_threshold): - token = click.prompt(f'Enter token {i+1}', hide_input=True, confirmation_prompt=False) + token = click.prompt(f'Enter token {i + 1}', hide_input=True, confirmation_prompt=False) assert token is not None - res = KeyMange(backend=InnerKVManger).unseal(token) - KeyMange.print_response(res) + res = KeyManage(backend=InnerKVManger).unseal(token) + KeyManage.print_response(res) @click.command() @@ -358,8 +353,8 @@ def cmdb_inner_secrets_seal(token): seal the secrets feature """ assert token is not None - res = KeyMange(backend=InnerKVManger()).seal(token) - KeyMange.print_response(res) + res = KeyManage(backend=InnerKVManger()).seal(token) + KeyManage.print_response(res) @click.command() @@ -368,7 +363,49 @@ def cmdb_inner_secrets_auto_seal(): """ auto seal the secrets feature """ - res = KeyMange(current_app.config.get("INNER_TRIGGER_TOKEN"), backend=InnerKVManger()).auto_unseal() - KeyMange.print_response(res) + res = KeyManage(current_app.config.get("INNER_TRIGGER_TOKEN"), backend=InnerKVManger()).auto_unseal() + KeyManage.print_response(res) +@click.command() +@with_appcontext +def cmdb_password_data_migrate(): + """ + Migrate CI password data, version >= v2.3.6 + """ + from api.models.cmdb import CIIndexValueText + from api.models.cmdb import CIValueText + from api.lib.secrets.inner import InnerCrypt + from api.lib.secrets.vault import VaultClient + + attrs = Attribute.get_by(to_dict=False) + for attr in attrs: + if attr.is_password: + + value_table = CIIndexValueText if attr.is_index else CIValueText + + for i in value_table.get_by(attr_id=attr.id, to_dict=False): + if current_app.config.get("SECRETS_ENGINE", 'inner') == 'inner': + _, status = InnerCrypt().decrypt(i.value) + if status: + continue + + encrypt_value, status = InnerCrypt().encrypt(i.value) + if status: + CIValueText.create(ci_id=i.ci_id, attr_id=attr.id, value=encrypt_value) + else: + continue + elif current_app.config.get("SECRETS_ENGINE") == 'vault': + if i.value == '******': + continue + + vault = VaultClient(current_app.config.get('VAULT_URL'), current_app.config.get('VAULT_TOKEN')) + try: + vault.update("/{}/{}".format(i.ci_id, i.attr_id), dict(v=i.value)) + except Exception as e: + print('save password to vault failed: {}'.format(e)) + continue + else: + continue + + i.delete() diff --git a/cmdb-api/api/lib/cmdb/search/ci/db/query_sql.py b/cmdb-api/api/lib/cmdb/search/ci/db/query_sql.py index 78e43e7..24aa0cb 100644 --- a/cmdb-api/api/lib/cmdb/search/ci/db/query_sql.py +++ b/cmdb-api/api/lib/cmdb/search/ci/db/query_sql.py @@ -7,6 +7,7 @@ QUERY_CIS_BY_VALUE_TABLE = """ attr.alias AS attr_alias, attr.value_type, attr.is_list, + attr.is_password, c_cis.type_id, {0}.ci_id, {0}.attr_id, @@ -26,7 +27,8 @@ QUERY_CIS_BY_IDS = """ A.attr_alias, A.value, A.value_type, - A.is_list + A.is_list, + A.is_password FROM ({1}) AS A {0} ORDER BY A.ci_id; @@ -43,7 +45,7 @@ FACET_QUERY1 = """ FACET_QUERY = """ SELECT {0}.value, - count({0}.ci_id) + count(distinct {0}.ci_id) FROM {0} INNER JOIN ({1}) AS F ON F.ci_id={0}.ci_id WHERE {0}.attr_id={2:d} diff --git a/cmdb-api/api/lib/secrets/inner.py b/cmdb-api/api/lib/secrets/inner.py index d5453d6..5caa6cf 100644 --- a/cmdb-api/api/lib/secrets/inner.py +++ b/cmdb-api/api/lib/secrets/inner.py @@ -1,25 +1,24 @@ +import os +import secrets +import sys from base64 import b64encode, b64decode + +from Cryptodome.Protocol.SecretSharing import Shamir from colorama import Back from colorama import Fore from colorama import Style from colorama import init as colorama_init from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.hazmat.primitives.ciphers import algorithms from cryptography.hazmat.primitives.ciphers import modes from cryptography.hazmat.primitives.ciphers.aead import AESGCM -from cryptography.hazmat.primitives import hashes -from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC - -import os -import secrets -import sys -from Cryptodome.Protocol.SecretSharing import Shamir +from flask import current_app # global_root_key just for test here -global_root_key = "" -global_encrypt_key = "" global_iv_length = 16 global_key_shares = 5 # Number of generated key shares global_key_threshold = 3 # Minimum number of shares required to rebuild the key @@ -44,7 +43,7 @@ def string_to_bytes(value): return byte_string -class cache_backend: +class CacheBackend: def __init__(self): pass @@ -62,7 +61,7 @@ class cache_backend: class Backend: def __init__(self, backend=None): if not backend: - self.backend = cache_backend + self.backend = CacheBackend else: self.backend = backend @@ -73,14 +72,13 @@ class Backend: return self.backend.add(key, value) -class KeyMange: +class KeyManage: def __init__(self, trigger=None, backend=None): self.trigger = trigger self.backend = backend if backend: self.backend = Backend(backend) - pass def hash_root_key(self, value): algorithm = hashes.SHA256() @@ -136,7 +134,6 @@ class KeyMange: "status": "failed" } backend_root_key_hash = self.backend.get(backend_root_key_name) - print(root_key, root_key_hash, backend_root_key_hash) if not backend_root_key_hash: return { "message": "should init firstly", @@ -153,12 +150,11 @@ class KeyMange: "message": "encrypt key is empty", "status": "failed" } - global global_encrypt_key - global global_root_key - global global_shares - global_encrypt_key = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes) - global_root_key = root_key - global_shares = [] + secrets_encrypt_key = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes) + setattr(current_app, 'secrets_encrypt_key', secrets_encrypt_key) + setattr(current_app, 'secrets_root_key', root_key) + setattr(current_app, 'secrets_shares', []) + return { "message": success, "status": success @@ -170,7 +166,7 @@ class KeyMange: "message": "current status is unseal, skip", "status": "skip" } - global global_shares, global_root_key, global_encrypt_key + global global_shares try: t = [i for i in b64decode(key)] v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2])) @@ -197,6 +193,7 @@ class KeyMange: return "already exist", [], False secret = AESGCM.generate_key(128) shares = self.generate_keys(secret) + return b64encode(secret), shares, True def init(self): @@ -229,10 +226,11 @@ class KeyMange: if not ok: return {"message": msg}, False # - global global_root_key, global_encrypt_key - global_root_key = root_key - global_encrypt_key = encrypt_key + setattr(current_app, 'secrets_root_key', root_key) + setattr(current_app, 'secrets_encrypt_key', encrypt_key) + self.print_token(shares, root_token=root_key) + return {"message": "OK", "details": { "root_token": root_key, @@ -289,10 +287,9 @@ class KeyMange: "status": "failed" } else: - global global_root_key - global global_encrypt_key - global_root_key = "" - global_encrypt_key = "" + setattr(current_app, 'secrets_root_key', '') + setattr(current_app, 'secrets_encrypt_key', '') + return { "message": success, "status": success @@ -303,9 +300,11 @@ class KeyMange: If there is no initialization or the root key is inconsistent, it is considered to be in a sealed state. :return: """ + secrets_root_key = getattr(current_app, 'secrets_root_key') root_key = self.backend.get(backend_root_key_name) - if root_key == "" or root_key != global_root_key: + if root_key == "" or root_key != secrets_root_key: return "invalid root key", True + return "", False @classmethod @@ -340,9 +339,11 @@ class KeyMange: else: print(Fore.GREEN, message) + class InnerCrypt: - def __init__(self, trigger=None): - self.encrypt_key = b64decode(global_encrypt_key.encode("utf-8")) + def __init__(self): + secrets_encrypt_key = getattr(current_app, 'secrets_encrypt_key', '') + self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8")) def encrypt(self, plaintext): """ @@ -389,8 +390,7 @@ class InnerCrypt: if __name__ == "__main__": - print(global_encrypt_key) - km = KeyMange() + km = KeyManage() # info, shares, status = km.generate_unseal_keys() # print(info, shares, status) # print("..................") @@ -415,4 +415,3 @@ if __name__ == "__main__": print("Ciphertext:", t_ciphertext) decrypted_plaintext, status2 = c.decrypt(t_ciphertext) print("Decrypted plaintext:", decrypted_plaintext) - print(global_encrypt_key) diff --git a/cmdb-api/api/lib/secrets/vault.py b/cmdb-api/api/lib/secrets/vault.py index 85bc6db..a5746f5 100644 --- a/cmdb-api/api/lib/secrets/vault.py +++ b/cmdb-api/api/lib/secrets/vault.py @@ -136,6 +136,6 @@ if __name__ == "__main__": # sdk.enable_secrets_engine() _data = {"key1": "value1", "key2": "value2", "key3": "value3"} _data = sdk.update(_path, _data, overwrite=True, encrypt=True) - print(_data.status_code) + print(_data) _data = sdk.read(_path, decrypt=True) print(_data) diff --git a/cmdb-api/api/views/cmdb/secrets.py b/cmdb-api/api/views/cmdb/secrets.py index d2e76fd..b98215f 100644 --- a/cmdb-api/api/views/cmdb/secrets.py +++ b/cmdb-api/api/views/cmdb/secrets.py @@ -1,6 +1,6 @@ from api.resource import APIView from api.models.cmdb import InnerKV -from api.lib.secrets.inner import KeyMange +from api.lib.secrets.inner import KeyManage from flask import request, abort @@ -10,7 +10,7 @@ class InnerSecretUnSealView(APIView): def post(self): unseal_key = request.headers.get("Inner-Token") - res = KeyMange(InnerKV()).unseal(unseal_key) + res = KeyManage(InnerKV()).unseal(unseal_key) if res.get("status") == "failed": return abort(400, res.get("message")) return self.jsonify(**res) @@ -21,7 +21,7 @@ class InnerSecretSealView(APIView): def post(self): unseal_key = request.headers.get("Inner-Token") - res = KeyMange(InnerKV()).seal(unseal_key) + res = KeyManage(InnerKV()).seal(unseal_key) if res.get("status") == "failed": return abort(400, res.get("message")) return self.jsonify(**res) diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index ab06a17..19152db 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -1,7 +1,7 @@ -i https://mirrors.aliyun.com/pypi/simple alembic==1.7.7 bs4==0.0.1 -celery==5.3.1 +celery>=5.3.1 celery-once==3.0.1 click==8.1.3 elasticsearch==7.17.9 From e725fb847324d5cf3b80bfa8def89353cc3ecf1f Mon Sep 17 00:00:00 2001 From: fxiang21 <fxiang21@126.com> Date: Fri, 27 Oct 2023 18:28:23 +0800 Subject: [PATCH 7/7] feat: add inner password storage --- cmdb-api/api/app.py | 2 ++ cmdb-api/api/extensions.py | 3 +++ cmdb-api/api/lib/secrets/inner.py | 38 +++++++++---------------------- 3 files changed, 16 insertions(+), 27 deletions(-) diff --git a/cmdb-api/api/app.py b/cmdb-api/api/app.py index 537abda..348a6b5 100644 --- a/cmdb-api/api/app.py +++ b/cmdb-api/api/app.py @@ -18,6 +18,7 @@ from flask.json.provider import DefaultJSONProvider import api.views.entry from api.extensions import (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd) +from api.extensions import inner_secrets from api.flask_cas import CAS from api.models.acl import User @@ -125,6 +126,7 @@ def register_extensions(app): app.config.update(app.config.get("CELERY")) celery.conf.update(app.config) + inner_secrets.init_app(app) def register_blueprints(app): diff --git a/cmdb-api/api/extensions.py b/cmdb-api/api/extensions.py index f540c21..0dd13ad 100644 --- a/cmdb-api/api/extensions.py +++ b/cmdb-api/api/extensions.py @@ -12,6 +12,8 @@ from flask_sqlalchemy import SQLAlchemy from api.lib.utils import ESHandler from api.lib.utils import RedisHandler +from api.lib.secrets.inner import KeyManage + bcrypt = Bcrypt() login_manager = LoginManager() db = SQLAlchemy(session_options={"autoflush": False}) @@ -21,3 +23,4 @@ celery = Celery() cors = CORS(supports_credentials=True) rd = RedisHandler() es = ESHandler() +inner_secrets = KeyManage() diff --git a/cmdb-api/api/lib/secrets/inner.py b/cmdb-api/api/lib/secrets/inner.py index 5caa6cf..6394373 100644 --- a/cmdb-api/api/lib/secrets/inner.py +++ b/cmdb-api/api/lib/secrets/inner.py @@ -22,7 +22,6 @@ from flask import current_app global_iv_length = 16 global_key_shares = 5 # Number of generated key shares global_key_threshold = 3 # Minimum number of shares required to rebuild the key -global_shares = [] backend_root_key_name = "root_key" backend_encrypt_key_name = "encrypt_key" @@ -30,7 +29,6 @@ backend_root_key_salt_name = "root_key_salt" backend_encrypt_key_salt_name = "encrypt_key_salt" success = "success" seal_status = True -cache = {} def string_to_bytes(value): @@ -43,27 +41,9 @@ def string_to_bytes(value): return byte_string -class CacheBackend: - def __init__(self): - pass - - @classmethod - def get(cls, key): - global cache - return cache.get(key) - - @classmethod - def add(cls, key, value): - cache[key] = value - return success, True - - class Backend: def __init__(self, backend=None): - if not backend: - self.backend = CacheBackend - else: - self.backend = backend + self.backend = backend def get(self, key): return self.backend.get(key) @@ -80,6 +60,9 @@ class KeyManage: if backend: self.backend = Backend(backend) + def init_app(self, app): + self.auto_unseal() + def hash_root_key(self, value): algorithm = hashes.SHA256() salt = self.backend.get(backend_root_key_salt_name) @@ -166,18 +149,19 @@ class KeyManage: "message": "current status is unseal, skip", "status": "skip" } - global global_shares try: t = [i for i in b64decode(key)] v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2])) - if v not in global_shares: - global_shares.append(v) - if len(global_shares) >= global_key_threshold: - recovered_secret = Shamir.combine(global_shares[:global_key_threshold]) + shares = getattr(current_app, "secrets_shares", []) + if v not in shares: + shares.append(v) + setattr(current_app, "secrets_shares", shares) + if len(shares) >= global_key_threshold: + recovered_secret = Shamir.combine(shares[:global_key_threshold]) return self.auth_root_secret(b64encode(recovered_secret)) else: return { - "message": "waiting for inputting other unseal key {0}/{1}".format(len(global_shares), + "message": "waiting for inputting other unseal key {0}/{1}".format(len(shares), global_key_threshold), "status": "waiting" }