Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cache Parameter Metadata #1845

Merged
merged 23 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
56ee9c2
Cache parameter metadata - missing completed tests + connection strin…
Jeffery-Wasty Jun 13, 2022
09c2892
Merge branch 'main' into cache-parameter-metadata
Jeffery-Wasty Jun 13, 2022
7839ea3
Added secure enclave retry logic + testing for caching and secure enc…
Jeffery-Wasty Jun 15, 2022
f4f73b6
Merge branch 'cache-parameter-metadata' of https://github.com/microso…
Jeffery-Wasty Jun 15, 2022
810a019
Added some things with testing.
Jeffery-Wasty Jun 15, 2022
9b25fd7
Fixed DriverJDBCVersion naming
Jeffery-Wasty Jun 15, 2022
729626c
Fixed another issue regarding the enclave keys
Jeffery-Wasty Jun 15, 2022
d6b00e5
Chaged AKV to WKS
Jeffery-Wasty Jun 15, 2022
1c5655d
Added comments and cleaned up code a bit
Jeffery-Wasty Jun 16, 2022
20bc4cf
Added 'reqExternalSetup' tags to prevent failures in public pipelines
Jeffery-Wasty Jun 16, 2022
a1857d1
Fixed access modifiers, exceptions, few lines in test, changed HashMa…
Jeffery-Wasty Jun 20, 2022
b1e0962
Removed extra lines in SQLQueryMetadataCache + changed access modifie…
Jeffery-Wasty Jun 20, 2022
7473cc0
Changed tags on tests.
Jeffery-Wasty Jun 20, 2022
dc8a6b2
Added a missing description.
Jeffery-Wasty Jun 20, 2022
05a2816
Fixed test error where column encryption was not working correctly (m…
Jeffery-Wasty Jun 20, 2022
769937a
Cleaned up tests, resolved comments
Jeffery-Wasty Jun 21, 2022
ab7afe7
Fixed whitespace issues.
Jeffery-Wasty Jun 21, 2022
482193e
Code clean up, more logging, better error handling + negative testing
Jeffery-Wasty Jun 21, 2022
8bbc122
Removed db name padding as its no longer necessary.
Jeffery-Wasty Jun 21, 2022
8025c30
Removed method in ParameterMetaDataCache that will not be used until …
Jeffery-Wasty Jun 21, 2022
b2d09e1
Changed errors in ParamaterMetaDataCache to include the error message…
Jeffery-Wasty Jun 21, 2022
345ef9f
In SQLServerConnectionTest, specifying columnEncryption=enabled cause…
Jeffery-Wasty Jun 21, 2022
64c4783
One small change in ISQLServerEnclaveProvider to allow AE tests to ru…
Jeffery-Wasty Jun 22, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/main/java/com/microsoft/sqlserver/jdbc/AE.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;


/**
Expand Down Expand Up @@ -230,6 +232,49 @@ boolean isAlgorithmInitialized() {
}


/*
* Represents a cache of all queries for a given enclave session.
*/
class CryptoCache {
private HashMap<String, Map<Integer, CekTableEntry>> cekMap = new HashMap<>();
private HashMap<String, HashMap<String, CryptoMetadata>> paramMap = new HashMap<>();

public HashMap<String, Map<Integer, CekTableEntry>> getCekMap() {
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
return cekMap;
}

public HashMap<String, HashMap<String, CryptoMetadata>> getParamMap() {
return paramMap;
}

// Returns a list of parameters with associated metadata, for a given enclave cache.
public Map<Integer, CekTableEntry> getEnclaveEntry(String enclaveLookupKey) {
return cekMap.get(enclaveLookupKey);
}

// Returns a list of parameters with associated metadata, for a given enclave cache.
public HashMap<String, CryptoMetadata> getCacheEntry(String cacheLookupKey) {
return paramMap.get(cacheLookupKey);
}

public void addCekEntry(String key, Map<Integer, CekTableEntry> value) {
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
cekMap.put(key, value);
}

public void addParamEntry(String key, HashMap<String, CryptoMetadata> value) {
paramMap.put(key, value);
}

public void removeCekEntry(String enclaveLookupKey) {
cekMap.remove(enclaveLookupKey);
}

public void removeParamEntry(String cacheLookupKey) {
paramMap.remove(cacheLookupKey);
}
}


// Fields in the first resultset of "sp_describe_parameter_encryption"
// We expect the server to return the fields in the resultset in the same order as mentioned below.
// If the server changes the below order, then transparent parameter encryption will break.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ default ResultSet executeSDPEv1(PreparedStatement stmt, String userSql,
*/
default void processSDPEv1(String userSql, String preparedTypeDefinitions, Parameter[] params,
ArrayList<String> parameterNames, SQLServerConnection connection, SQLServerStatement sqlServerStatement,
PreparedStatement stmt, ResultSet rs, ArrayList<byte[]> enclaveRequestedCEKs) throws SQLException {
PreparedStatement stmt, ResultSet rs, ArrayList<byte[]> enclaveRequestedCEKs,
EnclaveSession session) throws SQLException {
Map<Integer, CekTableEntry> cekList = new HashMap<>();
CekTableEntry cekEntry = null;
boolean isRequestedByEnclave = false;
Expand Down Expand Up @@ -238,6 +239,7 @@ default void processSDPEv1(String userSql, String preparedTypeDefinitions, Param
aev2CekEntry.put(provider.decryptColumnEncryptionKey(keyPath, algo, encryptedKey));
enclaveRequestedCEKs.add(aev2CekEntry.array());
}

}

// Process the second resultset.
Expand Down Expand Up @@ -279,6 +281,9 @@ default void processSDPEv1(String userSql, String preparedTypeDefinitions, Param
}
}
}

