1 前言
基于合规、外部网络攻击等环境要求,需要企业梳理敏感数据,有选择性地加密敏感字段内容、脱敏前端敏感字段内容,保证数据库敏感数据的安全、前端页面的数据防泄露。敏感数据以密文的形式存储,能保证即使在存储介质被窃听或者数据文件被非法复制的情况下,敏感数据仍然是安全。
2 技术选型
对称加密 or 非对称加密
在对称加密中,加密和解密使用的是同一份密钥。而非对称加密中,加密和解密使用的是不同的密钥 。 非对称加密中的密钥分为公钥和私钥。任何人都可以通过公钥进行信息加密,但是只有用户私钥的人才能完成信息解密。
本次功能是向所有端提供公共sdk,加密和解密使用者在同一端,所以选择对称加密,AES是对称加密的典型工具,本次选择AES加密工具
3 AES加密
### AES加密简介
AES加密算法是密码学中的高级加密标准(Advanced Encryption Standard,AES),又称Rijndael加密法,是美国联邦政府采用的一种区块加密标准。这个标准用来替代原先的DES,已经被多方分析且广为全世界所使用。
AES 是一个新的可以用于保护电子数据的加密算法。明确地说,AES 是一个迭代的、对称密钥分组的密码,它可以使用128、192 和 256 位密钥,并且用 128 位(16字节)分组加密和解密数据。与公共密钥密码使用密钥对不同,对称密钥密码使用相同的密钥加密和解密数据。通过分组密码返回的加密数据 的位数与输入数据相同。迭代加密使用一个循环结构,在该循环中重复置换(permutations )和替换(substitutions)输入数据。
AES加密有很多轮的重复和变换。大致步骤如下:
- 密钥扩展(KeyExpansion)。
- 初始轮(Initial Round)。
- 重复轮(Rounds),每一轮又包括:SubBytes、ShiftRows、MixColumns、AddRoundKey。
- 最终轮(Final Round),最终轮没有MixColumns。
AES128 vs AES256
AES128与AES256区别是,AES的性能更快,因为底层是迭代加密原理,AES128的迭代次数AES256少,速度更快,而AES256的安全性相对更好
在早期JDK版本中,由于受美国的密码出口条例约束,Java中涉及加解密功能的API被限制出口,jdk1.8.99版本后才允许AES256的使用,如果使用AES256加密,如果集成AES jar包的项目jdk版本低于jdk1.8.99那么就要对jdk进行修改,所以本次选择AES128
4 AES加密实现
public class AESUtils {
private static Logger log = LoggerFactory.getLogger(AESUtils.class);
private static final String encodeRules = "dst-aes";
private static final String AES = "AES";
private static final String SHA_1_PRNG = "SHA1PRNG";
private static final String UTF_8 = "utf-8";
/**
* 加密
* 1.构造密钥生成器
* 2.根据ecnodeRules规则初始化密钥生成器
* 3.产生密钥
* 4.创建和初始化密码器
* 5.内容加密
* 6.返回字符串
*/
public static String AESEncode(String content) {
try {
KeyGenerator keygen = KeyGenerator.getInstance(AES);
SecureRandom random = SecureRandom.getInstance(SHA_1_PRNG);
random.setSeed(encodeRules.getBytes());
keygen.init(128, random);
SecretKey original_key = keygen.generateKey();
byte[] raw = original_key.getEncoded();
SecretKey key = new SecretKeySpec(raw, AES);
Cipher cipher = Cipher.getInstance(AES);
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] byte_encode = content.getBytes(UTF_8);
byte[] byte_AES = cipher.doFinal(byte_encode);
String AES_encode = Base64.getEncoder().encodeToString(byte_AES);
return AES_encode;
} catch (Exception e) {
log.error("加密数据失败,异常信息",e);
throw new RuntimeException(e);
}
}
/**
* 解密
* 解密过程:
* 1.同加密1-4步
* 2.将加密后的字符串反纺成byte[]数组
* 3.将加密内容解密
*/
public static String AESDecode(String content) {
try {
KeyGenerator keygen = KeyGenerator.getInstance(AES);
SecureRandom random = SecureRandom.getInstance(SHA_1_PRNG);
random.setSeed(encodeRules.getBytes());
keygen.init(128, random);
SecretKey original_key = keygen.generateKey();
byte[] raw = original_key.getEncoded();
SecretKey key = new SecretKeySpec(raw, AES);
Cipher cipher = Cipher.getInstance(AES);
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] byte_content = new BASE64Decoder().decodeBuffer(content);
byte[] byte_decode = cipher.doFinal(byte_content);
String AES_decode = new String(byte_decode, UTF_8);
return AES_decode;
} catch (Exception e) {
log.error("解密数据失败,异常信息",e);
throw new RuntimeException(e);
}
}
}
实现比较简单,首先是输入种子,根据AES算法生成秘钥,再根据秘钥加密数据就可以了,解密时也一样,测试一下
public static void main(String[] args) {
// 1 身份证号加密
System.*out*.println("未加密身份证号/211011199202292013");
String enCodeIdnumber = AESUtils.*AESEncode*("211011199202292013");
System.*out*.println("加密后身份证号/" + enCodeIdnumber +"/位数" + enCodeIdnumber.length());
String decodeIdnumber = AESUtils.*AESDecode*(enCodeIdnumber);
System.*out*.println("解密后身份证号/" + decodeIdnumber);
// 2 手机号加密
System.*out*.println("未加密手机号/18218424921");
String enCodeIdnumber = AESUtils.*AESEncode*("18218424921");
System.*out*.println("加密后手机号/" + enCodeIdnumber +"/位数" + enCodeIdnumber.length());
String decodeIdnumber = AESUtils.*AESDecode*(enCodeIdnumber);
System.*out*.println("解密后手机号/" + decodeIdnumber);
// 3 银行卡号加密
System.*out*.println("未加密银行卡号/6217902000023951234");
String enCodeIdnumber = AESUtils.*AESEncode*("6217902000023951234");
System.*out*.println("加密后银行卡号/" + enCodeIdnumber +"/位数" + enCodeIdnumber.length());
String decodeIdnumber = AESUtils.*AESDecode*(enCodeIdnumber);
System.*out*.println("解密后银行卡号/" + decodeIdnumber);
}
控制台结果
未加密身份证号/211011199202292013
加密后身份证号/Go5Kb2L515RN1OkuRoiWgiFQV8oq57P74pa/do91WXo=:c2pLTFkwMDAwMDAwMDAwMA==/位数69
解密后身份证号/211011199202292013
未加密手机号/18218424921
加密后手机号/271M5stznel/jKhSGagjmQ==:c2pLTFkwMDAwMDAwMDAwMA==/位数49
解密后手机号/18218424921
未加密银行卡号/6217902000023951234
加密后银行卡号/knbBiyfYbAd1hmyUZwiq6mn0CAxuTlPBSG509j7jvbI=:c2pLTFkwMDAwMDAwMDAwMA==/位数69
解密后银行卡号/6217902000023951234
这时候就实现了AES的加密,但是还没结束,公司的不止java团队还有PHP团队,他们也要用AES加密,且要和java这边的加密解密保持一致,由于双方不清楚对方的代码,所以对接起来有些困难,而且AES加密有很多种,比如AES-128-ECB,AES-128-CBC
等,最终为了兼容PHP端,将实现改为了AES-128-CBC
方式
5 兼容PHP加密
实现AES-128-CBC加密
/**
* @Description: AES加密类 128 CBC位加密
* @author: qianyun
* @date: 2022/6/1 14:19
*/
public class AESUtils {
private static String CIPHER_NAME = "AES/CBC/PKCS5PADDING";
/**
* 128 bits
*/
private static int CIPHER_KEY_LEN = 16;
private static Logger log = LoggerFactory.getLogger(AESUtils.class);
private static final String AES = "AES";
private static final String UTF_8 = "utf-8";
static {
AESConfig.readConfigFile();
}
/**
* AES加密数据
* @author yangjiawen
* @date 2022/6/6 10:18
* @param strToEncrypt: 加密字符串
* @return String
*/
public static String encrypt(String strToEncrypt) {
String key = AESConfig.key;
String iv = key.substring(0,5);
iv += "00000000000";
try {
IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes(UTF_8));
SecretKeySpec secretKey = new SecretKeySpec(fixKey(key).getBytes(UTF_8), AES);
Cipher cipher = Cipher.getInstance(AESUtils.CIPHER_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
byte[] encryptedData = cipher.doFinal((strToEncrypt.getBytes()));
String encryptedDataInBase64 = Base64.getEncoder().encodeToString(encryptedData);
String ivInBase64 = Base64.getEncoder().encodeToString(iv.getBytes(UTF_8));
return encryptedDataInBase64 + ":" + ivInBase64;
} catch (Exception e) {
log.error("加密数据失败,异常信息",e);
throw new RuntimeException(e);
}
}
private static String fixKey(String key) {
if (key.length() < AESUtils.CIPHER_KEY_LEN) {
int numPad = AESUtils.CIPHER_KEY_LEN - key.length();
for (int i = 0; i < numPad; i++) {
//0 pad to len 16 bytes
key += "0";
}
return key;
}
if (key.length() > AESUtils.CIPHER_KEY_LEN) {
//truncate to 16 bytes
return key.substring(0, CIPHER_KEY_LEN);
}
return key;
};
/**
* AES解密数据
* @author yangjiawen
* @date 2022/6/6 10:19
* @param data: 加密后数据
* @return String
*/
public static String decrypt(String data) {
String key = AESConfig.key;
try {
String[] parts = data.split(":");
IvParameterSpec iv = new IvParameterSpec(Base64.getDecoder().decode(parts[1]));
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(UTF_8), AES);
Cipher cipher = Cipher.getInstance(AESUtils.CIPHER_NAME);
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
byte[] decodedEncryptedData = Base64.getDecoder().decode(parts[0]);
byte[] original = cipher.doFinal(decodedEncryptedData);
return new String(original);
} catch (Exception e) {
log.error("解密数据失败,异常信息",e);
throw new RuntimeException(e);
}
}
}
经过测试,双方的秘钥相同,加密后结果相同,这样一个AES加密就完成了。
6 结语
随着信息技术的发展,数据安全越来越重要,一些敏感数据还是需要加密的方式来存储,所以对一些加密技术进行一些了解很有好处,对称加密和非对称加密在银行等项目中是经常被使用的技术,跨语言的加解密也是一个经常遇到的问题,实现很简单,因为很多实现细节都已经封装好了,文章没有写的很深入,如果有兴趣可以研究研究底层。