perf(secrets): review

This commit is contained in:
pycook 2023-10-28 16:17:35 +08:00
parent 7bc62ba66b
commit 68aec77d75
5 changed files with 41 additions and 21 deletions

View File

@ -36,7 +36,7 @@ marshmallow = "==2.20.2"
celery = ">=5.3.1" celery = ">=5.3.1"
celery_once = "==3.0.1" celery_once = "==3.0.1"
more-itertools = "==5.0.0" more-itertools = "==5.0.0"
kombu = "==5.3.1" kombu = ">=5.3.1"
# common setting # common setting
timeout-decorator = "==0.5.0" timeout-decorator = "==0.5.0"
WTForms = "==3.0.0" WTForms = "==3.0.0"

View File

@ -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 (bcrypt, cache, celery, cors, db, es, login_manager, migrate, rd)
from api.extensions import inner_secrets from api.extensions import inner_secrets
from api.flask_cas import CAS from api.flask_cas import CAS
from api.models.acl import User
from api.lib.secrets.secrets import InnerKVManger from api.lib.secrets.secrets import InnerKVManger
from api.models.acl import User
HERE = os.path.abspath(os.path.dirname(__file__)) HERE = os.path.abspath(os.path.dirname(__file__))
PROJECT_ROOT = os.path.join(HERE, os.pardir) PROJECT_ROOT = os.path.join(HERE, os.pardir)
@ -127,6 +127,9 @@ def register_extensions(app):
app.config.update(app.config.get("CELERY")) app.config.update(app.config.get("CELERY"))
celery.conf.update(app.config) celery.conf.update(app.config)
if app.config.get('SECRETS_ENGINE') == 'inner':
with app.app_context():
inner_secrets.init_app(app, InnerKVManger()) inner_secrets.init_app(app, InnerKVManger())

View File

