PostgreSQL pgcrypto
- F.25.1. 普通哈希函数
- F.25.2. 口令哈希函数
- F.25.3. PGP 加密函数
- F.25.4. 原始的加密函数
- F.25.5. 随机数据函数
- F.25.6. 注解
pgcrypto
模块为PostgreSQL提供了密码函数。
该模块被认为是“trusted”,也就是说,它可以由对当前数据库具有CREATE
权限的非超级用户安装。
F.25.1. 普通哈希函数
F.25.1.1. digest()
digest(data text, type text) returns bytea
digest(data bytea, type text) returns bytea
计算一个给定data
的一个二进制哈希值。type
是要使用的算法。标准算法是md5
、sha1
、sha224
、sha256
、
sha384
和sha512
。如果使用 OpenSSL 编译了pgcrypto
,如表 F.19中所述,有更多算法可用。
如果你想摘要成为一个十六进制字符串,可以在结果上使用encode()
。例如:
CREATE OR REPLACE FUNCTION sha1(bytea) returns text AS $$
SELECT encode(digest($1, 'sha1'), 'hex')
$$ LANGUAGE SQL STRICT IMMUTABLE;
F.25.2. 口令哈希函数
函数crypt()
和gen_salt()
是特别设计用来做口令哈希的。crypt()
完成哈希,而gen_salt()
负责为前者准备算法参数。
crypt()
中的算法在以下方面不同于通常的 MD5 或 SHA1 哈希算法:
-
它们很慢。由于数据量很小,这是增加蛮力口令破解难度的唯一方法。
-
它们使用一个随机值(称为salt),这样具有相同口令的用户将得到不同的密文口令。这也是针对逆转算法的一种额外保护。
-
它们会在结果中包括算法类型,这样用不同算法哈希的口令能共存。
-
其中一些是自适应的 — 这意味着当计算机变得更快时,你可以调整该算法变得更慢,而不会产生与现有口令的不兼容。
表 F.16列出了crypt()
函数所支持的算法。
表 F.16. crypt()
支持的算法
算法 | 最大口令长度 | 自适应? | Salt 位数 | 输出长度 | 描述 |
---|---|---|---|---|---|
bf
|
72 | yes | 128 | 60 | 基于 Blowfish,变体 2a |
md5
|
unlimited | no | 48 | 34 | 基于 MD5 的加密 |
xdes
|
8 | yes | 24 | 20 | 扩展的 DES |
des
|
8 | no | 12 | 13 | 原生 UNIX 加密 |
F.25.2.1. crypt()
crypt(password text, salt text) 返回 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.25.2.2. gen_salt()
gen_salt(type text [, iter_count integer ]) 返回 text
产生一个在crypt()
中使用的新随机 salt 字符串。该 salt 字符串也告诉了crypt()
要使用哪种算法。
type
参数指定哈希算法。可接受的类型是:des
、xdes
、md5
以及bf
。
iter_count
参数让用户可以为使用迭代计数的算法指定迭代计数。计数越高,哈希口令花的时间更长并且因而需要更多时间去攻破它。不过使用太高的计数会导致计算一个哈希的时间高达数年 — 这并不使用。如果iter_count
参数被忽略,将使用默认的迭代计数。允许的iter_count
值与算法相关,如
表 F.17所示。
表 F.17. crypt()
的迭代计数
算法 | 默认值 | 最小值 | 最大值 |
---|---|---|---|
xdes
|
725 | 1 | 16777215 |
bf
|
6 | 4 | 31 |
对xdes
算法还有额外的限制:迭代计数必须是一个奇数。
要选取一个合适的迭代计数,考虑最初的 DES 加密被设计成在当时的硬件上每秒钟完成 4 次哈希。低于每秒 4 次哈希的速度很可能会损害可用性。而超过每秒 100 次哈希又可能太快了。
表 F.18给出了不同哈希算法的相对慢度的综述。该表展示了在假设口令只含有小写字母或者大小写字母及数字的情况下,在一个 8 字符口令中尝试所有字符组合所需要的时间。在crypt-bf
项中,在一个斜线之后的数字是gen_salt
的
iter_count
参数
表 F.18. 哈希算法速度
算法 | 次哈希/秒 | 对于[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-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 次循环/秒(结果中的微小差异符合pgcrypto
中的crypt-bf
实现与 John the Ripper 中的一致这一情况)。
注意“尝试所有组合”并非是现实中会采用的方式。通常口令破解都是在词典的帮助下完成的,词典中会包含常用词以及它们的多种变化。因此,甚至有些像词的口令被破解的时间可能会大大小于上面建议的数字,而一个 6 字符的不像词的口令可能会逃过破解,也可能不能逃脱。
F.25.3. PGP 加密函数
这里的函数实现了OpenPGP (RFC 4880) 标准的加密部分。对称密钥和公钥加密都被支持。
一个加密的 PGP 消息由两个部分或者包组成:
-
包含一个会话密钥的包 — 加密过的对称密钥或者公钥。
-
包含用会话密钥加密过的数据的包。
当用一个对称密钥(即一个口令)加密时:
-
给定的口令被使用一个 String2Key (S2K) 算法哈希。这更像
crypt()
算法 — 有目的地慢并且使用随机 salt — 但是它会产生一个全长度的二进制密钥。 -
如果要求一个独立的会话密钥,将会生成一个新的随机密钥。否则该 S2K 密钥将被直接用作会话密钥。
-
如果直接使用 S2K 密钥,那么只有 S2K 设置将被放入会话密钥包中。否则会话密钥会用 S2K 密钥加密并且放入会话密钥包中。
当使用一个公共密钥加密时:
-
一个新的随机会话密钥会被生成。
-
它被用公共密钥加密并且放入到会话密钥包中。
在两种情况下,要被加密的数据按下列步骤被处理:
-
可选的数据操纵:压缩、转换成 UTF-8 或者行末转换。
-
数据会被加上一个随机字节的块作为前缀。这等效于使用一个随机 IV。
-
追加一个随机前缀和数据的 SHA1 哈希。
-
所有这些都用会话密钥加密并且放在数据包中。
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 ]) 返回 text
pgp_sym_decrypt_bytea(msg bytea, psw text [, options text ]) returns bytea
解密一个用对称密钥加密过的 PGP 消息。
不允许使用pgp_sym_decrypt
解密bytea
数据。这是为了避免输出非法的字符数据。使用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 ]]) 返回 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.25.3.5. pgp_key_id()
pgp_key_id(bytea) 返回 text
pgp_key_id
抽取一个 PGP 公钥或私钥的密钥 ID。或者如果给定了一个加密过的消息,它给出一个用来加密数据的密钥 ID。
它能够返回 2 个特殊密钥 ID:
-
SYMKEY
该消息是用一个对称密钥加密的。
-
ANYKEY
该消息是用公钥加密的,但是密钥 ID 已经被移除。这意味着你将需要尝试你所有的密钥来看看哪个能解密该消息。
pgcrypto
本身不产生这样的消息。
注意不同的密钥可能具有相同的 ID。这很少见但是是一种正常事件。客户端应用则应该尝试用每一个去解密,看看哪个合适 — 像处理ANYKEY
一样。
F.25.3.6. armor()
, dearmor()
armor(data bytea [ , keys text[], values text[] ]) 返回 text
dearmor(data text) returns bytea
这些函数把二进制数据包装/解包成 PGP ASCII-armored 格式,其基本上是带有 CRC 和额外格式化的 Base64。
如果指定了keys
和values
数组,每一个 键/值对的 armored 格式上会增加一个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
中抽取 armor header。返回值是一个有两列的行集合,包括键和值。如果键或值 包含任何非-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 保护数据。使用这个选项的唯一好的理由是实现与古董级别 PGP 产品的兼容,这些产品在受 SHA-1 保护的包被加入到 RFC 4880 之前就已经存在了。最近的 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 - 不用 salt。危险!
1 - 用 salt 但是使用固定的迭代计数。
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
默认:use 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-保护格式导出一个公钥:
gpg -a --export KEYID > public.key
要以 ASCII-保护格式导出一个私钥:
gpg -a --export-secret-keys KEYID > secret.key
在把这些密钥交给 PGP 函数之前,你需要对它们使用dearmor()
。或者如果你能处理二进制数据,你可以从命令中去掉-a
。
更多细节请参考man gpg
、 The GNU
Privacy Handbook以及 https://www.gnupg.org/上的其他文档。
F.25.3.10. PGP 代码的限制
-
不支持签名。这也意味着它不检查加密子密钥是否属于主控密钥。
-
不支持加密密钥作为主控密钥。由于通常并不鼓励那种用法,这应该不是问题。
-
不支持多个子密钥。这可能看起来像一个问题,因为在实践中普遍需要多个子密钥。在另一方面,你不能把你的常规 GPG/PGP 密钥用于
pgcrypto
,而是创建一些新的密钥,因为使用场景相当不同。
F.25.4. 原始的加密函数
这些函数只在数据上运行一次加密,它们不具有 PGP 加密的任何先进特性。因此它们有一些主要的问题:
-
它们直接把用户密钥用作加密密钥。
-
它们不提供任何完整性检查来查看被加密数据是否被修改。
-
它们希望用户自己管理所有加密参数,甚至是 IV。
-
它们无法处理文本。
因此,在介绍了 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() 返回 uuid
返回一个版本 4 的(随机的)UUID。(已过时,此函数现在也包含在核心PostgreSQL中。)
F.25.6. 注解
F.25.6.1. 配置
pgcrypto
会根据查找主 PostgreSQL configure
脚本配置它自身。影响它的选项是--with-zlib
以及--with-openssl
。
在编译了 zlib 时,PGP 加密函数能够在加密前压缩数据。
在编译了 OpenSSL 时,会有更多可用算法。公钥加密函数也会更快,因为 OpenSSL 有优化得更好的 BIGNUM 函数。
表 F.19. 使用和不用 OpenSSL 的功能总结
功能 | 内建 | 使用 OpenSSL |
---|---|---|
MD5 | yes | yes |
SHA1 | yes | yes |
SHA224/256/384/512 | yes | yes |
其他摘要算法 | no | yes (注意 1) |
Blowfish | yes | yes |
AES | yes | yes |
DES/3DES/CAST5 | no | yes |
原始加密 | yes | yes |
PGP 对称加密 | yes | yes |
PGP 公钥加密 | yes | yes |
注意:
-
OpenSSL 支持的任何摘要算法都是自动选取的。这对于使用密码来说是不可能的,因为需要被显式地支持。
F.25.6.2. NULL 处理
按照 SQL 中的标准,只要任何参数是 NULL, 所有的函数都会返回 NULL。在不当使用时这可能会导致安全风险。
F.25.6.3. 安全性限制
所有pgcrypto
函数都在数据库服务器内部运行。这意味着在pgcrypto
和客户端应用之间移动的所有数据和口令都是明文。因此,你必须:
-
本地连接或者使用 SSL 连接。
-
信任系统管理员和数据库管理员。
如果你不能这样做,那么最好在客户端应用中进行加密。
该实现无法抵抗
侧信道攻击。例如,一个pgcrypto
解密函 数完成所需的时间是随着密文尺寸变化的。
F.25.6.4. 有益的读物
-
https://www.gnupg.org/gph/en/manual.html
The GNU Privacy Handbook.
-
https://www.openwall.com/crypt/
描述 crypt-blowfish 算法。
-
https://www.iusmentis.com/security/passphrasefaq/
如何选取一个好的口令。
-
http://world.std.com/~reinhold/diceware.html
选择口令的有趣的想法。
-
http://www.interhack.net/people/cmcurtin/snake-oil-faq.html
描述好的和不好的加密。
F.25.6.5. 技术性参考
-
https://tools.ietf.org/html/rfc4880
OpenPGP 消息格式。
-
https://tools.ietf.org/html/rfc1321
MD5 消息摘要算法。
-
https://tools.ietf.org/html/rfc2104
HMAC:用于消息认证的钥控哈希。
-
https://www.usenix.org/legacy/events/usenix99/provos.html
crypt-des、crypt-md5 以及 bcrypt 算法的比较。
-
https://en.wikipedia.org/wiki/Fortuna_(PRNG)
Fortuna CSPRNG 的描述。
-
Linux 的 Jean-Luc Cooke Fortuna-based
/dev/random
驱动。