
# 使用pgcrypto加密DWS数据
DWS数据库自8.2.0集群版本开始内置加密解密模块pgcrypto。pgcrypto模块允许数据库用户以加密形式存储数据的某些列，为敏感数据增加了一层额外的保护。因此在没有加密密钥的情况下，任何人都无法读取以加密形式存储在DWS数据库中的数据。
pgcrypto函数在数据库服务器内部运行，这意味着所有数据和密码都以明文形式在pgcrypto和客户端应用程序之间传输。为了获得最佳安全性，建议在客户端和DWS服务器之间使用SSL连接。
有关pgcrypto模块中各个函数的详细信息如下：
#### 通用哈希函数
- digest()
  digest()函数可以根据不同的算法生成数据的二进制哈希值，语法如下：
  ```
  digest(data text, type text) returns bytea 
  digest(data bytea, type text) returns bytea
  ```
  其中，data是原始数据，type是加密算法包括md5、sha1、sha224、sha256、sha384、sha512和sm3；函数的返回结果为二进制字符串。
  示例：
  使用digest() 函数对字符串DWS进行sha256加密存储：
  ```
  select digest('DWS', 'sha256');
                                 digest                               
  --------------------------------------------------------------------
   \x1d595d9e342bccc3b158fabfcdfece54d9f821cf9c7461c5d3b6c0157c442f94
  (1 row)
  ```
  
 
- hmac()
  hmac()函数可以根据不同的算法为带有密钥的数据计算出MAC值，语法如下：
  ```
  hmac(data text, key text, type text) returns bytea
  hmac(data bytea, key bytea, type text) returns bytea
  ```
  其中，data是原始数据，key是加密密钥，type是加密算法，包括md5、sha1、sha224、sha256、sha384、sha512以及sm3；函数的返回结果为二进制字符串。
  示例：
  使用key123密钥和sha256加密算法对字符串DWS计算MAC值：
  ```
  select hmac('DWS', 'key123', 'sha256');
  hmac 
  --------------------------------------------------------------------
  \xa2ee3b0d627a93166ac2e6a313deb86e0710ad4505cd7f7b88d563579174b8f9
  (1 row)
  ```
  对于digest() 函数，如果同时被修改了原始数据和加密结果，无法进行识别；hmac() 函数只要密钥没有泄露的话，可以发现被篡改的数据。
  如果该密钥大于哈希块的长度，它将先被哈希然后把结果用作密钥。
  
#### 密码哈希函数
crypt()和gen_salt()函数专用于哈希密码。crypt()执行哈希用于加密数据，gen_salt()用于生成加盐哈希。
crypt()中的算法和普通的MD5或者SHA1哈希算法存在以下不同之处：
- crypt()中算法很慢。由于密码包含的数据量很小，这是增加暴力破解难度的唯一方法。
- 它们使用一个随机值（称为*salt*，即盐值），因此这样具有相同口令的用户将得到不同的密文口令。这也是针对破解算法提供一种额外的安全保护。
- 它们的结果中包括了算法类型，因此可以针对不同用户使用不同的算法对密码进行加密。
- 其中一些算法具有自适应性，意味着当计算机性能变得更快时，可以调整该算法使其变得更慢，而不会产生与已有密码的不兼容性。
crypt()函数所支持的算法如下表：
表1crypt()支持的算法 
| 算法   | 密码最大长度    | 自适应性 | Salt位数 | 输出结果长度 | 描述                |
|:---|:---|:---|:---|:---|:---|
| bf   | 72        | √    | 128    | 60     | 基于Blowfish的2a变种算法 |
| md5  | unlimited | ×    | 48     | 34     | 基于MD5的加密算法        |
| xdes | 8         | √    | 24     | 20     | 扩展DES             |
| des  | 8         | ×    | 12     | 13     | 原生UNIX加密算法        |
   
