PostgreSQL 中文参考文档
🌐 英文原版 官方 ↗

F.26. pgcrypto — 密码学函数 #

F.26.1. 通用哈希函数
F.26.2. 密码哈希函数
F.26.3. PGP 加密函数
F.26.4. 原始加密函数
F.26.5. 随机数据函数
F.26.6. OpenSSL 支持函数
F.26.7. 配置参数
F.26.8. 注记
F.26.9. 作者

pgcrypto 模块为 PostgreSQL 提供密码学函数。

该模块被视为“可信”模块,也就是说,拥有当前数据库 CREATE 权限的非超级用户也可以安装它。

pgcrypto 需要 OpenSSL,如果构建 PostgreSQL 时未选择 OpenSSL 支持则不会安装。

F.26.1. 通用哈希函数 #

F.26.1.1. 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;

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 的散列消息认证码。type 与 digest() 中的相同。

它与 digest() 类似,但只有知道密钥才能重新计算哈希。这可以防止有人篡改数据并同时修改哈希以使其匹配。

如果密钥大于哈希块大小,会先对它做哈希,然后把结果用作密钥。

F.26.2. 密码哈希函数 #

函数 crypt() 与 gen_salt() 专为哈希密码而设计。crypt() 执行哈希,gen_salt() 为其准备算法参数。

crypt() 中的算法与常规的 MD5 或 SHA-1 哈希算法在以下方面有所不同:

  1. 它们很慢。由于数据量很小,这是使暴力破解密码变得困难唯一的手段。

  2. 它们使用一个称为 盐 的随机值,因此拥有相同密码的用户会得到不同的加密密码。这也是对抗算法逆向的额外防线。

  3. 它们把算法类型包含在结果中,因此用不同算法哈希的密码可以共存。

  4. 其中一些是自适应的 — 也就是说,当计算机变快时,你可以把算法调得更慢,而不会引入与现有密码的不兼容。

Table F.18 列出了 crypt() 函数支持的算法。

Table F.18. crypt() 支持的算法

算法最大密码长度自适应?盐位数输出长度描述
bf72是12860基于 Blowfish,变体 2a
md5无限制否4834基于 MD5 的 crypt
xdes8是2420扩展 DES
des8否1213原始 UNIX crypt
sha256crypt无限制是最多 3280改编自公开可用的参考实现 使用 SHA-256 与 SHA-512 的 Unix crypt
sha512crypt无限制是最多 32123改编自公开可用的参考实现 使用 SHA-256 与 SHA-512 的 Unix crypt

F.26.2.1. 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。

F.26.2.2. 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() 的迭代次数

算法默认最小最大
xdes725116777215
bf6431
sha256crypt, sha512crypt50001000999999999

对 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/817924 年3927 年100k
crypt-bf/736482 年1929 年50k
crypt-bf/671681 年982 年25k
crypt-bf/513504188 天521 年12.5k
crypt-md517158415 天41 年1k
crypt-des23221568157.5 分钟108 天7
sha13777427290 分钟68 天4
md5(哈希)15008550422.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 字符的非单词密码也许能逃过破解。也可能逃不过。

F.26.3. PGP 加密函数 #

这里的函数实现 OpenPGP(RFC 4880)标准的加密部分。同时支持对称密钥与公钥加密。

加密的 PGP 消息由 2 个部分(即 包)组成:

  • 包含会话密钥的包 — 或为对称密钥加密,或为公钥加密。

  • 包含用会话密钥加密的数据的包。

使用对称密钥(即密码)加密时:

  1. 给定的密码使用 String2Key(S2K)算法做哈希。它与 crypt() 算法颇为相似 — 刻意缓慢并带随机盐 — 但它产生全长的二进制密钥。

  2. 如果请求了单独的会话密钥,会生成一个新的随机密钥。否则 S2K 密钥会被直接用作会话密钥。

  3. 如果 S2K 密钥被直接使用,那么只有 S2K 设置会被放入会话密钥包。否则会话密钥会用 S2K 密钥加密后放入会话密钥包。

使用公钥加密时:

  1. 生成新的随机会话密钥。

  2. 它用公钥加密后放入会话密钥包。

无论哪种情况,要加密的数据按如下方式处理:

  1. 可选的数据处理:压缩、转换为 UTF-8,和/或行结束符转换。

  2. 数据前面加上一块随机字节。这相当于使用随机 IV。

  3. 附加随机前缀与数据的 SHA-1 哈希。

  4. 所有这些用会话密钥加密后放入数据包。

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 消息。

不允许用 pgp_sym_decrypt 解密 bytea 数据,以避免输出无效字符数据。用 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 中给出密码。如果没有密码但想指定选项,需要给一个空密码。

不允许用 pgp_pub_decrypt 解密 bytea 数据,以避免输出无效字符数据。用 pgp_pub_decrypt_bytea 解密原本就是文本的数据则没有问题。

options 参数可以包含选项设置,如下所述。

F.26.3.5. pgp_key_id() #

pgp_key_id(bytea) returns text

pgp_key_id 提取 PGP 公钥或私钥的密钥 ID。或者,如果给定了加密的消息,它给出加密该数据所用的密钥 ID。

它可以返回 2 个特殊的密钥 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 头部。两个数组都必须是一维的,且长度必须相同。键与值不能包含任何非 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 中提取 armor 头部。返回值是带两列(键与值)的行集合。如果键或值包含任何非 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 #

