openGauss数据库源码解析系列文章—安全管理源码解析(六)

openGauss数据库源码解析系列文章—安全管理源码解析(六)

六、数据安全技术

openGauss采用了多种加密解密技术来提升数据在各个环节的安全性。

6.1 数据加解密接口

用户在使用数据库时,除了需要基本的数据库安全之外,还会对导入的数据进行加密和解密的操作。openGauss提供了针对用户导入数据进行加密和解密的功能接口,用户使用该接口可以对其认为包含敏感信息的数据进行加密和解密操作。

1. 数据加密接口

openGauss提供的加密功能是基于标准的AES128加密算法进行实现,提供的加密接口函数为:

gs_encrypt_aes128 (encryptstr, keystr)

其中keystr是用户提供的密钥明文,加密函数通过标准的AES128加密算法对encryptstr字符串进行加密,并返回加密后的字符串。keystr的长度范围为1~16字节。加密函数支持的加密数据类型包括数值类型、字符类型、二进制类型中的RAW、日期/时间类型中的DATE、TIMESTAMP、SMALLDATETIME等。

加密函数返回的的密文值长度:至少为92字节,不超过4*[(Len+68)/3]字节,其中Len为加密前数据长度(单位为字节)。

使用示例如下:

opengauss=# CREATE table student005 (name text);opengauss=# INSERT into student005 values(gs_encrypt_aes128('zhangsan','gaussDB123'));INSERT 0 1opengauss=# SELECT * FROM student005;                                             name----------------------------------------------------------------------------------------------NrGJdx8pDgvUSE2NN7eM5mFDnSSJ41fq31/0SI2+4kABgOnCu9H2vkjpvcAdG/AhJ8OrBn906Xaj6oqyEHsTbcTvjrU= (1 row)

加密接口函数是通过函数gs_encrypt_aes128实现的,其代码源文件为:“builtins.h”和“cipherfn.cpp”。

该函数是一个openGauss的存储过程函数,通过用户输入的明文和密钥进行数据的加密操作。

主要流程如图29所示。

图片

图29 数据加密流程

2. 数据解密接口

openGauss提供的解密接口函数为:

gs_decrypt_aes128 (decryptstr, keystr)

以keystr为用户加密密钥对decryptstr加密字符串进行解密,返回解密后的字符串。解密使用的keystr必须保证与加密时使用的keystr一致才能正常解密。keystr不得为空。

使用示例如下。

opengauss=# SELECT gs_decrypt_aes128(name,'gaussDB123') FROM student005; gs_decrypt_aes128------------------- zhangsan(1 row)

解密接口函数是通过函数gs_decrypt_aes128实现的,其代码源文件为:“builtins.h”和“cipherfn.cpp”。

该函数是一个openGauss的存储过程函数,通过用户输入的密文(注明文加密生成的密文)和密钥进行数据的解密操作。

主要流程如图30所示。

图片

图30 数据解密流程

数据解密的代码如下逐个部分介绍。

通过存储过程的入参解析出需要解密的密文和密钥,并进行脱敏的decode操作。

开始将密文转换为明文过程。相关代码如下:

bool gs_decrypt_aes_speed(GS_UCHAR* ciphertext, GS_UINT32 cipherlen, GS_UCHAR* key, GS_UCHAR* plaintext, GS_UINT32* plainlen)……

将密文进行解码操作,分离密文和信息两个部分。

6.2 数据动态脱敏

数据脱敏,顾名思义就是将敏感数据通过变形、屏蔽等方式处理,其目的是保护隐私数据信息,防止数据泄露和恶意窥探。当企业或者机构收集用户个人身份数据、手机、银行卡号等敏感信息,然后将数据通过导出(非生产环境)或直接查询(结合生产环境)的方式投入使用时,按照隐私保护相关法律法规需将数据进行“脱敏”处理。

openGauss实现了数据动态脱敏机制,它根据一系列用户配置的“脱敏策略”来对查询命令进行分析匹配,最终将敏感数据屏蔽并返回。使用数据动态脱敏特性总的来说分为两个步骤:配置脱敏策略、触发脱敏策略。本小节将对这两个步骤进行具体分析。

