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 all 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
39 changes: 39 additions & 0 deletions src/main/java/com/microsoft/sqlserver/jdbc/AE.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


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


/**
* Represents a cache of all queries for a given enclave session.
*/
class CryptoCache {
/**
* The cryptocache stores both result sets returned from sp_describe_parameter_encryption calls. CEK data in cekMap,
* and parameter data in paramMap.
*/
private final ConcurrentHashMap<String, Map<Integer, CekTableEntry>> cekMap = new ConcurrentHashMap<>(16);
private ConcurrentHashMap<String, ConcurrentHashMap<String, CryptoMetadata>> paramMap = new ConcurrentHashMap<>(16);

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

void replaceParamMap(ConcurrentHashMap<String, ConcurrentHashMap<String, CryptoMetadata>> newMap) {
paramMap = newMap;
}

Map<Integer, CekTableEntry> getEnclaveEntry(String enclaveLookupKey) {
return cekMap.get(enclaveLookupKey);
}

ConcurrentHashMap<String, CryptoMetadata> getCacheEntry(String cacheLookupKey) {
return paramMap.get(cacheLookupKey);
}

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

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 @@ -279,6 +280,12 @@ default void processSDPEv1(String userSql, String preparedTypeDefinitions, Param
}
}
}

// If using Always Encrypted v1 (without secure enclaves), add to cache
if (!connection.enclaveEstablished() && session != null) {
ParameterMetaDataCache.addQueryMetadata(params, parameterNames, session.getCryptoCache(), connection,
sqlServerStatement, cekList);
}
}

/**
Expand Down Expand Up @@ -482,11 +489,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 +506,10 @@ byte[] getSessionSecret() {
return sessionSecret;
}

CryptoCache getCryptoCache() {
return cryptoCache;
}

synchronized long getCounter() {
return counter.getAndIncrement();
}
Expand Down
243 changes: 243 additions & 0 deletions src/main/java/com/microsoft/sqlserver/jdbc/ParameterMetaDataCache.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
/*
* 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.text.MessageFormat;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;


/**
* 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.
*
*/
class ParameterMetaDataCache {

static final int CACHE_SIZE = 2000; // Size of the cache in number of entries
static final int CACHE_TRIM_THRESHOLD = 300; // Threshold above which to trim the cache

static private java.util.logging.Logger metadataCacheLogger = java.util.logging.Logger
.getLogger("com.microsoft.sqlserver.jdbc.ParameterMetaDataCache");

/**
* 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
*
*/
static boolean getQueryMetadata(Parameter[] params, ArrayList<String> parameterNames, CryptoCache cache,
SQLServerConnection connection, SQLServerStatement stmt) throws SQLServerException {

AbstractMap.SimpleEntry<String, String> encryptionValues = getCacheLookupKeys(stmt, connection);
ConcurrentHashMap<String, CryptoMetadata> metadataMap = cache.getCacheEntry(encryptionValues.getKey());

if (metadataMap == null) {
if (metadataCacheLogger.isLoggable(java.util.logging.Level.FINEST)) {
metadataCacheLogger.finest("Cache Miss. Unable to retrieve cache entry from cache.");
}
return false;
}

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;
}
if (metadataCacheLogger.isLoggable(java.util.logging.Level.FINEST)) {
metadataCacheLogger
.finest("Cache Miss. Cache entry either has missing parameter or initialized algorithm.");
}
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 {
SQLServerSecurityUtility.decryptSymmetricKey(cryptoCopy, connection, stmt);
} catch (SQLServerException e) {

removeCacheEntry(stmt, cache, connection);

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

if (metadataCacheLogger.isLoggable(java.util.logging.Level.FINEST)) {
metadataCacheLogger.finest("Cache Miss. Unable to decrypt CEK.");
}
return false;
}
}
} catch (Exception e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CryptoCacheInaccessible"));
Object[] msgArgs = {e.getMessage()};
throw new SQLServerException(form.format(msgArgs), null);
}
}

if (metadataCacheLogger.isLoggable(java.util.logging.Level.FINEST)) {
metadataCacheLogger.finest("Cache Hit. Successfully retrieved metadata from cache.");
}
return true;
}

/**
*
* Adds the parameter metadata to the cache, also handles cache trimming.
*
* @param params
* List of parameters used
* @param parameterNames
* Names of parameters used
* @param session
* Enclave session containing the cryptocache
* @param connection
* SQLServerConnection
* @param stmt
* SQLServer statement used to retrieve keys to find correct cache
* @param cekList
* The list of CEKs (from the first RS) that is also added to the cache as well as parameter metadata
* @return true, if the query metadata has been added correctly
*/
static boolean addQueryMetadata(Parameter[] params, ArrayList<String> parameterNames, CryptoCache cache,
SQLServerConnection connection, SQLServerStatement stmt,
Map<Integer, CekTableEntry> cekList) throws SQLServerException {

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

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

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());
}
if (cryptoCopy != null && !cryptoCopy.isAlgorithmInitialized()) {
String paramName = parameterNames.get(i);
metadataMap.put(paramName, cryptoCopy);
} else {
return false;
}
} catch (SQLServerException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CryptoCacheInaccessible"));
Object[] msgArgs = {e.getMessage()};
throw new SQLServerException(form.format(msgArgs), null);
}
}

