跳转至

pgcrypto扩展(加密解密支持)

pgcrypto

pgcrypto 扩展模块用于 SeaboxSQL/SeaboxMPP中实现加密和解密功能。pgcrypto 属于“可信”模块;只要用户拥有当前数据库上的 CREATE 权限就可以安装该模块,不再需要超级用户权限。

pgcrypto 提供了两类加密算法:单向加密和双向加密。

单向加密属于不可逆加密,无法根据密文解密出明文,适用于数据的验证,例如登录密码验证。常用的单向加密算法有 MD5、SHA、HMAC 等。双向加密属于可逆加密,根据密文和密钥可解密出明文,适用于数据的安全传输,例如电子支付、数字签名等。常用的双向加密算法有 AES、DES、RSA、ECC 等。

单向加密—通用哈希函数

digest()
语法
sql digest(data text, type text) returns bytea digest(data bytea, type text) returns bytea
返回值
bytea
描述
digest()函数可以根据不同的算法生成数据的二进制哈希值. 上述语法中,data 是原始数据;type 是加密算法,包括 md5、sha1、sha224、sha256、sha384 以及 sha512;函数的返回结果为二进制字符串。
示例

假如存在以下用户表:

seaboxsql=# CREATE TABLE users (
  id SERIAL PRIMARY KEY,
  username varchar(20) NOT NULL,
  password text NOT NULL
);
CREATE TABLE

创建新用户时,可以使用 digest() 函数对密码进行加密存储:

seaboxsql=# INSERT INTO users(username, password) VALUES ('tony', encode(digest('123456','md5'), 'hex'));
INSERT 0 1
seaboxsql=# INSERT INTO users(username, password) VALUES ('anne', encode(digest('123456','md5'), 'hex'));
INSERT 0 1
seaboxsql=# INSERT INTO users(username, password) VALUES ('tony', encode(digest('123456','md5'), 'hex'));
INSERT 0 1
seaboxsql=# INSERT INTO users(username, password) VALUES ('anne', encode(digest('123456','md5'), 'hex'));
INSERT 0 1
seaboxsql=# INSERT INTO users(username, password) VALUES ('tony', encode(digest('123456','md5'), 'hex'));
INSERT 0 1
seaboxsql=# INSERT INTO users(username, password) VALUES ('anne', encode(digest('123456','md5'), 'hex'));
INSERT 0 1
seaboxsql=#
seaboxsql=# SELECT * FROM users;
 id | username |             password
----+----------+----------------------------------
  3 | tony     | e10adc3949ba59abbe56e057f20f883e
  4 | anne     | e10adc3949ba59abbe56e057f20f883e
  1 | tony     | e10adc3949ba59abbe56e057f20f883e
  2 | anne     | e10adc3949ba59abbe56e057f20f883e
  5 | tony     | e10adc3949ba59abbe56e057f20f883e
  6 | anne     | e10adc3949ba59abbe56e057f20f883e
(6 rows)

其中,encode 函数用于将二进制字符串转换为十六进制的文本。

当用户登录时,使用同样的方法加密输入的密码参数:

seaboxsql=#     -- 输入正确密码时
seaboxsql=# SELECT id FROM users WHERE username = 'tony' AND password = encode(digest('123456','md5'), 'hex');
 id
----
  3
  5
  1
(3 rows)

seaboxsql=# -- 输入错误密码时
seaboxsql=# SELECT id FROM users WHERE username = 'tony' AND password = encode(digest('abc123','md5'), 'hex');
 id
----
(0 rows)

这类加密算法的主要问题是相同的数据经过加密之后的结果相同。因此。在实际应用中可以将用户名和密码字符串连接之后再进行加密。

hmac()
语法
sql hmac(data text, key text, type text) returns bytea hmac(data bytea, key bytea, type text) returns bytea
返回值
bytea
描述
hmac()函数可以根据不同的算法生成数据的二进制哈希值. 上述语法中,data 是原始数据;key 是加密密钥;type 是加密算法,包括 md5、sha1、sha224、sha256、sha384 以及 sha512;函数的返回结果为二进制字符串。
示例

