PostgreSQL提供了几个不同级别的加密, 并且在保护数据不会因为数据库服务器偷窃、不道德的管理员、不安全网络等因素而泄漏方面 提供很高的灵活性。加密可能也是保护一些诸如医疗记录或财务交易等敏感数据所要求的。
pgcrypto模块允许对特定域进行加密存储。这个功能只对某些敏感数据有用。客户端提供解密的密钥,然后数据在服务器端解密并发送给客户端。在数据被解密和在服务器与客户端之间传递时,解密数据和解密密钥将会在服务器端存在短暂的一段时间。这就给那些能完全访问数据库服务器的人提供了一个短暂的截获密钥和数据的时间,例如系统管理员。
PGP 加密函数
这里的函数实现了OpenPGP (RFC 4880) 标准的加密部分。对称密钥和公钥加密都被支持。
一个加密的 PGP 消息由两个部分或者包组成:
• 包含一个会话密钥的包 — 加密过的对称密钥或者公钥。
• 包含用会话密钥加密过的数据的包。
当用一个对称密钥(即一个口令)加密时:
1. 给定的口令被使用一个 String2Key (S2K) 算法哈希。这更像crypt()算法 — 有目的地慢并且使用随机 salt — 但是它会产生一个全长度的二进制密钥。
2. 如果要求一个独立的会话密钥,将会生成一个新的随机密钥。否则该 S2K 密钥将被直接用作会话密钥。
3. 如果直接使用 S2K 密钥,那么只有 S2K 设置将被放入会话密钥包中。否则会话密钥会用S2K 密钥加密并且放入会话密钥包中。
当使用一个公共密钥加密时:
1. 一个新的随机会话密钥会被生成。
2. 它被用公共密钥加密并且放入到会话密钥包中。
在两种情况下,要被加密的数据按下列步骤被处理:
1. 可选的数据操纵:压缩、转换成 UTF-8 或者行末转换。
2. 数据会被加上一个随机字节的块作为前缀。这等效于使用一个随机 IV。
3. 追加一个随机前缀和数据的 SHA1 哈希。
4. 所有这些都用会话密钥加密并且放在数据包中。
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。
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解密原始文本数据是好的。
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。给这个函数一个私钥会产生一个错误。
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解密原始文本数据是好的。
pgp_key_id()
pgp_key_id(bytea) 返回 text
pgp_key_id抽取一个 PGP 公钥或私钥的密钥 ID。或者如果给定了一个加密过的消息,它给出一个用来加密数据的密钥 ID。
它能够返回 2 个特殊密钥 ID:
• SYMKEY
该消息是用一个对称密钥加密的。
• ANYKEY
该消息是用公钥加密的,但是密钥 ID 已经被移除。这意味着你将需要尝试你所有的密钥来看看哪个能解密该消息。pgcrypto本身不产生这样的消息。
注意不同的密钥可能具有相同的 ID。这很少见但是是一种正常事件。客户端应用则应该尝试用每一个去解密,看看哪个合适 — 像处理ANYKEY一样。
armor(), dearmor()
armor(data bytea [ , keys text[], values text[] ]) 返回 text
dearmor(data text) returns bytea
这些函数把二进制数据包装/解包成 PGP ASCII-armored 格式,其基本上是带有 CRC 和额外格式化的 Base64。
如果指定了keys和values数组,每一个 键/值对的 armored 格式上会增加一个armorheader。两个 数组都必须是单一维度的,并且它们的长度必须相同。键和值不能包含任何非 ASCII 字符。
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。
下面通过一个例子来看:
首先为 users 表增加一个字段:

使用使用 gpg命令(要在root用户下执行)生成 PGP密钥,根据提示输入就可以:

查看密钥(sec是私钥,ssb是公钥):

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

查看公钥:

查看私钥:

创建一个存储公钥的表public_keys:


接下来我们将card字段进行加密存储,查询结果显示 card 字段已经被加密存储:

使用 pgp_key_id() 函数验证加密使用的公钥:

创建存储私钥的表:


