com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionAzureKeyVaultProvider Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mssql-jdbc Show documentation
Show all versions of mssql-jdbc Show documentation
Microsoft JDBC Driver for SQL Server.
/*
* 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 static java.nio.charset.StandardCharsets.UTF_16LE;
import com.azure.core.http.HttpPipeline;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import com.azure.core.credential.TokenCredential;
import com.azure.identity.ManagedIdentityCredentialBuilder;
import com.azure.security.keyvault.keys.KeyClient;
import com.azure.security.keyvault.keys.KeyClientBuilder;
import com.azure.security.keyvault.keys.cryptography.CryptographyClient;
import com.azure.security.keyvault.keys.cryptography.CryptographyClientBuilder;
import com.azure.security.keyvault.keys.cryptography.models.KeyWrapAlgorithm;
import com.azure.security.keyvault.keys.cryptography.models.SignResult;
import com.azure.security.keyvault.keys.cryptography.models.SignatureAlgorithm;
import com.azure.security.keyvault.keys.cryptography.models.UnwrapResult;
import com.azure.security.keyvault.keys.cryptography.models.VerifyResult;
import com.azure.security.keyvault.keys.cryptography.models.WrapResult;
import com.azure.security.keyvault.keys.models.KeyType;
import com.azure.security.keyvault.keys.models.KeyVaultKey;
class CMKMetadataSignatureInfo {
String masterKeyPath;
boolean allowEnclaveComputations;
String signatureHexString;
public CMKMetadataSignatureInfo(String masterKeyPath, boolean allowEnclaveComputations, byte[] signature) {
this.masterKeyPath = masterKeyPath;
this.allowEnclaveComputations = allowEnclaveComputations;
this.signatureHexString = Util.byteToHexDisplayString(signature);
}
public String getMasterKeyPath() {
return masterKeyPath;
}
public boolean isAllowEnclaveComputations() {
return allowEnclaveComputations;
}
public String getSignatureHexString() {
return signatureHexString;
}
@Override
public int hashCode() {
int hash = 7;
hash = 31 * hash + (null != masterKeyPath ? masterKeyPath.hashCode() : 0);
hash = 31 * hash + (allowEnclaveComputations ? 1 : 0);
hash = 31 * hash + (null != signatureHexString ? signatureHexString.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (null != object && CMKMetadataSignatureInfo.class == object.getClass()) {
CMKMetadataSignatureInfo other = (CMKMetadataSignatureInfo) object;
if (hashCode() == other.hashCode()) {
return ((null == masterKeyPath ? null == other.masterKeyPath
: masterKeyPath.equals(other.masterKeyPath))
&& allowEnclaveComputations == other.allowEnclaveComputations
&& (null == signatureHexString ? null == other.signatureHexString
: signatureHexString.equals(other.signatureHexString)));
}
}
return false;
}
}
/**
* Provides implementation similar to certificate store provider. A CEK encrypted with certificate store provider should
* be decryptable by this provider and vice versa.
*
* Envelope Format for the encrypted column encryption key version + keyPathLength + ciphertextLength + keyPath +
* ciphertext + signature version: A single byte indicating the format version. keyPathLength: Length of the keyPath.
* ciphertextLength: ciphertext length keyPath: keyPath used to encrypt the column encryption key. This is only used for
* troubleshooting purposes and is not verified during decryption. ciphertext: Encrypted column encryption key
* signature: Signature of the entire byte array. Signature is validated before decrypting the column encryption key.
*/
public class SQLServerColumnEncryptionAzureKeyVaultProvider extends SQLServerColumnEncryptionKeyStoreProvider {
private final static java.util.logging.Logger akvLogger = java.util.logging.Logger
.getLogger("com.microsoft.sqlserver.jdbc.SQLServerColumnEncryptionAzureKeyVaultProvider");
private static final int KEY_NAME_INDEX = 4;
private static final int KEY_URL_SPLIT_LENGTH_WITH_VERSION = 6;
private static final String KEY_URL_DELIMITER = "/";
private static final String NULL_VALUE = "R_NullValue";
private HttpPipeline keyVaultPipeline;
private KeyVaultTokenCredential keyVaultTokenCredential;
/**
* Column Encryption Key Store Provider string
*/
String name = "AZURE_KEY_VAULT";
private static final String MSSQL_JDBC_PROPERTIES = "mssql-jdbc.properties";
private static final String AKV_TRUSTED_ENDPOINTS_KEYWORD = "AKVTrustedEndpoints";
private static final String RSA_ENCRYPTION_ALGORITHM_WITH_OAEP_FOR_AKV = "RSA-OAEP";
private static final String SHA_256 = "SHA-256";
private static final List akvTrustedEndpoints = getTrustedEndpoints();
/**
* Algorithm version
*/
private static final byte[] firstVersion = new byte[] {0x01};
private Map cachedKeyClients = new ConcurrentHashMap<>();
private Map cachedCryptographyClients = new ConcurrentHashMap<>();
private TokenCredential credential;
/**
* A cache of column encryption keys (once they are unwrapped). This is useful for rapidly decrypting multiple data
* values. The default expiration is set to 2 hours.
*/
private final SimpleTtlCache columnEncryptionKeyCache = new SimpleTtlCache<>();
/**
* A cache for storing the results of signature verification of column master key metadata. The default expiration
* is set to 10 days.
*/
private final SimpleTtlCache cmkMetadataSignatureVerificationCache = new SimpleTtlCache<>(
Duration.ofDays(10));
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
/**
* Returns the time-to-live for items in the columnEncryptionKeyCache.
*
* @return the time-to-live for items in the columnEncryptionKeyCache.
*/
@Override
public Duration getColumnEncryptionKeyCacheTtl() {
return columnEncryptionKeyCache.getCacheTtl();
}
/**
* Sets the the time-to-live for items in the columnEncryptionKeyCache.
*
* @param duration
* value to be set for the time-to-live for items in the columnEncryptionKeyCache.
*/
@Override
public void setColumnEncryptionCacheTtl(Duration duration) {
columnEncryptionKeyCache.setCacheTtl(duration);
}
/**
* Constructs a SQLServerColumnEncryptionAzureKeyVaultProvider to authenticate to AAD using the client id and client
* key. This is used by KeyVault client at runtime to authenticate to Azure Key Vault.
*
* @param clientId
* Identifier of the client requesting the token.
* @param clientKey
* Secret key of the client requesting the token.
* @throws SQLServerException
* when an error occurs
*/
public SQLServerColumnEncryptionAzureKeyVaultProvider(String clientId, String clientKey) throws SQLServerException {
if (null == clientId || clientId.isEmpty()) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
Object[] msgArgs1 = {"Client ID"};
throw new SQLServerException(form.format(msgArgs1), null);
}
if (null == clientKey || clientKey.isEmpty()) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
Object[] msgArgs1 = {"Client Key"};
throw new SQLServerException(form.format(msgArgs1), null);
}
// create a token credential with given client id and secret which internally identifies the tenant id.
keyVaultTokenCredential = new KeyVaultTokenCredential(clientId, clientKey);
// create the pipeline with the custom Key Vault credential
keyVaultPipeline = new KeyVaultHttpPipelineBuilder().credential(keyVaultTokenCredential).buildPipeline();
}
/**
* Constructs a SQLServerColumnEncryptionAzureKeyVaultProvider to authenticate to AAD. This is used by KeyVault
* client at runtime to authenticate to Azure Key Vault.
*
* @throws SQLServerException
* when an error occurs
*/
SQLServerColumnEncryptionAzureKeyVaultProvider() throws SQLServerException {
setCredential(new ManagedIdentityCredentialBuilder().build());
}
/**
* Constructs a SQLServerColumnEncryptionAzureKeyVaultProvider to authenticate to AAD. This is used by KeyVault
* client at runtime to authenticate to Azure Key Vault.
*
* @param clientId
* Identifier of the client requesting the token.
*
* @throws SQLServerException
* when an error occurs
*/
SQLServerColumnEncryptionAzureKeyVaultProvider(String clientId) throws SQLServerException {
if (null == clientId || clientId.isEmpty()) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
Object[] msgArgs1 = {"Client ID"};
throw new SQLServerException(form.format(msgArgs1), null);
}
setCredential(new ManagedIdentityCredentialBuilder().clientId(clientId).build());
}
/**
* Constructs a SQLServerColumnEncryptionAzureKeyVaultProvider using the provided TokenCredential to authenticate to
* AAD. This is used by KeyVault client at runtime to authenticate to Azure Key Vault.
*
* @param tokenCredential
* The TokenCredential to use to authenticate to Azure Key Vault.
*
* @throws SQLServerException
* when an error occurs
*/
public SQLServerColumnEncryptionAzureKeyVaultProvider(TokenCredential tokenCredential) throws SQLServerException {
if (null == tokenCredential) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
Object[] msgArgs1 = {"Token Credential"};
throw new SQLServerException(form.format(msgArgs1), null);
}
setCredential(tokenCredential);
}
/**
* Constructs a SQLServerColumnEncryptionAzureKeyVaultProvider with a callback function to authenticate to AAD. This
* is used by KeyVault client at runtime to authenticate to Azure Key Vault.
*
* This constructor is present to maintain backwards compatibility with 8.0 version of the driver. Deprecated for
* removal in next stable release.
*
* @param authenticationCallback
* - Callback function used for authenticating to AAD.
* @throws SQLServerException
* when an error occurs
*
* @deprecated
*/
@Deprecated(since = "12.1.0", forRemoval = true)
public SQLServerColumnEncryptionAzureKeyVaultProvider(
SQLServerKeyVaultAuthenticationCallback authenticationCallback) throws SQLServerException {
if (null == authenticationCallback) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
Object[] msgArgs1 = {"SQLServerKeyVaultAuthenticationCallback"};
throw new SQLServerException(form.format(msgArgs1), null);
}
keyVaultTokenCredential = new KeyVaultTokenCredential(authenticationCallback);
keyVaultPipeline = new KeyVaultHttpPipelineBuilder().credential(keyVaultTokenCredential).buildPipeline();
}
/**
* Sets the credential that will be used for authenticating requests to Key Vault service.
*
* @param credential
* A credential of type {@link TokenCredential}.
* @throws SQLServerException
* If the credential is null.
*/
private void setCredential(TokenCredential credential) throws SQLServerException {
if (null == credential) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString(NULL_VALUE));
Object[] msgArgs1 = {"Credential"};
throw new SQLServerException(form.format(msgArgs1), null);
}
this.credential = credential;
}
/**
* Decrypts an encrypted CEK with RSA encryption algorithm using the asymmetric key specified by the key path
*
* @param masterKeyPath
* - Complete path of an asymmetric key in AKV
* @param encryptionAlgorithm
* - Asymmetric Key Encryption Algorithm
* @param encryptedColumnEncryptionKey
* - Encrypted Column Encryption Key
* @return Plain text column encryption key
*/
@Override
public byte[] decryptColumnEncryptionKey(String masterKeyPath, String encryptionAlgorithm,
byte[] encryptedColumnEncryptionKey) throws SQLServerException {
// Validate the input parameters
this.validateNonEmptyAKVPath(masterKeyPath);
if (null == encryptedColumnEncryptionKey) {
throw new SQLServerException(SQLServerException.getErrString("R_NullEncryptedColumnEncryptionKey"), null);
}
if (0 == encryptedColumnEncryptionKey.length) {
throw new SQLServerException(SQLServerException.getErrString("R_EmptyEncryptedColumnEncryptionKey"), null);
}
// Validate encryptionAlgorithm
KeyWrapAlgorithm keyWrapAlgorithm = this.validateEncryptionAlgorithm(encryptionAlgorithm);
// Validate whether the key is RSA one or not and then get the key size
int keySizeInBytes = getAKVKeySize(masterKeyPath);
// Validate and decrypt the EncryptedColumnEncryptionKey
// Format is
// version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature
//
// keyPath is present in the encrypted column encryption key for identifying the original source of the
// asymmetric key pair and
// we will not validate it against the data contained in the CMK metadata (masterKeyPath).
// Validate the version byte
if (encryptedColumnEncryptionKey[0] != firstVersion[0]) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_InvalidEcryptionAlgorithmVersion"));
Object[] msgArgs = {String.format("%02X ", encryptedColumnEncryptionKey[0]),
String.format("%02X ", firstVersion[0])};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
// check cache if TimeToLive is not 0
boolean allowCache = false;
String encryptedColumnEncryptionKeyHexString = Util.byteToHexDisplayString(encryptedColumnEncryptionKey);
if (columnEncryptionKeyCache.getCacheTtl().getSeconds() > 0) {
allowCache = true;
if (columnEncryptionKeyCache.contains(encryptedColumnEncryptionKeyHexString)) {
return columnEncryptionKeyCache.get(encryptedColumnEncryptionKeyHexString);
}
}
// Get key path length
int currentIndex = firstVersion.length;
short keyPathLength = convertTwoBytesToShort(encryptedColumnEncryptionKey, currentIndex);
// We just read 2 bytes
currentIndex += 2;
// Get ciphertext length
short cipherTextLength = convertTwoBytesToShort(encryptedColumnEncryptionKey, currentIndex);
currentIndex += 2;
// Skip KeyPath
// KeyPath exists only for troubleshooting purposes and doesnt need validation.
currentIndex += keyPathLength;
// validate the ciphertext length
if (cipherTextLength != keySizeInBytes) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVKeyLengthError"));
Object[] msgArgs = {cipherTextLength, keySizeInBytes, masterKeyPath};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
// Validate the signature length
int signatureLength = encryptedColumnEncryptionKey.length - currentIndex - cipherTextLength;
if (signatureLength != keySizeInBytes) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVSignatureLengthError"));
Object[] msgArgs = {signatureLength, keySizeInBytes, masterKeyPath};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
// Get ciphertext
byte[] cipherText = new byte[cipherTextLength];
System.arraycopy(encryptedColumnEncryptionKey, currentIndex, cipherText, 0, cipherTextLength);
currentIndex += cipherTextLength;
// Get signature
byte[] signature = new byte[signatureLength];
System.arraycopy(encryptedColumnEncryptionKey, currentIndex, signature, 0, signatureLength);
// Compute the hash to validate the signature
byte[] hash = new byte[encryptedColumnEncryptionKey.length - signature.length];
System.arraycopy(encryptedColumnEncryptionKey, 0, hash, 0,
encryptedColumnEncryptionKey.length - signature.length);
MessageDigest md = null;
try {
md = MessageDigest.getInstance(SHA_256);
} catch (NoSuchAlgorithmException e) {
throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e);
}
md.update(hash);
byte[] dataToVerify = md.digest();
if (null == dataToVerify) {
throw new SQLServerException(SQLServerException.getErrString("R_HashNull"), null);
}
// Validate the signature
if (!azureKeyVaultVerifySignature(dataToVerify, signature, masterKeyPath)) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CEKSignatureNotMatchCMK"));
Object[] msgArgs = {Util.byteToHexDisplayString(signature), masterKeyPath};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
// Decrypt the CEK
byte[] decryptedCEK = this.azureKeyVaultUnWrap(masterKeyPath, keyWrapAlgorithm, cipherText);
if (allowCache) {
columnEncryptionKeyCache.put(encryptedColumnEncryptionKeyHexString, decryptedCEK);
}
return decryptedCEK;
}
private short convertTwoBytesToShort(byte[] input, int index) throws SQLServerException {
short shortVal;
if (index + 1 >= input.length) {
throw new SQLServerException(null, SQLServerException.getErrString("R_ByteToShortConversion"), null, 0,
false);
}
ByteBuffer byteBuffer = ByteBuffer.allocate(2);
byteBuffer.order(ByteOrder.LITTLE_ENDIAN);
byteBuffer.put(input[index]);
byteBuffer.put(input[index + 1]);
shortVal = byteBuffer.getShort(0);
return shortVal;
}
/**
* Encrypts CEK with RSA encryption algorithm using the asymmetric key specified by the key path.
*
* @param masterKeyPath
* - Complete path of an asymmetric key in AKV
* @param encryptionAlgorithm
* - Asymmetric Key Encryption Algorithm
* @param columnEncryptionKey
* - Plain text column encryption key
* @return Encrypted column encryption key
*/
@Override
public byte[] encryptColumnEncryptionKey(String masterKeyPath, String encryptionAlgorithm,
byte[] columnEncryptionKey) throws SQLServerException {
// Validate the input parameters
this.validateNonEmptyAKVPath(masterKeyPath);
if (null == columnEncryptionKey) {
throw new SQLServerException(SQLServerException.getErrString("R_NullColumnEncryptionKey"), null);
}
if (0 == columnEncryptionKey.length) {
throw new SQLServerException(SQLServerException.getErrString("R_EmptyCEK"), null);
}
// Validate encryptionAlgorithm
KeyWrapAlgorithm keyWrapAlgorithm = this.validateEncryptionAlgorithm(encryptionAlgorithm);
// Validate whether the key is RSA one or not and then get the key size
int keySizeInBytes = getAKVKeySize(masterKeyPath);
// Construct the encryptedColumnEncryptionKey
// Format is
// version + keyPathLength + ciphertextLength + ciphertext + keyPath + signature
//
// We currently only support one version
byte[] version = new byte[] {firstVersion[0]};
// Get the Unicode encoded bytes of cultureinvariant lower case masterKeyPath
byte[] masterKeyPathBytes = masterKeyPath.toLowerCase(Locale.ENGLISH).getBytes(UTF_16LE);
byte[] keyPathLength = new byte[2];
keyPathLength[0] = (byte) (((short) masterKeyPathBytes.length) & 0xff);
keyPathLength[1] = (byte) (((short) masterKeyPathBytes.length) >> 8 & 0xff);
// Encrypt the plain text
byte[] cipherText = this.azureKeyVaultWrap(masterKeyPath, keyWrapAlgorithm, columnEncryptionKey);
byte[] cipherTextLength = new byte[2];
cipherTextLength[0] = (byte) (((short) cipherText.length) & 0xff);
cipherTextLength[1] = (byte) (((short) cipherText.length) >> 8 & 0xff);
if (cipherText.length != keySizeInBytes) {
throw new SQLServerException(SQLServerException.getErrString("R_CipherTextLengthNotMatchRSASize"), null);
}
// Compute hash
// SHA-2-256(version + keyPathLength + ciphertextLength + keyPath + ciphertext)
byte[] dataToHash = new byte[version.length + keyPathLength.length + cipherTextLength.length
+ masterKeyPathBytes.length + cipherText.length];
int destinationPosition = version.length;
System.arraycopy(version, 0, dataToHash, 0, version.length);
System.arraycopy(keyPathLength, 0, dataToHash, destinationPosition, keyPathLength.length);
destinationPosition += keyPathLength.length;
System.arraycopy(cipherTextLength, 0, dataToHash, destinationPosition, cipherTextLength.length);
destinationPosition += cipherTextLength.length;
System.arraycopy(masterKeyPathBytes, 0, dataToHash, destinationPosition, masterKeyPathBytes.length);
destinationPosition += masterKeyPathBytes.length;
System.arraycopy(cipherText, 0, dataToHash, destinationPosition, cipherText.length);
MessageDigest md = null;
try {
md = MessageDigest.getInstance(SHA_256);
} catch (NoSuchAlgorithmException e) {
throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e);
}
md.update(dataToHash);
byte[] dataToSign = md.digest();
// Sign the hash
byte[] signedHash = azureKeyVaultSignHashedData(dataToSign, masterKeyPath);
if (signedHash.length != keySizeInBytes) {
throw new SQLServerException(SQLServerException.getErrString("R_SignedHashLengthError"), null);
}
if (!this.azureKeyVaultVerifySignature(dataToSign, signedHash, masterKeyPath)) {
throw new SQLServerException(SQLServerException.getErrString("R_InvalidSignatureComputed"), null);
}
// Construct the encrypted column encryption key
// EncryptedColumnEncryptionKey = version + keyPathLength + ciphertextLength + keyPath + ciphertext + signature
int encryptedColumnEncryptionKeyLength = version.length + cipherTextLength.length + keyPathLength.length
+ cipherText.length + masterKeyPathBytes.length + signedHash.length;
byte[] encryptedColumnEncryptionKey = new byte[encryptedColumnEncryptionKeyLength];
// Copy version byte
int currentIndex = 0;
System.arraycopy(version, 0, encryptedColumnEncryptionKey, currentIndex, version.length);
currentIndex += version.length;
// Copy key path length
System.arraycopy(keyPathLength, 0, encryptedColumnEncryptionKey, currentIndex, keyPathLength.length);
currentIndex += keyPathLength.length;
// Copy ciphertext length
System.arraycopy(cipherTextLength, 0, encryptedColumnEncryptionKey, currentIndex, cipherTextLength.length);
currentIndex += cipherTextLength.length;
// Copy key path
System.arraycopy(masterKeyPathBytes, 0, encryptedColumnEncryptionKey, currentIndex, masterKeyPathBytes.length);
currentIndex += masterKeyPathBytes.length;
// Copy ciphertext
System.arraycopy(cipherText, 0, encryptedColumnEncryptionKey, currentIndex, cipherText.length);
currentIndex += cipherText.length;
// copy the signature
System.arraycopy(signedHash, 0, encryptedColumnEncryptionKey, currentIndex, signedHash.length);
return encryptedColumnEncryptionKey;
}
/**
* Validates that the encryption algorithm is RSA_OAEP and if it is not, then throws an exception.
*
* @param encryptionAlgorithm
* - Asymmetric key encryptio algorithm
* @return The encryption algorithm that is going to be used.
* @throws SQLServerException
*/
private KeyWrapAlgorithm validateEncryptionAlgorithm(String encryptionAlgorithm) throws SQLServerException {
if (null == encryptionAlgorithm) {
throw new SQLServerException(null, SQLServerException.getErrString("R_NullKeyEncryptionAlgorithm"), null, 0,
false);
}
// Transform to standard format (dash instead of underscore) to support enum lookup
if ("RSA_OAEP".equalsIgnoreCase(encryptionAlgorithm)) {
encryptionAlgorithm = RSA_ENCRYPTION_ALGORITHM_WITH_OAEP_FOR_AKV;
}
if (!RSA_ENCRYPTION_ALGORITHM_WITH_OAEP_FOR_AKV.equalsIgnoreCase(encryptionAlgorithm.trim())) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_InvalidKeyEncryptionAlgorithm"));
Object[] msgArgs = {encryptionAlgorithm, RSA_ENCRYPTION_ALGORITHM_WITH_OAEP_FOR_AKV};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
return KeyWrapAlgorithm.fromString(encryptionAlgorithm);
}
/**
* Checks if the Azure Key Vault key path is Empty or Null (and raises exception if they are).
*
* @param masterKeyPath
* @throws SQLServerException
*/
private void validateNonEmptyAKVPath(String masterKeyPath) throws SQLServerException {
// throw appropriate error if masterKeyPath is null or empty
if (null == masterKeyPath || masterKeyPath.trim().isEmpty()) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVPathNull"));
Object[] msgArgs = {masterKeyPath};
throw new SQLServerException(null, form.format(msgArgs), null, 0, false);
} else {
URI parsedUri = null;
try {
parsedUri = new URI(masterKeyPath);
// A valid URI.
// Check if it is pointing to a trusted endpoint.
String host = parsedUri.getHost();
if (null != host) {
host = host.toLowerCase(Locale.ENGLISH);
}
for (final String endpoint : akvTrustedEndpoints) {
if (null != host && host.endsWith(endpoint)) {
return;
}
}
} catch (URISyntaxException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVURLInvalid"));
Object[] msgArgs = {masterKeyPath};
throw new SQLServerException(form.format(msgArgs), null, 0, e);
}
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVMasterKeyPathInvalid"));
Object[] msgArgs = {masterKeyPath};
throw new SQLServerException(null, form.format(msgArgs), null, 0, false);
}
}
/**
* Encrypts the text using specified Azure Key Vault key.
*
* @param masterKeyPath
* - Azure Key Vault key url.
* @param encryptionAlgorithm
* - Encryption Algorithm.
* @param columnEncryptionKey
* - Plain text Column Encryption Key.
* @return Returns an encrypted blob or throws an exception if there are any errors.
* @throws SQLServerException
*/
private byte[] azureKeyVaultWrap(String masterKeyPath, KeyWrapAlgorithm encryptionAlgorithm,
byte[] columnEncryptionKey) throws SQLServerException {
if (null == columnEncryptionKey) {
throw new SQLServerException(SQLServerException.getErrString("R_CEKNull"), null);
}
CryptographyClient cryptoClient = getCryptographyClient(masterKeyPath);
WrapResult wrappedKey = cryptoClient.wrapKey(KeyWrapAlgorithm.RSA_OAEP, columnEncryptionKey);
return wrappedKey.getEncryptedKey();
}
/**
* Encrypts the text using specified Azure Key Vault key.
*
* @param masterKeyPath
* - Azure Key Vault key url.
* @param encryptionAlgorithm
* - Encrypted Column Encryption Key.
* @param encryptedColumnEncryptionKey
* - Encrypted Column Encryption Key.
* @return Returns the decrypted plaintext Column Encryption Key or throws an exception if there are any errors.
* @throws SQLServerException
*/
private byte[] azureKeyVaultUnWrap(String masterKeyPath, KeyWrapAlgorithm encryptionAlgorithm,
byte[] encryptedColumnEncryptionKey) throws SQLServerException {
if (null == encryptedColumnEncryptionKey) {
throw new SQLServerException(SQLServerException.getErrString("R_EncryptedCEKNull"), null);
}
if (0 == encryptedColumnEncryptionKey.length) {
throw new SQLServerException(SQLServerException.getErrString("R_EmptyEncryptedCEK"), null);
}
CryptographyClient cryptoClient = getCryptographyClient(masterKeyPath);
UnwrapResult unwrappedKey = cryptoClient.unwrapKey(encryptionAlgorithm, encryptedColumnEncryptionKey);
return unwrappedKey.getKey();
}
private CryptographyClient getCryptographyClient(String masterKeyPath) throws SQLServerException {
if (this.cachedCryptographyClients.containsKey(masterKeyPath)) {
return cachedCryptographyClients.get(masterKeyPath);
}
KeyVaultKey retrievedKey = getKeyVaultKey(masterKeyPath);
CryptographyClient cryptoClient;
if (null != credential) {
cryptoClient = new CryptographyClientBuilder().credential(credential).keyIdentifier(retrievedKey.getId())
.buildClient();
} else {
cryptoClient = new CryptographyClientBuilder().pipeline(keyVaultPipeline)
.keyIdentifier(retrievedKey.getId()).buildClient();
}
cachedCryptographyClients.putIfAbsent(masterKeyPath, cryptoClient);
return cachedCryptographyClients.get(masterKeyPath);
}
/**
* Generates signature based on RSA PKCS#v1.5 scheme using a specified Azure Key Vault Key URL.
*
* @param dataToSign
* - Text to sign.
* @param masterKeyPath
* - Azure Key Vault key url.
* @return Signature
* @throws SQLServerException
*/
private byte[] azureKeyVaultSignHashedData(byte[] dataToSign, String masterKeyPath) throws SQLServerException {
assert ((null != dataToSign) && (0 != dataToSign.length));
CryptographyClient cryptoClient = getCryptographyClient(masterKeyPath);
SignResult signedData = cryptoClient.sign(SignatureAlgorithm.RS256, dataToSign);
return signedData.getSignature();
}
/**
* Verifies the given RSA PKCSv1.5 signature.
*
* @param dataToVerify
* @param signature
* @param masterKeyPath
* - Azure Key Vault key url.
* @return true if signature is valid, false if it is not valid
* @throws SQLServerException
*/
private boolean azureKeyVaultVerifySignature(byte[] dataToVerify, byte[] signature,
String masterKeyPath) throws SQLServerException {
assert ((null != dataToVerify) && (0 != dataToVerify.length));
assert ((null != signature) && (0 != signature.length));
CryptographyClient cryptoClient = getCryptographyClient(masterKeyPath);
VerifyResult valid = cryptoClient.verify(SignatureAlgorithm.RS256, dataToVerify, signature);
return valid.isValid();
}
/**
* Returns the public Key size in bytes.
*
* @param masterKeyPath
* - Azure Key Vault Key path
* @return Key size in bytes
* @throws SQLServerException
* when an error occurs
*/
private int getAKVKeySize(String masterKeyPath) throws SQLServerException {
KeyVaultKey retrievedKey = getKeyVaultKey(masterKeyPath);
return retrievedKey.getKey().getN().length;
}
/**
* Fetches the key from Azure Key Vault for given key path. If the key path includes a version, then that specific
* version of the key is retrieved, otherwise the latest key will be retrieved.
*
* @param masterKeyPath
* The key path associated with the key
* @return The Key Vault key.
* @throws SQLServerException
* If there was an error retrieving the key from Key Vault.
*/
private KeyVaultKey getKeyVaultKey(String masterKeyPath) throws SQLServerException {
String[] keyTokens = masterKeyPath.split(KEY_URL_DELIMITER);
String keyName = keyTokens[KEY_NAME_INDEX];
String keyVersion = null;
if (keyTokens.length == KEY_URL_SPLIT_LENGTH_WITH_VERSION) {
keyVersion = keyTokens[keyTokens.length - 1];
}
try {
KeyClient keyClient = getKeyClient(masterKeyPath);
KeyVaultKey retrievedKey;
if (null != keyVersion) {
retrievedKey = keyClient.getKey(keyName, keyVersion);
} else {
retrievedKey = keyClient.getKey(keyName);
}
if (null == retrievedKey) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_AKVKeyNotFound"));
Object[] msgArgs = {keyTokens[keyTokens.length - 1]};
throw new SQLServerException(null, form.format(msgArgs), null, 0, false);
}
if (retrievedKey.getKeyType() != KeyType.RSA && retrievedKey.getKeyType() != KeyType.RSA_HSM) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_NonRSAKey"));
Object[] msgArgs = {retrievedKey.getKeyType().toString()};
throw new SQLServerException(null, form.format(msgArgs), null, 0, false);
}
return retrievedKey;
} catch (RuntimeException e) {
throw new SQLServerException(e.getMessage(), e);
}
}
/**
* Creates a new {@link KeyClient} if one does not exist for the given key path. If the client already exists, the
* client is returned from the cache. As the client is stateless, it's safe to cache the client for each key path.
*
* @param masterKeyPath
* The key path for which the {@link KeyClient} will be created, if it does not exist.
* @return The {@link KeyClient} associated with the key path.
*/
private KeyClient getKeyClient(String masterKeyPath) {
if (cachedKeyClients.containsKey(masterKeyPath)) {
return cachedKeyClients.get(masterKeyPath);
}
String vaultUrl = getVaultUrl(masterKeyPath);
KeyClient keyClient;
if (null != credential) {
keyClient = new KeyClientBuilder().credential(credential).vaultUrl(vaultUrl).buildClient();
} else {
keyClient = new KeyClientBuilder().pipeline(keyVaultPipeline).vaultUrl(vaultUrl).buildClient();
}
cachedKeyClients.putIfAbsent(masterKeyPath, keyClient);
return cachedKeyClients.get(masterKeyPath);
}
/**
* Returns the vault url extracted from the master key path.
*
* @param masterKeyPath
* The master key path.
* @return The vault url.
*/
private static String getVaultUrl(String masterKeyPath) {
String[] keyTokens = masterKeyPath.split("/");
String hostName = keyTokens[2];
return "https://" + hostName;
}
@Override
public boolean verifyColumnMasterKeyMetadata(String masterKeyPath, boolean allowEnclaveComputations,
byte[] signature) throws SQLServerException {
if (!allowEnclaveComputations) {
return false;
}
KeyStoreProviderCommon.validateNonEmptyMasterKeyPath(masterKeyPath);
CMKMetadataSignatureInfo key = new CMKMetadataSignatureInfo(masterKeyPath, allowEnclaveComputations, signature);
if (cmkMetadataSignatureVerificationCache.contains(key)) {
return cmkMetadataSignatureVerificationCache.get(key);
}
byte[] signedHash = null;
boolean isValid = false;
try {
MessageDigest md = MessageDigest.getInstance(SHA_256);
md.update(name.toLowerCase().getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
md.update(masterKeyPath.toLowerCase().getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
// value of allowEnclaveComputations is always true here
md.update("true".getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
byte[] dataToVerify = md.digest();
if (null == dataToVerify) {
throw new SQLServerException(SQLServerException.getErrString("R_HashNull"), null);
}
// Sign the hash
signedHash = azureKeyVaultSignHashedData(dataToVerify, masterKeyPath);
if (null == signedHash) {
throw new SQLServerException(SQLServerException.getErrString("R_SignedHashLengthError"), null);
}
// Validate the signature
isValid = azureKeyVaultVerifySignature(dataToVerify, signature, masterKeyPath);
cmkMetadataSignatureVerificationCache.put(key, isValid);
} catch (NoSuchAlgorithmException e) {
throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e);
} catch (SQLServerException e) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_SignatureNotMatch"));
Object[] msgArgs = {Util.byteToHexDisplayString(signature),
(signedHash != null) ? Util.byteToHexDisplayString(signedHash) : " ", masterKeyPath,
": " + e.getMessage()};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
if (!isValid) {
MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_SignatureNotMatch"));
Object[] msgArgs = {Util.byteToHexDisplayString(signature), Util.byteToHexDisplayString(signedHash),
masterKeyPath, ""};
throw new SQLServerException(this, form.format(msgArgs), null, 0, false);
}
return isValid;
}
/**
* Sign column master key metadata
*
* @param masterKeyPath
* master key path
*
* @param allowEnclaveComputations
* flag whether to allow enclave computations
*
* @return
* column master key metadata
*
* @throws SQLServerException
* when an error occurs
*
*/
public byte[] signColumnMasterKeyMetadata(String masterKeyPath,
boolean allowEnclaveComputations) throws SQLServerException {
if (!allowEnclaveComputations) {
return null;
}
KeyStoreProviderCommon.validateNonEmptyMasterKeyPath(masterKeyPath);
byte[] signedHash = null;
try {
MessageDigest md = MessageDigest.getInstance(SHA_256);
md.update(name.toLowerCase().getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
md.update(masterKeyPath.toLowerCase().getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
// value of allowEnclaveComputations is always true here
md.update("true".getBytes(java.nio.charset.StandardCharsets.UTF_16LE));
byte[] dataToVerify = md.digest();
if (null == dataToVerify) {
throw new SQLServerException(SQLServerException.getErrString("R_HashNull"), null);
}
// Sign the hash
signedHash = azureKeyVaultSignHashedData(dataToVerify, masterKeyPath);
} catch (NoSuchAlgorithmException e) {
throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e);
}
if (null == signedHash) {
throw new SQLServerException(SQLServerException.getErrString("R_SignedHashLengthError"), null);
}
return signedHash;
}
private static List getTrustedEndpoints() {
Properties mssqlJdbcProperties = getMssqlJdbcProperties();
List trustedEndpoints = new ArrayList<>();
boolean append = true;
if (null != mssqlJdbcProperties) {
String endpoints = mssqlJdbcProperties.getProperty(AKV_TRUSTED_ENDPOINTS_KEYWORD);
if (null != endpoints && !endpoints.trim().isEmpty()) {
endpoints = endpoints.trim();
// Append if the list starts with a semicolon.
if (';' != endpoints.charAt(0)) {
append = false;
} else {
endpoints = endpoints.substring(1);
}
String[] entries = endpoints.split(";");
for (String entry : entries) {
if (null != entry && !entry.trim().isEmpty()) {
trustedEndpoints.add(entry.trim());
}
}
}
}
/*
* List of Azure trusted endpoints
* https://docs.microsoft.com/en-us/azure/key-vault/key-vault-secure-your-key-vault
*/
if (append) {
trustedEndpoints.add("vault.azure.net");
trustedEndpoints.add("vault.azure.cn");
trustedEndpoints.add("vault.usgovcloudapi.net");
trustedEndpoints.add("vault.microsoftazure.de");
trustedEndpoints.add("managedhsm.azure.net");
trustedEndpoints.add("managedhsm.azure.cn");
trustedEndpoints.add("managedhsm.usgovcloudapi.net");
trustedEndpoints.add("managedhsm.microsoftazure.de");
}
return trustedEndpoints;
}
/**
* Attempt to read MSSQL_JDBC_PROPERTIES.
*
* @return corresponding Properties object or null if failed to read the file.
*/
private static Properties getMssqlJdbcProperties() {
Properties props = null;
try (FileInputStream in = new FileInputStream(MSSQL_JDBC_PROPERTIES)) {
props = new Properties();
props.load(in);
} catch (IOException e) {
if (akvLogger.isLoggable(Level.FINER)) {
akvLogger.finer("Unable to load the mssql-jdbc.properties file: " + e);
}
}
return (null != props && !props.isEmpty()) ? props : null;
}
int getColumnEncryptionKeyCacheSize() {
return columnEncryptionKeyCache.getCacheSize();
}
int getCmkMetadataSignatureVerificationCacheSize() {
return cmkMetadataSignatureVerificationCache.getCacheSize();
}
}