fix(access): support encrypted database files

This commit is contained in:
t8y2 2026-07-11 10:44:20 +08:00
parent a2fd38d542
commit 28ee0ecc8c
5 changed files with 71 additions and 2 deletions

View File

@ -1,5 +1,6 @@
dependencies {
implementation 'io.github.spannm:ucanaccess:5.1.5'
implementation 'io.github.spannm:jackcess-encrypt:5.1.0'
}
tasks.named('shadowJar') {

View File

@ -30,6 +30,7 @@ import java.util.Map;
import java.util.Set;
public final class AccessAgent extends BaseDatabaseAgent {
private static final String ENCRYPTED_ACCESS_OPENER = EncryptedAccessOpener.class.getName();
private Connection connection;
private String databaseFile = "";
@ -285,6 +286,11 @@ public final class AccessAgent extends BaseDatabaseAgent {
}
rawUrl = appendUCanAccessParams(rawUrl, params.getUrl_params());
// The codec opener is safe for regular files and enables encrypted Access files when a password is supplied.
if (!containsIgnoreCase(rawUrl, "jackcessOpener=")) {
rawUrl += (rawUrl.endsWith(";") ? "" : ";") + "jackcessOpener=" + ENCRYPTED_ACCESS_OPENER;
}
if (!createIfMissing || Files.exists(Path.of(databasePath(params)))) {
return rawUrl;
}

View File

@ -0,0 +1,20 @@
package com.dbx.agent.access;
import io.github.spannm.jackcess.Database;
import io.github.spannm.jackcess.DatabaseBuilder;
import io.github.spannm.jackcess.encrypt.CryptCodecProvider;
import java.io.File;
import java.io.IOException;
import net.ucanaccess.jdbc.IJackcessOpenerInterface;
public final class EncryptedAccessOpener implements IJackcessOpenerInterface {
@Override
public Database open(File file, String password) throws IOException {
DatabaseBuilder builder = new DatabaseBuilder(file);
// UCanAccess flushes changes at transaction boundaries, so Jackcess auto-sync is unnecessary overhead.
builder.withAutoSync(false);
builder.withCodecProvider(new CryptCodecProvider(password));
builder.withReadOnly(false);
return builder.open();
}
}

View File

@ -6,6 +6,7 @@ import com.dbx.agent.ExecuteQueryOptions;
import com.dbx.agent.QueryPageOptions;
import com.dbx.agent.test.JdbcConnectedAgentTest;
import java.nio.file.Path;
import java.nio.file.Files;
import java.util.List;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@ -66,7 +67,7 @@ class AccessAgentTest extends JdbcConnectedAgentTest {
ConnectParams params = new ConnectParams("", 0, file.toString(), "", "", "memory=false;ignoreCase=true", "", false);
Assertions.assertEquals(
"jdbc:ucanaccess://" + file + ";memory=false;ignoreCase=true;newDatabaseVersion=V2010",
"jdbc:ucanaccess://" + file + ";memory=false;ignoreCase=true;jackcessOpener=com.dbx.agent.access.EncryptedAccessOpener;newDatabaseVersion=V2010",
AccessAgent.jdbcUrl(params, true)
);
}
@ -77,11 +78,52 @@ class AccessAgentTest extends JdbcConnectedAgentTest {
ConnectParams params = new ConnectParams("", 0, "jdbc:ucanaccess://" + file + ";", "", "", ";memory=false", "", false);
Assertions.assertEquals(
"jdbc:ucanaccess://" + file + ";memory=false;newDatabaseVersion=V2010",
"jdbc:ucanaccess://" + file + ";memory=false;jackcessOpener=com.dbx.agent.access.EncryptedAccessOpener;newDatabaseVersion=V2010",
AccessAgent.jdbcUrl(params, true)
);
}
@Test
void preservesCustomJackcessOpener() {
Path file = tempDir.resolve("custom-opener.accdb");
ConnectParams params = new ConnectParams(
"", 0, file.toString(), "", "", "jackcessOpener=example.CustomOpener", "", false
);
Assertions.assertEquals(
"jdbc:ucanaccess://" + file + ";jackcessOpener=example.CustomOpener;newDatabaseVersion=V2010",
AccessAgent.jdbcUrl(params, true)
);
}
@Test
void connectsToEncryptedAccessFile() throws Exception {
Path source = Path.of(getClass().getResource("/db2007-enc.accdb").toURI());
Path file = tempDir.resolve("encrypted.accdb");
Files.copy(source, file);
AccessAgent agent = new AccessAgent();
try {
agent.connect(new ConnectParams("", 0, file.toString(), "", "Test123", "", "", false));
Assertions.assertFalse(agent.listTables("").isEmpty());
} finally {
agent.disconnect();
}
}
@Test
void rejectsIncorrectEncryptedAccessPassword() throws Exception {
Path source = Path.of(getClass().getResource("/db2007-enc.accdb").toURI());
Path file = tempDir.resolve("encrypted-wrong-password.accdb");
Files.copy(source, file);
AccessAgent agent = new AccessAgent();
RuntimeException error = Assertions.assertThrows(RuntimeException.class, () ->
agent.connect(new ConnectParams("", 0, file.toString(), "", "wrong", "", "", false))
);
Assertions.assertTrue(error.getMessage().contains("Incorrect password provided"));
}
@Test
void supportsLimitOffsetSqlAndCursorResultPages() {
withAgent("pages", agent -> {