应用程序可以通过私钥解密card存储的值,1234567890是创建密钥时加的密码:

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 外加元数据和块 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 产品的兼容,这些产品在受 SHA-1 保护的包被加入到 RFC 4880 之前就已经存在了。最近的 gnupg.org和 pgp.com 软件能很好地支持它。
值:0,1
默认:0
适用于:pgp_sym_encrypt, pgp_pub_encrypt
sess-key
使用单独的会话密钥。公钥加密总是使用一个单独的会话密钥。这个选项是用于对称密钥加密的,对称密钥加密默认直接使用 S2K 密钥。
值:0,1
默认:0
适用于:pgp_sym_encrypt
s2k-mode
要使用哪一种 S2K 算法。
值:
0 - 不用 salt。危险!
1 - 用 salt 但是使用固定的迭代计数。
3 - 可变的迭代计数。
默认:3
适用于:pgp_sym_encrypt
s2k-count
S2K 算法要使用的迭代次数。它必须是一个位于 1024 和 65011712 之间的值, 首尾两个值包括在内。
默认:65536 和 253952 之间的一个随机值
适用于:pgp_sym_encrypt,只能用于 s2k-mode=3
s2k-digest-algo
要在 S2K 计算中使用哪种摘要算法。
值:md5, sha1
默认:sha1
适用于:pgp_sym_encrypt
s2k-cipher-algo
要用哪种密码来加密独立的会话密钥。
值:bf, aes, aes128, aes192, aes256
默认:use cipher-algo
适用于:pgp_sym_encrypt
unicode-mode
是否把文本数据在数据库内部编码和 UTF-8 之间来回转换。如果你的数据库已经是 UTF-8,将不会转换,但是消息将被标记为 UTF-8。没有这个选项它将不会被标记。
值:0,1
默认:0
适用于:pgp_sym_encrypt, pgp_pub_encrypt
PGP 代码的限制
• 不支持签名。这也意味着它不检查加密子密钥是否属于主控密钥。
• 不支持加密密钥作为主控密钥。由于通常并不鼓励那种用法,这应该不是问题。
• 不支持多个子密钥。这可能看起来像一个问题,因为在实践中普遍需要多个子密钥。在另一方面,你不能把你的常规 GPG/PGP 密钥用于pgcrypto,而是创建一些新的密钥,因为使用场景相当不同。
原始的加密函数
这些函数只在数据上运行一次加密,它们不具有 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_iv和decrypt_iv中,iv参数是 CBC 模式的初始值,ECB 会忽略它。如果不是准确的块尺寸,它会被修剪或用零填充。在没有这个参数的函数中,它的值都被默认为零。
随机数据函数
gen_random_bytes(count integer) returns bytea
返回count个密码上强壮的随机字节。一次最多可以抽取 1024 个字节。这是为了避免耗尽随机数发生池。
gen_random_uuid() 返回 uuid
返回一个版本 4 的(随机的)UUID,,从 PostgreSQL 13 开始成为了一个内置函数。
配置
pgcrypto会根据查找主 PostgreSQL configure脚本配置它自身。影响它的选项是--withzlib以及--with-openssl。
在编译了 zlib 时,PGP 加密函数能够在加密前压缩数据。
在编译了 OpenSSL 时,会有更多可用算法。公钥加密函数也会更快,因为 OpenSSL 有优化得更好的 BIGNUM 函数。
NULL 处理
按照 SQL 中的标准,只要任何参数是 NULL, 所有的函数都会返回 NULL。在不当使用时这可能会导致安全风险。
安全性限制
所有pgcrypto函数都在数据库服务器内部运行。这意味着在pgcrypto和客户端应用之间移动的所有数据和口令都是明文。因此,你必须:
1. 本地连接或者使用 SSL 连接。
2. 信任系统管理员和数据库管理员。
如果你不能这样做,那么最好在客户端应用中进行加密。该实现无法抵抗侧信道攻击。例如,一个pgcrypto解密函数完成所需的时间是随着密文尺寸变化的。
关于 pgcrypto 模块的更多信息可以参考 PostgreSQL 官方文档F.25。




