From 727e6d71ee375a48b4241a26a093becfe0965898 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C5=81ukasz=20Socha?=
<31014760+lukaszsocha2@users.noreply.github.com>
Date: Tue, 16 Jan 2024 15:28:44 +0100
Subject: [PATCH] feat: Introduce `IPrivateKeyDecryptor` to allow using custom
cryptography provider (#1226)
Closes: SDK-3575
---
README.md | 48 ++++++++-----
build.gradle | 14 +---
.../com/box/sdk/BCPrivateKeyDecryptor.java | 70 +++++++++++++++++++
src/main/java/com/box/sdk/BoxConfig.java | 9 +++
.../sdk/BoxDeveloperEditionAPIConnection.java | 61 +---------------
.../com/box/sdk/IPrivateKeyDecryptor.java | 20 ++++++
.../com/box/sdk/JWTEncryptionPreferences.java | 19 +++++
.../BoxDeveloperEditionAPIConnectionTest.java | 68 +++++++++++++++++-
8 files changed, 221 insertions(+), 88 deletions(-)
create mode 100644 src/main/java/com/box/sdk/BCPrivateKeyDecryptor.java
create mode 100644 src/main/java/com/box/sdk/IPrivateKeyDecryptor.java
diff --git a/README.md b/README.md
index 790e41a34..973600537 100644
--- a/README.md
+++ b/README.md
@@ -265,21 +265,35 @@ 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
@@ -287,8 +301,8 @@ 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:
@@ -313,13 +327,13 @@ Maven example:
org.bouncycastle
bcprov-jdk15on
- 1.70
+ 1.57
runtime
org.bouncycastle
bcpkix-jdk15on
- 1.70
+ 1.57
runtime
@@ -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.
diff --git a/build.gradle b/build.gradle
index c4279b351..07c2d2a19 100644
--- a/build.gradle
+++ b/build.gradle
@@ -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"
diff --git a/src/main/java/com/box/sdk/BCPrivateKeyDecryptor.java b/src/main/java/com/box/sdk/BCPrivateKeyDecryptor.java
new file mode 100644
index 000000000..536b8c434
--- /dev/null
+++ b/src/main/java/com/box/sdk/BCPrivateKeyDecryptor.java
@@ -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;
+ }
+}
diff --git a/src/main/java/com/box/sdk/BoxConfig.java b/src/main/java/com/box/sdk/BoxConfig.java
index 04f3da00e..e90e88f4e 100644
--- a/src/main/java/com/box/sdk/BoxConfig.java
+++ b/src/main/java/com/box/sdk/BoxConfig.java
@@ -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);
+ }
}
diff --git a/src/main/java/com/box/sdk/BoxDeveloperEditionAPIConnection.java b/src/main/java/com/box/sdk/BoxDeveloperEditionAPIConnection.java
index cb781558f..485638067 100644
--- a/src/main/java/com/box/sdk/BoxDeveloperEditionAPIConnection.java
+++ b/src/main/java/com/box/sdk/BoxDeveloperEditionAPIConnection.java
@@ -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;
@@ -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;
@@ -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.
@@ -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());
}
@@ -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()) {
@@ -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;
- }
-
}
diff --git a/src/main/java/com/box/sdk/IPrivateKeyDecryptor.java b/src/main/java/com/box/sdk/IPrivateKeyDecryptor.java
new file mode 100644
index 000000000..a8ecbab83
--- /dev/null
+++ b/src/main/java/com/box/sdk/IPrivateKeyDecryptor.java
@@ -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);
+}
diff --git a/src/main/java/com/box/sdk/JWTEncryptionPreferences.java b/src/main/java/com/box/sdk/JWTEncryptionPreferences.java
index e3350105f..b8bd774fd 100644
--- a/src/main/java/com/box/sdk/JWTEncryptionPreferences.java
+++ b/src/main/java/com/box/sdk/JWTEncryptionPreferences.java
@@ -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.
@@ -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;
+ }
}
diff --git a/src/test/java/com/box/sdk/BoxDeveloperEditionAPIConnectionTest.java b/src/test/java/com/box/sdk/BoxDeveloperEditionAPIConnectionTest.java
index 86a0a90cd..b95516586 100644
--- a/src/test/java/com/box/sdk/BoxDeveloperEditionAPIConnectionTest.java
+++ b/src/test/java/com/box/sdk/BoxDeveloperEditionAPIConnectionTest.java
@@ -12,6 +12,7 @@
import static com.github.tomakehurst.wiremock.stubbing.Scenario.STARTED;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.*;
import com.github.tomakehurst.wiremock.extension.Parameters;
import com.github.tomakehurst.wiremock.http.Request;
@@ -26,6 +27,12 @@
import org.junit.Assert;
import org.junit.Rule;
import org.junit.Test;
+import org.mockito.Mockito;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
public class BoxDeveloperEditionAPIConnectionTest {
@@ -159,6 +166,59 @@ public void retriesWithWhenJtiClaimIsDuplicated() {
assertThat(api.getAccessToken(), is(accessToken));
}
+ @Test
+ public void usesCustomDecryptorClassImplementation() throws NoSuchAlgorithmException, InvalidKeySpecException {
+ final String tokenPath = "/oauth2/token";
+ final String accessToken = "mNr1FrCvOeWiGnwLL0OcTL0Lux5jbyBa";
+ // This is freshly-generated private key, which is not used for a real Box account.
+ // It is safe to use in this unit test.
+ String decryptedPrivateKey = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC5fehGf7yDfLTDewsBcPyJSIIH1IB"
+ + "z3a7etHWCLEkUria6fVMltD/SMrqpOYL5ayFQEP3pzP0pskBD/uGFXCDfuPJ4SToHgaJYHIyw56YZcwqO6+T/3pWpQ54kuxUZH+"
+ + "ojkTLaJAyMPCiXCNTQD6UDYDHaYQLXafJYh3GWzQVTqXJmCwjv7CD22O0cWkQNH1pelnTQ6ZSvn9osSgnUV51CVlziC05T6"
+ + "LxgVd0UhiBa1HdTI3D7X5SbXx4bFs1x/qyqJCNQX4J7sB9jUtKl0s6SX+UN5j59tbDCE/x0t4OOpetSECWkVxRMX5R6CezVj"
+ + "Us+3PnNMCjXxGrb3DnB6P0dAgMBAAECggEAFyTHg2xSqBE6OJ20jNR9Hd/nIXT5JfvF4tGfS8OcxrDH8kLKygyIXgCoW47qc"
+ + "ZZVTLkiBTbna3lrHVDC8LHDBEb+MdXpIKCjEd1WDIiKp+g7rANwyiAKilj+dVTGWCEsRI3MS31t911WLyoR63fYPeiVr8qk4"
+ + "R29+B/GI2unO33VXNQj4lF81jvHdqgIQkYaY37nQSGx7MHamDscDLHvWKzrvY4sjidS4KgmVzkVmbMBwHvY6asoGhkZWTtir"
+ + "u0rJpirvHaGJAuWZjfVCaZkT4XAKloOrsZiMmAYCzG5xLy4EiB68zHFmQ8V/zLcMCSlofC9tmvp9NQHPY7cSUFzYQKBgQDqC"
+ + "5j+SFFx56gz63gazYKseGIfkFHbQFOulVdV6cSDuIjvZtQJ2PMaIv8i5tpGlIJsPiOqoJU0NI1/aGrPFtFUpNXChMaPWiBdm"
+ + "GDwJKUO8EWsHYk1sm0z7Nf4tfuPAV/kH75wsOMlcQ5qrtYU3rrcjfEIpILtG/VTja+37OAZmQKBgQDK5FaoHb8W4hanZQ6my"
+ + "AG39iF2Z9qITKWyvZ8yh3zGvhvO5RGR7KPjrCp1cnizNfmF+q6lJxmXXowijdTaL7opaVayCJt3ewvgatr9uhT8Vz6kgfeA7"
+ + "O+7dIRaTQB8+YTeMFRdlRnuVGx2JLbyUycIJCb3mmWHGTL9lW0ZWyHaJQKBgQDHOBIFuLci9uZ1M2Tro60sc9hKN8WFlI7ml"
+ + "5ZcufydhrGA3o10yGe+ArYcFlcMJxORYZ9oeQIoCue64L2yAyEyJJET34NIuJW+NZumLfsV6S3VINsPiw5rWZpIyVcU1j2yZ"
+ + "9bqA5eF4mM8KhBueVyjqmrWSXpsrBS6B2vgalAjWQKBgBtx0asCAxQ0Vv4jtFypF1psB9C9cZkYTR2lesBaBW3Yz2goIj1L9"
+ + "ktYwZGLf3o2Zd9SrocWh+aq2mfeKZmt9Q+e+SQx992snkmoCqFhp28O2iFklzcwValUtIaGffdpxShM/0x9W7maX+WHR9v1l"
+ + "YULZt39W5hvty8IJG7WnfilAoGBAM6SeUI0xN30tWV5r1cLCG4d+THzqGjZCisCiL2/QN9cWKhtan5Z0+EZd7YkPQpFPw7+R"
+ + "CDL/zrR6RMV0ZhLjxMwVwbameaHoYKYKzTP8rGYwrVFWDNeGh9arn9UdeF/CTfwBiYDtHSjHdVOUW3KYb6VnYuFG+uog0uOB"
+ + "5uEg9Z5";
+ byte[] privateKeyBytes = Base64.decode(decryptedPrivateKey);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
+
+ IPrivateKeyDecryptor decryptorMock = mock(IPrivateKeyDecryptor.class);
+
+ when(decryptorMock.decryptPrivateKey(anyString(), eq("testkey"))).thenReturn(privateKey);
+ BoxDeveloperEditionAPIConnection api = this.getBoxDeveloperEditionAPIConnection(decryptorMock);
+
+ this.mockFirstResponse(tokenPath);
+
+ this.wireMockRule.stubFor(requestMatching(this.getRequestMatcher(tokenPath))
+ .atPriority(2)
+ .inScenario("JWT Retry")
+ .whenScenarioStateIs("429 sent")
+ .willReturn(aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", APPLICATION_JSON)
+ .withBody(responseWithToken(accessToken))));
+
+ this.mockListener();
+
+ api.authenticate();
+
+ Mockito.verify(decryptorMock, times(2)).decryptPrivateKey(anyString(), eq("testkey"));
+ assertThat(api.getAccessToken(), is(accessToken));
+ }
+
private static String responseWithToken(String accessToken) {
return "{\n"
+ " \"access_token\": \"" + accessToken + "\",\n"
@@ -169,6 +229,10 @@ private static String responseWithToken(String accessToken) {
}
private BoxDeveloperEditionAPIConnection getBoxDeveloperEditionAPIConnection() {
+ return getBoxDeveloperEditionAPIConnection(null);
+ }
+
+ private BoxDeveloperEditionAPIConnection getBoxDeveloperEditionAPIConnection(IPrivateKeyDecryptor decryptor) {
final String baseURL = "https://localhost:" + wireMockRule.httpsPort();
final int expectedNumRetryAttempts = 2;
@@ -203,7 +267,9 @@ private BoxDeveloperEditionAPIConnection getBoxDeveloperEditionAPIConnection() {
prefs.setPrivateKey(new String(Base64.decode(privateKey)));
prefs.setPrivateKeyPassword("testkey");
prefs.setPublicKeyID("abcdefg");
-
+ if (decryptor != null) {
+ prefs.setPrivateKeyDecryptor(decryptor);
+ }
BoxDeveloperEditionAPIConnection api = new BoxDeveloperEditionAPIConnection("12345",
DeveloperEditionEntityType.USER, "foo", "bar", prefs, null);