Skip to content

Commit 8caf4a3

Browse files
SNOW-896818 Limited support for encrypted private keys
1 parent 75c57f2 commit 8caf4a3

File tree

5 files changed

+152
-73
lines changed

5 files changed

+152
-73
lines changed

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

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

8888
private SFClientConfig sfClientConfig;
8989

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

146148
public SFSession(DefaultSFConnectionHandler sfConnectionHandler) {
147149
super(sfConnectionHandler);
150+
securityUtil = new SecurityUtil();
148151
}
149152

150153
/**
@@ -613,6 +616,7 @@ public synchronized void open() throws SFException, SnowflakeSQLException {
613616
TelemetryService.disable();
614617
}
615618

619+
securityUtil.addBouncyCastleProvider();
616620
// propagate OCSP mode to SFTrustManager. Note OCSP setting is global on JVM.
617621
HttpUtil.initHttpClient(httpClientSettingsKey, null);
618622
SFLoginOutput loginOutput =

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,9 @@ public enum SFSessionProperty {
8080

8181
ENABLE_PATTERN_SEARCH("enablePatternSearch", false, Boolean.class),
8282

83-
DISABLE_GCS_DEFAULT_CREDENTIALS("disableGcsDefaultCredentials", false, Boolean.class);
83+
DISABLE_GCS_DEFAULT_CREDENTIALS("disableGcsDefaultCredentials", false, Boolean.class),
84+
85+
ENABLE_BOUNCY_CASTLE("enableBouncyCastle", false, Boolean.class);
8486

8587
// property key in string
8688
private String propertyKey;

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

+1-41
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
import java.io.File;
2020
import java.io.IOException;
2121
import java.io.OutputStream;
22-
import java.lang.reflect.InvocationTargetException;
2322
import java.math.BigInteger;
2423
import java.net.URI;
2524
import java.net.URISyntaxException;
@@ -29,8 +28,6 @@
2928
import java.security.KeyStoreException;
3029
import java.security.MessageDigest;
3130
import java.security.NoSuchAlgorithmException;
32-
import java.security.Provider;
33-
import java.security.Security;
3431
import java.security.Signature;
3532
import java.security.SignatureException;
3633
import java.security.cert.CertificateEncodingException;
@@ -155,9 +152,6 @@ public class SFTrustManager extends X509ExtendedTrustManager {
155152
private static final ASN1ObjectIdentifier SHA512RSA =
156153
new ASN1ObjectIdentifier("1.2.840.113549.1.1.13").intern();
157154

158-
private static final String DEFAULT_SECURITY_PROVIDER_NAME =
159-
"org.bouncycastle.jce.provider.BouncyCastleProvider";
160-
161155
private static final String ALGORITHM_SHA1_NAME = "SHA-1";
162156
/** Object mapper for JSON encoding and decoding */
163157
private static final ObjectMapper OBJECT_MAPPER = ObjectMapperFactory.getObjectMapper();
@@ -175,10 +169,7 @@ public class SFTrustManager extends X509ExtendedTrustManager {
175169
private static final int DEFAULT_OCSP_RESPONDER_CONNECTION_TIMEOUT = 10000;
176170
/** Default OCSP Cache server host name */
177171
private static final String DEFAULT_OCSP_CACHE_HOST = "http://ocsp.snowflakecomputing.com";
178-
/** provider name */
179-
private static final String BOUNCY_CASTLE_PROVIDER = "BC";
180-
/** provider name for FIPS */
181-
private static final String BOUNCY_CASTLE_FIPS_PROVIDER = "BCFIPS";
172+
182173
/** OCSP response file cache directory */
183174
private static final FileCacheManager fileCacheManager;
184175
/** Tolerable validity date range ratio. */
@@ -253,37 +244,6 @@ public class SFTrustManager extends X509ExtendedTrustManager {
253244
OCSP_RESPONSE_CODE_TO_STRING.put(OCSPResp.UNAUTHORIZED, "unauthorized");
254245
}
255246

256-
static {
257-
// Add Bouncy Castle to the security provider. This is required to
258-
// verify the signature on OCSP response and attached certificates.
259-
if (Security.getProvider(BOUNCY_CASTLE_PROVIDER) == null
260-
&& Security.getProvider(BOUNCY_CASTLE_FIPS_PROVIDER) == null) {
261-
Security.addProvider(instantiateSecurityProvider());
262-
}
263-
}
264-
265-
private static Provider instantiateSecurityProvider() {
266-
try {
267-
Class klass = Class.forName(DEFAULT_SECURITY_PROVIDER_NAME);
268-
return (Provider) klass.getDeclaredConstructor().newInstance();
269-
} catch (ExceptionInInitializerError
270-
| ClassNotFoundException
271-
| NoSuchMethodException
272-
| InstantiationException
273-
| IllegalAccessException
274-
| IllegalArgumentException
275-
| InvocationTargetException
276-
| SecurityException ex) {
277-
String errMsg =
278-
String.format(
279-
"Failed to load %s, err=%s. If you use Snowflake JDBC for FIPS jar, "
280-
+ "import BouncyCastleFipsProvider in the application.",
281-
DEFAULT_SECURITY_PROVIDER_NAME, ex.getMessage());
282-
LOGGER.error(errMsg, true);
283-
throw new RuntimeException(errMsg);
284-
}
285-
}
286-
287247
static {
288248
DATE_FORMAT_UTC.setTimeZone(TimeZone.getTimeZone("UTC"));
289249
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package net.snowflake.client.core;
2+
3+
import java.lang.reflect.InvocationTargetException;
4+
import java.security.Provider;
5+
import java.security.Security;
6+
import net.snowflake.client.log.SFLogger;
7+
import net.snowflake.client.log.SFLoggerFactory;
8+
9+
public class SecurityUtil {
10+
11+
private static final SFLogger LOGGER = SFLoggerFactory.getLogger(SecurityUtil.class);
12+
13+
/** provider name */
14+
private final String BOUNCY_CASTLE_PROVIDER = "BC";
15+
16+
/** provider name for FIPS */
17+
private final String BOUNCY_CASTLE_FIPS_PROVIDER = "BCFIPS";
18+
19+
private static final String DEFAULT_SECURITY_PROVIDER_NAME =
20+
"org.bouncycastle.jce.provider.BouncyCastleProvider";
21+
22+
public void addBouncyCastleProvider() {
23+
// Add Bouncy Castle to the list of security providers. This is required to
24+
// verify the signature on OCSP response and attached certificates.
25+
// It is also required to decrypt password protected private keys.
26+
// Check to see if the BouncyCastleFipsProvider has already been added.
27+
// If so, then we don't want to add the provider BouncyCastleProvider.
28+
// The addProvider() method won't add the provider if it already exists.
29+
if (Security.getProvider(BOUNCY_CASTLE_FIPS_PROVIDER) == null) {
30+
Security.addProvider(instantiateSecurityProvider());
31+
}
32+
}
33+
34+
public Provider instantiateSecurityProvider() {
35+
36+
try {
37+
Class klass = Class.forName(DEFAULT_SECURITY_PROVIDER_NAME);
38+
return (Provider) klass.getDeclaredConstructor().newInstance();
39+
} catch (ExceptionInInitializerError
40+
| ClassNotFoundException
41+
| NoSuchMethodException
42+
| InstantiationException
43+
| IllegalAccessException
44+
| IllegalArgumentException
45+
| InvocationTargetException
46+
| SecurityException ex) {
47+
String errMsg =
48+
String.format(
49+
"Failed to load %s, err=%s. If you use Snowflake JDBC for FIPS jar, "
50+
+ "import BouncyCastleFipsProvider in the application.",
51+
DEFAULT_SECURITY_PROVIDER_NAME, ex.getMessage());
52+
LOGGER.error(errMsg, true);
53+
throw new RuntimeException(errMsg);
54+
}
55+
}
56+
}

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

+88-31
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import com.nimbusds.jose.crypto.RSASSASigner;
1414
import com.nimbusds.jwt.JWTClaimsSet;
1515
import com.nimbusds.jwt.SignedJWT;
16+
import java.io.FileReader;
1617
import java.io.IOException;
1718
import java.io.StringReader;
1819
import java.nio.file.Files;
@@ -37,6 +38,14 @@
3738
import net.snowflake.client.log.SFLogger;
3839
import net.snowflake.client.log.SFLoggerFactory;
3940
import org.apache.commons.codec.binary.Base64;
41+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
42+
import org.bouncycastle.openssl.PEMParser;
43+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
44+
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
45+
import org.bouncycastle.operator.InputDecryptorProvider;
46+
import org.bouncycastle.operator.OperatorCreationException;
47+
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
48+
import org.bouncycastle.pkcs.PKCSException;
4049
import org.bouncycastle.util.io.pem.PemReader;
4150

4251
/** Class used to compute jwt token for key pair authentication Created by hyu on 1/16/18. */
@@ -66,6 +75,14 @@ class SessionUtilKeyPair {
6675

6776
private static final int JWT_DEFAULT_AUTH_TIMEOUT = 10;
6877

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;
85+
6986
SessionUtilKeyPair(
7087
PrivateKey privateKey,
7188
String privateKeyFile,
@@ -78,7 +95,7 @@ class SessionUtilKeyPair {
7895

7996
// check if in FIPS mode
8097
for (Provider p : Security.getProviders()) {
81-
if ("BCFIPS".equals(p.getName())) {
98+
if (BOUNCY_CASTLE_FIPS_PROVIDER.equals(p.getName())) {
8299
this.isFipsMode = true;
83100
this.SecurityProvider = p;
84101
break;
@@ -133,37 +150,30 @@ private SecretKeyFactory getSecretKeyFactory(String algorithm) throws NoSuchAlgo
133150

134151
private PrivateKey extractPrivateKeyFromFile(String privateKeyFile, String privateKeyFilePwd)
135152
throws SFException {
136-
try {
137-
String privateKeyContent = new String(Files.readAllBytes(Paths.get(privateKeyFile)));
138-
if (Strings.isNullOrEmpty(privateKeyFilePwd)) {
139-
// unencrypted private key file
140-
PemReader pr = new PemReader(new StringReader(privateKeyContent));
141-
byte[] decoded = pr.readPemObject().getContent();
142-
pr.close();
143-
PKCS8EncodedKeySpec encodedKeySpec = new PKCS8EncodedKeySpec(decoded);
144-
KeyFactory keyFactory = getKeyFactoryInstance();
145-
return keyFactory.generatePrivate(encodedKeySpec);
146-
} else {
147-
// encrypted private key file
148-
PemReader pr = new PemReader(new StringReader(privateKeyContent));
149-
byte[] decoded = pr.readPemObject().getContent();
150-
pr.close();
151-
EncryptedPrivateKeyInfo pkInfo = new EncryptedPrivateKeyInfo(decoded);
152-
PBEKeySpec keySpec = new PBEKeySpec(privateKeyFilePwd.toCharArray());
153-
SecretKeyFactory pbeKeyFactory = this.getSecretKeyFactory(pkInfo.getAlgName());
154-
PKCS8EncodedKeySpec encodedKeySpec =
155-
pkInfo.getKeySpec(pbeKeyFactory.generateSecret(keySpec));
156-
KeyFactory keyFactory = getKeyFactoryInstance();
157-
return keyFactory.generatePrivate(encodedKeySpec);
153+
154+
if (ENABLE_BOUNCYCASTLE_PROVIDER) {
155+
try {
156+
return extractPrivateKeyWithBouncyCastle(privateKeyFile, privateKeyFilePwd);
157+
} catch (IOException | PKCSException | OperatorCreationException e) {
158+
logger.error("Could not extract private key using Bouncy Castle provider");
159+
throw new SFException(e, ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY, e.getCause());
160+
}
161+
} else {
162+
try {
163+
return extractPrivateKeyWithJdk(privateKeyFile, privateKeyFilePwd);
164+
} catch (NoSuchAlgorithmException
165+
| InvalidKeySpecException
166+
| IOException
167+
| IllegalArgumentException
168+
| NullPointerException
169+
| InvalidKeyException e) {
170+
logger.error(
171+
"Could not extract private key. Try setting " + ENABLE_BOUNCYCASTLE_PROVIDER + "=TRUE");
172+
throw new SFException(
173+
e,
174+
ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY,
175+
privateKeyFile + ": " + e.getMessage());
158176
}
159-
} catch (NoSuchAlgorithmException
160-
| InvalidKeySpecException
161-
| IOException
162-
| IllegalArgumentException
163-
| NullPointerException
164-
| InvalidKeyException e) {
165-
throw new SFException(
166-
e, ErrorCode.INVALID_OR_UNSUPPORTED_PRIVATE_KEY, privateKeyFile + ": " + e.getMessage());
167177
}
168178
}
169179

@@ -222,4 +232,51 @@ public static int getTimeout() {
222232
}
223233
return jwtAuthTimeout;
224234
}
235+
236+
private PrivateKey extractPrivateKeyWithBouncyCastle(
237+
String privateKeyFile, String privateKeyFilePwd)
238+
throws IOException, PKCSException, OperatorCreationException {
239+
PrivateKeyInfo privateKeyInfo = null;
240+
PEMParser pemParser = new PEMParser(new FileReader(Paths.get(privateKeyFile).toFile()));
241+
Object pemObject = pemParser.readObject();
242+
if (pemObject instanceof PKCS8EncryptedPrivateKeyInfo) {
243+
// Handle the case where the private key is encrypted.
244+
PKCS8EncryptedPrivateKeyInfo encryptedPrivateKeyInfo =
245+
(PKCS8EncryptedPrivateKeyInfo) pemObject;
246+
InputDecryptorProvider pkcs8Prov =
247+
new JceOpenSSLPKCS8DecryptorProviderBuilder().build(privateKeyFilePwd.toCharArray());
248+
privateKeyInfo = encryptedPrivateKeyInfo.decryptPrivateKeyInfo(pkcs8Prov);
249+
} else if (pemObject instanceof PrivateKeyInfo) {
250+
// Handle the case where the private key is unencrypted.
251+
privateKeyInfo = (PrivateKeyInfo) pemObject;
252+
}
253+
pemParser.close();
254+
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BOUNCY_CASTLE_PROVIDER);
255+
return converter.getPrivateKey(privateKeyInfo);
256+
}
257+
258+
private PrivateKey extractPrivateKeyWithJdk(String privateKeyFile, String privateKeyFilePwd)
259+
throws IOException, NoSuchAlgorithmException, InvalidKeySpecException, InvalidKeyException {
260+
String privateKeyContent = new String(Files.readAllBytes(Paths.get(privateKeyFile)));
261+
if (Strings.isNullOrEmpty(privateKeyFilePwd)) {
262+
// 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+
} else {
270+
// 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);
280+
}
281+
}
225282
}

0 commit comments

Comments
 (0)