↑↓ 选择↵ 打开⌫ 切换范围完整搜索

PG.CENTER 连接 PostgreSQL 文档、百科与生态知识。由 Pigsty 维护。

支持中的版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发中的版本: 19 / 20devel
已结束支持的版本: 13 / 12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3
历史版本。 PostgreSQL 13 已结束支持。 2025-11-13. 请参阅 当前版本手册.

F.25. pgcrypto #

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

该模块被认为是“受信任的”,也就是说,它可以由在当前数据库上具有 CREATE 权限的非超级用户安装。

F.25.1. 通用 hash 函数

F.25.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。如果 pgcrypto 是在有 OpenSSL 的情况下构建的,则还有更多算法可用,详见表 F.19。

如果想把摘要表示为十六进制字符串,可以对结果使用 encode()。例如:

CREATE OR REPLACE FUNCTION sha1(bytea) returns text AS $$
    SELECT encode(digest($1, 'sha1'), 'hex')
$$ LANGUAGE SQL STRICT IMMUTABLE;

F.25.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.25.2. 密码 hash 函数

函数 crypt() 和 gen_salt() 是专门为对密码进行 hash 运算而设计的。crypt() 负责执行 hash 运算,而 gen_salt() 负责为其准备算法参数。

crypt() 中的算法在以下方面不同于通常的 MD5 或 SHA1 hash 算法:

  1. 它们很慢。由于处理的数据量很小,这是让暴力破解密码变得困难的唯一办法。

  2. 它们使用一个称为 salt 的随机盐值,这样使用相同密码的用户也会得到不同的 hash 结果。这也为逆向求解该算法增加了一层额外防护。

  3. 它们会在结果中包含算法类型,这样用不同算法进行 hash 运算的密码就能共存。

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

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

表 F.16. crypt() 支持的算法

算法最大密码长度是否自适应?盐值位数输出长度描述
bf72是12860基于 Blowfish,2a 变体
md5无限制否4834基于 MD5 的 crypt
xdes8是2420扩展 DES
des8否1213原始 UNIX crypt

F.25.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.25.2.2. gen_salt()

gen_salt(type text [, iter_count integer ]) returns text

生成一个供 crypt() 使用的新随机盐值字符串。该盐值字符串还会告诉 crypt() 应使用哪种算法。

type 参数指定 hash 算法。接受的类型有:des、xdes、md5 和 bf。

iter_count 参数允许用户为支持该参数的算法指定迭代次数。次数越高,对密码进行 hash 所需的时间越长,从而破解它所需时间也越长。不过,如果次数过高,计算一个 hash 可能需要数年时间 — 这就不太现实了。若省略 iter_count 参数,则使用默认迭代次数。允许的 iter_count 值取决于算法,如表 F.17 所示。

表 F.17. crypt() 的迭代计数

算法默认值最小值最大值
xdes725116777215
bf6431

对 xdes 算法还有额外的限制:迭代计数必须是一个奇数。

为了选择合适的迭代次数,可以考虑原始 DES crypt 在当时硬件上的设计速度是每秒 4 次 hash。低于每秒 4 次 hash 可能会影响可用性,而高于每秒 100 次 hash 则很可能过快。

表 F.18 概述了不同 hash 算法之间的相对速度差异。该表展示了在 8 字符密码上尝试所有字符组合所需的时间,假定密码只包含小写字母,或者包含大小写字母和数字。在 crypt-bf 条目中,斜杠后的数字是 gen_salt 的 iter_count 参数值。

表 F.18. Hash 算法速度

