blob: dc2961b303d3a07bcbe97f78ae02be26ee00be41 [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;
Robert Berry4a5c87d2018-03-19 18:00:46 +000025import android.util.ArrayMap;
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080026import android.util.Log;
27
Robert Berry4a5c87d2018-03-19 18:00:46 +000028import java.security.Key;
Robert Berry81ee34b2018-01-23 11:59:59 +000029import java.security.SecureRandom;
Robert Berry4a5c87d2018-03-19 18:00:46 +000030import java.security.UnrecoverableKeyException;
Bo Zhu7c1972f2018-02-22 21:43:52 -080031import java.security.cert.CertPath;
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080032import java.security.cert.CertificateException;
33import java.util.List;
Robert Berry4a5c87d2018-03-19 18:00:46 +000034import java.util.Locale;
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080035import java.util.Map;
Robert Berry81ee34b2018-01-23 11:59:59 +000036
37/**
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080038 * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a
Robert Berry81ee34b2018-01-23 11:59:59 +000039 * recovery agent.
40 *
41 * @hide
42 */
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -080043@SystemApi
Robert Berry81ee34b2018-01-23 11:59:59 +000044public class RecoverySession implements AutoCloseable {
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080045 private static final String TAG = "RecoverySession";
Robert Berry81ee34b2018-01-23 11:59:59 +000046
47 private static final int SESSION_ID_LENGTH_BYTES = 16;
48
49 private final String mSessionId;
50 private final RecoveryController mRecoveryController;
51
Dmitry Dementyev1e6a9dc2018-03-21 13:52:00 -070052 private RecoverySession(@NonNull RecoveryController recoveryController,
53 @NonNull String sessionId) {
Robert Berry81ee34b2018-01-23 11:59:59 +000054 mRecoveryController = recoveryController;
55 mSessionId = sessionId;
56 }
57
58 /**
Robert Berrybeafcb52018-02-26 19:00:29 +000059 * A new session, started by the {@link RecoveryController}.
Robert Berry81ee34b2018-01-23 11:59:59 +000060 */
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -080061 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
Dmitry Dementyev1e6a9dc2018-03-21 13:52:00 -070062 static @NonNull RecoverySession newInstance(RecoveryController recoveryController) {
Robert Berry81ee34b2018-01-23 11:59:59 +000063 return new RecoverySession(recoveryController, newSessionId());
64 }
65
66 /**
67 * Returns a new random session ID.
68 */
Dmitry Dementyev1e6a9dc2018-03-21 13:52:00 -070069 private static @NonNull String newSessionId() {
Robert Berry81ee34b2018-01-23 11:59:59 +000070 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 /**
Robert Berryc157e212018-04-06 10:13:16 +000081 * @deprecated Use {@link #start(String, CertPath, byte[], byte[], List)} instead.
Jeff Sharkey3990ee12018-04-11 10:19:55 -060082 * @removed
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080083 */
Bo Zhu7c1972f2018-02-22 21:43:52 -080084 @Deprecated
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -080085 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080086 @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 try {
93 byte[] recoveryClaim =
94 mRecoveryController.getBinder().startRecoverySession(
95 mSessionId,
96 verifierPublicKey,
97 vaultParams,
98 vaultChallenge,
99 secrets);
100 return recoveryClaim;
101 } catch (RemoteException e) {
102 throw e.rethrowFromSystemServer();
103 } catch (ServiceSpecificException e) {
Bo Zhu7f414d92018-02-28 09:28:19 -0800104 if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT
105 || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) {
Bo Zhu41d2dd22018-03-30 12:20:06 -0700106 throw new CertificateException("Invalid certificate for recovery session", e);
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800107 }
108 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
109 }
110 }
111
112 /**
Robert Berryc157e212018-04-06 10:13:16 +0000113 * @deprecated Use {@link #start(String, CertPath, byte[], byte[], List)} instead.
Jeff Sharkey3990ee12018-04-11 10:19:55 -0600114 * @removed
Bo Zhu7c1972f2018-02-22 21:43:52 -0800115 */
Bo Zhue7997a32018-03-21 19:50:50 -0700116 @Deprecated
Bo Zhu7c1972f2018-02-22 21:43:52 -0800117 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
118 @NonNull public byte[] start(
119 @NonNull CertPath verifierCertPath,
120 @NonNull byte[] vaultParams,
121 @NonNull byte[] vaultChallenge,
122 @NonNull List<KeyChainProtectionParams> secrets)
123 throws CertificateException, InternalRecoveryServiceException {
124 // Wrap the CertPath in a Parcelable so it can be passed via Binder calls.
125 RecoveryCertPath recoveryCertPath =
126 RecoveryCertPath.createRecoveryCertPath(verifierCertPath);
127 try {
128 byte[] recoveryClaim =
129 mRecoveryController.getBinder().startRecoverySessionWithCertPath(
130 mSessionId,
Bo Zhub31ab672018-03-20 22:44:18 -0700131 /*rootCertificateAlias=*/ "", // Use the default root cert
132 recoveryCertPath,
133 vaultParams,
134 vaultChallenge,
135 secrets);
136 return recoveryClaim;
137 } catch (RemoteException e) {
138 throw e.rethrowFromSystemServer();
139 } catch (ServiceSpecificException e) {
140 if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT
141 || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) {
Bo Zhu41d2dd22018-03-30 12:20:06 -0700142 throw new CertificateException("Invalid certificate for recovery session", e);
Bo Zhub31ab672018-03-20 22:44:18 -0700143 }
144 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
145 }
146 }
147
148 /**
149 * Starts a recovery session and returns a blob with proof of recovery secret possession.
150 * The method generates a symmetric key for a session, which trusted remote device can use to
151 * return recovery key.
152 *
153 * @param rootCertificateAlias The alias of the root certificate that is already in the Android
154 * OS. The root certificate will be used for validating {@code verifierCertPath}.
155 * @param verifierCertPath The certificate path used to create the recovery blob on the source
156 * device. Keystore will verify the certificate path by using the root of trust.
157 * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
158 * Used to limit number of guesses.
159 * @param vaultChallenge Data passed from server for this recovery session and used to prevent
160 * replay attacks.
161 * @param secrets Secrets provided by user, the method only uses type and secret fields.
Dmitry Dementyev86f5bb12018-03-27 16:58:50 -0700162 * @return The binary blob with recovery claim. It is encrypted with verifierPublicKey
163 * and contains a proof of user secrets possession, session symmetric
Bo Zhub31ab672018-03-20 22:44:18 -0700164 * key and parameters necessary to identify the counter with the number of failed recovery
165 * attempts.
166 * @throws CertificateException if the {@code verifierCertPath} is invalid.
167 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
168 * service.
Bo Zhub31ab672018-03-20 22:44:18 -0700169 */
170 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
171 @NonNull public byte[] start(
172 @NonNull String rootCertificateAlias,
173 @NonNull CertPath verifierCertPath,
174 @NonNull byte[] vaultParams,
175 @NonNull byte[] vaultChallenge,
176 @NonNull List<KeyChainProtectionParams> secrets)
177 throws CertificateException, InternalRecoveryServiceException {
178 // Wrap the CertPath in a Parcelable so it can be passed via Binder calls.
179 RecoveryCertPath recoveryCertPath =
180 RecoveryCertPath.createRecoveryCertPath(verifierCertPath);
181 try {
182 byte[] recoveryClaim =
183 mRecoveryController.getBinder().startRecoverySessionWithCertPath(
184 mSessionId,
185 rootCertificateAlias,
Bo Zhu7c1972f2018-02-22 21:43:52 -0800186 recoveryCertPath,
187 vaultParams,
188 vaultChallenge,
189 secrets);
190 return recoveryClaim;
191 } catch (RemoteException e) {
192 throw e.rethrowFromSystemServer();
193 } catch (ServiceSpecificException e) {
Bo Zhu7f414d92018-02-28 09:28:19 -0800194 if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT
195 || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) {
Bo Zhu41d2dd22018-03-30 12:20:06 -0700196 throw new CertificateException("Invalid certificate for recovery session", e);
Bo Zhu7c1972f2018-02-22 21:43:52 -0800197 }
198 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
199 }
200 }
201
202 /**
Robert Berryc157e212018-04-06 10:13:16 +0000203 * @deprecated Use {@link #recoverKeyChainSnapshot(byte[], List)} instead.
Jeff Sharkey3990ee12018-04-11 10:19:55 -0600204 * @removed
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800205 */
Robert Berry750b71c2018-03-21 16:31:52 +0000206 @Deprecated
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -0800207 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800208 public Map<String, byte[]> recoverKeys(
209 @NonNull byte[] recoveryKeyBlob,
210 @NonNull List<WrappedApplicationKey> applicationKeys)
211 throws SessionExpiredException, DecryptionFailedException,
212 InternalRecoveryServiceException {
213 try {
214 return (Map<String, byte[]>) mRecoveryController.getBinder().recoverKeys(
215 mSessionId, recoveryKeyBlob, applicationKeys);
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 /**
Robert Berry4a5c87d2018-03-19 18:00:46 +0000230 * Imports key chain snapshot recovered from a remote vault.
231 *
232 * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
233 * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
Dmitry Dementyev86f5bb12018-03-27 16:58:50 -0700234 * and session key generated by {@link #start}.
235 * @return {@code Map} from recovered keys aliases to their references.
Robert Berry4a5c87d2018-03-19 18:00:46 +0000236 * @throws SessionExpiredException if {@code session} has since been closed.
237 * @throws DecryptionFailedException if unable to decrypt the snapshot.
238 * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
Robert Berry4a5c87d2018-03-19 18:00:46 +0000239 */
240 @RequiresPermission(Manifest.permission.RECOVER_KEYSTORE)
Dmitry Dementyevfd4ae0b2018-03-23 11:06:24 -0700241 @NonNull public Map<String, Key> recoverKeyChainSnapshot(
Robert Berry4a5c87d2018-03-19 18:00:46 +0000242 @NonNull byte[] recoveryKeyBlob,
243 @NonNull List<WrappedApplicationKey> applicationKeys
244 ) throws SessionExpiredException, DecryptionFailedException, InternalRecoveryServiceException {
245 try {
246 Map<String, String> grantAliases = mRecoveryController
247 .getBinder()
248 .recoverKeyChainSnapshot(mSessionId, recoveryKeyBlob, applicationKeys);
249 return getKeysFromGrants(grantAliases);
250 } catch (RemoteException e) {
251 throw e.rethrowFromSystemServer();
252 } catch (ServiceSpecificException e) {
253 if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) {
254 throw new DecryptionFailedException(e.getMessage());
255 }
256 if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) {
257 throw new SessionExpiredException(e.getMessage());
258 }
259 throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
260 }
261 }
262
263 /** 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 -0700264 private @NonNull Map<String, Key> getKeysFromGrants(@NonNull Map<String, String> grantAliases)
Robert Berry4a5c87d2018-03-19 18:00:46 +0000265 throws InternalRecoveryServiceException {
266 ArrayMap<String, Key> keysByAlias = new ArrayMap<>(grantAliases.size());
267 for (String alias : grantAliases.keySet()) {
268 String grantAlias = grantAliases.get(alias);
269 Key key;
270 try {
271 key = mRecoveryController.getKeyFromGrant(grantAlias);
272 } catch (UnrecoverableKeyException e) {
273 throw new InternalRecoveryServiceException(
274 String.format(
275 Locale.US,
276 "Failed to get key '%s' from grant '%s'",
277 alias,
278 grantAlias), e);
279 }
280 keysByAlias.put(alias, key);
281 }
282 return keysByAlias;
283 }
284
285 /**
Robert Berry81ee34b2018-01-23 11:59:59 +0000286 * An internal session ID, used by the framework to match recovery claims to snapshot responses.
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800287 *
288 * @hide
Robert Berry81ee34b2018-01-23 11:59:59 +0000289 */
290 String getSessionId() {
291 return mSessionId;
292 }
293
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800294 /**
Dmitry Dementyev86f5bb12018-03-27 16:58:50 -0700295 * Deletes all data associated with {@code session}.
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800296 */
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -0800297 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
Robert Berry81ee34b2018-01-23 11:59:59 +0000298 @Override
299 public void close() {
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800300 try {
301 mRecoveryController.getBinder().closeSession(mSessionId);
302 } catch (RemoteException | ServiceSpecificException e) {
303 Log.e(TAG, "Unexpected error trying to close session", e);
304 }
Robert Berry81ee34b2018-01-23 11:59:59 +0000305 }
306}