Skip to content

Commit e0b1a9a

Browse files
SNOW-896818 Decrypt Private Keys With BouncyCastle
Added the JVM argument -Dnet.snowflake.jdbc.enableBouncyCastle to allow the JDBC driver to leverage the BouncyCastle provider to decrypt private keys instead of using the default security providers in the JDK. This helps work around a limitation with being able to decrypt private keys with PBES2 parameters. Ref: https://bugs.openjdk.org/browse/JDK-8228481
2 parents 8caf4a3 + 066e25a commit e0b1a9a

15 files changed

+466
-55
lines changed

FIPS/src/test/java/net/snowflake/client/jdbc/ConnectionFipsIT.java

+20-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import net.snowflake.client.ConditionalIgnoreRule;
2424
import net.snowflake.client.RunningOnGithubActions;
2525
import net.snowflake.client.category.TestCategoryFips;
26+
import net.snowflake.client.core.SecurityUtil;
2627
import org.apache.commons.codec.binary.Base64;
2728
import org.bouncycastle.crypto.CryptoServicesRegistrar;
2829
import org.bouncycastle.crypto.fips.FipsStatus;
@@ -161,7 +162,7 @@ public static void setup() throws Exception {
161162
}
162163

163164
// attempts an SSL connection to Google
164-
//connectToGoogle();
165+
// connectToGoogle();
165166
}
166167

167168
@AfterClass
@@ -205,9 +206,10 @@ public static void teardown() throws Exception {
205206
JAVA_SYSTEM_PROPERTY_SSL_TRUSTSTORE_TYPE,
206207
JAVA_SYSTEM_PROPERTY_SSL_TRUSTSTORE_TYPE_ORIGINAL_VALUE);
207208
}
209+
System.clearProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM);
208210

209211
// attempts an SSL connection to Google
210-
//connectToGoogle();
212+
// connectToGoogle();
211213
}
212214

213215
@Test
@@ -319,6 +321,22 @@ public void connectWithFipsAndPut() throws Exception {
319321
}
320322
}
321323