显然只有在配置脱敏策略后系统才能有根据地进行敏感数据脱敏。openGauss提供了脱敏策略配置(创建、修改、删除)语法,这些语法所涉及的语法解析节点内容大致相同,因此这里仅对创建策略相关数据结构进行分析,其余不再赘述。下面将结合一个具体示例对数据动态脱敏特性进行详细介绍。

表6给出了一张包含敏感信息(薪资、银行卡号)的个人信息表,策略管理员要对该表中的敏感信息创建脱敏策略:当用户user1或user2在IP地址10.123.123.123上使用JDBC或gsql连接数据库并查询个人信息表时,系统将自动屏蔽敏感信息。

表6 个人信息表person

idnamegendersalarycreditcards
1张三100006210630600006321083
2李四150006015431250003215514
3王五200005021134522201529881

首先策略管理员需要对敏感列打标签,随后使用标签创建脱敏策略,策略配置DDL语句如下。

例1脱敏策略配置示例。

配置资源标签:

(1) CREATE RESOURCE LABEL salary_label ADD COLUMN(person.salary);

(2) CREATE RESOURCE LABEL creditcard_label ADD COLUMN(person.creditcards);

配置脱敏策略:

(3) CREATE MASKING POLICY mask_person_policy MASKALL ON LABEL(salary_label), CREDITCARDMASKING ON label(creditcard_label) FILTER ON ROLES(user1,user2), IP(‘10.123.123.123’), APP(jdbc, gsql);

user1在10.123.123.123地址使用gsql查询敏感数据:

(4) SELECT id, salary, creditcards FROM public.person;

下面将对“CREATE MASKING POLICY”语句所涉及的语法结构定义进行逐一介绍。

脱敏策略创建语法是对CreateMaskingPolicyStmt函数进行填充,其中policy_data是由若干DefElem节点组成的List,每个DefElem指出了以何种方式脱敏数据库资源,DefElem->name标识脱敏方法,DefElem->arg代表脱敏对象。

“6.2 数据动态脱敏”小节中例1脱敏策略配置示例的步骤0对应的policy_data组织结构如图31所示。

图片

图31 脱敏策略配置示例对应的policy_data组织结构

policy_filters属性通过二叉逻辑树的形式描述了哪些用户场景(用户名、客户端、登录IP)可以使脱敏策略生效,policy_filters指向了逻辑树的根节点,只有当用户信息与逻辑树匹配时(匹配方式详见图35),脱敏策略才会被触发。

逻辑树节点分为操作符(op)节点和过滤数据(filter)节点。当op节点分为“与”或“或”关系,其op_value将置为“and”或“or”,其左右子树代表操作符左右子表达式。filter节点一般作为op的叶子节点出现,它标识具体的过滤信息并将其值存放在values链表中。需要注意的是,一个节点不可能既是op节点又是filter节点。“6.2 数据动态脱敏”小节中例1脱敏策略配置示例的步骤0对应的policy_filters组织结构如图32所示。

图片

图32 配置脱敏策略对应的policy_filters 组织结构

脱敏策略配置的总体流程如图33所示。

图片

图33 脱敏策略配置流程图

在查询编译脱敏策略配置SQL之后将进入策略增删改主函数中,首先会根据语法解析节点校验相关参数的合法性,做如下检查:

(1) 检查脱敏策略指定的数据库资源是否存在。

(2) 检查脱敏函数是否存在。

(3) 检查脱敏策略是否已存在。

(4) 检查脱敏相关约束:脱敏对象必须为基本表的数据列、脱敏列类型必须满足规格限制、脱敏列只允许加载一个脱敏函数。

(5) 检查Masking Filter是否冲突,不允许同一数据库资源在相同用户场景下触发多个策略。

