openGauss数据库源码解析系列文章—安全管理源码解析三(下篇)
6.2 数据动态脱敏
数据脱敏,顾名思义就是将敏感数据通过变形、屏蔽等方式处理,其目的是保护隐私数据信息,防止数据泄露和恶意窥探。当企业或者机构收集用户个人身份数据、手机、银行卡号等敏感信息,然后将数据通过导出(非生产环境)或直接查询(结合生产环境)的方式投入使用时,按照隐私保护相关法律法规需将数据进行“脱敏”处理。
openGauss实现了数据动态脱敏机制,它根据一系列用户配置的“脱敏策略”来对查询命令进行分析匹配,最终将敏感数据屏蔽并返回。使用数据动态脱敏特性总的来说分为两个步骤:配置脱敏策略、触发脱敏策略。本小节将对这两个步骤进行具体分析。
显然只有在配置脱敏策略后系统才能有根据地进行敏感数据脱敏。openGauss提供了脱敏策略配置(创建、修改、删除)语法,这些语法所涉及的语法解析节点内容大致相同,因此这里仅对创建策略相关数据结构进行分析,其余不再赘述。下面将结合一个具体示例对数据动态脱敏特性进行详细介绍。
表6给出了一张包含敏感信息(薪资、银行卡号)的个人信息表,策略管理员要对该表中的敏感信息创建脱敏策略:当用户user1或user2在IP地址10.123.123.123上使用JDBC或gsql连接数据库并查询个人信息表时,系统将自动屏蔽敏感信息。
表6 个人信息表person
id | name | gender | salary | creditcards |
1 | 张三 | 男 | 10000 | 6210630600006321083 |
2 | 李四 | 男 | 15000 | 6015431250003215514 |
3 | 王五 | 女 | 20000 | 5021134522201529881 |
首先策略管理员需要对敏感列打标签,随后使用标签创建脱敏策略,策略配置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代码如下:
typedef struct CreateMaskingPolicyStmt
{
NodeTag type;
char *policy_name; /* 脱敏策略名称 */
List *policy_data; /* 脱敏策略行为 */
List *policy_filters; /* 用户过滤条件 */
bool policy_enabled; /* 策略开关 */
} CreateMaskingPolicyStmt;
脱敏策略创建语法是对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),脱敏策略才会被触发。逻辑树节点结构如下所示:
typedef struct PolicyFilterNode
{
NodeTag type;
char *node_type; /* 逻辑操作类型,取值为“op”或“filter” */
char *op_value; /* 逻辑操作符,仅当node_type为op时取值为“and”或“or”,否则为NULL */
char *filter_type;/* 过滤数据类型,仅当node_type为filter时取值为“APP”、“ROLES”、“IP” */
List *values; /* 过滤数据值List,指出具体的过滤条件值,若node_type为op时置NULL */
Node *left; /* 左子树 */
Node *right; /* 右子树 */
} PolicyFilterNode;
逻辑树节点分为操作符(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是互斥的。
脱敏策略冲突或互斥场景如下所示:
策略A:CREATE MASKING POLICY mask_A MASKALL ON LABEL(creditcard_label) FILTER ON IP(’10.123.123.123’), APP(jdbc), ROLES(user1);
策略B:CREATE MASKING POLICY mask_B CREDITCARDMASKING ON LABEL(creditcard_label) FILTER ON IP(’10.123.123.123’,’10.90.132.132’), APP(jdbc, gsql), ROLES(user1);
策略C:CREATE MASKING POLICY mask_C CREDITCARDMASKING ON LABEL(creditcard_label) FILTER ON IP(’10.123.123.123’ ,’10.90.132.132’), APP(jdbc), ROLES(user2);
随后将依据策略配置信息更新系统表:
(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) 若当前用户信息与脱敏策略的过滤条件匹配,则说明对当前用户生效。
在每个脱敏策略从系统表读入缓存时,需要将对应的过滤条件逻辑表达式转换为逻辑树并将逻辑树根节点存入缓存中,将其作为脱敏策略筛选条件。逻辑树结构代码如下:
class PolicyLogicalTree {
public:
…
bool parse_logical_expression(const gs_stl::gs_string logical_expr_str); /* 逻辑表达式构造逻辑树入口函数 */
bool match(const FilterData *filter_item);
bool has_intersect(PolicyLogicalTree *arg);
private:
gs_stl::gs_vector<PolicyLogicalNode> m_nodes; /* 逻辑节点集合,包含了逻辑树中所有的节点 */
gs_stl::gs_vector<int> m_flat_tree; /* 利用数组将逻辑节点索引构造逻辑二叉树 */
/* 逻辑表达式转换为逻辑树的递归函数 */
bool parse_logical_expression_impl(const gs_stl::gs_string logical_expr_str, int *offset, int *idx, Edirection direction);
inline void create_node(int *idx, EnodeType type, bool has_operator_not); /* 创建单个逻辑树节点 */
void flatten_tree(); /* 将逻辑树刷新到m_nodes集合与m_flat_tree索引中 */
bool check_apps_intersect(string_sort_vector*, string_sort_vector*);
bool check_roles_intersect(oid_sort_vector*, oid_sort_vector*);
bool m_has_ip; /* 标识整个逻辑树是否涉及ip校验 */
bool m_has_role; /* 标识整个逻辑树是否涉及用户名校验 */
bool m_has_app; /* 标识整个逻辑树是否涉及客户端校验 */
};
逻辑树节点的结构与语法解析中的FILTER节点类似,具体可以参照PolicyFilterNode结构。相关代码如下:
struct PolicyLogicalNode {
...
EnodeType m_type;
int m_left; /* 左子节点索引 */
int m_right; /* 右子节点索引 */
void make_eval(const FilterData *filter_item); /* 判断用户信息是否满足本节点子树表示的逻辑。 */
bool m_eval_res;
oid_sort_vector m_roles; /* 本节点包含的用户名集合 */
string_sort_vector m_apps; /* 本节点包含的客户端名称集合 */
IPRange m_ip_range; /* 本节点包含的IP */
};
当需要将逻辑表达式转变为逻辑树时,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节点进行策略匹配并根据策略内容进行节点替换。
识别脱敏节点源码如下:
static bool mask_expr_node(ParseState *pstate, Expr*& expr,
const policy_set *policy_ids, masking_result *result, List* rtable, bool can_mask)
{
if (expr == NULL) {
return false;
}
switch (nodeTag(expr)) {
case T_SubLink:
... /* 解析SubLink节点 */
case T_FuncExpr:
... /* 解析FuncExpr节点 */
case T_Var:
return handle_masking_node(pstate, expr, policy_ids, result, rtable, can_mask); /* 进入最后脱敏处理过程 */
break;
case T_RelabelType:
... /* 解析RelabelType节点 */
case T_CoerceViaIO:
... /* 解析CoerceViaIO节点 */
case T_Aggref:
... /* 解析Aggref节点 */
case T_OpExpr:
... /* 解析OpExpr节点 */
default:
break;
}
return false;
}
在匹配脱敏策略时,首先需要将识别出的Var节点进行解析,将其转为PolicyLabelItem,该数据结构存储了数据列的全部路径信息,然后将其与已过滤出的脱敏策略集合进行匹配;若某个脱敏策略对应的数据库资源对象与PolicyLabelItem一致,将已匹配到的脱敏策略指定的方式替换该Var节点。相关数据结构PolicyLabelItem的代码如下:
struct PolicyLabelItem {
...
void get_fqdn_value(gs_stl::gs_string *value) const;
bool operator < (const PolicyLabelItem& arg) const;
bool operator == (const PolicyLabelItem& arg) const;
bool empty() const {return strlen(m_column) == 0;}
void set_object(const char *obj, int obj_type = 0);
void set_object(Oid objid, int obj_type = 0);
Oid m_schema; /* 数据库资源所属的namespace OID */
Oid m_object; /* 数据库资源所属的table OID */
char m_column[256];/* 列名
int m_obj_type; /* 资源类型,数据动态脱敏仅支持对column生效 */
};
脱敏策略匹配成功后,将会根据策略内容替换包含敏感信息的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的语法进行重构。将输入的查询语句转换成加密查询语句的主要函数入口代码如下:
void EncryptionPreProcess::set_new_query(char **query, size_t query_size, StringArgs string_args, int location,
int encrypted_value_location, size_t encrypted_value_size, size_t quote_num)
{
for (size_t i = 0; i < string_args.Size(); i++) {
/* 从string_args中读取键值存到变量中 */
char string_to_add[MAX_KEY_ADD_LEN];
errno_t rc = memset_s(string_to_add, MAX_KEY_ADD_LEN, 0, MAX_KEY_ADD_LEN);
securec_check_c(rc, "\0", "\0");
size_t total_in = 0;
if (string_args.at(i) == NULL) {
continue;
}
const char *key = string_args.at(i)->key;
const char *value = string_args.at(i)->value;
const size_t vallen = string_args.at(i)->valsize;
if (!key || !value) {
Assert(false);
continue;
}
Assert(vallen < MAX_KEY_ADD_LEN);
/* 将key和value构造成encrypted_value = '密文值'的形式 */
check_strncat_s(strncat_s(string_to_add, MAX_KEY_ADD_LEN, key, strlen(key)));
total_in += strlen(key);
check_strncat_s(strncat_s(string_to_add, MAX_KEY_ADD_LEN, "=\'", strlen("=\'")));
total_in += strlen("=\'");
check_strncat_s(strncat_s(string_to_add, MAX_KEY_ADD_LEN, value, vallen));
total_in += vallen;
check_strncat_s(strncat_s(string_to_add, MAX_KEY_ADD_LEN, "\'", strlen("\'")));
total_in += strlen("\'");
Assert(total_in < MAX_KEY_ADD_LEN);
/* encrypted_value_location不为空,则说明用户提供EXPECTED_VALUE,将明文值替换成密文值 */
if (encrypted_value_location && encrypted_value_size) {
*query = (char *)libpq_realloc(*query, query_size, query_size + vallen + 1);
if (*query == NULL) {
return;
}
check_memset_s(memset_s(*query + query_size, vallen + 1, 0, vallen + 1));
char *replace_dest = *query + encrypted_value_location + strlen("\'");
char *move_src =
*query + encrypted_value_location + encrypted_value_size + quote_num + strlen("\'");
char *move_dest = *query + encrypted_value_location + vallen + strlen("\'");
check_memmove_s(memmove_s(move_dest,
query_size - encrypted_value_location - encrypted_value_size - strlen("\'") + 1,
move_src,
query_size - encrypted_value_location - encrypted_value_size - strlen("\'")));
query_size = query_size + vallen - encrypted_value_size;
check_memcpy_s(memcpy_s(replace_dest, query_size - encrypted_value_location, value, vallen));
} else {
/* EXPECTED_VALUE是随机生成的,则直接插入原先的语句中 */
check_strcat_s(strcat_s(string_to_add, MAX_KEY_ADD_LEN, ","));
size_t string_to_add_size = strlen(string_to_add);
*query = (char *)libpq_realloc(*query, query_size, query_size + string_to_add_size + 1);
if (*query == NULL) {
return;
}
check_memmove_s(memmove_s(*query + location + string_to_add_size, query_size - location, *query + location,
query_size - location));
query_size += string_to_add_size;
check_memcpy_s(memcpy_s(*query + location, query_size - location, string_to_add, string_to_add_size));
}
query[0][query_size] = '\0';
}
return;
}
接下来创建加密表。
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));
创建加密表的SQL语句在语法解析后进入CreateStmt函数处理逻辑,在run_pre_create_statement函数中,对CreateStmt->tableElts中每个ListCell进行判断,当前加密表仍存在一定的约束,加密表列定义及约束处理函数段代码如下:
bool createStmtProcessor::run_pre_create_statement(const CreateStmt * const stmt, StatementData *statement_data)
{
…
/* 加密表列定义及约束处理 */
foreach (elements, stmt->tableElts) {
Node *element = (Node *)lfirst(elements);
switch (nodeTag(element)) {
case T_ColumnDef: {
…
/* 校验distribute by是否符合规格 */
if (column->colname != NULL &&
!check_distributeby(stmt->distributeby, column->colname)) {
return false;
}
/* 列定义处理,存储加密类型,加密密钥等信息 */
if (!process_column_defintion(column, element, &expr_vec, &cached_columns,
&cached_columns_for_defaults, statement_data)) {
return false;
}
break;
}
/* 处理check, unique 或其他约束 */
case T_Constraint: {
Constraint *constraint = (Constraint*)element;
if (constraint->keys != NULL) {
ListCell *ixcell = NULL;
foreach (ixcell, constraint->keys) {
char *ikname = strVal(lfirst(ixcell));
for (size_t i = 0; i < cached_columns.size(); i++) {
if (strcmp((cached_columns.at(i))->get_col_name(), ikname) == 0 && !check_constraint(
constraint, cached_columns.at(i)->get_data_type(), ikname, &cached_columns)) {
return false;
}
}
}
} else if (constraint->raw_expr != NULL) {
if (!transform_expr(constraint->raw_expr, "", &cached_columns)) {
return false;
}
}
break;
}
default:
break;
}
}
…
/* 加密约束中需要加密的明文数据 */
if (!RawValues::get_raw_values_from_consts_vec(&expr_vec, statement_data, 0, &raw_values_list)) {
return false;
}
return ValuesProcessor::process_values(statement_data, &cached_columns_for_defaults, 1,
&raw_values_list);
}
在将创建加密表的查询语句发送给服务端后,服务端创建成功并返回执行成功的消息。数据加密驱动程序能够实现在数据发送到数据库之前透明地加密数据,数据在整个语句的处理过程中以密文形式存在,在返回结果时,解密返回的数据集,从而保证整个过程对用户是透明、无感知的。
定义了完整的加密表后,用户就可以用正常的方式将数据插入到表中。完整的加密过程见encrypt_data函数,其核心逻辑代码如下所示:
int encrypt_data(const unsigned char *plain_text, int plain_text_length, const AeadAesHamcEncKey &column_encryption_key,
EncryptionType encryption_type, unsigned char *result, ColumnEncryptionAlgorithm column_encryption_algorithm)
{
……
/* 得到16位的iv值 */
unsigned char _iv [g_key_size + 1] = {0};
unsigned char iv_truncated[g_iv_size + 1] = {0};
/* 确定性加密,则通过hmac_sha256生成iv */
if (encryption_type == EncryptionType::DETERMINISTIC_TYPE) {
hmac_sha256(column_encryption_key.get_iv_key(), g_auth_tag_size, plain_text, plain_text_length, _iv);
……
} else {
/* 随机加密,则随机生成iv */
if (encryption_type != EncryptionType::RANDOMIZED_TYPE) {
return 0;
}
int res = RAND_priv_bytes(iv_truncated, g_block_size);
if (res != 1) {
return 0;
}
}
int cipherStart = g_algo_version_size + g_auth_tag_size + g_iv_size;
/* 调用encrypt计算密文 */
int cipherTextSize = encrypt(plain_text, plain_text_length, column_encryption_key.get_encyption_key(), iv_truncated,
result + cipherStart, column_encryption_algorithm);
……
int ivStartIndex = g_auth_tag_size + g_algo_version_size;
res = memcpy_s(result + ivStartIndex, g_iv_size, iv_truncated, g_iv_size);
securec_check_c(res, "\0", "\0");
/* 计算 HMAC */
int hmacDataSize = g_algo_version_size + g_iv_size + cipherTextSize;
hmac_sha256(column_encryption_key.get_mac_key(), g_auth_tag_size,
result + g_auth_tag_size, hmacDataSize, result);
return (g_auth_tag_size + hmacDataSize);
}
openGauss密态数据库在进行等值查询的时候,整个查询过程对用户是无感知的,虽然存储在数据库中的数据是密文形式,但在展示数据给用户的时候会将密文数据进行解密处理。以从加密表中进行等值查询语句为例,整个语句处理过程如图9-42所示。客户端解析SELECT查询语句中的列属性信息,如果缓存已有则从缓存中提取列属性信息;如果缓存中找不到,需要从服务端查询该信息,并缓存。列加密密钥CEK是以密文形式存储在服务端,客户端需要解密CEK。然后用其加密SELECT查询语句中条件参数。加密后的SELECT查询语句发送给数据库服务端执行完成后,返回加密的查询结果集。客户端用解密后的列加密密钥CEK解密SELECT查询结果集,并返回解密后的明文结果集给应用端。
图42 SELECT语句时序图
等值查询处理run_pre_insert_statement函数,其核心逻辑代码如下所示:
bool Processor::run_pre_select_statement(const SelectStmt * const select_stmt, const SetOperation &parent_set_operation,
const bool &parent_all, StatementData *statement_data, ICachedColumns *cached_columns, ICachedColumns *cached_columns_parents)
{
bool select_res = false;
/* 处理SELECT语句中的集合操作 */
if (select_stmt->op != SETOP_NONE) {
select_res = process_select_set_operation(select_stmt, statement_data, cached_columns);
RETURN_IF(!select_res);
}
/* 处理WHERE子句 */
ExprPartsList where_expr_parts_list;
select_res = exprProcessor::expand_expr(select_stmt->whereClause, statement_data, &where_expr_parts_list);
RETURN_IF(!select_res);
……
/* 从FROM子句中获取缓存加密列 */
CachedColumns cached_columns_from(false, true);
select_res = run_pre_from_list_statement(select_stmt->fromClause, statement_data, &cached_columns_from,
cached_columns_parents);
……
/* 将查询的加密列放在cached_columns结构中 */
for (size_t i = 0; i < cached_columns_from.size(); i++) {
if (find_in_name_map(target_list, cached_columns_from.at(i)->get_col_name())) {
CachedColumn *target = new (std::nothrow) CachedColumn(cached_columns_from.at(i));
if (target == NULL) {
fprintf(stderr, "failed to new CachedColumn object\n");
return false;
}
cached_columns->push(target);
}
}
if (cached_columns_from.is_empty()) {
return true;
}
/* 加密列不支持ORDER BY(排序)操作 */
if (!deal_order_by_statement(select_stmt, cached_columns)) {
return false;
}
/* 将WHERE子句中加密的值进行加密处理 */
if (!WhereClauseProcessor::process(&cached_columns_from, &where_expr_parts_list, statement_data)) {
return false;
}
……
return true;
}
完整的客户端密文解密函数代码如下所示:
int decrypt_data(const unsigned char *cipher_text, int cipher_text_length,
const AeadAesHamcEncKey &column_encryption_key, unsigned char *decryptedtext,
ColumnEncryptionAlgorithm column_encryption_algorithm)
{
if (cipher_text == NULL || cipher_text_length <= 0 || decryptedtext == NULL) {
return 0;
}
/* 校验密文长度 */
if (cipher_text_length < min_ciph_len_in_bytes_with_authen_tag) {
printf("ERROR(CLIENT): The length of cipher_text is invalid, cannot decrypt.\n");
return 0;
}
/* 校验密文中的版本号 */
if (cipher_text[g_auth_tag_size] != '1') {
printf("ERROR(CLIENT): Version byte of cipher_text is invalid, cannot decrypt.\n");
return 0;
}
……
/* 计算MAC标签 */
unsigned char authenticationTag [g_auth_tag_size] = {0};
int HMAC_length = cipher_text_length - g_auth_tag_size;
int res = hmac_sha256(column_encryption_key.get_mac_key(), g_auth_tag_size,
cipher_text + g_auth_tag_size, HMAC_length, authenticationTag);
if (res != 1) {
printf("ERROR(CLIENT): Fail to compute a keyed hash of a given text.\n");
return 0;
}
/* 校验密文是否被篡改 */
int cmp_result = my_memcmp(authenticationTag, cipher_text, g_auth_tag_size);
/* 解密数据 */
int decryptedtext_len = decrypt(cipher_text + cipher_start_index, cipher_value_length,
column_encryption_key.get_encyption_key(), iv, decryptedtext, column_encryption_algorithm);
if (decryptedtext_len < 0) {
return 0;
}
decryptedtext[decryptedtext_len] = '\0';
return decryptedtext_len;
}
七、 小结
随着信息安全的挑战越来越严重,保护系统安全可靠的运行,守护用户的数据隐私安全成为是当前数据库产品必须具备的能力。openGauss将逐步构建更加完备的安全能力,从身份认证、角色模型、权限管理、审计追踪、数据加解密等多维度来守护系统和数据安全。
本章节详细解析了openGauss的安全架构,并通过关键数据结构和关键函数代码解读描述每一种安全防护机制的实现细节,这些代码实现细节将有助于开发者了解openGauss的安全原理,并基于最新的安全标准来不断地优化和改善安全机制。
文章转载自公众号:openGauss