2024年9月26日:PostgreSQL 17 发布!
支持的版本:当前 (17) / 16 / 15 / 14 / 13 / 12
开发版本:devel
不受支持的版本: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 或 SHA1 哈希算法不同

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

  2. 它们使用一个随机值(称为 salt),因此具有相同密码的用户将具有不同的加密密码。这也是防止反向算法的额外防御措施。

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

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

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

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

算法 最大密码长度 自适应? Salt 位数 输出长度 描述
bf 72 128 60 基于 Blowfish,变体 2a
md5 无限制 48 34 基于 MD5 的 crypt
xdes 8 24 20 扩展 DES
des 8 12 13 原始 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 参数指定哈希算法。可接受的类型为:desxdesmd5bf

iter_count 参数允许用户为具有迭代次数的算法指定迭代次数。次数越高,哈希密码所需的时间越长,因此破解密码所需的时间也越长。尽管迭代次数过高,计算哈希值的时间可能需要数年——这在某种程度上是不切实际的。如果省略 iter_count 参数,则使用默认迭代次数。iter_count 的允许值取决于算法,并在 表 F.18 中显示。

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

算法 默认值 最小值 最大值
xdes 725 1 16777215
bf 6 4 31

对于 xdes,还有一个限制,即迭代次数必须为奇数。

要选择合适的迭代次数,请考虑原始 DES crypt 旨在在当时硬件上的速度为每秒 4 次哈希。低于每秒 4 次哈希可能会降低可用性。高于每秒 100 次哈希可能太快了。

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

表 F.19. 哈希算法速度

算法 哈希/秒 对于 [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

注释

  • 使用的机器是英特尔酷睿 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. 追加随机前缀和数据的 SHA1 哈希值。

  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 装甲格式中,该格式基本上是带 CRC 和附加格式的 Base64。

如果指定了 keysvalues 数组,则会为每个键/值对向装甲格式添加一个 装甲头。这两个数组必须是一维的,并且必须具有相同的长度。键和值不能包含任何非 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 中提取装甲头。返回值是一组包含两列的行,键和值。如果键或值包含任何非 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 - 无盐。危险!
  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 装甲格式导出公钥

gpg -a --export KEYID > public.key

要以 ASCII 装甲格式导出密钥

gpg -a --export-secret-keys KEYID > secret.key

在将这些密钥提供给 PGP 函数之前,您需要在这些密钥上使用 dearmor()。或者,如果您能够处理二进制数据,则可以从命令中删除 -a

有关更多详细信息,请参阅 man gpgGNU Privacy 手册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 — 下一个块取决于上一个块(默认值)

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

并且 padding 是以下之一

  • pkcs — 数据可以是任何长度(默认值)

  • none — 数据必须是密码块大小的倍数

因此,例如,以下内容是等效的

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

encrypt_ivdecrypt_iv 中, iv 参数是 CBC 模式的初始值;它在 ECB 中被忽略。如果长度不完全等于块大小,则将其剪切或用零填充。在没有此参数的函数中,它默认为全零。

F.26.5. 随机数据函数 #

gen_random_bytes(count integer) returns bytea

返回 count 个加密强随机字节。一次最多可以提取 1024 个字节。这是为了避免耗尽随机数生成器池。

gen_random_uuid() returns uuid

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

F.26.6. 注释 #

F.26.6.1. 配置 #

pgcrypto 根据主 PostgreSQL configure 脚本的发现结果进行自身配置。影响它的选项是 --with-zlib--with-ssl=openssl

使用 zlib 编译时,PGP 加密函数能够在加密之前压缩数据。

pgcrypto 需要 OpenSSL。否则,它将不会构建或安装。

当针对 OpenSSL 3.0.0 及更高版本编译时,必须在 openssl.cnf 配置文件中激活旧版提供程序才能使用旧版密码,例如 DES 或 Blowfish。

F.26.6.2. NULL 处理 #

按照 SQL 中的标准做法,如果任何参数为 NULL,则所有函数都返回 NULL。这可能会在不小心使用时造成安全风险。

F.26.6.3. 安全限制 #

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

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

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

如果您无法做到,则最好在客户端应用程序中执行加密。

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

F.26.7. 作者 #

Marko Kreen

pgcrypto 使用以下来源的代码

算法 作者 源代码来源
DES 加密 David Burren 及其他人员 FreeBSD libcrypt
MD5 加密 Poul-Henning Kamp FreeBSD libcrypt
Blowfish 加密 Solar Designer www.openwall.com

提交更正

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