pgcrypto 模块为 PostgreSQL 提供密码学函数。
该模块被视为“可信”模块,也就是说,拥有当前数据库 CREATE 权限的非超级用户也可以安装它。
pgcrypto 需要 OpenSSL,如果构建 PostgreSQL 时未选择 OpenSSL 支持则不会安装。
digest() #digest(data text, type text) returns bytea digest(data bytea, type text) returns bytea
计算给定 data 的二进制哈希。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;
hmac() #hmac(data text, key text, type text) returns bytea hmac(data bytea, key bytea, type text) returns bytea
使用密钥 key 计算 data 的散列消息认证码。type 与 digest() 中的相同。
它与 digest() 类似,但只有知道密钥才能重新计算哈希。这可以防止有人篡改数据并同时修改哈希以使其匹配。
如果密钥大于哈希块大小,会先对它做哈希,然后把结果用作密钥。
函数 crypt() 与 gen_salt()
专为哈希密码而设计。crypt() 执行哈希,gen_salt() 为其准备算法参数。
crypt() 中的算法与常规的 MD5 或 SHA-1 哈希算法在以下方面有所不同:
它们很慢。由于数据量很小,这是使暴力破解密码变得困难唯一的手段。
它们使用一个称为 盐 的随机值,因此拥有相同密码的用户会得到不同的加密密码。这也是对抗算法逆向的额外防线。
它们把算法类型包含在结果中,因此用不同算法哈希的密码可以共存。
其中一些是自适应的 — 也就是说,当计算机变快时,你可以把算法调得更慢,而不会引入与现有密码的不兼容。
Table F.18 列出了 crypt() 函数支持的算法。
Table F.18. 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 | 改编自公开可用的参考实现 使用 SHA-256 与 SHA-512 的 Unix crypt |
sha512crypt | 无限制 | 是 | 最多 32 | 123 | 改编自公开可用的参考实现 使用 SHA-256 与 SHA-512 的 Unix crypt |
crypt() #crypt(password text, salt text) returns text
计算 password 的 crypt(3) 风格哈希。存储新密码时,你需要使用 gen_salt() 生成新的 salt 值。检查密码时,把存储的哈希值作为 salt 传入,然后测试结果是否与存储值匹配。
设置新密码的示例:
UPDATE ... SET pswhash = crypt('new password', gen_salt('md5'));
认证示例:
SELECT (pswhash = crypt('entered password', pswhash)) AS pswmatch FROM ... ;
如果输入的密码正确,这会返回 true。
gen_salt() #gen_salt(type text [, iter_count integer ]) returns text
为 crypt() 生成新的随机盐字符串。盐字符串还会告诉 crypt() 要使用哪种算法。
type 参数指定哈希算法。接受的类型有:des、xdes、md5、bf、sha256crypt 与 sha512crypt。后两个(sha256crypt 与 sha512crypt)是现代基于 SHA-2 的密码哈希。
iter_count 参数让用户为具有迭代次数的算法指定迭代次数。次数越高,哈希密码所需的时间越长,破解它所需的时间也就越长。不过,次数过高时计算哈希的时间可能长达数年 — 这有些不切实际。如果省略 iter_count 参数,则使用默认迭代次数。iter_count 的允许值取决于算法,见 Table F.19。
Table F.19. crypt() 的迭代次数
| 算法 | 默认 | 最小 | 最大 |
|---|---|---|---|
xdes | 725 | 1 | 16777215 |
bf | 6 | 4 | 31 |
sha256crypt, sha512crypt | 5000 | 1000 | 999999999 |
对 xdes 还有一个额外限制:迭代次数必须是奇数。
选择合适的迭代次数时,可以考虑:最初的 DES crypt 被设计为在当时的硬件上达到每秒 4 次哈希的速度。慢于每秒 4 次哈希可能会损害可用性;快于每秒 100 次哈希则可能太快。
Table F.20 概述了不同哈希算法的相对缓慢程度。该表显示尝试 8 字符密码的所有字符组合需要多长时间,假定密码只包含小写字母,或包含大小写字母与数字。在 crypt-bf 条目中,斜杠后的数字是 gen_salt 的 iter_count 参数。
sha256crypt 与 sha512crypt 的默认 iter_count(5000)对现代硬件而言被认为过低,但可以调整以生成更强的密码哈希。除此之外,sha256crypt 与 sha512crypt 这两个哈希都被认为是安全的。
Table F.20. 哈希算法速度
| 算法 | 哈希/秒 | 针对 [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(哈希) | 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 循环/秒。(结果中非常小的差异符合如下事实:pgcrypto 中的 crypt-bf 实现与 John the Ripper 中使用的实现相同。)
请注意,“尝试所有组合”并非现实场景。密码破解通常借助词典进行,词典既包含常规单词也包含它们的多种变体。因此,即使多少有点像单词的密码也可能比上述数字暗示的更快被破解,而一个 6 字符的非单词密码也许能逃过破解。也可能逃不过。
这里的函数实现 OpenPGP(RFC 4880)标准的加密部分。同时支持对称密钥与公钥加密。
加密的 PGP 消息由 2 个部分(即 包)组成:
包含会话密钥的包 — 或为对称密钥加密,或为公钥加密。
包含用会话密钥加密的数据的包。
使用对称密钥(即密码)加密时:
给定的密码使用 String2Key(S2K)算法做哈希。它与 crypt() 算法颇为相似 — 刻意缓慢并带随机盐 — 但它产生全长的二进制密钥。
如果请求了单独的会话密钥,会生成一个新的随机密钥。否则 S2K 密钥会被直接用作会话密钥。
如果 S2K 密钥被直接使用,那么只有 S2K 设置会被放入会话密钥包。否则会话密钥会用 S2K 密钥加密后放入会话密钥包。
使用公钥加密时:
生成新的随机会话密钥。
它用公钥加密后放入会话密钥包。
无论哪种情况,要加密的数据按如下方式处理:
可选的数据处理:压缩、转换为 UTF-8,和/或行结束符转换。
数据前面加上一块随机字节。这相当于使用随机 IV。
附加随机前缀与数据的 SHA-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 参数可以包含选项设置,如下所述。
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 消息。
不允许用 pgp_sym_decrypt 解密 bytea 数据,以避免输出无效字符数据。用 pgp_sym_decrypt_bytea 解密原本就是文本的数据则没有问题。
options 参数可以包含选项设置,如下所述。
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 参数可以包含选项设置,如下所述。
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 解密原本就是文本的数据则没有问题。
options 参数可以包含选项设置,如下所述。
pgp_key_id() #pgp_key_id(bytea) returns text
pgp_key_id 提取 PGP 公钥或私钥的密钥 ID。或者,如果给定了加密的消息,它给出加密该数据所用的密钥 ID。
它可以返回 2 个特殊的密钥 ID:
SYMKEY
该消息用对称密钥加密。
ANYKEY
该消息用公钥加密,但密钥 ID 已被移除。这意味着你需要把所有私钥都试一遍,看哪一个能解密它。pgcrypto 本身不会产生这样的消息。
注意,不同的密钥可能有相同的 ID。这很罕见,但属于正常事件。客户端应用应当逐个尝试解密,看哪个合适 — 类似处理 ANYKEY。
armor()、dearmor() #armor(data bytea [ , keys text[], values text[] ]) returns text dearmor(data text) returns bytea
这些函数把二进制数据包装/解包为 PGP ASCII-armor 格式,它基本上是带 CRC 与额外格式化的 Base64。
如果指定了 keys 与 values 数组,会在封装格式中为每个键/值对添加一个 armor 头部。两个数组都必须是一维的,且长度必须相同。键与值不能包含任何非 ASCII 字符。
pgp_armor_headers #pgp_armor_headers(data text, key out text, value out text) returns setof record
pgp_armor_headers() 从 data 中提取 armor 头部。返回值是带两列(键与值)的行集合。如果键或值包含任何非 ASCII 字符,它们会被视为 UTF-8。
选项的命名与 GnuPG 类似。选项的值应写在等号之后;选项之间用逗号分隔。例如:
pgp_sym_encrypt(data, psw, 'compress-algo=1, cipher-algo=aes256')
除 convert-crlf 外的所有选项都只适用于加密函数。解密函数从 PGP 数据中获取参数。
最值得关注的选项大概是 compress-algo 与 unicode-mode。其余选项都有合理的默认值。
使用哪种加密算法。
Values: bf, aes128, aes192, aes256, 3des, cast5
Default: aes128
Applies to: pgp_sym_encrypt, pgp_pub_encrypt
使用哪种压缩算法。只有 PostgreSQL 带 zlib 构建时才可用。
Values:
0 - no compression
1 - ZIP compression
2 - ZLIB compression (= ZIP plus meta-data and block CRCs)
Default: 0
Applies to: pgp_sym_encrypt, pgp_pub_encrypt
压缩程度。级别越高压缩越小但越慢。0 禁用压缩。
Values: 0, 1-9
Default: 6
Applies to: pgp_sym_encrypt, pgp_pub_encrypt
是否在加密时把 \n 转换为 \r\n,在解密时把 \r\n 转换为 \n。RFC 4880 规定文本数据应使用 \r\n 行结束符存储。要用它来获得完全符合 RFC 的行为。
Values: 0, 1
Default: 0
Applies to: pgp_sym_encrypt, pgp_pub_encrypt, pgp_sym_decrypt, pgp_pub_decrypt
不用 SHA-1 保护数据。使用该选项唯一说得通的理由是实现与古老的 PGP 产品的兼容性,这些产品早于 RFC 4880 加入 SHA-1 保护包。最近的 gnupg.org 与 pgp.com 软件都能很好地支持它。
Values: 0, 1
Default: 0
Applies to: pgp_sym_encrypt, pgp_pub_encrypt
使用单独的会话密钥。公钥加密总是使用单独的会话密钥;该选项针对对称密钥加密,后者默认直接使用 S2K 密钥。
Values: 0, 1
Default: 0
Applies to: pgp_sym_encrypt
使用哪种 S2K 算法。
Values:
0 - Without salt. Dangerous!
1 - With salt but with fixed iteration count.
3 - Variable iteration count.
Default: 3
Applies to: pgp_sym_encrypt
要使用的 S2K 算法的迭代次数。它必须是 1024 与 65011712 之间(含)的值。
Default: A random value between 65536 and 253952
Applies to: pgp_sym_encrypt, only with s2k-mode=3
在 S2K 计算中使用哪种摘要算法。
Values: md5, sha1
Default: sha1
Applies to: pgp_sym_encrypt
用哪种加密算法加密单独的会话密钥。
Values: bf, aes, aes128, aes192, aes256
Default: use cipher-algo
Applies to: pgp_sym_encrypt
是否把文本数据从数据库内部编码转换为 UTF-8 再转换回来。如果你的数据库已经是 UTF-8,不会执行转换,但消息会被标记为 UTF-8。没有该选项则不会被标记。
Values: 0, 1
Default: 0
Applies to: pgp_sym_encrypt, pgp_pub_encrypt
危险!该选项指示 pgcrypto 使用与 CVE-2026-14663 修复之前的历史行为一致的错误解密算法,即完全忽略所用 OpenSSL 加密器的失败。它只面向需要恢复在所用 OpenSSL 配置下 cipher-algo 不可用时创建的、被错误加密的消息的用户。启用 ignore-cipher-failure 后,此类错误消息并不需要正确的解密密钥,因此无法保证解密出的明文确实来自密钥持有者。
对比另一种情况:消息被正确加密,但产生它的加密器在当前 OpenSSL 配置下不可用。通过 pgcrypto 恢复这类明文需要让 OpenSSL 真正获得该加密器,例如启用相应的 provider。ignore-cipher-failure 对该场景既无必要也无帮助。如果用该选项对正确加密的消息进行“解密”碰巧通过了 PGP 完整性检查,那只是巧合,并不能让恢复出的明文变得可信。
Values: 0, 1
Default: 0
Applies to: pgp_sym_decrypt, pgp_pub_decrypt
生成新密钥:
gpg --gen-key
首选的密钥类型是 “DSA and 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、
The GNU
Privacy Handbook 以及
https://www.gnupg.org/ 上的其他文档。
不支持签名。这也意味着它不检查加密子密钥是否属于主密钥。
不支持把加密密钥作为主密钥。由于这种做法通常不被推荐,这应当不构成问题。
不支持多个子密钥。这看起来可能是个问题,因为这是常见做法。不过,你不应把常用的 GPG/PGP 密钥用于 pgcrypto,而应创建新的密钥,因为使用场景相当不同。
这些函数只对数据运行加密器;它们不具备 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 — Blowfish
aes — 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 被忽略。如果不是恰好等于块大小,会被截断或用零填充。在不带该参数的函数中,它默认为全零。
gen_random_bytes(count integer) returns bytea
返回 count 个密码学强度高的随机字节。一次最多可以提取 1024 字节,以避免耗尽随机数生成器池。
gen_random_uuid() returns uuid
返回版本 4(随机)UUID。(已过时,该函数内部调用同名的 核心函数。)
有一个配置参数控制 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 中设置,不过超级用户也可以在自己的会话中即时修改。
pgcrypto 根据 PostgreSQL 主 configure 脚本的发现结果自行配置。影响它的选项是 --with-zlib 与 --with-ssl=openssl。
带 zlib 编译时,PGP 加密函数能够在加密前压缩数据。
pgcrypto 需要 OpenSSL。否则它不会被构建或安装。
针对 OpenSSL 3.0.0 及更新版本编译时,必须在 openssl.cnf 配置文件中激活 legacy provider 才能使用 DES 或 Blowfish 这类旧加密器。
按 SQL 的标准做法,如果任何参数为 NULL,所有函数都返回 NULL。粗心使用时这可能带来安全风险。
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 |