RecoveryController.java revision 0bbaf189c259f7d3154737c4284023921dc821b0
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.security.keystore.recovery;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.RequiresPermission;
22import android.annotation.SystemApi;
23import android.app.PendingIntent;
24import android.content.Context;
25import android.content.pm.PackageManager.NameNotFoundException;
26import android.os.RemoteException;
27import android.os.ServiceManager;
28import android.os.ServiceSpecificException;
29import android.security.KeyStore;
30import android.security.keystore.AndroidKeyStoreProvider;
31
32import com.android.internal.widget.ILockSettings;
33
34import java.security.Key;
35import java.security.UnrecoverableKeyException;
36import java.security.cert.CertPath;
37import java.security.cert.CertificateException;
38import java.security.cert.X509Certificate;
39import java.util.ArrayList;
40import java.util.List;
41import java.util.Map;
42
43/**
44 * An assistant for generating {@link javax.crypto.SecretKey} instances that can be recovered by
45 * other Android devices belonging to the user. The exported keychain is protected by the user's
46 * lock screen.
47 *
48 * <p>The RecoveryController must be paired with a recovery agent. The recovery agent is responsible
49 * for transporting the keychain to remote trusted hardware. This hardware must prevent brute force
50 * attempts against the user's lock screen by limiting the number of allowed guesses (to, e.g., 10).
51 * After that number of incorrect guesses, the trusted hardware no longer allows access to the
52 * key chain.
53 *
54 * <p>Only the recovery agent itself is able to create keys, so it is expected that the recovery
55 * agent is itself the system app.
56 *
57 * <p>A recovery agent requires the privileged permission
58 * {@code android.Manifest.permission#RECOVER_KEYSTORE}.
59 *
60 * @hide
61 */
62@SystemApi
63public class RecoveryController {
64    private static final String TAG = "RecoveryController";
65
66    /** Key has been successfully synced. */
67    public static final int RECOVERY_STATUS_SYNCED = 0;
68    /** Waiting for recovery agent to sync the key. */
69    public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
70    /** Key cannot be synced. */
71    public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
72
73    /**
74     * Failed because no snapshot is yet pending to be synced for the user.
75     *
76     * @hide
77     */
78    public static final int ERROR_NO_SNAPSHOT_PENDING = 21;
79
80    /**
81     * Failed due to an error internal to the recovery service. This is unexpected and indicates
82     * either a problem with the logic in the service, or a problem with a dependency of the
83     * service (such as AndroidKeyStore).
84     *
85     * @hide
86     */
87    public static final int ERROR_SERVICE_INTERNAL_ERROR = 22;
88
89    /**
90     * Failed because the user does not have a lock screen set.
91     *
92     * @hide
93     */
94    public static final int ERROR_INSECURE_USER = 23;
95
96    /**
97     * Error thrown when attempting to use a recovery session that has since been closed.
98     *
99     * @hide
100     */
101    public static final int ERROR_SESSION_EXPIRED = 24;
102
103    /**
104     * Failed because the format of the provided certificate is incorrect, e.g., cannot be decoded
105     * properly or misses necessary fields.
106     *
107     * <p>Note that this is different from {@link #ERROR_INVALID_CERTIFICATE}, which implies the
108     * certificate has a correct format but cannot be validated.
109     *
110     * @hide
111     */
112    public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25;
113
114    /**
115     * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong,
116     * the data has become corrupted, the data has been tampered with, etc.
117     *
118     * @hide
119     */
120    public static final int ERROR_DECRYPTION_FAILED = 26;
121
122    /**
123     * Error thrown if the format of a given key is invalid. This might be because the key has a
124     * wrong length, invalid content, etc.
125     *
126     * @hide
127     */
128    public static final int ERROR_INVALID_KEY_FORMAT = 27;
129
130    /**
131     * Failed because the provided certificate cannot be validated, e.g., is expired or has invalid
132     * signatures.
133     *
134     * <p>Note that this is different from {@link #ERROR_BAD_CERTIFICATE_FORMAT}, which denotes
135     * incorrect certificate formats, e.g., due to wrong encoding or structure.
136     *
137     * @hide
138     */
139    public static final int ERROR_INVALID_CERTIFICATE = 28;
140
141    private final ILockSettings mBinder;
142    private final KeyStore mKeyStore;
143
144    private RecoveryController(ILockSettings binder, KeyStore keystore) {
145        mBinder = binder;
146        mKeyStore = keystore;
147    }
148
149    /**
150     * Internal method used by {@code RecoverySession}.
151     *
152     * @hide
153     */
154    ILockSettings getBinder() {
155        return mBinder;
156    }
157
158    /**
159     * Gets a new instance of the class.
160     */
161    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
162    public static RecoveryController getInstance(Context context) {
163        ILockSettings lockSettings =
164                ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"));
165        return new RecoveryController(lockSettings, KeyStore.getInstance());
166    }
167
168    /**
169     * @deprecated Use {@link #initRecoveryService(String, byte[], byte[])} instead.
170     */
171    @Deprecated
172    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
173    public void initRecoveryService(
174            @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
175            throws CertificateException, InternalRecoveryServiceException {
176        try {
177            mBinder.initRecoveryService(rootCertificateAlias, signedPublicKeyList);
178        } catch (RemoteException e) {
179            throw e.rethrowFromSystemServer();
180        } catch (ServiceSpecificException e) {
181            if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT
182                    || e.errorCode == ERROR_INVALID_CERTIFICATE) {
183                throw new CertificateException(e.getMessage());
184            }
185            throw wrapUnexpectedServiceSpecificException(e);
186        }
187    }
188
189    /**
190     * Initializes the recovery service for the calling application. The detailed steps should be:
191     * <ol>
192     *     <li>Parse {@code signatureFile} to get relevant information.
193     *     <li>Validate the signer's X509 certificate, contained in {@code signatureFile}, against
194     *         the root certificate pre-installed in the OS and chosen by {@code
195     *         rootCertificateAlias}.
196     *     <li>Verify the public-key signature, contained in {@code signatureFile}, and verify it
197     *         against the entire {@code certificateFile}.
198     *     <li>Parse {@code certificateFile} to get relevant information.
199     *     <li>Check the serial number, contained in {@code certificateFile}, and skip the following
200     *         steps if the serial number is not larger than the one previously stored.
201     *     <li>Randomly choose a X509 certificate from the endpoint X509 certificates, contained in
202     *         {@code certificateFile}, and validate it against the root certificate pre-installed
203     *         in the OS and chosen by {@code rootCertificateAlias}.
204     *     <li>Store the chosen X509 certificate and the serial in local database for later use.
205     * </ol>
206     *
207     * @param rootCertificateAlias the alias of a root certificate pre-installed in the OS
208     * @param certificateFile the binary content of the XML file containing a list of recovery
209     *     service X509 certificates, and other metadata including the serial number
210     * @param signatureFile the binary content of the XML file containing the public-key signature
211     *     of the entire certificate file, and a signer's X509 certificate
212     * @throws CertificateException if the given certificate files cannot be parsed or validated
213     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
214     *     service.
215     */
216    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
217    public void initRecoveryService(
218            @NonNull String rootCertificateAlias, @NonNull byte[] certificateFile,
219            @NonNull byte[] signatureFile)
220            throws CertificateException, InternalRecoveryServiceException {
221        try {
222            mBinder.initRecoveryServiceWithSigFile(
223                    rootCertificateAlias, certificateFile, signatureFile);
224        } catch (RemoteException e) {
225            throw e.rethrowFromSystemServer();
226        } catch (ServiceSpecificException e) {
227            if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT
228                    || e.errorCode == ERROR_INVALID_CERTIFICATE) {
229                throw new CertificateException(e.getMessage());
230            }
231            throw wrapUnexpectedServiceSpecificException(e);
232        }
233    }
234
235    /**
236     * @deprecated Use {@link #getKeyChainSnapshot()}
237     */
238    @Deprecated
239    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
240    public @Nullable KeyChainSnapshot getRecoveryData() throws InternalRecoveryServiceException {
241        return getKeyChainSnapshot();
242    }
243
244    /**
245     * Returns data necessary to store all recoverable keys. Key material is
246     * encrypted with user secret and recovery public key.
247     *
248     * @return Data necessary to recover keystore or {@code null} if snapshot is not available.
249     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
250     *     service.
251     */
252    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
253    public @Nullable KeyChainSnapshot getKeyChainSnapshot()
254            throws InternalRecoveryServiceException {
255        try {
256            return mBinder.getKeyChainSnapshot();
257        } catch (RemoteException e) {
258            throw e.rethrowFromSystemServer();
259        } catch (ServiceSpecificException e) {
260            if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) {
261                return null;
262            }
263            throw wrapUnexpectedServiceSpecificException(e);
264        }
265    }
266
267    /**
268     * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
269     * #getKeyChainSnapshot} can be used to get the snapshot. Note that every recovery agent can
270     * have at most one registered listener at any time.
271     *
272     * @param intent triggered when new snapshot is available. Unregisters listener if the value is
273     *     {@code null}.
274     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
275     *     service.
276     */
277    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
278    public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
279            throws InternalRecoveryServiceException {
280        try {
281            mBinder.setSnapshotCreatedPendingIntent(intent);
282        } catch (RemoteException e) {
283            throw e.rethrowFromSystemServer();
284        } catch (ServiceSpecificException e) {
285            throw wrapUnexpectedServiceSpecificException(e);
286        }
287    }
288
289    /**
290     * Server parameters used to generate new recovery key blobs. This value will be included in
291     * {@code KeyChainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included
292     * in vaultParams {@link RecoverySession#start(CertPath, byte[], byte[], List)}.
293     *
294     * @param serverParams included in recovery key blob.
295     * @see #getKeyChainSnapshot
296     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
297     *     service.
298     */
299    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
300    public void setServerParams(@NonNull byte[] serverParams)
301            throws InternalRecoveryServiceException {
302        try {
303            mBinder.setServerParams(serverParams);
304        } catch (RemoteException e) {
305            throw e.rethrowFromSystemServer();
306        } catch (ServiceSpecificException e) {
307            throw wrapUnexpectedServiceSpecificException(e);
308        }
309    }
310
311    /**
312     * @deprecated Use {@link #getAliases()}.
313     */
314    @Deprecated
315    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
316    public List<String> getAliases(@Nullable String packageName)
317            throws InternalRecoveryServiceException {
318        return getAliases();
319    }
320
321    /**
322     * Returns a list of aliases of keys belonging to the application.
323     */
324    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
325    public @NonNull List<String> getAliases() throws InternalRecoveryServiceException {
326        try {
327            Map<String, Integer> allStatuses = mBinder.getRecoveryStatus();
328            return new ArrayList<>(allStatuses.keySet());
329        } catch (RemoteException e) {
330            throw e.rethrowFromSystemServer();
331        } catch (ServiceSpecificException e) {
332            throw wrapUnexpectedServiceSpecificException(e);
333        }
334    }
335
336    /**
337     * @deprecated Use {@link #setRecoveryStatus(String, int)}
338     */
339    @Deprecated
340    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
341    public void setRecoveryStatus(
342            @NonNull String packageName, String alias, int status)
343            throws NameNotFoundException, InternalRecoveryServiceException {
344        setRecoveryStatus(alias, status);
345    }
346
347    /**
348     * Sets the recovery status for given key. It is used to notify the keystore that the key was
349     * successfully stored on the server or that there was an error. An application can check this
350     * value using {@link #getRecoveryStatus(String, String)}.
351     *
352     * @param alias The alias of the key whose status to set.
353     * @param status The status of the key. One of {@link #RECOVERY_STATUS_SYNCED},
354     *     {@link #RECOVERY_STATUS_SYNC_IN_PROGRESS} or {@link #RECOVERY_STATUS_PERMANENT_FAILURE}.
355     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
356     *     service.
357     */
358    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
359    public void setRecoveryStatus(@NonNull String alias, int status)
360            throws InternalRecoveryServiceException {
361        try {
362            mBinder.setRecoveryStatus(alias, status);
363        } catch (RemoteException e) {
364            throw e.rethrowFromSystemServer();
365        } catch (ServiceSpecificException e) {
366            throw wrapUnexpectedServiceSpecificException(e);
367        }
368    }
369
370    /**
371     * @deprecated Use {@link #getRecoveryStatus(String)}.
372     */
373    @Deprecated
374    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
375    public int getRecoveryStatus(String packageName, String alias)
376            throws InternalRecoveryServiceException {
377        return getRecoveryStatus(alias);
378    }
379
380    /**
381     * Returns the recovery status for the key with the given {@code alias}.
382     *
383     * <ul>
384     *   <li>{@link #RECOVERY_STATUS_SYNCED}
385     *   <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS}
386     *   <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE}
387     * </ul>
388     *
389     * @see #setRecoveryStatus(String, int)
390     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
391     *     service.
392     */
393    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
394    public int getRecoveryStatus(@NonNull String alias) throws InternalRecoveryServiceException {
395        try {
396            Map<String, Integer> allStatuses = mBinder.getRecoveryStatus();
397            Integer status = allStatuses.get(alias);
398            if (status == null) {
399                return RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE;
400            } else {
401                return status;
402            }
403        } catch (RemoteException e) {
404            throw e.rethrowFromSystemServer();
405        } catch (ServiceSpecificException e) {
406            throw wrapUnexpectedServiceSpecificException(e);
407        }
408    }
409
410    /**
411     * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
412     * is necessary to recover data.
413     *
414     * @param secretTypes {@link KeyChainProtectionParams#TYPE_LOCKSCREEN}
415     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
416     *     service.
417     */
418    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
419    public void setRecoverySecretTypes(
420            @NonNull @KeyChainProtectionParams.UserSecretType int[] secretTypes)
421            throws InternalRecoveryServiceException {
422        try {
423            mBinder.setRecoverySecretTypes(secretTypes);
424        } catch (RemoteException e) {
425            throw e.rethrowFromSystemServer();
426        } catch (ServiceSpecificException e) {
427            throw wrapUnexpectedServiceSpecificException(e);
428        }
429    }
430
431    /**
432     * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
433     * necessary to generate KeyChainSnapshot.
434     *
435     * @return list of recovery secret types
436     * @see KeyChainSnapshot
437     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
438     *     service.
439     */
440    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
441    public @NonNull @KeyChainProtectionParams.UserSecretType int[] getRecoverySecretTypes()
442            throws InternalRecoveryServiceException {
443        try {
444            return mBinder.getRecoverySecretTypes();
445        } catch (RemoteException e) {
446            throw e.rethrowFromSystemServer();
447        } catch (ServiceSpecificException e) {
448            throw wrapUnexpectedServiceSpecificException(e);
449        }
450    }
451
452    /**
453     * Deprecated.
454     * Generates a AES256/GCM/NoPADDING key called {@code alias} and loads it into the recoverable
455     * key store. Returns the raw material of the key.
456     *
457     * @param alias The key alias.
458     * @param account The account associated with the key
459     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
460     *     service.
461     * @throws LockScreenRequiredException if the user has not set a lock screen. This is required
462     *     to generate recoverable keys, as the snapshots are encrypted using a key derived from the
463     *     lock screen.
464     */
465    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
466    public byte[] generateAndStoreKey(@NonNull String alias, byte[] account)
467            throws InternalRecoveryServiceException, LockScreenRequiredException {
468        try {
469            return mBinder.generateAndStoreKey(alias);
470        } catch (RemoteException e) {
471            throw e.rethrowFromSystemServer();
472        } catch (ServiceSpecificException e) {
473            if (e.errorCode == ERROR_INSECURE_USER) {
474                throw new LockScreenRequiredException(e.getMessage());
475            }
476            throw wrapUnexpectedServiceSpecificException(e);
477        }
478    }
479
480    /**
481     * @deprecated Use {@link #generateKey(String)}.
482     */
483    @Deprecated
484    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
485    public Key generateKey(@NonNull String alias, byte[] account)
486            throws InternalRecoveryServiceException, LockScreenRequiredException {
487        return generateKey(alias);
488    }
489
490    /**
491     * Generates a recoverable key with the given {@code alias}.
492     *
493     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
494     *     service.
495     * @throws LockScreenRequiredException if the user does not have a lock screen set. A lock
496     *     screen is required to generate recoverable keys.
497     */
498    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
499    public @NonNull Key generateKey(@NonNull String alias) throws InternalRecoveryServiceException,
500            LockScreenRequiredException {
501        try {
502            String grantAlias = mBinder.generateKey(alias);
503            if (grantAlias == null) {
504                throw new InternalRecoveryServiceException("null grant alias");
505            }
506            return getKeyFromGrant(grantAlias);
507        } catch (RemoteException e) {
508            throw e.rethrowFromSystemServer();
509        } catch (UnrecoverableKeyException e) {
510            throw new InternalRecoveryServiceException("Failed to get key from keystore", e);
511        } catch (ServiceSpecificException e) {
512            if (e.errorCode == ERROR_INSECURE_USER) {
513                throw new LockScreenRequiredException(e.getMessage());
514            }
515            throw wrapUnexpectedServiceSpecificException(e);
516        }
517    }
518
519    /**
520     * Imports a 256-bit recoverable AES key with the given {@code alias} and the raw bytes {@code
521     * keyBytes}.
522     *
523     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
524     *     service.
525     * @throws LockScreenRequiredException if the user does not have a lock screen set. A lock
526     *     screen is required to generate recoverable keys.
527     *
528     */
529    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
530    public @NonNull Key importKey(@NonNull String alias, @NonNull byte[] keyBytes)
531            throws InternalRecoveryServiceException, LockScreenRequiredException {
532        try {
533            String grantAlias = mBinder.importKey(alias, keyBytes);
534            if (grantAlias == null) {
535                throw new InternalRecoveryServiceException("Null grant alias");
536            }
537            return getKeyFromGrant(grantAlias);
538        } catch (RemoteException e) {
539            throw e.rethrowFromSystemServer();
540        } catch (UnrecoverableKeyException e) {
541            throw new InternalRecoveryServiceException("Failed to get key from keystore", e);
542        } catch (ServiceSpecificException e) {
543            if (e.errorCode == ERROR_INSECURE_USER) {
544                throw new LockScreenRequiredException(e.getMessage());
545            }
546            throw wrapUnexpectedServiceSpecificException(e);
547        }
548    }
549
550    /**
551     * Gets a key called {@code alias} from the recoverable key store.
552     *
553     * @param alias The key alias.
554     * @return The key.
555     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
556     *     service.
557     * @throws UnrecoverableKeyException if key is permanently invalidated or not found.
558     */
559    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
560    public @Nullable Key getKey(@NonNull String alias)
561            throws InternalRecoveryServiceException, UnrecoverableKeyException {
562        try {
563            String grantAlias = mBinder.getKey(alias);
564            if (grantAlias == null || "".equals(grantAlias)) {
565                return null;
566            }
567            return getKeyFromGrant(grantAlias);
568        } catch (RemoteException e) {
569            throw e.rethrowFromSystemServer();
570        } catch (ServiceSpecificException e) {
571            throw wrapUnexpectedServiceSpecificException(e);
572        }
573    }
574
575    /**
576     * Returns the key with the given {@code grantAlias}.
577     */
578    @NonNull Key getKeyFromGrant(@NonNull String grantAlias) throws UnrecoverableKeyException {
579        return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(
580                mKeyStore,
581                grantAlias,
582                KeyStore.UID_SELF);
583    }
584
585    /**
586     * Removes a key called {@code alias} from the recoverable key store.
587     *
588     * @param alias The key alias.
589     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
590     *     service.
591     */
592    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
593    public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException {
594        try {
595            mBinder.removeKey(alias);
596        } catch (RemoteException e) {
597            throw e.rethrowFromSystemServer();
598        } catch (ServiceSpecificException e) {
599            throw wrapUnexpectedServiceSpecificException(e);
600        }
601    }
602
603    /**
604     * Returns a new {@link RecoverySession}.
605     *
606     * <p>A recovery session is required to restore keys from a remote store.
607     */
608    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
609    public @NonNull RecoverySession createRecoverySession() {
610        return RecoverySession.newInstance(this);
611    }
612
613    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
614    public @NonNull Map<String, X509Certificate> getRootCertificates() {
615        return TrustedRootCertificates.getRootCertificates();
616    }
617
618    InternalRecoveryServiceException wrapUnexpectedServiceSpecificException(
619            ServiceSpecificException e) {
620        if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) {
621            return new InternalRecoveryServiceException(e.getMessage());
622        }
623
624        // Should never happen. If it does, it's a bug, and we need to update how the method that
625        // called this throws its exceptions.
626        return new InternalRecoveryServiceException("Unexpected error code for method: "
627                + e.errorCode, e);
628    }
629}
630