使用哪种加密算法。


Values: bf, aes128, aes192, aes256, 3des, cast5
Default: aes128
Applies to: pgp_sym_encrypt, pgp_pub_encrypt

F.26.3.8.2. compress-algo #

使用哪种压缩算法。只有 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

F.26.3.8.3. compress-level #

压缩程度。级别越高压缩越小但越慢。0 禁用压缩。


Values: 0, 1-9
Default: 6
Applies to: pgp_sym_encrypt, pgp_pub_encrypt

F.26.3.8.4. convert-crlf #

是否在加密时把 \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

F.26.3.8.5. disable-mdc #

不用 SHA-1 保护数据。使用该选项唯一说得通的理由是实现与古老的 PGP 产品的兼容性,这些产品早于 RFC 4880 加入 SHA-1 保护包。最近的 gnupg.org 与 pgp.com 软件都能很好地支持它。


Values: 0, 1
Default: 0
Applies to: pgp_sym_encrypt, pgp_pub_encrypt

F.26.3.8.6. sess-key #

使用单独的会话密钥。公钥加密总是使用单独的会话密钥;该选项针对对称密钥加密,后者默认直接使用 S2K 密钥。


Values: 0, 1
Default: 0
Applies to: pgp_sym_encrypt

F.26.3.8.7. s2k-mode #

使用哪种 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

F.26.3.8.8. s2k-count #

要使用的 S2K 算法的迭代次数。它必须是 1024 与 65011712 之间(含)的值。


Default: A random value between 65536 and 253952
Applies to: pgp_sym_encrypt, only with s2k-mode=3

F.26.3.8.9. s2k-digest-algo #

在 S2K 计算中使用哪种摘要算法。


Values: md5, sha1
Default: sha1
Applies to: pgp_sym_encrypt

F.26.3.8.10. s2k-cipher-algo #

用哪种加密算法加密单独的会话密钥。


Values: bf, aes, aes128, aes192, aes256
Default: use cipher-algo
Applies to: pgp_sym_encrypt

F.26.3.8.11. unicode-mode #

是否把文本数据从数据库内部编码转换为 UTF-8 再转换回来。如果你的数据库已经是 UTF-8,不会执行转换,但消息会被标记为 UTF-8。没有该选项则不会被标记。


Values: 0, 1
Default: 0
Applies to: pgp_sym_encrypt, pgp_pub_encrypt

F.26.3.8.12. ignore-cipher-failure #

危险!该选项指示 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

F.26.3.9. 使用 GnuPG 生成 PGP 密钥 #

生成新密钥:

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/ 上的其他文档。

F.26.3.10. PGP 代码的限制 #

  • 不支持签名。这也意味着它不检查加密子密钥是否属于主密钥。

  • 不支持把加密密钥作为主密钥。由于这种做法通常不被推荐,这应当不构成问题。

  • 不支持多个子密钥。这看起来可能是个问题,因为这是常见做法。不过,你不应把常用的 GPG/PGP 密钥用于 pgcrypto,而应创建新的密钥,因为使用场景相当不同。

F.26.4. 原始加密函数 #

这些函数只对数据运行加密器;它们不具备 PGP 加密的任何高级特性。因此它们有一些重大问题:

  1. 它们把用户密钥直接用作加密密钥。

  2. 它们不提供任何完整性检查,无法发现加密数据是否被修改。

  3. 它们期望用户自己管理所有加密参数,甚至包括 IV。

  4. 它们不处理文本。

因此,随着 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 被忽略。如果不是恰好等于块大小,会被截断或用零填充。在不带该参数的函数中,它默认为全零。

F.26.5. 随机数据函数 #

gen_random_bytes(count integer) returns bytea

返回 count 个密码学强度高的随机字节。一次最多可以提取 1024 字节,以避免耗尽随机数生成器池。

gen_random_uuid() returns uuid

返回版本 4(随机)UUID。(已过时,该函数内部调用同名的 核心函数。)

F.26.6. OpenSSL 支持函数 #

fips_mode() returns boolean

如果 OpenSSL 以启用的 FIPS 模式运行则返回 true,否则返回 false。

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 及更新版本编译时,必须在 openssl.cnf 配置文件中激活 legacy provider 才能使用 DES 或 Blowfish 这类旧加密器。

F.26.8.2. NULL 处理 #

按 SQL 的标准做法,如果任何参数为 NULL,所有函数都返回 NULL。粗心使用时这可能带来安全风险。

F.26.8.3. 安全限制 #

所有 pgcrypto 函数都在数据库服务器内部运行。这意味着所有数据与密码都以明文在 pgcrypto 与客户端应用之间移动。因此你必须:

  1. 本地连接或使用 SSL 连接。

  2. 同时信任系统管理员与数据库管理员。

如果无法做到,最好在客户端应用内部执行加密。

该实现不抵御 侧信道攻击。例如,pgcrypto 解密函数完成所需的时间在给定大小的不同密文之间存在差异。

F.26.9. 作者 #

Marko Kreen

pgcrypto 使用了来自以下来源的代码:

算法作者来源
DES cryptDavid Burren 等FreeBSD libcrypt
MD5 cryptPoul-Henning KampFreeBSD libcrypt
Blowfish cryptSolar Designerwww.openwall.com