com.microsoft.sqlserver.jdbc.ISQLServerEnclaveProvider 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 java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.GeneralSecurityException;
import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.interfaces.ECPrivateKey;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.text.MessageFormat;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import javax.crypto.KeyAgreement;
/**
*
* Provides an interface to create an Enclave Session
*
*/
interface ISQLServerEnclaveProvider {
/**
* Get the Enclave package
*
* @param userSQL
* user sql
* @param enclaveCEKs
* enclave CEKs
* @return the enclave package
* @throws SQLServerException
* if error
*/
default byte[] getEnclavePackage(String userSQL, ArrayList enclaveCEKs) throws SQLServerException {
EnclaveSession enclaveSession = getEnclaveSession();
if (null != enclaveSession) {
try {
ByteArrayOutputStream enclavePackage = new ByteArrayOutputStream();
enclavePackage.write(enclaveSession.getSessionID());
ByteArrayOutputStream keys = new ByteArrayOutputStream();
byte[] randomGUID = new byte[16];
new SecureRandom().nextBytes(randomGUID);
keys.write(randomGUID);
keys.write(ByteBuffer.allocate(8).order(ByteOrder.LITTLE_ENDIAN).putLong(enclaveSession.getCounter())
.array());
keys.write(MessageDigest.getInstance("SHA-256").digest((userSQL).getBytes(UTF_16LE)));
for (byte[] b : enclaveCEKs) {
keys.write(b);
}
enclaveCEKs.clear();
SQLServerAeadAes256CbcHmac256EncryptionKey encryptedKey = new SQLServerAeadAes256CbcHmac256EncryptionKey(
enclaveSession.getSessionSecret(),
SQLServerAeadAes256CbcHmac256Algorithm.AEAD_AES_256_CBC_HMAC_SHA256);
SQLServerAeadAes256CbcHmac256Algorithm algo = new SQLServerAeadAes256CbcHmac256Algorithm(encryptedKey,
SQLServerEncryptionType.RANDOMIZED, (byte) 0x1);
enclavePackage.write(algo.encryptData(keys.toByteArray()));
return enclavePackage.toByteArray();
} catch (GeneralSecurityException | SQLServerException | IOException e) {
SQLServerException.makeFromDriverError(null, this, e.getLocalizedMessage(), "0", false);
}
}
return null;
}
/**
* Execute sp_describe_parameter_encryption for AEv2
*
* @param stmt
* statement
* @param userSql
* user sql
* @param preparedTypeDefinitions
* prepared type definitions
* @param req
* request
* @return result set
* @throws SQLException
* if error
* @throws IOException
* if IO exception
*/
default ResultSet executeSDPEv2(PreparedStatement stmt, String userSql, String preparedTypeDefinitions,
BaseAttestationRequest req) throws SQLException, IOException {
((SQLServerPreparedStatement) stmt).isInternalEncryptionQuery = true;
stmt.setNString(1, userSql);
if (preparedTypeDefinitions != null && preparedTypeDefinitions.length() != 0) {
stmt.setNString(2, preparedTypeDefinitions);
} else {
stmt.setNString(2, "");
}
stmt.setBytes(3, req.getBytes());
return ((SQLServerPreparedStatement) stmt).executeQueryInternal();
}
/**
* Execute sp_describe_parameter_encryption
*
* @param stmt
* stmt
* @param userSql
* user sql
* @param preparedTypeDefinitions
* prepared type definitions
* @return result set
* @throws SQLException
* if error
*/
default ResultSet executeSDPEv1(PreparedStatement stmt, String userSql,
String preparedTypeDefinitions) throws SQLException {
((SQLServerPreparedStatement) stmt).isInternalEncryptionQuery = true;
stmt.setNString(1, userSql);
if (preparedTypeDefinitions != null && preparedTypeDefinitions.length() != 0) {
stmt.setNString(2, preparedTypeDefinitions);
} else {
stmt.setNString(2, "");
}
return ((SQLServerPreparedStatement) stmt).executeQueryInternal();
}
/**
* Process result from sp_describe_parameter_encryption
*
* @param userSql
* user sql
* @param preparedTypeDefinitions
* prepared type definitions
* @param params
* params
* @param parameterNames
* param names
* @param connection
* connection
* @param sqlServerStatement
* sqlServerStatement
* @param stmt
* statement
* @param rs
* result set
* @param enclaveRequestedCEKs
* enclave requested CEKs
* @throws SQLException
* if error
*/
default void processSDPEv1(String userSql, String preparedTypeDefinitions, Parameter[] params,
ArrayList parameterNames, SQLServerConnection connection, SQLServerStatement sqlServerStatement,
PreparedStatement stmt, ResultSet rs, ArrayList enclaveRequestedCEKs) throws SQLException {
Map cekList = new HashMap<>();
CekTableEntry cekEntry = null;
boolean isRequestedByEnclave = false;
SQLServerStatement statement = (SQLServerStatement) ((SQLServerPreparedStatement) stmt);
if (null != sqlServerStatement && sqlServerStatement.hasColumnEncryptionKeyStoreProvidersRegistered()) {
statement.registerColumnEncryptionKeyStoreProvidersOnStatement(
sqlServerStatement.statementColumnEncryptionKeyStoreProviders);
}
while (rs.next()) {
int currentOrdinal = rs.getInt(DescribeParameterEncryptionResultSet1.KEYORDINAL.value());
if (!cekList.containsKey(currentOrdinal)) {
cekEntry = new CekTableEntry(currentOrdinal);
cekList.put(cekEntry.ordinal, cekEntry);
} else {
cekEntry = cekList.get(currentOrdinal);
}
String keyStoreName = rs.getString(DescribeParameterEncryptionResultSet1.PROVIDERNAME.value());
String algo = rs.getString(DescribeParameterEncryptionResultSet1.KEYENCRYPTIONALGORITHM.value());
String keyPath = rs.getString(DescribeParameterEncryptionResultSet1.KEYPATH.value());
int dbID = rs.getInt(DescribeParameterEncryptionResultSet1.DBID.value());
byte[] mdVer = rs.getBytes(DescribeParameterEncryptionResultSet1.KEYMDVERSION.value());
int keyID = rs.getInt(DescribeParameterEncryptionResultSet1.KEYID.value());
byte[] encryptedKey = rs.getBytes(DescribeParameterEncryptionResultSet1.ENCRYPTEDKEY.value());
cekEntry.add(encryptedKey, dbID, keyID, rs.getInt(DescribeParameterEncryptionResultSet1.KEYVERSION.value()),
mdVer, keyPath, keyStoreName, algo);
// servers supporting enclave computations should always return a boolean indicating whether the key
// is
// required by enclave or not.
if (ColumnEncryptionVersion.AE_V2.value() <= connection.getServerColumnEncryptionVersion().value()) {
isRequestedByEnclave = rs
.getBoolean(DescribeParameterEncryptionResultSet1.ISREQUESTEDBYENCLAVE.value());
}
if (isRequestedByEnclave) {
byte[] keySignature = rs.getBytes(DescribeParameterEncryptionResultSet1.ENCLAVECMKSIGNATURE.value());
String serverName = connection.getTrustedServerNameAE();
SQLServerSecurityUtility.verifyColumnMasterKeyMetadata(connection, statement, keyStoreName, keyPath,
serverName, isRequestedByEnclave, keySignature);
// DBID(4) + MDVER(8) + KEYID(2) + CEK(32) = 46
ByteBuffer aev2CekEntry = ByteBuffer.allocate(46);
aev2CekEntry.order(ByteOrder.LITTLE_ENDIAN).putInt(dbID);
aev2CekEntry.put(mdVer);
aev2CekEntry.putShort((short) keyID);
SQLServerColumnEncryptionKeyStoreProvider provider = SQLServerSecurityUtility
.getColumnEncryptionKeyStoreProvider(keyStoreName, connection, statement);
aev2CekEntry.put(provider.decryptColumnEncryptionKey(keyPath, algo, encryptedKey));
enclaveRequestedCEKs.add(aev2CekEntry.array());
}
}
// Process the second resultset.
if (!stmt.getMoreResults()) {
throw new SQLServerException(null, SQLServerException.getErrString("R_UnexpectedDescribeParamFormat"), null,
0, false);
}
try (ResultSet rs2 = stmt.getResultSet()) {
while (rs2.next() && null != params) {
String paramName = rs2.getString(DescribeParameterEncryptionResultSet2.PARAMETERNAME.value());
int paramIndex = parameterNames.indexOf(paramName);
int cekOrdinal = rs2.getInt(DescribeParameterEncryptionResultSet2.COLUMNENCRYPTIONKEYORDINAL.value());
cekEntry = cekList.get(cekOrdinal);
// cekEntry will be null if none of the parameters are encrypted.
if ((null != cekEntry) && (cekList.size() < cekOrdinal)) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_InvalidEncryptionKeyOrdinal"));
Object[] msgArgs = {cekOrdinal, cekEntry.getSize()};
throw new SQLServerException(null, form.format(msgArgs), null, 0, false);
}
SQLServerEncryptionType encType = SQLServerEncryptionType
.of((byte) rs2.getInt(DescribeParameterEncryptionResultSet2.COLUMNENCRYPTIONTYPE.value()));
if (SQLServerEncryptionType.PLAINTEXT != encType) {
params[paramIndex].cryptoMeta = new CryptoMetadata(cekEntry, (short) cekOrdinal,
(byte) rs2.getInt(DescribeParameterEncryptionResultSet2.COLUMNENCRYPTIONALGORITHM.value()),
null, encType.value,
(byte) rs2.getInt(DescribeParameterEncryptionResultSet2.NORMALIZATIONRULEVERSION.value()));
// Decrypt the symmetric key.(This will also validate and throw if needed).
SQLServerSecurityUtility.decryptSymmetricKey(params[paramIndex].cryptoMeta, connection, statement);
} else {
if (params[paramIndex].getForceEncryption()) {
MessageFormat form = new MessageFormat(
SQLServerException.getErrString("R_ForceEncryptionTrue_HonorAETrue_UnencryptedColumn"));
Object[] msgArgs = {userSql, paramIndex + 1};
SQLServerException.makeFromDriverError(null, connection, form.format(msgArgs), "0", true);
}
}
}
}
// We only add if we're using Always Encrypted v1 / v3 AND we have something to add.
if (connection.getServerColumnEncryptionVersion() != ColumnEncryptionVersion.AE_V2 && params != null
&& params.length > 0) {
ParameterMetaDataCache.addQueryMetadata(params, parameterNames, connection, sqlServerStatement, userSql);
}
}
/**
* Returns the attestation parameters
*
* @param url
* attestation url
* @throws SQLServerException
* when an error occurs.
*/
void getAttestationParameters(String url) throws SQLServerException;
/**
* Creates the enclave session
*
* @param connection
* connection
* @param statement
* statement
* @param userSql
* user sql
* @param preparedTypeDefinitions
* preparedTypeDefinitions
* @param params
* params
* @param parameterNames
* parameterNames
* @return list of enclave requested CEKs
* @throws SQLServerException
* when an error occurs.
*/
ArrayList createEnclaveSession(SQLServerConnection connection, SQLServerStatement statement, String userSql,
String preparedTypeDefinitions, Parameter[] params,
ArrayList parameterNames) throws SQLServerException;
/**
* Invalidates an enclave session
*/
void invalidateEnclaveSession();
/**
* Returns the enclave session
*
* @return the enclave session
*/
EnclaveSession getEnclaveSession();
}
abstract class BaseAttestationRequest {
protected static final byte[] ECDH_MAGIC = {0x45, 0x43, 0x4b, 0x33, 0x30, 0x00, 0x00, 0x00};
protected static final int ENCLAVE_LENGTH = 104;
protected static final int BIG_INTEGER_SIZE = 48;
protected PrivateKey privateKey;
protected byte[] enclaveChallenge;
protected byte[] x;
protected byte[] y;
byte[] getBytes() throws IOException {
return null;
}
byte[] createSessionSecret(byte[] serverResponse) throws GeneralSecurityException, SQLServerException {
if (serverResponse == null || serverResponse.length != ENCLAVE_LENGTH) {
SQLServerException.makeFromDriverError(null, this,
SQLServerResource.getResource("R_MalformedECDHPublicKey"), "0", false);
}
ByteBuffer sr = ByteBuffer.wrap(serverResponse);
byte[] magic = new byte[8];
sr.get(magic);
if (!Arrays.equals(magic, ECDH_MAGIC)) {
SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_MalformedECDHHeader"),
"0", false);
}
byte[] xx = new byte[BIG_INTEGER_SIZE];
byte[] yy = new byte[BIG_INTEGER_SIZE];
sr.get(xx);
sr.get(yy);
/*
* Server returns X and Y coordinates, create a key using the point of the server and our key parameters.
* Public/Private key parameters are the same.
*/
ECPublicKeySpec keySpec = new ECPublicKeySpec(new ECPoint(new BigInteger(1, xx), new BigInteger(1, yy)),
((ECPrivateKey) privateKey).getParams());
KeyAgreement ka = KeyAgreement.getInstance("ECDH");
ka.init(privateKey);
// Generate a PublicKey from the above key specifications and do an agreement with our PrivateKey
ka.doPhase(KeyFactory.getInstance("EC").generatePublic(keySpec), true);
// Generate a Secret from the agreement and hash with SHA-256 to create Session Secret
return MessageDigest.getInstance("SHA-256").digest(ka.generateSecret());
}
void initBcryptECDH() throws SQLServerException {
/*
* Create our BCRYPT_ECCKEY_BLOB
*/
try {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
kpg.initialize(new ECGenParameterSpec("secp384r1"));
KeyPair kp = kpg.generateKeyPair();
ECPublicKey publicKey = (ECPublicKey) kp.getPublic();
privateKey = kp.getPrivate();
ECPoint w = publicKey.getW();
x = adjustBigInt(w.getAffineX().toByteArray());
y = adjustBigInt(w.getAffineY().toByteArray());
} catch (GeneralSecurityException | IOException e) {
SQLServerException.makeFromDriverError(null, this, e.getLocalizedMessage(), "0", false);
}
}
/*
* We need our BigInts to be 48 bytes
*/
private byte[] adjustBigInt(byte[] b) throws IOException {
if (0 == b[0] && BIG_INTEGER_SIZE < b.length) {
b = Arrays.copyOfRange(b, 1, b.length);
}
if (b.length < BIG_INTEGER_SIZE) {
ByteArrayOutputStream output = new ByteArrayOutputStream();
for (int i = 0; i < BIG_INTEGER_SIZE - b.length; i++) {
output.write(0);
}
output.write(b);
b = output.toByteArray();
}
return b;
}
}
abstract class BaseAttestationResponse {
protected int totalSize;
protected int identitySize;
protected int attestationTokenSize;
protected int enclaveType;
protected byte[] enclavePK;
protected int sessionInfoSize;
protected byte[] sessionID = new byte[8];
protected int dhpkSize;
protected int dhpkSsize;
protected byte[] dhPublicKey;
protected byte[] publicKeySig;
@SuppressWarnings("unused")
void validateDHPublicKey() throws SQLServerException, GeneralSecurityException {
/*-
* Java doesn't directly support PKCS1 padding for RSA keys. Parse the key bytes and create a RSAPublicKeySpec
* with the exponent and modulus.
*
* Static string "RSA1" - 4B (Unused)
* Bit count - 4B (Unused)
* Public Exponent Length - 4B
* Public Modulus Length - 4B
* Prime 1 - 4B (Unused)
* Prime 2 - 4B (Unused)
* Exponent - publicExponentLength bytes
* Modulus - publicModulusLength bytes
*/
ByteBuffer enclavePKBuffer = ByteBuffer.wrap(enclavePK).order(ByteOrder.LITTLE_ENDIAN);
byte[] rsa1 = new byte[4];
enclavePKBuffer.get(rsa1);
enclavePKBuffer.getInt(); // bit count unused
int publicExponentLength = enclavePKBuffer.getInt();
int publicModulusLength = enclavePKBuffer.getInt();
enclavePKBuffer.getInt(); // prime 1 unused
enclavePKBuffer.getInt(); // prime 2 unused
byte[] exponent = new byte[publicExponentLength];
enclavePKBuffer.get(exponent);
byte[] modulus = new byte[publicModulusLength];
enclavePKBuffer.get(modulus);
if (enclavePKBuffer.remaining() != 0) {
SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_EnclavePKLengthError"),
"0", false);
}
RSAPublicKeySpec spec = new RSAPublicKeySpec(new BigInteger(1, modulus), new BigInteger(1, exponent));
KeyFactory factory = KeyFactory.getInstance("RSA");
PublicKey pub = factory.generatePublic(spec);
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initVerify(pub);
sig.update(dhPublicKey);
if (!sig.verify(publicKeySig)) {
SQLServerException.makeFromDriverError(null, this, SQLServerResource.getResource("R_InvalidDHKeySignature"),
"0", false);
}
}
byte[] getDHpublicKey() {
return dhPublicKey;
}
byte[] getSessionID() {
return sessionID;
}
}
class EnclaveSession {
private byte[] sessionID;
private AtomicLong counter;
private byte[] sessionSecret;
EnclaveSession(byte[] cs, byte[] b) {
sessionID = cs;
sessionSecret = b;
counter = new AtomicLong(0);
}
byte[] getSessionID() {
return sessionID;
}
byte[] getSessionSecret() {
return sessionSecret;
}
long getCounter() {
return counter.getAndIncrement();
}
}
final class EnclaveSessionCache {
private ConcurrentHashMap sessionCache;
EnclaveSessionCache() {
sessionCache = new ConcurrentHashMap<>(0);
}
void addEntry(String servername, String catalog, String attestationUrl, BaseAttestationRequest b,
EnclaveSession e) {
StringBuilder sb = new StringBuilder(servername).append(catalog).append(attestationUrl);
sessionCache.put(sb.toString(), new EnclaveCacheEntry(b, e));
}
void removeEntry(EnclaveSession e) {
for (Entry entry : sessionCache.entrySet()) {
EnclaveCacheEntry ece = entry.getValue();
if (Arrays.equals(ece.getEnclaveSession().getSessionID(), e.getSessionID())) {
sessionCache.remove(entry.getKey());
}
}
}
EnclaveCacheEntry getSession(String key) {
EnclaveCacheEntry e = sessionCache.get(key);
if (null != e && e.expired()) {
sessionCache.remove(key);
return null;
}
return e;
}
}
class EnclaveCacheEntry {
private static final long EIGHT_HOURS_IN_SECONDS = 28800;
private BaseAttestationRequest bar;
private EnclaveSession es;
private long timeCreatedInSeconds;
EnclaveCacheEntry(BaseAttestationRequest b, EnclaveSession e) {
bar = b;
es = e;
timeCreatedInSeconds = Instant.now().getEpochSecond();
}
boolean expired() {
return (Instant.now().getEpochSecond() - timeCreatedInSeconds) > EIGHT_HOURS_IN_SECONDS;
}
BaseAttestationRequest getBaseAttestationRequest() {
return bar;
}
EnclaveSession getEnclaveSession() {
return es;
}
}