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.getMaxRequestAttempts());
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.isResponseRetryable(apiException.getResponseCode())) {
350                    throw apiException;
351                }
352
353                logger.log(Level.WARNING, "Retrying authentication request due to transient error status={0} body={1}",
354                        new Object[] {apiException.getResponseCode(), apiException.getResponse()});
355
356                try {
357                    this.backoffCounter.waitBackoff();
358                } catch (InterruptedException interruptedException) {
359                    Thread.currentThread().interrupt();
360                    throw apiException;
361                }
362
363                long endWaitTime = System.currentTimeMillis();
364                long secondsSinceResponseReceived = (endWaitTime - responseReceivedTime) / 1000;
365
366                try {
367                    // Use the Date advertised by the Box server in the exception
368                    // as the current time to synchronize clocks
369                    jwtTime = this.getDateForJWTConstruction(apiException, secondsSinceResponseReceived);
370                } catch (Exception e) {
371                    throw apiException;
372                }
373
374            }
375        }
376
377        if (json == null) {
378            throw new RuntimeException("Unable to read authentication response in SDK.");
379        }
380
381        JsonObject jsonObject = JsonObject.readFrom(json);
382        this.setAccessToken(jsonObject.get("access_token").asString());
383        this.setLastRefresh(System.currentTimeMillis());
384        this.setExpires(jsonObject.get("expires_in").asLong() * 1000);
385
386        //if token cache is specified, save to cache
387        if (this.accessTokenCache != null) {
388            String key = this.getAccessTokenCacheKey();
389            JsonObject accessTokenCacheInfo = new JsonObject()
390                    .add("accessToken", this.getAccessToken())
391                    .add("lastRefresh", this.getLastRefresh())
392                    .add("expires", this.getExpires());
393
394            this.accessTokenCache.put(key, accessTokenCacheInfo.toString());
395        }
396    }
397
398    private NumericDate getDateForJWTConstruction(BoxAPIException apiException, long secondsSinceResponseDateReceived) {
399        NumericDate currentTime;
400        List<String> responseDates = apiException.getHeaders().get("Date");
401
402        if (responseDates != null) {
403            String responseDate = responseDates.get(0);
404            SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss zzz");
405            try {
406                Date date = dateFormat.parse(responseDate);
407                currentTime = NumericDate.fromMilliseconds(date.getTime());
408                currentTime.addSeconds(secondsSinceResponseDateReceived);
409            } catch (ParseException e) {
410                currentTime = NumericDate.now();
411            }
412        } else {
413            currentTime = NumericDate.now();
414        }
415        return currentTime;
416    }
417
418    void setBackoffCounter(BackoffCounter counter) {
419        this.backoffCounter = counter;
420    }
421
422    /**
423     * BoxDeveloperEditionAPIConnection can always refresh, but this method is required elsewhere.
424     * @return true always.
425     */
426    public boolean canRefresh() {
427        return true;
428    }
429
430    /**
431     * Refresh's this connection's access token using Box Developer Edition.
432     * @throws IllegalStateException if this connection's access token cannot be refreshed.
433     */
434    public void refresh() {
435        this.getRefreshLock().writeLock().lock();
436
437        try {
438            this.authenticate();
439        } catch (BoxAPIException e) {
440            this.notifyError(e);
441            this.getRefreshLock().writeLock().unlock();
442            throw e;
443        }
444
445        this.notifyRefresh();
446        this.getRefreshLock().writeLock().unlock();
447    }
448
449    private String getAccessTokenCacheKey() {
450        return String.format("/%s/%s/%s/%s", this.getUserAgent(), this.getClientID(),
451                this.entityType.toString(), this.entityID);
452    }
453
454    private void tryRestoreUsingAccessTokenCache() {
455        if (this.accessTokenCache == null) {
456            //no cache specified so force authentication
457            this.authenticate();
458        } else {
459            String cachedTokenInfo = this.accessTokenCache.get(this.getAccessTokenCacheKey());
460            if (cachedTokenInfo == null) {
461                //not found; probably first time for this client config so authenticate; info will then be cached
462                this.authenticate();
463            } else {
464                //pull access token cache info; authentication will occur as needed (if token is expired)
465                JsonObject json = JsonObject.readFrom(cachedTokenInfo);
466                this.setAccessToken(json.get("accessToken").asString());
467                this.setLastRefresh(json.get("lastRefresh").asLong());
468                this.setExpires(json.get("expires").asLong());
469            }
470        }
471    }
472
473    private String constructJWTAssertion() {
474        return this.constructJWTAssertion(null);
475    }
476
477    private String constructJWTAssertion(NumericDate now) {
478        JwtClaims claims = new JwtClaims();
479        claims.setIssuer(this.getClientID());
480        claims.setAudience(JWT_AUDIENCE);
481        if (now == null) {
482            claims.setExpirationTimeMinutesInTheFuture(0.5f);
483        } else {
484            now.addSeconds(30L);
485            claims.setExpirationTime(now);
486        }
487        claims.setSubject(this.entityID);
488        claims.setClaim("box_sub_type", this.entityType.toString());
489        claims.setGeneratedJwtId(64);
490
491        JsonWebSignature jws = new JsonWebSignature();
492        jws.setPayload(claims.toJson());
493        jws.setKey(this.decryptPrivateKey());
494        jws.setAlgorithmHeaderValue(this.getAlgorithmIdentifier());
495        jws.setHeader("typ", "JWT");
496        if ((this.publicKeyID != null) && !this.publicKeyID.isEmpty()) {
497            jws.setHeader("kid", this.publicKeyID);
498        }
499
500        String assertion;
501
502        try {
503            assertion = jws.getCompactSerialization();
504        } catch (JoseException e) {
505            throw new BoxAPIException("Error serializing JSON Web Token assertion.", e);
506        }
507
508        return assertion;
509    }
510
511    private String getAlgorithmIdentifier() {
512        String algorithmId = AlgorithmIdentifiers.RSA_USING_SHA256;
513        switch (this.encryptionAlgorithm) {
514            case RSA_SHA_384:
515                algorithmId = AlgorithmIdentifiers.RSA_USING_SHA384;
516                break;
517            case RSA_SHA_512:
518                algorithmId = AlgorithmIdentifiers.RSA_USING_SHA512;
519                break;
520            case RSA_SHA_256:
521            default:
522                break;
523        }
524
525        return algorithmId;
526    }
527
528    private PrivateKey decryptPrivateKey() {
529        PrivateKey decryptedPrivateKey = null;
530        try {
531            PEMParser keyReader = new PEMParser(new StringReader(this.privateKey));
532            Object keyPair = keyReader.readObject();
533            keyReader.close();
534
535            if (keyPair instanceof PrivateKeyInfo) {
536                PrivateKeyInfo keyInfo = (PrivateKeyInfo) keyPair;
537                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
538            } else if (keyPair instanceof PEMEncryptedKeyPair) {
539                JcePEMDecryptorProviderBuilder builder = new JcePEMDecryptorProviderBuilder();
540                PEMDecryptorProvider decryptionProvider = builder.build(this.privateKeyPassword.toCharArray());
541                keyPair = ((PEMEncryptedKeyPair) keyPair).decryptKeyPair(decryptionProvider);
542                PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
543                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
544            } else if (keyPair instanceof PKCS8EncryptedPrivateKeyInfo) {
545                InputDecryptorProvider pkcs8Prov = new JceOpenSSLPKCS8DecryptorProviderBuilder().setProvider("BC")
546                    .build(this.privateKeyPassword.toCharArray());
547                PrivateKeyInfo keyInfo =  ((PKCS8EncryptedPrivateKeyInfo) keyPair).decryptPrivateKeyInfo(pkcs8Prov);
548                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
549            } else {
550                PrivateKeyInfo keyInfo = ((PEMKeyPair) keyPair).getPrivateKeyInfo();
551                decryptedPrivateKey = (new JcaPEMKeyConverter()).getPrivateKey(keyInfo);
552            }
553        } catch (IOException e) {
554            throw new BoxAPIException("Error parsing private key for Box Developer Edition.", e);
555        } catch (OperatorCreationException e) {
556            throw new BoxAPIException("Error parsing PKCS#8 private key for Box Developer Edition.", e);
557        } catch (PKCSException e) {
558            throw new BoxAPIException("Error parsing PKCS private key for Box Developer Edition.", e);
559        }
560        return decryptedPrivateKey;
561    }
562
563}