开源一个简单的数据加解密存储方案

162 阅读9分钟

在我们的日常项目中,经常会遇得到一些敏感数据的存储问题,比如用户的密码,各种登录认证的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模块):

github.com/veops/ops-t…