F.26. pgcrypto — 密码学函数 #
pgcrypto 模块为 PostgreSQL 提供密码学函数。
该模块被认为是“受信任的”,也就是说,它可以由在当前数据库上具有 CREATE 权限的非超级用户安装。
pgcrypto 需要 OpenSSL,如果构建 PostgreSQL 时没有选择 OpenSSL 支持,则不会安装它。
F.26.1. 通用 hash 函数 #
F.26.1.1. digest() #
digest(data text, type text) returns bytea digest(data bytea, type text) returns bytea
计算给定 data 的二进制 hash 值。type 指定要使用的算法。标准算法包括 md5、sha1、sha224、sha256、sha384 和 sha512。此外,OpenSSL 支持的任何摘要算法也都会被自动识别并纳入支持范围。
如果想把摘要表示为十六进制字符串,可以对结果使用 encode()。例如:
CREATE OR REPLACE FUNCTION sha1(bytea) RETURNS text AS $$
SELECT encode(digest($1, 'sha1'), 'hex')
$$ LANGUAGE SQL STRICT IMMUTABLE;
F.26.1.2. hmac() #
hmac(data text, key text, type text) returns bytea hmac(data bytea, key bytea, type text) returns bytea
使用密钥 key 为 data 计算基于 hash 的消息认证码(HMAC)。type 与 digest() 中的相同。
这与 digest() 类似,但只有知道密钥时才能重新计算该 hash。这可以防止有人篡改数据后再同时修改 hash 使之匹配。
如果密钥大于 hash 块大小,则会先对其进行 hash 运算,并将结果用作密钥。
F.26.2. 密码 hash 函数 #
函数 crypt() 和 gen_salt() 是专门为对密码进行 hash 运算而设计的。crypt() 负责执行 hash 运算,而 gen_salt() 负责为其准备算法参数。
crypt() 中的算法在以下方面不同于通常的 MD5 或 SHA-1 hash 算法:
它们很慢。由于处理的数据量很小,这是让暴力破解密码变得困难的唯一办法。
它们使用一个称为 salt 的随机盐值,这样使用相同密码的用户也会得到不同的 hash 结果。这也为逆向求解该算法增加了一层额外防护。
它们会在结果中包含算法类型,这样用不同算法进行 hash 运算的密码就能共存。
其中一些是自适应的 — 这意味着当计算机变快时,你可以把算法调得更慢,而不引入与现有密码的不兼容性。
表 F.19 列出了 crypt() 函数支持的算法。
表 F.19. crypt() 支持的算法
| 算法 | 最大密码长度 | 是否自适应? | 盐值位数 | 输出长度 | 描述 |
|---|---|---|---|---|---|
bf | 72 | 是 | 128 | 60 | 基于 Blowfish,2a 变体 |
md5 | 无限制 | 否 | 48 | 34 | 基于 MD5 的 crypt |
xdes | 8 | 是 | 24 | 20 | 扩展 DES |
des | 8 | 否 | 12 | 13 | 原始 UNIX crypt |
sha256crypt | 无限制 | 是 | 最多 32 | 80 | 改编自公开可用的参考实现 Unix crypt using SHA-256 and SHA-512 |
sha512crypt | 无限制 | 是 | 最多 32 | 123 | 改编自公开可用的参考实现 Unix crypt using SHA-256 and SHA-512 |
F.26.2.1. crypt() #
crypt(password text, salt text) returns text
计算 password 的一个 crypt(3) 风格 hash。存储新密码时,需要使用 gen_salt() 生成新的 salt 值。校验密码时,把已存储的 hash 值作为 salt 传入,并测试结果是否与已存储值匹配。
设置一个新密码的示例:
UPDATE ... SET pswhash = crypt('new password', gen_salt('md5'));
身份验证示例:
SELECT (pswhash = crypt('entered password', pswhash)) AS pswmatch FROM ... ;
如果输入的密码正确,这会返回 true。
F.26.2.2. gen_salt() #
gen_salt(type text [, iter_count integer ]) returns text
生成一个供 crypt() 使用的新随机盐值字符串。该盐值字符串还会告诉 crypt() 应使用哪种算法。
type 参数指定 hash 算法。接受的类型有:des、xdes、md5、bf、sha256crypt 和 sha512crypt。最后两种即 sha256crypt 和 sha512crypt,是现代的、基于 SHA-2 的密码 hash。
iter_count 参数允许用户为支持该参数的算法指定迭代次数。次数越高,对密码进行 hash 所需的时间越长,从而破解它所需时间也越长。不过,如果次数过高,计算一个 hash 可能需要数年时间 — 这就不太现实了。若省略 iter_count 参数,则使用默认迭代次数。允许的 iter_count 值取决于算法,如表 F.20 所示。
表 F.20. crypt() 的迭代计数
| 算法 | 默认值 | 最小值 | 最大值 |
|---|---|---|---|
xdes | 725 | 1 | 16777215 |
bf | 6 | 4 | 31 |
sha256crypt, sha512crypt | 5000 | 1000 | 999999999 |
对 xdes 算法还有额外的限制:迭代计数必须是一个奇数。
为了选择合适的迭代次数,可以考虑原始 DES crypt 在当时硬件上的设计速度是每秒 4 次 hash。低于每秒 4 次 hash 可能会影响可用性,而高于每秒 100 次 hash 则很可能过快。
表 F.21 概述了不同 hash 算法之间的相对速度差异。该表展示了在 8 字符密码上尝试所有字符组合所需的时间,假定密码只包含小写字母,或者包含大小写字母和数字。在 crypt-bf 条目中,斜杠后的数字是 gen_salt 的 iter_count 参数值。
对 sha256crypt 和 sha512crypt 而言,默认的 iter_count 值 5000 对现代硬件来说被认为过低,但可以调高以生成更强的密码 hash。除此之外,sha256crypt 和 sha512crypt 都被认为是安全的。
表 F.21. Hash 算法速度
| 算法 | 每秒 hash 次数 | 针对[a-z] | 针对[A-Za-z0-9] | 相对于 md5 hash 的耗时倍数 |
|---|---|---|---|---|
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 hash的数字来自 mdcrack 1.2。sha1的数字来自 lcrack-20031130-beta。crypt-bf的数字是使用一个简单程序测得的,该程序循环处理 1000 个 8 字符密码。这样可以展示不同迭代次数下的速度。作为参考:john -test对crypt-bf/5给出的结果是 13506 次循环/秒。(结果上的极小差异与这样一个事实一致:crypt-bf在pgcrypto中的实现与 John the Ripper 使用的是同一套实现。)
请注意,“尝试所有组合”并不是现实中的做法。通常密码破解是借助词典完成的,其中包含常见单词及其各种变体。因此,即使是稍微有点像单词的密码,被破解的速度也可能远快于上表所示;而一个 6 字符、不像单词的密码则可能逃过破解,也可能不会。
F.26.3. PGP 加密函数 #
这里的函数实现了 OpenPGP (RFC 4880) 标准中的加密部分。同时支持对称密钥加密和公钥加密。
一个加密的 PGP 消息由两个部分,或称两个包组成:
包含会话密钥的包 — 该会话密钥要么由对称密钥加密,要么由公钥加密。
包含用会话密钥加密的数据的包。
当使用对称密钥(即密码)加密时:
给定密码使用 String2Key (S2K) 算法进行 hash 运算。这与
crypt()算法颇为类似 — 故意设计得较慢,并带有随机盐值 — 但它产生的是一个全长度的二进制密钥。如果请求单独的会话密钥,则会生成一个新的随机密钥。否则,S2K 密钥将直接用作会话密钥。
如果直接使用 S2K 密钥,那么会话密钥包中只写入 S2K 设置。否则,会话密钥会先用 S2K 密钥加密,再放入会话密钥包。
当使用公钥加密时:
会生成一个新的随机会话密钥。
该密钥会用公钥加密,并放入会话密钥包中。
无论哪种情况,要加密的数据都会按如下步骤处理:
可选的数据处理包括:压缩、转换为 UTF-8 和转换行结束符,三者可任意组合。
数据前面会加上一个随机字节块。这相当于使用随机 IV。
追加对随机前缀和数据计算得到的 SHA-1 hash 值。
然后用会话密钥加密所有这些内容,并放入数据包。
F.26.3.1. 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
使用对称 PGP 密码 psw 加密 data。options 参数可以包含下文所述的选项设置。
F.26.3.2. 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
解密一个经过对称密钥加密的 PGP 消息。
bytea 数据不能用 pgp_sym_decrypt 解密。这是为了避免输出无效字符数据。若原始数据本来是文本,则使用 pgp_sym_decrypt_bytea 解密也没有问题。
options 参数可以包含下文所述的选项设置。
F.26.3.3. 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
使用 PGP 公钥 key 加密 data。向该函数提供私钥会报错。
options 参数可以包含下文所述的选项设置。
F.26.3.4. 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 中给出密码。如果没有密码但想指定选项,则需要传入空密码。
bytea 数据不能用 pgp_pub_decrypt 解密。这是为了避免输出无效字符数据。若原始数据本来是文本,则使用 pgp_pub_decrypt_bytea 解密也没有问题。
options 参数可以包含下文所述的选项设置。
F.26.3.5. pgp_key_id() #
pgp_key_id(bytea) returns text
pgp_key_id 提取 PGP 公钥或私钥的密钥 ID。如果传入的是加密消息,则返回用于加密该数据的密钥 ID。
它可以返回两个特殊的密钥 ID:
SYMKEY该消息是用对称密钥加密的。
ANYKEY该消息是用公钥加密的,但密钥 ID 已被移除。这意味着你需要尝试自己的所有私钥,看看哪一个能解密它。
pgcrypto本身不会生成这样的消息。
注意,不同的密钥可能具有相同的 ID。这种情况虽然罕见,但属于正常情况。客户端应用此时应该尝试使用每一个密钥解密,以判断哪个匹配 —
就像处理 ANYKEY 时一样。
F.26.3.6. armor(), dearmor() #
armor(data bytea [ , keys text[], values text[] ]) returns text dearmor(data text) returns bytea
这些函数将二进制数据封装/解封装为 PGP ASCII-armor 格式,它本质上就是带 CRC 和附加格式信息的 Base64。
如果指定了 keys 和 values 数组,则会为每个键/值对添加一个装甲头(armor header)。两个数组都必须是一维的,且长度相同。键和值都不能包含任何非 ASCII 字符。
F.26.3.7. pgp_armor_headers #
pgp_armor_headers(data text, key out text, value out text) returns setof record
pgp_armor_headers() 从 data 中提取装甲头。返回值是一个包含两列的行集合,列名为 key 和 value。如果键或值包含任何非 ASCII 字符,则按 UTF-8 处理。
F.26.3.8. PGP 函数的选项 #
这些选项的命名方式与 GnuPG 类似。选项值应写在等号后面;各选项之间用逗号分隔。例如:
pgp_sym_encrypt(data, psw, 'compress-algo=1, cipher-algo=aes256')
除 convert-crlf 外,所有选项都只适用于加密函数。解密函数会从 PGP 数据中获取这些参数。
最值得关注的选项可能是 compress-algo 和 unicode-mode。其余选项应该都具有合理的默认值。
F.26.3.8.1. cipher-algo #
使用哪种密码算法。
取值:bf, aes128, aes192, aes256, 3des, cast5
默认值:aes128
适用于:pgp_sym_encrypt, pgp_pub_encrypt
F.26.3.8.2. compress-algo #
使用哪种压缩算法。仅当 PostgreSQL 在编译时包含 zlib 时才可用。
取值:
0 - 无压缩
1 - ZIP 压缩
2 - ZLIB 压缩(= ZIP 加元数据和块 CRC)
默认值:0
适用于:pgp_sym_encrypt, pgp_pub_encrypt
F.26.3.8.3. compress-level #
压缩程度。级别越高,压缩后越小,但速度也越慢。0 表示禁用压缩。
取值:0, 1-9
默认值:6
适用于:pgp_sym_encrypt, pgp_pub_encrypt
F.26.3.8.4. convert-crlf #
加密时是否将\n 转换成\r\n,以及解密时是否将\r\n 转换成\n。RFC 4880 规定文本数据应使用\r\n 作为换行符。使用这个选项可以获得完全符合 RFC 的行为。
取值:0, 1
默认值:0
适用于:pgp_sym_encrypt, pgp_pub_encrypt, pgp_sym_decrypt, pgp_pub_decrypt
F.26.3.8.5. disable-mdc #
不要用 SHA-1 保护数据。使用这个选项的唯一合理理由,是为了与古老的 PGP 产品兼容,这些产品出现于受 SHA-1 保护的数据包被纳入 RFC 4880 之前。较新的 gnupg.org 和 pgp.com 软件都能很好地支持这一特性。
取值:0, 1
默认值:0
适用于:pgp_sym_encrypt, pgp_pub_encrypt
F.26.3.8.6. sess-key #
使用单独的会话密钥。公钥加密总是使用单独的会话密钥;这个选项用于对称密钥加密,因为后者默认直接使用 S2K 密钥。
取值:0, 1
默认值:0
适用于:pgp_sym_encrypt
F.26.3.8.7. s2k-mode #
使用哪一种 S2K 算法。
取值:
0 - 不使用盐值。危险!
1 - 使用盐值,但迭代次数固定。
3 - 迭代次数可变。
默认值:3
适用于:pgp_sym_encrypt
F.26.3.8.8. s2k-count #
S2K 算法要使用的迭代次数。它必须是一个位于 1024 和 65011712 之间的值,首尾两个值包括在内。
默认值:65536 到 253952 之间的随机值
适用于:pgp_sym_encrypt,仅在 s2k-mode=3 时
F.26.3.8.10. s2k-cipher-algo #
使用哪种密码算法来加密单独的会话密钥。
取值:bf, aes, aes128, aes192, aes256
默认值:使用 cipher-algo
适用于:pgp_sym_encrypt
F.26.3.8.11. unicode-mode #
是否将文本数据在数据库内部编码和 UTF-8 之间相互转换。如果数据库已经是 UTF-8,则不会发生转换,但消息会被标记为 UTF-8。如果不启用该选项,则不会进行这种标记。
取值:0, 1
默认值:0
适用于:pgp_sym_encrypt, pgp_pub_encrypt
F.26.3.8.12. ignore-cipher-failure #
危险!通过完全忽略正在使用的 OpenSSL 密码算法所报告的失败,指示 pgcrypto
使用一种错误的解密算法,以匹配修复 CVE-2026-14663 之前的历史行为。此选项仅供需要恢复以下错误加密消息的用户使用:消息创建时,cipher-algo 在当时使用的 OpenSSL 配置下不可用。启用 ignore-cipher-failure 时,解密此类有缺陷的消息并不需要正确的解密密钥,因此无法保证解密得到的明文确实来自密钥持有者。
相比之下,有一种情况是消息已正确加密,但生成该消息的密码算法在当前
OpenSSL 配置下不可用。要通过 pgcrypto
恢复这类明文,必须让实际使用的密码算法可供 OpenSSL 使用,例如启用相应的提供者。在这种情况下,ignore-cipher-failure 既无必要,也没有帮助。如果使用此选项对已正确加密的消息进行“解密”时碰巧通过 PGP 完整性检查,这只是巧合,并不能使恢复出的明文变得可信。
取值:0, 1
默认值:0
适用于:pgp_sym_decrypt, pgp_pub_decrypt
F.26.3.9. 使用 GnuPG 生成 PGP 密钥 #
要生成新密钥:
gpg --gen-key
首选的密钥类型是“DSA 和 Elgamal”。
对于 RSA 加密,你必须先创建一个仅用于签名的 DSA 或 RSA 主密钥,然后使用 gpg --edit-key 添加 RSA 加密子密钥。
要列出密钥:
gpg --list-secret-keys
要以 ASCII-armor 格式导出公钥:
gpg -a --export KEYID > public.key
要以 ASCII-armor 格式导出私钥:
gpg -a --export-secret-keys KEYID > secret.key
在把这些密钥交给 PGP 函数之前,需要先用 dearmor() 处理它们。或者,如果你能处理二进制数据,也可以从命令中去掉 -a。
更多细节见 man gpg、GNU Privacy Handbook 以及 https://www.gnupg.org/上的其他文档。
F.26.3.10. PGP 代码的限制 #
不支持签名。这也意味着不会检查加密子密钥是否属于主密钥。
不支持使用加密密钥作为主密钥。由于通常不鼓励这种做法,这不应成为问题。
不支持多个子密钥。这看起来可能是个问题,因为这在实践中相当常见。另一方面,你不应将常规的 GPG/PGP 密钥用于
pgcrypto,而应新建一套密钥,因为其使用场景相当不同。
F.26.4. 原始加密函数 #
这些函数只是对数据应用密码算法;它们不具备 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
使用 type 指定的加密算法对数据进行加密/解密。type 字符串的语法是:
algorithm[-mode] [/pad:padding]
其中 algorithm 可以是:
bf— Blowfishaes— AES(Rijndael-128、-192 或 -256)
而 mode 可以是:
cbc— 下一个块依赖于前一个块(默认)cfb— 下一个块依赖于前一个已加密块ecb— 每个块单独加密(仅用于测试)
而 padding 可以是:
pkcs— 数据长度可以任意(默认)none— 数据必须是分组大小的倍数
因此,例如下面两种写法是等价的:
encrypt(data, 'fooz', 'bf') encrypt(data, 'fooz', 'bf-cbc/pad:pkcs')
在 encrypt_iv 和 decrypt_iv 中,iv 参数是 CBC 和 CFB 模式的初始值;对于 ECB,它会被忽略。如果长度不等于块大小,则会被截断或用零填充。在不带该参数的函数中,它默认为全零。
F.26.5. 随机数据函数 #
gen_random_bytes(count integer) returns bytea
返回 count 个具有密码学安全强度的随机字节。一次最多可提取 1024 个字节,以避免耗尽随机数生成器池。
gen_random_uuid() returns uuid
返回一个第 4 版(随机)UUID。(已过时,该函数在内部调用同名的核心函数。)
F.26.7. 配置参数 #
有一个配置参数可以控制 pgcrypto 的行为。
-
pgcrypto.builtin_crypto_enabled(enum) # pgcrypto.builtin_crypto_enabled决定内置密码学函数gen_salt()和crypt()是否可用。将其设为off会禁用这些函数。on(默认值)使这些函数按正常方式工作。fips在检测到 OpenSSL 以 FIPS 模式运行时会禁用这些函数。pgp_sym_encrypt()和pgp_pub_encrypt()不使用内置密码学函数,因此不受影响。
在通常用法中,此参数在 postgresql.conf 中设置,不过超级用户也可以在各自会话中动态修改它。
F.26.8. 注解 #
F.26.8.1. 配置 #
pgcrypto 会根据 PostgreSQL 主 configure
脚本的探测结果自行配置。影响它的选项有 --with-zlib 和
--with-ssl=openssl。
如果编译时包含 zlib,PGP 加密函数就能够在加密前压缩数据。
pgcrypto 需要 OpenSSL。否则它不会被构建或安装。
当针对 OpenSSL 3.0.0 及更高版本编译时,若要使用 DES 或 Blowfish 等旧式密码算法,必须在 openssl.cnf
配置文件中启用 legacy provider(旧版提供程序)。
F.26.8.2. NULL 处理 #
按照 SQL 的标准,只要任一参数为 NULL,所有函数都返回 NULL。这在使用不慎时可能带来安全风险。
F.26.9. 作者 #
Marko Kreen <markokr@gmail.com>
pgcrypto 使用了来自以下来源的代码:
| 算法 | 作者 | 源代码来源 |
|---|---|---|
| DES crypt | David Burren 及其他人 | FreeBSD libcrypt |
| MD5 crypt | Poul-Henning Kamp | FreeBSD libcrypt |
| Blowfish crypt | Solar Designer | www.openwall.com |