mirror of https://github.com/veops/cmdb.git
perf(secrets): review
This commit is contained in:
parent
7bc62ba66b
commit
68aec77d75
|
@ -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"
|
||||||
|
|
|
@ -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())
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue