diff --git a/build.gradle b/build.gradle index e310bacb..a6c97ef3 100644 --- a/build.gradle +++ b/build.gradle @@ -50,10 +50,10 @@ dependencies { implementation('com.zaxxer:HikariCP:7.0.2') { exclude group: 'org.slf4j' } - implementation('org.jdbi:jdbi3-core:3.49.5') { + implementation('org.jdbi:jdbi3-core:3.51.0') { exclude group: 'org.slf4j' } - implementation('org.jdbi:jdbi3-sqlobject:3.49.5') { + implementation('org.jdbi:jdbi3-sqlobject:3.51.0') { exclude group: 'org.slf4j' } implementation('org.flywaydb:flyway-core:9.22.3') diff --git a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java index 35674916..ca42ee04 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/db/DbPersistence.java @@ -83,6 +83,7 @@ public class DbPersistence implements Persistence { ECKey encryptionKey = getEncryptionKey(password, storage.getWalletFile(), alreadyDerivedKey); migrate(storage, MASTER_SCHEMA, encryptionKey); + validateSchema(storage, MASTER_SCHEMA, encryptionKey); Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey)); masterWallet = jdbi.withHandle(handle -> { @@ -112,6 +113,7 @@ public class DbPersistence implements Persistence { Map childWallets = new TreeMap<>(); for(String schema : childSchemas) { migrate(storage, schema, encryptionKey); + validateSchema(storage, schema, encryptionKey); Jdbi childJdbi = getJdbi(storage, getFilePassword(encryptionKey)); Wallet wallet = childJdbi.withHandle(handle -> { @@ -409,6 +411,52 @@ public class DbPersistence implements Persistence { } } + private void validateSchema(Storage storage, String schema, ECKey encryptionKey) throws StorageException { + Jdbi jdbi = getJdbi(storage, getFilePassword(encryptionKey)); + try { + jdbi.useHandle(handle -> { + List routines = handle.createQuery("SELECT ROUTINE_NAME FROM INFORMATION_SCHEMA.ROUTINES WHERE ROUTINE_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list(); + if(!routines.isEmpty()) { + throw new RuntimeException(new StorageException("Wallet file contains unexpected database routines: " + String.join(", ", routines) + ".")); + } + + List triggers = handle.createQuery("SELECT TRIGGER_NAME FROM INFORMATION_SCHEMA.TRIGGERS WHERE TRIGGER_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list(); + if(!triggers.isEmpty()) { + throw new RuntimeException(new StorageException("Wallet file contains unexpected database triggers: " + String.join(", ", triggers) + ".")); + } + + List checkConstraints = handle.createQuery("SELECT CHECK_CLAUSE FROM INFORMATION_SCHEMA.CHECK_CONSTRAINTS WHERE UPPER(CONSTRAINT_SCHEMA) = UPPER(:schema)") + .bind("schema", schema).mapTo(String.class).list(); + if(!checkConstraints.isEmpty()) { + throw new RuntimeException(new StorageException("Wallet file contains unexpected check constraints: " + String.join(", ", checkConstraints) + ".")); + } + + List> nonBaseTables = handle.createQuery("SELECT TABLE_NAME, TABLE_TYPE FROM INFORMATION_SCHEMA.TABLES WHERE UPPER(TABLE_SCHEMA) = UPPER(:schema) " + + "AND TABLE_TYPE <> 'BASE TABLE' AND UPPER(TABLE_NAME) <> 'FLYWAY_SCHEMA_HISTORY'").bind("schema", schema).mapToMap().list(); + if(!nonBaseTables.isEmpty()) { + String detail = nonBaseTables.stream().map(m -> m.get("TABLE_NAME") + " (" + m.get("TABLE_TYPE") + ")").collect(Collectors.joining(", ")); + throw new RuntimeException(new StorageException("Wallet file contains unexpected database object types: " + detail + ".")); + } + + List generatedColumns = handle.createQuery("SELECT TABLE_NAME || '.' || COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE UPPER(TABLE_SCHEMA) = UPPER(:schema) AND GENERATION_EXPRESSION IS NOT NULL") + .bind("schema", schema).mapTo(String.class).list(); + if(!generatedColumns.isEmpty()) { + throw new RuntimeException(new StorageException("Wallet file contains unexpected generated columns: " + String.join(", ", generatedColumns) + ".")); + } + + List domains = handle.createQuery("SELECT DOMAIN_NAME FROM INFORMATION_SCHEMA.DOMAINS WHERE DOMAIN_SCHEMA <> 'INFORMATION_SCHEMA'").mapTo(String.class).list(); + if(!domains.isEmpty()) { + throw new RuntimeException(new StorageException("Wallet file contains unexpected database domains: " + String.join(", ", domains) + ".")); + } + }); + } catch(RuntimeException e) { + if(e.getCause() instanceof StorageException) { + throw new StorageException("This is not a valid wallet file.\n\n" + e.getCause().getMessage()); + } + throw e; + } + } + private void cleanAndMigrate(Storage storage, String schema, String password) throws StorageException { File migrationDir = getMigrationDir(); try { @@ -698,8 +746,8 @@ public class DbPersistence implements Persistence { } private String getUrl(File walletFile, String password) throws StorageException { - if(JDBC_URL_INJECTION_PATTERN.matcher(walletFile.getName()).find()) { - throw new StorageException("Wallet file name contains invalid characters"); + if(JDBC_URL_INJECTION_PATTERN.matcher(walletFile.getAbsolutePath()).find()) { + throw new StorageException("Wallet file path contains invalid characters"); } return "jdbc:h2:" + walletFile.getAbsolutePath().replace("." + getType().getExtension(), "") + ";INIT=SET TRACE_LEVEL_FILE=4;TRACE_LEVEL_FILE=4;DEFRAG_ALWAYS=true;MAX_COMPACT_TIME=5000;DATABASE_TO_UPPER=false" + (password == null ? "" : ";CIPHER=AES"); }