RecoverySession.java revision 5af199c56e2723f0eb98027644a1e3486f22ef18
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.annotation.NonNull; 20import android.annotation.RequiresPermission; 21import android.annotation.SystemApi; 22import android.os.RemoteException; 23import android.os.ServiceSpecificException; 24import android.util.Log; 25 26import java.security.SecureRandom; 27import java.security.cert.CertPath; 28import java.security.cert.CertificateException; 29import java.util.List; 30import java.util.Map; 31 32/** 33 * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a 34 * recovery agent. 35 * 36 * @hide 37 */ 38@SystemApi 39public class RecoverySession implements AutoCloseable { 40 private static final String TAG = "RecoverySession"; 41 42 private static final int SESSION_ID_LENGTH_BYTES = 16; 43 44 private final String mSessionId; 45 private final RecoveryController mRecoveryController; 46 47 private RecoverySession(RecoveryController recoveryController, String sessionId) { 48 mRecoveryController = recoveryController; 49 mSessionId = sessionId; 50 } 51 52 /** 53 * A new session, started by {@code recoveryManager}. 54 */ 55 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 56 static RecoverySession newInstance(RecoveryController recoveryController) { 57 return new RecoverySession(recoveryController, newSessionId()); 58 } 59 60 /** 61 * Returns a new random session ID. 62 */ 63 private static String newSessionId() { 64 SecureRandom secureRandom = new SecureRandom(); 65 byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES]; 66 secureRandom.nextBytes(sessionId); 67 StringBuilder sb = new StringBuilder(); 68 for (byte b : sessionId) { 69 sb.append(Byte.toHexString(b, /*upperCase=*/ false)); 70 } 71 return sb.toString(); 72 } 73 74 /** 75 * Starts a recovery session and returns a blob with proof of recovery secret possession. 76 * The method generates a symmetric key for a session, which trusted remote device can use to 77 * return recovery key. 78 * 79 * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key 80 * used to create the recovery blob on the source device. 81 * Keystore will verify the certificate using root of trust. 82 * @param vaultParams Must match the parameters in the corresponding field in the recovery blob. 83 * Used to limit number of guesses. 84 * @param vaultChallenge Data passed from server for this recovery session and used to prevent 85 * replay attacks 86 * @param secrets Secrets provided by user, the method only uses type and secret fields. 87 * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is 88 * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric 89 * key and parameters necessary to identify the counter with the number of failed recovery 90 * attempts. 91 * @throws CertificateException if the {@code verifierPublicKey} is in an incorrect 92 * format. 93 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery 94 * service. 95 * @deprecated Use {@link #start(CertPath, byte[], byte[], List)} instead. 96 */ 97 @Deprecated 98 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 99 @NonNull public byte[] start( 100 @NonNull byte[] verifierPublicKey, 101 @NonNull byte[] vaultParams, 102 @NonNull byte[] vaultChallenge, 103 @NonNull List<KeyChainProtectionParams> secrets) 104 throws CertificateException, InternalRecoveryServiceException { 105 try { 106 byte[] recoveryClaim = 107 mRecoveryController.getBinder().startRecoverySession( 108 mSessionId, 109 verifierPublicKey, 110 vaultParams, 111 vaultChallenge, 112 secrets); 113 return recoveryClaim; 114 } catch (RemoteException e) { 115 throw e.rethrowFromSystemServer(); 116 } catch (ServiceSpecificException e) { 117 if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT) { 118 throw new CertificateException(e.getMessage()); 119 } 120 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); 121 } 122 } 123 124 /** 125 * Starts a recovery session and returns a blob with proof of recovery secret possession. 126 * The method generates a symmetric key for a session, which trusted remote device can use to 127 * return recovery key. 128 * 129 * @param verifierCertPath The certificate path used to create the recovery blob on the source 130 * device. Keystore will verify the certificate path by using the root of trust. 131 * @param vaultParams Must match the parameters in the corresponding field in the recovery blob. 132 * Used to limit number of guesses. 133 * @param vaultChallenge Data passed from server for this recovery session and used to prevent 134 * replay attacks 135 * @param secrets Secrets provided by user, the method only uses type and secret fields. 136 * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is 137 * encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric 138 * key and parameters necessary to identify the counter with the number of failed recovery 139 * attempts. 140 * @throws CertificateException if the {@code verifierCertPath} is invalid. 141 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery 142 * service. 143 */ 144 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 145 @NonNull public byte[] start( 146 @NonNull CertPath verifierCertPath, 147 @NonNull byte[] vaultParams, 148 @NonNull byte[] vaultChallenge, 149 @NonNull List<KeyChainProtectionParams> secrets) 150 throws CertificateException, InternalRecoveryServiceException { 151 // Wrap the CertPath in a Parcelable so it can be passed via Binder calls. 152 RecoveryCertPath recoveryCertPath = 153 RecoveryCertPath.createRecoveryCertPath(verifierCertPath); 154 try { 155 byte[] recoveryClaim = 156 mRecoveryController.getBinder().startRecoverySessionWithCertPath( 157 mSessionId, 158 recoveryCertPath, 159 vaultParams, 160 vaultChallenge, 161 secrets); 162 return recoveryClaim; 163 } catch (RemoteException e) { 164 throw e.rethrowFromSystemServer(); 165 } catch (ServiceSpecificException e) { 166 if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT) { 167 throw new CertificateException(e.getMessage()); 168 } 169 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); 170 } 171 } 172 173 /** 174 * Imports keys. 175 * 176 * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session. 177 * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob 178 * and session. KeyStore only uses package names from the application info in {@link 179 * WrappedApplicationKey}. Caller is responsibility to perform certificates check. 180 * @return Map from alias to raw key material. 181 * @throws SessionExpiredException if {@code session} has since been closed. 182 * @throws DecryptionFailedException if unable to decrypt the snapshot. 183 * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service. 184 */ 185 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 186 public Map<String, byte[]> recoverKeys( 187 @NonNull byte[] recoveryKeyBlob, 188 @NonNull List<WrappedApplicationKey> applicationKeys) 189 throws SessionExpiredException, DecryptionFailedException, 190 InternalRecoveryServiceException { 191 try { 192 return (Map<String, byte[]>) mRecoveryController.getBinder().recoverKeys( 193 mSessionId, recoveryKeyBlob, applicationKeys); 194 } catch (RemoteException e) { 195 throw e.rethrowFromSystemServer(); 196 } catch (ServiceSpecificException e) { 197 if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) { 198 throw new DecryptionFailedException(e.getMessage()); 199 } 200 if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) { 201 throw new SessionExpiredException(e.getMessage()); 202 } 203 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e); 204 } 205 } 206 207 /** 208 * An internal session ID, used by the framework to match recovery claims to snapshot responses. 209 * 210 * @hide 211 */ 212 String getSessionId() { 213 return mSessionId; 214 } 215 216 /** 217 * Deletes all data associated with {@code session}. Should not be invoked directly but via 218 * {@link RecoverySession#close()}. 219 */ 220 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE) 221 @Override 222 public void close() { 223 try { 224 mRecoveryController.getBinder().closeSession(mSessionId); 225 } catch (RemoteException | ServiceSpecificException e) { 226 Log.e(TAG, "Unexpected error trying to close session", e); 227 } 228 } 229} 230