以下语句使用 hmac() 函数重新设置了用户的密码:

seaboxsql=# UPDATE users
seaboxsql-#    SET password = encode(hmac('123456', username, 'md5'), 'hex');
UPDATE 6
seaboxsql=# select * from users;
 id | username |             password
----+----------+----------------------------------
  2 | anne     | 9079d683b5fc5033427c2af2b6de4d01
  1 | tony     | 7a86cd4a12d7a54d65a4fe5854aaf41f
  3 | tony     | 7a86cd4a12d7a54d65a4fe5854aaf41f
  4 | anne     | 9079d683b5fc5033427c2af2b6de4d01
  5 | tony     | 7a86cd4a12d7a54d65a4fe5854aaf41f
  6 | anne     | 9079d683b5fc5033427c2af2b6de4d01
(6 rows)

其中,encode 函数用于将二进制字符串转换为十六进制的文本。该示例中,使用 username 作为密钥,相同的密码加密之后的结果不同。

对于 digest() 函数,如果同时被修改了原始数据和加密结果,无法进行识别;hmac() 函数只要密钥没有泄露的话,可以发现被篡改的数据。

单向加密—密码哈希函数

crypt()gen_salt() 函数专用于密码加密,其中 crypt()用于加密数据,gen_salt() 用于生成 salt(加盐)。

crypt()
语法
sql crypt(password text, salt text) returns text
返回值
text
描述

该函数返回 password 字符串 crypt(3) 格式的哈希值,salt 参数由 gen_salt() 函数生成。

crypt() 中的算法和普通的 MD5 或者 SHA1 哈希算法存在以下不同之处:

  1. crypt() 中的算法它们更慢。由于密码包含的数据量很小,这是增加暴力破解难度的唯一方法

  2. 它们使用了一个随机值(称为盐值),因此密码的用户加密后的密码不同。这也可以针对破解算法提供一种额外的安全保护。

  3. 它们的结果中包括了算法类型,因此可以针对不同用户使用不同的算法对密码进行加密。

  4. 其中一些算法具有自适应性,意味着当计算机性能变得更快时,可以调整算法使其变得更慢,而不会产生与已有密码的不兼容性。

下表列出了 crypt() 函数支持的算法:

算法 密码最大长度 自适应性 盐值比特位数 输出结果长度 描述
bf 72 YES 128 60 基于 Blowfish 的 2a 变种算法
md5 无限 NO 48 34 基于 MD5 的加密算法
xdes 8 YES 24 20 扩展 DES
des 8 NO 12 13 原始 UNIX 加密算法
示例

以下语句使用 crypt() 函数重新设置了用户的密码:

seaboxsql=# UPDATE users
seaboxsql-#    SET password = crypt('123456', gen_salt('md5'));
UPDATE 6
seaboxsql=# select * from users;
 id | username |              password
----+----------+------------------------------------
  3 | tony     | $1$HpD13yT7$1MMQP7BK1oErbRHrovUBB1
  4 | anne     | $1$AntKXnNp$eMXf9Txryu9jhP/mTwxsY/
  5 | tony     | $1$1lRLUeKb$hC9JN9JN56oXsT3SsTGFd1
  6 | anne     | $1$/Pt7jFRw$3i81/byMiq0RbxQIDAjqY0
  2 | anne     | $1$VB3DSija$s53Jy1JTKwjvcszHqu5mm.
  1 | tony     | $1$MtT1/gF5$GucU5TbvtUOBxw2/HsNTS.
(6 rows)

对于相同的密码,crypt() 函数每次也会返回不同的结果,因为 gen_salt() 函数每次都会生成不同的 salt。校验密码时可以将之前生成的哈希结果作为 salt:

seaboxsql=# SELECT id
seaboxsql-# FROM users
seaboxsql-# WHERE username = 'tony'
seaboxsql-# AND password = crypt('123456', password);
 id
----
  3
  1
  5
(3 rows)
gen_salt()
语法
sql gen_salt(type text [, iter_count integer ]) returns text
返回值
text
描述

gen_salt() 函数用于生成盐值 salt。

