HerryDong 2018-05-14
公司内部的P2P平台由于监管,进行了一系列不合规项的整改。其中有一条就是要对数据库中一些比较敏感的信息进行加密,比如:手机,邮箱,身份证,银行卡之类的。解决方案是,在整个过程,查询的时候在java服务端进行解密,插入和更新的时候在java服务端进行加密。数据库端则存放加密后的信息。本来只需要考虑java服务端加解密后能保持一致即可,但是为了方便某些在数据库端进行运维的人能使用sql对已加密的信息进行处理。这里要求了java服务端加密的内容能够在数据库端进行解密。所以两边需要使用一样的算法。
首先,第一步就是在postgresql数据库端安装pgcrypto模块
postgres=# \c superp2b_test
You are now connected to database "superp2b_test" as user "enterprisedb".
superp2b_test=# create extension pgcrypto;
CREATE EXTENSION
superp2b_test=#
安装完后就可以使用postgresql数据库中的decrypt加密函数和encrypt解密函数了。
在网上找了java的AES加解密的工具类,进行加密,然后数据库端进行解密,报错。发现两边的加解密不一致。网上查了AES算法,才发现AES算法有多种加密模式,填充模式,如果两边的模式不一致,则加密出来的内容也会不一致。且加密其实是对byte字节的加密,我对加密前字符串转字节的过程和加密后字节转字符串的过程是否一致也产生了怀疑。
由于java服务端的加解密对我们来说相对透明可见,而数据库端的加解密的过程则完全不清楚,只暴露了一个函数名和几个函数参数。这里查询了postgresql官网。由于当前数据库使用的版本是9.3。故查询了
https://www.postgresql.org/docs/9.3/static/pgcrypto.html
可知默认用cbc模式,数据可为任何长度。
把数据库的模式改成aes-ecb后再次尝试分别在java和数据库端进行加解密,两边结果依然不一致。很是困惑。经过很久的排查,最后才发现网上搜到的java端aes算法大部分都额外会对密钥进行处理,从而导致了两边加解密不一致。
这里贴一下java端的AES加解密工具类代码:
package com.xxx.xxx.framework.ext.sqlfilter; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; public class AESUtil { private static Logger logger = LoggerFactory.getLogger(AESUtil.class); private static String MODEL = "AES/ECB/PKCS5Padding"; private static String useKey ="0000000000888888"; public static String encrypt(String content) { if(StringUtils.isEmpty(content)){ return content; } String result = content; try { byte[] contentBytes = content.getBytes("UTF-8"); SecretKeySpec skeySpec = new SecretKeySpec(useKey.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance(MODEL); cipher.init(Cipher.ENCRYPT_MODE, skeySpec); byte[] encryptResult = cipher.doFinal(contentBytes); result = Base64.encodeBase64String(encryptResult); //替换\r \n result = result.replace("\n", "").replace("\r", ""); } catch (Exception ex) { logger.error("进行自动加密时出错,加密内容为"+content+",异常信息"+ex.getMessage(), ex); throw new RuntimeException(ex); } return result; } public static String decrypt(String content){ if(StringUtils.isEmpty(content)){ return content; } String result = content; byte[] contentBytes =null; try{ if(content.length()%4==0){ contentBytes =Base64.decodeBase64(content); }else{ logger.error("进行自动解密时出错,字符串{}不是base64编码过的字符串!",content); throw new RuntimeException("字符串"+content+"不是base64编码过的字符串!"); } } catch (Exception ex) { logger.error("进行自动解密时出错,字符串"+content+"不是base64编码过的字符串,进行base64解码出错!出错信息:"+ex.getMessage(),ex); throw new RuntimeException(ex); } if(contentBytes!=null){ try{ SecretKeySpec skeySpec = new SecretKeySpec(useKey.getBytes("UTF-8"), "AES"); Cipher cipher = Cipher.getInstance(MODEL); cipher.init(Cipher.DECRYPT_MODE, skeySpec); byte[] decryptResult = cipher.doFinal(contentBytes); if (decryptResult != null) { result = new String(decryptResult, "UTF-8"); } } catch (Exception ex) { logger.error("进行自动解密时出错,加密内容为"+content+",异常信息"+ex.getMessage(), ex); throw new RuntimeException(ex); } } return result; } /* public static void main(String args[]) { String content ="test123456"; System.out.println(encrypt(content)); System.out.println(decrypt(encrypt(content))); System.out.println(decrypt("954545test")); }*/ }
而数据库端,加密则select encode(encrypt('test123456'::bytea,'0000000000888888','aes-ecb'),'base64');解密则为select convert_from(decrypt(decode('cbbuXr3h9EWL0QOSDreFsw==','base64'),'0000000000888888','aes-ecb'),'SQL_ASCII');
可在数据库端建立函数mydec(varchar,varchar):
BEGIN IF $1 IS NULL OR $1='' THEN RETURN $1; ELSE IF $2 IS NULL OR $2='' THEN RETURN $1; ELSE RETURN encode(encrypt($1::bytea,$2::bytea,'aes-ecb'),'base64'); END IF; END IF; EXCEPTION WHEN OTHERS THEN RAISE EXCEPTION '(%)',$1; return -1; END
进行加密则可简化为select myenc('test123456','0000000000888888');
解密同理。以上的0000000000888888就是密钥。
再次过程,需要保证数据库和java两端的加解密模式都是一样的。上面的例子里,数据库和java两端的加密
模式都是ECB。当java端使用ECB,而数据库模式是使用CBC模式,则在加密16个字节以上的内容时,会出现乱码。两边加解密不一致。这个应该跟AES内部把数据按照16个字节进行分块有关吧。没有去仔细研究。
这里是简单的描述下自己在java后端和数据库端两边进行加解密的过程中遇到的坑,希望对其他人有所帮助。