RecoverySession.java revision b31ab6740d66b21a74ffa77b753ea3364288254e
1/* 2 * Copyright (C) 2018 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.RequiresPermission; 21import android.annotation.SystemApi; 22import android.os.RemoteException; 23import android.os.ServiceSpecificException; 24import android.util.Log; 25 26import java.security.SecureRandom; 27import java.security.cert.CertPath; 28import java.security.cert.CertificateException; 29import java.util.List; 30import java.util.Map; 31 32/** 33 * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a 34 * recovery agent. 35 * 36 * @hide 37 */ 38@SystemApi 39public class RecoverySession implements AutoCloseable { 40 private static final String TAG = "RecoverySession"; 41 42 private static final int SESSION_ID_LENGTH_BYTES = 16; 43 44 private final String mSessionId; 45 private final RecoveryController mRecoveryController; 46 47 private RecoverySession(RecoveryController recoveryController, String sessionId) { 48 mRecoveryController = recoveryController; 49 mSessionId = sessionId; 50 } 51 52 /** 53 * A new session, started by the {@link RecoveryController}. 54 */ 55 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 56 static RecoverySession newInstance(RecoveryController recoveryController) { 57 return new RecoverySession(recoveryController, newSessionId()); 58 } 59 60 /** 61 * Returns a new random session ID. 62 */ 63 private static String newSessionId() { 64 SecureRandom secureRandom = new SecureRandom(); 65 byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES]; 66 secureRandom.nextBytes(sessionId); 67 StringBuilder sb = new StringBuilder(); 68 for (byte b : sessionId) { 69 sb.append(Byte.toHexString(b, /*upperCase=*/ false)); 70 } 71 return sb.toString(); 72 } 73 74 /** 75 * @deprecated Use {@link #start(CertPath, byte[], byte[], List)} instead. 76 */ 77 @Deprecated 78 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 79 @NonNull public byte[] start( 80 @NonNull byte[] verifierPublicKey, 81 @NonNull byte[] vaultParams, 82 @NonNull byte[] vaultChallenge, 83 @NonNull List<KeyChainProtectionParams> secrets) 84 throws CertificateException, InternalRecoveryServiceException { 85 try { 86 byte[] recoveryClaim = 87 mRecoveryController.getBinder().startRecoverySession( 88 mSessionId, 89 verifierPublicKey, 90 vaultParams, 91 vaultChallenge, 92 secrets); 93 return recoveryClaim; 94 } catch (RemoteException e) { 95 throw e.rethrowFromSystemServer(); 96 } catch (ServiceSpecificException e) { 97 if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT 98 || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) { 99 throw new CertificateException(e.getMessage()); 100 } 101 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); 102 } 103 } 104 105 /** 106 * Starts a recovery session and returns a blob with proof of recovery secret possession. 107 * The method generates a symmetric key for a session, which trusted remote device can use to 108 * return recovery key. 109 * 110 * @param verifierCertPath The certificate path used to create the recovery blob on the source 111 * device. Keystore will verify the certificate path by using the root of trust. 112 * @param vaultParams Must match the parameters in the corresponding field in the recovery blob. 113 * Used to limit number of guesses. 114 * @param vaultChallenge Data passed from server for this recovery session and used to prevent 115 * replay attacks. 116 * @param secrets Secrets provided by user, the method only uses type and secret fields. 117 * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is 118 * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric 119 * key and parameters necessary to identify the counter with the number of failed recovery 120 * attempts. 121 * @throws CertificateException if the {@code verifierCertPath} is invalid. 122 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery 123 * service. 124 */ 125 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 126 @NonNull public byte[] start( 127 @NonNull CertPath verifierCertPath, 128 @NonNull byte[] vaultParams, 129 @NonNull byte[] vaultChallenge, 130 @NonNull List<KeyChainProtectionParams> secrets) 131 throws CertificateException, InternalRecoveryServiceException { 132 // Wrap the CertPath in a Parcelable so it can be passed via Binder calls. 133 RecoveryCertPath recoveryCertPath = 134 RecoveryCertPath.createRecoveryCertPath(verifierCertPath); 135 try { 136 byte[] recoveryClaim = 137 mRecoveryController.getBinder().startRecoverySessionWithCertPath( 138 mSessionId, 139 /*rootCertificateAlias=*/ "", // Use the default root cert 140 recoveryCertPath, 141 vaultParams, 142 vaultChallenge, 143 secrets); 144 return recoveryClaim; 145 } catch (RemoteException e) { 146 throw e.rethrowFromSystemServer(); 147 } catch (ServiceSpecificException e) { 148 if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT 149 || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) { 150 throw new CertificateException(e.getMessage()); 151 } 152 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); 153 } 154 } 155 156 /** 157 * Starts a recovery session and returns a blob with proof of recovery secret possession. 158 * The method generates a symmetric key for a session, which trusted remote device can use to 159 * return recovery key. 160 * 161 * @param rootCertificateAlias The alias of the root certificate that is already in the Android 162 * OS. The root certificate will be used for validating {@code verifierCertPath}. 163 * @param verifierCertPath The certificate path used to create the recovery blob on the source 164 * device. Keystore will verify the certificate path by using the root of trust. 165 * @param vaultParams Must match the parameters in the corresponding field in the recovery blob. 166 * Used to limit number of guesses. 167 * @param vaultChallenge Data passed from server for this recovery session and used to prevent 168 * replay attacks. 169 * @param secrets Secrets provided by user, the method only uses type and secret fields. 170 * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is 171 * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric 172 * key and parameters necessary to identify the counter with the number of failed recovery 173 * attempts. 174 * @throws CertificateException if the {@code verifierCertPath} is invalid. 175 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery 176 * service. 177 * 178 * @hide 179 */ 180 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 181 @NonNull public byte[] start( 182 @NonNull String rootCertificateAlias, 183 @NonNull CertPath verifierCertPath, 184 @NonNull byte[] vaultParams, 185 @NonNull byte[] vaultChallenge, 186 @NonNull List<KeyChainProtectionParams> secrets) 187 throws CertificateException, InternalRecoveryServiceException { 188 // Wrap the CertPath in a Parcelable so it can be passed via Binder calls. 189 RecoveryCertPath recoveryCertPath = 190 RecoveryCertPath.createRecoveryCertPath(verifierCertPath); 191 try { 192 byte[] recoveryClaim = 193 mRecoveryController.getBinder().startRecoverySessionWithCertPath( 194 mSessionId, 195 rootCertificateAlias, 196 recoveryCertPath, 197 vaultParams, 198 vaultChallenge, 199 secrets); 200 return recoveryClaim; 201 } catch (RemoteException e) { 202 throw e.rethrowFromSystemServer(); 203 } catch (ServiceSpecificException e) { 204 if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT 205 || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) { 206 throw new CertificateException(e.getMessage()); 207 } 208 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); 209 } 210 } 211 212 /** 213 * Imports keys. 214 * 215 * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session. 216 * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob 217 * and session. KeyStore only uses package names from the application info in {@link 218 * WrappedApplicationKey}. Caller is responsibility to perform certificates check. 219 * @return Map from alias to raw key material. 220 * @throws SessionExpiredException if {@code session} has since been closed. 221 * @throws DecryptionFailedException if unable to decrypt the snapshot. 222 * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service. 223 */ 224 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 225 public Map<String, byte[]> recoverKeys( 226 @NonNull byte[] recoveryKeyBlob, 227 @NonNull List<WrappedApplicationKey> applicationKeys) 228 throws SessionExpiredException, DecryptionFailedException, 229 InternalRecoveryServiceException { 230 try { 231 return (Map<String, byte[]>) mRecoveryController.getBinder().recoverKeys( 232 mSessionId, recoveryKeyBlob, applicationKeys); 233 } catch (RemoteException e) { 234 throw e.rethrowFromSystemServer(); 235 } catch (ServiceSpecificException e) { 236 if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) { 237 throw new DecryptionFailedException(e.getMessage()); 238 } 239 if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) { 240 throw new SessionExpiredException(e.getMessage()); 241 } 242 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); 243 } 244 } 245 246 /** 247 * An internal session ID, used by the framework to match recovery claims to snapshot responses. 248 * 249 * @hide 250 */ 251 String getSessionId() { 252 return mSessionId; 253 } 254 255 /** 256 * Deletes all data associated with {@code session}. Should not be invoked directly but via 257 * {@link RecoverySession#close()}. 258 */ 259 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 260 @Override 261 public void close() { 262 try { 263 mRecoveryController.getBinder().closeSession(mSessionId); 264 } catch (RemoteException | ServiceSpecificException e) { 265 Log.e(TAG, "Unexpected error trying to close session", e); 266 } 267 } 268} 269