diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b3b3eef1..b0bc8192f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) +## [12.4.0] Stable Release +### Fixed issues +- Revert #2051 (Big decimal precision) / #2116 (Fix for bigDecimal values between 0 and 1 having too high of a precision) [#2176](https://github.com/microsoft/mssql-jdbc/pull/2176) +- Fixed server certificate validation for encrypt=strict [#2174](https://github.com/microsoft/mssql-jdbc/pull/2174) +- Fixed issues identified by SonarQube [#2145](https://github.com/microsoft/mssql-jdbc/pull/2145) + ## [12.3.1] Preview Release ### Added - Added a case to throw XAER_RMFAIL on connection reset for XA transactions [2118](https://github.com/microsoft/mssql-jdbc/pull/2118) diff --git a/README.md b/README.md index 2096a1b72..8dd8a9aee 100644 --- a/README.md +++ b/README.md @@ -11,13 +11,13 @@ The Microsoft JDBC Driver for SQL Server is a Type 4 JDBC driver that provides d We hope you enjoy using the Microsoft JDBC Driver for SQL Server. -SQL Server Team +Microsoft JDBC driver for SQL Server Team ## Take our survey Let us know how you think we're doing. - + ## Status of Most Recent Builds | Azure Pipelines (Windows) | Azure Pipelines (Linux) | Azure Pipelines (MacOS) | @@ -28,10 +28,7 @@ Let us know how you think we're doing. What's coming next? We will look into adding a more comprehensive set of tests, improving our javadocs, and start developing the next set of features. ## Get Started -* [**Ubuntu + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java/ubuntu) -* [**Red Hat + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java/rhel) -* [**Mac + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java/mac) -* [**Windows + SQL Server + Java**](https://www.microsoft.com/en-us/sql-server/developer-get-started/java/windows) +[**Getting started with SQL Server and Java**](https://github.com/AzureSQLDB/sql-driver-examples/blob/main/examples/sql/drivers/java-driver-example.md) ## Build ### Prerequisites @@ -82,7 +79,7 @@ We're now on the Maven Central Repository. Add the following to your POM file to com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 ``` The driver can be downloaded from [Microsoft](https://aka.ms/downloadmssqljdbc). @@ -93,7 +90,7 @@ To get the latest version of the driver, add the following to your POM file: com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 ``` @@ -128,7 +125,7 @@ Projects that require either of the two features need to explicitly declare the com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 compile @@ -146,7 +143,7 @@ Projects that require either of the two features need to explicitly declare the com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 compile @@ -173,7 +170,7 @@ When setting 'useFmtOnly' property to 'true' for establishing a connection or cr com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 @@ -213,7 +210,7 @@ Preview releases happen approximately monthly between stable releases. This give You can see what is going into a future release by monitoring [Milestones](https://github.com/Microsoft/mssql-jdbc/milestones) in the repository. ### Version conventions -Starting with 6.0, stable versions have an even minor version. For example, 6.0, 6.2, 6.4, 7.0, 7.2, 7.4, 8.2, 8.4, 9.2, 9.4, 10.2, 11.2, 12.2. Preview versions have an odd minor version. For example, 6.1, 6.3, 6.5, 7.1, 7.3, 8.1, 9.1, 10.1, 11.1, 12.1, and so on. +Starting with 6.0, stable versions have an even minor version. For example, 6.0, 6.2, 6.4, 7.0, 7.2, 7.4, 8.2, 8.4, 9.2, 9.4, 10.2, 11.2, 12.2, 12.4. Preview versions have an odd minor version. For example, 6.1, 6.3, 6.5, 7.1, 7.3, 8.1, 9.1, 10.1, 11.1, 12.1, 12.3, and so on. ## Contributors Special thanks to everyone who has contributed to the project. @@ -229,13 +226,13 @@ Here are our Top 15 contributors from the community: - simon04 (Simon Legner) - gstojsic - cosmofrit -- harawata- shayaantx -- (Iwao AVE!) -- nsidhaye (Nikhil Sidhaye) +- mmimica (Milan Mimica) +- harawata (Iwao AVE!) - rPraml (Roland Praml) +- laeubi (Christoph Laubrich) - worldtiki (Daniel Albuquerque) - shayaantx -- mfriesen (Mike Friesen) +- mnhubspot ## License diff --git a/build.gradle b/build.gradle index 0d7365a96..4ce995b8d 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,8 @@ apply plugin: 'java' -version = '12.4.0-SNAPSHOT' +version = '12.5.0-SNAPSHOT' +def releaseExt = '-preview' def jreVersion = "" def testOutputDir = file("build/classes/java/test") def archivesBaseName = 'mssql-jdbc' @@ -85,7 +86,7 @@ if(hasProperty('buildProfile') && buildProfile == "jre8") { } } -jar.archiveFileName = "${archivesBaseName}-${version}.${jreVersion}-preview.jar" +jar.archiveFileName = "${archivesBaseName}-${version}.${jreVersion}${releaseExt}.jar" jar { manifest { attributes 'Title': "Microsoft JDBC Driver ${archiveVersion} for SQL Server", diff --git a/pom.xml b/pom.xml index 560c76f3b..099530b9b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ 4.0.0 com.microsoft.sqlserver mssql-jdbc - 12.4.0-SNAPSHOT + 12.5.0-SNAPSHOT jar Microsoft JDBC Driver for SQL Server @@ -37,7 +37,8 @@ xSQLv11 - - - - - - For tests not compatible with SQL Server 2012 - - - - xSQLv12 - - - - - - For tests not compatible with SQL Server 2008 R2 - 2014 xSQLv14 - - - - - - For tests not compatible with SQL Server 2016 - 2017 - xSQLv15 - - - - - - For tests not compatible with SQL Server 2019 - - - - + xSQLv15 - - - - - - For tests not compatible with SQL Server 2019 - - - - + xSQLv16 - - - - - - For tests not compatible with SQL Server 2022 - - - - xAzureSQLDB - - - - For tests not compatible with Azure SQL Database - - xAzureSQLDW - - - - For tests not compatible with Azure Data Warehouse - xAzureSQLMI - - - - For tests not compatible with Azure SQL Managed Instance @@ -53,14 +54,14 @@ -preview 6.0.0 - 4.6.1 + 4.6.4 1.9.0 1.13.8 1.1.0 4.9.3 2.10.1 - 1.70 - 1.70 + 1.76 + 1.76 [1.3.2, 1.9.0] 5.8.2 @@ -68,7 +69,7 @@ 2.7.0 1.7.30 2.1.0.RELEASE - 2.1.214 + 2.2.220 UTF-8 ${project.build.sourceEncoding} false @@ -114,15 +115,15 @@ org.bouncycastle - bcprov-jdk15on - ${bcprov-jdk15on.version} + bcprov-jdk18on + ${bcprov-jdk18on.version} true org.bouncycastle - bcpkix-jdk15on - ${bcpkix-jdk15on.version} + bcpkix-jdk18on + ${bcpkix-jdk18on.version} true diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java index 7b710bc04..9358f835e 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/DataTypes.java @@ -1115,7 +1115,7 @@ private DataTypes() { static final void throwConversionError(String fromType, String toType) throws SQLServerException { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_unsupportedConversionFromTo")); Object[] msgArgs = {fromType, toType}; - SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), null, true); + SQLServerException.makeFromDriverError(null, null, form.format(msgArgs), SQLState.ERROR_IN_ASSIGNMENT.getSQLStateCode(), true); } /** diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java index 9d0548031..bb709307d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/IOBuffer.java @@ -1693,7 +1693,7 @@ else if (con.getTrustManagerClass() != null) { // Otherwise, we'll validate the certificate using a real TrustManager obtained // from the a security provider that is capable of validating X.509 certificates. else { - if (isTDS8) { + if (isTDS8 && serverCert != null) { if (logger.isLoggable(Level.FINEST)) logger.finest(toString() + " Verify server certificate for TDS 8"); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java index 89745b762..aa64c87b8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Parameter.java @@ -531,36 +531,9 @@ private void setTypeDefinition(DTV dtv) { param.typeDefinition = SSType.DECIMAL.toString() + "(" + valueLength + "," + scale + ")"; } } else { - if (dtv.getJavaType() == JavaType.BIGDECIMAL && null != dtv.getSetterValue()) { - String[] plainValueArray - = ((BigDecimal) dtv.getSetterValue()).abs().toPlainString().split("\\."); - - // Precision is computed as opposed to using BigDecimal.precision(). This is because the - // BigDecimal method can lead to inaccurate results. - int calculatedPrecision; - - // If the string array has two parts, e.g .the input was a decimal, check if the first - // part is a 0. For BigDecimals with leading zeroes, the leading zero does not count towards - // precision. For all other decimals, we include the integer portion as part of the precision - // When the string array has just one part, we only look at that part to compute precision. - if (plainValueArray.length == 2) { - if (plainValueArray[0].length() == 1 && (Integer.parseInt(plainValueArray[0]) == 0)) { - calculatedPrecision = plainValueArray[1].length(); - } else { - calculatedPrecision = plainValueArray[0].length() + plainValueArray[1].length(); - } - } else { - calculatedPrecision = plainValueArray[0].length(); - } - - param.typeDefinition = SSType.DECIMAL.toString() + "(" + calculatedPrecision + "," + - (plainValueArray.length == 2 ? plainValueArray[1].length() : 0) + ")"; - } else { - param.typeDefinition = SSType.DECIMAL.toString() + "(" - + SQLServerConnection.MAX_DECIMAL_PRECISION + "," + scale + ")"; - } + param.typeDefinition = SSType.DECIMAL.toString() + "(" + + SQLServerConnection.MAX_DECIMAL_PRECISION + "," + scale + ")"; } - break; case MONEY: diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java index 57bbfb6fe..8adbb06fc 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLJdbcVersion.java @@ -7,7 +7,7 @@ final class SQLJdbcVersion { static final int MAJOR = 12; - static final int MINOR = 4; + static final int MINOR = 5; static final int PATCH = 0; static final int BUILD = 0; /* diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java index 884f638ea..e51328cfc 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerCertificateUtils.java @@ -281,9 +281,8 @@ static void validateServerNameInCertificate(X509Certificate cert, String hostNam */ static void validateServerCerticate(X509Certificate cert, String certFile) throws CertificateException { try (InputStream is = fileToStream(certFile)) { - if (!CertificateFactory.getInstance("X509").generateCertificate(is).getPublicKey() - .equals(cert.getPublicKey())) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_publicKeyMismatch")); + if (!CertificateFactory.getInstance("X509").generateCertificate(is).equals(cert)) { + MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_serverCertError")); Object[] msgArgs = {certFile}; throw new CertificateException(form.format(msgArgs)); } @@ -353,8 +352,7 @@ static KeyManager[] readPKCS8Certificate(String certPath, String keyPath, } private static KeyManager[] readPKCS12Certificate(String certPath, - String keyPassword) throws NoSuchAlgorithmException, CertificateException, IOException, - UnrecoverableKeyException, KeyStoreException, SQLServerException { + String keyPassword) throws NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException, KeyStoreException, SQLServerException { KeyStore keyStore = loadPKCS12KeyStore(certPath, keyPassword); KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(SUN_X_509); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java index 73f37f0bb..89a433f08 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionAzureKeyVaultProvider.java @@ -429,7 +429,7 @@ public byte[] decryptColumnEncryptionKey(String masterKeyPath, String encryption // Validate the signature if (!azureKeyVaultVerifySignature(dataToVerify, signature, masterKeyPath)) { MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_CEKSignatureNotMatchCMK")); - Object[] msgArgs = {masterKeyPath}; + Object[] msgArgs = {Util.byteToHexDisplayString(signature), masterKeyPath}; throw new SQLServerException(this, form.format(msgArgs), null, 0, false); } @@ -876,6 +876,9 @@ public boolean verifyColumnMasterKeyMetadata(String masterKeyPath, boolean allow 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)); @@ -889,19 +892,65 @@ public boolean verifyColumnMasterKeyMetadata(String masterKeyPath, boolean allow } // Sign the hash - byte[] signedHash = azureKeyVaultSignHashedData(dataToVerify, masterKeyPath); + signedHash = azureKeyVaultSignHashedData(dataToVerify, masterKeyPath); if (null == signedHash) { throw new SQLServerException(SQLServerException.getErrString("R_SignedHashLengthError"), null); } // Validate the signature - boolean isValid = azureKeyVaultVerifySignature(dataToVerify, signature, masterKeyPath); + 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; + } + + 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); + } - return isValid; + // 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() { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java index 2b822fbf4..46a3fa051 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionCertificateStoreProvider.java @@ -49,23 +49,15 @@ public String getName() { return this.name; } + @Override public byte[] encryptColumnEncryptionKey(String masterKeyPath, String encryptionAlgorithm, byte[] plainTextColumnEncryptionKey) throws SQLServerException { + // not supported throw new SQLServerException(null, SQLServerException.getErrString("R_InvalidWindowsCertificateStoreEncryption"), null, 0, false); } - private byte[] decryptColumnEncryptionKeyWindows(String masterKeyPath, String encryptionAlgorithm, - byte[] encryptedColumnEncryptionKey) throws SQLServerException { - try { - return AuthenticationJNI.DecryptColumnEncryptionKey(masterKeyPath, encryptionAlgorithm, - encryptedColumnEncryptionKey); - } catch (DLLException e) { - DLLException.buildException(e.getErrCode(), e.getParam1(), e.getParam2(), e.getParam3()); - return null; - } - } - + @Override public byte[] decryptColumnEncryptionKey(String masterKeyPath, String encryptionAlgorithm, byte[] encryptedColumnEncryptionKey) throws SQLServerException { windowsCertificateStoreLogger.entering(SQLServerColumnEncryptionCertificateStoreProvider.class.getName(), @@ -92,4 +84,16 @@ public boolean verifyColumnMasterKeyMetadata(String masterKeyPath, boolean allow return false; } } + + private byte[] decryptColumnEncryptionKeyWindows(String masterKeyPath, String encryptionAlgorithm, + byte[] encryptedColumnEncryptionKey) throws SQLServerException { + try { + return AuthenticationJNI.DecryptColumnEncryptionKey(masterKeyPath, encryptionAlgorithm, + encryptedColumnEncryptionKey); + } catch (DLLException e) { + DLLException.buildException(e.getErrCode(), e.getParam1(), e.getParam2(), e.getParam3()); + return null; + } + } + } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java index 5fbc111a3..319b2b268 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerColumnEncryptionJavaKeyStoreProvider.java @@ -113,6 +113,83 @@ public byte[] decryptColumnEncryptionKey(String masterKeyPath, String encryption return plainCEK; } + @Override + public boolean verifyColumnMasterKeyMetadata(String masterKeyPath, boolean allowEnclaveComputations, + byte[] signature) throws SQLServerException { + + if (!allowEnclaveComputations) { + return false; + } + + KeyStoreProviderCommon.validateNonEmptyMasterKeyPath(masterKeyPath); + CertificateDetails certificateDetails = getCertificateDetails(masterKeyPath); + + 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(); + Signature sig = Signature.getInstance("SHA256withRSA"); + + sig.initSign((PrivateKey) certificateDetails.privateKey); + sig.update(dataToVerify); + + signedHash = sig.sign(); + + sig.initVerify(certificateDetails.certificate.getPublicKey()); + sig.update(dataToVerify); + isValid = sig.verify(signature); + } catch (NoSuchAlgorithmException e) { + throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e); + } catch (InvalidKeyException | SignatureException 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; + } + + public byte[] signColumnMasterKeyMetadata(String masterKeyPath, + boolean allowEnclaveComputations) throws SQLServerException { + if (!allowEnclaveComputations) + return null; + + KeyStoreProviderCommon.validateNonEmptyMasterKeyPath(masterKeyPath); + CertificateDetails certificateDetails = getCertificateDetails(masterKeyPath); + + 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)); + + Signature sig = Signature.getInstance("SHA256withRSA"); + sig.initSign((PrivateKey) certificateDetails.privateKey); + sig.update(md.digest()); + + return sig.sign(); + + } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) { + throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e); + } + } + private CertificateDetails getCertificateDetails(String masterKeyPath) throws SQLServerException { FileInputStream fis = null; KeyStore keyStore = null; @@ -319,45 +396,16 @@ private byte[] getLittleEndianBytesFromShort(short value) { * Verify signature against certificate */ private boolean rsaVerifySignature(byte[] dataToVerify, byte[] signature, - CertificateDetails certificateDetails) throws SQLServerException { - try { - Signature sig = Signature.getInstance("SHA256withRSA"); - sig.initSign((PrivateKey) certificateDetails.privateKey); - sig.update(dataToVerify); - byte[] signedHash = sig.sign(); + CertificateDetails certificateDetails) throws InvalidKeyException, NoSuchAlgorithmException, SignatureException { + Signature sig = Signature.getInstance("SHA256withRSA"); - sig.initVerify(certificateDetails.certificate.getPublicKey()); - sig.update(dataToVerify); + sig.initSign((PrivateKey) certificateDetails.privateKey); + sig.update(dataToVerify); - return sig.verify(signedHash); + byte[] signedHash = sig.sign(); - } catch (InvalidKeyException | NoSuchAlgorithmException | SignatureException e) { - MessageFormat form = new MessageFormat(SQLServerException.getErrString("R_VerifySignatureFailed")); - Object[] msgArgs = {e.getMessage()}; - throw new SQLServerException(this, form.format(msgArgs), null, 0, false); - } + sig.initVerify(certificateDetails.certificate.getPublicKey()); + sig.update(dataToVerify); + return sig.verify(signature); } - - @Override - public boolean verifyColumnMasterKeyMetadata(String masterKeyPath, boolean allowEnclaveComputations, - byte[] signature) throws SQLServerException { - - if (!allowEnclaveComputations) - return false; - - KeyStoreProviderCommon.validateNonEmptyMasterKeyPath(masterKeyPath); - CertificateDetails certificateDetails = getCertificateDetails(masterKeyPath); - - 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)); - return rsaVerifySignature(md.digest(), signature, certificateDetails); - } catch (NoSuchAlgorithmException e) { - throw new SQLServerException(SQLServerException.getErrString("R_NoSHA256Algorithm"), e); - } - } - } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java index ec3e86c3c..acb062ce5 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerConnection.java @@ -1859,7 +1859,7 @@ Connection connect(Properties propsIn, SQLServerPooledConnection pooledConnectio if (0 == connectRetryCount) { // connection retry disabled throw e; - } else if (connectRetryAttempt++ > connectRetryCount) { + } else if (++connectRetryAttempt > connectRetryCount) { // maximum connection retry count reached if (connectionlogger.isLoggable(Level.FINE)) { connectionlogger.fine("Connection failed. Maximum connection retry count " @@ -3249,6 +3249,7 @@ private void login(String primary, String primaryInstanceName, int primaryPortNu || (SQLServerException.ERROR_SOCKET_TIMEOUT == driverErrorCode // socket timeout && (!isDBMirroring || attemptNumber > 0)) // If mirroring, only close after failover has been tried (attempt >= 1) || timerHasExpired(timerExpire) + || (state.equals(State.CONNECTED) && !isDBMirroring) // for non-dbmirroring cases, do not retry after tcp socket connection succeeds ) { // close the connection and throw the error back @@ -7389,7 +7390,7 @@ void endRequestInternal() throws SQLException { String replaceParameterMarkers(String sqlSrc, int[] paramPositions, Parameter[] params, boolean isReturnValueSyntax) { final int MAX_PARAM_NAME_LEN = 6; - char[] sqlDst = new char[sqlSrc.length() + params.length * (MAX_PARAM_NAME_LEN + OUT.length)]; + char[] sqlDst = new char[sqlSrc.length() + (params.length * (MAX_PARAM_NAME_LEN + OUT.length)) + (params.length * 2)]; int dstBegin = 0; int srcBegin = 0; int nParam = 0; @@ -7403,8 +7404,8 @@ String replaceParameterMarkers(String sqlSrc, int[] paramPositions, Parameter[] if (sqlSrc.length() == srcEnd) break; - dstBegin += makeParamName(nParam++, sqlDst, dstBegin); - srcBegin = srcEnd + 1; + dstBegin += makeParamName(nParam++, sqlDst, dstBegin, true); + srcBegin = srcEnd + 1 <= sqlSrc.length() - 1 && sqlSrc.charAt(srcEnd + 1) == ' ' ? srcEnd + 2 : srcEnd + 1; if (params[paramIndex++].isOutput() && (!isReturnValueSyntax || paramIndex > 1)) { System.arraycopy(OUT, 0, sqlDst, dstBegin, OUT.length); @@ -7423,33 +7424,88 @@ String replaceParameterMarkers(String sqlSrc, int[] paramPositions, Parameter[] * @param name * the parameter name * @param offset + * the offset + * @param isPreparedSQL + * if the param is for build preparedSQL * @return int */ - static int makeParamName(int nParam, char[] name, int offset) { - name[offset + 0] = '@'; - name[offset + 1] = 'P'; + static int makeParamName(int nParam, char[] name, int offset, boolean isPreparedSQL) { + buildParamInitial(name, offset, isPreparedSQL); if (nParam < 10) { - name[offset + 2] = (char) ('0' + nParam); - return 3; + return buildParamLt10(nParam, name, offset, isPreparedSQL); } else { if (nParam < 100) { - int nBase = 2; - while (true) { // make a char[] representation of the param number 2.26 - if (nParam < nBase * 10) { - name[offset + 2] = (char) ('0' + (nBase - 1)); - name[offset + 3] = (char) ('0' + (nParam - ((nBase - 1) * 10))); - return 4; - } - nBase++; - } + return buildParamLt100(nParam, name, offset, isPreparedSQL); } else { - String sParam = "" + nParam; - sParam.getChars(0, sParam.length(), name, offset + 2); - return 2 + sParam.length(); + return buildParamMt100(nParam, name, offset, isPreparedSQL); + } + } + } + + private static void buildParamInitial(char[] name, int offset, boolean isPreparedSQL) { + int preparedSQLOffset = 0; + if (isPreparedSQL) { + name[offset + 0] = ' '; + preparedSQLOffset++; + } + name[offset + preparedSQLOffset + 0] = '@'; + name[offset + preparedSQLOffset + 1] = 'P'; + } + + private static int buildParamLt10(int nParam, char[] name, int offset, boolean isPreparedSQL) { + int preparedSQLOffset = 0; + + if (isPreparedSQL) { + preparedSQLOffset++; + } + + name[offset + preparedSQLOffset + 2] = (char) ('0' + nParam); + + if (isPreparedSQL) { + name[offset + 4] = ' '; + return 5; + } + + return 3; + } + + private static int buildParamLt100(int nParam, char[] name, int offset, boolean isPreparedSQL) { + int nBase = 2; + int preparedSQLOffset = 0; + + if (isPreparedSQL) { + preparedSQLOffset = 1; + } + + while (true) { // make a char[] representation of the param number 2.26 + if (nParam < nBase * 10) { + name[offset + preparedSQLOffset + 2] = (char) ('0' + (nBase - 1)); + name[offset + preparedSQLOffset + 3] = (char) ('0' + (nParam - ((nBase - 1) * 10))); + + if (isPreparedSQL) { + name[offset + 5] = ' '; + preparedSQLOffset++; + } + + return 4 + preparedSQLOffset; } + nBase++; } } + private static int buildParamMt100(int nParam, char[] name, int offset, boolean isPreparedSQL) { + int preparedSQLOffset = 0; + String sParam = Integer.toString(nParam); + + if (isPreparedSQL) { + preparedSQLOffset++; + sParam = nParam + " "; + } + + sParam.getChars(0, sParam.length(), name, offset + preparedSQLOffset + 2); + return 2 + sParam.length() + preparedSQLOffset; + } + /** * Notify any interested parties (e.g. pooling managers) of a ConnectionEvent activity on the connection. Calling * notifyPooledConnection with null event will place this connection back in the pool. Calling diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java index af82279de..8141f5383 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerDatabaseMetaData.java @@ -1518,7 +1518,7 @@ public java.sql.ResultSet getSchemas(String catalog, String schemaPattern) throw if (loggerExternal.isLoggable(Level.FINER) && Util.isActivityTraceOn()) { loggerExternal.finer(toString() + ACTIVITY_ID + ActivityCorrelator.getCurrent().toString()); } - return getSchemasInternal(catalog, schemaPattern); + return getSchemasInternal(catalog, escapeIDName(schemaPattern)); } @Override @@ -1709,12 +1709,30 @@ public String getUserName() throws SQLServerException, SQLTimeoutException { } checkClosed(); String result = ""; - try (SQLServerStatement s = (SQLServerStatement) connection.createStatement(); - SQLServerResultSet rs = s.executeQueryInternal("select system_user")) { - // Select system_user will always return a row. - boolean next = rs.next(); - assert next; - result = rs.getString(1); + try (SQLServerStatement s = (SQLServerStatement) connection.createStatement()) { + try (SQLServerResultSet rs = s.executeQueryInternal("SELECT SYSTEM_USER")) { + // Select system_user will always return a row. + boolean next = rs.next(); + assert next; + result = rs.getString(1); + } catch (SQLServerException e) { + // execution using impersonated security context is disallowed for Azure SQL Server so return CURRENT_USER instead + if (e.getErrorCode() == SQLServerException.IMPERSONATION_CONTEXT_NOT_SUPPORTED) { + if (loggerExternal.isLoggable(Level.FINEST)) { + loggerExternal.finest(toString() + + " Impersonation context is not supported in this version of SQL Server. Re-try getting CURRENT_USER"); + } + + try (SQLServerResultSet rs = s.executeQueryInternal("SELECT CURRENT_USER")) { + boolean next = rs.next(); + assert next; + result = rs.getString(1); + } + } else { + // re-throw + throw e; + } + } } return result; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java index 374d0c5c8..6721c7b55 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerException.java @@ -17,7 +17,8 @@ enum SQLState { DATA_EXCEPTION_DATETIME_FIELD_OVERFLOW("22008"), NUMERIC_DATA_OUT_OF_RANGE("22003"), DATA_EXCEPTION_LENGTH_MISMATCH("22026"), - COL_NOT_FOUND("42S22"); + COL_NOT_FOUND("42S22"), + ERROR_IN_ASSIGNMENT("22005"); private final String sqlStateCode; @@ -59,6 +60,7 @@ public final class SQLServerException extends java.sql.SQLException { static final String EXCEPTION_XOPEN_CONNECTION_CANT_ESTABLISH = "08001"; static final String EXCEPTION_XOPEN_CONNECTION_DOES_NOT_EXIST = "08003"; static final String EXCEPTION_XOPEN_CONNECTION_FAILURE = "08006"; // After connection was connected OK + static final String EXCEPTION_XOPEN_ERROR_IN_ASSIGNMENT = "22005"; // Error code is the same in both SQL-99 and X/Open static final String LOG_CLIENT_CONNECTION_ID_PREFIX = " ClientConnectionId:"; @@ -66,8 +68,9 @@ public final class SQLServerException extends java.sql.SQLException { static final int LOGON_FAILED = 18456; static final int PASSWORD_EXPIRED = 18488; static final int USER_ACCOUNT_LOCKED = 18486; - static final java.util.logging.Logger exLogger = java.util.logging.Logger - .getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerException"); + + // Built-in function '%.*ls' in impersonation context is not supported in this version of SQL Server. + static final int IMPERSONATION_CONTEXT_NOT_SUPPORTED = 40529; // Facility for driver-specific error codes static final int DRIVER_ERROR_NONE = 0; @@ -84,6 +87,9 @@ public final class SQLServerException extends java.sql.SQLException { static final int DATA_CLASSIFICATION_INVALID_LABEL_INDEX = 12; static final int DATA_CLASSIFICATION_INVALID_INFORMATION_TYPE_INDEX = 13; + static final java.util.logging.Logger exLogger = java.util.logging.Logger + .getLogger("com.microsoft.sqlserver.jdbc.internals.SQLServerException"); + /** driver error code */ private int driverErrorCode = DRIVER_ERROR_NONE; @@ -302,6 +308,8 @@ static String mapFromXopen(String state) { return "08S01"; case SQLServerException.EXCEPTION_XOPEN_CONNECTION_FAILURE: return "08S01"; + case SQLServerException.EXCEPTION_XOPEN_ERROR_IN_ASSIGNMENT: + return "22005"; default: return ""; } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java index 5554fa3b1..52044cdf2 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerMSAL4JUtils.java @@ -69,8 +69,8 @@ private SQLServerMSAL4JUtils() { throw new UnsupportedOperationException(SQLServerException.getErrString("R_notSupported")); } - static SqlAuthenticationToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user, String password, - String authenticationString) throws SQLServerException { + static synchronized SqlAuthenticationToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, String user, + String password, String authenticationString) throws SQLServerException { ExecutorService executorService = Executors.newSingleThreadExecutor(); if (logger.isLoggable(Level.FINEST)) { @@ -108,8 +108,8 @@ static SqlAuthenticationToken getSqlFedAuthToken(SqlFedAuthInfo fedAuthInfo, Str } } - static SqlAuthenticationToken getSqlFedAuthTokenPrincipal(SqlFedAuthInfo fedAuthInfo, String aadPrincipalID, - String aadPrincipalSecret, String authenticationString) throws SQLServerException { + static synchronized SqlAuthenticationToken getSqlFedAuthTokenPrincipal(SqlFedAuthInfo fedAuthInfo, + String aadPrincipalID, String aadPrincipalSecret, String authenticationString) throws SQLServerException { ExecutorService executorService = Executors.newSingleThreadExecutor(); if (logger.isLoggable(Level.FINEST)) { @@ -152,7 +152,7 @@ static SqlAuthenticationToken getSqlFedAuthTokenPrincipal(SqlFedAuthInfo fedAuth } } - static SqlAuthenticationToken getSqlFedAuthTokenPrincipalCertificate(SqlFedAuthInfo fedAuthInfo, + static synchronized SqlAuthenticationToken getSqlFedAuthTokenPrincipalCertificate(SqlFedAuthInfo fedAuthInfo, String aadPrincipalID, String certFile, String certPassword, String certKey, String certKeyPassword, String authenticationString) throws SQLServerException { ExecutorService executorService = Executors.newSingleThreadExecutor(); @@ -253,7 +253,7 @@ static SqlAuthenticationToken getSqlFedAuthTokenPrincipalCertificate(SqlFedAuthI } } - static SqlAuthenticationToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, + static synchronized SqlAuthenticationToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAuthInfo, String authenticationString) throws SQLServerException { ExecutorService executorService = Executors.newSingleThreadExecutor(); @@ -300,7 +300,7 @@ static SqlAuthenticationToken getSqlFedAuthTokenIntegrated(SqlFedAuthInfo fedAut } } - static SqlAuthenticationToken getSqlFedAuthTokenInteractive(SqlFedAuthInfo fedAuthInfo, String user, + static synchronized SqlAuthenticationToken getSqlFedAuthTokenInteractive(SqlFedAuthInfo fedAuthInfo, String user, String authenticationString) throws SQLServerException { ExecutorService executorService = Executors.newSingleThreadExecutor(); diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java index 56fce55c0..aeb7a99d9 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerParameterMetaData.java @@ -397,7 +397,8 @@ private Map getParameterInfo(int param) { private boolean isValidParamProc(int n) { // Note row 1 is the 'return value' meta data - return ((stmtParent.bReturnValueSyntax && isTVP && procMetadata.size() >= n) || procMetadata.size() > n); + return (n >= 1 // JDBC index is 1 based + && ((stmtParent.bReturnValueSyntax && isTVP && procMetadata.size() >= n) || procMetadata.size() > n)); } private boolean isValidParamQuery(int n) { diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java index b00064565..2a2adf12d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerPreparedStatement.java @@ -461,7 +461,7 @@ private String buildParamTypeDefinitions(Parameter[] params, boolean renewDefini if (i > 0) sb.append(','); - int l = SQLServerConnection.makeParamName(i, cParamName, 0); + int l = SQLServerConnection.makeParamName(i, cParamName, 0, false); String parameterName = String.valueOf(cParamName, 0, l); sb.append(parameterName); sb.append(' '); @@ -750,7 +750,7 @@ void sendParamsByRPC(TDSWriter tdsWriter, Parameter[] params) throws SQLServerEx for (int index = 0; index < params.length; index++) { if (JDBCType.TVP == params[index].getJdbcType()) { cParamName = new char[10]; - int paramNameLen = SQLServerConnection.makeParamName(index, cParamName, 0); + int paramNameLen = SQLServerConnection.makeParamName(index, cParamName, 0, false); tdsWriter.writeByte((byte) paramNameLen); tdsWriter.writeString(new String(cParamName, 0, paramNameLen)); } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java index 0652a4205..9aa648779 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerResource.java @@ -359,7 +359,7 @@ protected Object[][] getContents() { {"R_AccessTokenCannotBeEmpty", "AccessToken cannot be empty."}, {"R_SetBothAuthenticationAndAccessToken", "Cannot set the AccessToken property if \"Authentication\" has been specified in the connection string."}, {"R_NoUserPasswordForActivePassword", "Both \"User\" (or \"UserName\") and \"Password\" connection string keywords must be specified, if \"Authentication=ActiveDirectoryPassword\"."}, - {"R_NoUserPasswordForActiveServicePrincipal", "Both \"UserName\" and \"Password\" connection string keywords must be specified, if \"Authentication=ActiveDirectoryServicePrincipal."}, + {"R_NoUserPasswordForActiveServicePrincipal", "Both \"UserName\" and \"Password\" connection string keywords must be specified, if \"Authentication=ActiveDirectoryServicePrincipal\"."}, {"R_NoUserOrCertForActiveServicePrincipalCertificate", "\"Both \"UserName\" and \"clientCertificate\" connection string keyword must be specified, if \"Authentication=ActiveDirectoryServicePrincipalCertificate\"."}, {"R_NoUserPasswordForSqlPassword", "Both \"User\" (or \"UserName\") and \"Password\" connection string keywords must be specified, if \"Authentication=SqlPassword\"."}, {"R_BothUserPasswordandDeprecated", "Both \"User\" (or \"UserName\"), \"Password\" and \"AADSecurePrincipalId\", \"AADSecurePrincipalSecret\" connection string keywords are specified, please use \"User\" (or \"UserName\"), \"Password\" only."}, @@ -382,8 +382,9 @@ protected Object[][] getContents() { {"R_AKVSignatureLengthError", "The specified encrypted column encryption key''s signature length: {0} does not match the signature length: {1} when using column master key (Azure Key Vault key) in {2}. The encrypted column encryption key may be corrupt, or the specified Azure Key Vault key path may be incorrect."}, {"R_HashNull", "Hash should not be null while decrypting encrypted column encryption key."}, {"R_NoSHA256Algorithm", "SHA-256 Algorithm is not supported."}, - {"R_VerifySignature", "Unable to verify signature of the column encryption key."}, - {"R_CEKSignatureNotMatchCMK", "The specified encrypted column encryption key signature does not match the signature computed with the column master key (Asymmetric key in Azure Key Vault) in {0}. The encrypted column encryption key may be corrupt, or the specified path may be incorrect."}, + {"R_VerifySignatureFailed", "Unable to verify the signature of the column encryption key."}, + {"R_SignatureNotMatch", "The specified encrypted column encryption key signature \"{0}\" does not match the signature \"{1}\" computed with the column master key (certificate) in \"{2}\". The encrypted column encryption key may be corrupt, or the specified path may be incorrect. {3}"}, + {"R_CEKSignatureNotMatchCMK", "The specified encrypted column encryption key signature \"{0}\" does not match the signature computed with the column master key (Asymmetric key in Azure Key Vault) in {1}. The encrypted column encryption key may be corrupt, or the specified path may be incorrect."}, {"R_DecryptCEKError", "Unable to decrypt column encryption key using specified Azure Key Vault key."}, {"R_EncryptCEKError", "Unable to encrypt column encryption key using specified Azure Key Vault key."}, {"R_CipherTextLengthNotMatchRSASize", "CipherText length does not match the RSA key size."}, @@ -409,6 +410,7 @@ protected Object[][] getContents() { // This is used for connection settings. {0}-> property name as is, {1}-> value {"R_InvalidConnectionSetting", "The {0} value \"{1}\" is not valid."}, {"R_InvalidWindowsCertificateStoreEncryption", "Cannot encrypt a column encryption key with the Windows Certificate Store."}, + {"R_InvalidWindowsCertificateStoreSignCMK", "Cannot sign column master key metadata with the Windows Certificate Store."}, {"R_AEKeypathEmpty", "Internal error. Certificate path cannot be null. Use the following format: \"certificate location/certificate store/certificate thumbprint\", where \"certificate location\" is either LocalMachine or CurrentUser."}, {"R_AEWinApiErr", "Windows Api native error."}, {"R_AECertpathBad", "Internal error. Invalid certificate path: {0}. Use the following format: \"certificate location/certificate store/certificate thumbprint\", where \"certificate location\" is either LocalMachine or CurrentUser."}, @@ -531,7 +533,6 @@ protected Object[][] getContents() { {"R_illegalArgumentTrustManager", "Interal error. Peer certificate chain or key exchange algorithem can not be null or empty."}, {"R_serverCertExpired", "Server Certificate has expired: {0}: {1}"}, {"R_serverCertNotYetValid", "Server Certificate is not yet valid: {0}: {1}"}, - {"R_publicKeyMismatch", "Error validating Server Certificate: public key mismatch: {0}"}, {"R_serverCertError", "Error validating Server Certificate: {0}: \n{1}:\n{2}."}, {"R_SecureStringInitFailed", "Failed to initialize SecureStringUtil to store secure strings"}, {"R_ALPNFailed", "Failed to negotiate Application-Layer Protocol {0}. Server returned: {1}."}, diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java index 6e24c3584..3bb2eb32d 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerSecurityUtility.java @@ -315,7 +315,7 @@ static void verifyColumnMasterKeyMetadata(SQLServerConnection connection, SQLSer } if (!provider.verifyColumnMasterKeyMetadata(keyPath, isEnclaveEnabled, cmkSignature)) { - throw new SQLServerException(SQLServerException.getErrString("R_VerifySignature"), null); + throw new SQLServerException(SQLServerException.getErrString("R_VerifySignatureFailed"), null); } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java index 924ec0d38..7f9c44a2f 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/SQLServerXAResource.java @@ -946,7 +946,10 @@ private static int nextResourceID() { } private enum ResourceManagerFailure { - CONN_RESET("Connection reset"); + CONN_RESET("Connection reset"), + CONN_RESET_BY_PEER("Connection reset by peer"), + CONN_TIMEOUT("Connection timed out"), + CONN_RESILIENCY_CLIENT_UNRECOVERABLE(SQLServerException.getErrString("R_crClientUnrecoverable")); private final String errString; @@ -984,6 +987,10 @@ private boolean isResourceManagerFailure(Throwable throwable) { return false; } + if (xaLogger.isLoggable(Level.FINE)) { + xaLogger.fine(toString() + " Resource manager failure root exception: " + root); + } + ResourceManagerFailure err = ResourceManagerFailure.fromString(root.getMessage()); if (null == err) { @@ -991,16 +998,15 @@ private boolean isResourceManagerFailure(Throwable throwable) { } // Add as needed here for future XAER_RMFAIL exceptions - if (root instanceof SocketException) { - switch (err) { - case CONN_RESET: - return true; - default: - return false; - } + switch (err) { + case CONN_RESET: + case CONN_RESET_BY_PEER: + case CONN_TIMEOUT: + case CONN_RESILIENCY_CLIENT_UNRECOVERABLE: + return true; + default: + return false; } - - return false; } } diff --git a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java index fde8e94fe..70b9c9aa8 100644 --- a/src/main/java/com/microsoft/sqlserver/jdbc/Util.java +++ b/src/main/java/com/microsoft/sqlserver/jdbc/Util.java @@ -48,7 +48,8 @@ private Util() { private static final Lock LOCK = new ReentrantLock(); static boolean isIBM() { - return SYSTEM_JRE.startsWith("IBM"); + String vmName = System.getProperty("java.vm.name"); + return SYSTEM_JRE.startsWith("IBM") && vmName.startsWith("IBM"); } static String getJVMArchOnWindows() { diff --git a/src/samples/adaptive/pom.xml b/src/samples/adaptive/pom.xml index 0b9fd8da1..96e2be9a0 100644 --- a/src/samples/adaptive/pom.xml +++ b/src/samples/adaptive/pom.xml @@ -15,7 +15,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 diff --git a/src/samples/alwaysencrypted/pom.xml b/src/samples/alwaysencrypted/pom.xml index 95bce7ab4..294d4b419 100644 --- a/src/samples/alwaysencrypted/pom.xml +++ b/src/samples/alwaysencrypted/pom.xml @@ -15,7 +15,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 diff --git a/src/samples/azureactivedirectoryauthentication/pom.xml b/src/samples/azureactivedirectoryauthentication/pom.xml index 02c4c23c7..518da61a7 100644 --- a/src/samples/azureactivedirectoryauthentication/pom.xml +++ b/src/samples/azureactivedirectoryauthentication/pom.xml @@ -14,7 +14,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 diff --git a/src/samples/connections/pom.xml b/src/samples/connections/pom.xml index e2f2128c8..209697c3c 100644 --- a/src/samples/connections/pom.xml +++ b/src/samples/connections/pom.xml @@ -14,7 +14,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 diff --git a/src/samples/constrained/pom.xml b/src/samples/constrained/pom.xml index 131e686c9..c2cc82ae6 100644 --- a/src/samples/constrained/pom.xml +++ b/src/samples/constrained/pom.xml @@ -16,7 +16,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 diff --git a/src/samples/dataclassification/pom.xml b/src/samples/dataclassification/pom.xml index e3c71dc8a..74ba22bb0 100644 --- a/src/samples/dataclassification/pom.xml +++ b/src/samples/dataclassification/pom.xml @@ -16,7 +16,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 diff --git a/src/samples/datatypes/pom.xml b/src/samples/datatypes/pom.xml index d16a5bc74..148d73819 100644 --- a/src/samples/datatypes/pom.xml +++ b/src/samples/datatypes/pom.xml @@ -15,7 +15,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 diff --git a/src/samples/resultsets/pom.xml b/src/samples/resultsets/pom.xml index 09c1f42dd..d466f5e75 100644 --- a/src/samples/resultsets/pom.xml +++ b/src/samples/resultsets/pom.xml @@ -14,7 +14,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 diff --git a/src/samples/sparse/pom.xml b/src/samples/sparse/pom.xml index c5a7f7aec..e6e3dd4de 100644 --- a/src/samples/sparse/pom.xml +++ b/src/samples/sparse/pom.xml @@ -14,7 +14,7 @@ com.microsoft.sqlserver mssql-jdbc - 12.2.0.jre11 + 12.4.0.jre11 diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java index 96e8ead85..541f2da16 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/AESetup.java @@ -82,6 +82,8 @@ public class AESetup extends AbstractTest { public static final String SCALE_DATE_TABLE_AE = TestUtils.escapeSingleQuotes( AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("JDBCEncryptedScaleDate"))); + final static char[] HEXCHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + enum ColumnType { PLAIN, RANDOMIZED, @@ -177,17 +179,19 @@ static void setAEConnectionString(String serverName, String url, String protocol if (!isSqlLinux() && null != serverName && null != url && null != protocol) { enclaveProperties = "serverName=" + serverName + ";" + Constants.ENCLAVE_ATTESTATIONURL + "=" + url + ";" + Constants.ENCLAVE_ATTESTATIONPROTOCOL + "=" + protocol; - AETestConnectionString = connectionString + ";sendTimeAsDateTime=false" + ";columnEncryptionSetting=enabled" - + ";" + enclaveProperties; + AETestConnectionString = connectionString + ";sendTimeAsDateTime=false;columnEncryptionSetting=enabled;" + + enclaveProperties; // show progress if testing multiple servers if (enclaveServer.length > 1) { System.out.println("Testing enclave: " + enclaveProperties); } } else { - AETestConnectionString = connectionString + ";sendTimeAsDateTime=false" - + ";columnEncryptionSetting=enabled"; + AETestConnectionString = connectionString + ";sendTimeAsDateTime=false;columnEncryptionSetting=enabled;"; } + + // TODO: update AE test servers to support + AETestConnectionString += ";encrypt=false;trustServerCertificate=true;"; } @BeforeAll @@ -220,18 +224,18 @@ public static void setupAETest() throws Exception { setAEConnectionString(serverName, url, protocol); createCMK(AETestConnectionString, cmkJks, Constants.JAVA_KEY_STORE_NAME, javaKeyAliases, - Constants.CMK_SIGNATURE); + TestUtils.byteToHexDisplayString(jksProvider.signColumnMasterKeyMetadata(javaKeyAliases, true))); createCEK(AETestConnectionString, cmkJks, cekJks, jksProvider); if (null != keyIDs && !keyIDs[0].isEmpty()) { createCMK(AETestConnectionString, cmkAkv, Constants.AZURE_KEY_VAULT_NAME, keyIDs[0], - Constants.CMK_SIGNATURE_AKV); + TestUtils.byteToHexDisplayString(akvProvider.signColumnMasterKeyMetadata(keyIDs[0], true))); createCEK(AETestConnectionString, cmkAkv, cekAkv, akvProvider); } if (null != windowsKeyPath) { createCMK(AETestConnectionString, cmkWin, Constants.WINDOWS_KEY_STORE_NAME, windowsKeyPath, - Constants.CMK_SIGNATURE); + Constants.CMK_SIGNATURE_WIN); createCEK(AETestConnectionString, cmkWin, cekWin, null); } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/EnclaveTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/EnclaveTest.java index 9b136afd3..4b55573af 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/EnclaveTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/EnclaveTest.java @@ -23,10 +23,12 @@ import org.junit.runners.Parameterized; import com.microsoft.sqlserver.jdbc.EnclavePackageTest; +import com.microsoft.sqlserver.jdbc.RandomUtil; import com.microsoft.sqlserver.jdbc.SQLServerConnection; import com.microsoft.sqlserver.jdbc.SQLServerStatement; import com.microsoft.sqlserver.jdbc.TestResource; import com.microsoft.sqlserver.jdbc.TestUtils; +import com.microsoft.sqlserver.testframework.AbstractSQLGenerator; import com.microsoft.sqlserver.testframework.Constants; import com.microsoft.sqlserver.testframework.PrepUtil; @@ -194,6 +196,145 @@ public void testAEv2Disabled(String serverName, String url, String protocol) thr } } + /* + * Negative Test - bad JKS signature + */ + @ParameterizedTest + @MethodSource("enclaveParams") + public void testVerifyBadJksSignature(String serverName, String url, String protocol) throws Exception { + setAEConnectionString(serverName, url, protocol); + + // create CMK with a bad signature + String badCmk = Constants.CMK_NAME + "_badCMK"; + String badCek = Constants.CEK_NAME + "_badCek"; + String badTable = TestUtils.escapeSingleQuotes( + AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("testVerifyBadJksSignature"))); + + try (SQLServerConnection c = PrepUtil.getConnection(AETestConnectionString, AEInfo); + Statement s = c.createStatement()) { + createCMK(AETestConnectionString, badCmk, Constants.JAVA_KEY_STORE_NAME, javaKeyAliases, "0x666"); + createCEK(AETestConnectionString, badCmk, badCek, jksProvider); + + createTable(badTable, badCek, varcharTableSimple); + + PreparedStatement pstmt = c.prepareStatement("INSERT INTO " + badTable + " VALUES (?,?,?)"); + pstmt.setString(1, "a"); + pstmt.setString(2, "b"); + pstmt.setString(3, "test"); + pstmt.execute(); + + pstmt = c.prepareStatement("SELECT * FROM " + badTable + " WHERE RANDOMIZEDVarchar LIKE ?"); + pstmt.setString(1, "t%"); + try (ResultSet rs = pstmt.executeQuery()) { + fail(TestResource.getResource("R_expectedFailPassed")); + } + } catch (Exception e) { + assertTrue( + e.getMessage().matches(TestUtils.formatErrorMsg("R_SignatureNotMatch")) + || e.getMessage().matches(TestUtils.formatErrorMsg("R_VerifySignatureFailed")), + e.getMessage()); + } finally { + try (Statement s = connection.createStatement()) { + TestUtils.dropTableIfExists(badTable, s); + dropCEK(badCek, s); + dropCMK(badCmk, s); + } + } + } + + /* + * Negative Test - bad AKS signature + */ + @ParameterizedTest + @MethodSource("enclaveParams") + public void testVerifyBadAkvSignature(String serverName, String url, String protocol) throws Exception { + setAEConnectionString(serverName, url, protocol); + + // create CMK with a bad signature + String badCmk = Constants.CMK_NAME + "_badCMK"; + String badCek = Constants.CEK_NAME + "_badCek"; + String badTable = TestUtils.escapeSingleQuotes( + AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("testVerifyBadAkvSignature"))); + + try (SQLServerConnection c = PrepUtil.getConnection(AETestConnectionString, AEInfo); + Statement s = c.createStatement()) { + createCMK(AETestConnectionString, badCmk, Constants.AZURE_KEY_VAULT_NAME, keyIDs[0], "0x666"); + createCEK(AETestConnectionString, badCmk, badCek, akvProvider); + + createTable(badTable, badCek, varcharTableSimple); + + PreparedStatement pstmt = c.prepareStatement("INSERT INTO " + badTable + " VALUES (?,?,?)"); + pstmt.setString(1, "a"); + pstmt.setString(2, "b"); + pstmt.setString(3, "test"); + pstmt.execute(); + + pstmt = c.prepareStatement("SELECT * FROM " + badTable + " WHERE RANDOMIZEDVarchar LIKE ?"); + pstmt.setString(1, "t%"); + try (ResultSet rs = pstmt.executeQuery()) { + fail(TestResource.getResource("R_expectedFailPassed")); + } + } catch (Exception e) { + assertTrue( + e.getMessage().matches(TestUtils.formatErrorMsg("R_SignatureNotMatch")) + || e.getMessage().matches(TestUtils.formatErrorMsg("R_VerifySignatureFailed")), + e.getMessage()); + } finally { + try (Statement s = connection.createStatement()) { + TestUtils.dropTableIfExists(badTable, s); + dropCEK(badCek, s); + dropCMK(badCmk, s); + } + } + } + + /* + * Negative Test - bad AKS signature + */ + @ParameterizedTest + @MethodSource("enclaveParams") + public void testVerifyBadWinSignature(String serverName, String url, String protocol) throws Exception { + setAEConnectionString(serverName, url, protocol); + + String badCmk = Constants.CMK_NAME + "_badCMK"; + String badCek = Constants.CEK_NAME + "_badCek"; + String badTable = TestUtils.escapeSingleQuotes( + AbstractSQLGenerator.escapeIdentifier(RandomUtil.getIdentifier("testVerifyBadWinSignature"))); + + try (SQLServerConnection c = PrepUtil.getConnection(AETestConnectionString, AEInfo); + Statement s = c.createStatement()) { + // create CMK with a bad signature + createCMK(AETestConnectionString, badCmk, Constants.WINDOWS_KEY_STORE_NAME, windowsKeyPath, "0x666"); + createCEK(AETestConnectionString, badCmk, badCek, null); + + createTable(badTable, badCek, varcharTableSimple); + + PreparedStatement pstmt = c.prepareStatement("INSERT INTO " + badTable + " VALUES (?,?,?)"); + pstmt.setString(1, "a"); + pstmt.setString(2, "b"); + pstmt.setString(3, "test"); + pstmt.execute(); + + pstmt = c.prepareStatement("SELECT * FROM " + badTable + " WHERE RANDOMIZEDVarchar LIKE ?"); + pstmt.setString(1, "t%"); + try (ResultSet rs = pstmt.executeQuery()) { + fail(TestResource.getResource("R_expectedFailPassed")); + } + } catch (Exception e) { + // windows error message is different + assertTrue( + e.getMessage().contains("signature does not match") + || e.getMessage().matches(TestUtils.formatErrorMsg("R_VerifySignatureFailed")), + e.getMessage()); + } finally { + try (Statement s = connection.createStatement()) { + TestUtils.dropTableIfExists(badTable, s); + dropCEK(badCek, s); + dropCMK(badCmk, s); + } + } + } + /* * Tests alter column encryption on char tables */ @@ -213,6 +354,7 @@ public void testChar(String serverName, String url, String protocol) throws Exce /* * Tests alter column encryption on char tables with AKV */ + @ParameterizedTest @MethodSource("enclaveParams") public void testCharAkv(String serverName, String url, String protocol) throws Exception { @@ -306,6 +448,7 @@ public void testStringRichQuery(String serverName, String url, String protocol) try (SQLServerConnection c = PrepUtil.getConnection(AETestConnectionString, AEInfo); Statement s = c.createStatement()) { createTable(CHAR_TABLE_AE, cekJks, varcharTableSimple); + PreparedStatement pstmt = c.prepareStatement("INSERT INTO " + CHAR_TABLE_AE + " VALUES (?,?,?)"); pstmt.setString(1, "a"); pstmt.setString(2, "b"); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MSITest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MSITest.java index 5afa2e78e..9aeb0da8f 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MSITest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MSITest.java @@ -51,6 +51,7 @@ public class MSITest extends AESetup { @Tag(Constants.xSQLv12) @Tag(Constants.xSQLv14) @Tag(Constants.xSQLv15) + @Tag(Constants.xSQLv16) @Test public void testManagedIdentityAuth() throws SQLException { String connStr = connectionString; @@ -78,6 +79,7 @@ private void testSimpleConnect(String connStr) { @Tag(Constants.xSQLv12) @Tag(Constants.xSQLv14) @Tag(Constants.xSQLv15) + @Tag(Constants.xSQLv16) @Test public void testManagedIdentityAuthWithManagedIdentityClientId() throws SQLException { String connStr = connectionString; @@ -116,6 +118,7 @@ public void testManagedIdentityAuthWithManagedIdentityClientId() throws SQLExcep @Tag(Constants.xSQLv12) @Tag(Constants.xSQLv14) @Tag(Constants.xSQLv15) + @Tag(Constants.xSQLv16) @Test public void testDSManagedIdentityAuth() throws SQLException { String connStr = connectionString; @@ -141,6 +144,7 @@ public void testDSManagedIdentityAuth() throws SQLException { @Tag(Constants.xSQLv12) @Tag(Constants.xSQLv14) @Tag(Constants.xSQLv15) + @Tag(Constants.xSQLv16) @Test public void testDSManagedIdentityAuthWithManagedIdentityClientId() throws SQLException { String connStr = connectionString; @@ -169,6 +173,7 @@ public void testDSManagedIdentityAuthWithManagedIdentityClientId() throws SQLExc @Tag(Constants.xSQLv12) @Tag(Constants.xSQLv14) @Tag(Constants.xSQLv15) + @Tag(Constants.xSQLv16) @Test public void testActiveDirectoryDefaultAuth() throws SQLException { String connStr = connectionString; @@ -188,6 +193,7 @@ public void testActiveDirectoryDefaultAuth() throws SQLException { @Tag(Constants.xSQLv12) @Tag(Constants.xSQLv14) @Tag(Constants.xSQLv15) + @Tag(Constants.xSQLv16) @Test public void testActiveDirectoryDefaultAuthDS() throws SQLException { String connStr = connectionString; diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java index 77630e8a4..2619a79b5 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/AlwaysEncrypted/MultiUserAKVTest.java @@ -355,7 +355,7 @@ public void testConnectionCustomKeyStoreProviderDuringAeQuery() throws Exception // Create cmk and cek for DummyKeyStoreProvider createCMK(AETestConnectionString, cmkDummy, Constants.DUMMY_KEYSTORE_NAME, keyIDs[0], - Constants.CMK_SIGNATURE_AKV); + TestUtils.byteToHexDisplayString(akvProvider.signColumnMasterKeyMetadata(keyIDs[0], true))); createCEK(AETestConnectionString, cmkDummy, cekDummy, akvProvider); // Create an empty table for testing @@ -440,7 +440,7 @@ public void testStatementCustomKeyStoreProviderDuringAeQuery() throws Exception // Create an empty table for testing createCMK(AETestConnectionString, cmkDummy, Constants.DUMMY_KEYSTORE_NAME, keyIDs[0], - Constants.CMK_SIGNATURE_AKV); + TestUtils.byteToHexDisplayString(akvProvider.signColumnMasterKeyMetadata(keyIDs[0], true))); createCEK(AETestConnectionString, cmkDummy, cekDummy, akvProvider); createTableForCustomProvider(AETestConnectionString, customProviderTableName, cekDummy); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java index 70336f8d3..ded0aa256 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/SQLServerConnectionTest.java @@ -336,6 +336,22 @@ public void testEncryptedConnection() throws SQLException { try (Connection con = ds.getConnection()) {} } + @Tag(Constants.xSQLv11) + @Tag(Constants.xSQLv12) + @Tag(Constants.xSQLv14) + @Tag(Constants.xSQLv15) + @Tag(Constants.xAzureSQLDW) + @Tag(Constants.xAzureSQLDB) + @Test + public void testEncryptedStrictConnection() throws SQLException { + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setURL(connectionString); + ds.setServerCertificate(serverCertificate); + ds.setEncrypt(Constants.STRICT); + + try (Connection con = ds.getConnection()) {} + } + @Test public void testJdbcDataSourceMethod() throws SQLFeatureNotSupportedException { SQLServerDataSource fxds = new SQLServerDataSource(); @@ -429,6 +445,75 @@ public void testConnectionPoolGetTwice() throws SQLException { } } + /** + * Tests whether connectRetryCount and connectRetryInterval are properly respected in the login loop. As well, tests + * that connection is retried the proper number of times. + */ + @Test + public void testConnectCountInLoginAndCorrectRetryCount() { + long timerStart = 0; + + int connectRetryCount = 3; + int connectRetryInterval = 1; + int longLoginTimeout = loginTimeOutInSeconds * 4; // 120 seconds + + try { + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setURL(connectionString); + ds.setLoginTimeout(longLoginTimeout); + ds.setConnectRetryCount(connectRetryCount); + ds.setConnectRetryInterval(connectRetryInterval); + ds.setDatabaseName(RandomUtil.getIdentifier("DataBase")); + timerStart = System.currentTimeMillis(); + + try (Connection con = ds.getConnection()) { + assertTrue(con == null, TestResource.getResource("R_shouldNotConnect")); + } + } catch (Exception e) { + assertTrue(e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")), e.getMessage()); + long totalTime = System.currentTimeMillis() - timerStart; + int expectedMinimumTimeInMillis = (connectRetryCount * connectRetryInterval) * 1000; // 3 seconds + + // Minimum time is 0 seconds per attempt and connectRetryInterval * connectRetryCount seconds of interval. + // Maximum is unknown, but is needs to be less than longLoginTimeout or else this is an issue. + assertTrue(totalTime > expectedMinimumTimeInMillis, TestResource.getResource("R_executionNotLong")); + assertTrue(totalTime < (longLoginTimeout * 1000L), TestResource.getResource("R_executionTooLong")); + } + } + + /** + * Tests whether connectRetryCount and connectRetryInterval are properly respected in the login loop. As well, tests + * that connection is retried the proper number of times. This is for cases with zero retries. + */ + @Test + public void testConnectCountInLoginAndCorrectRetryCountWithZeroRetry() { + long timerStart = 0; + + int connectRetryCount = 0; + int connectRetryInterval = 60; + int longLoginTimeout = loginTimeOutInSeconds * 3; // 90 seconds + + try { + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setURL(connectionString); + ds.setLoginTimeout(longLoginTimeout); + ds.setConnectRetryCount(connectRetryCount); + ds.setConnectRetryInterval(connectRetryInterval); + ds.setDatabaseName(RandomUtil.getIdentifier("DataBase")); + timerStart = System.currentTimeMillis(); + + try (Connection con = ds.getConnection()) { + assertTrue(con == null, TestResource.getResource("R_shouldNotConnect")); + } + } catch (Exception e) { + assertTrue(e.getMessage().contains(TestResource.getResource("R_cannotOpenDatabase")), e.getMessage()); + long totalTime = System.currentTimeMillis() - timerStart; + + // Maximum is unknown, but is needs to be less than longLoginTimeout or else this is an issue. + assertTrue(totalTime < (longLoginTimeout * 1000L), TestResource.getResource("R_executionTooLong")); + } + } + @Test @Tag(Constants.xAzureSQLDW) @Tag(Constants.xAzureSQLDB) @@ -939,65 +1024,63 @@ public void run() { assertTrue(status && future.isCancelled(), TestResource.getResource("R_threadInterruptNotSet")); } + /** + * Test thread count when finding socket using threading. + */ + @Test + @Tag(Constants.xAzureSQLDB) + @Tag(Constants.xAzureSQLDW) + public void testThreadCountWhenFindingSocket() { + ExecutorService executor = null; + ManagementFactory.getThreadMXBean().resetPeakThreadCount(); + + // First, check to see if there is a reachable local host, or else test will fail. + try { + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setServerName("localhost"); + Connection con = ds.getConnection(); + } catch (SQLServerException e) { + // Assume this will be an error different than 'localhost is unreachable'. If it is 'localhost is + // unreachable' abort and skip the test. + Assume.assumeFalse(e.getMessage().startsWith(TestResource.getResource("R_tcpipConnectionToHost"))); + } + + try { + executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "")); + executor.submit(() -> { + try { + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setServerName("localhost"); + Thread.sleep(5000); + Connection conn2 = ds.getConnection(); + } catch (Exception e) { + if (!(e instanceof SQLServerException)) { + fail(TestResource.getResource("R_unexpectedException") + e.getMessage()); + } + } + }); + SQLServerDataSource ds = new SQLServerDataSource(); + ds.setServerName("localhost"); + Connection conn = ds.getConnection(); + Thread.sleep(5000); + } catch (Exception e) { + if (!(e instanceof SQLServerException)) { + fail(TestResource.getResource("R_unexpectedException") + e.getMessage()); + } + } finally { + if (executor != null) { + executor.shutdown(); + } + } + + // At this point, thread count has returned to normal. If the peak was more + // than 2 times the current, this is an issue and the test should fail. + if (ManagementFactory.getThreadMXBean().getPeakThreadCount() > 2 + * ManagementFactory.getThreadMXBean().getThreadCount()) { + fail(TestResource.getResource("R_unexpectedThreadCount")); + } + } - /** - * Test thread count when finding socket using threading. - */ - @Test - @Tag(Constants.xAzureSQLDB) - @Tag(Constants.xAzureSQLDW) - public void testThreadCountWhenFindingSocket() { - ExecutorService executor = null; - ManagementFactory.getThreadMXBean().resetPeakThreadCount(); - - // First, check to see if there is a reachable local host, or else test will fail. - try { - SQLServerDataSource ds = new SQLServerDataSource(); - ds.setServerName("localhost"); - Connection con = ds.getConnection(); - } catch (SQLServerException e) { - // Assume this will be an error different than 'localhost is unreachable'. If it is 'localhost is - // unreachable' abort and skip the test. - Assume.assumeFalse(e.getMessage().startsWith(TestResource.getResource("R_tcpipConnectionToHost"))); - } - - try { - executor = Executors.newSingleThreadExecutor(r -> new Thread(r, "")); - executor.submit(() -> { - try { - SQLServerDataSource ds = new SQLServerDataSource(); - ds.setServerName("localhost"); - Thread.sleep(5000); - Connection conn2 = ds.getConnection(); - } catch (Exception e) { - if (!(e instanceof SQLServerException)) { - fail(TestResource.getResource("R_unexpectedException") + e.getMessage()); - } - } - }); - SQLServerDataSource ds = new SQLServerDataSource(); - ds.setServerName("localhost"); - Connection conn = ds.getConnection(); - Thread.sleep(5000); - } catch (Exception e) { - if (!(e instanceof SQLServerException)) { - fail(TestResource.getResource("R_unexpectedException") + e.getMessage()); - } - } finally { - if (executor != null) { - executor.shutdown(); - } - } - - // At this point, thread count has returned to normal. If the peak was more - // than 2 times the current, this is an issue and the test should fail. - if (ManagementFactory.getThreadMXBean().getPeakThreadCount() - > 2 * ManagementFactory.getThreadMXBean().getThreadCount()) { - fail(TestResource.getResource("R_unexpectedThreadCount")); - } - } - - /** * Test calling method to get redirected server string. */ diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java index 8041a35be..d4bab3996 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/databasemetadata/DatabaseMetaDataTest.java @@ -18,6 +18,7 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.sql.CallableStatement; import java.sql.Connection; import java.sql.DatabaseMetaData; import java.sql.ResultSet; @@ -43,6 +44,7 @@ import com.microsoft.sqlserver.jdbc.RandomUtil; import com.microsoft.sqlserver.jdbc.SQLServerDataSource; import com.microsoft.sqlserver.jdbc.SQLServerDatabaseMetaData; +import com.microsoft.sqlserver.jdbc.SQLServerException; import com.microsoft.sqlserver.jdbc.StringUtils; import com.microsoft.sqlserver.jdbc.TestResource; import com.microsoft.sqlserver.jdbc.TestUtils; @@ -155,8 +157,6 @@ public void testGetURL() throws SQLException { * @throws SQLException */ @Test - @Tag(Constants.xAzureSQLDW) - @Tag(Constants.xAzureSQLDB) public void testDBUserLogin() throws SQLException { try (Connection conn = getConnection()) { DatabaseMetaData databaseMetaData = conn.getMetaData(); @@ -187,6 +187,45 @@ public void testDBUserLogin() throws SQLException { } } + @Test + @Tag(Constants.xAzureSQLDW) + public void testImpersonateGetUserName() throws SQLException { + String newUser = "newUser" + UUID.randomUUID(); + + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + String escapedNewUser = AbstractSQLGenerator.escapeIdentifier(newUser); + String password = "password" + UUID.randomUUID(); + + stmt.execute("IF EXISTS (select * from sys.sysusers where name = '" + escapedNewUser + "') DROP USER" + + escapedNewUser); + + // create new user and login + try { + stmt.execute("CREATE USER " + escapedNewUser + " WITH password='" + password + "'"); + } catch (SQLServerException e) { + // handle failed cases when database is master + if (e.getMessage().contains("contained database")) { + stmt.execute("CREATE LOGIN " + escapedNewUser + " WITH password='" + password + "'"); + stmt.execute("CREATE USER " + escapedNewUser); + } + } + + DatabaseMetaData databaseMetaData = conn.getMetaData(); + try (CallableStatement asOtherUser = conn.prepareCall("EXECUTE AS USER = '" + newUser + "'")) { + asOtherUser.execute(); + assertTrue(newUser.equalsIgnoreCase(databaseMetaData.getUserName()), + TestResource.getResource("R_userNameNotMatch")); + } catch (Exception e) { + fail(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage()); + } finally { + stmt.execute("IF EXISTS (select * from sys.sysusers where name = '" + escapedNewUser + "') DROP USER" + + escapedNewUser); + stmt.execute("IF EXISTS (select * from sys.sysusers where name = '" + escapedNewUser + "') DROP LOGIN" + + escapedNewUser); + } + } + } + /** * Testing of {@link SQLServerDatabaseMetaData#getSchemas()} * @@ -306,6 +345,57 @@ public void testDBSchemasForDashedCatalogNameWithPattern() throws SQLException { } } + /** + * Tests that the schemaPattern parameter containing _ and % are escaped by + * {@link SQLServerDatabaseMetaData#getSchemas(String catalog, String schemaPattern)}. + * + * @throws SQLException + */ + @Test + @Tag(Constants.xAzureSQLDW) + @Tag(Constants.xAzureSQLDB) + public void testDBSchemasForSchemaPatternWithWildcards() throws SQLException { + UUID id = UUID.randomUUID(); + String testCatalog = "catalog" + id; + String[] schemas = {"some_schema", "some%schema", "some[schema"}; + String[] schemaPatterns = {"some\\_schema", "some\\%schema", "some\\[schema"}; + + try (Connection conn = getConnection(); Statement stmt = conn.createStatement()) { + TestUtils.dropDatabaseIfExists(testCatalog, connectionString); + stmt.execute(String.format("CREATE DATABASE [%s]", testCatalog)); + stmt.execute(String.format("USE [%s]", testCatalog)); + + for (int i = 0; i < schemas.length; ++i) { + stmt.execute(String.format("CREATE SCHEMA [%s]", schemas[i])); + + try (ResultSet rs = conn.getMetaData().getSchemas(testCatalog, schemaPatterns[i])) { + + MessageFormat schemaEmptyFormat = new MessageFormat(TestResource.getResource("R_nameEmpty")); + Object[] schemaMsgArgs = {schemas[i]}; + Object[] catalogMsgArgs = {testCatalog}; + + boolean hasResults = false; + while (rs.next()) { + hasResults = true; + String schemaName = rs.getString(1); + String catalogName = rs.getString(2); + assertTrue(!StringUtils.isEmpty(schemaName), schemaEmptyFormat.format(schemaMsgArgs)); + assertTrue(!StringUtils.isEmpty(catalogName), schemaEmptyFormat.format(catalogMsgArgs)); + assertEquals(schemaName, schemaMsgArgs[0]); + assertEquals(catalogName, catalogMsgArgs[0]); + } + + MessageFormat atLeastOneFoundFormat = new MessageFormat(TestResource.getResource("R_atLeastOneFound")); + assertTrue(hasResults, atLeastOneFoundFormat.format(schemaMsgArgs)); + } + } + } catch (Exception e) { + fail(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage()); + } finally { + TestUtils.dropDatabaseIfExists(testCatalog, connectionString); + } + } + /** * Get All Tables. * @@ -758,6 +848,7 @@ public void testGetColumns() throws SQLException { @Tag(Constants.xSQLv12) @Tag(Constants.xSQLv14) @Tag(Constants.xSQLv15) + @Tag(Constants.xSQLv16) @Tag(Constants.xAzureSQLDB) @Tag(Constants.xAzureSQLMI) public void testGetImportedKeysDW() throws SQLException { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java index 4ddff40de..5c6a5acc2 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/datatypes/DataTypesTest.java @@ -1624,6 +1624,23 @@ public void testUpdateMisc() throws Exception { fail(TestResource.getResource("R_expectedExceptionNotThrown")); } + + exceptionThrown = true; + // Verify SQLState 22005 is in exception for conversion errors + try { + Timestamp timestamp = Timestamp.valueOf("9999-12-31 23:59:59.998"); + rs.updateTimestamp(1, timestamp); + rs.updateRow(); + rs.getLong(1); + exceptionThrown = false; + } catch (SQLServerException e) { + assertEquals("22005", e.getSQLState()); + } + + if (!exceptionThrown) { + fail(TestResource.getResource("R_expectedExceptionNotThrown")); + } + // Update time(5) from Timestamp with nanos more precise than 100ns Timestamp ts = Timestamp.valueOf("2010-01-12 11:05:23"); ts.setNanos(987659999); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/fips/FipsTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/fips/FipsTest.java index 5a032c59c..10853f2f6 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/fips/FipsTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/fips/FipsTest.java @@ -86,7 +86,7 @@ public void fipsPropertyTest() throws Exception { props.remove(Constants.ENCRYPT); props.remove(Constants.TRUST_SERVER_CERTIFICATE); - try (Connection con = PrepUtil.getConnection(connectionString, props)) { + try (Connection con = PrepUtil.getConnection(connectionString + ";encrypt=false", props)) { Assertions.assertTrue(!StringUtils.isEmpty(con.getSchema())); } catch (Exception e) { fail(TestResource.getResource("R_unexpectedErrorMessage") + e.getMessage()); diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataTest.java index 61b9936af..736a431c9 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/parametermetadata/ParameterMetaDataTest.java @@ -4,6 +4,7 @@ */ package com.microsoft.sqlserver.jdbc.parametermetadata; +import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -127,6 +128,10 @@ public void testParameterMetaData() throws SQLException { assert (metadata.getPrecision(2) == 38); assert (metadata.getScale(2) == 5); assert (!metadata.isSigned(1)); + + // test invalid index + assertThrows(SQLException.class, () -> metadata.getParameterType(0)); + assertThrows(SQLException.class, () -> metadata.getParameterType(3)); } } finally { TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); @@ -160,6 +165,10 @@ public void testParameterMetaDataProc() throws SQLException { assert (metadata.getScale(1) == 0); assert (metadata.isNullable(1) == ParameterMetaData.parameterNullable); assert (!metadata.isSigned(1)); + + // test invalid index + assertThrows(SQLException.class, () -> metadata.getParameterType(0)); + assertThrows(SQLException.class, () -> metadata.getParameterType(3)); } } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java index 6b7d5c5d1..b19d25248 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/resiliency/BasicConnectionTest.java @@ -296,6 +296,7 @@ public void testPooledConnectionLang() throws SQLException { @Tag(Constants.xSQLv12) @Tag(Constants.xSQLv14) @Tag(Constants.xSQLv15) + @Tag(Constants.xSQLv16) @Tag(Constants.xAzureSQLDW) @Tag(Constants.reqExternalSetup) @Test diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/resultset/DataClassificationTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/DataClassificationTest.java index cbc1f9378..5b876e3e9 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/resultset/DataClassificationTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/resultset/DataClassificationTest.java @@ -167,6 +167,7 @@ public void testDataClassificationMetadata() throws Exception { @Tag(Constants.xAzureSQLDB) @Tag(Constants.xAzureSQLDW) @Tag(Constants.xSQLv15) + @Tag(Constants.xSQLv16) @Test public void testDataClassificationNotSupported() throws Exception { try (Statement stmt = connection.createStatement();) { diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java index a61a9b2eb..c7bd10414 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/PreparedStatementTest.java @@ -50,6 +50,7 @@ public class PreparedStatementTest extends AbstractTest { final static String tableName = RandomUtil.getIdentifier("tableTestStatementPoolingInternal1"); final static String tableName2 = RandomUtil.getIdentifier("tableTestStatementPoolingInternal2"); final static String tableName3 = RandomUtil.getIdentifier("tableTestPreparedStatementWithSpPrepare"); + final static String tableName4 = RandomUtil.getIdentifier("tableTestPreparedStatementWithMultipleParams"); @BeforeAll public static void setupTests() throws Exception { @@ -117,6 +118,94 @@ public void testPreparedStatementWithSpPrepare() throws SQLException { } } + @Test + public void testPreparedStatementParamNameSpacingWithMultipleParams() throws SQLException { + int paramNameCount = 105; + + StringBuilder insertSql = new StringBuilder("insert into " + AbstractSQLGenerator.escapeIdentifier(tableName4) + + "values("); + + for (int i = 1; i <= paramNameCount; i++) { + insertSql.append(i % 10); + + if (i != paramNameCount) { + insertSql.append(","); + } else { + insertSql.append(")"); + } + } + + StringBuilder createTableSql = new StringBuilder("create table " + AbstractSQLGenerator.escapeIdentifier(tableName4) + "("); + + for (int i = 1; i <= paramNameCount; i++) { + createTableSql.append("c" + i + " char(1)"); + + if (i != paramNameCount) { + createTableSql.append(","); + } else { + createTableSql.append(")"); + } + } + + try (SQLServerConnection con = (SQLServerConnection) getConnection()) { + executeSQL(con, createTableSql.toString()); + executeSQL(con, insertSql.toString()); + + // There are no typos in the queries eg. The 'c1=?and' is not a typo. We are testing the spacing, or lack of spacing. + // The driver should automatically space out the params eg. 'c1=?and' becomes 'c1= ? and' in the final string we send to the server. + + // Testing with less than 10 params + String sql1 = "select * from " + AbstractSQLGenerator.escapeIdentifier(tableName4) + " where c1=?and c2=?"; + + // Testing with number of params between 10 and 100 + String sql2 = "select * from " + AbstractSQLGenerator.escapeIdentifier(tableName4) + + " where c1=?and c2=? and c3=?and c4=? and c5=? and c6=? and c7=? and c8=? and c9=? and c10=? and c11=? and c12=?"; + + // Testing with more than 100 params + StringBuilder sql3 = new StringBuilder("select * from " + AbstractSQLGenerator.escapeIdentifier(tableName4) + + " where c1=?and "); + + for (int i = 2; i <= paramNameCount; i++) { + sql3.append("c" + i + "=?"); + + if (i != paramNameCount) { + sql3.append(" and "); + } + } + + try (SQLServerPreparedStatement ps = (SQLServerPreparedStatement) con.prepareStatement(sql1)) { + ps.setString(1, "1"); + ps.setString(2, "2"); + ps.executeQuery(); + } + + try (SQLServerPreparedStatement ps = (SQLServerPreparedStatement) con.prepareStatement(sql2)) { + ps.setString(1, "1"); + ps.setString(2, "2"); + ps.setString(3, "3"); + ps.setString(4, "4"); + ps.setString(5, "5"); + ps.setString(6, "6"); + ps.setString(7, "7"); + ps.setString(8, "8"); + ps.setString(9, "9"); + ps.setString(10, "0"); + ps.setString(11, "1"); + ps.setString(12, "2"); + ps.executeQuery(); + } + + try (SQLServerPreparedStatement ps = (SQLServerPreparedStatement) con.prepareStatement(sql3.toString())) { + ps.setString(1, "1"); + for (int i = 2; i <= paramNameCount; i++) { + ps.setString(i, Integer.toString(i % 10)); + } + + ps.executeQuery(); + } + } + } + @Test public void testPreparedStatementPoolEvictionWithSpPrepare() throws SQLException { int cacheSize = 3; @@ -787,6 +876,7 @@ private static void dropTables() throws Exception { TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName), stmt); TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName2), stmt); TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName3), stmt); + TestUtils.dropTableIfExists(AbstractSQLGenerator.escapeIdentifier(tableName4), stmt); } } diff --git a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java index 3bf8e0b1c..76f764e41 100644 --- a/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java +++ b/src/test/java/com/microsoft/sqlserver/jdbc/unit/statement/StatementTest.java @@ -1426,228 +1426,6 @@ public void testFailedToResumeTransaction() throws Exception { } } - /** - * Tests that big decimal values with a precision less than 38 hold their precision. Tests cases where scale is - * 38 (integer part is a 0) and less than 38 (integer part is a non-zero). - * - * @throws SQLException - * when an error occurs - */ - @Test - public void testSmallBigDecimalValuesForLossOfPrecision() throws SQLException { - try (Connection con = getConnection(); - Statement stmt = con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_UPDATABLE)) { - double bigDecimalLessThanOne = 0.1235; - double bigDecimalGreaterThanOne = 1.1235; - String query = "CREATE PROCEDURE " + procName - + " @col1Value decimal(4,4) OUTPUT, @col2Value decimal(5,4) OUTPUT AS BEGIN SET @col1Value = " - + bigDecimalLessThanOne + " SET @col2Value = " + bigDecimalGreaterThanOne + " END"; - stmt.execute(query); - - try (CallableStatement cstmt = con.prepareCall("{CALL " + procName + "(?, ?)}")) { - cstmt.registerOutParameter("col1Value", java.sql.Types.DECIMAL, "DECIMAL"); - cstmt.registerOutParameter("col2Value", java.sql.Types.DECIMAL, "DECIMAL"); - cstmt.execute(); - - // Previously, the leading 0 would be counted as part of the precision. This would lead to the actual - // value being stored as 0.123. - assertEquals(0, - cstmt.getObject("col1Value", BigDecimal.class).compareTo(BigDecimal.valueOf(bigDecimalLessThanOne))); - assertEquals(0, - cstmt.getObject("col2Value", BigDecimal.class).compareTo(BigDecimal.valueOf(bigDecimalGreaterThanOne))); - } - } - } - - /** - * Tests that big decimal values with a precision equal to 38 hold their precision. Tests cases where scale is - * 38 (integer part is a 0) and less than 38 (integer part is a non-zero). - * - * @throws SQLException - * when an error occurs - */ - @Test - public void testLongBigDecimalValuesForLossOfPrecision() throws SQLException { - try (Connection con = getConnection(); Statement stmt = con.createStatement()) { - stmt.executeUpdate("CREATE TABLE " + tableName + " (col1 decimal(38,38), col2 decimal(38,37))"); - - // col1 has maximum scale (38) with a leading zero, for a precision of 38. col2 has maximum scale (37) when - // using a lead integer other than zero, also resulting in a precision of 38. - stmt.executeUpdate("INSERT INTO " + tableName + " VALUES(0.98432319763138435186412316842316874322, 1.9843231976313843518641231684231687432)"); - - try (PreparedStatement pstmt = con.prepareStatement("SELECT * FROM " + tableName)) { - - try (ResultSet rs = pstmt.executeQuery()) { - rs.next(); - assertEquals(new BigDecimal("0.98432319763138435186412316842316874322"), rs.getObject(1)); - assertEquals(new BigDecimal("1.9843231976313843518641231684231687432"), rs.getObject(2)); - } - } - } - } - - /** - * Tests result of math operation in prepared statement using subtraction - * - * @throws SQLException - * when an error occurs - */ - @Test - public void testMathBigDecimalSubtraction() throws SQLException { - try (Connection con = getConnection(); Statement stmt = con.createStatement()) { - stmt.executeUpdate("CREATE TABLE " + tableName + " (test_column decimal(10,5))"); - stmt.executeUpdate("INSERT INTO " + tableName + " VALUES(99999.12345)"); - try (PreparedStatement pstmt = con.prepareStatement("SELECT (test_column - ?), " - + "(test_column - ?), (test_column - ?), (test_column - ?) FROM " + tableName)) { - BigDecimal value1 = new BigDecimal("1.5"); - pstmt.setObject(1, value1); - BigDecimal value2 = new BigDecimal("0"); - pstmt.setObject(2, value2); - BigDecimal value3 = new BigDecimal("99999.12345"); - pstmt.setObject(3, value3); - BigDecimal value4 = new BigDecimal("99999.2"); - pstmt.setObject(4, value4); - - BigDecimal base = new BigDecimal("99999.12345"); - BigDecimal expected1 = base.subtract(value1); - BigDecimal expected2 = base.subtract(value2); - BigDecimal expected3 = base.subtract(value3); - BigDecimal expected4 = base.subtract(value4); - - try (ResultSet rs = pstmt.executeQuery()) { - rs.next(); - assertEquals(expected1, rs.getObject(1)); - assertEquals(expected2, rs.getObject(2)); - assertEquals(expected3, rs.getObject(3)); - assertEquals(expected4, rs.getObject(4)); - } - } - } - } - - /** - * Tests result of math operation in prepared statement using addition - * - * @throws SQLException - * when an error occurs - */ - @Test - public void testMathBigDecimalAddition() throws SQLException { - try (Connection con = getConnection(); Statement stmt = con.createStatement()) { - stmt.executeUpdate("CREATE TABLE " + tableName + " (test_column decimal(10,5))"); - stmt.executeUpdate("INSERT INTO " + tableName + " VALUES(99999.12345)"); - try (PreparedStatement pstmt = con.prepareStatement("SELECT (test_column + ?), " - + "(test_column + ?), (test_column + ?), (test_column + ?) FROM " + tableName)) { - BigDecimal value1 = new BigDecimal("1.5"); - pstmt.setObject(1, value1); - BigDecimal value2 = new BigDecimal("0"); - pstmt.setObject(2, value2); - BigDecimal value3 = new BigDecimal("99999.12345"); - pstmt.setObject(3, value3); - BigDecimal value4 = new BigDecimal("99999.2"); - pstmt.setObject(4, value4); - - BigDecimal base = new BigDecimal("99999.12345"); - BigDecimal expected1 = base.add(value1); - BigDecimal expected2 = base.add(value2); - BigDecimal expected3 = base.add(value3); - BigDecimal expected4 = base.add(value4); - - try (ResultSet rs = pstmt.executeQuery()) { - rs.next(); - assertEquals(expected1, rs.getObject(1)); - assertEquals(expected2, rs.getObject(2)); - assertEquals(expected3, rs.getObject(3)); - assertEquals(expected4, rs.getObject(4)); - } - } - } - } - - /** - * Tests result of math operation in prepared statement using multiplication - * - * @throws SQLException - * when an error occurs - */ - @Test - public void testMathBigDecimalMultiplication() throws SQLException { - try (Connection con = getConnection(); Statement stmt = con.createStatement()) { - stmt.executeUpdate("CREATE TABLE " + tableName + " (test_column decimal(10,5))"); - stmt.executeUpdate("INSERT INTO " + tableName + " VALUES(99999.12345)"); - try (PreparedStatement pstmt = con.prepareStatement("SELECT (test_column * ?), " - + "(test_column * ?), (test_column * ?), (test_column * ?) FROM " + tableName)) { - BigDecimal value1 = new BigDecimal("1.5"); - pstmt.setObject(1, value1); - BigDecimal value2 = new BigDecimal("0"); - pstmt.setObject(2, value2); - BigDecimal value3 = new BigDecimal("99999.12345"); - pstmt.setObject(3, value3); - BigDecimal value4 = new BigDecimal("99999.2"); - pstmt.setObject(4, value4); - - BigDecimal base = new BigDecimal("99999.12345"); - BigDecimal expected1 = base.multiply(value1); - BigDecimal expected2 = base.multiply(value2); - BigDecimal expected3 = base.multiply(value3); - BigDecimal expected4 = base.multiply(value4); - - try (ResultSet rs = pstmt.executeQuery()) { - rs.next(); - assertEquals(expected1, rs.getObject(1)); - assertEquals(expected2, rs.getObject(2)); - assertEquals(expected3, rs.getObject(3)); - assertEquals(expected4, rs.getObject(4)); - } - } - } - } - - /** - * Tests result of math operation in prepared statement using division - * - * @throws SQLException - * when an error occurs - */ - @Test - public void testMathBigDecimalDivision() throws SQLException { - try (Connection con = getConnection(); Statement stmt = con.createStatement()) { - stmt.executeUpdate("CREATE TABLE " + tableName + " (test_column decimal(10,5))"); - stmt.executeUpdate("INSERT INTO " + tableName + " VALUES(99999.12345)"); - try (PreparedStatement pstmt = con.prepareStatement("select (test_column / ?), " - + "(test_column / ?), (test_column / ?), (test_column / ?) FROM " + tableName)) { - - /* - * Division has some unique properties in sql server math operations. - * Notably in this case we cannot compare a result with an infinite trailing decimal - * and the returned value has an expanded precision. - */ - BigDecimal value1 = new BigDecimal("1.5"); - pstmt.setObject(1, value1); - BigDecimal value2 = new BigDecimal("0.1"); - pstmt.setObject(2, value2); - BigDecimal value3 = new BigDecimal("99999.12345"); - pstmt.setObject(3, value3); - BigDecimal value4 = new BigDecimal("1"); - pstmt.setObject(4, value4); - - BigDecimal base = new BigDecimal("99999.12345"); - BigDecimal expected1 = base.divide(value1, RoundingMode.HALF_UP); - BigDecimal expected2 = base.divide(value2, RoundingMode.HALF_UP); - BigDecimal expected3 = base.divide(value3, RoundingMode.HALF_UP); - BigDecimal expected4 = base.divide(value4, RoundingMode.HALF_UP); - - try (ResultSet rs = pstmt.executeQuery()) { - rs.next(); - assertEquals(0, expected1.compareTo((BigDecimal) rs.getObject(1))); - assertEquals(0, expected2.compareTo((BigDecimal) rs.getObject(2))); - assertEquals(0, expected3.compareTo((BigDecimal) rs.getObject(3))); - assertEquals(0, expected4.compareTo((BigDecimal) rs.getObject(4))); - } - } - } - } - /** * * @throws Exception diff --git a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java index 09c3c245e..9113ca192 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/AbstractTest.java @@ -77,13 +77,15 @@ public abstract class AbstractTest { protected static String trustStore = ""; protected static String trustStorePassword = ""; + protected static String serverCertificate = ""; + protected static String encrypt = ""; protected static String trustServerCertificate = ""; protected static String windowsKeyPath = null; protected static String javaKeyPath = null; protected static String javaKeyAliases = null; - protected static SQLServerColumnEncryptionKeyStoreProvider jksProvider = null; + protected static SQLServerColumnEncryptionJavaKeyStoreProvider jksProvider = null; protected static SQLServerColumnEncryptionAzureKeyVaultProvider akvProvider = null; static boolean isKspRegistered = false; @@ -210,6 +212,12 @@ public static void setup() throws Exception { trustStorePassword); } + serverCertificate = getConfiguredProperty("serverCertificate", ""); + if (!serverCertificate.trim().isEmpty()) { + connectionString = TestUtils.addOrOverrideProperty(connectionString, "serverCertificate", + serverCertificate); + } + Map map = new HashMap(); if (null == jksProvider) { jksProvider = new SQLServerColumnEncryptionJavaKeyStoreProvider(javaKeyPath, diff --git a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java index d5b475941..3b2d08721 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/Constants.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/Constants.java @@ -20,6 +20,7 @@ private Constants() {} * xSQLv12 - - - - - - For tests not compatible with SQL Server 2008 R2 - 2014 * xSQLv14 - - - - - - For tests not compatible with SQL Server 2016 - 2017 * xSQLv15 - - - - - - For tests not compatible with SQL Server 2019 + * xSQLv16 - - - - - - For tests not compatible with SQL Server 2022 * xAzureSQLDB - - - - For tests not compatible with Azure SQL Database * xAzureSQLDW - - - - For tests not compatible with Azure Data Warehouse * xAzureSQLMI - - - - For tests not compatible with Azure SQL Managed Instance @@ -36,6 +37,7 @@ private Constants() {} public static final String xSQLv12 = "xSQLv12"; public static final String xSQLv14 = "xSQLv14"; public static final String xSQLv15 = "xSQLv15"; + public static final String xSQLv16 = "xSQLv16"; public static final String xAzureSQLDB = "xAzureSQLDB"; public static final String xAzureSQLDW = "xAzureSQLDW"; public static final String xAzureSQLMI = "xAzureSQLMI"; @@ -113,8 +115,9 @@ private Constants() {} public static final String UID = UUID.randomUUID().toString(); public static final Long RANDOM_LONG = Long.valueOf((long) (RANDOM.nextDouble() * Math.pow(10, 10))); public static final String CMK_NAME = "JDBC_CMK_" + RANDOM_LONG; - public static final String CMK_SIGNATURE = "0x5859A75C4F8C05C9865DAE46CA70F34B94EF1DF25D185DE8AB67918E5CF02953F2C787EFDC57ACAD5AEC2634C24B0A4C98B3A659220E980D948F91238A5D98CD898EF76CCA4AED9A6D51EBF4EABDEAA28CF975A7C8083E188AA4677303DAC9D6F49C81CB42E4BF14365B56BFEDD6E4B0DF0BC69E9FE313A7001613822D724A97055FA110C495A98EE37BE137293DF9E569D0845B96C4A6DD8554D42E83E784E3DCAFFDDD2CB3A7352920C3AAC43EA79BE5B8C7203B0E2F9B7C317C542E632C529BC46D792D9C85E16B5B3EE50D6D5E53103CC27B60175F79ECA1F2B28995FD1C12FA109E53810192BDD38A3B9A5D2CCC2BCBF16F936FE211A55FD999689C8CC4"; - public static final String CMK_SIGNATURE_AKV = "0x0474EC516A93B3EBCBD58E3AC35699E125F937AE6A56E96EC4FC37ACD09284CC99702B4762E56E8518A23A4D7EBF62FF735C8EEF4B61326CB8300ED09076D69008ACEB4156D9F2E1F95A8373335C33FCDD7DD7DF69CDD23544AB0D63B6D93379593E15C24D31EC0020F62BA23A19165C8A58AFAE8304DAC2996470919EBAB97587D685AEF4FFA3666E65DA673F41B2204410AFA69B9E05402853AB2AF0D22F4CF498394EB9DA8CA55814601DE6E004B12886C069010F911AE1F0EDA5DA3F1A09A211C7C30D21A567D47A8F133DF20A44E694900344FF3E189A8D6069CB86AA63D168B90CE4150F09A78DC09EF20FC239EA299E964762AE1FF711E407936FA9C9"; + + public static final String CMK_SIGNATURE_WIN = "0x625F3105C77DF6881D11B9D5A64491452F288AA9CE66189F19AF9591FAF2525B90CE580DD29CF897F6809EC30AF583CF1603477C5591D785A89DA3CE60E03D9CC494926322E0EF15A4EA38C0FFA0B90D29D051DA037631E6D3B435F87BAB46927ECF366AE96C5259D03A227A2BC04B15E09794CA1ABD1B34CB117BF06962225D4157DD1FBEF998D547EC7E201C00ACF2D2B8A746D1FD3FAEED791132D97EA396CABE9C472C8B559C5EF35342EE9D5635C7C18C1BD168647E3BA52B674626D9F8C3CFAC150300EDA157DE3D1B1D7FFB946D1CCE37ACFD28F62D57365BA89088627EE9469B04142A5F5505B71C94999A8D4A1269F15842DF3DA3CC5B41DDD7B115"; + public static final String CEK_NAME = "JDBC_CEK_" + RANDOM_LONG; public static final String CEK_ALGORITHM = "RSA_OAEP"; public static final String CEK_STRING = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; diff --git a/src/test/java/com/microsoft/sqlserver/testframework/DummyKeyStoreProvider.java b/src/test/java/com/microsoft/sqlserver/testframework/DummyKeyStoreProvider.java index fbfa3fda8..bca8fa1a1 100644 --- a/src/test/java/com/microsoft/sqlserver/testframework/DummyKeyStoreProvider.java +++ b/src/test/java/com/microsoft/sqlserver/testframework/DummyKeyStoreProvider.java @@ -9,7 +9,7 @@ /** - * DummyKeyStoreProvider Class for Tests. + * DummyKeyStoreProvider Class for Tests. * */ public class DummyKeyStoreProvider extends SQLServerColumnEncryptionKeyStoreProvider { @@ -28,21 +28,19 @@ public byte[] decryptColumnEncryptionKey(String masterKeyPath, String encryption byte[] encryptedColumnEncryptionKey) throws SQLServerException { // Not implemented throw new UnsupportedOperationException(); - } + } @Override - public byte[] encryptColumnEncryptionKey(String masterKeyPath, - String encryptionAlgorithm, + public byte[] encryptColumnEncryptionKey(String masterKeyPath, String encryptionAlgorithm, byte[] columnEncryptionKey) throws SQLServerException { // Not implemented throw new UnsupportedOperationException(); } @Override - public boolean verifyColumnMasterKeyMetadata(String masterKeyPath, - boolean allowEnclaveComputations, + public boolean verifyColumnMasterKeyMetadata(String masterKeyPath, boolean allowEnclaveComputations, byte[] signature) throws SQLServerException { // Not implemented throw new UnsupportedOperationException(); } -} \ No newline at end of file +}