- crypt()
  crypt()语法格式如下：
  ```
  crypt(password text, salt text) returns text
  ```
  该函数返回password字符串crypt(3)格式的哈希值，salt参数由gen_salt()函数生成。
  对于相同的密码，crypt()函数每次也会返回不同的结果，因为gen_salt()函数每次都会生成不同的salt。校验密码时可以将之前生成的哈希结果作为salt。
  例如：设置一个新密码：
  ```
  UPDATE ... SET pswhash = crypt('new password', gen_salt('bf',10));
  ```
  通过比较存储的密码哈希值验证输入的密码的正确性。
  ```
  SELECT (pswhash = crypt('entered password', pswhash)) AS pswmatch FROM ... ;
  ```
  如果输入的口令正确，这会返回true。
  示例：
  ```
  create table userpwd(userid int8, pwd text);
  CREATE TABLE
  insert into userpwd values (1, crypt('this is a pwd', gen_salt('bf',10)));
  INSERT 0 1
  select crypt('this is a pwd', pwd)=pwd as result from userpwd where userid =1;
   result 
  --------
   t
  (1 row)
  select crypt('this is a wrong pwd', pwd)=pwd as result from userpwd where userid =1;
   result 
  --------
   f
  (1 row)
  ```
  

- gen_salt()
  gen_salt()函数用来产生随机的参数给crypt，语法如下：
  ```
  gen_salt(type text [, iter_count integer ]) returns text
  ```
  该函数每次都会生成一个随机的盐值(salt)字符串，该字符串同时决定了crypt()函数使用的算法；type参数用于指定一个生成字符串的哈希算法，取值包括 des、xdes、md5 以及 bf。对于xdes和bf算法，iter_count指迭代次数, 数字越大加密时间越长, 被破解需要的时间也越长。
  ```
  SELECT gen_salt('des'), gen_salt('xdes'), gen_salt('md5'), gen_salt('bf');
   gen_salt | gen_salt  |  gen_salt   |           gen_salt            
  ----------+-----------+-------------+-------------------------------
   qh       | _J9..uEUi | $1$SNgqyKAi | $2a$06$B/Etc3J8zYBV49LrDU97MO
  (1 row)
  ```
  每种算法生成的salt拥有固定的格式，例如bf算法结果中的$2a$06$，2a表示Blowfish的2a变种算法，06表示迭代的次数。
  如果忽略 iter_count，将会使用默认的迭代次数。允许的iter_count值与算法相关，如下表所示。对于xdes算法，迭代次数必须是一个奇数。
  表2crypt()的迭代计数 
  | 算法   | 默认值 | 最小值 | 最大值      |
  |:---|:---|:---|:---|
  | xdes | 725 | 1   | 16777215 |
  | bf   | 6   | 4   | 31       |
     
  
#### PGP加密函数
DWS的PGP加密函数遵循OpenPGP(RFC 4880)标准，包括对称密钥加密（私钥加密）和非对称密钥加密（公钥加密）。
加密后的PGP消息由两部分组成：
- 这个消息的会话密钥（加密过的对称密钥或者公钥）。
- 使用该会话密钥加密的数据。
 
对于对称密钥（也就是密码）加密：
1. 使用String2Key（S2K）算法对密钥进行加密，类似于执行一个特意减慢并且包含随机salt的crypt() 算法，生成一个完整长度的二进制密钥。
2. 如果要求一个单独的会话密钥，生成一个随机的密钥。否则使用上面的S2K密钥直接作为会话密钥。
3. 如果直接使用S2K密钥，只将S2K设置加入会话密钥包中。否则，使用S2K密钥对会话密钥进行加密，然后放入会话密钥包中。
对于公钥加密：
1. 生成一个随机的会话密钥。
2. 使用公钥对其进行加密后放入会话密钥包中。
无论哪种情况，数据的加密过程如下：
1. 执行可选的数据操作：压缩、转换成UTF-8或者换行符的转换。
2. 在数据前面增加一个随机字节组成的块，相当于使用了一个随机的初始值（IV）。
3. 追加随机前缀和数据的SHA1哈希值到数据后面。
4. 将所有内容使用会话密钥进行加密后放入数据包中。
**支持的PGP加密函数**：
- 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
  ```
  其中，data是要加密的数据；psw是PGP对称密钥；options 参数用于设置选项，参考[表3]。
  
- pgp_sym_decrypt() 描述：用于解密PGP对称密钥加密后的消息。
  语法格式：
  ```
  pgp_sym_decrypt(msg bytea, psw text [, options text ]) returns text 
  pgp_sym_decrypt_bytea(msg bytea, psw text [, options text ]) returns bytea
  ```
  其中，msg是要解密的消息；psw是PGP对称密钥；options参数用于设置选项，参考[表3]。为了避免输出无效的字符，不允许使用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
  ```
  其中，data是要加密的数据；key是PGP公钥，如果传入一个私钥将会返回错误；options参数用于设置选项，参考[表3]。
  
