Skip to content

Commit

Permalink
feat: Introduce IPrivateKeyDecryptor to allow using custom cryptogr…
Browse files Browse the repository at this point in the history
…aphy provider (#1226)

Closes: SDK-3575
  • Loading branch information
lukaszsocha2 authored Jan 16, 2024
1 parent 827a9a0 commit 727e6d7
Show file tree
Hide file tree
Showing 8 changed files with 221 additions and 88 deletions.
48 changes: 31 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,30 +265,44 @@ Javadocs are generated when `gradle javadoc` is run and can be found in

## FIPS 140-2 Compliance

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`).
To generate a Json Web Signature used for retrieving tokens in the JWT authentication method, the Box Java SDK decrypts an encrypted private key.
For this purpose, Box Java SDK uses libraries (`org.bouncycastle:bcpkix-jdk15on:1.70` and `org.bouncycastle:bcprov-jdk15on:1.70`)
that are NOT compatible with FIPS 140-2 validated cryptographic library (`org.bouncycastle:bc-fips`).

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

There are some moderate vulnerabilities reported against those versions:
1. You can provide a custom implementation of the `IPrivateKeyDecryptor` interface,
which performs the decryption operation using FIPS-certified library of your choice. The interface requires the
implementation of just one method:
```java
PrivateKey decryptPrivateKey(String encryptedPrivateKey, String passphrase);
```
After implementing the custom decryptor, you need to set your custom decryptor class in the Box Config.
Below is an example of setting up a `BoxDeveloperEditionAPIConnection` with a config file and the custom decryptor.
```java
Reader reader = new FileReader(JWT_CONFIG_PATH);
BoxConfig boxConfig = BoxConfig.readFrom(reader);
boxConfig.setPrivateKeyDecryptor(customDecryptor)
BoxDeveloperEditionAPIConnection api = BoxDeveloperEditionAPIConnection.getAppEnterpriseConnection(boxConfig);
```

2. Alternative method is to override the Bouncy Castle libraries to the v.1.57 version,
which are compatible with the FIPS 140-2 validated cryptographic library (`org.bouncycastle:bc-fips`).

NOTE: This solution is not recommended as Bouncy Castle v.1.57 has some moderate vulnerabilities reported against those versions, including:
- [CVE-2020-26939](https://github.com/advisories/GHSA-72m5-fvvv-55m6) - Observable Differences in Behavior to Error Inputs in Bouncy Castle
- [CVE-2020-15522](https://github.com/advisories/GHSA-6xx3-rg99-gc3p) - Timing based private key exposure in Bouncy Castle

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)
cryptographic module. Some of our customers require certified cryptography module and our SDK must work with it.

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:
Furthermore,using Bouncy Castle v.1.57 may lead to [Bouncycastle BadPaddingException for JWT auth](#bouncycastle-badPaddingException-for-jWT-auth).

Gradle example
```groovy
implementation('com.box:box-java-sdk:x.y.z') {
exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on'
exclude group: 'org.bouncycastle', module: 'bcpkix-jdk15on'
}
runtimeOnly('org.bouncycastle:bcprov-jdk15on:1.70')
runtimeOnly('org.bouncycastle:bcpkix-jdk15on:1.70')
runtimeOnly('org.bouncycastle:bcprov-jdk15on:1.57')
runtimeOnly('org.bouncycastle:bcpkix-jdk15on:1.57')
```

Maven example:
Expand All @@ -313,13 +327,13 @@ Maven example:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.70</version>
<version>1.57</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
<version>1.57</version>
<scope>runtime</scope>
</dependency>
</dependencies>
Expand All @@ -328,11 +342,11 @@ Maven example:
### Bouncycastle BadPaddingException for JWT auth

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))
are no longer compatible with Bouncy Castle version 1.57, which is utilized in the Box Java SDK.
are no longer compatible with Bouncy Castle version 1.57, which was utilized in the Box Java SDK up to v4.6.1.
Attempting to use a JWT configuration downloaded from the Developer Console results in a
`javax.crypto.BadPaddingException: pad block corrupted` error.
While we continue our efforts to address this issue, two possible workarounds are available:
1. Override the Bouncy Castle library version with a newer one, following the steps described above.
Prossible solutions:
1. Upgrade to the v4.7.0 of Box Java SDK, which uses newer version of the Bouncy Castle library. (recommended)
2. Manually generate a keypair using OpenSSL version 1.0.x and add the Public Key to the Developer Console.
The [manually add keypair guide](https://developer.box.com/guides/authentication/jwt/jwt-setup/#manually-add-keypair) provides assistance in this process.

Expand Down
14 changes: 2 additions & 12 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -51,18 +51,8 @@ configurations {
dependencies {
implementation "com.eclipsesource.minimal-json:minimal-json:0.9.5"
implementation "org.bitbucket.b_c:jose4j:0.9.4"
implementation("org.bouncycastle:bcprov-jdk15on") {
version {
strictly("1.57")
}
because "v1.57 is compatible with org.bouncycastle:bc-fips:1.0.2.1 which is needed for FIPS compliance purposes"
}
implementation("org.bouncycastle:bcpkix-jdk15on") {
version {
strictly("1.57")
}
because "v1.57 is compatible with org.bouncycastle:bc-fips:1.0.2.1 which is needed for FIPS compliance purposes"
}
implementation "org.bouncycastle:bcprov-jdk15on:1.70"
implementation "org.bouncycastle:bcpkix-jdk15on:1.70"
implementation "com.squareup.okhttp3:okhttp:4.10.0"
testsCommonImplementation "junit:junit:4.13.2"
testsCommonImplementation "org.hamcrest:hamcrest-library:2.2"
Expand Down
70 changes: 70 additions & 0 deletions src/main/java/com/box/sdk/BCPrivateKeyDecryptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.box.sdk;

import java.io.IOException;
import java.io.StringReader;
import java.security.PrivateKey;
import java.security.Security;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;

/**
* The default implementation of `IPrivateKeyDecryptor`, which uses Bouncy Castle library to decrypt the private key.
*/
public class BCPrivateKeyDecryptor implements IPrivateKeyDecryptor {

/**
* Decrypts private key with provided passphrase using Bouncy Castle library
*
* @param encryptedPrivateKey Encoded private key string.
* @param passphrase Private key passphrase.
* @return java.security.PrivateKey instance representing decrypted private key.
*/
@Override
public PrivateKey decryptPrivateKey(String encryptedPrivateKey, String passphrase) {
Security.addProvider(new BouncyCastleProvider());
PrivateKey decryptedPrivateKey;
try {
PEMParser keyReader = new PEMParser(new StringReader(encryptedPrivateKey));
Object keyPair = keyReader.readObject();
keyReader.close();

if (keyPair instanceof PrivateKeyInfo) {
PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair;
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
} else if (keyPair instanceof PEMEncryptedKeyPair) {
JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder();
PEMDecryptorProvider decryptionProvider = builder.build(passphrase.toCharArray());
keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider);
PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
} else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) {
InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder()
.setProvider("BC")
.build(passphrase.toCharArray());
PrivateKeyInfo keyInfo = ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov);
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
} else {
PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
}
} catch (IOException e) {
throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e);
} catch (OperatorCreationException e) {
throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e);
} catch (PKCSException e) {
throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e);
}
return decryptedPrivateKey;
}
}
9 changes: 9 additions & 0 deletions src/main/java/com/box/sdk/BoxConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -176,4 +176,13 @@ public String getClientId() {
public void setClientId(String clientId) {
this.clientId = clientId;
}

/**
* Sets a custom decryptor used for decrypting the private key.
*
* @param privateKeyDecryptor privateKeyDecryptor the decryptor used for decrypting the private key.
*/
public void setPrivateKeyDecryptor(IPrivateKeyDecryptor privateKeyDecryptor) {
this.jwtEncryptionPreferences.setPrivateKeyDecryptor(privateKeyDecryptor);
}
}
61 changes: 3 additions & 58 deletions src/main/java/com/box/sdk/BoxDeveloperEditionAPIConnection.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,12 @@

import com.eclipsesource.json.Json;
import com.eclipsesource.json.JsonObject;
import java.io.IOException;
import java.io.StringReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.PrivateKey;
import java.security.Security;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMDecryptorProvider;
import org.bouncycastle.openssl.PEMEncryptedKeyPair;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
import org.bouncycastle.operator.InputDecryptorProvider;
import org.bouncycastle.operator.OperatorCreationException;
import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
import org.bouncycastle.pkcs.PKCSException;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
Expand All @@ -43,10 +26,6 @@ public class BoxDeveloperEditionAPIConnection extends BoxAPIConnection {
"grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=%s&client_secret=%s&assertion=%s";
private static final int DEFAULT_MAX_ENTRIES = 100;

static {
Security.addProvider(new BouncyCastleProvider());
}

private final String entityID;
private final DeveloperEditionEntityType entityType;
private final EncryptionAlgorithm encryptionAlgorithm;
Expand All @@ -55,6 +34,7 @@ public class BoxDeveloperEditionAPIConnection extends BoxAPIConnection {
private final String privateKeyPassword;
private BackoffCounter backoffCounter;
private final IAccessTokenCache accessTokenCache;
private final IPrivateKeyDecryptor privateKeyDecryptor;

/**
* Constructs a new BoxDeveloperEditionAPIConnection leveraging an access token cache.
Expand All @@ -79,6 +59,7 @@ public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityT
this.privateKey = encryptionPref.getPrivateKey();
this.privateKeyPassword = encryptionPref.getPrivateKeyPassword();
this.encryptionAlgorithm = encryptionPref.getEncryptionAlgorithm();
this.privateKeyDecryptor = encryptionPref.getPrivateKeyDecryptor();
this.accessTokenCache = accessTokenCache;
this.backoffCounter = new BackoffCounter(new Time());
}
Expand Down Expand Up @@ -500,7 +481,7 @@ private String constructJWTAssertion(NumericDate now) {

JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setKey(this.decryptPrivateKey());
jws.setKey(this.privateKeyDecryptor.decryptPrivateKey(this.privateKey, this.privateKeyPassword));
jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier());
jws.setHeader("typ", "JWT");
if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) {
Expand Down Expand Up @@ -534,40 +515,4 @@ private String getAlgorithmIdentifier() {

return algorithmId;
}

private PrivateKey decryptPrivateKey() {
PrivateKey decryptedPrivateKey;
try {
PEMParser keyReader = new PEMParser(new StringReader(this.privateKey));
Object keyPair = keyReader.readObject();
keyReader.close();

if (keyPair instanceof PrivateKeyInfo) {
PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair;
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
} else if (keyPair instanceof PEMEncryptedKeyPair) {
JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder();
PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray());
keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider);
PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
} else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) {
InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider("BC")
.build(this.privateKeyPassword.toCharArray());
PrivateKeyInfo keyInfo = ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov);
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
} else {
PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
}
} catch (IOException e) {
throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e);
} catch (OperatorCreationException e) {
throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e);
} catch (PKCSException e) {
throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e);
}
return decryptedPrivateKey;
}

}
20 changes: 20 additions & 0 deletions src/main/java/com/box/sdk/IPrivateKeyDecryptor.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.box.sdk;

import java.security.PrivateKey;

/**
* Implement this interface to provide a custom private key decryptor.
* If you require the decryption operation to be FIPS compliant,
* ensure that your implementation exclusively utilizes FIPS certified libraries.
*/
public interface IPrivateKeyDecryptor {

/**
* Decrypts private key with provided passphrase using Bouncy Castle library
*
* @param encryptedPrivateKey Encoded private key string.
* @param passphrase Private key passphrase.
* @return java.security.PrivateKey instance representing decrypted private key.
*/
PrivateKey decryptPrivateKey(String encryptedPrivateKey, String passphrase);
}
19 changes: 19 additions & 0 deletions src/main/java/com/box/sdk/JWTEncryptionPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class JWTEncryptionPreferences {
private String privateKey;
private String privateKeyPassword;
private EncryptionAlgorithm encryptionAlgorithm;
private IPrivateKeyDecryptor privateKeyDecryptor = new BCPrivateKeyDecryptor();

/**
* Returns the ID for public key for validating the JWT signature.
Expand Down Expand Up @@ -81,4 +82,22 @@ public EncryptionAlgorithm getEncryptionAlgorithm() {
public void setEncryptionAlgorithm(EncryptionAlgorithm encryptionAlgorithm) {
this.encryptionAlgorithm = encryptionAlgorithm;
}

/**
* Gets a decryptor used for decrypting the private key.
*
* @return the decryptor used for decrypting the private key.
*/
public IPrivateKeyDecryptor getPrivateKeyDecryptor() {
return privateKeyDecryptor;
}

/**
* Sets a custom decryptor used for decrypting the private key.
*
* @param privateKeyDecryptor the decryptor used for decrypting the private key.
*/
public void setPrivateKeyDecryptor(IPrivateKeyDecryptor privateKeyDecryptor) {
this.privateKeyDecryptor = privateKeyDecryptor;
}
}
Loading

0 comments on commit 727e6d7

Please sign in to comment.