使用pgcrypto加密GaussDB(DWS)数据
GaussDB(DWS)数据库自8.2.0集群版本开始内置加密解密模块pgcrypto。pgcrypto模块允许数据库用户以加密形式存储数据的某些列,为敏感数据增加了一层额外的保护。因此在没有加密密钥的情况下,任何人都无法读取以加密形式存储在GaussDB(DWS)数据库中的数据。
pgcrypto函数在数据库服务器内部运行,这意味着所有数据和密码都以明文形式在pgcrypto和客户端应用程序之间传输。为了获得最佳安全性,建议在客户端和GaussDB(DWS)服务器之间使用SSL连接。
有关pgcrypto模块中各个函数的详细信息如下:
通用哈希函数
- digest()
digest()函数可以根据不同的算法生成数据的二进制哈希值,语法如下:
digest(data text, type text) returns bytea digest(data bytea, type text) returns bytea
其中,data是原始数据,type是加密算法包括md5、sha1、sha224、sha256、sha384、sha512和sm3;函数的返回结果为二进制字符串。
示例:
使用digest() 函数对字符串GaussDB(DWS)进行sha256加密存储:
select digest('GaussDB(DWS)', 'sha256'); digest -------------------------------------------------------------------- \xcc2d1b97c6adfba44bbce7386516f63f16fc6e6a10bd938861d3aba501ac8aab (1 row)
- hmac()
hmac()函数可以根据不同的算法为带有密钥的数据计算出MAC值,语法如下:
hmac(data text, key text, type text) returns bytea hmac(data bytea, key bytea, type text) returns bytea
其中,data是原始数据,key是加密密钥,type是加密算法,包括md5、sha1、sha224、sha256、sha384、sha512以及sm3;函数的返回结果为二进制字符串。
示例:
使用key123密钥和sha256加密算法对字符串GaussDB(DWS)计算MAC值:
select hmac('GaussDB(DWS)', 'key123', 'sha256'); hmac -------------------------------------------------------------------- \x14e1d9e110e9b11ab8379dc02b49533d50a6f4deafe6d6cd451d06c106c97d83 (1 row)
对于digest() 函数,如果同时被修改了原始数据和加密结果,无法进行识别;hmac() 函数只要密钥没有泄露的话,可以发现被篡改的数据。
如果该密钥大于哈希块的长度,它将先被哈希然后把结果用作密钥。
密码哈希函数
crypt()和gen_salt()函数专用于哈希密码。crypt()执行哈希用于加密数据,gen_salt()用于生成加盐哈希。
crypt()中的算法和普通的MD5或者SHA1哈希算法存在以下不同之处:
- crypt()中算法很慢。由于密码包含的数据量很小,这是增加暴力破解难度的唯一方法。
- 它们使用一个随机值(称为salt,即盐值),因此这样具有相同口令的用户将得到不同的密文口令。这也是针对破解算法提供一种额外的安全保护。
- 它们的结果中包括了算法类型,因此可以针对不同用户使用不同的算法对密码进行加密。
- 其中一些算法具有自适应性,意味着当计算机性能变得更快时,可以调整该算法使其变得更慢,而不会产生与已有密码的不兼容性。
crypt()函数所支持的算法如下表:
算法 |
密码最大长度 |
自适应性 |
Salt位数 |
输出结果长度 |
描述 |
---|---|---|---|---|---|
bf |
72 |
√ |
128 |
60 |
基于Blowfish的2a变种算法 |
md5 |
unlimited |
× |
48 |
34 |
基于MD5的加密算法 |
xdes |
8 |
√ |
24 |
20 |
扩展DES |
des |
8 |
× |
12 |
13 |
原生UNIX加密算法 |
- crypt()
该函数返回password字符串crypt(3)格式的哈希值,salt参数由gen_salt()函数生成。
对于相同的密码,crypt()函数每次也会返回不同的结果,因为gen_salt()函数每次都会生成不同的salt。校验密码时可以将之前生成的哈希结果作为salt。
例如:设置一个新密码:
UPDATE ... SET pswhash = crypt('new password', gen_salt('bf',10));
通过比较存储的密码哈希值验证输入的密码的正确性。
SELECT (pswhash = crypt('entered password', pswhash)) AS pswmatch FROM ... ;
如果输入的口令正确,这会返回true。
示例:
create table userpwd(userid int8, pwd text); CREATE TABLE insert into userpwd values (1, crypt('this is a pwd', gen_salt('bf',10))); INSERT 0 1 select crypt('this is a pwd', pwd)=pwd as result from userpwd where userid =1; result -------- t (1 row) select crypt('this is a wrong pwd', pwd)=pwd as result from userpwd where userid =1; result -------- f (1 row)
- gen_salt()
该函数每次都会生成一个随机的盐值(salt)字符串,该字符串同时决定了crypt()函数使用的算法;type参数用于指定一个生成字符串的哈希算法,取值包括 des、xdes、md5 以及 bf。对于xdes和bf算法,iter_count指迭代次数,数字越大加密时间越长,被破解需要的时间也越长。
1 2 3 4 5
SELECT gen_salt('des'), gen_salt('xdes'), gen_salt('md5'), gen_salt('bf'); gen_salt | gen_salt | gen_salt | gen_salt ----------+-----------+-------------+------------------------------- qh | _J9..uEUi | $1$SNgqyKAi | $2a$06$B/Etc3J8zYBV49LrDU97MO (1 row)
每种算法生成的salt拥有固定的格式,例如bf算法结果中的$2a$06$,2a表示Blowfish的2a变种算法,06表示迭代的次数。
如果忽略 iter_count,将会使用默认的迭代次数。允许的iter_count值与算法相关,如下表所示。对于xdes算法,迭代次数必须是一个奇数。
表2 crypt()的迭代计数 算法
默认值
最小值
最大值
xdes
725
1
16777215
bf
6
4
31
PGP加密函数
GaussDB(DWS)的PGP加密函数遵循OpenPGP(RFC 4880)标准,包括对称密钥加密(私钥加密)和非对称密钥加密(公钥加密)。
加密后的PGP消息由两部分组成:
- 这个消息的会话密钥(加密过的对称密钥或者公钥)。
- 使用该会话密钥加密的数据。
对于对称密钥(也就是密码)加密:
- 使用String2Key(S2K)算法对密钥进行加密,类似于执行一个特意减慢并且包含随机salt的crypt() 算法,生成一个完整长度的二进制密钥。
- 如果要求一个单独的会话密钥,生成一个随机的密钥。否则使用上面的S2K密钥直接作为会话密钥。
- 如果直接使用S2K密钥,只将S2K设置加入会话密钥包中。否则,使用S2K密钥对会话密钥进行加密,然后放入会话密钥包中。
对于公钥加密:
- 生成一个随机的会话密钥。
- 使用公钥对其进行加密后放入会话密钥包中。
无论哪种情况,数据的加密过程如下:
- 执行可选的数据操作:压缩、转换成UTF-8或者换行符的转换。
- 在数据前面增加一个随机字节组成的块,相当于使用了一个随机的初始值(IV)。
- 追加随机前缀和数据的SHA1哈希值到数据后面。
- 将所有内容使用会话密钥进行加密后放入数据包中。
支持的PGP加密函数:
- pgp_sym_encrypt()
语法格式:
pgp_sym_encrypt(data text, psw text [, options text ]) returns bytea pgp_sym_encrypt_bytea(data bytea, psw text [, options text ]) returns bytea
其中,data是要加密的数据;psw是PGP对称密钥;options 参数用于设置选项,参考表3。
- pgp_sym_decrypt()
语法格式:
pgp_sym_decrypt(msg bytea, psw text [, options text ]) returns text pgp_sym_decrypt_bytea(msg bytea, psw text [, options text ]) returns bytea
其中,msg是要解密的消息;psw是PGP对称密钥;options参数用于设置选项,参考表3。为了避免输出无效的字符,不允许使用pgp_sym_decrypt函数对bytea数据进行解密;可以使用 pgp_sym_decrypt_bytea 对原始文本数据进行解密。
- pgp_pub_encrypt()
语法格式:
pgp_pub_encrypt(data text, key bytea [, options text ]) returns bytea pgp_pub_encrypt_bytea(data bytea, key bytea [, options text ]) returns bytea
其中,data是要加密的数据;key是PGP公钥,如果传入一个私钥将会返回错误;options参数用于设置选项,参考表3。
- pgp_pub_decrypt()
语法格式:
pgp_pub_decrypt(msg bytea, key bytea [, psw text [, options text ]]) returns text pgp_pub_decrypt_bytea(msg bytea, key bytea [, psw text [, options text ]]) returns bytea
解密一个公共密钥加密的消息。key必须是对应于用来加密的公钥的私钥。如果私钥是用口令保护的,必须在psw中给出该口令。如果没有口令,但想要指定选项,需要给出一个空口令。
为了避免输出无效的字符,不允许使用pgp_pub_decrypt函数对bytea数据进行解密;可以使用pgp_pub_decrypt_bytea对原始文本数据进行解密。
其中,key是公共密钥对应的私钥;如果私钥使用了密码保护功能,必须在psw参数中指定密码;如果没有使用密码保护,想要指定options参数时必须指定一个空的psw。options参数用于设置选项,参考表3。
- pgp_key_id()
描述:用于提取PGP公钥或者私钥的密钥ID;如果传入一个加密后的消息,将会返回加密该消息使用的密钥ID。
语法格式:pgp_key_id(bytea) returns text
该函数可能返回两个特殊密钥ID:
- SYMKEY,表示该消息使用对称密钥进行加密。
- ANYKEY,表示该消息使用公钥进行加密,但是密钥ID已经被删除。这意味着需要尝试所有的密钥,查找可以解密该消息的私钥。pgcrypto不会产生这种加密消息。
不同的密钥可能拥有相同的ID,这种情况很少见但可能存在。客户端应用程序需要自己尝试使用不同的密钥进行解密,就像处理ANYKEY一样。
- armor()
描述:用于将二进制数据转换为PGP ASCII-armor格式,相当于Base64加上CRC以及额外的格式化。
语法格式:
armor(data bytea [ , keys text[], values text[] ]) returns text
- dearmor()
语法格式:
dearmor(data text) returns bytea
将加密后的数据bytea,转换成PGP ASCII-armor格式或者反向转换。
其中,data是需要转换的数据;如果指定了keys和values数值,每个key/value对都会生成一个armor header并添加到编码格式中;两个数组都是一维数组,长度相同,并且不能包含非ASCII字符。
- pgp_armor_headers()
描述:函数用于返回数据中的armor header。
pgp_armor_headers(data text, key out text, value out text) returns setof record
返回结果是一个包含key和value两个字段的数据行集,如果其中包含任何非ASCII字符,都会被视作UTF-8字符。
用GnuPG生成PGP密钥
要生成一个新密钥:
gpg --gen-key
更好的密钥类型是“DSA和Elgamal”。
对于RSA密钥,必须创建仅用于签名的DSA或RSA密钥作为主控密钥,然后用gpg --edit-key增加一个RSA加密子密钥。
要列举密钥:
gpg --list-secret-keys
要以ASCII-保护格式导出一个公钥:
gpg -a --export KEYID > public.key
要以ASCII-保护格式导出一个私钥:
gpg -a --export-secret-keys KEYID > secret.key
在把这些密钥交给PGP函数之前,需要对它们使用dearmor()。或者如果你能处理二进制数据,可以从命令中去掉-a。
PGP加密函数的存在以下限制:
- 不支持签名。这也意味着它不会检查加密子密钥是否属于主控密钥。
- 不支持加密密钥作为主控密钥。由于通常并不鼓励这种用法,因此这应该不是问题。
- 不支持多个子密钥。由于实际应用中经常需要多个子密钥,这可能是个问题。另一方面,不要使用常规GPG/PGP密钥作为pgcrypto加密密钥,而应该创建新的密钥,因为这是非常不同的使用场景。
PGP函数的选项
pgcrypto函数中的选项名称和GnuPG类似,选项的值使用等号设置,每个选项使用逗号进行分隔。例如:
pgp_sym_encrypt(data, psw, 'compress-algo=1, cipher-algo=aes256')
除了convert-crlf之外,其他选项仅适用于加密函数。解密函数会从PGP数据中获取参数。
最常设置的选项包括compress-algo和unicode-mode。其他选项通常使用默认值。
表3 pgcrypto加密选项 选项
描述
默认值
取值
适用函数
cipher-algo
使用的密码算法。
aes128
bf, aes128, aes192, aes256, 3des, cast5
pgp_sym_encrypt, pgp_pub_encrypt
compress-algo
使用的压缩算法。
0
- 0,表示不压缩。
- 1,表示ZIP压缩。
- 2,表示ZLIB压缩 (= ZIP加上元数据和CRC)
pgp_sym_encrypt, pgp_pub_encrypt
compress-level
压缩级别。级别越高压缩得越小但速度也越慢。0表示不压缩。
6
0,1-9
pgp_sym_encrypt, pgp_pub_encrypt
convert-crlf
加密时是否将\n转换成\r\n并且解密时执行相反的转换是否把\r\n转换成\n。RFC4880指定文本数据存储时需要使用\r\n作为换行符。
0
0,1
pgp_sym_encrypt, pgp_pub_encrypt, pgp_sym_decrypt, pgp_pub_decrypt
disable-mdc
不用SHA-1保护数据。仅用于兼容老旧的PGP产品。
0
0,1
pgp_sym_encrypt, pgp_pub_encrypt
sess-key
使用单独的会话密钥。公钥加密总是使用一个单独的会话密钥。该选项用于对称密钥加密,因为对称密钥加密默认直接使用 S2K密钥。
0
0,1
pgp_sym_encrypt
s2k-mode
使用的S2K算法。
3
- 0,表示不使用salt。不推荐!
- 1,表示使用salt ,但是迭代固定次数。
- 3,可变的迭代计数。
pgp_sym_encrypt
s2k-count
S2K算法的迭代次数。
65536 和 253952 之间的一个随机数值
大于等于1024并且小于等于65011712
pgp_sym_encrypt,并且s2k-mode=3
s2k-digest-algo
S2K计算时的摘要算法。
sha1
md5, sha1
pgp_sym_encrypt
s2k-cipher-algo
加密单独会话密钥时使用的密码。
默认使用cipher-algo的算法
bf, aes, aes128, aes192, aes256
pgp_sym_encrypt
unicode-mode
是否将文本数据在数据库内部编码和UTF-8之间来回转换。如果当前数据库已经是UTF-8,不会执行转换,但是消息将被标记为UTF-8。没有指定该选项就不会被标记
0
0,1
pgp_sym_encrypt, pgp_pub_encrypt
原始加密函数
原始加密函数仅仅会对数据运行一次加密,不支持PGP加密的任何高级功能,因此存在以下问题:
- 直接将用户密钥作为加密密钥。
- 不提供任何完整性检查来校验加密后的数据是否被修改。
- 需要用户自己关联所有加密参数,包括初始值(IV)。
- 不支持处理文本数据。
因此,在引入了PGP加密后,不建议使用这些原始加密函数。
encrypt(data bytea, key bytea, type text) returns bytea decrypt(data bytea, key bytea, type text) returns bytea encrypt_iv(data bytea, key bytea, iv bytea, type text) returns bytea decrypt_iv(data bytea, key bytea, iv bytea, type text) returns bytea
其中,data是需要加密的数据;type用于指定加密/解密方法。type参数的语法如下:
algorithm [ - mode ] [ /pad: padding ]
其中algorithm的取值范围如下:
- bf,Blowfish算法。包括近义词:BF,BF-CBC;BLOWFISH,BF-CBC;BLOWFISH-CBC,BF-CBC;BLOWFISH-ECB,BF-ECB;BLOWFISH-CFB,BF-CFB。
- aes,AES算法(Rijndael-128, -192或-256)。包括近义词:AES,AES-CBC;RIJNDAEL,AES-CBC;RIJNDAEL,AES-CBC;RIJNDAEL-CBC,AES-CBC;RIJNDAEL-ECB,AES-ECB。
- DES算法。包括近义词:DES,DES-CBC;3DES,DES3-CBC;3DES-ECB,DES3-ECB;3DES-CBC,DES3-CBC
- sm4,SM4算法。包括近义词:SM4-CBC
- CAST5算法。包括近义词:CAST5-CBC
mode的可能取值范围如下:
- cbc,下一个块依赖前一个块(默认值)
- ecb,每个块独立加密(不推荐,仅用于测试)
padding的取值范围如下:
- pkcs,数据可以是任意长度(默认值)
- none,数据长度必须是密码块大小的倍数
例如,以下函数的加密结果相同:
encrypt(data, 'fooz', 'bf') encrypt(data, 'fooz', 'bf-cbc/pad:pkcs')
对于函数encrypt_iv和decrypt_iv,参数iv表示CBC模式的初始值,ECB模式会忽略该参数。如果它的长度不是准确的块大小,可能会被截断或者使用0进行填充。对于没有该参数的两个函数,默认全部使用0填充。
随机数据函数
- gen_random_bytes()函数用于生成具有强加密性的随机字节。
gen_random_bytes(count integer) returns bytea
其中,count表示返回的字节数,取值为1到1024。
示例:
SELECT gen_random_bytes(16); gen_random_bytes ------------------------------------ \x1f1eddc11153afdde0f9e1229f8f4caf (1 row)
- gen_random_uuid()函数用于返回一个version 4的随机UUID。
SELECT gen_random_uuid(); gen_random_uuid -------------------------------------- 2bd664a2-b760-4859-8af6-8d09ccc5b830