其中Masking Filter冲突校验的目的是防止用户场景同时满足多个脱敏策略限制,导致策略匹配时系统无法判断应该触发哪种脱敏策略。因此在创建策略时要保证其过滤条件与现存的策略互斥,主要是判断是否存在一种用户场景能够同时满足多个MASKING FILTER。在“6.2 数据动态脱敏”小节所示的表6数据基础上,如下表中策略A和策略B是相互冲突的,而策略A和策略C是互斥的。

随后将依据策略配置信息更新系统表:

(1) 更新gs_masking_policy系统表,存储policy基本信息。

(2) 更新gs_masking_policy_actions系统表,存储策略对应的脱敏方式及脱敏对象。

(3) 更新gs_masking_policy_filter系统表,存储脱敏用户场景过滤信息。此时会将逻辑树转换为逻辑表达式字符串进行存储,在之后的敏感数据访问时该字符串将会重新转换为逻辑树进行场景校验。

为了降低策略读取I/O损耗,openGauss维护了一组线程级别的策略缓存,用于保存已配置的脱敏策略,并在策略配置后进行实时刷新。

在用户进行数据查询时,数据动态脱敏特性使用openGauss的HOOK机制,将查询编译生成的查询树钩取出来与脱敏策略进行匹配,最后将查询树按照脱敏策略内容改写成不包含敏感数据的“脱敏”查询树返还给解析层继续执行,最终实现屏蔽敏感数据的能力。其执行流程如图34所示。

图片

图34 脱敏策略执行流程图

在对一个访问数据库资源的查询树进行脱敏之前,需要准备一份待匹配的脱敏策略集合,其依据就是用户登录信息,check_masking_policy_filter函数的任务就是将用户信息与所有的脱敏策略进行匹配,筛选出可能被查询触发的脱敏策略。最终筛选如下脱敏策略。

(1) 若脱敏策略没有配置过滤条件信息,说明对所有用户生效。

(2) 若当前用户信息与脱敏策略的过滤条件匹配,则说明对当前用户生效。

在每个脱敏策略从系统表读入缓存时,需要将对应的过滤条件逻辑表达式转换为逻辑树并将逻辑树根节点存入缓存中,将其作为脱敏策略筛选条件。

逻辑树节点的结构与语法解析中的FILTER节点类似,具体可以参照PolicyFilterNode结构.

当需要将逻辑表达式转变为逻辑树时,parse_logical_expression_impl函数将对逻辑表达式字符串进行递归解析,识别出表达式包含的操作符(and或or)以及过滤条件信息(ip、roles、app),构造出PolicyLogicalNode并使用左右子节点索引(m_left、m_right)链接起来形成逻辑树并将每个节点存入m_nodes中,最终利用m_nodes构造m_flat_tree数组来模拟二叉树。

m_flat_tree数组的作用是标记逻辑树节点间关系以及标识哪些节点是逻辑树的叶子节点。当用户信息与逻辑树某节点进行匹配时,首先需要与其左右子树进行匹配,然后根据该节点的逻辑运算符来判断是否满足过滤条件要求,而左右子树的判断结果又依赖于它们的子树的结果,因此这种递归判断方法首先将会是取叶子节点进行用户信息匹配。

openGauss使用“自底向上”的方式来进行用于信息与逻辑树的匹配。从m_flat_tree末尾(叶子节点)进行匹配,将匹配结果记录下来,当匹配到非叶子节点时(op节点)只需使用其左右子节点结果进行判断即可,最终实现整个逻辑树的匹配。在例1脱敏策略配置示例中创建脱敏策略后,当用户使用非受限的客户端访问敏感数据时,逻辑树匹配结果如图35所示。

图片

图35 逻辑树匹配示例

在筛选出脱敏策略后,就需要对查询树所有TargetEntry进行识别和策略匹配。从openGauss源码可以看到,脱敏策略支持对SubLink、Aggref、OpExpr、RelabelType、FuncExpr、CoerceViaIO、Var类型的节点进行解析识别。数据脱敏的核心思路是:Var类型节点代表了访问的数据库资源,而非Var类型节点可能包含Var节点;因此需要根据其参数递归的寻找Var节点,最后将识别到的所有Var节点进行策略匹配并根据策略内容进行节点替换。

