2025年9月25日: PostgreSQL 18 发布!
支持版本:当前 (18) / 17 / 16 / 15 / 14 / 13
开发版本:devel
不支持的版本:12 / 11 / 10 / 9.6 / 9.5 / 9.4 / 9.3 / 9.2 / 9.1 / 9.0 / 8.4 / 8.3

F.26. pgcrypto — 加密函数 #

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 是要使用的算法。标准算法有 md5sha1sha224sha256sha384sha512。此外,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 的哈希 MAC。 typedigest() 中的相同。

这与 digest() 类似,但如果不知道密钥,则无法重新计算哈希。这可以防止有人篡改数据并更改哈希以匹配的情况。

如果密钥大于哈希块大小,它将首先被哈希,然后结果将用作密钥。

F.26.2. 密码哈希函数 #

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

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

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

  2. 它们使用一个称为 salt 的随机值,以便具有相同密码的用户将具有不同的加密密码。这也是防止算法反转的额外安全措施。

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

  4. 其中一些是自适应的 — 这意味着当计算机速度变快时,你可以调整算法使其变慢,而不会与现有密码不兼容。

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

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

算法 最大密码长度 自适应? Salt 位数 输出长度 描述
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

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() 生成一个新的随机 salt 字符串。 salt 字符串还告诉 crypt() 使用哪种算法。

参数 type 指定哈希算法。可接受的类型有:desxdesmd5bfsha256cryptsha512crypt。后两种 sha256cryptsha512crypt 是基于 SHA-2 的现代密码哈希。

参数 iter_count 允许用户指定迭代次数(对于具有该参数的算法)。计数越高,哈希密码所需的时间就越长,因此破解密码所需的时间也越长。尽管计数过高可能需要数年时间才能计算出哈希值 — 这不太实用。如果省略 iter_count 参数,则使用默认的迭代次数。 iter_count 的允许值取决于算法,并在 表 F.19 中显示。

表 F.19. crypt() 的迭代次数

算法 默认 最小 最大
xdes 725 1 16777215
bf 6 4 31
sha256crypt, sha512crypt 5000 1000 999999999

对于 xdes,还有一个额外的限制,即迭代次数必须是奇数。

为了选择合适的迭代次数,请注意,原始 DES crypt 被设计成在当时硬件上每秒能进行 4 次哈希。每秒慢于 4 次哈希可能会影响可用性。每秒快于 100 次哈希可能太快了。

表 F.20 概述了不同哈希算法的相对速度。该表显示了尝试 8 个字符密码的所有字符组合所需的时间,假设密码只包含小写字母,或者包含大写字母、小写字母和数字。在 crypt-bf 条目中,斜杠后的数字是 gen_saltiter_count 参数。

sha256cryptsha512crypt 的默认 iter_count5000,这对于现代硬件来说太低了,但可以调整以生成更强的密码哈希。否则,这两种哈希 sha256cryptsha512crypt 都被认为是安全的。

表 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 (hash) 150085504 22.5 分钟 17 天 1

注释

  • 使用的机器是 Intel Mobile Core i3。

  • crypt-descrypt-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() 算法相当相似 — 故意做得慢并带有随机 salt — 但它会生成一个全长二进制密钥。

  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 加密 dataoptions 参数可以包含选项设置,如下所述。

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。

如果指定了 keysvalues 数组,则会在 armor 格式中为每个键/值对添加一个 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-algounicode-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 转换为 \nRFC4880 规定文本数据应使用 \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 保护的数据包之前。RFC4880。最近的 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 - 无 salt。 危险!
  1 - 有 salt 但迭代次数固定。
  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.9. s2k-digest-algo #

在 S2K 计算中要使用的摘要算法。


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

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.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 gpgThe GNU Privacy Handbook 以及 https://www.gnupg.org/ 上的其他文档。

F.26.3.10. PGP 代码的局限性 #

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

  • 不支持将加密密钥作为主密钥。由于这种做法通常不被推荐,所以这应该不是问题。

  • 不支持多个子密钥。这可能看起来像个问题,因为这是常见的做法。另一方面,您不应在 pgcrypto 中使用您的常规 GPG/PGP 密钥,而应创建新的密钥,因为使用场景大不相同。

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_ivdecrypt_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 模式运行时禁用这些函数。

在常规使用中,此参数在 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 配置文件中激活旧式提供程序。

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 crypt David Burren 和其他人 FreeBSD libcrypt
MD5 crypt Poul-Henning Kamp FreeBSD libcrypt
Blowfish crypt Solar Designer www.openwall.com

提交更正

如果您在文档中看到任何不正确、与您对特定功能的体验不符或需要进一步澄清的内容,请使用此表单报告文档问题。