Skip to content

F.28. pgcrypto — 加密函数#

F.28.1. 通用哈希函数
F.28.2. 密码哈希函数
F.28.3. PGP 加密函数
F.28.4. 原始加密函数
F.28.5. 随机数据函数
F.28.6. 备注
F.28.7. 作者

pgcrypto模块为PostgreSQL提供加密函数。

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

pgcrypto需要 OpenSSL,如果在构建 PostgreSQL 时未选择 OpenSSL 支持,则不会安装它。

F.28.1. 通用哈希函数#

F.28.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.28.1.2.hmac()#

hmac(data text, key text, type text) returns bytea
hmac(data bytea, key bytea, type text) returns bytea

使用密钥*keydata计算哈希 MAC。type*与digest()中的相同。

这类似于digest(),但只有知道密钥才能重新计算哈希。这可以防止有人更改数据并更改哈希以匹配的情况。

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

F.28.2. 密码哈希函数#

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

crypt()中的算法与通常的 MD5 或 SHA1 哈希算法在以下方面有所不同

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

  2. 它们使用一个称为 的随机值,以便拥有相同密码的用户将具有不同的加密密码。这也是防止算法逆转的另一道防线。

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

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

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

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

算法最大密码长度自适应?盐位输出长度说明
bf7212860基于 Blowfish,变体 2a
md5无限制4834基于 MD5 的 crypt
xdes82420扩展 DES
des81213原始 UNIX crypt

F.28.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.28.2.2.gen_salt()#

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

为在crypt()中使用生成新的随机盐字符串。盐字符串还告诉crypt()使用哪种算法。

*type*参数指定哈希算法。可接受的类型有:desxdesmd5bf

*iter_count参数允许用户指定迭代次数,适用于具有迭代次数的算法。次数越高,对密码进行哈希所需的时间就越长,因此破解密码所需的时间就越长。虽然次数过高,计算哈希所需的时间可能长达数年,这有些不切实际。如果省略iter_count参数,则使用默认迭代次数。iter_count*的允许值取决于算法,并在表 F.19中显示。

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

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

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

要选择合适的迭代次数,请考虑原始 DES crypt 设计为在当时的硬件上每秒进行 4 次哈希。每秒低于 4 次哈希可能会降低可用性。每秒超过 100 次哈希可能太快。

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

表 F.20. 哈希算法速度

算法哈希/秒对于 [a-z]对于 [A-Za-z0-9]相对于 md5 哈希 的持续时间
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(哈希)15008550422.5 分钟17 天1

注释

  • 使用的机器是英特尔移动酷睿 i3。

  • crypt-descrypt-md5 算法数字取自 John the Ripper v1.6.38 -test 输出。

  • md5 哈希 数字取自 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.28.3. PGP 加密函数#

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

加密的 PGP 消息包含 2 个部分或数据包

  • 包含会话密钥的数据包 — 对称密钥或公钥加密。

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

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

  1. 使用 String2Key (S2K) 算法对给定的密码进行哈希处理。这与 crypt() 算法非常相似 — 故意缓慢且带有随机盐 — 但它会产生一个全长的二进制密钥。

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

  3. 如果要直接使用 S2K 密钥,则只会将 S2K 设置放入会话密钥数据包中。否则,会话密钥将使用 S2K 密钥加密并放入会话密钥数据包中。

使用公钥加密时

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

  2. 使用公钥加密该密钥并将其放入会话密钥数据包中。

在任何情况下,要加密的数据都将按如下方式处理

  1. 可选数据操作:压缩、转换为 UTF-8 和/或转换行尾。

  2. 数据前缀为随机字节块。这相当于使用随机 IV。

  3. 附加随机前缀和数据的 SHA1 哈希值。

  4. 使用会话密钥加密所有这些内容并将其放入数据包中。

F.28.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.28.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.28.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.28.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.28.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.28.3.6.armor(),dearmor()#

armor(data bytea [ , keys text[], values text[] ]) returns text
dearmor(data text) returns bytea

这些函数将二进制数据包装/解包到 PGP ASCII-armor 格式中,该格式基本上是带有 CRC 和附加格式的 Base64。

如果指定了*keysvalues数组,则会为每个键/值对向装甲格式添加一个装甲头*。这两个数组都必须是单维的,并且它们必须具有相同的长度。键和值不能包含任何非 ASCII 字符。

F.28.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.28.3.8. PGP 函数的选项#

选项的命名类似于 GnuPG。选项的值应在等号后给出;用逗号将选项彼此分隔。例如

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

除了convert-crlf之外,所有选项都仅适用于加密函数。解密函数从 PGP 数据中获取参数。

最有趣的选项可能是compress-algounicode-mode。其余选项应具有合理的默认值。

