背景
近期在做一个对数据安全要求比较高的软件,用户要求做到对接口、文件、以及数据库部分敏感字段进行加密。由于系统中文件内容比较敏感,用户要求除了客户其他人不能查看文件具体内容,包括运维人员和开发人员。
探讨
其实文件加密并不算太复杂。无非就是在用户上传文件的时候将文件内容读出加密写入后再存到服务器,然后用户下载的时候将内容读出然后解密再写入输出流即可。
简单实现
计算机数据内容是二进制,针对二进制最简单高效的修改是进行位运算,考虑加密以及解密,可以使用异或(^)计算,具体代码如下:
public static void main(String[] args) throws Exception{
// 加密解密秘钥
private static final int secretKey = 0xff;
// 文件内容加密
try (FileInputStream in = new FileInputStream("加密原文件.md");
FileOutputStream out = new FileOutputStream("加密后文件.md")){
int dataByte;
while ((dataByte = in.read()) != -1){
out.write(dataByte ^ secretKey);
}
}
// 文件内容解密
try (FileInputStream in = new FileInputStream("加密后文件.md");
FileOutputStream out = new FileOutputStream("解密后文件.md")){
int dataByte;
while ((dataByte = in.read()) != -1){
out.write(dataByte ^ secretKey);
}
}
}
JDK文件加解密
加密涉及到大文件加密过程,不能直接使用Cipher.doFinal(byte[] bytes)方法进行直接加密,超大文件会导致内存溢出。
Java已经帮我们解决了这一问题,通过文件加密流CipherInputStream和CipherOutputStream,使用这两个加密流我们可以实现使用不同的加密算法对文件加解密。
通过CipherInputStream加解密文件
CipherInputStream加密流是对Cipher和InputStream的包装,它在读取流的时候进行加密或者解密。
public static void encryptFile(String sourceFilePath, String destFilePath, String key, int mode) throws Exception {
File sourceFile = new File(sourceFilePath);
File destFile = new File(destFilePath);
if (!sourceFile.exists() && !sourceFile.isFile()) {
throw new IllegalArgumentException("源文件不存在");
}
if (!destFile.getParentFile().exists()) {
destFile.getParentFile().mkdirs();
}
destFile.createNewFile();
InputStream in = new FileInputStream(sourceFile);
OutputStream out = new FileOutputStream(destFile);
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(mode, secretKeySpec);
// 对输入流包装
CipherInputStream cin = new CipherInputStream(in, cipher);
byte[] dataByte = new byte[1024];
int read;
while ((read= cin.read(dataByte)) != -1) {
out.write(dataByte, 0, read);
out.flush();
}
out.close();
cin.close();
in.close();
}
通过CipherOutputStream加解密文件
public static void encryptFile(String sourceFilePath, String destFilePath, String key, int mode) throws Exception {
File sourceFile = new File(sourceFilePath);
File destFile = new File(destFilePath);
if (!sourceFile.exists() && !sourceFile.isFile()) {
throw new IllegalArgumentException("源文件不存在");
}
if (!destFile.getParentFile().exists()) {
destFile.getParentFile().mkdirs();
}
destFile.createNewFile();
InputStream in = new FileInputStream(sourceFile);
OutputStream out = new FileOutputStream(destFile);
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(mode, secretKeySpec);
// 对输出流包装
CipherOutputStream cout = new CipherOutputStream(out, cipher);
byte[] dataByte = new byte[1024];
int read;
while ((read = in.read(dataByte)) != -1) {
cout.write(dataByte, 0, read);
cout.flush();
}
cout.close();
out.close();
in.close();
}
CipherInputStream加密流是对Cipher和OutputStream的包装,它在write() 方法在将数据写出到基础 OutputStream 之前先对该数据进行加密或解密。
文件加解密工具类实现
public class FileCryptoUtil {
private static final int IV_LENGTH = 16;
private static final int KEY_LENGTH = 16;
private static final int KEY_HASH_LENGTH = 32;
private FileCryptoUtil() {
}
/**
* 文件加密
*
* @param fis 原始文件读取流
* @param fos 加密文件输出流
* @param encKey 加密密钥
* @throws IOException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws InvalidAlgorithmParameterException
*/
public static void encryptFile(FileInputStream fis, FileOutputStream fos, String encKey) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
// 获取16位加密密钥
byte[] encKeyBytes = getEncKeyBytes(encKey);
// 记录输入的加密密码的消息摘要,32位
final byte[] encKeySha256 = sha256(encKeyBytes);
fos.write(encKeySha256);
// 获取系统时间作为IV
byte[] ivBytes = getRandomIv();
// 记录IV,16位
fos.write(ivBytes);
// 获取加密算法
Cipher cipher = getCipher(encKeyBytes, ivBytes, Cipher.ENCRYPT_MODE);
// 构造加密流并输出
try (CipherInputStream cis = new CipherInputStream(fis, cipher)) {
byte[] buffer = new byte[1024];
int n;
while ((n = cis.read(buffer)) != -1) {
fos.write(buffer, 0, n);
}
}
}
/**
* 文件解密
*
* @param fis 加密文件输入流
* @param fos 解密文件输出流
* @param encKey 解密密钥
* @throws IOException
* @throws InvalidKeyException
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws InvalidAlgorithmParameterException
*/
public static void decryptedFile(FileInputStream fis, FileOutputStream fos, String encKey) throws IOException, InvalidKeyException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidAlgorithmParameterException {
final byte[] encKeyBytes = getEncKeyBytes(encKey);
byte[] encKeySha256 = new byte[KEY_HASH_LENGTH];
// 读记录的文件加密密码的消息摘要,并判断是否匹配
if (fis.read(encKeySha256) != KEY_HASH_LENGTH || !Arrays.equals(sha256(encKeyBytes), encKeySha256)) {
throw new IllegalArgumentException("解密失败,解密密钥不匹配");
}
// 获取IV值
byte[] ivBytes = new byte[IV_LENGTH];
final int read = fis.read(ivBytes);
if (read != IV_LENGTH) {
throw new IllegalArgumentException("读取IV向量失败,长度不够");
}
// 获取解密算法
Cipher cipher = getCipher(encKeyBytes, ivBytes, Cipher.DECRYPT_MODE);
// 构造解密流并输出
try (CipherInputStream cis = new CipherInputStream(fis, cipher)) {
byte[] buffer = new byte[1024];
int n;
while ((n = cis.read(buffer)) != -1) {
fos.write(buffer, 0, n);
}
}
}
/**
* 获取系统时间作为IV
*
* @return
*/
private static byte[] getRandomIv() {
byte[] ivBytes = new byte[IV_LENGTH];
Random random = new Random(System.currentTimeMillis());
random.nextBytes(ivBytes);
return ivBytes;
}
/**
* 提取16位加密密钥
*
* @param encKey 加密密钥,长度不能小于16,加解密时要一致
* @return
*/
private static byte[] getEncKeyBytes(String encKey) {
if (encKey == null || encKey.length() < KEY_LENGTH) {
throw new IllegalArgumentException("encKey illegal");
}
return encKey.substring(0, KEY_LENGTH).getBytes(StandardCharsets.UTF_8);
}
/**
* 构造加密/解密算法
* <p>
* AES/CFB/PKCS5Padding 密码反馈模式
*
* @param encKeyBytes 加密密钥
* @param ivBytes 加密向量
* @param encryptMode 加密/解密
* @return
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
* @throws InvalidKeyException
* @throws InvalidAlgorithmParameterException
*/
private static Cipher getCipher(byte[] encKeyBytes, byte[] ivBytes, int encryptMode) throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException {
Cipher cipher = Cipher.getInstance("AES/CFB/PKCS5Padding");
SecretKeySpec secretKeySpec = new SecretKeySpec(encKeyBytes, "AES");
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(encryptMode, secretKeySpec, iv);
return cipher;
}
/**
* sha256摘要算法
*
* @param bytes 摘要原文
* @return 摘要结果
* @throws NoSuchAlgorithmException
*/
private static byte[] sha256(byte[] bytes) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(bytes);
}
}
参考
https://github.com/corningsun/fileEncrypt
https://www.cnblogs.com/gne-hwz/p/14736496.html