在匹配脱敏策略时,首先需要将识别出的Var节点进行解析,将其转为PolicyLabelItem,该数据结构存储了数据列的全部路径信息,然后将其与已过滤出的脱敏策略集合进行匹配;若某个脱敏策略对应的数据库资源对象与PolicyLabelItem一致,将已匹配到的脱敏策略指定的方式替换该Var节点。

脱敏策略匹配成功后,将会根据策略内容替换包含敏感信息的Var节点,使之外嵌脱敏函数。最后将修改后的查询树返还给解析器继续执行,最终敏感数据将会在脱敏函数的作用下以脱敏的形式返回给客户端。“9.6.2 数据动态脱敏”小节中例9-1脱敏策略配置示例步骤(4)中,当SELECT语句触发脱敏策略时,查询树被替换前后的数据结构如图36所示。

图片

图36 查询树脱敏前后的数据结构示例

至此整个查询树已经完成了脱敏策略的匹配与重写,随后将重新回归查询解析模块并继续执行后续处理,最终系统将返回脱敏后的数据结果。

6.3 密态等值查询

除了传统的数据存储加密和数据脱敏等数据保护技术外,openGauss从1.1.0版本开始支持了一种全新的数据全生命周期保护方案:全密态数据库机制。在这种机制下数据在客户端就被加密,从客户端传输到数据库内核,到在内核中完成查询运算,到返回结果给客户端,数据始终处于加密状态,而数据加解密所需的密钥则由用户持有;从而实现了数据拥有者和数据处理者的数据权属分离,有效规避由内鬼和不可信第三方等威胁造成的数据泄漏风险。

本小节重点介绍全密态数据库的第一阶段能力——密态等值查询。与非加密数据库相比,密态等值查询主要提供以下能力。

(1) 数据加密:openGauss通过客户端驱动加密敏感数据,保证敏感数据明文不在除客户端驱动外的地方存在。遵循密钥分级原则将密钥分为数据加密密钥和密钥加密密钥,客户端驱动仅需要妥善保管密钥加密密钥即可保证只有自己才拥有解密数据密文的能力。

(2) 数据检索:openGauss支持在用户无感知的情况下,为其提供对数据库密文进行等值检索的能力。在数据加密阶段,openGauss会将与加密相关的元数据存储在系统表中,当处理敏感数据时,客户端会自动检索加密相关元数据并对数据进行加解密。

openGauss新增数据加解密表语法,通过采用驱动层过滤技术,在客户端的加密驱动中集成了SQL语法解析、密钥管理和敏感数据加解密等模块来处理相关语法。加密驱动源码流程如图37所示。

图片

图37 客户端加密驱动源码流程

用户执行SQL查询语句时,通过Pqexec函数执行SQL语句,SQL语句在发送之前首先进入run_pre_query函数函数,通过前端解析器解析涉及密态的语法。然后在run_pre_statement函数中通过分类器对语法标签进行识别,进入对应语法的处理逻辑。在不同的处理逻辑函数中,查找出要替换的数据参数,并存储在结构体StatementData中,数据结构如图38所示。最后通过replace_raw_values函数重构SQL语句,将其发送给服务端。在PqgetResult函数接收到从服务端返回的数据时,若是加密数据类型,则用deprocess_value函数对加密数据进行解密。接收完数据后还需要在run_post_query函数中刷新相应的缓存。

图片

图38 客户端加密驱动数据结构

openGauss密态数据库采用列级加密,用户在创建加密表的时候需要指定加密列的列加密密钥(Column Encryption Key,CEK)和加密类型,以确定该数据列以何种方式进行加密。同时,在创建表前,应该先创建客户端主密钥(client master key,CMK)。

整个加密步骤和语法可简化为如下3个阶段:创建客户端密钥CMK、创建列加密密钥CEK和创建加密表。下面将结合一个具体示例对密态等值查询特性进行详细介绍。

密态等值查询示例如下。

(1) 创建CMK客户端主密钥。