F.28.3.8.1. cipher-algo#

要使用的密码算法。

值: bf、aes128、aes192、aes256、3des、cast5

默认: aes128

适用于: pgp_sym_encrypt、pgp_pub_encrypt

F.28.3.8.2. compress-algo#

使用哪种压缩算法。仅当PostgreSQL使用 zlib 构建时才可用。

0 - 不压缩

1 - ZIP 压缩

2 - ZLIB 压缩 (= ZIP 加元数据和块 CRC)

默认: 0

适用于: pgp_sym_encrypt、pgp_pub_encrypt

F.28.3.8.3. compress-level#

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

值: 0、1-9

默认: 6

适用于: pgp_sym_encrypt、pgp_pub_encrypt

F.28.3.8.4. convert-crlf#

加密时是否将\n转换为\r\n,解密时是否将\r\n转换为\n。RFC4880 规定应使用\r\n换行符存储文本数据。使用此选项可获得完全符合 RFC 的行为。

值: 0、1

默认: 0

适用于: pgp_sym_encrypt、pgp_pub_encrypt、pgp_sym_decrypt、pgp_pub_decrypt

F.28.3.8.5. disable-mdc#

不使用 SHA-1 保护数据。使用此选项的唯一充分理由是与旧版 PGP 产品兼容,这些产品早于RFC4880 中添加 SHA-1 保护数据包。

值: 0、1

默认: 0

适用于: pgp_sym_encrypt、pgp_pub_encrypt

F.28.3.8.6. sess-key#

使用单独的会话密钥。公钥加密始终使用单独的会话密钥;此选项用于对称密钥加密,默认情况下直接使用 S2K 密钥。

值: 0、1

默认: 0

适用于: pgp_sym_encrypt

F.28.3.8.7. s2k-mode#

使用哪种 S2K 算法。

0 - 无盐。 危险!

1 - 带盐但具有固定迭代次数。

3 - 可变迭代次数。

默认: 3

适用于: pgp_sym_encrypt

F.28.3.8.8. s2k-count#

要使用的 S2K 算法的迭代次数。它必须是介于 1024 和 65011712 之间的数值(含)。

默认: 65536 到 253952 之间的随机值

适用于: pgp_sym_encrypt,仅当 s2k-mode=3

F.28.3.8.9. s2k-digest-algo#

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

值: md5、sha1

默认: sha1

适用于: pgp_sym_encrypt

F.28.3.8.10. s2k-cipher-algo#

用于加密单独会话密钥的密码。

值: bf、aes、aes128、aes192、aes256

默认:使用 cipher-algo

适用于: pgp_sym_encrypt

F.28.3.8.11. unicode-mode#

是否将文本数据从数据库内部编码转换为 UTF-8,然后再转换回来。如果您的数据库已经是 UTF-8,则不会执行转换,但消息将标记为 UTF-8。如果不使用此选项,则不会标记为 UTF-8。

值: 0、1

默认: 0

适用于: pgp_sym_encrypt、pgp_pub_encrypt

F.28.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 隐私手册https://www.gnupg.org/上的其他文档。

F.28.3.10. PGP 代码的限制#

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

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

  • 不支持多个子密钥。这似乎是个问题,因为这是常见的做法。另一方面,您不应该将常规 GPG/PGP 密钥与 pgcrypto 一起使用,而是创建新的密钥,因为使用场景完全不同。

F.28.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.28.5. 随机数据函数#

gen_random_bytes(count integer) returns bytea

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

gen_random_uuid() returns uuid

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

F.28.6. 备注#

F.28.6.1. 配置#

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

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

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

针对OpenSSL3.0.0 及更高版本编译时,必须在openssl.cnf配置文件中激活旧版提供程序,才能使用 DES 或 Blowfish 等较旧的密码。

F.28.6.2. NULL 处理#

与 SQL 中的标准一样,如果任何参数为 NULL,则所有函数都返回 NULL。这可能会在使用不当时造成安全风险。

F.28.6.3. 安全限制#

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

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

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

如果您无法做到,那么最好在客户端应用程序内进行加密。

该实现无法抵御旁路攻击。例如,pgcrypto解密函数完成所需的时间因给定大小的密码文本而异。

F.28.6.4. 有用读物#

F.28.6.5. 技术参考#

F.28.7. 作者#

Marko Kreen<[[email protected]](/cdn-cgi/l/email-protection#f09d91829b9f9b82b0979d91999cde939f9d)>

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

算法作者源代码
DES cryptDavid Burren 等人FreeBSD libcrypt
MD5 cryptPoul-Henning KampFreeBSD libcrypt
Blowfish cryptSolar Designerwww.openwall.com