在我们的日常项目中,经常会遇得到一些敏感数据的存储问题,比如用户的密码,各种登录认证的token等等,如何安全的存储这些敏感信息十分重要。本文提供一种基于python实现的简单加密解密方案,避免用户在做敏感信息存储时重复造轮子。
01 概述
本方案主要为数据的双向加解密功能,用户只需要关注数据的加密解密,不用关心密钥管理的实现细节。主要给出两种解决方案,一种是独立加解密方案,这种方案无任何外部依赖;另一种是对接密码管理系统vault,前提是用户需要安装vault,这种方案对于已经部署有vault的用户来说比较友好。
02 独立方案
功能
该方案最终使用中提供一个加密函数和一个解密函数函数,通过直接传递值进行加解密操作。用户在进行写入数据之前通过调用加密函数对数据加密,获取到加密数据以后,用户再根据自己场景进行存储。
使用方法
1.秘钥管理
关于内部密码的管理逻辑是由KeyMange类进行控制,其结构大致如下。
global_key_shares = 5 # Number of generated key shares
global_key_threshold = 3 # Minimum number of shares required to rebuild the key
class KeyMange
def __init__(self, trigger=None, backend=None):
self.trigger = trigger
self.backend = backend
if backend:
self.backend = Backend(backend)
def init(self):
...
def unseal(self, key):
...
def seal(self, key):
...
def auto_unseal(self):
...
数据存储: 在执行初始化之前,需要设计一个用来存储加密密钥、根令牌、盐值等的持久化存储后端 backend ,这样,在后续的初始化过程中才能将密钥、令牌、盐等数据长久地保存下来,供后续使用。
例如定义一个持久化的后端存储,主要包含add/get/update三个方法,其结构大致如下,其中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
@classmethod
def update(cls, key, value):
res = InnerKV.get_by(first=True, to_dict=False, **{"key": key})
if not res:
return None
res.value = value
t = res.update()
if t.key == key:
return "success", True
return "update failed", True
定义好这个backend之后,在实例化KeyMange类时指定backend 为InnerKVManger即可。
初始化: 在使用服务之前需要进行初始化,初始化即生成必须的认证令牌、解封秘钥以及加解密密钥,这些密钥均为自动生成,初始化完成之后需要记录下来,以便于后续的处理。
执行init函数用于自动生成各种密钥,主要包括root token、解封秘钥以及加解密密钥。以下是调用init的核心输出结果样例:
输出中生成5个unseal token以及一个root token, unseal key是解封秘钥,主要用于服务的解封。在未解封的情况下,是不能提供服务的,执行完解封,用于加载加解密密钥才能用于加解密功能。root token为根密钥,具有最高的权限,可通过此密钥重构解封秘钥、执行封印、api认证、自动解封等场景中。这两组密钥需要在执行完初始化后手动在其他安全的地方进行保存,确保不要丢失和泄漏。解封: 通过unseal 函数执行解封的功能,解封功能。执行解封需要global_key_threshold个不同的解封秘钥方可解封,例如本实例中需要输入任意三个不同的unseal token进行解封。
封印: 即通过执行函数关闭密码服务,该操作会将内存中的加解密密钥以及root token等置空,然后必须再执行一次解封操作才能恢复它。如果检测到异常迹象,可以用最快的速度锁定密码服务,有效减小保存的机密信息的损失。
2.使用
一旦初始化完成,就可以使用InnerCrypt进行数据的加解密,其中encrypt负责加密,decrypt负责解密,注意InnerCrypt只负责加解密的功能,并不负责对数据进行存储,存储交给定义backend来完成。
class InnerCrypt:
def __init__(self):
self.encrypt_key = b64decode(global_encrypt_key.encode("utf-8"))
def encrypt(self, plaintext):
status = True
encrypt_value = self.aes_encrypt(self.encrypt_key, plaintext)
return encrypt_value, status
def decrypt(self, ciphertext):
status = True
decrypt_value = self.aes_decrypt(self.encrypt_key, ciphertext)
return decrypt_value, status
管理原理
1.秘钥生成
主要生成的密钥包括根令牌(root token),解封秘钥(unseal key)以及加解密密钥。当进行初始化时首先会生成一个根令牌,然后使用Shamir算法将根令牌进行拆分,具体主密钥拆分的份数以及解封要求的最低份数可以自行配置,默认情况下,将主密钥拆分成5份,需要至少 3 份才能重建主密钥。
对于拆分之后的解封秘钥,最好分开存放在不同的地方,防止同时泄漏造成的风险。
加密密钥(encrypt key)则是由root token进行加随机盐生成,然后经过Aes加密存储到后端,解封时通过获取的更令牌进行密钥解密,所有写入后端存储的数据都会用该密钥加密,读取时使用改密钥进行解密。生成根令牌以及解封秘钥的代码大致人如下:
class KeyMange:
@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 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
2.解封
一般初始化以后,默认自动解封,加密秘钥会存储在内存中, 当程序重启时需要重新进行解封操作,将加密钥再次写入内存中。只有通过外部的这些秘钥才能解密出加密秘钥。解封需要输入多个解封秘钥才能进行解封,核心代码如下。
class KeyManage:
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
t = [i for i in b64decode(key)]
global_shares.append((int("".join([chr(i) for i in t[-2:]])), bytes(t[:-2])))
if len(global_shares) >= global_key_threshold:
recovered_secret = Shamir.combine(global_shares[:global_key_threshold])
return self.auth_root_secret(recovered_secret)
else:
return {
"Process": "{0}/{1}".format(len(global_shares), global_key_threshold),
"message": "waiting for inputting other unseal key",
"status": "waiting"
}
3.封印
封印相对来讲比较简单,即通过输入根令牌对内存中存储的加密密钥进行清除,从而关闭加解密功能。封印之后可以通过解封恢复服务。
4.自动解封
自动解封主要是考虑到服务每次重启都需要进行手动解封的麻烦,尤其是如果你在容器中运行程序,每次重启都进行手动解封变得几乎不可能,因此自动解封变得十分必要。
这一块逻辑目前是选择将令牌放在配置文件中进行自动解封,不过这种方法相对来说不算太安全,毕竟将令牌放在配置文件中有一些风险;另外一种是使用外部的服务,系统每次启动是触发调用第三方服务的api,从而实现自动解封的功能,这部分功能目前用户可根据自己的场景进行补充,我们推荐是采用第二种方法。
class KeyManage:
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"
}
03 对接vault
功能
本功能使用前提是需要首先部署第三方密码管理软件vault或者已经有了vault服务,将敏感信息直接存储在第三方密码管理软件上,当前程序只需要配置服务地址以及令牌,然后执行增删改查的方法即可。
使用方法
使用时需配置如下一些信息:
-
启动了vault服务,创建一个kv密码引擎,并创建mount_path,如命名为cmdb,同时启动一个transit引擎;也可以选择使用执行enable_secrets_engine来自动创建。
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 {} -
程序侧配置vault的地址,以及调用api的X-Vault-Token,程序使用示例如下
_base_url = "http://localhost:8200"
_token = "your token"
# Example
sdk = VaultClient(_base_url, _token, mount_path="cmdb")
_data = {"key1": "value1", "key2": "value2", "key3": "value3"}
_path = "test001"
_data = sdk.update(_path, _data, overwrite=True, encrypt=True)
_data = sdk.read(_path, decrypt=True)
调用update方法可进行数据的增加或者修改,调用read读取数据。通过ui可以看到数据已经写入到kv引擎里面了,样例如下
原理
主要原理包括数据加解密、数据存取两部分,其逻辑如下:
- 写入时对data数据的所有键值对的value进行base64编码,调用vault的transit密码引擎进行加密操作。
- 对处理后的数据通过调用创建或者修改的方法进行存储。
- 读取时先通过读取接口获取加密数据data。
- 调用transit的解密方法对data的每个value进行解密、base64解码,还原最终的数据。
部分参考代码如下:
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 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(self, ciphertext):
response = self.client.secrets.transit.decrypt_data(name='transit-key', ciphertext=ciphertext)
plaintext = response['data']['plaintext']
return plaintext
# read data
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 )
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(v))
except:
return data, True
return data, True
# delete data
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
@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
结语
本文主要介绍了在敏感数据存储上的使用的一些方法,也提供了一些简单密码管理的一些思路,供读者参考。
开源地址如下(secrets模块):