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

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 11 已结束支持。 2023-11-09. 请参阅 当前版本手册.

F.25. pgcrypto #

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

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.6.4. 有用的阅读材料

F.25.6.5. 技术参考资料

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

报告文档问题

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