324+
/** Added in > 3.15.1 */
325+
@Test
326+
@ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubActions.class)
327+
public void connectWithFipsKeyPairWithBouncyCastle() throws Exception {
328+
System.setProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM, "true");
329+
connectWithFipsKeyPair();
330+
}
331+
332+
/** Added in > 3.15.1 */
333+
@Test
334+
@ConditionalIgnoreRule.ConditionalIgnore(condition = RunningOnGithubActions.class)
335+
public void testConnectUsingKeyPairWithBouncyCastle() throws Exception {
336+
System.setProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM, "true");
337+
testConnectUsingKeyPair();
338+
}
339+
322340
private static void connectToGoogle() throws Exception {
323341
URL url = new URL("https://www.google.com/");
324342
HttpsURLConnection con = (HttpsURLConnection) url.openConnection();

src/main/java/net/snowflake/client/core/SFBaseSession.java

+10
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ public abstract class SFBaseSession {
142142

143143
private Map<String, Object> commonParameters;
144144

145+
private boolean isJdbcArrowTreatDecimalAsInt = true;
146+
145147
protected SFBaseSession(SFConnectionHandler sfConnectionHandler) {
146148
this.sfConnectionHandler = sfConnectionHandler;
147149
}
@@ -270,6 +272,14 @@ public void setJdbcTreatDecimalAsInt(boolean jdbcTreatDecimalAsInt) {
270272
isJdbcTreatDecimalAsInt = jdbcTreatDecimalAsInt;
271273
}
272274

275+
public boolean isJdbcArrowTreatDecimalAsInt() {
276+
return isJdbcArrowTreatDecimalAsInt;
277+
}
278+
279+
public void setJdbcArrowTreatDecimalAsInt(boolean jdbcArrowTreatDecimalAsInt) {
280+
isJdbcArrowTreatDecimalAsInt = jdbcArrowTreatDecimalAsInt;
281+
}
282+
273283
public String getServerUrl() {
274284
if (connectionPropertiesMap.containsKey(SFSessionProperty.SERVER_URL)) {
275285
return (String) connectionPropertiesMap.get(SFSessionProperty.SERVER_URL);

src/main/java/net/snowflake/client/core/SFSession.java

+6-4
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,6 @@ public class SFSession extends SFBaseSession {
8787

8888
private SFClientConfig sfClientConfig;
8989

90-
private SecurityUtil securityUtil;
91-
9290
/**
9391
* Amount of seconds a user is willing to tolerate for establishing the connection with database.
9492
* In our case, it means the first login request to get authorization token.
@@ -147,7 +145,6 @@ public SFSession() {
147145

148146
public SFSession(DefaultSFConnectionHandler sfConnectionHandler) {
149147
super(sfConnectionHandler);
150-
securityUtil = new SecurityUtil();
151148
}
152149

153150
/**
@@ -478,6 +475,12 @@ public void addSFSessionProperty(String propertyName, Object propertyValue) thro
478475
}
479476
break;
480477

478+
case JDBC_ARROW_TREAT_DECIMAL_AS_INT:
479+
if (propertyValue != null) {
480+
setJdbcArrowTreatDecimalAsInt(getBooleanValue(propertyValue));
481+
}
482+
break;
483+
481484
default:
482485
break;
483486
}
@@ -616,7 +619,6 @@ public synchronized void open() throws SFException, SnowflakeSQLException {
616619
TelemetryService.disable();
617620
}
618621

619-
securityUtil.addBouncyCastleProvider();
620622
// propagate OCSP mode to SFTrustManager. Note OCSP setting is global on JVM.
621623
HttpUtil.initHttpClient(httpClientSettingsKey, null);
622624
SFLoginOutput loginOutput =

src/main/java/net/snowflake/client/core/SFSessionProperty.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public enum SFSessionProperty {
8282

8383
DISABLE_GCS_DEFAULT_CREDENTIALS("disableGcsDefaultCredentials", false, Boolean.class),
8484

85-
ENABLE_BOUNCY_CASTLE("enableBouncyCastle", false, Boolean.class);
85+
JDBC_ARROW_TREAT_DECIMAL_AS_INT("JDBC_ARROW_TREAT_DECIMAL_AS_INT", false, Boolean.class);
8686

8787
// property key in string
8888
private String propertyKey;

src/main/java/net/snowflake/client/core/SecurityUtil.java

+9-7
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,22 @@
66
import net.snowflake.client.log.SFLogger;
77
import net.snowflake.client.log.SFLoggerFactory;
88

9+
@SnowflakeJdbcInternalApi
910
public class SecurityUtil {
1011

1112
private static final SFLogger LOGGER = SFLoggerFactory.getLogger(SecurityUtil.class);
1213

13-
/** provider name */
14-
private final String BOUNCY_CASTLE_PROVIDER = "BC";
15-
1614
/** provider name for FIPS */
17-
private final String BOUNCY_CASTLE_FIPS_PROVIDER = "BCFIPS";
15+
public static final String BOUNCY_CASTLE_FIPS_PROVIDER = "BCFIPS";
1816

17+
public static final String BOUNCY_CASTLE_PROVIDER = "BC";
1918
private static final String DEFAULT_SECURITY_PROVIDER_NAME =
2019
"org.bouncycastle.jce.provider.BouncyCastleProvider";
2120

22-
public void addBouncyCastleProvider() {
21+
public static final String ENABLE_BOUNCYCASTLE_PROVIDER_JVM =
22+
"net.snowflake.jdbc.enableBouncyCastle";
23+
24+
public static void addBouncyCastleProvider() {
2325
// Add Bouncy Castle to the list of security providers. This is required to
2426
// verify the signature on OCSP response and attached certificates.
2527
// It is also required to decrypt password protected private keys.
@@ -31,7 +33,7 @@ public void addBouncyCastleProvider() {
3133
}
3234
}
3335

34-
public Provider instantiateSecurityProvider() {
36+
private static Provider instantiateSecurityProvider() {
3537

3638
try {
3739
Class klass = Class.forName(DEFAULT_SECURITY_PROVIDER_NAME);
@@ -50,7 +52,7 @@ public Provider instantiateSecurityProvider() {
5052
+ "import BouncyCastleFipsProvider in the application.",
5153
DEFAULT_SECURITY_PROVIDER_NAME, ex.getMessage());
5254
LOGGER.error(errMsg, true);
53-
throw new RuntimeException(errMsg);
55+
throw new RuntimeException(errMsg, ex);
5456
}
5557
}
5658
}

src/main/java/net/snowflake/client/core/SessionUtilKeyPair.java

+49-30
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
import net.snowflake.client.log.SFLoggerFactory;
4040
import org.apache.commons.codec.binary.Base64;
4141
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
42+
import org.bouncycastle.openssl.PEMKeyPair;
4243
import org.bouncycastle.openssl.PEMParser;
4344
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
4445
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
@@ -67,21 +68,13 @@ class SessionUtilKeyPair {
6768

6869
private Provider SecurityProvider = null;
6970

70-
private SecretKeyFactory secretKeyFactory = null;
71-
7271
private static final String ISSUER_FMT = "%s.%s.%s";
7372

7473
private static final String SUBJECT_FMT = "%s.%s";
7574

7675
private static final int JWT_DEFAULT_AUTH_TIMEOUT = 10;
7776

78-
/** provider name */
79-
private static final String BOUNCY_CASTLE_PROVIDER = "BC";
80-
81-
/** provider name for FIPS */
82-
private static final String BOUNCY_CASTLE_FIPS_PROVIDER = "BCFIPS";
83-
84-
private boolean ENABLE_BOUNCYCASTLE_PROVIDER = true;
77+
private boolean isBouncyCastleProviderEnabled = false;
8578

8679
SessionUtilKeyPair(
8780
PrivateKey privateKey,
@@ -92,10 +85,14 @@ class SessionUtilKeyPair {
9285
throws SFException {
9386
this.userName = userName.toUpperCase();
9487
this.accountName = accountName.toUpperCase();
95-
88+
String enableBouncyCastleJvm =
89+
System.getProperty(SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM);
90+
if (enableBouncyCastleJvm != null) {
91+
isBouncyCastleProviderEnabled = enableBouncyCastleJvm.equalsIgnoreCase("true");
92+
}
9693
// check if in FIPS mode
9794
for (Provider p : Security.getProviders()) {
98-
if (BOUNCY_CASTLE_FIPS_PROVIDER.equals(p.getName())) {
95+
if (SecurityUtil.BOUNCY_CASTLE_FIPS_PROVIDER.equals(p.getName())) {
9996
this.isFipsMode = true;
10097
this.SecurityProvider = p;
10198
break;
@@ -151,11 +148,11 @@ private SecretKeyFactory getSecretKeyFactory(String algorithm) throws NoSuchAlgo
151148
private PrivateKey extractPrivateKeyFromFile(String privateKeyFile, String privateKeyFilePwd)
152149
throws SFException {
153150

154-
if (ENABLE_BOUNCYCASTLE_PROVIDER) {
151+
if (isBouncyCastleProviderEnabled) {
155152
try {
156153
return extractPrivateKeyWithBouncyCastle(privateKeyFile, privateKeyFilePwd);
157154
} catch (IOException | PKCSException | OperatorCreationException e) {
158-
logger.error("Could not extract private key using Bouncy Castle provider");
155+
logger.error("Could not extract private key using Bouncy Castle provider", e);
159156
throw new SFException(e, ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY, e.getCause());
160157
}
161158
} else {
@@ -168,7 +165,8 @@ private PrivateKey extractPrivateKeyFromFile(String privateKeyFile, String priva
168165
| NullPointerException
169166
| InvalidKeyException e) {
170167
logger.error(
171-
"Could not extract private key. Try setting " + ENABLE_BOUNCYCASTLE_PROVIDER + "=TRUE");
168+
"Could not extract private key. Try setting the JVM argument: " + "-D{}" + "=TRUE",
169+
SecurityUtil.ENABLE_BOUNCYCASTLE_PROVIDER_JVM);
172170
throw new SFException(
173171
e,
174172
ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY,
@@ -246,12 +244,20 @@ private PrivateKey extractPrivateKeyWithBouncyCastle(
246244
InputDecryptorProvider pkcs8Prov =
247245
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(privateKeyFilePwd.toCharArray());
248246
privateKeyInfo = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(pkcs8Prov);
247+
} else if (pemObject instanceof PEMKeyPair) {
248+
// PKCS#1 private key
249+
privateKeyInfo = ((PEMKeyPair) pemObject).getPrivateKeyInfo();
249250
} else if (pemObject instanceof PrivateKeyInfo) {
250251
// Handle the case where the private key is unencrypted.
251252
privateKeyInfo = (PrivateKeyInfo) pemObject;
252253
}
253254
pemParser.close();
254-
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER);
255+
JcaPEMKeyConverter converter =
256+
new JcaPEMKeyConverter()
257+
.setProvider(
258+
isFipsMode
259+
? SecurityUtil.BOUNCY_CASTLE_FIPS_PROVIDER
260+
: SecurityUtil.BOUNCY_CASTLE_PROVIDER);
255261
return converter.getPrivateKey(privateKeyInfo);
256262
}
257263

@@ -260,23 +266,36 @@ private PrivateKey extractPrivateKeyWithJdk(String privateKeyFile, String privat
260266
String privateKeyContent = new String(Files.readAllBytes(Paths.get(privateKeyFile)));
261267
if (Strings.isNullOrEmpty(privateKeyFilePwd)) {
262268
// unencrypted private key file
263-
PemReader pr = new PemReader(new StringReader(privateKeyContent));
264-
byte[] decoded = pr.readPemObject().getContent();
265-
pr.close();
266-
PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(decoded);
267-
KeyFactory keyFactory = getKeyFactoryInstance();
268-
return keyFactory.generatePrivate(encodedKeySpec);
269+
return generatePrivateKey(false, privateKeyContent, privateKeyFilePwd);
269270
} else {
270271
// encrypted private key file
271-
PemReader pr = new PemReader(new StringReader(privateKeyContent));
272-
byte[] decoded = pr.readPemObject().getContent();
273-
pr.close();
274-
EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(decoded);
275-
PBEKeySpec keySpec = new PBEKeySpec(privateKeyFilePwd.toCharArray());
276-
SecretKeyFactory pbeKeyFactory = this.getSecretKeyFactory(pkInfo.getAlgName());
277-
PKCS8EncodedKeySpec encodedKeySpec = pkInfo.getKeySpec(pbeKeyFactory.generateSecret(keySpec));
278-
KeyFactory keyFactory = getKeyFactoryInstance();
279-
return keyFactory.generatePrivate(encodedKeySpec);
272+
return generatePrivateKey(true, privateKeyContent, privateKeyFilePwd);
273+
}
274+
}
275+
276+
private PrivateKey generatePrivateKey(
277+
boolean isEncrypted, String privateKeyContent, String privateKeyFilePwd)
278+
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
279+
if (isEncrypted) {
280+
try (PemReader pr = new PemReader(new StringReader(privateKeyContent))) {
281+
byte[] decoded = pr.readPemObject().getContent();
282+
pr.close();
283+
EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(decoded);
284+
PBEKeySpec keySpec = new PBEKeySpec(privateKeyFilePwd.toCharArray());
285+
SecretKeyFactory pbeKeyFactory = this.getSecretKeyFactory(pkInfo.getAlgName());
286+
PKCS8EncodedKeySpec encodedKeySpec =
287+
pkInfo.getKeySpec(pbeKeyFactory.generateSecret(keySpec));
288+
KeyFactory keyFactory = getKeyFactoryInstance();
289+
return keyFactory.generatePrivate(encodedKeySpec);
290+
}
291+
} else {
292+
try (PemReader pr = new PemReader(new StringReader(privateKeyContent))) {
293+
byte[] decoded = pr.readPemObject().getContent();
294+
pr.close();
295+
PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(decoded);
296+
KeyFactory keyFactory = getKeyFactoryInstance();
297+
return keyFactory.generatePrivate(encodedKeySpec);
298+
}
280299
}
281300
}
282301
}

src/main/java/net/snowflake/client/core/arrow/AbstractArrowVectorConverter.java

+11
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ abstract class AbstractArrowVectorConverter implements ArrowVectorConverter {
3838

3939
protected TimeZone sessionTimeZone;
4040

41+
private boolean shouldTreatDecimalAsInt;
42+
4143
/** Field names of the struct vectors used by timestamp */
4244
public static final String FIELD_NAME_EPOCH = "epoch"; // seconds since epoch
4345

@@ -53,6 +55,11 @@ abstract class AbstractArrowVectorConverter implements ArrowVectorConverter {
5355
this.valueVector = valueVector;
5456
this.columnIndex = vectorIndex + 1;
5557
this.context = context;
58+
this.shouldTreatDecimalAsInt =
59+
context == null
60+
|| context.getSession() == null
61+
|| context.getSession().isJdbcArrowTreatDecimalAsInt()
62+
|| context.getSession().isJdbcTreatDecimalAsInt();
5663
}
5764

5865
@Override
@@ -146,6 +153,10 @@ public BigDecimal toBigDecimal(int index) throws SFException {
146153
ErrorCode.INVALID_VALUE_CONVERT, logicalTypeStr, SnowflakeUtil.BIG_DECIMAL_STR, "");
147154
}
148155

156+
boolean shouldTreatDecimalAsInt() {
157+
return shouldTreatDecimalAsInt;
158+
}
159+
149160
@Override
150161
public void setTreatNTZAsUTC(boolean isUTC) {
151162
this.treatNTZasUTC = isUTC;

src/main/java/net/snowflake/client/core/arrow/BigIntToFixedConverter.java

+3-2
Original file line numberDiff line numberDiff line change
@@ -137,9 +137,10 @@ public BigDecimal toBigDecimal(int index) {
137137
public Object toObject(int index) throws SFException {
138138
if (bigIntVector.isNull(index)) {
139139
return null;
140-
} else {
141-
return getLong(index);
140+
} else if (!shouldTreatDecimalAsInt()) {
141+
return BigDecimal.valueOf(getLong(index), sfScale);
142142
}
143+
return getLong(index);
143144
}
144145

145146
@Override

src/main/java/net/snowflake/client/core/arrow/IntToFixedConverter.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,12 @@ public BigDecimal toBigDecimal(int index) throws SFException {
105105

106106
@Override
107107
public Object toObject(int index) throws SFException {
108-
return isNull(index) ? null : (long) getInt(index);
108+
if (isNull(index)) {
109+
return null;
110+
} else if (!shouldTreatDecimalAsInt()) {
111+
return BigDecimal.valueOf((long) getInt(index), sfScale);
112+
}
113+
return (long) getInt(index);
109114
}
110115

111116
@Override

src/main/java/net/snowflake/client/core/arrow/SmallIntToFixedConverter.java

+2
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ public double toDouble(int index) throws SFException {
9999
public Object toObject(int index) throws SFException {
100100
if (isNull(index)) {
101101
return null;
102+
} else if (!shouldTreatDecimalAsInt()) {
103+
return BigDecimal.valueOf((long) getShort(index), sfScale);
102104
}
103105
return (long) getShort(index);
104106
}

src/main/java/net/snowflake/client/core/arrow/TinyIntToFixedConverter.java

+6-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,12 @@ public BigDecimal toBigDecimal(int index) throws SFException {
9090

9191
@Override
9292
public Object toObject(int index) throws SFException {
93-
return isNull(index) ? null : (long) toByte(index);
93+
if (isNull(index)) {
94+
return null;
95+
} else if (!shouldTreatDecimalAsInt()) {
96+
return BigDecimal.valueOf((long) getByte(index), sfScale);
97+
}
98+
return (long) toByte(index);
9499
}
95100

96101
@Override

0 commit comments

Comments
 (0)