added support for encrypted archives

This commit is contained in:
Radek Davidek 2026-02-04 15:06:31 +01:00
parent 6cdcb3bbfe
commit 72661040df
5 changed files with 181 additions and 60 deletions

10
pom.xml
View File

@ -91,11 +91,21 @@
<artifactId>commons-compress</artifactId> <artifactId>commons-compress</artifactId>
<version>1.21</version> <version>1.21</version>
</dependency> </dependency>
<dependency>
<groupId>org.tukaani</groupId>
<artifactId>xz</artifactId>
<version>1.9</version>
</dependency>
<dependency> <dependency>
<groupId>com.github.junrar</groupId> <groupId>com.github.junrar</groupId>
<artifactId>junrar</artifactId> <artifactId>junrar</artifactId>
<version>7.4.1</version> <version>7.4.1</version>
</dependency> </dependency>
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.11.5</version>
</dependency>
<dependency> <dependency>
<groupId>com.formdev</groupId> <groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId> <artifactId>flatlaf</artifactId>

View File

@ -15,7 +15,7 @@ import java.io.InputStreamReader;
*/ */
public class MainApp { public class MainApp {
public static final String APP_VERSION = "1.1.1"; public static final String APP_VERSION = "1.1.2";
public enum OS { public enum OS {
WINDOWS, LINUX, MACOS, UNKNOWN WINDOWS, LINUX, MACOS, UNKNOWN

View File

@ -13,6 +13,9 @@ import java.util.Set;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.zip.*; import java.util.zip.*;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.model.FileHeader;
/** /**
* Service for file operations - copy, move, delete, etc. * Service for file operations - copy, move, delete, etc.
*/ */
@ -1106,49 +1109,77 @@ public class FileOperations {
} }
private static void extractSevenZ(File archive, File targetDir, ProgressCallback callback) throws IOException { private static void extractSevenZ(File archive, File targetDir, ProgressCallback callback) throws IOException {
try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile = new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive)) { char[] password = null;
org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry; while (true) {
long current = 0; try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile =
while ((entry = sevenZFile.getNextEntry()) != null) { (password == null) ? new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive) :
current++; new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive, password)) {
if (callback != null) callback.onProgress(current, -1, entry.getName()); org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
long current = 0;
File newFile = new File(targetDir, entry.getName()); while ((entry = sevenZFile.getNextEntry()) != null) {
if (entry.isDirectory()) { current++;
newFile.mkdirs(); if (callback != null) callback.onProgress(current, -1, entry.getName());
} else {
newFile.getParentFile().mkdirs(); File newFile = new File(targetDir, entry.getName());
try (OutputStream os = Files.newOutputStream(newFile.toPath())) { if (entry.isDirectory()) {
byte[] buffer = new byte[8192]; newFile.mkdirs();
int bytesRead; } else {
while ((bytesRead = sevenZFile.read(buffer)) != -1) { newFile.getParentFile().mkdirs();
os.write(buffer, 0, bytesRead); try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
byte[] buffer = new byte[8192];
int bytesRead;
while ((bytesRead = sevenZFile.read(buffer)) != -1) {
os.write(buffer, 0, bytesRead);
}
} }
} }
} }
break;
} catch (IOException e) {
if (e.getMessage() != null && (e.getMessage().toLowerCase().contains("password") || e.getMessage().toLowerCase().contains("encrypted"))) {
String pass = (callback != null) ? callback.requestPassword(archive.getName()) : null;
if (pass == null) throw new IOException("Password required for " + archive.getName());
password = pass.toCharArray();
} else {
throw e;
}
} }
} }
} }
private static void extractRar(File archiveFile, File targetDir, ProgressCallback callback) throws Exception { private static void extractRar(File archiveFile, File targetDir, ProgressCallback callback) throws Exception {
try (com.github.junrar.Archive archive = new com.github.junrar.Archive(archiveFile)) { String password = null;
com.github.junrar.rarfile.FileHeader fh = archive.nextFileHeader(); while (true) {
long current = 0; try (com.github.junrar.Archive archive = (password == null) ? new com.github.junrar.Archive(archiveFile) : new com.github.junrar.Archive(archiveFile, password)) {
while (fh != null) { if (archive.isEncrypted() && password == null) {
current++; password = (callback != null) ? callback.requestPassword(archiveFile.getName()) : null;
String entryName = fh.getFileName().replace('\\', '/'); if (password == null) throw new IOException("Password required");
if (callback != null) callback.onProgress(current, -1, entryName); continue;
File newFile = new File(targetDir, entryName);
if (fh.isDirectory()) {
newFile.mkdirs();
} else {
newFile.getParentFile().mkdirs();
try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
archive.extractFile(fh, os);
}
} }
fh = archive.nextFileHeader(); com.github.junrar.rarfile.FileHeader fh = archive.nextFileHeader();
long current = 0;
while (fh != null) {
if (fh.isEncrypted() && password == null) {
password = (callback != null) ? callback.requestPassword(archiveFile.getName()) : null;
if (password == null) throw new IOException("Password required");
break; // exit inner loop to reopen with password
}
current++;
String entryName = fh.getFileName().replace('\\', '/');
if (callback != null) callback.onProgress(current, -1, entryName);
File newFile = new File(targetDir, entryName);
if (fh.isDirectory()) {
newFile.mkdirs();
} else {
newFile.getParentFile().mkdirs();
try (OutputStream os = Files.newOutputStream(newFile.toPath())) {
archive.extractFile(fh, os);
}
}
fh = archive.nextFileHeader();
}
if (fh == null) break; // Finished successfully
} }
} }
} }
@ -1161,39 +1192,71 @@ public class FileOperations {
Files.createDirectories(targetDirectory.toPath()); Files.createDirectories(targetDirectory.toPath());
} }
try (org.apache.commons.compress.archivers.zip.ZipFile zf = new org.apache.commons.compress.archivers.zip.ZipFile(zipFile)) { ZipFile zf = null;
java.util.List<org.apache.commons.compress.archivers.zip.ZipArchiveEntry> entries = java.util.Collections.list(zf.getEntries()); String password = null;
try {
zf = new ZipFile(zipFile);
// Check if archive is encrypted and request password
if (zf.isEncrypted()) {
password = (callback != null) ? callback.requestPassword(zipFile.getName()) : null;
if (password == null) {
throw new IOException("Password required for " + zipFile.getName());
}
zf.setPassword(password.toCharArray());
}
List<FileHeader> entries = zf.getFileHeaders();
long totalItems = entries.size(); long totalItems = entries.size();
long currentItem = 0; long currentItem = 0;
for (org.apache.commons.compress.archivers.zip.ZipArchiveEntry entry : entries) { for (FileHeader entry : entries) {
currentItem++; currentItem++;
File newFile = new File(targetDirectory, entry.getName()); File newFile = new File(targetDirectory, entry.getFileName());
if (callback != null) { if (callback != null) {
callback.onProgress(currentItem, totalItems, entry.getName()); callback.onProgress(currentItem, totalItems, entry.getFileName());
if (callback.isCancelled()) break;
} }
if (entry.isDirectory()) { try {
if (!newFile.isDirectory() && !newFile.mkdirs()) { if (entry.isDirectory()) {
throw new IOException("Failed to create directory " + newFile); if (!newFile.isDirectory() && !newFile.mkdirs()) {
throw new IOException("Failed to create directory " + newFile);
}
} else {
File parent = newFile.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
zf.extractFile(entry, targetDirectory.getAbsolutePath());
} }
setPermissionsFromMode(newFile.toPath(), entry.getUnixMode()); } catch (Exception e) {
} else { // Check if this is a password-related error on an encrypted file
// create parent directories if they don't exist if (entry.isEncrypted() && e.getMessage() != null &&
File parent = newFile.getParentFile(); (e.getMessage().toLowerCase().contains("password") ||
if (parent != null && !parent.exists()) { e.getMessage().toLowerCase().contains("decrypt") ||
parent.mkdirs(); e.getMessage().toLowerCase().contains("checksum"))) {
throw new IOException("Incorrect password for encrypted archive", e);
} }
// For other errors, skip the file and continue
if (newFile.exists() && newFile.isDirectory()) { System.err.println("Warning: Failed to extract " + entry.getFileName() + ": " + e.getMessage());
deleteDirectoryInternal(newFile.toPath()); }
} }
} catch (Exception e) {
try (InputStream is = zf.getInputStream(entry)) { if (e instanceof IOException && e.getMessage() != null &&
Files.copy(is, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING); (e.getMessage().contains("Password required") ||
} e.getMessage().contains("Incorrect password"))) {
setPermissionsFromMode(newFile.toPath(), entry.getUnixMode()); throw (IOException) e;
}
throw new IOException("Failed to unzip: " + e.getMessage(), e);
} finally {
if (zf != null) {
try {
zf.close();
} catch (Exception e) {
// Ignore errors during close
} }
} }
} }
@ -1223,6 +1286,7 @@ public class FileOperations {
default OverwriteResponse confirmOverwrite(File source, File destination) { return OverwriteResponse.YES; } default OverwriteResponse confirmOverwrite(File source, File destination) { return OverwriteResponse.YES; }
default SymlinkResponse confirmSymlink(File symlink) { return SymlinkResponse.IGNORE; } default SymlinkResponse confirmSymlink(File symlink) { return SymlinkResponse.IGNORE; }
default ErrorResponse onError(File file, Exception e) { return ErrorResponse.ABORT; } default ErrorResponse onError(File file, Exception e) { return ErrorResponse.ABORT; }
default String requestPassword(String archiveName) { return null; }
} }
/** /**

View File

@ -1310,7 +1310,35 @@ public class FilePanelTab extends JPanel {
if (archive == null || !archive.isFile()) return null; if (archive == null || !archive.isFile()) return null;
try { try {
Path tempDir = Files.createTempDirectory("kfmanager-archive-"); Path tempDir = Files.createTempDirectory("kfmanager-archive-");
FileOperations.extractArchive(archive, tempDir.toFile(), null); FileOperations.extractArchive(archive, tempDir.toFile(), new FileOperations.ProgressCallback() {
@Override
public void onProgress(long current, long total, String currentFile) {}
@Override
public String requestPassword(String archiveName) {
final String[] result = new String[1];
try {
if (SwingUtilities.isEventDispatchThread()) {
JPasswordField pf = new JPasswordField();
int ok = JOptionPane.showConfirmDialog(FilePanelTab.this, pf, "Enter password for " + archiveName, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
if (ok == JOptionPane.OK_OPTION) {
result[0] = new String(pf.getPassword());
}
} else {
SwingUtilities.invokeAndWait(() -> {
JPasswordField pf = new JPasswordField();
int ok = JOptionPane.showConfirmDialog(FilePanelTab.this, pf, "Enter password for " + archiveName, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
if (ok == JOptionPane.OK_OPTION) {
result[0] = new String(pf.getPassword());
}
});
}
} catch (Exception e) {
result[0] = null;
}
return result[0];
}
});
return tempDir; return tempDir;
} catch (Exception ex) { } catch (Exception ex) {
// extraction failed; attempt best-effort cleanup // extraction failed; attempt best-effort cleanup

View File

@ -2698,6 +2698,25 @@ public class MainWindow extends JFrame {
return result[0]; return result[0];
} }
@Override
public String requestPassword(String archiveName) {
final String[] result = new String[1];
try {
SwingUtilities.invokeAndWait(() -> {
JPasswordField pf = new JPasswordField();
int ok = JOptionPane.showConfirmDialog(progressDialog, pf, "Enter password for " + archiveName, JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE);
if (ok == JOptionPane.OK_OPTION) {
result[0] = new String(pf.getPassword());
} else {
result[0] = null;
}
});
} catch (Exception e) {
result[0] = null;
}
return result[0];
}
@Override @Override
public FileOperations.SymlinkResponse confirmSymlink(File symlink) { public FileOperations.SymlinkResponse confirmSymlink(File symlink) {
final FileOperations.SymlinkResponse[] result = new FileOperations.SymlinkResponse[1]; final FileOperations.SymlinkResponse[] result = new FileOperations.SymlinkResponse[1];