001package com.box.sdk;
002
003import java.io.IOException;
004import java.io.StringReader;
005import java.net.MalformedURLException;
006import java.net.URL;
007import java.security.PrivateKey;
008import java.security.Security;
009import java.text.ParseException;
010import java.text.SimpleDateFormat;
011import java.util.Date;
012import java.util.List;
013import java.util.logging.Level;
014import java.util.logging.Logger;
015
016import org.bouncycastle.asn1.pkcs.PrivateKeyInfo;
017import org.bouncycastle.jce.provider.BouncyCastleProvider;
018import org.bouncycastle.openssl.PEMDecryptorProvider;
019import org.bouncycastle.openssl.PEMEncryptedKeyPair;
020import org.bouncycastle.openssl.PEMKeyPair;
021import org.bouncycastle.openssl.PEMParser;
022import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
023import org.bouncycastle.openssl.jcajce.JceOpenSSLPKCS8DecryptorProviderBuilder;
024import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder;
025import org.bouncycastle.operator.InputDecryptorProvider;
026import org.bouncycastle.operator.OperatorCreationException;
027import org.bouncycastle.pkcs.PKCS8EncryptedPrivateKeyInfo;
028import org.bouncycastle.pkcs.PKCSException;
029import org.jose4j.jws.AlgorithmIdentifiers;
030import org.jose4j.jws.JsonWebSignature;
031import org.jose4j.jwt.JwtClaims;
032import org.jose4j.jwt.NumericDate;
033import org.jose4j.lang.JoseException;
034
035import com.eclipsesource.json.JsonObject;
036
037/**
038 * Represents an authenticated Box Developer Edition connection to the Box API.
039 *
040 * <p>This class handles everything for Box Developer Edition that isn't already handled by BoxAPIConnection.</p>
041 */
042public class BoxDeveloperEditionAPIConnection extends BoxAPIConnection {
043
044    private static final String JWT_AUDIENCE = "https://api.box.com/oauth2/token";
045    private static final String JWT_GRANT_TYPE =
046            "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&client_id=%s&client_secret=%s&assertion=%s";
047
048    private final String entityID;
049    private final DeveloperEditionEntityType entityType;
050    private final EncryptionAlgorithm encryptionAlgorithm;
051    private final String publicKeyID;
052    private final String privateKey;
053    private final String privateKeyPassword;
054    private BackoffCounter backoffCounter;
055
056    private IAccessTokenCache accessTokenCache;
057
058    static {
059        Security.addProvider(new BouncyCastleProvider());
060    }
061
062    /**
063     * Disabling an invalid constructor for Box Developer Edition.
064     * @param  accessToken  an initial access token to use for authenticating with the API.
065     */
066    private BoxDeveloperEditionAPIConnection(String accessToken) {
067        super(accessToken);
068        throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection.");
069    }
070
071    /**
072     * Disabling an invalid constructor for Box Developer Edition.
073     * @param  clientID     the client ID to use when refreshing the access token.
074     * @param  clientSecret the client secret to use when refreshing the access token.
075     * @param  accessToken  an initial access token to use for authenticating with the API.
076     * @param  refreshToken an initial refresh token to use when refreshing the access token.
077     */
078    private BoxDeveloperEditionAPIConnection(String clientID, String clientSecret, String accessToken,
079        String refreshToken) {
080        super(accessToken);
081        throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection.");
082    }
083
084    /**
085     * Disabling an invalid constructor for Box Developer Edition.
086     * @param  clientID     the client ID to use when exchanging the auth code for an access token.
087     * @param  clientSecret the client secret to use when exchanging the auth code for an access token.
088     * @param  authCode     an auth code obtained from the first half of the OAuth process.
089     */
090    private BoxDeveloperEditionAPIConnection(String clientID, String clientSecret, String authCode) {
091        super(clientID, clientSecret, authCode);
092        throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection.");
093    }
094
095    /**
096     * Disabling an invalid constructor for Box Developer Edition.
097     * @param  clientID     the client ID to use when requesting an access token.
098     * @param  clientSecret the client secret to use when requesting an access token.
099     */
100    private BoxDeveloperEditionAPIConnection(String clientID, String clientSecret) {
101        super(clientID, clientSecret);
102        throw new BoxAPIException("This constructor is not available for BoxDeveloperEditionAPIConnection.");
103    }
104
105    /**
106     * Constructs a new BoxDeveloperEditionAPIConnection.
107     * @param entityId             enterprise ID or a user ID.
108     * @param entityType           the type of entityId.
109     * @param clientID             the client ID to use when exchanging the JWT assertion for an access token.
110     * @param clientSecret         the client secret to use when exchanging the JWT assertion for an access token.
111     * @param encryptionPref       the encryption preferences for signing the JWT.
112     *
113     * @deprecated Use the version of this constructor that accepts an IAccessTokenCache to prevent unneeded
114     * requests to Box for access tokens.
115     */
116    @Deprecated
117    public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType,
118        String clientID, String clientSecret, JWTEncryptionPreferences encryptionPref) {
119
120        this(entityId, entityType, clientID, clientSecret, encryptionPref, null);
121    }
122
123
124    /**
125     * Constructs a new BoxDeveloperEditionAPIConnection leveraging an access token cache.
126     * @param entityId              enterprise ID or a user ID.
127     * @param entityType            the type of entityId.
128     * @param clientID              the client ID to use when exchanging the JWT assertion for an access token.
129     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
130     * @param encryptionPref        the encryption preferences for signing the JWT.
131     * @param accessTokenCache      the cache for storing access token information (to minimize fetching new tokens)
132     */
133    public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType,
134                                            String clientID, String clientSecret,
135                                            JWTEncryptionPreferences encryptionPref,
136                                            IAccessTokenCache accessTokenCache) {
137
138        super(clientID, clientSecret);
139
140        this.entityID = entityId;
141        this.entityType = entityType;
142        this.publicKeyID = encryptionPref.getPublicKeyID();
143        this.privateKey = encryptionPref.getPrivateKey();
144        this.privateKeyPassword = encryptionPref.getPrivateKeyPassword();
145        this.encryptionAlgorithm = encryptionPref.getEncryptionAlgorithm();
146        this.accessTokenCache = accessTokenCache;
147        this.backoffCounter = new BackoffCounter(new Time());
148    }
149
150    /**
151     * Constructs a new BoxDeveloperEditionAPIConnection.
152     * @param entityId             enterprise ID or a user ID.
153     * @param entityType           the type of entityId.
154     * @param boxConfig            box configuration settings object
155     * @param accessTokenCache      the cache for storing access token information (to minimize fetching new tokens)
156     *
157     */
158    public BoxDeveloperEditionAPIConnection(String entityId, DeveloperEditionEntityType entityType,
159        BoxConfig boxConfig, IAccessTokenCache accessTokenCache) {
160
161        this(entityId, entityType, boxConfig.getClientId(), boxConfig.getClientSecret(),
162            boxConfig.getJWTEncryptionPreferences(), accessTokenCache);
163    }
164
165    /**
166     * Creates a new Box Developer Edition connection with enterprise token.
167     * @param enterpriseId          the enterprise ID to use for requesting access token.
168     * @param clientId              the client ID to use when exchanging the JWT assertion for an access token.
169     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
170     * @param encryptionPref        the encryption preferences for signing the JWT.
171     * @return a new instance of BoxAPIConnection.
172     *
173     * @deprecated Use the version of this method that accepts an IAccessTokenCache to prevent unneeded
174     * requests to Box for access tokens.
175     */
176    @Deprecated
177    public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId,
178        String clientSecret, JWTEncryptionPreferences encryptionPref) {
179
180        BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId,
181            DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref);
182
183        connection.authenticate();
184
185        return connection;
186    }
187
188    /**
189     * Creates a new Box Developer Edition connection with enterprise token leveraging an access token cache.
190     * @param enterpriseId          the enterprise ID to use for requesting access token.
191     * @param clientId              the client ID to use when exchanging the JWT assertion for an access token.
192     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
193     * @param encryptionPref        the encryption preferences for signing the JWT.
194     * @param accessTokenCache      the cache for storing access token information (to minimize fetching new tokens)
195     * @return a new instance of BoxAPIConnection.
196     */
197    public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(String enterpriseId, String clientId,
198            String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) {
199
200        BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(enterpriseId,
201                DeveloperEditionEntityType.ENTERPRISE, clientId, clientSecret, encryptionPref, accessTokenCache);
202
203        connection.tryRestoreUsingAccessTokenCache();
204
205        return connection;
206    }
207
208    /**
209     * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig.
210     * @param boxConfig             box configuration settings object
211     * @return a new instance of BoxAPIConnection.
212     */
213    public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig) {
214
215        BoxDeveloperEditionAPIConnection connection = getAppEnterpriseConnection(boxConfig.getEnterpriseId(),
216                boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences());
217
218        return connection;
219    }
220
221    /**
222     * Creates a new Box Developer Edition connection with enterprise token leveraging BoxConfig and access token cache.
223     * @param boxConfig             box configuration settings object
224     * @param accessTokenCache      the cache for storing access token information (to minimize fetching new tokens)
225     * @return a new instance of BoxAPIConnection.
226     */
227    public static BoxDeveloperEditionAPIConnection getAppEnterpriseConnection(BoxConfig boxConfig,
228                                                                              IAccessTokenCache accessTokenCache) {
229
230        BoxDeveloperEditionAPIConnection connection = getAppEnterpriseConnection(boxConfig.getEnterpriseId(),
231                boxConfig.getClientId(), boxConfig.getClientSecret(), boxConfig.getJWTEncryptionPreferences(),
232                accessTokenCache);
233
234        return connection;
235    }
236
237    /**
238     * Creates a new Box Developer Edition connection with App User token.
239     * @param userId                the user ID to use for an App User.
240     * @param clientId              the client ID to use when exchanging the JWT assertion for an access token.
241     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
242     * @param encryptionPref        the encryption preferences for signing the JWT.
243     * @return a new instance of BoxAPIConnection.
244     *
245     * @deprecated Use the version of this method that accepts an IAccessTokenCache to prevent unneeded
246     * requests to Box for access tokens.
247     */
248    @Deprecated
249    public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, String clientId,
250        String clientSecret, JWTEncryptionPreferences encryptionPref) {
251
252        BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId,
253            DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref);
254
255        connection.authenticate();
256
257        return connection;
258    }
259
260    /**
261     * Creates a new Box Developer Edition connection with App User token.
262     * @param userId                the user ID to use for an App User.
263     * @param clientId              the client ID to use when exchanging the JWT assertion for an access token.
264     * @param clientSecret          the client secret to use when exchanging the JWT assertion for an access token.
265     * @param encryptionPref        the encryption preferences for signing the JWT.
266     * @param accessTokenCache      the cache for storing access token information (to minimize fetching new tokens)
267     * @return a new instance of BoxAPIConnection.
268     */
269    public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, String clientId,
270        String clientSecret, JWTEncryptionPreferences encryptionPref, IAccessTokenCache accessTokenCache) {
271
272        BoxDeveloperEditionAPIConnection connection = new BoxDeveloperEditionAPIConnection(userId,
273                DeveloperEditionEntityType.USER, clientId, clientSecret, encryptionPref, accessTokenCache);
274
275        connection.tryRestoreUsingAccessTokenCache();
276
277        return connection;
278    }
279
280    /**
281     * Creates a new Box Developer Edition connection with App User token levaraging BoxConfig.
282     * @param userId                the user ID to use for an App User.
283     * @param boxConfig             box configuration settings object
284     * @return a new instance of BoxAPIConnection.
285     */
286    public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, BoxConfig boxConfig) {
287        return getAppUserConnection(userId, boxConfig.getClientId(), boxConfig.getClientSecret(),
288                boxConfig.getJWTEncryptionPreferences());
289    }
290
291    /**
292     * Creates a new Box Developer Edition connection with App User token leveraging BoxConfig and access token cache.
293     * @param userId                the user ID to use for an App User.
294     * @param boxConfig             box configuration settings object
295     * @param accessTokenCache      the cache for storing access token information (to minimize fetching new tokens)
296     * @return a new instance of BoxAPIConnection.
297     */
298    public static BoxDeveloperEditionAPIConnection getAppUserConnection(String userId, BoxConfig boxConfig,
299                                                                        IAccessTokenCache accessTokenCache) {
300        return getAppUserConnection(userId, boxConfig.getClientId(), boxConfig.getClientSecret(),
301                boxConfig.getJWTEncryptionPreferences(), accessTokenCache);
302    }
303
304    /**
305     * Disabling the non-Box Developer Edition authenticate method.
306     * @param authCode an auth code obtained from the first half of the OAuth process.
307     */
308    public void authenticate(String authCode) {
309        throw new BoxAPIException("BoxDeveloperEditionAPIConnection does not allow authenticating with an auth code.");
310    }
311
312    /**
313     * Authenticates the API connection for Box Developer Edition.
314     */
315    public void authenticate() {
316        URL url;
317        try {
318            url = new URL(this.getTokenURL());
319        } catch (MalformedURLException e) {
320            assert false : "An invalid token URL indicates a bug in the SDK.";
321            throw new RuntimeException("An invalid token URL indicates a bug in the SDK.", e);
322        }
323
324        this.backoffCounter.reset(this.getMaxRetryAttempts() + 1);
325        NumericDate jwtTime = null;
326        String jwtAssertion;
327        String urlParameters;
328        BoxAPIRequest request;
329        String json = null;
330        final Logger logger = Logger.getLogger(BoxAPIRequest.class.getName());
331
332        while (this.backoffCounter.getAttemptsRemaining() > 0) {
333            // Reconstruct the JWT assertion, which regenerates the jti claim, with the new "current" time
334            jwtAssertion = this.constructJWTAssertion(jwtTime);
335            urlParameters = String.format(JWT_GRANT_TYPE, this.getClientID(), this.getClientSecret(), jwtAssertion);
336
337            request = new BoxAPIRequest(this, url, "POST");
338            request.shouldAuthenticate(false);
339            request.setBody(urlParameters);
340
341            try {
342                BoxJSONResponse response = (BoxJSONResponse) request.sendWithoutRetry();
343                json = response.getJSON();
344                break;
345            } catch (BoxAPIException apiException) {
346                long responseReceivedTime = System.currentTimeMillis();
347
348                if (!this.backoffCounter.decrement()
349                    || (!BoxAPIRequest.isRequestRetryable(apiException)
350                    && !BoxAPIRequest.isResponseRetryable(apiException.getResponseCode(), apiException))) {
351                    throw apiException;
352                }
353
354                logger.log(Level.WARNING, "Retrying authentication request due to transient error status={0} body={1}",
355                        new Object[] {apiException.getResponseCode(), apiException.getResponse()});
356
357                try {
358                    List<String> retryAfterHeader = apiException.getHeaders().get("Retry-After");
359                    if (retryAfterHeader == null) {
360                        this.backoffCounter.waitBackoff();
361                    } else {
362                        int retryAfterDelay = Integer.parseInt(retryAfterHeader.get(0)) * 1000;
363                        this.backoffCounter.waitBackoff(retryAfterDelay);
364                    }
365                } catch (InterruptedException interruptedException) {
366                    Thread.currentThread().interrupt();
367                    throw apiException;
368                }
369
370                long endWaitTime = System.currentTimeMillis();
371                long secondsSinceResponseReceived = (endWaitTime - responseReceivedTime) / 1000;
372
373                try {
374                    // Use the Date advertised by the Box server in the exception
375                    // as the current time to synchronize clocks
376                    jwtTime = this.getDateForJWTConstruction(apiException, secondsSinceResponseReceived);
377                } catch (Exception e) {
378                    throw apiException;
379                }
380
381            }
382        }
383
384        if (json == null) {
385            throw new RuntimeException("Unable to read authentication response in SDK.");
386        }
387
388        JsonObject jsonObject = JsonObject.readFrom(json);
389        this.setAccessToken(jsonObject.get("access_token").asString());
390        this.setLastRefresh(System.currentTimeMillis());
391        this.setExpires(jsonObject.get("expires_in").asLong() * 1000);
392
393        //if token cache is specified, save to cache
394        if (this.accessTokenCache != null) {
395            String key = this.getAccessTokenCacheKey();
396            JsonObject accessTokenCacheInfo = new JsonObject()
397                    .add("accessToken", this.getAccessToken())
398                    .add("lastRefresh", this.getLastRefresh())
399                    .add("expires", this.getExpires());
400
401            this.accessTokenCache.put(key, accessTokenCacheInfo.toString());
402        }
403    }
404
405    private NumericDate getDateForJWTConstruction(BoxAPIException apiException, long secondsSinceResponseDateReceived) {
406        NumericDate currentTime;
407        List<String> responseDates = apiException.getHeaders().get("Date");
408
409        if (responseDates != null) {
410            String responseDate = responseDates.get(0);
411            SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz");
412            try {
413                Date date = dateFormat.parse(responseDate);
414                currentTime = NumericDate.fromMilliseconds(date.getTime());
415                currentTime.addSeconds(secondsSinceResponseDateReceived);
416            } catch (ParseException e) {
417                currentTime = NumericDate.now();
418            }
419        } else {
420            currentTime = NumericDate.now();
421        }
422        return currentTime;
423    }
424
425    void setBackoffCounter(BackoffCounter counter) {
426        this.backoffCounter = counter;
427    }
428
429    /**
430     * BoxDeveloperEditionAPIConnection can always refresh, but this method is required elsewhere.
431     * @return true always.
432     */
433    public boolean canRefresh() {
434        return true;
435    }
436
437    /**
438     * Refresh's this connection's access token using Box Developer Edition.
439     * @throws IllegalStateException if this connection's access token cannot be refreshed.
440     */
441    public void refresh() {
442        this.getRefreshLock().writeLock().lock();
443
444        try {
445            this.authenticate();
446        } catch (BoxAPIException e) {
447            this.notifyError(e);
448            this.getRefreshLock().writeLock().unlock();
449            throw e;
450        }
451
452        this.notifyRefresh();
453        this.getRefreshLock().writeLock().unlock();
454    }
455
456    private String getAccessTokenCacheKey() {
457        return String.format("/%s/%s/%s/%s", this.getUserAgent(), this.getClientID(),
458                this.entityType.toString(), this.entityID);
459    }
460
461    private void tryRestoreUsingAccessTokenCache() {
462        if (this.accessTokenCache == null) {
463            //no cache specified so force authentication
464            this.authenticate();
465        } else {
466            String cachedTokenInfo = this.accessTokenCache.get(this.getAccessTokenCacheKey());
467            if (cachedTokenInfo == null) {
468                //not found; probably first time for this client config so authenticate; info will then be cached
469                this.authenticate();
470            } else {
471                //pull access token cache info; authentication will occur as needed (if token is expired)
472                JsonObject json = JsonObject.readFrom(cachedTokenInfo);
473                this.setAccessToken(json.get("accessToken").asString());
474                this.setLastRefresh(json.get("lastRefresh").asLong());
475                this.setExpires(json.get("expires").asLong());
476            }
477        }
478    }
479
480    private String constructJWTAssertion() {
481        return this.constructJWTAssertion(null);
482    }
483
484    private String constructJWTAssertion(NumericDate now) {
485        JwtClaims claims = new JwtClaims();
486        claims.setIssuer(this.getClientID());
487        claims.setAudience(JWT_AUDIENCE);
488        if (now == null) {
489            claims.setExpirationTimeMinutesInTheFuture(0.5f);
490        } else {
491            now.addSeconds(30L);
492            claims.setExpirationTime(now);
493        }
494        claims.setSubject(this.entityID);
495        claims.setClaim("box_sub_type", this.entityType.toString());
496        claims.setGeneratedJwtId(64);
497
498        JsonWebSignature jws = new JsonWebSignature();
499        jws.setPayload(claims.toJson());
500        jws.setKey(this.decryptPrivateKey());
501        jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier());
502        jws.setHeader("typ", "JWT");
503        if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) {
504            jws.setHeader("kid", this.publicKeyID);
505        }
506
507        String assertion;
508
509        try {
510            assertion = jws.getCompactSerialization();
511        } catch (JoseException e) {
512            throw new BoxAPIException("Error serializing JSON Web Token assertion.", e);
513        }
514
515        return assertion;
516    }
517
518    private String getAlgorithmIdentifier() {
519        String algorithmId = AlgorithmIdentifiers.RSA_USING_SHA256;
520        switch (this.encryptionAlgorithm) {
521            case RSA_SHA_384:
522                algorithmId = AlgorithmIdentifiers.RSA_USING_SHA384;
523                break;
524            case RSA_SHA_512:
525                algorithmId = AlgorithmIdentifiers.RSA_USING_SHA512;
526                break;
527            case RSA_SHA_256:
528            default:
529                break;
530        }
531
532        return algorithmId;
533    }
534
535    private PrivateKey decryptPrivateKey() {
536        PrivateKey decryptedPrivateKey = null;
537        try {
538            PEMParser keyReader = new PEMParser(new StringReader(this.privateKey));
539            Object keyPair = keyReader.readObject();
540            keyReader.close();
541
542            if (keyPair instanceof PrivateKeyInfo) {
543                PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair;
544                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
545            } else if (keyPair instanceof PEMEncryptedKeyPair) {
546                JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder();
547                PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray());
548                keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider);
549                PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
550                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
551            } else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) {
552                InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider("BC")
553                    .build(this.privateKeyPassword.toCharArray());
554                PrivateKeyInfo keyInfo =  ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov);
555                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
556            } else {
557                PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
558                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
559            }
560        } catch (IOException e) {
561            throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e);
562        } catch (OperatorCreationException e) {
563            throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e);
564        } catch (PKCSException e) {
565            throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e);
566        }
567        return decryptedPrivateKey;
568    }
569
570}