该函数每次都会生成一个随机的盐值字符串,该字符串同时决定了 crypt() 函数使用的算法;type 参数用于指定一个生成字符串的哈希算法,可能的取值包括 des、xdes、md5 以及 bf。

对于 xdes 和 bf 算法,iter_count 参数用于指定迭代的次数。迭代次数越多,计算的时间越长,破解所需的时间也越长。过高的迭代次数可能使得计算一个哈希值需要几年的时间,但是这并没有什么实际用途。如果忽略 iter_count,将会使用默认的迭代次数。

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

对于 xdes 算法,迭代次数必须是一个奇数。

如果想要选择一个合适的迭代次数,可以参考原始 DES 加密算法设计时的性能是在当时的硬件上每秒执行 4 次加密。每秒少于 4 次加密可能会降低可用性,每秒多于 100 次加密又可能太快了。

下表给出了不同哈希算法的相对性能比较。表中还列出了它们遍历所有由 8 字符组成的密码所需的时间,密码只包含小写字母、或者大小写字母及数字。 对于 crypt-bf 算法,斜杠后面的数字代表了 gen_salt() 函数中的 iter_count 参数。

算法 哈希次数/秒 [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-des 和 crypt-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 相同)。

示例

以下是使用 gen_salt() 函数不同算法的输出对比:

seaboxsql=# SELECT gen_salt('des'), gen_salt('xdes'), gen_salt('md5'), gen_salt('bf');
 gen_salt | gen_salt  |  gen_salt   |           gen_salt
----------+-----------+-------------+-------------------------------
 aK       | _J9..iz8C | $1$bHg.9MOF | $2a$06$ZZUVLgMTsARiL4bEza/GdO
(1 row)

每种算法生成的 salt 拥有固定的格式,例如 bf 算法结果中的 2a06$,2a 表示 Blowfish 的 2a 变种算法,06 表示迭代的次数。

双向加密—PGP 加密函数

PGP 加密函数实现了 OpenPGP(RFC 4880)标准中的加密功能,包括对称密钥加密(私钥加密)和非对称密钥加密(公钥加密)。

一个加密后的 PGP 消息由 2 部分(包)组成:

  • 一个包含会话密钥(加密后的对称密钥或者公钥)的包;

  • 一个使用会话密钥对数据加密后的包。

对于对称密钥(也就是口令)加密:

  1. 使用 String2Key(S2K)算法对密钥进行加密,类似于执行一个特意减慢并且包含随机 salt 的 crypt() 算法,生成一个完整长度的二进制密钥。

  2. 如果要求使用一个单独的会话密钥,生成一个随机的密钥;否则,使用上面的 S2K 密钥直接作为会话密钥。

  3. 如果直接使用 S2K 密钥,只将 S2K 设置加入会话密钥包中;否则,使用 S2K 密钥对会话密钥进行加密,然后放入会话密码包中。

对于公钥加密:

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

  2. 使用公钥对其进行加密后放入会话密钥包中。

无论哪种情况,对于数据的加密过程如下:

  1. 执行可选的数据操作:压缩、转换为 UTF-8 以及/或者换行符的转换。

  2. 在数据前面增加一个随机字节组成的块,相当于使用了一个随机的初始值(IV)。

  3. 计算随机前缀和数据的 SHA1 哈希值,追加到数据的后面。

  4. 将所有内容使用会话密钥进行加密后放入数据包中。

pgp_sym_encrypt()
语法
sql pgp_sym_encrypt(data text, psw text [, options text ]) returns bytea pgp_sym_encrypt_bytea(data bytea, psw text [, options text ]) returns bytea
返回值
bytea
描述

pgp_sym_encrypt() 函数用于对称密钥加密。

其中,data 是要加密的数据;psw 是 PGP 对称密钥;options 参数用于设置选项,参考下文。

pgp_sym_decrypt()
语法
sql pgp_sym_decrypt(msg bytea, psw text [, options text ]) returns text pgp_sym_decrypt_bytea(msg bytea, psw text [, options text ]) returns bytea
返回值
bytea
描述

