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