CREATE CLIENT MASTER KEY cmk_1 WITH (KEY_STORE = LOCALKMS , KEY_PATH = "kms_1" , ALGORITHM = RSA_2048);

(2) 创建CEK列加密密钥。

CREATE COLUMN ENCRYPTION KEY cek_1 WITH VALUES (CLIENT_MASTER_KEY = cmk_1, ALGORITHM = AEAD_AES_256_CBC_HMAC_SHA256);

(3) 创建加密表。

CREATE TABLE creditcard_info (id_number int, name text encrypted with (column_encryption_key = cek_1, encryption_type = DETERMINISTIC), gender varchar(10) encrypted with (column_encryption_key = cek_1, encryption_type = DETERMINISTIC), salary float4 encrypted with (column_encryption_key = cek_1, encryption_type = DETERMINISTIC),credit_card varchar(19) encrypted with (column_encryption_key = cek_1, encryption_type = DETERMINISTIC));

如示例所示,首先使用“CREATE CLIENT MASTER KEY”语法创建客户端主密钥,其所涉及的语法结构定义如下:

/*  保存创建客户端主密钥的语法信息  */
typedef struct CreateClientLogicGlobal {
    NodeTag type;
    List *global_key_name;         /*  全密态数据库主密钥名称  */
    List *global_setting_params;  /*  全密态数据库主密钥参数,每一个元素是一个ClientLogicGlobalparam  */
} CreateClientLogicGlobal;

/*  保存客户端主密钥参数信息  */
typedef struct ClientLogicGlobalParam {
    NodeTag type;
    ClientLogicGlobalProperty key;  /*  键  */
    char *value;                       /*  值  */
    unsigned int len;                 /*  值长度  */
    int location;                     /*  位置标记  */
} ClientLogicGlobalParam;

/*  保存客户端主密钥参数的key的枚举类型 */
typedef enum class ClientLogicGlobalProperty {
    CLIENT_GLOBAL_FUNCTION,  /*  默认为encryption  */
    CMK_KEY_STORE,             /*  目前仅支持localkms  */
    CMK_KEY_PATH,              /*  密钥存储路径  */
    CMK_ALGORITHM              /*  指定加密CEK的算法  */
} ClientLogicGlobalProperty;
CREATE CLIENT MASTER KEY cmk_1 WITH (KEY_STORE = LOCALKMS , KEY_PATH = "kms_1" , ALGORITHM = RSA_2048);

上面命令的参数说明为:

(1) KEY_STORE:指定管理CMK的组件或工具;目前仅支持localkms模式。

(2) KEY_PATH:一个KEY_STORE中存储了多个CMK,而KEY_PATH用于唯一标识CMK。

(3) ALGORITHM:CMK被用于加密CEK,该参数指定加密CEK的算法,即指定CMK的密钥类型。

客户端主密钥创建语法本质上是将CMK的元信息解析并保存在CreateClientLogicGlobal结构体中。其中global_key_name是密钥名称,global_setting_params是一个List结构,每个节点是一个ClientLogicGlobalParam结构,以键值的形式保存着密钥的信息。客户端先通过解析器“fe_raw_parser()”解析为CreateClientLogicGlobal结构体,对其参数进行校验并发送查询语句到服务端;服务端解析为CreateClientLogicGlobal结构体并检查用户namespace等权限,CMK元信息保存在系统表中。创建CMK的总体流程如图39所示。

图片

图39 客户端主密钥CMK创建流程

有了主密钥CMK,可以基于此创建CEK,下面将对CREATE COLUMN ENCRYPTION KEY语句所涉及的语法结构定义进行逐一介绍。

CREATE COLUMN ENCRYPTION KEY语法相关数据结构:

/*  保存创建列加密密钥的语法信息  */
typedef struct CreateClientLogicColumn {
    NodeTag type;
    List *column_key_name;        /*  列加密密钥名称  */
    List *column_setting_params; /*  列加密密钥参数  */
} CreateClientLogicColumn;

