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);