diff --git a/.travis.yml b/.travis.yml index 7ff46efec..a6ece9453 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,4 @@ -sudo: required +sudo: required language: java jdk: @@ -18,8 +18,17 @@ env: cache: directories: - $HOME/.m2 - + +before_install: + - mkdir AE_Certificates + install: + - cd AE_Certificates + - openssl req -newkey rsa:2048 -x509 -keyout cakey.pem -out cacert.pem -days 3650 -subj "/C=US/ST=WA/L=Redmond/O=Microsoft Corporation/OU=SQL Server/CN=JDBC Driver" -nodes + - openssl pkcs12 -export -in cacert.pem -inkey cakey.pem -out identity.p12 -password pass:password + - keytool -importkeystore -destkeystore clientcert.jks -deststorepass password -srckeystore identity.p12 -srcstoretype PKCS12 -srcstorepass password + - keytool -list -v -keystore clientcert.jks -storepass "password" > JavaKeyStore.txt + - cd .. - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -Pbuild41 - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -Pbuild42 @@ -36,4 +45,4 @@ script: #after_success: # instead of after success we are using && operator for conditional submitting coverage report. -# - bash <(curl -s https://codecov.io/bash) +# - bash <(curl -s https://codecov.io/bash) \ No newline at end of file diff --git a/appveyor.yml b/appveyor.yml index 857c635f8..2ab7d47ef 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,14 +14,25 @@ install: - ps: choco pack - ps: choco install jce -fdv -s . -y -failonstderr - ps: cd.. + - ps: mkdir AE_Certificates + - ps: cd AE_Certificates + - ps: $cert = New-SelfSignedCertificate -dns "AlwaysEncryptedCert" -CertStoreLocation Cert:CurrentUser\My + - ps: $pwd = ConvertTo-SecureString -String "password" -Force -AsPlainText + - ps: $path = 'cert:\CurrentUser\My\' + $cert.thumbprint + - ps: $certificate = Export-PfxCertificate -cert $path -FilePath cert.pfx -Password $pwd + - ps: Get-ChildItem -path cert:\CurrentUser\My > certificate.txt cache: - C:\Users\appveyor\.m2 -> pom.xml build_script: + - keytool -importkeystore -srckeystore cert.pfx -srcstoretype pkcs12 -destkeystore clientcert.jks -deststoretype JKS -srcstorepass password -deststorepass password + - keytool -list -v -keystore clientcert.jks -storepass "password" > JavaKeyStore.txt + - cd.. - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -Pbuild41 - mvn install -DskipTests=true -Dmaven.javadoc.skip=true -B -V -Pbuild42 test_script: - mvn test -B -Pbuild41 - mvn test -B -Pbuild42 + \ No newline at end of file diff --git a/pom.xml b/pom.xml index b3ddccaa4..84680c0a5 100644 --- a/pom.xml +++ b/pom.xml @@ -222,6 +222,13 @@ **/*.csv + + AE_Certificates + + **/*.txt + **/*.jks + + diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java new file mode 100644 index 000000000..b646d8e45 --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java @@ -0,0 +1,198 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.AlwaysEncrypted; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.Properties; + +import javax.xml.bind.DatatypeConverter; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.opentest4j.TestAbortedException; + +import com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionJavaKeyStoreProvider; +import com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionKeyStoreProvider; +import com.microsoft.sqlserver.jdbc.SQLServerConnection; +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.testframework.AbstractTest; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.Utils; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Setup for Always Encrypted test + * This test will work on Appveyor and Travis-ci as java key store gets created from the .yml scripts. Users on their local machine should create the + * keystore manually and save the alias name in JavaKeyStore.txt file. For local test purposes, put this in the target/test-classes directory + * + */ +@RunWith(JUnitPlatform.class) +public class AESetup extends AbstractTest { + + static String javaKeyStoreInputFile = "JavaKeyStore.txt"; + static String keyStoreName = "MSSQL_JAVA_KEYSTORE"; + static String jksName = "clientcert.jks"; + static String filePath = null; + static String thumbprint = null; + static SQLServerConnection con = null; + static Statement stmt = null; + static String cmkName = "JDBC_CMK"; + static String cekName = "JDBC_CEK"; + static String keyPath = null; + static String javaKeyAliases = null; + static String OS = System.getProperty("os.name").toLowerCase(); + static SQLServerColumnEncryptionKeyStoreProvider storeProvider = null; + static String secretstrJks = "password"; + static String numericTable = "numericTable"; + + /** + * Create connection, statement and generate path of resource file + * @throws Exception + * @throws TestAbortedException + */ + @BeforeAll + static void setUpConnection() throws TestAbortedException, Exception { + assumeTrue(13 <= new DBConnection(connectionString).getServerVersion(), + "Aborting test case as SQL Server version is not compatible with Always encrypted "); + + readFromFile(javaKeyStoreInputFile, "Alias name"); + con = (SQLServerConnection) DriverManager.getConnection(connectionString); + stmt = con.createStatement(); + Utils.dropTableIfExists(numericTable, stmt); + dropCEK(); + dropCMK(); + con.close(); + + + keyPath = Utils.getCurrentClassPath() + jksName; + storeProvider = new SQLServerColumnEncryptionJavaKeyStoreProvider(keyPath, secretstrJks.toCharArray()); + Properties info = new Properties(); + info.setProperty("ColumnEncryptionSetting", "Enabled"); + info.setProperty("keyStoreAuthentication", "JavaKeyStorePassword"); + info.setProperty("keyStoreLocation", keyPath); + info.setProperty("keyStoreSecret", secretstrJks); + con = (SQLServerConnection) DriverManager.getConnection(connectionString, info); + stmt = con.createStatement(); + createCMK(keyStoreName, javaKeyAliases); + } + + /** + * Read the alias from file which is created during creating jks + * If the jks and alias name in JavaKeyStore.txt does not exists, will not run! + * @param inputFile + * @param lookupValue + * @throws IOException + */ + private static void readFromFile(String inputFile, + String lookupValue) throws IOException { + BufferedReader buffer = null; + filePath = Utils.getCurrentClassPath(); + try { + File f = new File(filePath + inputFile); + assumeTrue(f.exists(), "Aborting test case since no java key store and alias name exists!"); + buffer = new BufferedReader(new FileReader(f)); + String readLine = ""; + String[] linecontents; + + while ((readLine = buffer.readLine()) != null) { + if (readLine.trim().contains(lookupValue)) { + linecontents = readLine.split(" "); + javaKeyAliases = linecontents[2]; + break; + } + } + + } + catch (IOException e) { + fail(e.toString());; + } + finally{ + if (null != buffer){ + buffer.close(); + } + } + + } + + /** + * Creating numeric table + */ + static void createNumericTable() { + String sql = "create table " + numericTable + " (" + "PlainSmallint smallint null," + + "RandomizedSmallint smallint ENCRYPTED WITH (ENCRYPTION_TYPE = RANDOMIZED, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL," + + "DeterministicSmallint smallint ENCRYPTED WITH (ENCRYPTION_TYPE = DETERMINISTIC, ALGORITHM = 'AEAD_AES_256_CBC_HMAC_SHA_256', COLUMN_ENCRYPTION_KEY = " + + cekName + ") NULL" + ");"; + + try { + stmt.execute(sql); + } + catch (SQLException e) { + fail(e.toString()); + } + } + + /** + * Create column master key + * @param keyStoreName + * @param keyPath + * @throws SQLException + */ + private static void createCMK(String keyStoreName, + String keyPath) throws SQLException { + String sql = " if not exists (SELECT name from sys.column_master_keys where name='" + cmkName + "')" + " begin" + " CREATE COLUMN MASTER KEY " + + cmkName + " WITH (KEY_STORE_PROVIDER_NAME = '" + keyStoreName + "', KEY_PATH = '" + keyPath + "')" + " end"; + stmt.execute(sql); + } + + /** + * Create column encryption key + * @param storeProvider + * @param certStore + * @throws SQLException + */ + static void createCEK(SQLServerColumnEncryptionKeyStoreProvider storeProvider) throws SQLException { + String letters = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + byte[] valuesDefault = letters.getBytes(); + String cekSql = null; + byte[] key = storeProvider.encryptColumnEncryptionKey(javaKeyAliases, "RSA_OAEP", valuesDefault); + cekSql = "CREATE COLUMN ENCRYPTION KEY " + cekName + " WITH VALUES " + "(COLUMN_MASTER_KEY = " + cmkName + + ", ALGORITHM = 'RSA_OAEP', ENCRYPTED_VALUE = 0x" + DatatypeConverter.printHexBinary(key) + ")" + ";"; + stmt.execute(cekSql); + } + + /** + * Dropping column encryption key + * @throws SQLServerException + * @throws SQLException + */ + static void dropCEK() throws SQLServerException, SQLException { + String cekSql = " if exists (SELECT name from sys.column_encryption_keys where name='" + cekName + "')" + " begin" + + " drop column encryption key " + cekName + " end"; + stmt.execute(cekSql); + } + + /** + * Dropping column master key + * @throws SQLServerException + * @throws SQLException + */ + static void dropCMK() throws SQLServerException, SQLException { + String cekSql = " if exists (SELECT name from sys.column_master_keys where name='" + cmkName + "')" + " begin" + " drop column master key " + + cmkName + " end"; + stmt.execute(cekSql); + } +} diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java new file mode 100644 index 000000000..ad3729d7a --- /dev/null +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/JDBCEncryptionDecryptionTest.java @@ -0,0 +1,129 @@ +/* + * Microsoft JDBC Driver for SQL Server + * + * Copyright(c) Microsoft Corporation All rights reserved. + * + * This program is made available under the terms of the MIT License. See the LICENSE file in the project root for more information. + */ +package com.microsoft.sqlserver.jdbc.AlwaysEncrypted; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.platform.runner.JUnitPlatform; +import org.junit.runner.RunWith; +import org.opentest4j.TestAbortedException; + +import com.microsoft.sqlserver.jdbc.SQLServerException; +import com.microsoft.sqlserver.jdbc.SQLServerPreparedStatement; +import com.microsoft.sqlserver.jdbc.SQLServerStatementColumnEncryptionSetting; +import com.microsoft.sqlserver.testframework.DBConnection; +import com.microsoft.sqlserver.testframework.Utils; + +/** + * Tests Decryption and encryption of values + * + */ +@RunWith(JUnitPlatform.class) +public class JDBCEncryptionDecryptionTest extends AESetup { + private static SQLServerPreparedStatement pstmt = null; + String[] values = {"10"}; + + /** + * Test encryption and decryption of numeric values + * + * @throws Exception + * @throws TestAbortedException + */ + @Test + @DisplayName("test numeric values") + public void testNumeric() throws TestAbortedException, Exception { + assumeTrue(13 <= new DBConnection(connectionString).getServerVersion(), + "Aborting test case as SQL Server version is not compatible with Always encrypted "); + + try { + createCEK(storeProvider); + createNumericTable(); + populateNumeric(values); + verifyResults(); + } + finally { + Utils.dropTableIfExists(numericTable, stmt); + } + } + + /** + * Dropping all CMKs and CEKs and any open resources. + * + * @throws SQLServerException + * @throws SQLException + */ + @AfterAll + static void dropAll() throws SQLServerException, SQLException { + Utils.dropTableIfExists(numericTable, stmt); + dropCEK(); + dropCMK(); + if (null != stmt) { + stmt.close(); + } + if (null != con) { + con.close(); + } + } + + /** + * Populating the table + * + * @param values + * @throws SQLException + */ + private void populateNumeric(String[] values) throws SQLException { + String sql = "insert into " + numericTable + " values( " + "?,?,?" + ")"; + + pstmt = (SQLServerPreparedStatement) con.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, + connection.getHoldability(), SQLServerStatementColumnEncryptionSetting.Enabled); + + for (int i = 1; i <= 3; i++) { + pstmt.setShort(i, Short.valueOf(values[0])); + } + pstmt.execute(); + if (null != pstmt) { + pstmt.close(); + } + } + + /** + * Verify the decryption and encryption of values + * + * @throws NumberFormatException + * @throws SQLException + */ + private void verifyResults() throws NumberFormatException, SQLException { + String sql = "select * from " + numericTable; + pstmt = (SQLServerPreparedStatement) connection.prepareStatement(sql, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY, + connection.getHoldability(), SQLServerStatementColumnEncryptionSetting.Enabled); + ResultSet rs = null; + + rs = pstmt.executeQuery(); + + while (rs.next()) { + assertEquals(Short.valueOf(values[0]), rs.getObject(1)); + assertEquals(Short.valueOf(values[0]), rs.getObject(2)); + assertEquals(Short.valueOf(values[0]), rs.getObject(3)); + } + + if (null != rs) { + rs.close(); + } + if (null != pstmt) { + pstmt.close(); + } + } + +}