/*  保存列加密密钥参数,保存在CreateClientLogicColumn的column_setting_params中  */
typedef struct ClientLogicColumnParam {
    NodeTag type;
    ClientLogicColumnProperty key;
    char *value;
    unsigned int len;
    List *qualname;
    int location;
} ClientLogicColumnParam;

/*  保存列加密密钥参数的key的枚举类型  */
typedef enum class ClientLogicColumnProperty {
    CLIENT_GLOBAL_SETTING,    /*  加密CEK的CMK  */
    CEK_ALGORITHM,             /*  加密用户数据的算法  */
    CEK_EXPECTED_VALUE,       /*  CEK密钥明文,可选参数  */
    COLUMN_COLUMN_FUNCTION,  /*  默认为encryption  */
} ClientLogicColumnProperty;

CREATE COLUMN ENCRYPTION KEY cek_1 WITH VALUES (CLIENT_MASTER_KEY = cmk_1, ALGORITHM = AEAD_AES_256_CBC_HMAC_SHA256);

上面命令的参数说明为:

(1) CLIENT_MASTER_KEY:指定用于加密CEK的CMK对象。

(2) ALGORITHM:CEK被用于加密用户数据,该参数指定加密用户数据的算法,即指定CEK的密钥类型。

(3) ENCRYPTED_VALUE:列加密密钥的明文,默认随机生成,也可由用户指定,用户指定时密钥长度范围为28~256位。

列加密密钥创建语法是通过前端解析器将参数解析成CreateClientLogicColumn结构体后,通过校验指定用于加密CEK的CMK对象是否存在后加载CMK缓存,然后通过“HooksManager::ColumnSettings::pre_create”语句调用加密函数“EncryptionColumnHookExecutor::pre_create”来校验各参数并生成或加密ENCRYPTED_VALUE值,最后在“EncryptionPreProcess::set_new_query”函数中替换ENCRYPTED_VALUE参数为CEK密文,重构SQL查询语句。重构后的SQL语句发送给服务端后服务端解析为CreateClientLogicColumn结构体并检查用户namespace等权限,将CEK的信息保存在系统表中。创建CEK的总体流程如图40所示,组织结构如图41所示。

图片

图40 列加密密钥CEK创建流程

图片

图41 客户端主密钥CMK的组织结构

在对CEK参数进行解析后,使用CMK对ENCRYPTED_VALUE参数进行加密,加密完成后使用加密后的ENCRYPTED_VALUE参数和其他参数对创建CEK的语法进行重构。

接下来创建加密表。

创建加密表的SQL语句在语法解析后进入CreateStmt函数处理逻辑,在run_pre_create_statement函数中,对CreateStmt->tableElts中每个ListCell进行判断,当前加密表仍存在一定的约束.

在将创建加密表的查询语句发送给服务端后,服务端创建成功并返回执行成功的消息。数据加密驱动程序能够实现在数据发送到数据库之前透明地加密数据,数据在整个语句的处理过程中以密文形式存在,在返回结果时,解密返回的数据集,从而保证整个过程对用户是透明、无感知的。

定义了完整的加密表后,用户就可以用正常的方式将数据插入到表中。完整的加密过程见encrypt_data函数.

openGauss密态数据库在进行等值查询的时候,整个查询过程对用户是无感知的,虽然存储在数据库中的数据是密文形式,但在展示数据给用户的时候会将密文数据进行解密处理。以从加密表中进行等值查询语句为例,整个语句处理过程如图9-42所示。客户端解析SELECT查询语句中的列属性信息,如果缓存已有则从缓存中提取列属性信息;如果缓存中找不到,需要从服务端查询该信息,并缓存。列加密密钥CEK是以密文形式存储在服务端,客户端需要解密CEK。然后用其加密SELECT查询语句中条件参数。加密后的SELECT查询语句发送给数据库服务端执行完成后,返回加密的查询结果集。客户端用解密后的列加密密钥CEK解密SELECT查询结果集,并返回解密后的明文结果集给应用端。

图片

图42 SELECT语句时序图

等值查询处理ru

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值