From 68aec77d7502777d59df3acf58222904a7cb2ce4 Mon Sep 17 00:00:00 2001 From: pycook Date: Sat, 28 Oct 2023 16:17:35 +0800 Subject: [PATCH] perf(secrets): review --- cmdb-api/Pipfile | 2 +- cmdb-api/api/app.py | 7 +++-- cmdb-api/api/lib/secrets/inner.py | 47 +++++++++++++++++++---------- cmdb-api/api/lib/secrets/secrets.py | 4 ++- cmdb-api/requirements.txt | 2 +- 5 files changed, 41 insertions(+), 21 deletions(-) diff --git a/cmdb-api/Pipfile b/cmdb-api/Pipfile index 5f071d1..776410e 100644 --- a/cmdb-api/Pipfile +++ b/cmdb-api/Pipfile @@ -36,7 +36,7 @@ marshmallow = "==2.20.2" celery = ">=5.3.1" celery_once = "==3.0.1" more-itertools = "==5.0.0" -kombu = "==5.3.1" +kombu = ">=5.3.1" # common setting timeout-decorator = "==0.5.0" WTForms = "==3.0.0" diff --git a/cmdb-api/api/app.py b/cmdb-api/api/app.py index c1c8d06..6ea299d 100644 --- a/cmdb-api/api/app.py +++ b/cmdb-api/api/app.py @@ -20,8 +20,8 @@ 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 from api.lib.secrets.secrets import InnerKVManger +from api.models.acl import User HERE = os.path.abspath(os.path.dirname(__file__)) PROJECT_ROOT = os.path.join(HERE, os.pardir) @@ -127,7 +127,10 @@ def register_extensions(app): app.config.update(app.config.get("CELERY")) celery.conf.update(app.config) - inner_secrets.init_app(app, InnerKVManger()) + + if app.config.get('SECRETS_ENGINE') == 'inner': + with app.app_context(): + inner_secrets.init_app(app, InnerKVManger()) def register_blueprints(app): diff --git a/cmdb-api/api/lib/secrets/inner.py b/cmdb-api/api/lib/secrets/inner.py index 9ce8857..856bc12 100644 --- a/cmdb-api/api/lib/secrets/inner.py +++ b/cmdb-api/api/lib/secrets/inner.py @@ -1,24 +1,23 @@ import os import secrets import sys -from base64 import b64encode, b64decode +from base64 import b64decode, b64encode -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 colorama import Style +from Cryptodome.Protocol.SecretSharing import Shamir 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 Cipher from cryptography.hazmat.primitives.ciphers import modes from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from flask import current_app -# global_root_key just for test here 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 @@ -38,6 +37,7 @@ def string_to_bytes(value): byte_string = value else: byte_string = value.encode("utf-8") + return byte_string @@ -65,8 +65,9 @@ class KeyManage: if not self.trigger: return self.backend = backend - # resp = self.auto_unseal() - # self.print_response(resp) + + resp = self.auto_unseal() + self.print_response(resp) def hash_root_key(self, value): algorithm = hashes.SHA256() @@ -76,6 +77,7 @@ class KeyManage: msg, ok = self.backend.add(backend_root_key_salt_name, salt) if not ok: return msg, ok + kdf = PBKDF2HMAC( algorithm=algorithm, length=32, @@ -83,6 +85,7 @@ class KeyManage: iterations=100000, ) key = kdf.derive(string_to_bytes(value)) + return b64encode(key).decode('utf-8'), True def generate_encrypt_key(self, key): @@ -90,6 +93,7 @@ class KeyManage: salt = self.backend.get(backend_encrypt_key_salt_name) if not salt: salt = secrets.token_hex(32) + kdf = PBKDF2HMAC( algorithm=algorithm, length=32, @@ -106,21 +110,22 @@ class KeyManage: @classmethod def generate_keys(cls, secret): - shares = Shamir.split(global_key_threshold, global_key_shares, secret) + shares = Shamir.split(global_key_threshold, global_key_shares, secret, False) 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) if not backend_root_key_hash: return { @@ -132,12 +137,14 @@ class KeyManage: "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" } + secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes) if ok: current_app.config["secrets_encrypt_key"] = secrets_encrypt_key @@ -156,19 +163,17 @@ class KeyManage: "message": "current status is unseal, skip", "status": "skip" } + try: t = [i for i in b64decode(key)] v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2])) - print("............") - # shares = getattr(current_app.config, "secrets_shares", []) shares = current_app.config.get("secrets_shares", []) - print("222222222222") if v not in shares: shares.append(v) current_app.config["secrets_shares"] = shares - print("shares:", shares) + if len(shares) >= global_key_threshold: - recovered_secret = Shamir.combine(shares[:global_key_threshold]) + recovered_secret = Shamir.combine(shares[:global_key_threshold], False) return self.auth_root_secret(b64encode(recovered_secret)) else: return { @@ -186,6 +191,7 @@ class KeyManage: info = self.backend.get(backend_root_key_name) if info: return "already exist", [], False + secret = AESGCM.generate_key(128) shares = self.generate_keys(secret) @@ -203,27 +209,31 @@ class KeyManage: 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 current_app.config["secrets_root_key"] = root_key current_app.config["secrets_encrypt_key"] = encrypt_key - print(".....", current_app.config["secrets_root_key"], current_app.config["secrets_encrypt_key"]) self.print_token(shares, root_token=root_key) return {"message": "OK", @@ -238,6 +248,7 @@ class KeyManage: "message": "trigger config is empty, skip", "status": "skip" } + if self.trigger.startswith("http"): return { "message": "todo in next step, skip", @@ -270,6 +281,7 @@ class KeyManage: "message": root_key_hash, "status": "failed" } + backend_root_key_hash = self.backend.get(backend_root_key_name) if not backend_root_key_hash: return { @@ -315,10 +327,12 @@ class KeyManage: 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 @@ -338,7 +352,6 @@ class KeyManage: class InnerCrypt: def __init__(self): secrets_encrypt_key = current_app.config.get("secrets_encrypt_key", "") - print("secrets_encrypt_key:", secrets_encrypt_key) self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8")) def encrypt(self, plaintext): @@ -364,6 +377,7 @@ class InnerCrypt: 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 @@ -379,6 +393,7 @@ class InnerCrypt: 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 diff --git a/cmdb-api/api/lib/secrets/secrets.py b/cmdb-api/api/lib/secrets/secrets.py index 5f541f3..bf24c5f 100644 --- a/cmdb-api/api/lib/secrets/secrets.py +++ b/cmdb-api/api/lib/secrets/secrets.py @@ -11,11 +11,13 @@ class InnerKVManger(object): 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}) + res = InnerKV.get_by(first=True, to_dict=False, **{"key": key}) if not res: return None + return res.value diff --git a/cmdb-api/requirements.txt b/cmdb-api/requirements.txt index 19152db..187cc5f 100644 --- a/cmdb-api/requirements.txt +++ b/cmdb-api/requirements.txt @@ -23,7 +23,7 @@ itsdangerous==2.1.2 Jinja2==3.1.2 jinja2schema==0.1.4 jsonschema==4.18.0 -kombu==5.3.1 +kombu>=5.3.1 Mako==1.2.4 MarkupSafe==2.1.3 marshmallow==2.20.2