pgp_sym_decrypt() 函数用于解密 PGP 对称密钥加密后的消息。

其中,msg 是要解密的消息;psw 是 PGP 对称密钥;options 参数用于设置选项,参考下文。为了避免输出无效的字符,不允许使用 pgp_sym_decrypt 函数对 bytea 数据进行解密;可以使用 pgp_sym_decrypt_bytea 对原始文本数据进行解密。

pgp_pub_encrypt()
语法
sql pgp_pub_encrypt(data text, key bytea [, options text ]) returns bytea pgp_pub_encrypt_bytea(data bytea, key bytea [, options text ]) returns bytea
返回值
bytea
描述

pgp_pub_encrypt() 函数用于公共密钥加密。

其中,data 是要加密的数据;key 是 PGP 公钥,如果传入一个私钥将会返回错误;options 参数用于设置选项,参考下文。

pgp_pub_decrypt()
语法
sql 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
返回值
text|bytea
描述

pgp_pub_decrypt() 函数用于解密 PGP 公共密钥加密后的消息.

其中,key 是公共密钥对应的私钥;如果私钥使用了密码保护功能,必须在 psw 参数中指定密码;如果没有使用密码保护,想要指定 options 参数时必须指定一个空的 psw。options 参数用于设置选项,参考下文。为了避免输出无效的字符,不允许使用 pgp_pub_decrypt 函数对 bytea 数据进行解密;可以使用 pgp_pub_decrypt_bytea 对原始文本数据进行解密。

pgp_key_id()
语法
sql pgp_key_id(bytea) returns text
返回值
text
描述

pgp_key_id() 函数用于提取 PGP 公钥或者私钥的密钥 ID;如果传入一个加密后的消息,将会返回加密该消息使用的密钥 ID。

该函数可能返回 2 个特殊的密钥 ID:

  • SYMKEY,表明该消息使用对称密钥进行加密。

  • ANYKEY,表明该消息使用公共密钥进行加密,但是密钥 ID 已经被删除。这也意味着需要尝试所有的私钥,查找可以解密该消息的私钥。pgcrypto 不会产生这种加密消息。

注意,不同的密钥可能拥有相同的 ID,这种情况很少见但可能存在。客户端应用程序需要自己尝试使用不同的密钥进行解密,就像处理 ANYKEY 一样。

armor()
语法

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

