Pgcrypto
From PostgreSQL 中文维基, PostgreSQL 中文站, PostgreSQL 中国社区, PostgreSQL Chinese community
[编辑] pgcrypto
目录 |
pgcrypto 模块为 PostgreSQL 提供了加密功能。
[编辑] 通用的哈希函数
[编辑] digest()
digest(data text, type text) returns bytea digest(data bytea, type text) returns bytea
为给出的数据计算一个二进制哈希。type 是使用的算法。标准算法有 md5,sha1,sha224,sha256,sha384 和 sha512。如果带着 OpenSSL 制作 pgcrypto,那么有更多可用的算法,如表 F-21 所示。
如果你希望以十六进制字串获取信息摘要,那么在结果上使用 encode()。比如:
CREATE OR REPLACE FUNCTION sha1(bytea) returns text AS $$
SELECT encode(digest($1, 'sha1'), 'hex')
$$ LANGUAGE SQL STRICT IMMUTABLE;
[编辑] hmac()
hmac(data text, key text, type text) returns bytea hmac(data bytea, key text, type text) returns bytea
使用密钥 key 为数据计算哈希后的 MAC (信息认证码,Message Authencate Code,不是以太网的 MAC 地址)。类型和 digest() 里的一样。
这个类似于 digest(),但是哈希只有在知道密钥后才能重新计算。这样就避免了有人修改数据同时也修改了待匹配的哈希的情况。
如果密钥比哈希块大小更大,那么它首先会被哈希,然后再把结果用做密钥。
[编辑] 口令哈希函数
函数 crypt() 和 gen_salt() 是专门设计给哈希口令使用的。crypt() 进行哈希而 gen_salt() 为其准备算法参数。
crypt() 里的算法和类似 MD5 或者 SHA1 那样的常用哈希算法不同的地方有:
- 它们很慢。因为数据很少,所以这是唯一的让蛮力破解口令困难些的方法。
- 它们使用一个随机值,叫盐粒,这样口令相同的用户会有不同的加密口令。这也是防止逆向算法的额外的防护。
- 它们在结果里包含算法类型,因此用不同算法哈希的口令可以共存。
- 它们中有些是可适应的 -- 也就是说如果计算机变得更快了,你可以调节算法变得更慢,而不用引入和现有口令不兼容的东西。
| 算法 | 最大密码长度 | 是否可适应? | 盐粒位数 | 描述 |
| bf | 72 | 是 | 128 | 基于 Blowfish 的,变体 2a |
| md5 | 无限 | 否 | 48 | 基于 MD5 的加密 |
| xdes | 8 | 是 | 24 | 扩展的 DES |
| des | 8 | 否 | 12 | 最初的 UNIX 加密 |
[编辑] crypt()
crypt(password text, salt text) returns text
计算一个 crypt(3) 风格的密码哈希。在存储一个新的密码的时候,你需要使用 gen_salt() 来生成新的盐粒值。要检查一个密码,把已存储的哈希值当盐粒传入,然后检查结果是否和存储的值相同。
设置新密码的例子:
UPDATE ... SET pswhash = crypt('new password', gen_salt('md5'));
认证的例子:
SELECT pswhash = crypt('entered password', pswhash) FROM ... ;
如果输入的密码是正确的,这个语句返回真。
[编辑] gen_salt()
gen_salt(type text [, iter_count integer ]) returns text
为在 crypt() 中使用生成一个新的随机盐粒。盐粒字串也告诉 crypt() 应该使用哪种算法。
参数 type 声明哈希算法。接受的类型是:des,xdes,md5 和 bf。
iter_count 参数让用户声明循环次数,用于那些有循环次数的算法。次数越多,计算密码的哈希的时间越长,因而破解越要花时间。当然,如果次数抬高,那么计算一个哈希的时间可能需要好几年 --- 这就不太实际了。如果省略 iter_count 参数,则使用缺省的次数。iter_count 的可用值取决于算法:
| 算法 | 缺省 | 最小值 | 最大值 |
| xdes | 725 | 1 | 16777215 |
| bf | 6 | 4 | 31 |
对于 xdes,还有额外的限制是循环次数必须是奇数。
想选取一个合适的循环次数,就要知道最早的 DES 在设计的时候所拥有的硬件是每秒钟能做 4 个哈希。比每秒 4 哈希慢可能就会让使用者觉得不便。而比每秒 100 次哈希还快就可能是太快了。
下面是一个表,它给出了不同哈希算法的速度的一个概要。这个表显示了在一个 8 字符的口令里尝试所有组合所需要的时间,假设口令构成是:包含全部小写字母,或者大小写字母和数字。在 crypt-bf 项后面,些港后面的数字是 gen_salt 的 iter_count 参数。
| 算法 | 哈希/sec | 在 [a-z] 内 | 在 [A-Za-z0-9] 内 |
| crypt-bf/8 | 28 | 246 年 | 251322 年 |
| crypt-bf/7 | 57 | 121 年 | 123457 年 |
| crypt-bf/6 | 112 | 62 年 | 62831 年 |
| crypt-bf/5 | 211 | 33 年 | 33351 年 |
| crypt-md5 | 2681 | 2.6 年 | 2625 年 |
| crypt-des | 362837 | 7 天 | 19 年 |
| sha1 | 590223 | 4 天 | 12 年 |
| md5 | 2345086 | 1 天 | 3 年 |
注意:
- 这台机器使用一个 1.5 GHz 的 Pentium 4.
- crypt-des 和 crypt-md5 算法数是从 John the Ripper v1.6.38 测试输出获取的。
- md5 的数值是从 mdcrack 1.2 获取的。
- sha1 的数值来自 lcrack-20031130-beta。
- crypt-bf 的数值是从一个简单的在 1000 个 8 字节口令上循环的程序获取的。这样我可以显示不同循环次数的速度。引用 john -test 显示了 crypt-bf/5 是 213 循环/秒。(结果差别非常小是因为 pgcrypto 用的 crypt-bf 实现和 John the Ripper 用的实现是一样的。)
请注意“尝试所有组合”不是一个有实际意义的方法。通常口令破解是在字典的帮助下进行的,字典通常包含普通的词和这些词的变换。所以,某些类似单词的口令可能比上面的数值还要快地被破解,而一个 6 字符常的非单词样的口令则可能避免被破解,当然,也可能无法避免。
[编辑] PGP 加密函数
这里的函数实现了 OpenPGP (RFC 4880) 标准的加密部门。支持对称密钥和公开密钥加密。
一个加密后的 PGP 消息包含两个部分(或者说两个包):
- 包含会话密钥的包 --- 此会话密钥要么是对称密钥加密要么是公开密钥加密的。
- 包含用会话密钥加密后的包。
在使用对称密钥加密时(也就是用口令加密):
- 给出的密钥使用 String2Key(S2K)算法哈希过。这点和 crypt() 算法很像 --- 但它生成的是全长的二进制密钥。
- 如果要求一个独立的会话密钥,那么就会生成一个新的随机密钥。否则会直接拿 S2K 密钥做会话密钥。
- 如果直接使用 S2K 密钥,那么只有 S2K 设置会放进会话密钥包中。否则会话密钥会用 S2K 密钥加密然后放到会话密钥包中。
在使用公开密钥加密时:
- 生成一个新的随机会话密钥。
- 然后用公钥加密并放进会话密钥包中。
在两种情况下,要加密的数据都是按照下面过程处理的:
- 可选的数据操作:压缩,转换成 UTF-8,和/或转换行结束符。
- 用一块随机字节做数据的前缀。这么做等效于使用随机 IV。
- 附加一个随机前缀和数据的 SHA1 哈希。
- 所有这些都都是用会话密钥加密并放进数据包中。
[编辑] 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 加密数据。options 参数可以包括选项设置,入下文所述。
[编辑] 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 参数可以包含选项设置,入下所述。
[编辑] 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 加密数据。给这个函数传递一个私钥会产生一个错误。
options 参数可以包含选项设置,如下所述。
[编辑] 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 参数可以包含选项设置,如下所述。
[编辑] pgp_key_id()
pgp_key_id(bytea) returns text
pgp_key_id 抽取一个 PGP 公钥或者私钥的密钥 ID。或者在给出一个加密的信息时,它给出用于加密数据的密钥 ID。
它可以返回两种特定的密钥 ID:
- SYMKEY
- 消息是用一个对称的密钥加密的。
- ANYKEY
- 消息是公钥加密的,但是密钥 ID 已经删除了。这就意味着你需要尝试自己的所有私钥来看看是哪个加密的它。pgcrypto 自己并不生成这样的信息。
请注意不同的键可能有相同的 ID。这是一个罕见但又正常的情况。这时候客户端应用应该尝试用所有的密钥进行解密,看看哪个何时 --- 就像操作 ANYKEY 一样。
[编辑] armor(), dearmor()
armor(data bytea) returns text dearmor(data text) returns bytea
这些函数把二进制数据打包/解包成 PGP Ascii 铠甲格式,基本上就是 Base64 加上 CRC 和一些额外的格式。
[编辑] PGP 函数的选项
选项的命名是类似 GnuPG的。一个选项的值应该在等号标志之后给出;每个选项之间用逗号分隔:
pgp_sym_encrypt(data, psw, 'compress-algo=1, cipher-algo=aes256')
除了 convert-crlf 之外所有选项都只适用于加密函数。解密函数从 PGP 数据中获取参数。
最有趣的选项可能是 compress-algo 和 unicode-mode。剩下的都可以有合理的缺省。
[编辑] cipher-algo
使用哪个加密算法。
数值:bf,aes128,aes192,aes256 (只有 OpenSSL:3des,cast5) 缺省:aes128 适用于:pgp_sym_encrypt, pgp_pub_encrypt
[编辑] compress-algo
要使用哪个压缩算法。只有在 PostgreSQL 带着 zlib 制作的时候才可用。
值:
0 - 无压缩
1 - ZIP 压缩
2 - ZLIB 压缩 (= ZIP 加上元数据和 block CRC)
缺省:0
适用于:pgp_sym_encrypt, pgp_pub_encrypt
[编辑] compress-level
压缩程度如何。高级别的压缩压缩得更小,但是更慢。0 禁止压缩。
数值:0, 1-9 缺省:6 适用于:pgp_sym_encrypt, pgp_pub_encrypt
[编辑] 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
[编辑] disable-mdc
不要用 SHA-1 保护数据。使用这个选项的唯一的好理由是为了和古老的 PGP 产品 --- 在向 RFC 4880 中添加 SHA-1 保护包之前的产品兼容。最近的 gnupg.org 和 pgp.com 软件都支持得很好。
数值:0, 1 缺省:0 适用于:pgp_sym_encrypt, pgp_pub_encrypt
[编辑] enable-session-key
使用独立的会话密钥。公钥加密总是使用独立的会话密钥;这个选项是给对称密钥加密用的,缺省的时候是直接用 S2K 密钥。
数值:0, 1 缺省:0 适用于:pgp_sym_encrypt
[编辑] s2k-mode
使用哪个 S2K 算法。
数值:
0 - 无盐粒儿。危险!
1 - 有盐粒儿,但是循环次数固定。
3 - 变量循环次数。
缺省:3
适用于:pgp_sym_encrypt
[编辑] s2k-digest-algo
在 S2K 计算中使用哪种摘要算法。
数值:md5, sha1 缺省:sha1 适用于:pgp_sym_encrypt
[编辑] s2k-cipher-algo
加密独立的会话密钥时使用哪个加密算法。
数值:bf, aes, aes128, aes192, aes256 缺省:使用 cipher-algo 适用于:pgp_sym_encrypt
[编辑] unicode-mode
是否把来自数据库内部编码的文本信息在内部编码和 UTF-8 之间互转。如果你的数据库已经是 UTF-8,就不必做转换,但消息会标记为 UTF-8。没有这个选项它就不是 UTF-8。
数值:0, 1 缺省:0 适用于:pgp_sym_encrypt, pgp_pub_encrypt
[编辑] 用 GnuPG 生成 PGP 密钥
生成一个新密钥:
gpg --gen-key
优选的密钥是“DSA and 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。
更多细节请参考 gpg 的 man,GNU 隐私手册和其它在 http://www.gnupg.org 上的信息。
[编辑] PGP代码的限制
- 不支持签名。同时也就意味着不检查加密的子密钥是否属于主密钥。
- 不支持加密密钥做主密钥。因为通常并不鼓励这么做,所以这应该不是个问题。
- 不支持多个子密钥。这一点很可能是一个问题,因为这是常见的做法。从另外一个角度来看,你不应该在 pgcrypto 里用自己常用的 GPG/PGP 密钥,而是应该创建一个新的,因为使用的场景是差别相当大的。
[编辑] 裸加密函数
这些函数只是在数据上执行加密,并不具有任何 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 字串的语法是:
算法 [ - 模式 ] [ /pad: 填充物 ]
这里的算法是下列之一:
- bf — Blowfish
- aes — AES (Rijndael-128)
模式是下列之一:
- cbc — 下一个块依赖于前一块(缺省)
- ecb — 每个块都分别加密(只用于测试)
填充物是下列之一:
- pkcs — 数据可以是任意长度(缺省)
- none — 数据必须是加密块尺寸的倍数
所以,举例说,下面是相同的:
encrypt(data, 'fooz', 'bf') encrypt(data, 'fooz', 'bf-cbc/pad:pkcs')
对于 encrypt_iv 和 decrypt_iv 而言,iv 参数是 CBC 模式的初始值;在 ECB 里面被忽略。如果没有确切的块大小,那么就会截断或者用零填充。如果没有这个参数,函数里面缺省是全零。
[编辑] 随机数据函数
gen_random_bytes(count integer) returns bytea
返回 count 个强加密的随机字节。一次最多可以抽取 1024 字节。这样可以避免耗尽随机性发生器池中的数据。
[编辑] 注意
[编辑] 配置
pgcrypto 根据自己在 PostgreSQL 的主配置脚本中找到的东西配置自己。影响它的选项是 --with-zlib 和 --with-openssl。
在编译了 zlib 的时候,PGP 加密函数就可以在加密之前压缩数据。
在编译了 OpenSSL 的时候,将会有更多算法可用。而且公钥加密函数也会更快,因为 OpenSSL 的 BIGNUM 函数优化得更好。
| 功能 | 内置 | 有 OpenSSL |
| MD5 | 是 | 是 |
| SHA1 | 是 | 是 |
| SHA224/256/384/512 | 是 | 是(注 1) |
| 其它摘要算法 | 无 | 是(注 2) |
| Blowfish | 是 | 是 |
| AES | 是 | 是(注 3) |
| DES/3DES/CAST5 | 无 | 是 |
| 裸加密 | 是 | 是 |
| PGP 对称加密 | 是 | 是 |
| PGP 公钥加密 | 是 | 是 |
注:
- SHA2 在 OpenSSL 0.9.8 版本中添加。对于更老的版本,pgcrypto 将使用内置的代码。
- 会自动选取任何 OpenSSL 支持的摘要算法。这点对加密来说是不可能的,因为加密需要明确地支持。
- AES 是在 OpenSSL 0.9.7 里考试支持的。对于更老的版本,pgcrypto 会使用内置的代码。
[编辑] 处理 NULL
和 SQL 标准一样,如果任意参数是 NULL,那么所有函数都返回 NULL。在粗心地使用中,这可能会导致安全风险。
[编辑] 安全限制
所有 pgcrypto 函数都运行在数据库服务器内部。那就意味着所有在 pgcrypto 和客户端应用之间跑的数据都是明文。因此你必须:
- 本地连接或者使用 SSL 连接。
- 信任两边的系统和数据库管理员。
如果不能,那最好是在客户端应用内部进行加密。
[编辑] 有用的读物
- GNU 隐私手册。
- 描述 crypt-blowfish 算法。
- 如何选择一个好的口令。
- 选取密码的有趣观点。
- 描述好的和坏的加密方法。
[编辑] 技术引用
- OpenPGP 信息格式。
- MD5 信息摘要算法。
- HMAC: 用于信息认证的密钥哈希(Keyed-Hashing for Message Authentication)。
- crypt-des,crypt-md5和 bcrypt 算法的比较。
- DES,3DES 和 AES 的标准。
- Fortuna CSPRNG 的描述。
- Linux 的以 Jean-Luc Cooke Fortuna 为基础的 /dev/random 驱动。
- 一堆加密相关的连接。
[编辑] 作者
Marko Kreen <markokr@gmail.com>
pgcrypto 使用了下列源代码中的代码:
| 算法 | 作者 | 源初代码 |
| DES crypt | David Burren 及其他 | FreeBSD libcrypt |
| MD5 crypt | Poul-Henning Kamp | FreeBSD libcrypt |
| Blowfish crypt | Solar Designer | www.openwall.com |
| Blowfish cipher | Simon Tatham | PuTTY |
| Rijndael cipher | Brian Gladman | OpenBSD sys/crypto |
| MD5 和 SHA1 | WIDE 项目 | KAME kame/sys/crypto |
| SHA256/384/512 | Aaron D. Gifford | OpenBSD sys/crypto |
| BIGNUM 数学 | Michael J. Fromberger | dartmouth.edu/~sting/sw/imath |
