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();
+ }
+ }
+
+}