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