SQLQueryMetadataCache.addQueryMetadata(params, parameterNames, session, connection, sqlServerStatement,
cekList, isRequestedByEnclave);
}

/**
Expand Down Expand Up @@ -482,11 +487,13 @@ class EnclaveSession {
private byte[] sessionID;
private AtomicLong counter;
private byte[] sessionSecret;
private CryptoCache cryptoCache;

EnclaveSession(byte[] cs, byte[] b) {
sessionID = cs;
sessionSecret = b;
counter = new AtomicLong(0);
cryptoCache = new CryptoCache();
}

byte[] getSessionID() {
Expand All @@ -497,6 +504,10 @@ byte[] getSessionSecret() {
return sessionSecret;
}

CryptoCache getCryptoCache() {
return cryptoCache;
}

synchronized long getCounter() {
return counter.getAndIncrement();
}
Expand Down
251 changes: 251 additions & 0 deletions src/main/java/com/microsoft/sqlserver/jdbc/SQLQueryMetadataCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/*
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
* 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;

import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.HashMap;
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
import java.util.Map;


/**
* Implements a cache for query metadata returned from sp_describe_parameter_encryption calls. Adding, removing, and
* reading from the cache is handled here, with the location of the cache being in the EnclaveSession.
*
*/

Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
public class SQLQueryMetadataCache {

final static int cacheSize = 2000; // Size of the cache in number of entries
final static int cacheTrimThreshold = 300; // Threshold above which to trim the cache
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved

/**
* Retrieves the metadata from the cache, should it exist.
*
* @param params
* Array of parameters used
* @param parameterNames
* Names of parameters used
* @param session
* The current enclave session containing the cache
* @param connection
* The SQLServer connection
* @param stmt
* The SQLServer statement, whose returned metadata we're checking
* @return true, if the metadata for the query can be retrieved
*
*/
public static boolean getQueryMetadataIfExists(Parameter[] params, ArrayList<String> parameterNames,
EnclaveSession session, SQLServerConnection connection, SQLServerStatement stmt) {

// Caching is enabled if column encryption is enabled, return false if it's not
if (connection.activeConnectionProperties
.getProperty(SQLServerDriverStringProperty.COLUMN_ENCRYPTION.toString()).equalsIgnoreCase("Disabled")) {
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
return false;
}

AbstractMap.SimpleEntry<String, String> encryptionValues = getCacheLookupKeysFromSqlCommand(stmt, connection);
HashMap<String, CryptoMetadata> metadataMap = session.getCryptoCache().getCacheEntry(encryptionValues.getKey());

if (metadataMap == null) {
return false;
}

// Iterate over all parameters and get the metadata
for (int i = 0; i < params.length; i++) {
boolean found = metadataMap.containsKey(parameterNames.get(i));
CryptoMetadata foundData = metadataMap.get(parameterNames.get(i));

/*
* If ever the map doesn't contain a parameter, the cache entry cannot be used. If there is data found, it
* should never have the initialized algorithm as that would contain the key. Clear all metadata that has
* already been assigned in either case.
*/
if (!found || (foundData != null && foundData.isAlgorithmInitialized())) {
for (Parameter param : params) {
param.cryptoMeta = null;
}
return false;
}

params[i].cryptoMeta = foundData;
}

// Assign the key using a metadata copy. We shouldn't load from the cached version for security reasons.
for (int i = 0; i < params.length; ++i) {
try {
CryptoMetadata cryptoCopy = null;
CryptoMetadata metaData = params[i].getCryptoMetadata();
if (metaData != null) {
cryptoCopy = new CryptoMetadata(metaData.getCekTableEntry(), metaData.getOrdinal(),
metaData.getEncryptionAlgorithmId(), metaData.getEncryptionAlgorithmName(),
metaData.getEncryptionType().getValue(), metaData.getNormalizationRuleVersion());
}

params[i].cryptoMeta = cryptoCopy;

if (cryptoCopy != null) {
// Try to get the encryption key. If the key information is stale, this might fail.
// In this case, just fail the cache lookup.
try {
SQLServerSecurityUtility.decryptSymmetricKey(cryptoCopy, connection, stmt);
} catch (SQLServerException e) {

removeCacheEntry(stmt, session, connection);

for (Parameter paramToCleanup : params) {
paramToCleanup.cryptoMeta = null;
}

return false;
}
}
} catch (Exception e) {
e.printStackTrace();
}

}

// Logic for checking enclave retry
Map<Integer, CekTableEntry> enclaveKeys = session.getCryptoCache().getEnclaveEntry(encryptionValues.getValue());
if (enclaveKeys != null) {
//something something = copyEnclaveKeys(enclaveKeys);
}

return true;
}

// Add the metadata for a specific query to the cache.
public static boolean addQueryMetadata(Parameter[] params, ArrayList<String> parameterNames, EnclaveSession session,
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
SQLServerConnection connection, SQLServerStatement stmt, Map<Integer, CekTableEntry> cekList, boolean isRequestedByEnclave) {

// Caching is enabled if column encryption is enabled, return false if it's not
if (connection.activeConnectionProperties
.getProperty(SQLServerDriverStringProperty.COLUMN_ENCRYPTION.toString()).equalsIgnoreCase("Disabled")) {
return false;
}

AbstractMap.SimpleEntry<String, String> encryptionValues = getCacheLookupKeysFromSqlCommand(stmt, connection);
if (encryptionValues.getKey() == null) {
return false;
}

HashMap<String, CryptoMetadata> metadataMap = new HashMap<>(params.length);

// Create a copy of the cypherMetadata that doesn't have the algorithm
for (int i = 0; i < params.length; i++) {
try {
CryptoMetadata cryptoCopy = null;
CryptoMetadata metaData = params[i].getCryptoMetadata();
if (metaData != null) {

cryptoCopy = new CryptoMetadata(metaData.getCekTableEntry(), metaData.getOrdinal(),
metaData.getEncryptionAlgorithmId(), metaData.getEncryptionAlgorithmName(),
metaData.getEncryptionType().getValue(), metaData.getNormalizationRuleVersion());
}
// Cached cipher MD should never have an initialized algorithm since this would contain the key.
if (cryptoCopy != null && !cryptoCopy.isAlgorithmInitialized()) {
String paramName = parameterNames.get(i);
metadataMap.put(paramName, cryptoCopy);
} else {
return false;
}
} catch (SQLServerException e) {
e.printStackTrace();
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
}
}

// If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly.
int cacheSizeCurrent = session.getCryptoCache().getParamMap().size();
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
if (cacheSizeCurrent > cacheSize + cacheTrimThreshold) {
try {
int entriesToRemove = cacheSizeCurrent - cacheSize;
HashMap<String, HashMap<String, CryptoMetadata>> newMap = new HashMap<>();
HashMap<String, HashMap<String, CryptoMetadata>> oldMap = session.getCryptoCache().getParamMap();
int count = 0;

for (Map.Entry<String, HashMap<String, CryptoMetadata>> entry : oldMap.entrySet()) {
if (count >= entriesToRemove) {
newMap.put(entry.getKey(), entry.getValue());
}
count++;
}

} catch (Exception e) {
e.printStackTrace();
}
}

// Servers supporting enclave computationsa always return a boolean indicating whether the key
// is required by enclave or not. If it is required we need to save it.
if (isRequestedByEnclave) {
Map<Integer, CekTableEntry> keysToBeCached = copyEnclaveKeys(cekList);
session.getCryptoCache().addCekEntry(encryptionValues.getValue(), keysToBeCached);
}

return true;
}

public static void removeCacheEntry(SQLServerStatement stmt, EnclaveSession session,
SQLServerConnection connection) {
AbstractMap.SimpleEntry<String, String> encryptionValues = getCacheLookupKeysFromSqlCommand(stmt, connection);
if (encryptionValues.getKey() == null) {
return;
}

session.getCryptoCache().removeParamEntry(encryptionValues.getKey());
}

private static AbstractMap.SimpleEntry<String, String> getCacheLookupKeysFromSqlCommand(
SQLServerStatement statement, SQLServerConnection connection) {
final int sqlIdentifierLength = 128;
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved

// Return null if we have no connection.
if (connection == null) {
Jeffery-Wasty marked this conversation as resolved.
Show resolved Hide resolved
return new AbstractMap.SimpleEntry<>(null, null);
}

StringBuilder cacheLookupKeyBuilder = new StringBuilder();
cacheLookupKeyBuilder.append(":::");
// Pad database name to 128 characters to avoid any false cache matches because of weird DB names.
String databaseName = connection.activeConnectionProperties
.getProperty(SQLServerDriverStringProperty.DATABASE_NAME.toString());
cacheLookupKeyBuilder.append(databaseName);
for (int i = databaseName.length() - 1; i < sqlIdentifierLength; ++i) {
cacheLookupKeyBuilder.append(" ");
}
cacheLookupKeyBuilder.append(":::");
cacheLookupKeyBuilder.append(statement.toString());

String cacheLookupKey = cacheLookupKeyBuilder.toString();
String enclaveLookupKey = cacheLookupKeyBuilder.append(":::enclaveKeys").toString();

return new AbstractMap.SimpleEntry<>(cacheLookupKey, enclaveLookupKey);
}

/**
*
*
*
* @param keysToBeSentToEnclave
* @return
*/
private static Map<Integer, CekTableEntry> copyEnclaveKeys(
Map<Integer, CekTableEntry> keysToBeSentToEnclave) {
Map<Integer, CekTableEntry> cekList = new HashMap<>();

for (Map.Entry<Integer, CekTableEntry> entry : keysToBeSentToEnclave.entrySet()) {
int ordinal = entry.getKey();
CekTableEntry original = entry.getValue();
CekTableEntry copy = new CekTableEntry(ordinal);
for (EncryptionKeyInfo cekInfo : original.getColumnEncryptionKeyValues()) {
copy.add(cekInfo.encryptedKey, cekInfo.databaseId, cekInfo.cekId, cekInfo.cekVersion,
cekInfo.cekMdVersion, cekInfo.keyPath, cekInfo.keyStoreName, cekInfo.algorithmName);
}
cekList.put(ordinal, copy);
}
return cekList;
}
}
Loading