受支持版本: 当前版本 (18) / 17 / 16 / 15 / 14
开发版本: devel

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

使用密钥keydata计算基于散列的消息认证码(HMAC)。 typedigest()中的相同。

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

如果密钥大于哈希块大小,则会先对其进行哈希,并将结果用作密钥。

F.26.2. 密码哈希函数 #

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

crypt()中的算法在以下方面不同于通常的 MD5 或 SHA-1 哈希算法:

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

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

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

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

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

Table F.15. 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

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参数指定哈希算法。 接受的类型有:desxdesmd5bfsha256cryptsha512crypt。 最后两种即 sha256cryptsha512crypt, 是现代的、基于 SHA-2 的密码哈希。

iter_count参数允许用户为支持该参数的算法指定迭代次数。 次数越高,密码哈希所需时间越长,从而破解它所需时间也越长。 不过,如果次数过高,计算一个哈希可能需要数年时间 — 这显然不切实际。 若省略iter_count参数,则使用默认迭代次数。 允许的iter_count值取决于算法,如Table F.16所示。

Table F.16. crypt()的迭代计数

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

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

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

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

sha256cryptsha512crypt而言, 默认的iter_count5000 对现代硬件来说被认为过低, 但可以调高以生成更强的密码哈希。除此之外, sha256cryptsha512crypt都被认为是安全的。

Table F.17. 哈希算法速度

算法 每秒哈希次数 针对[a-z] 针对[A-Za-z0-9] 相对于md5 哈希的耗时倍数
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 哈希的数字来自 mdcrack 1.2。

  • sha1的数字来自 lcrack-20031130-beta。

  • crypt-bf的数字是使用一个简单程序测得的,该程序循环处理 1000 个 8 字符密码。这样可以展示不同迭代次数下的速度。作为参考: john -testcrypt-bf/5 给出的结果是 13506 次循环/秒。(结果上的极小差异与这样一个事实一致: crypt-bfpgcrypto中的实现与 John the Ripper 使用的是同一套实现。)

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

F.26.3. PGP 加密函数 #

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

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

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

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

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

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

bytea数据不能用pgp_sym_decrypt解密。 这是为了避免输出无效字符数据。若原始数据本来是文本, 则使用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中给出密码。 如果没有密码但想指定选项,则需要传入空密码。

bytea数据不能用pgp_pub_decrypt解密。 这是为了避免输出无效字符数据。若原始数据本来是文本, 则使用pgp_pub_decrypt_bytea解密也没有问题。

options参数可以包含下文所述的选项设置。

F.26.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.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 header)。 两个数组都必须是一维的,且长度相同。键和值都不能包含任何非 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中提取 装甲头。返回值是一个包含两列的行集合,列名为 key 和 value。 如果键或值包含任何非 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转换成\nRFC 4880 规定文本数据应使用\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 保护的数据包被纳入 RFC 4880 之前。 较新的 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 - 不使用盐值。危险!
  1 - 使用盐值,但迭代次数固定。
  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 gpgGNU 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_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 配置文件中启用 legacy provider(旧版提供程序)。

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

提交更正

如果您发现文档中有不正确的内容、与您使用特定功能的经验不符或需要进一步说明,请使用此表单来报告文档问题。