RecoveryController.java revision 86f5bb1a8cfe2d169767fb723d315955dda3a0e6
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 @NonNull public static RecoveryController getInstance(@NonNull 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 throw new UnsupportedOperationException("Operation is not supported, use generateKey"); 469 } 470 471 /** 472 * @deprecated Use {@link #generateKey(String)}. 473 */ 474 @Deprecated 475 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 476 public Key generateKey(@NonNull String alias, byte[] account) 477 throws InternalRecoveryServiceException, LockScreenRequiredException { 478 return generateKey(alias); 479 } 480 481 /** 482 * Generates a recoverable key with the given {@code alias}. 483 * 484 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery 485 * service. 486 * @throws LockScreenRequiredException if the user does not have a lock screen set. A lock 487 * screen is required to generate recoverable keys. 488 */ 489 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 490 public @NonNull Key generateKey(@NonNull String alias) throws InternalRecoveryServiceException, 491 LockScreenRequiredException { 492 try { 493 String grantAlias = mBinder.generateKey(alias); 494 if (grantAlias == null) { 495 throw new InternalRecoveryServiceException("null grant alias"); 496 } 497 return getKeyFromGrant(grantAlias); 498 } catch (RemoteException e) { 499 throw e.rethrowFromSystemServer(); 500 } catch (UnrecoverableKeyException e) { 501 throw new InternalRecoveryServiceException("Failed to get key from keystore", e); 502 } catch (ServiceSpecificException e) { 503 if (e.errorCode == ERROR_INSECURE_USER) { 504 throw new LockScreenRequiredException(e.getMessage()); 505 } 506 throw wrapUnexpectedServiceSpecificException(e); 507 } 508 } 509 510 /** 511 * Imports a 256-bit recoverable AES key with the given {@code alias} and the raw bytes {@code 512 * keyBytes}. 513 * 514 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery 515 * service. 516 * @throws LockScreenRequiredException if the user does not have a lock screen set. A lock 517 * screen is required to generate recoverable keys. 518 * 519 */ 520 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 521 public @NonNull Key importKey(@NonNull String alias, @NonNull byte[] keyBytes) 522 throws InternalRecoveryServiceException, LockScreenRequiredException { 523 try { 524 String grantAlias = mBinder.importKey(alias, keyBytes); 525 if (grantAlias == null) { 526 throw new InternalRecoveryServiceException("Null grant alias"); 527 } 528 return getKeyFromGrant(grantAlias); 529 } catch (RemoteException e) { 530 throw e.rethrowFromSystemServer(); 531 } catch (UnrecoverableKeyException e) { 532 throw new InternalRecoveryServiceException("Failed to get key from keystore", e); 533 } catch (ServiceSpecificException e) { 534 if (e.errorCode == ERROR_INSECURE_USER) { 535 throw new LockScreenRequiredException(e.getMessage()); 536 } 537 throw wrapUnexpectedServiceSpecificException(e); 538 } 539 } 540 541 /** 542 * Gets a key called {@code alias} from the recoverable key store. 543 * 544 * @param alias The key alias. 545 * @return The key. 546 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery 547 * service. 548 * @throws UnrecoverableKeyException if key is permanently invalidated or not found. 549 */ 550 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 551 public @Nullable Key getKey(@NonNull String alias) 552 throws InternalRecoveryServiceException, UnrecoverableKeyException { 553 try { 554 String grantAlias = mBinder.getKey(alias); 555 if (grantAlias == null || "".equals(grantAlias)) { 556 return null; 557 } 558 return getKeyFromGrant(grantAlias); 559 } catch (RemoteException e) { 560 throw e.rethrowFromSystemServer(); 561 } catch (ServiceSpecificException e) { 562 throw wrapUnexpectedServiceSpecificException(e); 563 } 564 } 565 566 /** 567 * Returns the key with the given {@code grantAlias}. 568 */ 569 @NonNull Key getKeyFromGrant(@NonNull String grantAlias) throws UnrecoverableKeyException { 570 return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore( 571 mKeyStore, 572 grantAlias, 573 KeyStore.UID_SELF); 574 } 575 576 /** 577 * Removes a key called {@code alias} from the recoverable key store. 578 * 579 * @param alias The key alias. 580 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery 581 * service. 582 */ 583 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 584 public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException { 585 try { 586 mBinder.removeKey(alias); 587 } catch (RemoteException e) { 588 throw e.rethrowFromSystemServer(); 589 } catch (ServiceSpecificException e) { 590 throw wrapUnexpectedServiceSpecificException(e); 591 } 592 } 593 594 /** 595 * Returns a new {@link RecoverySession}. 596 * 597 * <p>A recovery session is required to restore keys from a remote store. 598 */ 599 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 600 public @NonNull RecoverySession createRecoverySession() { 601 return RecoverySession.newInstance(this); 602 } 603 604 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 605 public @NonNull Map<String, X509Certificate> getRootCertificates() { 606 return TrustedRootCertificates.getRootCertificates(); 607 } 608 609 InternalRecoveryServiceException wrapUnexpectedServiceSpecificException( 610 ServiceSpecificException e) { 611 if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) { 612 return new InternalRecoveryServiceException(e.getMessage()); 613 } 614 615 // Should never happen. If it does, it's a bug, and we need to update how the method that 616 // called this throws its exceptions. 617 return new InternalRecoveryServiceException("Unexpected error code for method: " 618 + e.errorCode, e); 619 } 620} 621