// If the size of the cache exceeds the threshold, set that we are in trimming and trim the cache accordingly.
int cacheSizeCurrent = cache.getParamMap().size();
if (cacheSizeCurrent > CACHE_SIZE + CACHE_TRIM_THRESHOLD) {
int entriesToRemove = cacheSizeCurrent - CACHE_SIZE;
ConcurrentHashMap<String, ConcurrentHashMap<String, CryptoMetadata>> newMap = new ConcurrentHashMap<>();
ConcurrentHashMap<String, ConcurrentHashMap<String, CryptoMetadata>> oldMap = cache.getParamMap();
int count = 0;

for (Map.Entry<String, ConcurrentHashMap<String, CryptoMetadata>> entry : oldMap.entrySet()) {
if (count >= entriesToRemove) {
newMap.put(entry.getKey(), entry.getValue());
}
count++;
}
cache.replaceParamMap(newMap);
if (metadataCacheLogger.isLoggable(java.util.logging.Level.FINEST)) {
metadataCacheLogger.finest("Cache successfully trimmed.");
}
}

cache.addParamEntry(encryptionValues.getKey(), metadataMap);
return true;
}

/**
*
* Remove the cache entry.
*
* @param stmt
* SQLServer statement used to retrieve keys
* @param session
* The enclave session where the cryptocache is stored
* @param connection
* The SQLServerConnection, also used to retrieve keys
*/
static void removeCacheEntry(SQLServerStatement stmt, CryptoCache cache, SQLServerConnection connection) {
AbstractMap.SimpleEntry<String, String> encryptionValues = getCacheLookupKeys(stmt, connection);
if (encryptionValues.getKey() == null) {
return;
}

cache.removeParamEntry(encryptionValues.getKey());
}

/**
*
* Returns the cache and enclave lookup keys for a given connection and statement
*
* @param statement
* The SQLServer statement used to construct part of the keys
* @param connection
* The connection from which database name is retrieved
* @return A key value pair containing cache lookup key and enclave lookup key
*/
private static AbstractMap.SimpleEntry<String, String> getCacheLookupKeys(SQLServerStatement statement,
SQLServerConnection connection) {

StringBuilder cacheLookupKeyBuilder = new StringBuilder();
cacheLookupKeyBuilder.append(":::");
String databaseName = connection.activeConnectionProperties
.getProperty(SQLServerDriverStringProperty.DATABASE_NAME.toString());
cacheLookupKeyBuilder.append(databaseName);
cacheLookupKeyBuilder.append(":::");
cacheLookupKeyBuilder.append(statement.toString());

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

return new AbstractMap.SimpleEntry<>(cacheLookupKey, enclaveLookupKey);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -126,25 +126,31 @@ private ArrayList<byte[]> describeParameterEncryption(SQLServerConnection connec
ArrayList<String> parameterNames) throws SQLServerException {
ArrayList<byte[]> enclaveRequestedCEKs = new ArrayList<>();
try (PreparedStatement stmt = connection.prepareStatement(connection.enclaveEstablished() ? SDPE1 : SDPE2)) {
try (ResultSet rs = connection.enclaveEstablished() ? executeSDPEv1(stmt, userSql,
preparedTypeDefinitions) : executeSDPEv2(stmt, userSql, preparedTypeDefinitions, aasParams)) {
if (null == rs) {
// No results. Meaning no parameter.
// Should never happen.
return enclaveRequestedCEKs;
}
processSDPEv1(userSql, preparedTypeDefinitions, params, parameterNames, connection, statement, stmt, rs,
enclaveRequestedCEKs);
// Process the third resultset.
if (connection.isAEv2() && stmt.getMoreResults()) {
try (ResultSet hgsRs = (SQLServerResultSet) stmt.getResultSet()) {
if (hgsRs.next()) {
hgsResponse = new AASAttestationResponse(hgsRs.getBytes(1));
// This validates and establishes the enclave session if valid
validateAttestationResponse();
} else {
SQLServerException.makeFromDriverError(null, this,
SQLServerException.getErrString("R_UnableRetrieveParameterMetadata"), "0", false);
// Check the cache for metadata only if we're using AEv1 (without secure enclaves)
if (connection.getServerColumnEncryptionVersion() != ColumnEncryptionVersion.AE_V1
|| !ParameterMetaDataCache.getQueryMetadata(params, parameterNames, enclaveSession.getCryptoCache(),
connection, statement)) {
try (ResultSet rs = connection.enclaveEstablished() ? executeSDPEv1(stmt, userSql,
preparedTypeDefinitions) : executeSDPEv2(stmt, userSql, preparedTypeDefinitions, aasParams)) {
if (null == rs) {
// No results. Meaning no parameter.
// Should never happen.
return enclaveRequestedCEKs;
}
processSDPEv1(userSql, preparedTypeDefinitions, params, parameterNames, connection, statement, stmt,
rs, enclaveRequestedCEKs, enclaveSession);
// Process the third resultset.
if (connection.isAEv2() && stmt.getMoreResults()) {
try (ResultSet hgsRs = (SQLServerResultSet) stmt.getResultSet()) {
if (hgsRs.next()) {
hgsResponse = new AASAttestationResponse(hgsRs.getBytes(1));
// This validates and establishes the enclave session if valid
validateAttestationResponse();
} else {
SQLServerException.makeFromDriverError(null, this,
SQLServerException.getErrString("R_UnableRetrieveParameterMetadata"), "0",
false);
}
}
}
}
Expand Down
Loading