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