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