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>
<version>1.21</version>
</dependency>
<dependency>
<groupId>org.tukaani</groupId>
<artifactId>xz</artifactId>
<version>1.9</version>
</dependency>
<dependency>
<groupId>com.github.junrar</groupId>
<artifactId>junrar</artifactId>
<version>7.4.1</version>
</dependency>
<dependency>
<groupId>net.lingala.zip4j</groupId>
<artifactId>zip4j</artifactId>
<version>2.11.5</version>
</dependency>
<dependency>
<groupId>com.formdev</groupId>
<artifactId>flatlaf</artifactId>

View File

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

View File

@ -13,6 +13,9 @@ import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.*;
import net.lingala.zip4j.ZipFile;
import net.lingala.zip4j.model.FileHeader;
/**
* Service for file operations - copy, move, delete, etc.
*/
@ -1106,7 +1109,11 @@ public class FileOperations {
}
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;
while (true) {
try (org.apache.commons.compress.archivers.sevenz.SevenZFile sevenZFile =
(password == null) ? new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive) :
new org.apache.commons.compress.archivers.sevenz.SevenZFile(archive, password)) {
org.apache.commons.compress.archivers.sevenz.SevenZArchiveEntry entry;
long current = 0;
while ((entry = sevenZFile.getNextEntry()) != null) {
@ -1127,14 +1134,36 @@ public class FileOperations {
}
}
}
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 {
try (com.github.junrar.Archive archive = new com.github.junrar.Archive(archiveFile)) {
String password = null;
while (true) {
try (com.github.junrar.Archive archive = (password == null) ? new com.github.junrar.Archive(archiveFile) : new com.github.junrar.Archive(archiveFile, password)) {
if (archive.isEncrypted() && password == null) {
password = (callback != null) ? callback.requestPassword(archiveFile.getName()) : null;
if (password == null) throw new IOException("Password required");
continue;
}
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);
@ -1150,6 +1179,8 @@ public class FileOperations {
}
fh = archive.nextFileHeader();
}
if (fh == null) break; // Finished successfully
}
}
}
@ -1161,39 +1192,71 @@ public class FileOperations {
Files.createDirectories(targetDirectory.toPath());
}
try (org.apache.commons.compress.archivers.zip.ZipFile zf = new org.apache.commons.compress.archivers.zip.ZipFile(zipFile)) {
java.util.List<org.apache.commons.compress.archivers.zip.ZipArchiveEntry> entries = java.util.Collections.list(zf.getEntries());
ZipFile zf = null;
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 currentItem = 0;
for (org.apache.commons.compress.archivers.zip.ZipArchiveEntry entry : entries) {
for (FileHeader entry : entries) {
currentItem++;
File newFile = new File(targetDirectory, entry.getName());
File newFile = new File(targetDirectory, entry.getFileName());
if (callback != null) {
callback.onProgress(currentItem, totalItems, entry.getName());
callback.onProgress(currentItem, totalItems, entry.getFileName());
if (callback.isCancelled()) break;
}
try {
if (entry.isDirectory()) {
if (!newFile.isDirectory() && !newFile.mkdirs()) {
throw new IOException("Failed to create directory " + newFile);
}
setPermissionsFromMode(newFile.toPath(), entry.getUnixMode());
} else {
// create parent directories if they don't exist
File parent = newFile.getParentFile();
if (parent != null && !parent.exists()) {
parent.mkdirs();
}
if (newFile.exists() && newFile.isDirectory()) {
deleteDirectoryInternal(newFile.toPath());
zf.extractFile(entry, targetDirectory.getAbsolutePath());
}
try (InputStream is = zf.getInputStream(entry)) {
Files.copy(is, newFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
// Check if this is a password-related error on an encrypted file
if (entry.isEncrypted() && e.getMessage() != null &&
(e.getMessage().toLowerCase().contains("password") ||
e.getMessage().toLowerCase().contains("decrypt") ||
e.getMessage().toLowerCase().contains("checksum"))) {
throw new IOException("Incorrect password for encrypted archive", e);
}
setPermissionsFromMode(newFile.toPath(), entry.getUnixMode());
// For other errors, skip the file and continue
System.err.println("Warning: Failed to extract " + entry.getFileName() + ": " + e.getMessage());
}
}
} catch (Exception e) {
if (e instanceof IOException && e.getMessage() != null &&
(e.getMessage().contains("Password required") ||
e.getMessage().contains("Incorrect password"))) {
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 SymlinkResponse confirmSymlink(File symlink) { return SymlinkResponse.IGNORE; }
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;
try {
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;
} catch (Exception ex) {
// extraction failed; attempt best-effort cleanup

View File

@ -2698,6 +2698,25 @@ public class MainWindow extends JFrame {
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
public FileOperations.SymlinkResponse confirmSymlink(File symlink) {
final FileOperations.SymlinkResponse[] result = new FileOperations.SymlinkResponse[1];