算法每秒 hash 次数针对[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(hash)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 次循环/秒。(结果上的极小差异与这样一个事实一致:crypt-bf 在 pgcrypto 中的实现与 John the Ripper 使用的是同一套实现。)

请注意,“尝试所有组合”并不是现实中的做法。通常密码破解是借助词典完成的,其中包含常见单词及其各种变体。因此,即使是稍微有点像单词的密码,被破解的速度也可能远快于上表所示;而一个 6 字符、不像单词的密码则可能逃过破解,也可能不会。

F.25.3. PGP 加密函数

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

一个加密的 PGP 消息由两个部分,或称两个包组成:

  • 包含会话密钥的包 — 该会话密钥要么由对称密钥加密,要么由公钥加密。

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

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

  1. 给定密码使用 String2Key (S2K) 算法进行 hash 运算。这与 crypt() 算法颇为类似 — 故意设计得较慢,并带有随机盐值 — 但它产生的是一个全长度的二进制密钥。

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

  3. 如果直接使用 S2K 密钥,那么会话密钥包中只写入 S2K 设置。否则,会话密钥会先用 S2K 密钥加密,再放入会话密钥包。

当使用公钥加密时:

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

  2. 该密钥会用公钥加密,并放入会话密钥包中。

无论哪种情况,要加密的数据都会按如下步骤处理:

  1. 可选的数据处理包括:压缩、转换为 UTF-8 和转换行结束符,三者可任意组合。

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

  3. 追加对随机前缀和数据计算得到的 SHA-1 hash 值。

  4. 然后用会话密钥加密所有这些内容,并放入数据包。

F.25.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.25.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.25.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.25.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.25.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.25.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.25.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.25.3.8. PGP 函数的选项

这些选项的命名方式与 GnuPG 类似。选项值应写在等号后面;各选项之间用逗号分隔。例如:

pgp_sym_encrypt(data, psw, 'compress-algo=1, cipher-algo=aes256')

除 convert-crlf 外,所有选项都只适用于加密函数。解密函数会从 PGP 数据中获取这些参数。

最值得关注的选项可能是 compress-algo 和 unicode-mode。其余选项应该都具有合理的默认值。

F.25.3.8.1. cipher-algo

使用哪种密码算法。


取值:bf, aes128, aes192, aes256(仅限 OpenSSL:3des、cast5)
默认值:aes128
适用于:pgp_sym_encrypt, pgp_pub_encrypt

F.25.3.8.2. compress-algo

使用哪种压缩算法。仅当 PostgreSQL 在编译时包含 zlib 时才可用。


取值:
  0 - 无压缩
  1 - ZIP 压缩
  2 - ZLIB 压缩(= ZIP 加元数据和块 CRC)
默认值:0
适用于:pgp_sym_encrypt, pgp_pub_encrypt

F.25.3.8.3. compress-level

压缩程度。级别越高,压缩后越小,但速度也越慢。0 表示禁用压缩。


取值:0, 1-9
默认值:6
适用于:pgp_sym_encrypt, pgp_pub_encrypt

F.25.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.25.3.8.5. disable-mdc

不使用 SHA-1 保护数据。使用此选项唯一合理的理由是兼容早于 RFC 4880 加入 SHA-1 保护包的古老 PGP 产品。较新的 gnupg.org 和 pgp.com 软件都能很好地支持它。


取值:0, 1
默认值:0
适用于:pgp_sym_encrypt, pgp_pub_encrypt

F.25.3.8.6. sess-key

使用单独的会话密钥。公钥加密总是使用单独的会话密钥;这个选项用于对称密钥加密,因为后者默认直接使用 S2K 密钥。


取值:0, 1
默认值:0
适用于:pgp_sym_encrypt

F.25.3.8.7. s2k-mode

使用哪一种 S2K 算法。


取值:
  0 - 不使用盐值。危险!
  1 - 使用盐值,但迭代次数固定。
  3 - 迭代次数可变。
默认值:3
适用于:pgp_sym_encrypt

F.25.3.8.8. s2k-count

S2K 算法要使用的迭代次数。它必须是一个位于 1024 和 65011712 之间的值,首尾两个值包括在内。


默认值:65536 到 253952 之间的随机值
适用于:pgp_sym_encrypt,仅在 s2k-mode=3 时

F.25.3.8.9. s2k-digest-algo

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


取值:md5, sha1
默认值:sha1
适用于:pgp_sym_encrypt

F.25.3.8.10. s2k-cipher-algo

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


取值:bf, aes, aes128, aes192, aes256
默认值:使用 cipher-algo
适用于:pgp_sym_encrypt

F.25.3.8.11. unicode-mode

是否将文本数据在数据库内部编码和 UTF-8 之间相互转换。如果数据库已经是 UTF-8,则不会发生转换,但消息会被标记为 UTF-8。如果不启用该选项,则不会进行这种标记。


取值:0, 1
默认值:0
适用于:pgp_sym_encrypt, pgp_pub_encrypt

F.25.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.25.3.10. PGP 代码的限制

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

  • 不支持使用加密密钥作为主密钥。由于通常不鼓励这种做法,这不应成为问题。

  • 不支持多个子密钥。这看起来可能是个问题,因为这在实践中相当常见。另一方面,你不应将常规的 GPG/PGP 密钥用于 pgcrypto,而应新建一套密钥,因为其使用场景相当不同。

F.25.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 — 下一个块依赖于前一个块(默认)

  • ecb — 每个块单独加密(仅用于测试)

而 padding 可以是:

  • pkcs — 数据长度可以任意(默认)

  • none — 数据必须是分组大小的倍数

因此,例如下面两种写法是等价的:

encrypt(data, 'fooz', 'bf')
encrypt(data, 'fooz', 'bf-cbc/pad:pkcs')

在 encrypt_iv 和 decrypt_iv 中,iv 参数是 CBC 模式的初始值;对于 ECB,它会被忽略。如果长度不等于块大小,则会被截断或用零填充。在不带该参数的函数中,它默认为全零。

F.25.5. 随机数据函数

gen_random_bytes(count integer) returns bytea

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

gen_random_uuid() returns uuid

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

F.25.6. 注意事项

F.25.6.1. 配置

pgcrypto 会根据 PostgreSQL 主 configure 脚本的探测结果自行配置。影响它的选项有 --with-zlib 和 --with-openssl。

如果编译时包含 zlib,PGP 加密函数就能够在加密前压缩数据。

如果编译时包含 OpenSSL,则会有更多算法可用。同时公钥加密函数也会更快,因为 OpenSSL 具有更加优化的 BIGNUM 函数。

表 F.19. 有无 OpenSSL 时的功能概要

功能内置包含 OpenSSL 时
MD5是是
SHA1是是
SHA224/256/384/512是是
其他摘要算法否是(注 1)
Blowfish是是
AES是是
DES/3DES/CAST5否是
原始加密是是
PGP 对称加密是是
PGP 公钥加密是是

当针对 OpenSSL 3.0.0 及更高版本编译时,若要使用 DES 或 Blowfish 等旧式密码算法,必须在 openssl.cnf 配置文件中启用 legacy provider(旧版提供程序)。

注意:

  1. OpenSSL 支持的任何摘要算法都会被自动识别并纳入支持。但密码算法不能这样做,需要显式支持。

F.25.6.2. NULL 处理

按照 SQL 的标准,只要任一参数为 NULL,所有函数都返回 NULL。这在使用不慎时可能带来安全风险。

F.25.6.3. 安全性限制

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

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

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

如果做不到,最好在客户端应用内部执行密码学操作。

该实现无法抵御侧信道攻击。例如,对于给定大小的密文,pgcrypto 解密函数完成所需的时间会因具体密文不同而变化。

F.25.7. 作者

Marko Kreen

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

算法作者源代码来源
DES cryptDavid Burren 及其他人FreeBSD libcrypt
MD5 cryptPoul-Henning KampFreeBSD libcrypt
Blowfish cryptSolar Designerwww.openwall.com
Blowfish 密码算法Simon TathamPuTTY
Rijndael 密码算法Brian GladmanOpenBSD sys/crypto
MD5 hash 和 SHA1WIDE ProjectKAME kame/sys/crypto
SHA256/384/512Aaron D. GiffordOpenBSD sys/crypto
BIGNUM 运算Michael J. Frombergerdartmouth.edu/~sting/sw/imath

报告文档问题

阅读 上游文档. 反馈更正前请先核对 当前版本手册.