Skip to content

Commit 727e6d7

Browse files
authored
feat: Introduce IPrivateKeyDecryptor to allow using custom cryptography provider (#1226)
Closes: SDK-3575
1 parent 827a9a0 commit 727e6d7

8 files changed

Lines changed: 221 additions & 88 deletions

README.md

Lines changed: 31 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -265,30 +265,44 @@ Javadocs are generated when `gradle javadoc` is run and can be found in
265265

266266
## FIPS 140-2 Compliance
267267

268-
The Box Java SDK uses libraries (`org.bouncycastle:bcpkix-jdk15on:1.57` and `org.bouncycastle:bcprov-jdk15on:1.57`) that are compatible with FIPS 140-2 validated cryptographic libraries (`org.bouncycastle:bc-fips:1.0.2.1`).
268+
To generate a Json Web Signature used for retrieving tokens in the JWT authentication method, the Box Java SDK decrypts an encrypted private key.
269+
For this purpose, Box Java SDK uses libraries (`org.bouncycastle:bcpkix-jdk15on:1.70` and `org.bouncycastle:bcprov-jdk15on:1.70`)
270+
that are NOT compatible with FIPS 140-2 validated cryptographic library (`org.bouncycastle:bc-fips`).
269271

270-
### Vulnerabilities in Bouncycastle libraries
271-
In Box Java SDK we are using:
272-
- `org.bouncycastle:bcpkix-jdk15on:1.57`
273-
- `org.bouncycastle:bcprov-jdk15on:1.57`
272+
There are two ways of ensuring that decryption operation is FIPS-compiant.
274273

275-
There are some moderate vulnerabilities reported against those versions:
274+
1. You can provide a custom implementation of the `IPrivateKeyDecryptor` interface,
275+
which performs the decryption operation using FIPS-certified library of your choice. The interface requires the
276+
implementation of just one method:
277+
```java
278+
PrivateKey decryptPrivateKey(String encryptedPrivateKey, String passphrase);
279+
```
280+
After implementing the custom decryptor, you need to set your custom decryptor class in the Box Config.
281+
Below is an example of setting up a `BoxDeveloperEditionAPIConnection` with a config file and the custom decryptor.
282+
```java
283+
Reader reader = new FileReader(JWT_CONFIG_PATH);
284+
BoxConfig boxConfig = BoxConfig.readFrom(reader);
285+
boxConfig.setPrivateKeyDecryptor(customDecryptor)
286+
BoxDeveloperEditionAPIConnection api = BoxDeveloperEditionAPIConnection.getAppEnterpriseConnection(boxConfig);
287+
```
288+
289+
2. Alternative method is to override the Bouncy Castle libraries to the v.1.57 version,
290+
which are compatible with the FIPS 140-2 validated cryptographic library (`org.bouncycastle:bc-fips`).
291+
292+
NOTE: This solution is not recommended as Bouncy Castle v.1.57 has some moderate vulnerabilities reported against those versions, including:
276293
- [CVE-2020-26939](https://github.com/advisories/GHSA-72m5-fvvv-55m6) - Observable Differences in Behavior to Error Inputs in Bouncy Castle
277294
- [CVE-2020-15522](https://github.com/advisories/GHSA-6xx3-rg99-gc3p) - Timing based private key exposure in Bouncy Castle
278295

279-
We cannot upgrade those libraries as they are working with [FIPS 140-2 certified](https://csrc.nist.gov/projects/cryptographic-module-validation-program/certificate/3514)
280-
cryptographic module. Some of our customers require certified cryptography module and our SDK must work with it.
281-
282-
If you want to use modern `bcpkix-jdk15on` and `bcprov-jdk15on` than you can exclude them while importing Java Box SDK and provide you own versions:
296+
Furthermore,using Bouncy Castle v.1.57 may lead to [Bouncycastle BadPaddingException for JWT auth](#bouncycastle-badPaddingException-for-jWT-auth).
283297

284298
Gradle example
285299
```groovy
286300
implementation('com.box:box-java-sdk:x.y.z') {
287301
exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
288302
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
289303
}
290-
runtimeOnly('org.bouncycastle:bcprov-jdk15on:1.70')
291-
runtimeOnly('org.bouncycastle:bcpkix-jdk15on:1.70')
304+
runtimeOnly('org.bouncycastle:bcprov-jdk15on:1.57')
305+
runtimeOnly('org.bouncycastle:bcpkix-jdk15on:1.57')
292306
```
293307

294308
Maven example:
@@ -313,13 +327,13 @@ Maven example:
313327
<dependency>
314328
<groupId>org.bouncycastle</groupId>
315329
<artifactId>bcprov-jdk15on</artifactId>
316-
<version>1.70</version>
330+
<version>1.57</version>
317331
<scope>runtime</scope>
318332
</dependency>
319333
<dependency>
320334
<groupId>org.bouncycastle</groupId>
321335
<artifactId>bcpkix-jdk15on</artifactId>
322-
<version>1.70</version>
336+
<version>1.57</version>
323337
<scope>runtime</scope>
324338
</dependency>
325339
</dependencies>
@@ -328,11 +342,11 @@ Maven example:
328342
### Bouncycastle BadPaddingException for JWT auth
329343

330344
As of October 2023, RSA keypairs generated on the Developer Console (refer to the [Generate a keypair guide](https://developer.box.com/guides/authentication/jwt/jwt-setup/#generate-a-keypair-recommended))
331-
are no longer compatible with Bouncy Castle version 1.57, which is utilized in the Box Java SDK.
345+
are no longer compatible with Bouncy Castle version 1.57, which was utilized in the Box Java SDK up to v4.6.1.
332346
Attempting to use a JWT configuration downloaded from the Developer Console results in a
333347
`javax.crypto.BadPaddingException: pad block corrupted` error.
334-
While we continue our efforts to address this issue, two possible workarounds are available:
335-
1. Override the Bouncy Castle library version with a newer one, following the steps described above.
348+
Prossible solutions:
349+
1. Upgrade to the v4.7.0 of Box Java SDK, which uses newer version of the Bouncy Castle library. (recommended)
336350
2. Manually generate a keypair using OpenSSL version 1.0.x and add the Public Key to the Developer Console.
337351
The [manually add keypair guide](https://developer.box.com/guides/authentication/jwt/jwt-setup/#manually-add-keypair) provides assistance in this process.
338352

build.gradle

Lines changed: 2 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,8 @@ configurations {
5151
dependencies {
5252
implementation "com.eclipsesource.minimal-json:minimal-json:0.9.5"
5353
implementation "org.bitbucket.b_c:jose4j:0.9.4"
54-
implementation("org.bouncycastle:bcprov-jdk15on") {
55-
version {
56-
strictly("1.57")
57-
}
58-
because "v1.57 is compatible with org.bouncycastle:bc-fips:1.0.2.1 which is needed for FIPS compliance purposes"
59-
}
60-
implementation("org.bouncycastle:bcpkix-jdk15on") {
61-
version {
62-
strictly("1.57")
63-
}
64-
because "v1.57 is compatible with org.bouncycastle:bc-fips:1.0.2.1 which is needed for FIPS compliance purposes"
65-
}
54+
implementation "org.bouncycastle:bcprov-jdk15on:1.70"
55+
implementation "org.bouncycastle:bcpkix-jdk15on:1.70"
6656
implementation "com.squareup.okhttp3:okhttp:4.10.0"
6757
testsCommonImplementation "junit:junit:4.13.2"
6858
testsCommonImplementation "org.hamcrest:hamcrest-library:2.2"
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.box.sdk;
2+
3+
import java.io.IOException;
4+
import java.io.StringReader;
5+
import java.security.PrivateKey;
6+
import java.security.Security;
7+
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
8+
import org.bouncycastle.jce.provider.BouncyCastleProvider;
9+
import org.bouncycastle.openssl.PEMDecryptorProvider;
10+
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
11+
import org.bouncycastle.openssl.PEMKeyPair;
12+
import org.bouncycastle.openssl.PEMParser;
13+
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
14+
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
15+
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
16+
import org.bouncycastle.operator.InputDecryptorProvider;
17+
import org.bouncycastle.operator.OperatorCreationException;
18+
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
19+
import org.bouncycastle.pkcs.PKCSException;
20+
21+
/**
22+
* The default implementation of `IPrivateKeyDecryptor`, which uses Bouncy Castle library to decrypt the private key.
23+
*/
24+
public class BCPrivateKeyDecryptor implements IPrivateKeyDecryptor {
25+
26+
/**
27+
* Decrypts private key with provided passphrase using Bouncy Castle library
28+
*
29+
* @param encryptedPrivateKey Encoded private key string.
30+
* @param passphrase Private key passphrase.
31+
* @return java.security.PrivateKey instance representing decrypted private key.
32+
*/
33+
@Override
34+
public PrivateKey decryptPrivateKey(String encryptedPrivateKey, String passphrase) {
35+
Security.addProvider(new BouncyCastleProvider());
36+
PrivateKey decryptedPrivateKey;
37+
try {
38+
PEMParser keyReader = new PEMParser(new StringReader(encryptedPrivateKey));
39+
Object keyPair = keyReader.readObject();
40+
keyReader.close();
41+
42+
if (keyPair instanceof PrivateKeyInfo) {
43+
PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair;
44+
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
45+
} else if (keyPair instanceof PEMEncryptedKeyPair) {
46+
JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder();
47+
PEMDecryptorProvider decryptionProvider = builder.build(passphrase.toCharArray());
48+
keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider);
49+
PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
50+
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
51+
} else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) {
52+
InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder()
53+
.setProvider("BC")
54+
.build(passphrase.toCharArray());
55+
PrivateKeyInfo keyInfo = ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov);
56+
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
57+
} else {
58+
PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
59+
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
60+
}
61+
} catch (IOException e) {
62+
throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e);
63+
} catch (OperatorCreationException e) {
64+
throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e);
65+
} catch (PKCSException e) {
66+
throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e);
67+
}
68+
return decryptedPrivateKey;
69+
}
70+
}

src/main/java/com/box/sdk/BoxConfig.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,4 +176,13 @@ public String getClientId() {
176176
public void setClientId(String clientId) {
177177
this.clientId = clientId;
178178
}
179+
180+
/**
181+
* Sets a custom decryptor used for decrypting the private key.
182+
*
183+
* @param privateKeyDecryptor privateKeyDecryptor the decryptor used for decrypting the private key.
184+
*/
185+
public void setPrivateKeyDecryptor(IPrivateKeyDecryptor privateKeyDecryptor) {
186+
this.jwtEncryptionPreferences.setPrivateKeyDecryptor(privateKeyDecryptor);
187+
}
179188
}

src/main/java/com/box/sdk/BoxDeveloperEditionAPIConnection.java

Lines changed: 3 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,29 +2,12 @@
22

33
import com.eclipsesource.json.Json;
44
import com.eclipsesource.json.JsonObject;
5-
import java.io.IOException;
6-
import java.io.StringReader;
75
import java.net.MalformedURLException;
86
import java.net.URL;
9-
import java.security.PrivateKey;
10-
import java.security.Security;
117
import java.text.ParseException;
128
import java.text.SimpleDateFormat;
139
import java.util.Date;
1410
import java.util.List;
15-
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
16-
import org.bouncycastle.jce.provider.BouncyCastleProvider;
17-
import org.bouncycastle.openssl.PEMDecryptorProvider;
18-
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
19-
import org.bouncycastle.openssl.PEMKeyPair;
20-
import org.bouncycastle.openssl.PEMParser;
21-
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
22-
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
23-
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
24-
import org.bouncycastle.operator.InputDecryptorProvider;
25-
import org.bouncycastle.operator.OperatorCreationException;
26-
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
27-
import org.bouncycastle.pkcs.PKCSException;
2811
import org.jose4j.jws.AlgorithmIdentifiers;
2912
import org.jose4j.jws.JsonWebSignature;
3013
import org.jose4j.jwt.JwtClaims;
@@ -43,10 +26,6 @@ public class BoxDeveloperEditionAPIConnection extends BoxAPIConnection {
4326
"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=%s&client_secret=%s&assertion=%s";
4427
private static final int DEFAULT_MAX_ENTRIES = 100;
4528

46-
static {
47-
Security.addProvider(new BouncyCastleProvider());
48-
}
49-
5029
private final String entityID;
5130
private final DeveloperEditionEntityType entityType;
5231
private final EncryptionAlgorithm encryptionAlgorithm;
@@ -55,6 +34,7 @@ public class BoxDeveloperEditionAPIConnection extends BoxAPIConnection {
5534
private final String privateKeyPassword;
5635
private BackoffCounter backoffCounter;
5736
private final IAccessTokenCache accessTokenCache;
37+
private final IPrivateKeyDecryptor privateKeyDecryptor;
5838

5939
/**
6040
* Constructs a new BoxDeveloperEditionAPIConnection leveraging an access token cache.
@@ -79,6 +59,7 @@ public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityT
7959
this.privateKey = encryptionPref.getPrivateKey();
8060
this.privateKeyPassword = encryptionPref.getPrivateKeyPassword();
8161
this.encryptionAlgorithm = encryptionPref.getEncryptionAlgorithm();
62+
this.privateKeyDecryptor = encryptionPref.getPrivateKeyDecryptor();
8263
this.accessTokenCache = accessTokenCache;
8364
this.backoffCounter = new BackoffCounter(new Time());
8465
}
@@ -500,7 +481,7 @@ private String constructJWTAssertion(NumericDate now) {
500481

501482
JsonWebSignature jws = new JsonWebSignature();
502483
jws.setPayload(claims.toJson());
503-
jws.setKey(this.decryptPrivateKey());
484+
jws.setKey(this.privateKeyDecryptor.decryptPrivateKey(this.privateKey, this.privateKeyPassword));
504485
jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier());
505486
jws.setHeader("typ", "JWT");
506487
if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) {
@@ -534,40 +515,4 @@ private String getAlgorithmIdentifier() {
534515

535516
return algorithmId;
536517
}
537-
538-
private PrivateKey decryptPrivateKey() {
539-
PrivateKey decryptedPrivateKey;
540-
try {
541-
PEMParser keyReader = new PEMParser(new StringReader(this.privateKey));
542-
Object keyPair = keyReader.readObject();
543-
keyReader.close();
544-
545-
if (keyPair instanceof PrivateKeyInfo) {
546-
PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair;
547-
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
548-
} else if (keyPair instanceof PEMEncryptedKeyPair) {
549-
JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder();
550-
PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray());
551-
keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider);
552-
PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
553-
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
554-
} else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) {
555-
InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider("BC")
556-
.build(this.privateKeyPassword.toCharArray());
557-
PrivateKeyInfo keyInfo = ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov);
558-
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
559-
} else {
560-
PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
561-
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
562-
}
563-
} catch (IOException e) {
564-
throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e);
565-
} catch (OperatorCreationException e) {
566-
throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e);
567-
} catch (PKCSException e) {
568-
throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e);
569-
}
570-
return decryptedPrivateKey;
571-
}
572-
573518
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.box.sdk;
2+
3+
import java.security.PrivateKey;
4+
5+
/**
6+
* Implement this interface to provide a custom private key decryptor.
7+
* If you require the decryption operation to be FIPS compliant,
8+
* ensure that your implementation exclusively utilizes FIPS certified libraries.
9+
*/
10+
public interface IPrivateKeyDecryptor {
11+
12+
/**
13+
* Decrypts private key with provided passphrase using Bouncy Castle library
14+
*
15+
* @param encryptedPrivateKey Encoded private key string.
16+
* @param passphrase Private key passphrase.
17+
* @return java.security.PrivateKey instance representing decrypted private key.
18+
*/
19+
PrivateKey decryptPrivateKey(String encryptedPrivateKey, String passphrase);
20+
}

src/main/java/com/box/sdk/JWTEncryptionPreferences.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public class JWTEncryptionPreferences {
99
private String privateKey;
1010
private String privateKeyPassword;
1111
private EncryptionAlgorithm encryptionAlgorithm;
12+
private IPrivateKeyDecryptor privateKeyDecryptor = new BCPrivateKeyDecryptor();
1213

1314
/**
1415
* Returns the ID for public key for validating the JWT signature.
@@ -81,4 +82,22 @@ public EncryptionAlgorithm getEncryptionAlgorithm() {
8182
public void setEncryptionAlgorithm(EncryptionAlgorithm encryptionAlgorithm) {
8283
this.encryptionAlgorithm = encryptionAlgorithm;
8384
}
85+
86+
/**
87+
* Gets a decryptor used for decrypting the private key.
88+
*
89+
* @return the decryptor used for decrypting the private key.
90+
*/
91+
public IPrivateKeyDecryptor getPrivateKeyDecryptor() {
92+
return privateKeyDecryptor;
93+
}
94+
95+
/**
96+
* Sets a custom decryptor used for decrypting the private key.
97+
*
98+
* @param privateKeyDecryptor the decryptor used for decrypting the private key.
99+
*/
100+
public void setPrivateKeyDecryptor(IPrivateKeyDecryptor privateKeyDecryptor) {
101+
this.privateKeyDecryptor = privateKeyDecryptor;
102+
}
84103
}

0 commit comments

Comments
 (0)