@ -1,24 +1,23 @@
import os import os
import secrets import secrets
import sys 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 Back
from colorama import Fore from colorama import Fore
from colorama import Style
from colorama import init as colorama_init 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.backends import default_backend
from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import padding 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 algorithms
from cryptography.hazmat.primitives.ciphers import Cipher
from cryptography.hazmat.primitives.ciphers import modes from cryptography.hazmat.primitives.ciphers import modes
from cryptography.hazmat.primitives.ciphers.aead import AESGCM from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from flask import current_app from flask import current_app
# global_root_key just for test here
global_iv_length = 16 global_iv_length = 16
global_key_shares = 5 # Number of generated key shares global_key_shares = 5 # Number of generated key shares
global_key_threshold = 3 # Minimum number of shares required to rebuild the key 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 byte_string = value
else: else:
byte_string = value.encode("utf-8") byte_string = value.encode("utf-8")
return byte_string return byte_string
@ -65,8 +65,9 @@ class KeyManage:
if not self.trigger: if not self.trigger:
return return
self.backend = backend 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): def hash_root_key(self, value):
algorithm = hashes.SHA256() algorithm = hashes.SHA256()
@ -76,6 +77,7 @@ class KeyManage:
msg, ok = self.backend.add(backend_root_key_salt_name, salt) msg, ok = self.backend.add(backend_root_key_salt_name, salt)
if not ok: if not ok:
return msg, ok return msg, ok
kdf = PBKDF2HMAC( kdf = PBKDF2HMAC(
algorithm=algorithm, algorithm=algorithm,
length=32, length=32,
@ -83,6 +85,7 @@ class KeyManage:
iterations=100000, iterations=100000,
) )
key = kdf.derive(string_to_bytes(value)) key = kdf.derive(string_to_bytes(value))
return b64encode(key).decode('utf-8'), True return b64encode(key).decode('utf-8'), True
def generate_encrypt_key(self, key): def generate_encrypt_key(self, key):
@ -90,6 +93,7 @@ class KeyManage:
salt = self.backend.get(backend_encrypt_key_salt_name) salt = self.backend.get(backend_encrypt_key_salt_name)
if not salt: if not salt:
salt = secrets.token_hex(32) salt = secrets.token_hex(32)
kdf = PBKDF2HMAC( kdf = PBKDF2HMAC(
algorithm=algorithm, algorithm=algorithm,
length=32, length=32,
@ -106,21 +110,22 @@ class KeyManage:
@classmethod @classmethod
def generate_keys(cls, secret): 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 = [] new_shares = []
for share in shares: for share in shares:
t = [i for i in share[1]] + [ord(i) for i in "{:0>2}".format(share[0])] t = [i for i in share[1]] + [ord(i) for i in "{:0>2}".format(share[0])]
new_shares.append(b64encode(bytes(t))) new_shares.append(b64encode(bytes(t)))
return new_shares return new_shares
def auth_root_secret(self, root_key): 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) root_key_hash, ok = self.hash_root_key(root_key)
if not ok: if not ok:
return { return {
"message": root_key_hash, "message": root_key_hash,
"status": "failed" "status": "failed"
} }
backend_root_key_hash = self.backend.get(backend_root_key_name) backend_root_key_hash = self.backend.get(backend_root_key_name)
if not backend_root_key_hash: if not backend_root_key_hash:
return { return {
@ -132,12 +137,14 @@ class KeyManage:
"message": "invalid root key", "message": "invalid root key",
"status": "failed" "status": "failed"
} }
encrypt_key_aes = self.backend.get(backend_encrypt_key_name) encrypt_key_aes = self.backend.get(backend_encrypt_key_name)
if not encrypt_key_aes: if not encrypt_key_aes:
return { return {
"message": "encrypt key is empty", "message": "encrypt key is empty",
"status": "failed" "status": "failed"
} }
secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes) secrets_encrypt_key, ok = InnerCrypt.aes_decrypt(string_to_bytes(root_key), encrypt_key_aes)
if ok: if ok:
current_app.config["secrets_encrypt_key"] = secrets_encrypt_key current_app.config["secrets_encrypt_key"] = secrets_encrypt_key
@ -156,19 +163,17 @@ class KeyManage:
"message": "current status is unseal, skip", "message": "current status is unseal, skip",
"status": "skip" "status": "skip"
} }
try: try:
t = [i for i in b64decode(key)] t = [i for i in b64decode(key)]
v = (int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2])) 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", []) shares = current_app.config.get("secrets_shares", [])
print("222222222222")
if v not in shares: if v not in shares:
shares.append(v) shares.append(v)
current_app.config["secrets_shares"] = shares current_app.config["secrets_shares"] = shares
print("shares:", shares)
if len(shares) >= global_key_threshold: 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)) return self.auth_root_secret(b64encode(recovered_secret))
else: else:
return { return {
@ -186,6 +191,7 @@ class KeyManage:
info = self.backend.get(backend_root_key_name) info = self.backend.get(backend_root_key_name)
if info: if info:
return "already exist", [], False return "already exist", [], False
secret = AESGCM.generate_key(128) secret = AESGCM.generate_key(128)
shares = self.generate_keys(secret) shares = self.generate_keys(secret)
@ -203,27 +209,31 @@ class KeyManage:
root_key, shares, status = self.generate_unseal_keys() root_key, shares, status = self.generate_unseal_keys()
if not status: if not status:
return {"message": root_key}, False return {"message": root_key}, False
# hash root key and store in backend # hash root key and store in backend
root_key_hash, ok = self.hash_root_key(root_key) root_key_hash, ok = self.hash_root_key(root_key)
if not ok: if not ok:
return {"message": root_key_hash}, False return {"message": root_key_hash}, False
msg, ok = self.backend.add(backend_root_key_name, root_key_hash) msg, ok = self.backend.add(backend_root_key_name, root_key_hash)
if not ok: if not ok:
return {"message": msg}, False return {"message": msg}, False
# generate encrypt key from root_key and store in backend # generate encrypt key from root_key and store in backend
encrypt_key, ok = self.generate_encrypt_key(root_key) encrypt_key, ok = self.generate_encrypt_key(root_key)
if not ok: if not ok:
return {"message": encrypt_key} return {"message": encrypt_key}
encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key) encrypt_key_aes, status = InnerCrypt.aes_encrypt(root_key, encrypt_key)
if not status: if not status:
return {"message": encrypt_key_aes} return {"message": encrypt_key_aes}
msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes) msg, ok = self.backend.add(backend_encrypt_key_name, encrypt_key_aes)
if not ok: if not ok:
return {"message": msg}, False return {"message": msg}, False
current_app.config["secrets_root_key"] = root_key current_app.config["secrets_root_key"] = root_key
current_app.config["secrets_encrypt_key"] = encrypt_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) self.print_token(shares, root_token=root_key)
return {"message": "OK", return {"message": "OK",
@ -238,6 +248,7 @@ class KeyManage:
"message": "trigger config is empty, skip", "message": "trigger config is empty, skip",
"status": "skip" "status": "skip"
} }
if self.trigger.startswith("http"): if self.trigger.startswith("http"):
return { return {
"message": "todo in next step, skip", "message": "todo in next step, skip",
@ -270,6 +281,7 @@ class KeyManage:
"message": root_key_hash, "message": root_key_hash,
"status": "failed" "status": "failed"
} }
backend_root_key_hash = self.backend.get(backend_root_key_name) backend_root_key_hash = self.backend.get(backend_root_key_name)
if not backend_root_key_hash: if not backend_root_key_hash:
return { 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." 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." " 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) " Successful unsealing is necessary to enable the password feature." + Style.RESET_ALL)
for i, v in enumerate(shares): for i, v in enumerate(shares):
print( print(
"unseal token " + str(i + 1) + ": " + Fore.RED + Back.CYAN + v.decode("utf-8") + Style.RESET_ALL) "unseal token " + str(i + 1) + ": " + Fore.RED + Back.CYAN + v.decode("utf-8") + Style.RESET_ALL)
print() print()
print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL) print(Fore.GREEN + "root token: " + root_token.decode("utf-8") + Style.RESET_ALL)
@classmethod @classmethod
@ -338,7 +352,6 @@ class KeyManage:
class InnerCrypt: class InnerCrypt:
def __init__(self): def __init__(self):
secrets_encrypt_key = current_app.config.get("secrets_encrypt_key", "") 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")) self.encrypt_key = b64decode(secrets_encrypt_key.encode("utf-8"))
def encrypt(self, plaintext): def encrypt(self, plaintext):
@ -364,6 +377,7 @@ class InnerCrypt:
v_padder = padding.PKCS7(algorithms.AES.block_size).padder() v_padder = padding.PKCS7(algorithms.AES.block_size).padder()
padded_plaintext = v_padder.update(plaintext) + v_padder.finalize() padded_plaintext = v_padder.update(plaintext) + v_padder.finalize()
ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize() ciphertext = encryptor.update(padded_plaintext) + encryptor.finalize()
return b64encode(iv + ciphertext).decode("utf-8"), True return b64encode(iv + ciphertext).decode("utf-8"), True
except Exception as e: except Exception as e:
return str(e), False return str(e), False
@ -379,6 +393,7 @@ class InnerCrypt:
decrypted_padded_plaintext = decrypter.update(ciphertext) + decrypter.finalize() decrypted_padded_plaintext = decrypter.update(ciphertext) + decrypter.finalize()
unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder() unpadder = padding.PKCS7(algorithms.AES.block_size).unpadder()
plaintext = unpadder.update(decrypted_padded_plaintext) + unpadder.finalize() plaintext = unpadder.update(decrypted_padded_plaintext) + unpadder.finalize()
return plaintext.decode('utf-8'), True return plaintext.decode('utf-8'), True
except Exception as e: except Exception as e:
return str(e), False return str(e), False

View File

@ -11,11 +11,13 @@ class InnerKVManger(object):
res = InnerKV.create(**data) res = InnerKV.create(**data)
if res.key == key: if res.key == key:
return "success", True return "success", True
return "add failed", False return "add failed", False
@classmethod @classmethod
def get(cls, key): 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: if not res:
return None return None
return res.value return res.value

View File

@ -23,7 +23,7 @@ itsdangerous==2.1.2
Jinja2==3.1.2 Jinja2==3.1.2
jinja2schema==0.1.4 jinja2schema==0.1.4
jsonschema==4.18.0 jsonschema==4.18.0
kombu==5.3.1 kombu>=5.3.1
Mako==1.2.4 Mako==1.2.4
MarkupSafe==2.1.3 MarkupSafe==2.1.3
marshmallow==2.20.2 marshmallow==2.20.2