```

返回值
text
描述

armor() 函数用于将二进制数据转换为 PGP ASCII-armor 格式,相当于 Base64 加上 CRC 以及额外的格式化。

其中,data 是需要转换的数据;如果指定了 keys 和 values 数值,每个 key/value 对都会生成一个 armor header 并添加到编码格式中;两个数组都是一维数组,长度相同,并且不能包含非 ASCII 字符。

dearmor()
语法
sql dearmor(data text) returns bytea
返回值
bytea
描述
dearmor() 函数是armor()的逆转换。
pgp_armor_headers()
语法
sql pgp_armor_headers(data text, key out text, value out text) returns setof record
返回值
setof record
描述
pgp_armor_headers() 函数用于返回数据中的 armor header。返回结果是一个包含 key 和 value 两个字段的数据行集,如果其中包含任何非 ASCII 字符,都会被看作 UTF-8 字符。
PGP 加密函数使用示例

下面我们来看一个实例:

  1. 首先为 users 表增加一个信用卡字段:
seaboxsql=# ALTER TABLE users ADD COLUMN card bytea;
ALTER TABLE
seaboxsql=# select * from users;
 id | username |              password              | card
----+----------+------------------------------------+------
  4 | anne     | $1$AntKXnNp$eMXf9Txryu9jhP/mTwxsY/ |
  2 | anne     | $1$VB3DSija$s53Jy1JTKwjvcszHqu5mm. |
  6 | anne     | $1$/Pt7jFRw$3i81/byMiq0RbxQIDAjqY0 |
  3 | tony     | $1$HpD13yT7$1MMQP7BK1oErbRHrovUBB1 |
  1 | tony     | $1$MtT1/gF5$GucU5TbvtUOBxw2/HsNTS. |
  5 | tony     | $1$1lRLUeKb$hC9JN9JN56oXsT3SsTGFd1 |
(6 rows)
  1. 然后我们需要生成 PGP 密钥,对于 Linux 操作系统可以使用 gpg 工具。执行以下命令创建一个新的密钥:
[root@mpp1 ~]# gpg --gen-key
gpg (GnuPG) 2.0.22; Copyright (C) 2013 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

gpg: 已创建目录‘/root/.gnupg’
gpg: 新的配置文件‘/root/.gnupg/gpg.conf’已建立
gpg: 警告:在‘/root/.gnupg/gpg.conf’里的选项于此次运行期间未被使用
gpg: 钥匙环‘/root/.gnupg/secring.gpg’已建立
gpg: 钥匙环‘/root/.gnupg/pubring.gpg’已建立
请选择您要使用的密钥种类:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (仅用于签名)
   (4) RSA (仅用于签名)
您的选择? 2
DSA 密钥长度应在 1024 位与 3072 位之间。
您想要用多大的密钥尺寸?(2048)
您所要求的密钥尺寸是 2048 位
请设定这把密钥的有效期限。
         0 = 密钥永不过期
      <n>  = 密钥在 n 天后过期
      <n>w = 密钥在 n 周后过期
      <n>m = 密钥在 n 月后过期
      <n>y = 密钥在 n 年后过期
密钥的有效期限是?(0) 0
密钥永远不会过期
以上正确吗?(y/n)y

You need a user ID to identify your key; the software constructs the user ID
from the Real Name, Comment and Email Address in this form:
    "Heinrich Heine (Der Dichter) <heinrichh@duesseldorf.de>"

真实姓名:boush.wang
电子邮件地址:boush.wang@seaboxdata.com
注释:wbxian
您选定了这个用户标识:
    “boush.wang (wbxian) <boush.wang@seaboxdata.com>”

更改姓名(N)、注释(C)、电子邮件地址(E)或确定(O)/退出(Q)?O
您需要一个密码来保护您的私钥。

我们需要生成大量的随机字节。这个时候您可以多做些琐事(像是敲打键盘、移动
鼠标、读写硬盘之类的),这会让随机数字发生器有更好的机会获得足够的熵数。
gpg: WARNING: some OpenPGP programs can't handle a DSA key with this digest size
我们需要生成大量的随机字节。这个时候您可以多做些琐事(像是敲打键盘、移动
鼠标、读写硬盘之类的),这会让随机数字发生器有更好的机会获得足够的熵数。
gpg: /root/.gnupg/trustdb.gpg:建立了信任度数据库
gpg: 密钥 8D01FD90 被标记为绝对信任
公钥和私钥已经生成并经签名。

gpg: 正在检查信任度数据库
gpg: 需要 3 份勉强信任和 1 份完全信任,PGP 信任模型
gpg: 深度:0 有效性:  1 已签名:  0 信任度:0-,0q,0n,0m,0f,1u
pub   2048D/8D01FD90 2023-03-06
密钥指纹 = DBC1 3F84 4D17 C352 D4BE  E5CD 73E4 ED94 8D01 FD90
uid                  boush.wang (wbxian) <boush.wang@seaboxdata.com>
sub   2048g/1F64AFA0 2023-03-06

然后按照提示输入相关信息。推荐使用 DSA and Elgamal 密钥;对于 RSA 加密,必须创建一个仅用于签名的 DSA 或者 RSA 密钥作为主控密钥,然后使用 gpg –edit-key 增加一个 RSA 加密子密钥。

注意:请牢记此处输入的密码。

  1. 使用 gpg –list-secret-keys 查看创建的密钥
[root@mpp1 ~]# gpg --list-secret-keys
/root/.gnupg/secring.gpg
------------------------
sec   2048D/8D01FD90 2023-03-06
uid                  boush.wang (wbxian) <boush.wang@seaboxdata.com>
ssb   2048g/1F64AFA0 2023-03-06

其中,2048D 是密钥的比特长度, 8D01FD90 是私钥,1F64AFA0 是公钥。

  1. 将公钥和私钥转换为 ASCII-armor 格式
[root@mpp1 ~]# gpg -a --export 1F64AFA0 >public.key
[root@mpp1 ~]# gpg -a --export-secret-keys 8D01FD90 >secret.key

其中,-a 表示 armour 格式;默认的密钥是二进制格式,不方便处理。在使用 pgcrypto PGP 加密/解密函数时需要利用 dearmor() 函数将密钥转换为二进制再传入参数;如果可以直接处理二进制数据,也可以去掉 -a 选项。

  1. 直接查看公钥 public.key和私钥secret.key 的内容如下
[root@mpp1 ~]# cat public.key
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v2.0.22 (GNU/Linux)

... ...
-----END PGP PUBLIC KEY BLOCK-----

[root@mpp1 ~]# cat secret.key
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: GnuPG v2.0.22 (GNU/Linux)

... ...
-----END PGP PRIVATE KEY BLOCK-----
  1. 创建一个存储公钥的表 keys以方便处理,把 public.key 的内容插入到keys表中。
seaboxsql=# CREATE TABLE keys(v text);
CREATE TABLE
seaboxsql=#
seaboxsql=#
seaboxsql=#
seaboxsql=#
seaboxsql=# INSERT INTO keys VALUES ('-----BEGIN PGP PUBLIC KEY BLOCK-----
seaboxsql'# Version: GnuPG v2.0.22 (GNU/Linux)
seaboxsql'#
seaboxsql'# ... ...
seaboxsql'# -----END PGP PUBLIC KEY BLOCK-----');
INSERT 0 1
  1. 将信用卡号进行加密存储
seaboxsql=# UPDATE users SET card = pgp_pub_encrypt('62220001', dearmor(keys.v)) FROM keys WHERE username = 'tony';
UPDATE 3
seaboxsql=# SELECT card FROM users WHERE username = 'tony';
     
         card   
-----------------------------
 \xc1c14e03 ... ... 5cbfb14917c
 \xc1c14e03 ... ... 8657c9098c1
 \xc1c14e03 ... ... 9bdfbf14860
(3 rows)

查询结果显示 card 字段已经被加密存储。

  1. 可以使用 pgp_key_id() 函数验证加密使用的公钥.
seaboxsql=#  SELECT pgp_key_id(card) FROM users WHERE username = 'tony';
    pgp_key_id
------------------
 EFF48C221F64AFA0
 EFF48C221F64AFA0
 EFF48C221F64AFA0
(3 rows)

seaboxsql=# SELECT pgp_key_id(dearmor(v)) FROM keys;
    pgp_key_id
------------------
 EFF48C221F64AFA0
(1 row)
  1. 应用程序可以通过私钥 secret.key 解密信用卡号
seaboxsql=# SELECT pgp_pub_decrypt(card, dearmor('-----BEGIN PGP PRIVATE KEY BLOCK-----
seaboxsql'# Version: GnuPG v2.0.22 (GNU/Linux)
seaboxsql'#
seaboxsql'# ... ...
seaboxsql'# -----END PGP PRIVATE KEY BLOCK-----
seaboxsql'# '), '******')
seaboxsql-# FROM users
seaboxsql-# WHERE username = 'tony';
 pgp_pub_decrypt
-----------------
 62220001
 62220001
 62220001
(3 rows)

其中'******'是步骤2创建密钥时输入的口令。

注意,PGP 代码存在以下限制:

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

  • 不支持加密密钥作为主控密钥。

  • 不支持多个子密钥。因此,不要使用常规 GPG/PGP 密钥作为 pgcrypto 加密密钥,而应该创建新的密钥。

PGP 函数选项

pgcrypto 函数中的选项名称和 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
适用函数
pgp_sym_encrypt、pgp_pub_encrypt
compress-algo

使用的压缩算法。

取值
0,不压缩,默认值;1,ZIP 压缩;2,ZLIB 压缩(ZIP 加上元数据和 CRC)
适用函数
pgp_sym_encrypt、pgp_pub_encrypt
compress-level

压缩级别,级别越高结果越小但速度更慢,0 表示不压缩。

取值
0、1-9,默认为 6
适用函数
pgp_sym_encrypt、pgp_pub_encrypt
convert-crlf

加密时是否将\n 转换为 \r\n 并且解密时执行相反的转换,RFC 4880 指定文本数据需要使用 \r\n 作为换行符。

取值
0(默认值)、1
适用函数
pgp_sym_encrypt、pgp_pub_encrypt、pgp_sym_decrypt、pgp_pub_decrypt
disable-mdc

不使用 SHA-1 保护数据,仅用于兼容古老的 PGP 产品。

取值
0(默认值)、1
适用函数
pgp_sym_encrypt、pgp_pub_encrypt
sess-key

使用单独的会话密钥。公钥加密总是使用单独的会话密钥;该选项用于对称密钥加密,因为它默认直接使用 S2K 密钥。

取值
0、1-9,默认为 6
适用函数
pgp_sym_encrypt
s2k-mode

使用的 S2K 算法。

取值
0,不使用 salt,危险;1,使用 salt 但是迭代固定次数;3(默认值),使用 salt 同时迭代次数可变。
适用函数
pgp_sym_encrypt
s2k-count

S2K 算法的迭代次数。

取值
大于等于 1024 并且小于等于 65011712,默认为 65536 到 253952 之间的随机数。
适用函数
pgp_sym_encrypt 并且 s2k-mode=3
s2k-digest-algo

S2K 计算时的摘要算法。

取值
md5、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;如果没有指定这个选项就不会被标记。

取值
0(默认值)、1
适用函数
pgp_sym_encrypt、pgp_pub_encrypt

双向加密—原始加密函数

原始加密函数仅仅会对数据进行一次加密,不支持 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

其中,data 是需要加密的数据;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 模式忽略该参数。如果它的长度不是准确的块大小,可能会被截断或者使用 0 进行填充。对于没有该参数的两个函数,默认全部使用 0 填充。

双向加密—随机数据函数

gen_random_bytes()
语法
sql gen_random_bytes(count integer) returns bytea
返回值
bytea
描述
gen_random_bytes() 函数用于生成具有强加密性的随机字节. 其中,count 表示返回的字节数,取值从 1 到 1024。
示例
``` sql seaboxsql=# SELECT encode(gen_random_bytes(16), 'hex'); encode
0d292ec638c03bc6a92999aafc67ffb5 (1 row) ```
gen_random_bytes()
语法
sql gen_random_uuid() returns uuid
返回值
uuid
描述
gen_random_uuid()函数用于返回一个 version 4 的随机 UUID。
示例
``` sql seaboxsql=# SELECT gen_random_uuid(); gen_random_uuid
6791aa0d-39d1-4bb9-a4b8-63ced465139b (1 row) ```

其他事项

pgcrypto 配置

SeaboxSQL数据库编译时使用了 OpenSSL 选项,PGP 加密函数可以支持更多的算法;同时公钥加密函数速度会更快,因为 OpenSSL 提供了优化的 BIGNUM 函数。下表比较了使用或者不使用 OpenSSL 时支持的功能:

支持功能 内置 OpenSSL
MD5 YES YES
SHA1 YES YES
SHA224/256/384/512 YES YES
其他摘要算法 NO YES
Blowfish YES YES
AES YES YES
DES/3DES/CAST5 NO YES
原始加密 YES YES
PGP 对称加密 YES YES
PGP 公钥加密 YES YES
NULL 处理

所有函数都遵循 SQL 表中,如果任何参数为 NULL,结果返回 NULL。如果使用时不小心,可能会造成安全风险。

安全限制

所有的 pgcrypto 函数都在数据库服务器中运行,意味着数据和密码在客户端和 pgcrypto 之间使用明文进行传输。因此必须:

  • 使用本地连接或者 SSL 连接;

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

如果无法做到以上两点,一个更好的方式就是在客户端应用程序中完成加密/解密。

另外,pgcrypto 的实现无法抵抗旁路攻击(Side Channel Attacks)。例如,对于指定大小的不同密文,pgcrypto 解密函数所需的时间不同。