Skip to content

Commit 20fd427

Browse files
committed
fix: 补修遗留 high 与精选 medium(FTP/7z/AES/ZIP/图片流)
- FtpContext: 静态共享 FTPClient 改为每次调用独立连接;readFile 按文件独立缓冲 (修复跨文件内容累积);retrieveFileStream null 防护;downFile 创建正确目录并传播结果 - FileCompressionFor7z: extractSlow 按条目只开一次输出流(此前每个 chunk 重开导致只留最后一块), Zip Slip 校验与父目录创建移到解压前 - FileEncryptionForAES: CBC 固定全零 IV 改随机 IV 前置于密文,解密读取 IV,校验密钥长度 - ZipToolsForApache: try-with-resources、保留目录层级避免重复条目、排除目标 zip 自包含 - JavaApiFileCompressionForZIP: 条目名保留相对路径用 '/'、排除目标 zip 自包含 - ImageFormatConversion: 流 try-with-resources、只取首个 reader 并 dispose - SFTPContext: 连接失败抛出真实异常、无父目录时不再 NPE
1 parent 55d609d commit 20fd427

7 files changed

Lines changed: 192 additions & 192 deletions

File tree

‎src/main/java/com/jon/file/compression/FileCompressionFor7z.java‎

Lines changed: 32 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -98,32 +98,40 @@ public static void unzipFor7z(String filePath,final String newFilePath){
9898
if (!item.isFolder()) {
9999
ExtractOperationResult result;
100100
final long[] sizeArray = new long[1];
101-
result = item.extractSlow(data -> {
102-
// Write to file
103-
try {
104-
// Zip Slip 防护:归档内路径规范化后必须仍落在目标目录内
105-
File targetDir = new File(newFilePath).getCanonicalFile();
106-
File file = new File(targetDir, item.getPath()).getCanonicalFile();
107-
if (!file.getPath().startsWith(targetDir.getPath() + File.separator)) {
108-
throw new IOException("非法归档条目路径: " + item.getPath());
109-
}
110-
// 嵌套目录需要先创建父目录
111-
File parent = file.getParentFile();
112-
if (parent != null && !parent.exists()) {
113-
parent.mkdirs();
114-
}
115-
try (FileOutputStream fos = new FileOutputStream(file)) {
101+
// Zip Slip 防护 + 目录创建必须在 extractSlow 之前完成
102+
File targetFile = null;
103+
try {
104+
File targetDir = new File(newFilePath).getCanonicalFile();
105+
targetFile = new File(targetDir, item.getPath()).getCanonicalFile();
106+
if (!targetFile.getPath().startsWith(targetDir.getPath() + File.separator)) {
107+
throw new IOException("非法归档条目路径: " + item.getPath());
108+
}
109+
File parent = targetFile.getParentFile();
110+
if (parent != null && !parent.exists()) {
111+
parent.mkdirs();
112+
}
113+
} catch (IOException e) {
114+
System.err.println("跳过条目 " + item.getPath() + ": " + e.getMessage());
115+
targetFile = null;
116+
}
117+
if (targetFile == null) {
118+
continue;
119+
}
120+
// 整个条目只开一个输出流:extractSlow 会按 chunk 多次回调,每次重开(截断)只会留下最后一块
121+
try (FileOutputStream fos = new FileOutputStream(targetFile)) {
122+
result = item.extractSlow(data -> {
123+
try {
116124
fos.write(data);
125+
} catch (IOException e) {
126+
e.printStackTrace();
117127
}
118-
} catch (IOException e) {
119-
e.printStackTrace();
120-
}
121-
// Consume data
122-
hash[0] ^= Arrays.hashCode(data);
123-
sizeArray[0] += data.length;
124-
//Return amount of consumed data
125-
return data.length;
126-
});
128+
// Consume data
129+
hash[0] ^= Arrays.hashCode(data);
130+
sizeArray[0] += data.length;
131+
//Return amount of consumed data
132+
return data.length;
133+
});
134+
}
127135
if (result == ExtractOperationResult.OK) {
128136
System.out.println(String.format("%9X | %10s | %s",
129137
hash[0], sizeArray[0], item.getPath()));

‎src/main/java/com/jon/file/compression/JavaApiFileCompressionForZIP.java‎

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,13 @@
1717
*/
1818
public class JavaApiFileCompressionForZIP {
1919

20+
/** 正在写入的目标 zip,用于排除"压缩目录里包含目标 zip 自身"的自包含 */
21+
private File currentZipFile;
22+
2023
public File FileCompressionForZIP(String dataSource, String zipFilePath) throws IOException {
2124
File file = new File(dataSource);
2225
File zipFile = new File(zipFilePath);
26+
currentZipFile = zipFile;
2327
try (OutputStream out = new FileOutputStream(zipFile); BufferedOutputStream buf = new BufferedOutputStream(out);
2428
// jdk1.7支持
2529
ZipOutputStream zipOut = new ZipOutputStream(buf, StandardCharsets.UTF_8)) {
@@ -59,12 +63,17 @@ private void zipFile(File file, String basePath, ZipOutputStream zipOut) throws
5963
zipOut.putNextEntry(new ZipEntry(pathName));
6064
zipFile(newFile, basePath, zipOut);
6165
} else {
66+
// 目标 zip 位于待压缩目录内时跳过,避免边写边读自身
67+
if (currentZipFile != null && newFile.getCanonicalPath().equals(currentZipFile.getCanonicalPath())) {
68+
continue;
69+
}
6270
pathName = newFile.getPath();
6371
try (InputStream is = new FileInputStream(pathName);
6472
BufferedInputStream buffer = new BufferedInputStream(is)) {
65-
// zipOut.putNextEntry(new ZipEntry(pathName));带目录压缩
66-
// 不带目录压缩
67-
zipOut.putNextEntry(new ZipEntry(newFile.getName()));
73+
// 条目名保留相对目录结构并使用 '/',否则不同目录同名文件会互相覆盖
74+
String entryName = newFile.getPath().substring(basePath.length() + 1)
75+
.replace(File.separatorChar, '/');
76+
zipOut.putNextEntry(new ZipEntry(entryName));
6877
// zipOut.setLevel(9);设置zip压缩级别 0~9
6978
while ((length = buffer.read(buff)) != -1) {
7079
zipOut.write(buff, 0, length);

‎src/main/java/com/jon/file/compression/ZipToolsForApache.java‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@ public void fileCompress(String srcPathName) {
2525
if (!file.exists()) {
2626
throw new RuntimeException(srcPathName + "不存在");
2727
}
28-
try {
29-
FileOutputStream fileOut = new FileOutputStream(zipFile);
28+
try (FileOutputStream fileOut = new FileOutputStream(zipFile);
3029
CheckedOutputStream checkOut = new CheckedOutputStream(fileOut, new CRC32());
31-
ZipOutputStream zipOut = new ZipOutputStream(checkOut);
30+
ZipOutputStream zipOut = new ZipOutputStream(checkOut)) {
3231
String baseDir = "";
3332
fileCompress(file, zipOut, baseDir);
34-
zipOut.close();
33+
zipOut.finish();
3534
} catch (Exception e) {
3635
e.printStackTrace();
3736
}
@@ -55,11 +54,9 @@ private void compressDirectory(File file, ZipOutputStream zipOut, String baseDir
5554
}
5655
File[] files = file.listFiles();
5756
if (files != null && files.length > 0) {
58-
// 递归
57+
// 递归,保留目录层级:否则不同子目录下的同名文件会产生重复的 ZipEntry
5958
for (int i = 0; i < files.length; i++) {
60-
// fileCompress(files[i],zipOut,baseDir+file.getName()+File.separator);带目录压缩
61-
// 不带目录压缩
62-
fileCompress(files[i], zipOut, baseDir);
59+
fileCompress(files[i], zipOut, baseDir + file.getName() + "/");
6360
}
6461
}
6562
}
@@ -72,15 +69,20 @@ private void compressFile(File file, ZipOutputStream zipOut, String baseDir) {
7269
return;
7370
}
7471
try {
75-
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
72+
// 目标 zip 本身位于待压缩目录内时跳过,否则会把正在写的归档读进来
73+
if (file.getCanonicalPath().equals(zipFile.getCanonicalPath())) {
74+
return;
75+
}
76+
} catch (Exception ignored) {
77+
}
78+
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file))) {
7679
ZipEntry zip = new ZipEntry(baseDir + file.getName());
7780
zipOut.putNextEntry(zip);
7881
int count;
7982
byte[] data = new byte[BUFFER];
8083
while ((count = bis.read(data, 0, BUFFER)) != -1) {
8184
zipOut.write(data, 0, count);
8285
}
83-
bis.close();
8486
} catch (Exception e) {
8587
e.printStackTrace();
8688
}

‎src/main/java/com/jon/file/encryption/FileEncryptionForAES.java‎

Lines changed: 35 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,62 +3,67 @@
33
import java.io.ByteArrayOutputStream;
44
import java.io.FileInputStream;
55
import java.io.FileOutputStream;
6+
import java.security.SecureRandom;
67
import java.util.Arrays;
78
import javax.crypto.Cipher;
89
import javax.crypto.SecretKey;
910
import javax.crypto.spec.IvParameterSpec;
1011
import javax.crypto.spec.SecretKeySpec;
1112

1213
/**
13-
* AES文件加密
14+
* AES文件加密<br>
15+
* 每次加密生成随机 IV 并置于密文头部(CBC 固定 IV 会导致确定性加密,泄露明文相等性)
1416
*/
1517
public class FileEncryptionForAES {
1618

17-
/**
18-
* AES加密
19-
*/
20-
public static byte[] encodeAES(byte[] key, byte[] content) throws Exception {
21-
// 不是16的倍数的,补足
22-
int base = 16;
23-
if (key.length % base != 0) {
24-
int groups = key.length / base + (key.length % base != 0 ? 1 : 0);
25-
byte[] temp = new byte[groups * base];
26-
Arrays.fill(temp, (byte)0);
19+
private static final int BLOCK_SIZE = 16;
20+
21+
private static SecretKey normalizeKey(byte[] key) {
22+
if (key == null) {
23+
throw new IllegalArgumentException("key 不能为空");
24+
}
25+
if (key.length % BLOCK_SIZE != 0) {
26+
int groups = key.length / BLOCK_SIZE + 1;
27+
byte[] temp = new byte[groups * BLOCK_SIZE];
2728
System.arraycopy(key, 0, temp, 0, key.length);
2829
key = temp;
2930
}
31+
if (key.length != 16 && key.length != 24 && key.length != 32) {
32+
throw new IllegalArgumentException("AES 密钥长度必须是 16/24/32 字节(补零后为 " + key.length + ")");
33+
}
34+
return new SecretKeySpec(key, "AES");
35+
}
3036

31-
SecretKey secretKey = new SecretKeySpec(key, "AES");
32-
IvParameterSpec iv = new IvParameterSpec(new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
37+
/**
38+
* AES加密,输出 = 16 字节随机 IV + 密文
39+
*/
40+
public static byte[] encodeAES(byte[] key, byte[] content) throws Exception {
41+
SecretKey secretKey = normalizeKey(key);
42+
byte[] iv = new byte[BLOCK_SIZE];
43+
new SecureRandom().nextBytes(iv);
3344
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
34-
cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
35-
byte[] tgtBytes = cipher.doFinal(content);
36-
return tgtBytes;
45+
cipher.init(Cipher.ENCRYPT_MODE, secretKey, new IvParameterSpec(iv));
46+
byte[] cipherText = cipher.doFinal(content);
47+
byte[] result = Arrays.copyOf(iv, BLOCK_SIZE + cipherText.length);
48+
System.arraycopy(cipherText, 0, result, BLOCK_SIZE, cipherText.length);
49+
return result;
3750
}
3851

3952
/**
40-
* AES解密
53+
* AES解密,密文前 16 字节为 IV
4154
*
4255
* @param key 密钥信息
4356
* @param content 待加密信息
4457
*/
4558
public static byte[] decodeAES(byte[] key, byte[] content) throws Exception {
46-
// 不是16的倍数的,补足
47-
int base = 16;
48-
if (key.length % base != 0) {
49-
int groups = key.length / base + (key.length % base != 0 ? 1 : 0);
50-
byte[] temp = new byte[groups * base];
51-
Arrays.fill(temp, (byte)0);
52-
System.arraycopy(key, 0, temp, 0, key.length);
53-
key = temp;
59+
if (content == null || content.length <= BLOCK_SIZE) {
60+
throw new IllegalArgumentException("密文长度非法");
5461
}
55-
56-
SecretKey secretKey = new SecretKeySpec(key, "AES");
57-
IvParameterSpec iv = new IvParameterSpec(new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0});
62+
SecretKey secretKey = normalizeKey(key);
63+
IvParameterSpec iv = new IvParameterSpec(content, 0, BLOCK_SIZE);
5864
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
5965
cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
60-
byte[] tgtBytes = cipher.doFinal(content);
61-
return tgtBytes;
66+
return cipher.doFinal(content, BLOCK_SIZE, content.length - BLOCK_SIZE);
6267
}
6368

6469
/**

0 commit comments

Comments
 (0)