blob: 0ffeb5a05de058386ac868a90f1df9a2fe113ad2 [file] [log] [blame]
/*
*
* 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