更新时间:2024-12-30 GMT+08:00

使用pgcrypto加密GaussDB(DWS)数据

GaussDB(DWS)数据库自8.2.0集群版本开始内置加密解密模块pgcrypto。pgcrypto模块允许数据库用户以加密形式存储数据的某些列,为敏感数据增加了一层额外的保护。因此在没有加密密钥的情况下,任何人都无法读取以加密形式存储在GaussDB(DWS)数据库中的数据。

pgcrypto函数在数据库服务器内部运行,这意味着所有数据和密码都以明文形式在pgcrypto和客户端应用程序之间传输。为了获得最佳安全性,建议在客户端和GaussDB(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() 函数对字符串GaussDB(DWS)进行sha256加密存储:

    select digest('GaussDB(DWS)', 'sha256');
                                   digest                               
    --------------------------------------------------------------------
     \xcc2d1b97c6adfba44bbce7386516f63f16fc6e6a10bd938861d3aba501ac8aab
    (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加密算法对字符串GaussDB(DWS)计算MAC值:

    select hmac('GaussDB(DWS)', 'key123', 'sha256');
    hmac 
    --------------------------------------------------------------------
    \x14e1d9e110e9b11ab8379dc02b49533d50a6f4deafe6d6cd451d06c106c97d83
    (1 row) 

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

    如果该密钥大于哈希块的长度,它将先被哈希然后把结果用作密钥。

密码哈希函数

crypt()和gen_salt()函数专用于哈希密码。crypt()执行哈希用于加密数据,gen_salt()用于生成加盐哈希。

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

  • crypt()中算法很慢。由于密码包含的数据量很小,这是增加暴力破解难度的唯一方法。
  • 它们使用一个随机值(称为salt,即盐值),因此这样具有相同口令的用户将得到不同的密文口令。这也是针对破解算法提供一种额外的安全保护。
  • 它们的结果中包括了算法类型,因此可以针对不同用户使用不同的算法对密码进行加密。
  • 其中一些算法具有自适应性,意味着当计算机性能变得更快时,可以调整该算法使其变得更慢,而不会产生与已有密码的不兼容性。

crypt()函数所支持的算法如下表:

表1 crypt()支持的算法

算法

密码最大长度

自适应性

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指迭代次数, 数字越大加密时间越长, 被破解需要的时间也越长。

    1
    2
    3
    4
    5
    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算法,迭代次数必须是一个奇数。

    表2 crypt()的迭代计数

    算法

    默认值

    最小值

    最大值

    xdes

    725

    1

    16777215

    bf

    6

    4

    31

PGP加密函数

GaussDB(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不会产生这种加密消息。

    不同的密钥可能拥有相同的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。

    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。其他选项通常使用默认值。

    表3 pgcrypto加密选项

    选项

    描述

    默认值

    取值

    适用函数

    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