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