声明:本文的部分内容参考了他人的文章。在编写过程中,我们尊重他人的知识产权和学术成果,力求遵循合理使用原则,并在适用的情况下注明引用来源。
本文主要参考了 OpenGauss1.1.0 的开源代码和《OpenGauss数据库源码解析》一书以及OpenGauss社区学习文档和一些学习资料
在【OpenGauss源码学习 —— 列存储(CU)(一)】中我们初步认识了 CU 的结构和作用,以及在【 OpenGauss源码学习 —— 列存储(CU)(二)】中学习了CU 的压缩和解压方法。本文则来了解一下对 CU 的加密和解密操作。
CUDataEncrypt 函数用于在 CU 数据写入磁盘之前,对数据进行加密。它首先检查 CU 是否已经被加密,然后计算待加密数据的长度,接着调用加密函数执行加密操作,最后更新 CU 的元数据,以标记 CU 已被加密。这是数据安全性的一个关键组成部分,确保数据在磁盘上存储时得到保护。其函数源码如下所示:(路径:src/gausskernel/storage/cstore/cu.cpp
)
/*
* CU 数据加密。
* 由于 CU 没有缓冲区并直接写入磁盘,因此没有并发线程问题。
*/
void CU::CUDataEncrypt(char* buf)
{
// plainLength 用于存储未加密数据的长度,cipherLength 用于存储加密后数据的长度。
size_t plainLength = 0;
size_t cipherLength = 0;
// 如果当前的 CU 处于加密状态,执行以下操作。
if (isEncryptedCluster()) {
// 计算待加密数据的长度。
plainLength = m_cuSizeExcludePadding - GetCUHeaderSize() - m_bpNullCompressedSize;
// 如果待加密数据的长度大于 0,说明有数据需要加密。
if (plainLength > 0) {
// 调用 encryptBlockOrCUData 函数执行实际的加密操作。
// 参数包括输入缓冲区 buf、待加密数据的长度 plainLength,
// 输出缓冲区 buf,以及指向 cipherLength 的指针,用于存储加密后数据的长度。
encryptBlockOrCUData(buf, plainLength, buf, &cipherLength);
// 更新 CU 的信息模式(infoMode)以指示 CU 已被加密。
m_infoMode |= CU_ENCRYPT;
}
}
}
isEncryptedCluster 函数主要用于检查透明数据加密(Transparent Data Encryption,TDE)是否在集群中启用,如果不启用或相关条件不满足,它会返回 false,否则返回 true。如果透明数据加密被禁用或不受支持,函数可能会生成相应的日志或错误信息。总的来说,该函数用于检查是否在集群中启用了透明数据加密。以下是代码的详细注释:(路径:src/gausskernel/cbb/utils/aes/cipherfn.cpp
)
bool isEncryptedCluster()
{
// 如果当前处于安全模式(security mode)并且透明加密字符串为空或未设置,那么透明加密被禁用。
if (isSecurityMode && (g_instance.attr.attr_security.transparent_encrypted_string == NULL ||
*g_instance.attr.attr_security.transparent_encrypted_string == '\0')) {
// 输出一条 DEBUG5 级别的日志,指示在 DWS(可能是数据仓库系统)中已禁用透明加密。
ereport(DEBUG5, (errmsg("Transparent encryption disabled in DWS.\n")));
return false; // 返回 false 表示透明加密被禁用。
}
// 如果不处于安全模式并且透明加密 KMS(Key Management Service)的 URL 为空或未设置,那么透明加密被禁用。
else if ((!isSecurityMode) && (g_instance.attr.attr_common.transparent_encrypt_kms_url == NULL ||
*g_instance.attr.attr_common.transparent_encrypt_kms_url == '\0')) {
// 输出一条 DEBUG5 级别的日志,指示在 GaussDB(可能是高斯数据库)中已禁用透明加密。
ereport(DEBUG5, (errmsg("Transparent encryption disabled in GaussDB.\n")));
return false; // 返回 false 表示透明加密被禁用。
}
// 检查是否已禁用透明数据加密特性。如果已禁用,抛出一个错误并返回 false。
if (is_feature_disabled(TRANSPARENT_ENCRYPTION) == true) {
ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("TDE is not supported.")));
return false; // 返回 false 表示透明加密被禁用。
}
// 如果以上条件都未触发,说明透明加密在当前集群中启用。
return true; // 返回 true 表示透明加密已启用。
}
encryptBlockOrCUData 函数用于对数据进行加密,并处理了加密失败时的重试机制,以确保数据安全性。它首先检查加密 DEK 和 IV 是否已初始化,然后根据指定的加密算法执行加密操作。如果加密失败,会尝试最多三次,然后根据不同情况,抛出 PANIC 级别的错误以避免进一步的处理。其函数源码如下所示:(路径:src/gausskernel/cbb/utils/aes/cipherfn.cpp
)
/*
* encryptBlockOrCUData:
* 加密数据块或列单元(CU),不包括头部和填充部分,用户数据必须加密。
* 如果加密失败,重试三次,如果仍然失败,则退出进程,应使用 panic 而不是 fatal,
* 以避免在检查点或后台写入进程(bgwriter)中挂起。
*
* @IN plainText:明文数据及其长度
* @OUT cipherText:密文数据及其长度
* @RETURN: 无
*/
void encryptBlockOrCUData(const char* plainText, const size_t plainLength, char* cipherText, size_t* cipherLength)
{
int retryCnt = 0;
GS_UINT32 ret = 0;
/*
* 解密数据块或列单元(CU),不包括头部和填充部分,用户数据必须解密。
* 如果解密失败,重试三次,如果仍然失败,则退出进程,应使用 panic 而不是 fatal,
* 以避免在自动清理(autovacuum)过程中挂起。
*
* @IN cipherText:密文数据及其长度
* @OUT plainText:明文数据及其长度
* @RETURN: 无
*/
if ((trans_encrypt_dek == NULL || *trans_encrypt_dek == '\0') ||
(trans_encrypt_iv == NULL || *trans_encrypt_iv == '\0')) {
// 如果加密 DEK(Data Encryption Key)或 IV(Initialization Vector)未初始化,
// 则抛出 PANIC 级别的错误,指示参数未初始化或 DEK 和 IV 的长度不等于 16 字节。
ereport(PANIC,
(errmsg("这是一个加密集群,但参数未初始化!"),
errdetail("解密 DEK 或 IV 为空或两者的长度不等于16字节")));
}
do {
// 根据加密算法执行加密操作,使用加密 DEK 和 IV。
// 加密操作的结果存储在 cipherText 中,其长度存储在 cipherLength 中。
if (g_tde_algo == TDE_ALGO_SM4_CTR_128) {
ret = sm4_ctr_enc_partial_mode(
plainText, plainLength, cipherText, cipherLength, trans_encrypt_dek, trans_encrypt_iv);
} else {
ret = aes_ctr_enc_partial_mode(
plainText, plainLength, cipherText, cipherLength, trans_encrypt_dek, trans_encrypt_iv);
}
if (ret != 0) {
retryCnt++;
} else {
// 如果加密成功,检查明文和密文的长度是否一致。
if (plainLength != *cipherLength) {
ereport(PANIC,
(errmsg("加密失败,返回码为 %u!", ret),
errdetail("密文长度不正确,明文长度为 %lu,密文长度为 %lu",
plainLength, *cipherLength)));
}
return;
}
if (retryCnt == 2) {
// 如果重试三次后仍然加密失败,抛出 PANIC 级别的错误,指示返回码为无效参数或内部错误。
ereport(PANIC,
(errmsg("加密在重试三次后失败,错误码为 %u!", ret),
errdetail("返回码为无效参数或内部错误")));
}
} while (retryCnt < 3);
}
CUDataDecrypt 函数主要用于对已加密的 CU 数据进行解密。首先,它检查 CU 的信息模式,如果包含 CU_ENCRYPT 标志位,表示数据已被加密。然后,它计算密文数据的长度,以便调用解密函数 decryptBlockOrCUData 执行解密操作。解密后的数据将替代原始数据存储在 buf 中,同时清除了 CU_ENCRYPT 标志,以表示数据已解密。这有助于在读取 CU 数据时对其进行解密,以便访问明文数据。其函数源码如下所示:(路径:src/gausskernel/storage/cstore/cu.cpp
)
/*
* CU 数据解密。
* 由于 CU 没有缓冲区并直接写入磁盘,所以没有并发线程问题。
*/
void CU::CUDataDecrypt(char* buf)
{
// 检查 CU 的信息模式(infoMode),如果包含 CU_ENCRYPT 标志位,表示数据已被加密。
if ((m_infoMode & CU_ENCRYPT) != 0) {
size_t cipherLength = 0;
size_t plainLength = 0;
// 计算密文数据的长度,以便进行解密操作。
cipherLength = m_cuSizeExcludePadding - GetCUHeaderSize() - m_bpNullCompressedSize;
// 调用解密函数 decryptBlockOrCUData,对数据进行解密。
// 解密后的数据存储在原始的 buf 中,解密后的数据长度存储在 plainLength 中。
decryptBlockOrCUData(buf, cipherLength, buf, &plainLength);
// 清除 CU_ENCRYPT 标志位,表示数据已解密。
m_infoMode &= ~CU_ENCRYPT;
}
}
decryptBlockOrCUData 函数用于对已加密的数据进行解密,它会检查解密 DEK 和 IV 是否已初始化,并根据指定的解密算法执行解密操作。如果解密失败,它会尝试最多三次,然后根据不同情况,抛出错误以避免进一步的处理。这有助于确保在自动清理(autovacuum)等关键进程中不会挂起。函数源码如下所示:(路径:src/gausskernel/cbb/utils/aes/cipherfn.cpp
)
/*
* 解密数据块或列单元,不包括头部和填充部分,用户数据必须解密。
* 如果解密失败,重试三次,如果仍然失败,则退出进程,应使用 panic 而不是 fatal,
* 以避免在自动清理(autovacuum)过程中挂起。
*
* @IN cipherText:密文数据及其长度
* @OUT plainText:明文数据及其长度
* @RETURN: 无
*/
void decryptBlockOrCUData(const char* cipherText, const size_t cipherLength, char* plainText, size_t* plainLength)
{
int retryCnt = 0;
GS_UINT32 ret = 0;
// 检查解密 DEK(Data Encryption Key)和 IV(Initialization Vector)是否已初始化,如果没有则抛出错误。
if ((trans_encrypt_dek == NULL || *trans_encrypt_dek == '\0') ||
(trans_encrypt_iv == NULL || *trans_encrypt_iv == '\0')) {
ereport(ERROR,
(errcode(ERRCODE_UNEXPECTED_NULL_VALUE),
errmsg("这是一个加密集群,但参数未初始化!"),
errdetail("解密 DEK 或 IV 为空或两者的长度不等于16字节")));
}
do {
// 根据解密算法执行解密操作,使用解密 DEK 和 IV。
// 解密后的数据存储在 plainText 中,其长度存储在 plainLength 中。
if (g_tde_algo == TDE_ALGO_SM4_CTR_128) {
ret = sm4_ctr_dec_partial_mode(
cipherText, cipherLength, plainText, plainLength, trans_encrypt_dek, trans_encrypt_iv);
} else {
ret = aes_ctr_dec_partial_mode(
cipherText, cipherLength, plainText, plainLength, trans_encrypt_dek, trans_encrypt_iv);
}
if (ret != 0) {
retryCnt++;
} else {
// 如果解密成功,检查密文和明文的长度是否一致。
if (cipherLength != *plainLength) {
ereport(ERROR,
(errcode(ERRCODE_STRING_DATA_LENGTH_MISMATCH),
errmsg("解密失败,返回码为 %u!", ret),
errdetail("plainLength 长度不正确,cipherLength 为 %lu,plainLength 为 %lu",
cipherLength, *plainLength)));
}
return;
}
if (retryCnt == 2) {
// 如果重试三次后仍然解密失败,抛出错误,指示返回码为无效参数或内部错误。
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("解密在重试三次后失败,错误码为 %u!", ret),
errdetail("返回码为无效参数或内部错误")));
}
} while (retryCnt < 3);
}