| /* |
| * |
| * Derby - Class org.apache.derbyTesting.functionTests.tests.store.EncryptionKeyTest |
| * |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, |
| * either express or implied. See the License for the specific |
| * language governing permissions and limitations under the License. |
| */ |
| package org.apache.derbyTesting.functionTests.tests.store; |
| |
| import java.io.File; |
| |
| import java.sql.Connection; |
| import java.sql.CallableStatement; |
| import java.sql.PreparedStatement; |
| import java.sql.ResultSet; |
| import java.sql.Statement; |
| import java.sql.SQLException; |
| import java.sql.SQLWarning; |
| |
| import javax.sql.DataSource; |
| |
| import org.apache.derbyTesting.functionTests.util.PrivilegedFileOpsForTests; |
| |
| import org.apache.derbyTesting.junit.BaseJDBCTestCase; |
| import org.apache.derbyTesting.junit.JDBCDataSource; |
| import org.apache.derbyTesting.junit.SupportFilesSetup; |
| |
| /** |
| * Tests various connection sequences to encrypted databases. |
| * Four kinds of external keys are used: |
| * <ol><li>the correct key |
| * <li>an incorrect key |
| * <li>a key with odd length (in hex representation) |
| * <li>a key containing invalid characters |
| * </ol> |
| * <p> |
| * The algorithms used for en-/decryption is determined by the subclasses, |
| * where a single algorithm and a set of keys are associated with each |
| * subclass. |
| * <p> |
| * Databases are created in the <tt>EXTINOUT</tt> directory. Backups are made |
| * to <tt>EXTINOUT/backups</tt> and restored databases are put into |
| * <tt>EXTINOUT/restored</tt> <b>if and only if</b> the databases need to be |
| * both written and read. Otherwise backups are placed in <tt>EXTOUT</tt>. |
| * Testsuites generated from this class must be wrapped in a |
| * <code>SupportFileSetup</code> decorator. |
| */ |
| //@NotThreadSafe |
| public abstract class EncryptionKeyTest |
| extends BaseJDBCTestCase { |
| |
| /** Correct key constant. */ |
| protected static final int CORRECT_KEY = 0; |
| /** Wrong key constant. */ |
| protected static final int WRONG_KEY = 1; |
| /** Odd length key constant. */ |
| protected static final int ODD_LENGTH_KEY = 2; |
| /** Invalid char key constant. */ |
| protected static final int INVALID_CHAR_KEY = 3; |
| |
| /** Table holding the test data. */ |
| private static final String TABLE = "encryptionkeytestdata"; |
| /** Test data inserted into database and used for verification. */ |
| private static final int[] DATA = {9,4,2,34,6543,3,123,434,5436,-123,0,123}; |
| |
| /** The algorithm used by the fixture. */ |
| private final String algorithm; |
| |
| /** The correct key. */ |
| private final String keyCorrect; |
| /** An incorrect key. */ |
| private final String keyWrong; |
| /** A key with odd length. */ |
| private final String keyOddLength; |
| /** A key with an invalid char in it. */ |
| private final String keyInvalidChar; |
| |
| /** |
| * Variable to hold the various connections. |
| * No guarantee is made about the state of this connection, but it is |
| * closed at tear-down. |
| */ |
| private Connection con = null; |
| |
| /** |
| * Configures a new setup by specifying the encryption properties. |
| * |
| * @param name name of the fixture |
| * @param algorithm encryption algorithm to use |
| * @param correctKey the correct encryption key |
| * @param wrongKey an incorrect encryption key |
| * @param oddLengthKey a key of odd length |
| * @param invalidCharKey a key with invalid characters |
| */ |
| public EncryptionKeyTest(String name, |
| String algorithm, |
| String correctKey, |
| String wrongKey, |
| String oddLengthKey, |
| String invalidCharKey) { |
| super(name); |
| this.algorithm = algorithm; |
| this.keyCorrect = correctKey; |
| this.keyWrong = wrongKey; |
| this.keyOddLength = oddLengthKey; |
| this.keyInvalidChar = invalidCharKey; |
| } |
| |
| /** |
| * Clean up the connection maintained by this test. |
| */ |
| protected void tearDown() |
| throws java.lang.Exception { |
| if (con != null && !con.isClosed()) { |
| con.rollback(); |
| con.close(); |
| } |
| con = null; |
| super.tearDown(); |
| } |
| |
| /** |
| * Test a sequence of connections and connection attempts. |
| * Sequence: Create database, connect to database using correct key, |
| * try to connect using incorrect key, connect using correct key. |
| */ |
| public void testConnectionSequence1() |
| throws SQLException { |
| String dbName = "encryptedDB_ConnectionSequence1"; |
| // Create database. |
| con = createAndPopulateDB(dbName); |
| validateDBContents(con); |
| // Shutdown the database. |
| con.close(); |
| shutdown(dbName); |
| // Connect using correct key. |
| con = getConnection(dbName, CORRECT_KEY); |
| validateDBContents(con); |
| con.close(); |
| // Shutdown the database. |
| shutdown(dbName); |
| // Try to connect using wrong key. |
| try { |
| getConnection(dbName, WRONG_KEY); |
| fail("Booting with an incorrect encryption key should fail."); |
| } catch (SQLException sqle) { |
| assertSQLState("XJ040", sqle); |
| assertSQLState("XBCXK", getLastSQLException(sqle)); |
| } |
| // Connect using correct key. |
| con = getConnection(dbName, CORRECT_KEY); |
| validateDBContents(con); |
| con.close(); |
| // Shutdown the database. |
| shutdown(dbName); |
| } |
| |
| /** |
| * Test a sequence of connections and connection attempts. |
| * Sequence: Create database, connect to database using odd length key, |
| * try to connect using incorrect key, connect using correct key. |
| */ |
| public void testConnectionSequence2() |
| throws SQLException { |
| String dbName = "encryptedDB_ConnectionSequence2"; |
| // Create database. |
| con = createAndPopulateDB(dbName); |
| validateDBContents(con); |
| // Shutdown the database. |
| con.close(); |
| shutdown(dbName); |
| // Connect using odd length key. |
| try { |
| con = getConnection(dbName, ODD_LENGTH_KEY); |
| fail("Connected with an odd length key."); |
| } catch (SQLException sqle) { |
| assertSQLState("XJ040", sqle); |
| SQLException lastSQLE = getLastSQLException(sqle); |
| String sqlState = lastSQLE.getSQLState(); |
| // The state of this exception varies with the security provider |
| // the test is run with. |
| // Briefly stated, the deciding factor is whether the error is |
| // caught by checks in the Derby code, or by the checks in the |
| // security provider. For instance, the (current Sun JCE) DES |
| // key implementation does not verify the key length, whereas the |
| // AES key implementation does. For other providers, the situation |
| // might be different. |
| // XBCX0 : A general crypto exception, wraps the exception from the |
| // security provider. |
| // XBCXM : A specific Derby exception for external keys of invalid |
| // lengths. |
| if (!sqlState.equals("XBCX0") && !sqlState.equals("XBCXM")) { |
| throw lastSQLE; |
| } |
| } |
| confirmNonBootedDB(dbName); |
| // Try to connect using wrong key. |
| try { |
| getConnection(dbName, WRONG_KEY); |
| fail("Booting with an incorrect encryption key should fail."); |
| } catch (SQLException sqle) { |
| assertSQLState("XJ040", sqle); |
| assertSQLState("XBCXK", getLastSQLException(sqle)); |
| } |
| // Connect using correct key. |
| con = getConnection(dbName, CORRECT_KEY); |
| validateDBContents(con); |
| con.close(); |
| // Shutdown the database. |
| shutdown(dbName); |
| } |
| |
| /** |
| * Backup an encrypted database. |
| */ |
| public void testBackupEncryptedDatabase() |
| throws SQLException { |
| String dbName = "encryptionKeyDBToBackup"; |
| // Create the database. |
| con = createAndPopulateDB(dbName); |
| validateDBContents(con); |
| CallableStatement cs = con.prepareCall( |
| "CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE(?)"); |
| cs.setString(1, |
| new File(SupportFilesSetup.EXTINOUT, "backups").getPath()); |
| // Perform backup. |
| cs.execute(); |
| cs.close(); |
| con.close(); |
| shutdown(dbName); |
| // Connect to original database after backup. |
| con = getConnection(dbName, CORRECT_KEY); |
| validateDBContents(con); |
| con.close(); |
| shutdown(dbName); |
| } |
| |
| /** |
| * Create a new database from a backup image. |
| */ |
| public void testCreateDbFromBackup() |
| throws SQLException { |
| // No ordering imposed by JUnit, so we create our own db and backup. |
| // Setup paths and names. |
| final String dbName = "encryptionKeyDBToCreateFrom"; |
| final String backupDbLocation = |
| SupportFilesSetup.getReadWrite( |
| new File("backups", "encryptionKeyDBToCreateFrom").getPath() |
| ).getPath(); |
| // Create the database. |
| con = createAndPopulateDB(dbName); |
| validateDBContents(con); |
| CallableStatement cs = con.prepareCall( |
| "CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE(?)"); |
| cs.setString(1, |
| new File(SupportFilesSetup.EXTINOUT, "backups").getPath()); |
| // Perform backup. |
| cs.execute(); |
| cs.close(); |
| con.close(); |
| shutdown(dbName); |
| // Create a new database from backup. |
| String dbNameRestored = dbName + "Restored"; |
| con = getConnection(dbNameRestored, CORRECT_KEY, |
| "createFrom=" + backupDbLocation); |
| validateDBContents(con); |
| con.close(); |
| shutdown(dbNameRestored, "restored"); |
| // Try to create a new database from backup with the wrong key. |
| dbNameRestored = dbName + "RestoreAttemptedWrongKey"; |
| try { |
| con = getConnection(dbNameRestored, WRONG_KEY, |
| "createFrom=" + backupDbLocation); |
| fail("Created database from encrypted backup with wrong key."); |
| } catch (SQLException sqle) { |
| assertSQLState("XJ040", sqle); |
| assertSQLState("XBCXK", getLastSQLException(sqle)); |
| } |
| assertTrue(con.isClosed()); |
| // Try to create a new database from backup with an invalid key. |
| dbNameRestored = dbName + "RestoreAttemptedInvalidKey"; |
| try { |
| con = getConnection(dbNameRestored, INVALID_CHAR_KEY, |
| "createFrom=" + backupDbLocation); |
| fail("Created database from encrypted backup with an invalid key."); |
| } catch (SQLException sqle) { |
| assertSQLState("XJ040", sqle); |
| assertSQLState("XBCXN", getLastSQLException(sqle)); |
| } |
| assertTrue(con.isClosed()); |
| // Try to create a new database from backup with an odd length key. |
| dbNameRestored = dbName + "RestoreAttemptedOddLengthKey"; |
| try { |
| con = getConnection(dbNameRestored, ODD_LENGTH_KEY, |
| "createFrom=" + backupDbLocation); |
| fail("Created db from encrypted backup with an odd length key."); |
| } catch (SQLException sqle) { |
| assertSQLState("XJ040", sqle); |
| SQLException lastSQLE = getLastSQLException(sqle); |
| String sqlState = lastSQLE.getSQLState(); |
| // The state of this exception varies with the security provider |
| // the test is run with. |
| // Briefly stated, the deciding factor is whether the error is |
| // caught by checks in the Derby code, or by the checks in the |
| // security provider. For instance, the (current Sun JCE) DES |
| // key implementation does not verify the key length, whereas the |
| // AES key implementation does. For other providers, the situation |
| // might be different. |
| // XBCX0 : A general crypto exception, wraps the exception from the |
| // security provider. |
| // XBCXM : A specific Derby exception for external keys of invalid |
| // lengths. |
| if (!sqlState.equals("XBCX0") && !sqlState.equals("XBCXM")) { |
| throw lastSQLE; |
| } |
| } |
| assertTrue(con.isClosed()); |
| // Create a new database from backup again. |
| dbNameRestored = dbName + "RestoredOnceMore"; |
| con = getConnection(dbNameRestored, CORRECT_KEY, |
| "createFrom=" + backupDbLocation); |
| validateDBContents(con); |
| con.close(); |
| shutdown(dbNameRestored, "restored"); |
| } |
| |
| /** |
| * Recover the database using <tt>restoreFrom</tt>. |
| */ |
| public void testRestoreFrom() |
| throws SQLException { |
| // No ordering imposed by JUnit, so we create our own db and backup. |
| String dbName = "encryptionKeyDBToRestoreFrom"; |
| String dbNameRestored = dbName + "Restored"; |
| createBackupRestore(dbName, dbNameRestored); |
| shutdown(dbNameRestored, "restored"); |
| } |
| |
| /** |
| * Try to recover database with an invalid key. |
| * <p> |
| * It should be noted that the existing database, which has been previously |
| * recovered from the same backup image, is deleted/overwritten even though |
| * Derby is unable to boot the backup image. |
| */ |
| public void testInvalidRestoreFrom() |
| throws SQLException { |
| // No ordering imposed by JUnit, so we create our own db and backup. |
| String dbName = "encryptionKeyDBToInvalidRestoreFrom"; |
| String dbNameRestored = dbName + "Restored"; |
| createBackupRestore(dbName, dbNameRestored); |
| shutdown(dbNameRestored, "restored"); |
| // Check that the database is not booted. |
| confirmNonBootedDB("restored/" + dbNameRestored); |
| // Validate the existing database. |
| con = getConnection("restored/" + dbNameRestored, CORRECT_KEY); |
| validateDBContents(con); |
| con.close(); |
| shutdown(dbNameRestored, "restored"); |
| // Confirm that trying a restore with an invalid key will overwrite |
| // the existing database we are trying to restore to/into. This is |
| // expected behavior currently, but should maybe change? |
| try { |
| con = getConnection(dbNameRestored, INVALID_CHAR_KEY, |
| ";restoreFrom=" + obtainDbName(dbName, null)); |
| fail("Restored database with an invalid key."); |
| } catch (SQLException sqle) { |
| assertSQLState("XBCXN", sqle); |
| } |
| // The database should no longer exist. |
| try { |
| // The "" is a hack to avoid using "create=true". |
| con = getConnection("restored/" + dbNameRestored, CORRECT_KEY, ""); |
| fail("Expected connection to fail due to non-existent database."); |
| } catch (SQLException sqle) { |
| assertSQLState("XJ004", sqle); |
| } |
| } |
| |
| /** |
| * Try to create database with a key of odd length. |
| */ |
| public void testCreateWithOddEncryptionKeyLength() |
| throws SQLException { |
| try { |
| getConnection("encryptedDB_oddKeyLength", ODD_LENGTH_KEY); |
| fail("Database creation with odd key length should fail."); |
| } catch (SQLException sqle) { |
| assertSQLState("XJ041", sqle); |
| SQLException lastSQLE = getLastSQLException(sqle); |
| String sqlState = lastSQLE.getSQLState(); |
| // The state of this exception varies with the security provider |
| // the test is run with. In general, it depends on whether it is |
| // Derby code or the security provider code that detects the |
| // problem with the encryption key. |
| if (!sqlState.equals("XBCXM") && !sqlState.equals("XJ001")) { |
| throw lastSQLE; |
| } |
| } |
| } |
| |
| /** |
| * Try to create database with a key containing one or more invalid chars. |
| */ |
| public void testCreateWithInvalidEncryptionKey() { |
| try { |
| getConnection("encryptedDB_invkeyChar", INVALID_CHAR_KEY); |
| fail("Database creation with invalid key should fail."); |
| } catch (SQLException sqle) { |
| assertSQLState("XJ041", sqle); |
| assertSQLState("XBCXN", getLastSQLException(sqle)); |
| } |
| } |
| |
| /* ********************************************************************* * |
| * H E L P E R M E T H O D S * |
| * ********************************************************************* */ |
| |
| /** |
| * Obtain absolute path for the specified database name. |
| * <p> |
| * This absolute path is the name of the database (specified) prefixed with |
| * the absolute path to the EXTINOUT directory. The latter is determined by |
| * consulting <code>SupportFilesSetup</code>. |
| * |
| * @param dbName name of the database |
| * @param subdirectory directory to prefix the database name with (can be |
| * <code>null</code>). Note that the database name will be prefixed |
| * with the path to the EXTINOUT directory even if this parameter is |
| * <code>null</code>. |
| * @return A string with the absolute path to the database. |
| * @see SupportFilesSetup |
| */ |
| String obtainDbName(String dbName, String subdirectory) { |
| File tmp = new File(dbName); |
| if (subdirectory != null) { |
| tmp = new File(subdirectory, dbName); |
| } |
| return PrivilegedFileOpsForTests.getAbsolutePath( |
| new File(SupportFilesSetup.EXTINOUT, tmp.getPath())); |
| } |
| |
| /** |
| * Create encrypted database, validate it, backup, restore and validate |
| * recovered database. |
| * <p> |
| * The source db is shutdown, the recovered db is left booted. |
| * |
| * @param sourceDb the original database to create |
| * @param targetDb the database to recover to |
| */ |
| private void createBackupRestore(String sourceDb, String targetDb) |
| throws SQLException { |
| // Create the database. |
| con = createAndPopulateDB(sourceDb); |
| validateDBContents(con); |
| CallableStatement cs = con.prepareCall( |
| "CALL SYSCS_UTIL.SYSCS_BACKUP_DATABASE(?)"); |
| cs.setString(1, |
| new File(SupportFilesSetup.EXTINOUT, "backups").getPath()); |
| // Perform backup. |
| cs.execute(); |
| con.close(); |
| shutdown(sourceDb); |
| confirmNonBootedDB(sourceDb); |
| // Use the restoreFrom attribute. |
| con = getConnection(targetDb, CORRECT_KEY, |
| ";restoreFrom=" + obtainDbName(sourceDb, "backups")); |
| validateDBContents(con); |
| con.close(); |
| } |
| |
| /** |
| * Confirm that the specified encrypted database has not been booted. |
| * |
| * @param dbName name of an encrypted database |
| * @throws junit.framework.AssertionFailedError if the database has been |
| * booted (connection may or may not be established) |
| */ |
| private void confirmNonBootedDB(String dbName) { |
| DataSource ds = JDBCDataSource.getDataSource(obtainDbName(dbName, null)); |
| try { |
| ds.getConnection(); |
| } catch (SQLException sqle) { |
| assertSQLState("Database booted? <state:" + sqle.getSQLState() + |
| ", msg:" + sqle.getMessage() + ">", "XJ040", sqle); |
| } |
| } |
| |
| /** |
| * Try to establish a connection to the named database with the |
| * specified type of key. |
| * |
| * @param dbName name of the database |
| * @param keyMode what kind of key to use (correct, wrong, invalid, odd) |
| * @return A connection to the database. |
| * @throws SQLException if connection fails |
| */ |
| private Connection getConnection(String dbName, int keyMode) |
| throws SQLException { |
| return getConnection(dbName, keyMode, null); |
| } |
| |
| /** |
| * Create a new connection to the specified database, using the given |
| * connection attributes. |
| * |
| * @param dbName name of the database |
| * @param keyMode what kind of key to use (correct, wrong, invalid, odd) |
| * @param recoveryAttribute attribute to recover a database from a backup, |
| * for instance <code>createFrom</code> or <code>restoreFrom</code>. |
| * Both the attribute and its value is expected. |
| * @return A connection to the database. |
| * @throws SQLException if connection fails |
| */ |
| private Connection getConnection(String dbName, |
| int keyMode, |
| String recoveryAttribute) |
| throws SQLException { |
| DataSource ds = JDBCDataSource.getDataSource( |
| obtainDbName(dbName, |
| recoveryAttribute == null ? null : "restored")); |
| StringBuffer str = new StringBuffer(75); |
| if (recoveryAttribute == null) { |
| // Enable data encryption is this database is being created. |
| JDBCDataSource.setBeanProperty(ds, "CreateDatabase", "create"); |
| str.append("dataEncryption=true;"); |
| } else { |
| str.append(recoveryAttribute); |
| str.append(";"); |
| } |
| // Add the encryption algorithm. |
| str.append("encryptionAlgorithm="); |
| str.append(algorithm); |
| str.append(";"); |
| // Add the key. |
| str.append("encryptionKey="); |
| switch (keyMode) { |
| case CORRECT_KEY: |
| str.append(keyCorrect); |
| break; |
| case WRONG_KEY: |
| str.append(keyWrong); |
| break; |
| case ODD_LENGTH_KEY: |
| str.append(keyOddLength); |
| break; |
| case INVALID_CHAR_KEY: |
| str.append(keyInvalidChar); |
| break; |
| default: |
| throw new IllegalArgumentException( |
| "Invalid key mode specified: " + keyMode); |
| } |
| str.append(";"); |
| JDBCDataSource.setBeanProperty( |
| ds, "connectionAttributes", str.toString()); |
| return ds.getConnection(); |
| } |
| |
| /** |
| * Shutdown the specified database. |
| * |
| * @param databaseName the name of the database |
| */ |
| protected void shutdown(String databaseName) |
| throws SQLException { |
| shutdown(databaseName, null); |
| } |
| |
| /** |
| * Shutdown the database, specified by the database name and prefix. |
| * |
| * @param databaseName the name of the database |
| * @param dir sub-directory prefix for the database |
| */ |
| protected void shutdown(String databaseName, String dir) |
| throws SQLException { |
| DataSource ds = JDBCDataSource.getDataSource( |
| obtainDbName(databaseName, dir)); |
| JDBCDataSource.shutdownDatabase(ds); |
| } |
| |
| /** |
| * Create a new database and populate it. |
| * <p> |
| * The method fails with an exception if the database already exists. |
| * This is because it is the creation process that is to be tested. |
| * |
| * @param dbName name of the database to create |
| * @return A connection the to the newly created database. |
| * @throws SQLException if the database already exist, or |
| * a general error happens during database interaction |
| */ |
| protected Connection createAndPopulateDB(String dbName) |
| throws SQLException { |
| Connection newCon = getConnection(dbName, CORRECT_KEY); |
| SQLWarning warning = newCon.getWarnings(); |
| // If the database already exists, fail the test. |
| if (warning != null) { |
| if ("01J01".equals(warning.getSQLState())) { |
| fail("Refusing to continue, database already exists <" + |
| warning.getMessage() + ">"); |
| } |
| } |
| Statement stmt = newCon.createStatement(); |
| stmt.executeUpdate("CREATE TABLE " + TABLE + " (id int NOT NULL, " + |
| "val int NOT NULL, PRIMARY KEY(id))"); |
| stmt.close(); |
| PreparedStatement ps = newCon.prepareStatement("INSERT INTO " + TABLE + |
| " (id, val) VALUES (?,?)"); |
| for (int i=0; i < DATA.length; i++) { |
| ps.setInt(1, i); |
| ps.setInt(2, DATA[i]); |
| ps.executeUpdate(); |
| } |
| ps.close(); |
| return newCon; |
| } |
| |
| /** |
| * Validate the data in the database against the data model. |
| * |
| * @param con the database to validate the contents of |
| * @throws junit.framework.AssertionFailedError if there is a mismatch |
| * between the data in the database and the model |
| */ |
| protected void validateDBContents(Connection con) |
| throws SQLException { |
| Statement stmt = con.createStatement(); |
| ResultSet rs = stmt.executeQuery("SELECT id, val FROM " + TABLE + |
| " ORDER BY id"); |
| int id, val; |
| while (rs.next()) { |
| id = rs.getInt(1); |
| val = rs.getInt(2); |
| if (id >= DATA.length) { |
| fail("Id in database out of bounds for data model; " + |
| id + " >= " + DATA.length); |
| } |
| if (val != DATA[id]) { |
| fail("Mismatch between db and model for id " + id + ";" + |
| val + " != " + DATA[id]); |
| } |
| } |
| rs.close(); |
| stmt.close(); |
| } |
| } // End EncryptionKeyTest |