加密是防止原始数据被窃取之后导致里面的敏感信息泄露的典型手段,比如数据库文件被拖走。如果数据经过加密,则即使黑客拿到了数据库文件,也会因为无法解密而保护原始信息不泄露。
这里以 AES 算法加密为例
- 加随机盐得到 SaltPlainText:在密钥前加个固定长度的随机盐
- 通过AES算法加密加盐后的明文得到 secretText1
- 利用 Base64 编码控制 secretText1 的长度 得到 secretBase64
具体实现如下所示:
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.SecureRandom;
import java.util.Base64;
/**
* 与 golang 的加密相对应,AES-128 CFB 模式
*
*/
public class AesKeyUtil {
/**
* 偏移变量,固定占16位字节
*/
private static final String IV = "0000000000000000";
private static final String CIPHER_ALGORITHM = "AES/CFB/NoPadding";
private static final String ALGORITHM = "AES";
private static final String CHARSET = "UTF-8";
private static final int BLOCK_SIZE = 16;
//加密
public static String encrypt(String key, String plainText) throws Exception {
//1. 加随机盐
byte[] result = addSaltBytes(plainText.getBytes(CHARSET));
//2. Aes 算法加密
result = encryptAes(key, result);
//3. Base64 编码
return base64Encode(result);
}
private static String base64Encode(byte[] src) {
return Base64.getEncoder().encodeToString(src);
}
private static byte[] encryptAes(String key, byte[] plainBytes) throws Exception {
if (StringUtils.length(key) != BLOCK_SIZE) {
throw new BusinessException("秘钥的长度必须等于 " + BLOCK_SIZE);
}
IvParameterSpec iv = new IvParameterSpec(IV.getBytes(CHARSET));
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(CHARSET), ALGORITHM);
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, iv);
return cipher.doFinal(plainBytes);
}
private static byte[] addSaltBytes(byte[] src) {
byte[] dst = new byte[src.length + BLOCK_SIZE];
System.arraycopy(src, 0, dst, BLOCK_SIZE, src.length);
// 生成固定长度的随机数
SecureRandom secureRandom = new SecureRandom();
byte[] seed = secureRandom.generateSeed(BLOCK_SIZE);
System.arraycopy(seed, 0, dst, 0, BLOCK_SIZE);
return dst;
}
public static String decrypt(String key, String secretText) throws Exception {
byte[] result = base64Decode(secretText);
result = decryptAes(key, result);
result = deleteSaltBytes(result);
return new String(result, CHARSET);
}
private static byte[] base64Decode(String src) {
try {
return Base64.getDecoder().decode(src.getBytes(CHARSET));
} catch (UnsupportedEncodingException e) {
throw new BusinessException("unsupported encoding " + CHARSET);
}
}
private static byte[] decryptAes(String key, byte[] secretBytes) throws Exception {
if (StringUtils.length(key) != BLOCK_SIZE) {
throw new RuntimeException("秘钥的长度必须等于 " + BLOCK_SIZE);
}
SecretKey secretKey = new SecretKeySpec(key.getBytes(CHARSET), ALGORITHM);
IvParameterSpec iv = new IvParameterSpec(IV.getBytes(CHARSET));
Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
return cipher.doFinal(secretBytes);
}
private static byte[] deleteSaltBytes(byte[] result) {
byte[] dst = new byte[result.length - BLOCK_SIZE];
System.arraycopy(result, BLOCK_SIZE, dst, 0, dst.length);
return dst;
}
}
测试代码如下所示
import org.junit.Test;
/**
* @author se7en
* @date 2021/12/4.
*/
public class AesKeyUtilTest {
@Test
public void testKey() throws Exception {
// 这里要求的密钥长度必须是 16位数
String key = "0000000000000000";
String secretText = AesKeyUtil.encrypt(key, "123456");
System.out.println(secretText);
String plainText = AesKeyUtil.decrypt(key, secretText);
System.out.println(plainText);
}
}
结果如下图所示:
FQA
为什么要加盐?
加盐的目的是混淆加密后的内容,如果没有加盐,一旦别人知道对应的算法,就可以通过撞库的方式破解,就可以知道对应的密码。
为什么加的是随机盐?
其实加固定盐也是可以,只是需要另外存储盐值,想着不如控制长度得了,所以就选择用随机盐来替代
为什么用Base64?
以AES加密为例,加密后一般为二进制字节码。密文写入数据库时可以直接写入,也可以先BASE64编码或转换为十六进制文本后再写入;而用于配置文件或在屏幕展示时,由于加密后的密文不利于展示或编辑,一般都需要先BASE64编码或转换为十六进制文本。
BASE64是一种编码技术,可以将二进制数据转换为64个可打印字符,这个转换过程没有密钥参与,因此它不是加密算法。