- pgp_pub_decrypt() 描述：用于解密PGP公共密钥加密后的消息。
  语法格式：
  ```
  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 对原始文本数据进行解密。
  其中，key是公共密钥对应的私钥；如果私钥使用了密码保护功能，必须在psw参数中指定密码；如果没有使用密码保护，想要指定options参数时必须指定一个空的psw。options参数用于设置选项，参考[表3]。
  
- pgp_key_id() 描述：用于提取PGP公钥或者私钥的密钥ID；如果传入一个加密后的消息，将会返回加密该消息使用的密钥ID。
  语法格式：
  ```
  pgp_key_id(bytea) returns text
  ```
  该函数可能返回两个特殊密钥ID：
  - SYMKEY，表示该消息使用对称密钥进行加密。
  
  - ANYKEY，表示该消息使用公钥进行加密，但是密钥ID已经被删除。这意味着需要尝试所有的密钥，查找可以解密该消息的私钥。pgcrypto不会产生这种加密消息。
  
  
  ![](https://support.huaweicloud.com/devg-dws/public_sys-resources/note_3.0-zh-cn.png)
  不同的密钥可能拥有相同的ID，这种情况很少见但可能存在。客户端应用程序需要自己尝试使用不同的密钥进行解密，就像处理ANYKEY一样。
  
- armor() 描述：用于将二进制数据转换为PGP ASCII-armor格式，相当于Base64加上CRC以及额外的格式化。
  语法格式：
  ```
  armor(data bytea [ , keys text[], values text[] ]) returns text
  ```
  
- dearmor() 描述：用于执行相反的转换。
  语法格式：
  ```
  dearmor(data text) returns bytea
  ```
  将加密后的数据bytea，转换成PGP ASCII-armor格式.或者反向转换。
  其中，data是需要转换的数据；如果指定了keys和values数值，每个key/value对都会生成一个armor header并添加到编码格式中；两个数组都是一维数组，长度相同，并且不能包含非ASCII字符。
  
- pgp_armor_headers()
  描述：函数用于返回数据中的armor header。
  ```
  pgp_armor_headers(data text, key out text, value out text) returns setof record
  ```
  返回结果是一个包含key和value两个字段的数据行集，如果其中包含任何非ASCII字符，都会被视作UTF-8字符。
  
  **用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。
  ![](https://support.huaweicloud.com/devg-dws/public_sys-resources/notice_3.0-zh-cn.png)
  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。其他选项通常使用默认值。
   表3pgcrypto加密选项 
  | 选项              | 描述                                                                                | 默认值                      | 取值                                                                                                                                                                                                                                                                                                                                                                                                      | 适用函数                                                               |
  |:---|:---|:---|:---|:---|
  | cipher-algo     | 使用的密码算法。                                                                          | aes128                   | bf, aes128, aes192, aes256, 3des, cast5                                                                                                                                                                                                                                                                                                                                                                 | pgp_sym_encrypt, pgp_pub_encrypt                                   |
  | compress-algo   | 使用的压缩算法。                                                                          | 0                        | - 0，表示不压缩。  - 1，表示ZIP压缩。  - 2，表示ZLIB压缩 （= ZIP加上元数据和CRC）          | pgp_sym_encrypt, pgp_pub_encrypt                                   |
  | compress-level  | 压缩级别。级别越高压缩得越小但速度也越慢。0表示不压缩。                                                      | 6                        | 0，1-9                                                                                                                                                                                                                                                                                                                                                                                                   | pgp_sym_encrypt, pgp_pub_encrypt                                   |
  | convert-crlf    | 加密时是否将\\n转换成\\r\\n并且解密时执行相反的转换是否把\\r\\n转换成\\n。RFC4880指定文本数据存储时需要使用\\r\\n作为换行符。    | 0                        | 0，1                                                                                                                                                                                                                                                                                                                                                                                                     | pgp_sym_encrypt, pgp_pub_encrypt, pgp_sym_decrypt, pgp_pub_decrypt |
  | disable-mdc     | 不用 SHA-1 保护数据。仅用于兼容老旧的PGP产品。                                                      | 0                        | 0，1                                                                                                                                                                                                                                                                                                                                                                                                     | pgp_sym_encrypt, pgp_pub_encrypt                                   |
  | sess-key        | 使用单独的会话密钥。公钥加密总是使用一个单独的会话密钥。该选项用于对称密钥加密，因为对称密钥加密默认直接使用 S2K 密钥。                    | 0                        | 0，1                                                                                                                                                                                                                                                                                                                                                                                                     | pgp_sym_encrypt                                                    |
  | s2k-mode        | 使用的S2K 算法。                                                                        | 3                        | - 0 ，表示不使用salt。不推荐！  - 1，表示使用 salt ，但是迭代固定次数。  - 3 ，可变的迭代计数。   | pgp_sym_encrypt                                                    |
  | s2k-count       | S2K 算法的迭代次数。                                                                      | 65536 和 253952 之间的一个随机数值 | 大于等于1024并且小于等于65011712                                                                                                                                                                                                                                                                                                                                                                                  | pgp_sym_encrypt，并且s2k-mode=3                                       |
  | s2k-digest-algo | S2K计算时的摘要算法。                                                                      | sha1                     | md5, sha1                                                                                                                                                                                                                                                                                                                                                                                               | pgp_sym_encrypt                                                    |
  | s2k-cipher-algo | 加密单独会话密钥时使用的密码。                                                                   | 默认使用 cipher-algo的算法      | bf, aes, aes128, aes192, aes256                                                                                                                                                                                                                                                                                                                                                                         | pgp_sym_encrypt                                                    |
  | unicode-mode    | 是否将文本数据在数据库内部编码和 UTF-8 之间来回转换。如果当前数据库已经是UTF-8，不会执行转换，但是消息将被标记为UTF-8。没有指定该选项就不会被标记 | 0                        | 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算法。包括近义词：BF，BF-CBC；BLOWFISH，BF-CBC；BLOWFISH-CBC，BF-CBC；BLOWFISH-ECB，BF-ECB；BLOWFISH-CFB，BF-CFB。
- aes，AES算法(Rijndael-128, -192或-256)。包括近义词：AES，AES-CBC；RIJNDAEL，AES-CBC；RIJNDAEL，AES-CBC；RIJNDAEL-CBC，AES-CBC；RIJNDAEL-ECB，AES-ECB。
- DES算法。包括近义词：DES，DES-CBC；3DES，DES3-CBC；3DES-ECB，DES3-ECB；3DES-CBC，DES3-CBC
- sm4，SM4算法。包括近义词：SM4-CBC
- CAST5算法。包括近义词：CAST5-CBC
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()函数用于生成具有强加密性的随机字节。
  ```
  gen_random_bytes(count integer) returns bytea
  ```
  其中，count表示返回的字节数，取值为1到1024。
  示例：
  ```
  SELECT gen_random_bytes(16);
            gen_random_bytes          
  ------------------------------------
   \x1f1eddc11153afdde0f9e1229f8f4caf
  (1 row)
  ```
  

- gen_random_uuid()函数用于返回一个version 4的随机UUID。
  ```
  SELECT gen_random_uuid();
  gen_random_uuid 
  --------------------------------------
  2bd664a2-b760-4859-8af6-8d09ccc5b830
  ```
  
 
