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"
                 }