文章目录
-
- pgcrypto 简介
- pgcrypto 安装
- 单向加密
-
- 通用哈希函数
- 密码哈希函数
- 双向加密
-
- PGP 加密函数
- PGP 函数选项
- 原始加密函数
- 随机数据函数
- 其他事项
-
- pgcrypto 配置
- NULL 处理
- 安全限制
大家好,我是只谈技术不剪发的 Tony 老师。
根据信息安全的相关要求,用户的敏感信息通常需要在数据库中进行加密存储,例如登录密码、信用卡号、身份证号等。PostgreSQL 提供了一个扩展的模块:pgcrypto ;今天我们就来介绍一下这个模块的使用方法和案例。
如果觉得文章有用,欢迎评论、点赞、推荐
pgcrypto 简介
pgcrypto 扩展模块可以用于 PostgreSQL 中实现加密和解密功能。从 PostgreSQL 13 版本开始 pgcrypto 属于“可信”模块;只要用户拥有当前数据库上的 CREATE 权限就可以安装该模块,不再需要超级用户权限。
pgcrypto 提供了两类加密算法:单向加密和双向加密。
单向加密属于不可逆加密,无法根据密文解密出明文,适用于数据的验证,例如登录密码验证。常用的单向加密算法有 MD5、SHA、HMAC 等。双向加密属于可逆加密,根据密文和密钥可解密出明文,适用于数据的安全传输,例如电子支付、数字签名等。常用的双向加密算法有 AES、DES、RSA、ECC 等。
pgcrypto 安装
首先,我们需要安装 pgcrypto 模块。对于 Linux 操作系统,可以通过 postgresql-contrib 软件包下载 PostgreSQL 扩展模块;对于 Windows 和 macOS,默认安装已经包含了扩展模块。我们只需要执行以下语句在当前数据库中安装 pgcrypto 模块:
CREATE EXTENSION pgcrypto;
单向加密
通用哈希函数
digest()
函数可以根据不同的算法生成数据的二进制哈希值,语法如下:
digest(data text, type text) returns bytea
digest(data bytea, type text) returns bytea
其中,data 是原始数据;type 是加密算法,包括 md5、sha1、sha224、sha256、sha384 以及 sha512;函数的返回结果为二进制字符串。
假如存在以下用户表:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
username varchar(20) NOT NULL UNIQUE,
password text NOT NULL
);
创建新用户时,可以使用 digest() 函数对密码进行加密存储:
INSERT INTO users(username, password)
VALUES ('tony', encode(digest('123456','md5'), 'hex'));
INSERT INTO users(username, password)
VALUES ('anne', encode(digest('123456','md5'), 'hex'));
SELECt * FROM users;
id|username|password |
--|--------|--------------------------------|
1|tony |e10adc3949ba59abbe56e057f20f883e|
2|anne |e10adc3949ba59abbe56e057f20f883e|
其中,encode 函数用于将二进制字符串转换为十六进制的文本。
当用户登录时,使用同样的方法加密输入的密码参数:
-- 输入正确密码时
SELECt id
FROM users
WHERe username = 'tony'
AND password = encode(digest('123456','md5'), 'hex');
id|
--|
1|
-- 输入错误密码时
SELECt id
FROM users
WHERe username = 'tony'
AND password = encode(digest('abc123','md5'), 'hex');
PostgreSQL 还提供了内置的 md5()、sha224()、sha256()、sha384() 以及 sha512() 函数。
这类加密算法的主要问题是相同的数据经过加密之后的结果相同。因此。在实际应用中可以将用户名和密码字符串连接之后再进行加密。另一种方法就是使用hmac()
函数:
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;函数的返回结果为二进制字符串。
以下语句使用 hmac() 函数重新设置了用户的密码:
UPDATe users
SET password = encode(hmac('123456', username, 'md5'), 'hex');
SELECT * FROM users;
id|username|password |
--|--------|--------------------------------|
1|tony |7a86cd4a12d7a54d65a4fe5854aaf41f|
2|anne |9079d683b5fc5033427c2af2b6de4d01|
使用 username 作为密钥,相同的密码加密之后的结果不同。
对于 digest() 函数,如果同时被修改了原始数据和加密结果,无法进行识别;hmac() 函数只要密钥没有泄露的话,可以发现被篡改的数据。
密码哈希函数
crypt() 和 gen_salt() 函数专用于密码加密,其中 crypt() 用于加密数据,gen_salt() 用于生成 salt(加盐)。
crypt() 中的算法和普通的 MD5 或者 SHA1 哈希算法存在以下不同之处:
- crypt() 中的算法它们更慢。由于密码包含的数据量很小,这是增加暴力破解难度的唯一方法。
- 它们使用了一个随机值(称为盐值),因此密码的用户加密后的密码不同。这也可以针对破解算法提供一种额外的安全保护。
- 它们的结果中包括了算法类型,因此可以针对不同用户使用不同的算法对密码进行加密。
- 其中一些算法具有自适应性,意味着当计算机性能变得更快时,可以调整算法使其变得更慢,而不会产生与已有密码的不兼容性。
下表列出了 crypt() 函数支持的算法:
算法 | 密码最大长度 | 自适应性 | 盐值比特位数 | 输出结果长度 | 描述 |
---|---|---|---|---|---|
bf | 72 | ️ | 128 | 60 | 基于 Blowfish 的 2a 变种算法 |
md5 | 无限 | 48 | 34 | 基于 MD5 的加密算法 | |
xdes | 8 | ️ | 24 | 20 | 扩展 DES |
des | 8 | 12 | 13 | 原始 UNIX 加密算法 |
crypt() 函数的语法如下:
crypt(password text, salt text) returns text
该函数返回 password 字符串 crypt(3) 格式的哈希值,salt 参数由 gen_salt() 函数生成。例如:
UPDATe users
SET password = crypt('123456', gen_salt('md5'));
SELECT * FROM users;
id|username|password |
--|--------|----------------------------------|
1|tony |$1$ivLmU/yu$PS07Htg2x3KCiTVEu/rPz.|
2|anne |$1$NwIIA5wL$USvujGEN8otgSkRNf9BAN1|
对于相同的密码,crypt() 函数每次也会返回不同的结果,因为 gen_salt() 函数每次都会生成不同的 salt。校验密码时可以将之前生成的哈希结果作为 salt:
SELECt id
FROM users
WHERe username = 'tony'
AND password = crypt('123456', password);
id|
--|
1|
gen_salt() 函数用于生成盐值 salt,语法如下:
gen_salt(type text [, iter_count integer ]) returns text
该函数每次都会生成一个随机的盐值字符串,该字符串同时决定了 crypt() 函数使用的算法;type 参数用于指定一个生成字符串的哈希算法,可能的取值包括 des、xdes、md5 以及 bf。
SELECt gen_salt('des'), gen_salt('xdes'), gen_salt('md5'), gen_salt('bf');
gen_salt|gen_salt |gen_salt |gen_salt |
--------|---------|-----------|-----------------------------|
vT |_J9..AtLK|$1$Ukh6Ogiu|$2a$06$GFfpofxOmtSWbnO3GXs5oe|
每种算法生成的 salt 拥有固定的格式,例如 bf 算法结果中的 $2a$06$,2a 表示 Blowfish 的 2a 变种算法,06 表示迭代的次数。
对于 xdes 和 bf 算法,iter_count 参数用于指定迭代的次数。迭代次数越多,计算的时间越长,破解所需的时间也越长。过高的迭代次数可能使得计算一个哈希值需要几年的时间,但是这并没有什么实际用途。如果忽略 iter_count,将会使用默认的迭代次数。
算法 | 默认次数 | 最小次数 | 最大次数 |
---|---|---|---|
xdes | 725 | 1 | 16777215 |
bf | 6 | 4 | 31 |
对于 xdes 算法,迭代次数必须是一个奇数。
如果想要选择一个合适的迭代次数,可以参考原始 DES 加密算法设计时的性能是在当时的硬件上每秒执行 4 次加密。每秒少于 4 次加密可能会降低可用性,每秒多于 100 次加密又可能太快了。
下表给出了不同哈希算法的相对性能比较。表中还列出了它们遍历所有由 8 字符组成的密码所需的时间,密码只包含小写字母、或者大小写字母及数字。 对于 crypt-bf 算法,斜杠后面的数字代表了 gen_salt() 函数中的 iter_count 参数。
算法 | 哈希次数/秒 | [a-z] | [A-Za-z0-9] | 相当于 MD5 消耗的时间倍数 |
---|---|---|---|---|
crypt-bf/8 | 1792 | 4 年 | 3927 年 | 100k |
crypt-bf/7 | 3648 | 2 年 | 1929 年 | 50k |
crypt-bf/6 | 7168 | 1 年 | 982 年 | 25k |
crypt-bf/5 | 13504 | 188 天 | 521 年 | 12.5k |
crypt-md5 | 171584 | 15 天 | 41 年 | 1k |
crypt-des | 23221568 | 157.5 分钟 | 108 天 | 7 |
sha1 | 37774272 | 90 分钟 | 68 天 | 4 |
md5 (hash) | 150085504 | 22.5 分钟 | 17 天 | 1 |
备注:
- 以上测试使用的机器是 Intel Mobile Core i3。
- crypt-des 和 crypt-md5 算法的结果来自 John the Ripper v1.6.38 -test 结果。
- md5 哈希的结果来自 mdcrack 1.2。
- sha1 的结果来自 lcrack-20031130-beta。
- crypt-bf 的结果通过简单遍历 1000 次 8 字符组成的密码得到。这种方式可以比较不同迭代次数的性能。以下结果可以作为参考:john -test 显示 crypt-bf/5 每秒循环 13506 次(结果中的细微差异说明 pgcrypto 中的 crypt-bf 实现和 John the Ripper 相同)。
实际情况下并不会使用“遍历所有组合”的破解方法,通常是利用一个包含常用词汇及其各种变化形式的字典进行密码破解。因此,一些类似于单词的密码可能会比上面的字符串更快被破解;而由 6 个字符组成的不像单词的密码可能不会被破解,也可能会被破解。
双向加密
PGP 加密函数
PGP 加密函数实现了 OpenPGP(RFC 4880)标准中的加密功能,包括对称密钥加密(私钥加密)和非对称密钥加密(公钥加密)。
一个加密后的 PGP 消息由 2 部分(包)组成:
- 一个包含会话密钥(加密后的对称密钥或者公钥)的包;
- 一个使用会话密钥对数据加密后的包。
对于对称密钥(也就是口令)加密:
- 使用 String2Key(S2K)算法对密钥进行加密,类似于执行一个特意减慢并且包含随机 salt 的 crypt() 算法,生成一个完整长度的二进制密钥。
- 如果要求使用一个单独的会话密钥,生成一个随机的密钥;否则,使用上面的 S2K 密钥直接作为会话密钥。
- 如果直接使用 S2K 密钥,只将 S2K 设置加入会话密钥包中;否则,使用 S2K 密钥对会话密钥进行加密,然后放入会话密码包中。
对于公钥加密:
- 生成一个随机的会话密钥。
- 使用公钥对其进行加密后放入会话密钥包中。
无论哪种情况,对于数据的加密过程如下:
- 执行可选的数据操作:压缩、转换为 UTF-8 以及/或者换行符的转换。
- 在数据前面增加一个随机字节组成的块,相当于使用了一个随机的初始值(IV)。
- 计算随机前缀和数据的 SHA1 哈希值,追加到数据的后面。
- 将所有内容使用会话密钥进行加密后放入数据包中。
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 参数用于设置选项,参考下文。
pgp_sym_decrypt()
函数用于解密 PGP 对称密钥加密后的消息:
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 参数用于设置选项,参考下文。为了避免输出无效的字符,不允许使用 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 参数用于设置选项,参考下文。
pgp_pub_decrypt()
函数用于解密 PGP 公共密钥加密后的消息:
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 参数中指定密码;如果没有使用密码保护,想要指定 options 参数时必须指定一个空的 psw。options 参数用于设置选项,参考下文。为了避免输出无效的字符,不允许使用 pgp_pub_decrypt 函数对 bytea 数据进行解密;可以使用 pgp_pub_decrypt_bytea 对原始文本数据进行解密。
pgp_key_id()
函数用于提取 PGP 公钥或者私钥的密钥 ID;如果传入一个加密后的消息,将会返回加密该消息使用的密钥 ID:
pgp_key_id(bytea) returns text
该函数可能返回 2 个特殊的密钥 ID:
- SYMKEY,表明该消息使用对称密钥进行加密。
- ANYKEY,表明该消息使用公共密钥进行加密,但是密钥 ID 已经被删除。这也意味着你需要尝试所有的私钥,查找可以解密该消息的私钥。pgcrypto 不会产生这种加密消息。
注意,不同的密钥可能拥有相同的 ID,这种情况很少见但可能存在。客户端应用程序需要自己尝试使用不同的密钥进行解密,就像处理 ANYKEY 一样。
armor()
函数用于将二进制数据转换为 PGP ASCII-armor 格式,相当于 Base64 加上 CRC 以及额外的格式化。dearmor()
函数用于执行相反的转换:
armor(data bytea [ , keys text[], values text[] ]) returns text
dearmor(data text) returns bytea
其中,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 字符。
下面我们来看一个实例,首先为 users 表增加一个信用卡字段:
ALTER TABLE users ADD COLUMN card bytea UNIQUE;
然后我们需要生成 PGP 密钥,对于 Linux 操作系统可以使用 gpg 工具,对于 Windows 系统可以下载 GnuPG。执行以下命令创建一个新的密钥:
gpg --gen-key
然后按照提示输入相关信息。推荐使用 DSA and Elgamal 密钥;对于 RSA 加密,必须创建一个仅用于签名的 DSA 或者 RSA 密钥作为主控密钥,然后使用 gpg --edit-key 增加一个 RSA 加密子密钥。
然后可以使用 gpg --list-secret-keys 查看创建的密钥:
gpg --list-secret-keys
/root/.gnupg/secring.gpg
------------------------
sec 2048R/92A1CA53 2020-10-15
uid tonydong (good luck) <tony.dong@163.com>
ssb 2048R/4A973FF0 2020-10-15
其中,2048R 是密钥的比特长度, 92A1CA53 是私钥,4A973FF0 是公钥。
将公钥和私钥转换为 ASCII-armor 格式:
gpg -a --export 4A973FF0 > public.key
gpg -a --export-secret-keys 92A1CA53 > secret.key
其中,-a 表示 armour 格式;默认的密钥是二进制格式,不方便处理。在使用 pgcrypto PGP 加密/解密函数时需要利用 dearmor() 函数将密钥转换为二进制再传入参数;如果可以直接处理二进制数据,也可以去掉 -a 选项。
更多关于 GnuPG 的使用信息,可以参考The GNU Privacy Handbook 或者其他文档。
直接查看公钥 public.key 的内容如下:
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2.0.22 (GNU/Linux)
mQENBF+H26ABCACZ69PvxKPxPxPAXUAUT6xVvcrlkXQfCUccIVtoLx5YnhrJ8Itp
xu+hRB4XD7ZOA37PxZQi/3NPxxtAyhXOKuhITiqPSMDWblLWAnIC2ZANseNrqPA7
/yTdmQNT3cOk/MIqhBgF7f4O5JTfxvNdPeAxn+y5wxzUL+vpDXTlzeNNSMX41ukM
DexBFbiORLv992ACq56KnKDkOJgt82eMENL87Kac0/few5RHb/SrQLHQjpBVU7es
XL3ihernBA3bD/LpC5+pv3sC468NsOWGoevGSxkqprJ4mrsW7zSvZCyPmhWZz5Ra
zUrgPNvzmG8NRe/ZE1GwQwTTIozjzZXKCf4jABEBAAG0KHRvbnlkb25nIChnb29k
IGx1Y2spIDx0b255LmRvbmdAaWd0LmNvbT6JATkEEwECACMFAl+H26ACGwMHCwkI
BwMCAQYVCAIJCgsEFgIDAQIeAQIXgAAKCRB0hYKKkqHKU4auB/wKHey1k4xnYLX1
I2GJL9Huj8dgp1LzUf+mgfgqkNPPJtDOk27gcCpi2lCelLhLAAM3KLVhtr9na6wk
YlH9DFod59dwZ18gKJyMstfDg40pmjQz5QZhWkPIoPRvCGQ2XA2PVWbLNH3eMXC8
n6VTLybFiMb+PJG80to9Rmez51XPxA5+NCX+X/KLESm2Zngafsm5Rw/nR5ne9Qcr
jW4uKG8RZqp4lVh7NTnwsKdMu9BC4i4yqwh16IT506ibl7e3BLb+hMgENjzp871k
YlQbRxQpToaIaMyrWmWiT9TZ6kXd8euB6DhdH8pjRLqpmF/V0pjVVY8bwsw16Keg
7LBGr2zyuQENBF+H26ABCACuBYd+cDkAnjV91QX5BtnIcuWvhLr4q3t4oZnPQE7K
70AcnXMTu6YTjs80/Ds5d7NAn7ZPsSRGOxChsT7F+VviTsP0LzrH9cXxa3jLCSr0
k0hpWCKPOCHmDrS1kiOMw3b7Q+6ooOmlCBu+wZbkVSUWM9vud24zXqDWZZ9ssFh1
vxh0IaAUWUV9wTmTeMK6TGhAcG9/78r60P2MRKWrvU6AZRTwDa3HmnR91Y/MUmvE
ZuGuKCxFdL4d3647vemvWQM2zSLCJUfwvVPT9C7UsghRG1V24POE/sHDOe/gpyNE
7WXgfkFZRLIoSwoFdnETX0HYYzcodK6B+S7Q7qN7m3nbABEBAAGJAR8EGAECAAkF
Al+H26ACGwwACgkQdIWCipKhylMwbQf/foXSHwXhvBQ5ddF2eToz3rhH4RdIpJA2
EtOoIN37p0Svafz/j9BMDMAaA1TK0H8wuqALI92LK+EgYQJdi9JSjruKLjNq3IHm
7KP1CdP7Rvfk1TKKpj7gAeIsb7YOvZc1jb2QPurq7ehsJdlLTq8gf43NlvTd7a8c
O20nRVLAv97BEylJKFCcazpTfQTknDkdx7v/XFWEwUbw/Ex1BCpQjqRITNroAMt4
ZKtOt6bQ7hwiIuoFeZaEX3IRhqbaSyGukCav6TGFXytIO0iKjT0/pNcXonCisQ+C
mKeM4pyVJLHW/a8Rv3UrKBv7f/avuQ2IRkf2xAr2JbeWyaLCi4x7Hg==
=4f0y
-----END PGP PUBLIC KEY BLOCK-----
为了方便处理,可以创建一个存储公钥的表 keys:
CREATE TABLE keys(v text);
INSERT INTO keys VALUES ('');
把 public.key 的内容替换到 INSERT 语句,将公钥插入该表中。
接下来我们就可以将信用卡号进行加密存储:
UPDATE users
SET card = pgp_pub_encrypt('62220001', dearmor(keys.v))
FROM keys
WHERe username = 'tony';
SELECt card
FROM users
WHERe username = 'tony';
card |
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
ÁÀL ;pFuJ ?ð L G£âC òÔðã ± ± í äÝ´#B =ÂÅe 3N W³ ë$e}U;÷ jv(· /Û'\k¥Î C nîö 3ç§ õ 6i IØ*Å¿ K * à _4ô Éa ¬U z* éú© F mv .íÞ§'å n D^Y ½j° i9/ ø ¤ =[ £U y· w ¨LáN¿ Á4{ (Ï»m@ ¥ r ÝC\ý ÎcGg üñG Q© oµWX õ s ð ½^/ @Ï TÒOÁ¦0 ï>UÖHÐ 6µ} ¦ I 2 |
查询结果显示 card 字段已经被加密存储。
我们可以使用 pgp_key_id() 函数验证加密使用的公钥:
SELECt pgp_key_id(card)
FROM users
WHERe username = 'tony';
pgp_key_id |
----------------|
3B7046754A973FF0|
SELECt pgp_key_id(dearmor(v))
FROM keys;
pgp_key_id |
----------------|
3B7046754A973FF0|
应用程序可以通过私钥 secret.key 解密信用卡号:
SELECt pgp_pub_decrypt(card, dearmor('-----BEGIN PGP PRIVATE KEY BLOCK----- ... -----END PGP PRIVATE KEY BLOCK-----'), '1234567890')
FROM users
WHERe username = 'tony';
pgp_pub_decrypt|
---------------|
62220001 |
其中,1234567890 是创建密钥时输入的口令。
注意,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,其他选项通常使用默认值。
选项 | 描述 | 取值 | 适用函数 |
---|---|---|---|
cipher-algo | 使用的密码算法。 | bf、aes128(默认值)、aes192、aes256;使用 OpenSSL 时还支持:3des、cast5 | pgp_sym_encrypt、pgp_pub_encrypt |
compress-algo | 使用的压缩算法,只有编译 PostgreSQL 时使用了 zlib 参数可用。 | 0,不压缩,默认值;1,ZIP 压缩;2,ZLIB 压缩(ZIP 加上元数据和 CRC) | pgp_sym_encrypt、pgp_pub_encrypt |
compress-level | 压缩级别,级别越高结果越小但速度更慢,0 表示不压缩 | 0、1-9,默认为 6 | pgp_sym_encrypt、pgp_pub_encrypt |
convert-crlf | 加密时是否将 \n 转换为 \r\n 并且解密时执行相反的转换,RFC 4880 指定文本数据需要使用 \r\n 作为换行符。 | 0(默认值)、1 | pgp_sym_encrypt、pgp_pub_encrypt pgp_sym_decrypt、pgp_pub_decrypt |
disable-mdc | 不使用 SHA-1 保护数据,仅用于兼容古老的 PGP 产品。 | 0(默认值)、1 | pgp_sym_encrypt、pgp_pub_encrypt |
sess-key | 使用单独的会话密钥。公钥加密总是使用单独的会话密钥;该选项用于对称密钥加密,因为它默认直接使用 S2K 密钥。 | 0(默认值)、1 | pgp_sym_encrypt |
s2k-mode | 使用的 S2K 算法。 | 0,不使用 salt,危险;1,使用 salt 但是迭代固定次数;3(默认值),使用 salt 同时迭代次数可变。 | pgp_sym_encrypt |
s2k-count | S2K 算法的迭代次数。 | 大于等于 1024 并且小于等于 65011712,默认为 65536 到 253952 之间的随机数。 | pgp_sym_encrypt 并且 s2k-mode=3 |
s2k-digest-algo | S2K 计算时的摘要算法。 | md5、sha1(默认值) | pgp_sym_encrypt |
s2k-cipher-algo | 加密单独会话密钥时使用的密码。 | bf、aes、aes128、aes192、aes256,默认使用 cipher-algo 的算法。 | pgp_sym_encrypt |
unicode-mode | 是否将文本数据在数据库内部编码和 UTF-8 之间来回转换。如果数据库已经是 UTF-8、不会执行转换,但是消息将被标记为 UTF-8;如果没有指定这个选项就不会被标记。 | 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 算法
- aes,AES 算法(Rijndael-128、-192 或者-256)
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 encode(gen_random_bytes(16), 'hex');
encode |
--------------------------------|
8f8ac42ff5cbb82637f8dd8e653328e1|
gen_random_uuid()
函数用于返回一个 version 4 的随机 UUID,从 PostgreSQL 13 开始成为了一个内置函数:
SELECT gen_random_uuid();
gen_random_uuid |
------------------------------------|
69657400-23b1-4ee6-9f37-fbcad4c9562c|
其他事项
pgcrypto 配置
pgcrypto 可以根据 PostgreSQL 编译时的 configure 脚本进行自我配置,相关的选项包括 --with-zlib 以及 --with-openssl。
如果编译时使用了 zlib 选项,PGP 加密函数可以在加密之前对数据进行压缩。如果编译时使用了 OpenSSL 选项,PGP 加密函数可以支持更多的算法;同时公钥加密函数速度会更快,因为 OpenSSL 提供了优化的 BIGNUM 函数。下表比较了使用或者不使用 OpenSSL 时支持的功能:
支持功能 | 内置 | OpenSSL |
---|---|---|
MD5 | ️ | ️ |
SHA1 | ️ | ️ |
SHA224/256/384/512 | ️ | ️ |
其他摘要算法 | ️ (1) | |
Blowfish | ️ | ️ |
AES | ️ | ️ |
DES/3DES/CAST5 | ️ | |
原始加密 | ️ | ️ |
PGP 对称加密 | ️ | ️ |
PGP 公钥加密 | ️ | ️ |
备注 1:OpenSSL 支持的任何摘要算法都是自动选择的;无法支持密码。
NULL 处理
所有函数都遵循 SQL 表中,如果任何参数为 NULL,结果返回 NULL。如果使用时不小心,可能会造成安全风险。
安全限制
所有的 pgcrypto 函数都在数据库服务器中运行,意味着数据和密码在客户端和 pgcrypto 之间使用明文进行传输。因此我们必须:
- 使用本地连接或者 SSL 连接;
- 信任系统管理员和数据库管理员。
如果无法做到以上两点,一个更好的方式就是在客户端应用程序中完成加密/解密。
另外,pgcrypto 的实现无法抵抗旁路攻击(Side Channel Attacks)。例如,对于指定大小的不同密文,pgcrypto 解密函数所需的时间不同。
关于 pgcrypto 模块的更多信息可以参考 PostgreSQL 官方文档。