blob: 2b2438ad2e1123205fdc09dc2b56bf7e37b9c701 [file] [log] [blame]
Robert Berry81ee34b2018-01-23 11:59:59 +00001/*
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
Robert Berry4a5c87d2018-03-19 18:00:46 +000019import android.Manifest;
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080020import android.annotation.NonNull;
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -080021import android.annotation.RequiresPermission;
22import android.annotation.SystemApi;
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080023import android.os.RemoteException;
24import android.os.ServiceSpecificException;
Max Bires13f98ce2018-11-02 10:50:40 -070025import android.security.keystore.KeyPermanentlyInvalidatedException;
Robert Berry4a5c87d2018-03-19 18:00:46 +000026import android.util.ArrayMap;
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080027import android.util.Log;
28
Robert Berry4a5c87d2018-03-19 18:00:46 +000029import java.security.Key;
Robert Berry81ee34b2018-01-23 11:59:59 +000030import java.security.SecureRandom;
Robert Berry4a5c87d2018-03-19 18:00:46 +000031import java.security.UnrecoverableKeyException;
Bo Zhu7c1972f2018-02-22 21:43:52 -080032import java.security.cert.CertPath;
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080033import java.security.cert.CertificateException;
34import java.util.List;
Robert Berry4a5c87d2018-03-19 18:00:46 +000035import java.util.Locale;
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080036import java.util.Map;
Robert Berry81ee34b2018-01-23 11:59:59 +000037
38/**
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080039 * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a
Robert Berry81ee34b2018-01-23 11:59:59 +000040 * recovery agent.
41 *
42 * @hide
43 */
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -080044@SystemApi
Robert Berry81ee34b2018-01-23 11:59:59 +000045public class RecoverySession implements AutoCloseable {
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080046 private static final String TAG = "RecoverySession";
Robert Berry81ee34b2018-01-23 11:59:59 +000047
48 private static final int SESSION_ID_LENGTH_BYTES = 16;
49
50 private final String mSessionId;
51 private final RecoveryController mRecoveryController;
52
Dmitry Dementyev1e6a9dc2018-03-21 13:52:00 -070053 private RecoverySession(@NonNull RecoveryController recoveryController,
54 @NonNull String sessionId) {
Robert Berry81ee34b2018-01-23 11:59:59 +000055 mRecoveryController = recoveryController;
56 mSessionId = sessionId;
57 }
58
59 /**
Robert Berrybeafcb52018-02-26 19:00:29 +000060 * A new session, started by the {@link RecoveryController}.
Robert Berry81ee34b2018-01-23 11:59:59 +000061 */
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -080062 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
Dmitry Dementyev1e6a9dc2018-03-21 13:52:00 -070063 static @NonNull RecoverySession newInstance(RecoveryController recoveryController) {
Robert Berry81ee34b2018-01-23 11:59:59 +000064 return new RecoverySession(recoveryController, newSessionId());
65 }
66
67 /**
68 * Returns a new random session ID.
69 */
Dmitry Dementyev1e6a9dc2018-03-21 13:52:00 -070070 private static @NonNull String newSessionId() {
Robert Berry81ee34b2018-01-23 11:59:59 +000071 SecureRandom secureRandom = new SecureRandom();
72 byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
73 secureRandom.nextBytes(sessionId);
74 StringBuilder sb = new StringBuilder();
75 for (byte b : sessionId) {
76 sb.append(Byte.toHexString(b, /*upperCase=*/ false));
77 }
78 return sb.toString();
79 }
80
81 /**
Bo Zhub31ab672018-03-20 22:44:18 -070082 * Starts a recovery session and returns a blob with proof of recovery secret possession.
83 * The method generates a symmetric key for a session, which trusted remote device can use to
84 * return recovery key.
85 *
86 * @param rootCertificateAlias The alias of the root certificate that is already in the Android
87 * OS. The root certificate will be used for validating {@code verifierCertPath}.
88 * @param verifierCertPath The certificate path used to create the recovery blob on the source
89 * device. Keystore will verify the certificate path by using the root of trust.
90 * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
91 * Used to limit number of guesses.
92 * @param vaultChallenge Data passed from server for this recovery session and used to prevent
93 * replay attacks.
94 * @param secrets Secrets provided by user, the method only uses type and secret fields.
Dmitry Dementyev86f5bb12018-03-27 16:58:50 -070095 * @return The binary blob with recovery claim. It is encrypted with verifierPublicKey
96 * and contains a proof of user secrets possession, session symmetric
Bo Zhub31ab672018-03-20 22:44:18 -070097 * key and parameters necessary to identify the counter with the number of failed recovery
98 * attempts.
99 * @throws CertificateException if the {@code verifierCertPath} is invalid.
100 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
101 * service.
Bo Zhub31ab672018-03-20 22:44:18 -0700102 */
103 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
104 @NonNull public byte[] start(
105 @NonNull String rootCertificateAlias,
106 @NonNull CertPath verifierCertPath,
107 @NonNull byte[] vaultParams,
108 @NonNull byte[] vaultChallenge,
109 @NonNull List<KeyChainProtectionParams> secrets)
110 throws CertificateException, InternalRecoveryServiceException {
111 // Wrap the CertPath in a Parcelable so it can be passed via Binder calls.
112 RecoveryCertPath recoveryCertPath =
113 RecoveryCertPath.createRecoveryCertPath(verifierCertPath);
114 try {
115 byte[] recoveryClaim =
116 mRecoveryController.getBinder().startRecoverySessionWithCertPath(
117 mSessionId,
118 rootCertificateAlias,
Bo Zhu7c1972f2018-02-22 21:43:52 -0800119 recoveryCertPath,
120 vaultParams,
121 vaultChallenge,
122 secrets);
123 return recoveryClaim;
124 } catch (RemoteException e) {
125 throw e.rethrowFromSystemServer();
126 } catch (ServiceSpecificException e) {
Bo Zhu7f414d92018-02-28 09:28:19 -0800127 if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT
128 || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) {
Bo Zhu41d2dd22018-03-30 12:20:06 -0700129 throw new CertificateException("Invalid certificate for recovery session", e);
Bo Zhu7c1972f2018-02-22 21:43:52 -0800130 }
131 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
132 }
133 }
134
135 /**
Robert Berry4a5c87d2018-03-19 18:00:46 +0000136 * Imports key chain snapshot recovered from a remote vault.
137 *
138 * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
139 * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
Dmitry Dementyev86f5bb12018-03-27 16:58:50 -0700140 * and session key generated by {@link #start}.
141 * @return {@code Map} from recovered keys aliases to their references.
Robert Berry4a5c87d2018-03-19 18:00:46 +0000142 * @throws SessionExpiredException if {@code session} has since been closed.
143 * @throws DecryptionFailedException if unable to decrypt the snapshot.
144 * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
Robert Berry4a5c87d2018-03-19 18:00:46 +0000145 */
146 @RequiresPermission(Manifest.permission.RECOVER_KEYSTORE)
Dmitry Dementyevfd4ae0b2018-03-23 11:06:24 -0700147 @NonNull public Map<String, Key> recoverKeyChainSnapshot(
Robert Berry4a5c87d2018-03-19 18:00:46 +0000148 @NonNull byte[] recoveryKeyBlob,
149 @NonNull List<WrappedApplicationKey> applicationKeys
150 ) throws SessionExpiredException, DecryptionFailedException, InternalRecoveryServiceException {
151 try {
152 Map<String, String> grantAliases = mRecoveryController
153 .getBinder()
154 .recoverKeyChainSnapshot(mSessionId, recoveryKeyBlob, applicationKeys);
155 return getKeysFromGrants(grantAliases);
156 } catch (RemoteException e) {
157 throw e.rethrowFromSystemServer();
158 } catch (ServiceSpecificException e) {
159 if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) {
160 throw new DecryptionFailedException(e.getMessage());
161 }
162 if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) {
163 throw new SessionExpiredException(e.getMessage());
164 }
165 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
166 }
167 }
168
169 /** Given a map from alias to grant alias, returns a map from alias to a {@link Key} handle. */
Dmitry Dementyev0bbaf182018-03-23 17:36:58 -0700170 private @NonNull Map<String, Key> getKeysFromGrants(@NonNull Map<String, String> grantAliases)
Robert Berry4a5c87d2018-03-19 18:00:46 +0000171 throws InternalRecoveryServiceException {
172 ArrayMap<String, Key> keysByAlias = new ArrayMap<>(grantAliases.size());
173 for (String alias : grantAliases.keySet()) {
174 String grantAlias = grantAliases.get(alias);
175 Key key;
176 try {
177 key = mRecoveryController.getKeyFromGrant(grantAlias);
Max Bires13f98ce2018-11-02 10:50:40 -0700178 } catch (KeyPermanentlyInvalidatedException | UnrecoverableKeyException e) {
Robert Berry4a5c87d2018-03-19 18:00:46 +0000179 throw new InternalRecoveryServiceException(
180 String.format(
181 Locale.US,
182 "Failed to get key '%s' from grant '%s'",
183 alias,
184 grantAlias), e);
185 }
186 keysByAlias.put(alias, key);
187 }
188 return keysByAlias;
189 }
190
191 /**
Robert Berry81ee34b2018-01-23 11:59:59 +0000192 * An internal session ID, used by the framework to match recovery claims to snapshot responses.
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800193 *
194 * @hide
Robert Berry81ee34b2018-01-23 11:59:59 +0000195 */
196 String getSessionId() {
197 return mSessionId;
198 }
199
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800200 /**
Dmitry Dementyev86f5bb12018-03-27 16:58:50 -0700201 * Deletes all data associated with {@code session}.
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800202 */
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -0800203 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
Robert Berry81ee34b2018-01-23 11:59:59 +0000204 @Override
205 public void close() {
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800206 try {
207 mRecoveryController.getBinder().closeSession(mSessionId);
208 } catch (RemoteException | ServiceSpecificException e) {
209 Log.e(TAG, "Unexpected error trying to close session", e);
210 }
Robert Berry81ee34b2018-01-23 11:59:59 +0000211 }
212}