RecoverySession.java revision 4a5c87def075c805d4fcae7ff01dd2e78ec27b1a
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 recoveryCertPath, 145 vaultParams, 146 vaultChallenge, 147 secrets); 148 return recoveryClaim; 149 } catch (RemoteException e) { 150 throw e.rethrowFromSystemServer(); 151 } catch (ServiceSpecificException e) { 152 if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT 153 || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) { 154 throw new CertificateException(e.getMessage()); 155 } 156 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); 157 } 158 } 159 160 /** 161 * Imports keys. 162 * 163 * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session. 164 * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob 165 * and session. KeyStore only uses package names from the application info in {@link 166 * WrappedApplicationKey}. Caller is responsibility to perform certificates check. 167 * @return Map from alias to raw key material. 168 * @throws SessionExpiredException if {@code session} has since been closed. 169 * @throws DecryptionFailedException if unable to decrypt the snapshot. 170 * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service. 171 */ 172 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 173 public Map<String, byte[]> recoverKeys( 174 @NonNull byte[] recoveryKeyBlob, 175 @NonNull List<WrappedApplicationKey> applicationKeys) 176 throws SessionExpiredException, DecryptionFailedException, 177 InternalRecoveryServiceException { 178 try { 179 return (Map<String, byte[]>) mRecoveryController.getBinder().recoverKeys( 180 mSessionId, recoveryKeyBlob, applicationKeys); 181 } catch (RemoteException e) { 182 throw e.rethrowFromSystemServer(); 183 } catch (ServiceSpecificException e) { 184 if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) { 185 throw new DecryptionFailedException(e.getMessage()); 186 } 187 if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) { 188 throw new SessionExpiredException(e.getMessage()); 189 } 190 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); 191 } 192 } 193 194 /** 195 * Imports key chain snapshot recovered from a remote vault. 196 * 197 * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session. 198 * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob 199 * and session. 200 * @throws SessionExpiredException if {@code session} has since been closed. 201 * @throws DecryptionFailedException if unable to decrypt the snapshot. 202 * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service. 203 * 204 * @hide 205 */ 206 @RequiresPermission(Manifest.permission.RECOVER_KEYSTORE) 207 public Map<String, Key> recoverKeyChainSnapshot( 208 @NonNull byte[] recoveryKeyBlob, 209 @NonNull List<WrappedApplicationKey> applicationKeys 210 ) throws SessionExpiredException, DecryptionFailedException, InternalRecoveryServiceException { 211 try { 212 Map<String, String> grantAliases = mRecoveryController 213 .getBinder() 214 .recoverKeyChainSnapshot(mSessionId, recoveryKeyBlob, applicationKeys); 215 return getKeysFromGrants(grantAliases); 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 /** Given a map from alias to grant alias, returns a map from alias to a {@link Key} handle. */ 230 private Map<String, Key> getKeysFromGrants(Map<String, String> grantAliases) 231 throws InternalRecoveryServiceException { 232 ArrayMap<String, Key> keysByAlias = new ArrayMap<>(grantAliases.size()); 233 for (String alias : grantAliases.keySet()) { 234 String grantAlias = grantAliases.get(alias); 235 Key key; 236 try { 237 key = mRecoveryController.getKeyFromGrant(grantAlias); 238 } catch (UnrecoverableKeyException e) { 239 throw new InternalRecoveryServiceException( 240 String.format( 241 Locale.US, 242 "Failed to get key '%s' from grant '%s'", 243 alias, 244 grantAlias), e); 245 } 246 keysByAlias.put(alias, key); 247 } 248 return keysByAlias; 249 } 250 251 /** 252 * An internal session ID, used by the framework to match recovery claims to snapshot responses. 253 * 254 * @hide 255 */ 256 String getSessionId() { 257 return mSessionId; 258 } 259 260 /** 261 * Deletes all data associated with {@code session}. Should not be invoked directly but via 262 * {@link RecoverySession#close()}. 263 */ 264 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 265 @Override 266 public void close() { 267 try { 268 mRecoveryController.getBinder().closeSession(mSessionId); 269 } catch (RemoteException | ServiceSpecificException e) { 270 Log.e(TAG, "Unexpected error trying to close session", e); 271 } 272 } 273} 274