blob: 71a36f19a3609eb08d057ef84706fd262ec08003 [file] [log] [blame]
Robert Berry81ee34b2018-01-23 11:59:59 +00001/*
2 * Copyright (C) 2017 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.Nullable;
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080021import android.annotation.RequiresPermission;
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -080022import android.annotation.SystemApi;
Robert Berry81ee34b2018-01-23 11:59:59 +000023import android.app.PendingIntent;
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080024import android.content.Context;
Robert Berry81ee34b2018-01-23 11:59:59 +000025import android.content.pm.PackageManager.NameNotFoundException;
26import android.os.RemoteException;
27import android.os.ServiceManager;
28import android.os.ServiceSpecificException;
Robert Berry81ee34b2018-01-23 11:59:59 +000029
30import com.android.internal.widget.ILockSettings;
31
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -080032import java.security.cert.CertificateException;
33import java.util.ArrayList;
Robert Berry81ee34b2018-01-23 11:59:59 +000034import java.util.List;
35import java.util.Map;
36
37/**
38 * An assistant for generating {@link javax.crypto.SecretKey} instances that can be recovered by
39 * other Android devices belonging to the user. The exported keychain is protected by the user's
40 * lock screen.
41 *
42 * <p>The RecoveryController must be paired with a recovery agent. The recovery agent is responsible
43 * for transporting the keychain to remote trusted hardware. This hardware must prevent brute force
44 * attempts against the user's lock screen by limiting the number of allowed guesses (to, e.g., 10).
45 * After that number of incorrect guesses, the trusted hardware no longer allows access to the
46 * key chain.
47 *
48 * <p>For now only the recovery agent itself is able to create keys, so it is expected that the
49 * recovery agent is itself the system app.
50 *
51 * <p>A recovery agent requires the privileged permission
52 * {@code android.Manifest.permission#RECOVER_KEYSTORE}.
53 *
54 * @hide
55 */
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -080056@SystemApi
Robert Berry81ee34b2018-01-23 11:59:59 +000057public class RecoveryController {
58 private static final String TAG = "RecoveryController";
59
60 /** Key has been successfully synced. */
61 public static final int RECOVERY_STATUS_SYNCED = 0;
62 /** Waiting for recovery agent to sync the key. */
63 public static final int RECOVERY_STATUS_SYNC_IN_PROGRESS = 1;
64 /** Recovery account is not available. */
65 public static final int RECOVERY_STATUS_MISSING_ACCOUNT = 2;
66 /** Key cannot be synced. */
67 public static final int RECOVERY_STATUS_PERMANENT_FAILURE = 3;
68
69 /**
70 * Failed because no snapshot is yet pending to be synced for the user.
71 *
72 * @hide
73 */
74 public static final int ERROR_NO_SNAPSHOT_PENDING = 21;
75
76 /**
77 * Failed due to an error internal to the recovery service. This is unexpected and indicates
78 * either a problem with the logic in the service, or a problem with a dependency of the
79 * service (such as AndroidKeyStore).
80 *
81 * @hide
82 */
83 public static final int ERROR_SERVICE_INTERNAL_ERROR = 22;
84
85 /**
86 * Failed because the user does not have a lock screen set.
87 *
88 * @hide
89 */
90 public static final int ERROR_INSECURE_USER = 23;
91
92 /**
93 * Error thrown when attempting to use a recovery session that has since been closed.
94 *
95 * @hide
96 */
97 public static final int ERROR_SESSION_EXPIRED = 24;
98
99 /**
100 * Failed because the provided certificate was not a valid X509 certificate.
101 *
102 * @hide
103 */
104 public static final int ERROR_BAD_CERTIFICATE_FORMAT = 25;
105
106 /**
107 * Error thrown if decryption failed. This might be because the tag is wrong, the key is wrong,
108 * the data has become corrupted, the data has been tampered with, etc.
109 *
110 * @hide
111 */
112 public static final int ERROR_DECRYPTION_FAILED = 26;
113
114
115 private final ILockSettings mBinder;
116
117 private RecoveryController(ILockSettings binder) {
118 mBinder = binder;
119 }
120
121 /**
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800122 * Internal method used by {@code RecoverySession}.
123 *
124 * @hide
125 */
126 ILockSettings getBinder() {
127 return mBinder;
128 }
129
130 /**
Robert Berry81ee34b2018-01-23 11:59:59 +0000131 * Gets a new instance of the class.
132 */
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800133 public static RecoveryController getInstance(Context context) {
Robert Berry81ee34b2018-01-23 11:59:59 +0000134 ILockSettings lockSettings =
135 ILockSettings.Stub.asInterface(ServiceManager.getService("lock_settings"));
136 return new RecoveryController(lockSettings);
137 }
138
139 /**
140 * Initializes key recovery service for the calling application. RecoveryController
141 * randomly chooses one of the keys from the list and keeps it to use for future key export
142 * operations. Collection of all keys in the list must be signed by the provided {@code
143 * rootCertificateAlias}, which must also be present in the list of root certificates
144 * preinstalled on the device. The random selection allows RecoveryController to select
145 * which of a set of remote recovery service devices will be used.
146 *
147 * <p>In addition, RecoveryController enforces a delay of three months between
148 * consecutive initialization attempts, to limit the ability of an attacker to often switch
149 * remote recovery devices and significantly increase number of recovery attempts.
150 *
151 * @param rootCertificateAlias alias of a root certificate preinstalled on the device
152 * @param signedPublicKeyList binary blob a list of X509 certificates and signature
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800153 * @throws CertificateException if the {@code signedPublicKeyList} is in a bad format.
Robert Berry81ee34b2018-01-23 11:59:59 +0000154 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
155 * service.
156 */
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800157 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
Robert Berry81ee34b2018-01-23 11:59:59 +0000158 public void initRecoveryService(
159 @NonNull String rootCertificateAlias, @NonNull byte[] signedPublicKeyList)
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800160 throws CertificateException, InternalRecoveryServiceException {
Robert Berry81ee34b2018-01-23 11:59:59 +0000161 try {
162 mBinder.initRecoveryService(rootCertificateAlias, signedPublicKeyList);
163 } catch (RemoteException e) {
164 throw e.rethrowFromSystemServer();
165 } catch (ServiceSpecificException e) {
166 if (e.errorCode == ERROR_BAD_CERTIFICATE_FORMAT) {
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800167 throw new CertificateException(e.getMessage());
Robert Berry81ee34b2018-01-23 11:59:59 +0000168 }
169 throw wrapUnexpectedServiceSpecificException(e);
170 }
171 }
172
173 /**
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800174 * Returns data necessary to store all recoverable keys. Key material is
Robert Berry81ee34b2018-01-23 11:59:59 +0000175 * encrypted with user secret and recovery public key.
176 *
Robert Berry81ee34b2018-01-23 11:59:59 +0000177 * @return Data necessary to recover keystore.
178 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
179 * service.
180 */
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800181 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
182 public @NonNull KeyChainSnapshot getRecoveryData()
Robert Berry81ee34b2018-01-23 11:59:59 +0000183 throws InternalRecoveryServiceException {
184 try {
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800185 return mBinder.getRecoveryData(/*account=*/ new byte[]{});
Robert Berry81ee34b2018-01-23 11:59:59 +0000186 } catch (RemoteException e) {
187 throw e.rethrowFromSystemServer();
188 } catch (ServiceSpecificException e) {
189 if (e.errorCode == ERROR_NO_SNAPSHOT_PENDING) {
190 return null;
191 }
192 throw wrapUnexpectedServiceSpecificException(e);
193 }
194 }
195
196 /**
197 * Sets a listener which notifies recovery agent that new recovery snapshot is available. {@link
198 * #getRecoveryData} can be used to get the snapshot. Note that every recovery agent can have at
199 * most one registered listener at any time.
200 *
201 * @param intent triggered when new snapshot is available. Unregisters listener if the value is
202 * {@code null}.
203 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
204 * service.
205 */
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800206 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
Robert Berry81ee34b2018-01-23 11:59:59 +0000207 public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
208 throws InternalRecoveryServiceException {
209 try {
210 mBinder.setSnapshotCreatedPendingIntent(intent);
211 } catch (RemoteException e) {
212 throw e.rethrowFromSystemServer();
213 } catch (ServiceSpecificException e) {
214 throw wrapUnexpectedServiceSpecificException(e);
215 }
216 }
217
218 /**
Robert Berry81ee34b2018-01-23 11:59:59 +0000219 * Server parameters used to generate new recovery key blobs. This value will be included in
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800220 * {@code KeyChainSnapshot.getEncryptedRecoveryKeyBlob()}. The same value must be included
Robert Berry81ee34b2018-01-23 11:59:59 +0000221 * in vaultParams {@link #startRecoverySession}
222 *
223 * @param serverParams included in recovery key blob.
224 * @see #getRecoveryData
225 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
226 * service.
227 */
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800228 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
Robert Berry81ee34b2018-01-23 11:59:59 +0000229 public void setServerParams(byte[] serverParams) throws InternalRecoveryServiceException {
230 try {
231 mBinder.setServerParams(serverParams);
232 } catch (RemoteException e) {
233 throw e.rethrowFromSystemServer();
234 } catch (ServiceSpecificException e) {
235 throw wrapUnexpectedServiceSpecificException(e);
236 }
237 }
238
239 /**
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800240 * Gets aliases of recoverable keys for the application.
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -0800241 *
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800242 * @param packageName which recoverable keys' aliases will be returned.
243 *
244 * @return {@code List} of all aliases.
245 */
246 public List<String> getAliases(@Nullable String packageName)
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -0800247 throws InternalRecoveryServiceException {
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800248 try {
249 // TODO: update aidl
250 Map<String, Integer> allStatuses = mBinder.getRecoveryStatus(packageName);
251 return new ArrayList<>(allStatuses.keySet());
252 } catch (RemoteException e) {
253 throw e.rethrowFromSystemServer();
254 } catch (ServiceSpecificException e) {
255 throw wrapUnexpectedServiceSpecificException(e);
256 }
257 }
258
259 /**
260 * Updates recovery status for given key. It is used to notify keystore that key was
Robert Berry81ee34b2018-01-23 11:59:59 +0000261 * successfully stored on the server or there were an error. Application can check this value
262 * using {@code getRecoveyStatus}.
263 *
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800264 * @param packageName Application whose recoverable key's status are to be updated.
265 * @param alias Application-specific key alias.
Robert Berry81ee34b2018-01-23 11:59:59 +0000266 * @param status Status specific to recovery agent.
267 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
268 * service.
269 */
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800270 @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
Robert Berry81ee34b2018-01-23 11:59:59 +0000271 public void setRecoveryStatus(
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800272 @NonNull String packageName, String alias, int status)
Robert Berry81ee34b2018-01-23 11:59:59 +0000273 throws NameNotFoundException, InternalRecoveryServiceException {
274 try {
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800275 // TODO: update aidl
276 String[] aliases = alias == null ? null : new String[]{alias};
Robert Berry81ee34b2018-01-23 11:59:59 +0000277 mBinder.setRecoveryStatus(packageName, aliases, status);
278 } catch (RemoteException e) {
279 throw e.rethrowFromSystemServer();
280 } catch (ServiceSpecificException e) {
281 throw wrapUnexpectedServiceSpecificException(e);
282 }
283 }
284
285 /**
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800286 * Returns recovery status for Application's KeyStore key.
Robert Berry81ee34b2018-01-23 11:59:59 +0000287 * Negative status values are reserved for recovery agent specific codes. List of common codes:
288 *
289 * <ul>
290 * <li>{@link #RECOVERY_STATUS_SYNCED}
291 * <li>{@link #RECOVERY_STATUS_SYNC_IN_PROGRESS}
292 * <li>{@link #RECOVERY_STATUS_MISSING_ACCOUNT}
293 * <li>{@link #RECOVERY_STATUS_PERMANENT_FAILURE}
294 * </ul>
295 *
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800296 * @param packageName Application whose recoverable key status is returned.
297 * @param alias Application-specific key alias.
298 * @return Recovery status.
Robert Berry81ee34b2018-01-23 11:59:59 +0000299 * @see #setRecoveryStatus
300 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
301 * service.
302 */
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800303 public int getRecoveryStatus(String packageName, String alias)
304 throws InternalRecoveryServiceException {
Robert Berry81ee34b2018-01-23 11:59:59 +0000305 try {
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800306 // TODO: update aidl
307 Map<String, Integer> allStatuses = mBinder.getRecoveryStatus(packageName);
308 Integer status = allStatuses.get(alias);
309 if (status == null) {
310 return RecoveryController.RECOVERY_STATUS_PERMANENT_FAILURE;
311 } else {
312 return status;
313 }
Robert Berry81ee34b2018-01-23 11:59:59 +0000314 } catch (RemoteException e) {
315 throw e.rethrowFromSystemServer();
316 } catch (ServiceSpecificException e) {
317 throw wrapUnexpectedServiceSpecificException(e);
318 }
319 }
320
321 /**
322 * Specifies a set of secret types used for end-to-end keystore encryption. Knowing all of them
323 * is necessary to recover data.
324 *
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800325 * @param secretTypes {@link KeyChainProtectionParams#TYPE_LOCKSCREEN} or {@link
326 * KeyChainProtectionParams#TYPE_CUSTOM_PASSWORD}
Robert Berry81ee34b2018-01-23 11:59:59 +0000327 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
328 * service.
329 */
330 public void setRecoverySecretTypes(
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800331 @NonNull @KeyChainProtectionParams.UserSecretType int[] secretTypes)
Robert Berry81ee34b2018-01-23 11:59:59 +0000332 throws InternalRecoveryServiceException {
333 try {
334 mBinder.setRecoverySecretTypes(secretTypes);
335 } catch (RemoteException e) {
336 throw e.rethrowFromSystemServer();
337 } catch (ServiceSpecificException e) {
338 throw wrapUnexpectedServiceSpecificException(e);
339 }
340 }
341
342 /**
343 * Defines a set of secret types used for end-to-end keystore encryption. Knowing all of them is
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800344 * necessary to generate KeyChainSnapshot.
Robert Berry81ee34b2018-01-23 11:59:59 +0000345 *
346 * @return list of recovery secret types
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800347 * @see KeyChainSnapshot
Robert Berry81ee34b2018-01-23 11:59:59 +0000348 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
349 * service.
350 */
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800351 public @NonNull @KeyChainProtectionParams.UserSecretType int[] getRecoverySecretTypes()
Robert Berry81ee34b2018-01-23 11:59:59 +0000352 throws InternalRecoveryServiceException {
353 try {
354 return mBinder.getRecoverySecretTypes();
355 } catch (RemoteException e) {
356 throw e.rethrowFromSystemServer();
357 } catch (ServiceSpecificException e) {
358 throw wrapUnexpectedServiceSpecificException(e);
359 }
360 }
361
362 /**
363 * Returns a list of recovery secret types, necessary to create a pending recovery snapshot.
364 * When user enters a secret of a pending type {@link #recoverySecretAvailable} should be
365 * called.
366 *
367 * @return list of recovery secret types
368 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
369 * service.
370 */
371 @NonNull
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800372 public @KeyChainProtectionParams.UserSecretType int[] getPendingRecoverySecretTypes()
Robert Berry81ee34b2018-01-23 11:59:59 +0000373 throws InternalRecoveryServiceException {
374 try {
375 return mBinder.getPendingRecoverySecretTypes();
376 } catch (RemoteException e) {
377 throw e.rethrowFromSystemServer();
378 } catch (ServiceSpecificException e) {
379 throw wrapUnexpectedServiceSpecificException(e);
380 }
381 }
382
383 /**
384 * Method notifies KeyStore that a user-generated secret is available. This method generates a
385 * symmetric session key which a trusted remote device can use to return a recovery key. Caller
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800386 * should use {@link KeyChainProtectionParams#clearSecret} to override the secret value in
Robert Berry81ee34b2018-01-23 11:59:59 +0000387 * memory.
388 *
389 * @param recoverySecret user generated secret together with parameters necessary to regenerate
390 * it on a new device.
391 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
392 * service.
393 */
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800394 public void recoverySecretAvailable(@NonNull KeyChainProtectionParams recoverySecret)
Robert Berry81ee34b2018-01-23 11:59:59 +0000395 throws InternalRecoveryServiceException {
396 try {
397 mBinder.recoverySecretAvailable(recoverySecret);
398 } catch (RemoteException e) {
399 throw e.rethrowFromSystemServer();
400 } catch (ServiceSpecificException e) {
401 throw wrapUnexpectedServiceSpecificException(e);
402 }
403 }
404
405 /**
Dmitry Dementyevf8ae5de2018-01-08 18:08:23 -0800406 * Generates a AES256/GCM/NoPADDING key called {@code alias} and loads it into the recoverable
407 * key store. Returns the raw material of the key.
Robert Berry81ee34b2018-01-23 11:59:59 +0000408 *
409 * @param alias The key alias.
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800410 * @param account The account associated with the key
Robert Berry81ee34b2018-01-23 11:59:59 +0000411 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
412 * service.
413 * @throws LockScreenRequiredException if the user has not set a lock screen. This is required
414 * to generate recoverable keys, as the snapshots are encrypted using a key derived from the
415 * lock screen.
416 */
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800417 public byte[] generateAndStoreKey(@NonNull String alias, byte[] account)
Robert Berry81ee34b2018-01-23 11:59:59 +0000418 throws InternalRecoveryServiceException, LockScreenRequiredException {
419 try {
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800420 // TODO: add account
Robert Berry81ee34b2018-01-23 11:59:59 +0000421 return mBinder.generateAndStoreKey(alias);
422 } catch (RemoteException e) {
423 throw e.rethrowFromSystemServer();
424 } catch (ServiceSpecificException e) {
425 if (e.errorCode == ERROR_INSECURE_USER) {
426 throw new LockScreenRequiredException(e.getMessage());
427 }
428 throw wrapUnexpectedServiceSpecificException(e);
429 }
430 }
431
432 /**
433 * Removes a key called {@code alias} from the recoverable key store.
434 *
435 * @param alias The key alias.
436 * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
437 * service.
438 */
439 public void removeKey(@NonNull String alias) throws InternalRecoveryServiceException {
440 try {
441 mBinder.removeKey(alias);
442 } catch (RemoteException e) {
443 throw e.rethrowFromSystemServer();
444 } catch (ServiceSpecificException e) {
445 throw wrapUnexpectedServiceSpecificException(e);
446 }
447 }
448
Dmitry Dementyev0916e7c2018-01-23 13:02:08 -0800449 InternalRecoveryServiceException wrapUnexpectedServiceSpecificException(
Robert Berry81ee34b2018-01-23 11:59:59 +0000450 ServiceSpecificException e) {
451 if (e.errorCode == ERROR_SERVICE_INTERNAL_ERROR) {
452 return new InternalRecoveryServiceException(e.getMessage());
453 }
454
455 // Should never happen. If it does, it's a bug, and we need to update how the method that
456 // called this throws its exceptions.
457 return new InternalRecoveryServiceException("Unexpected error code for method: "
458 + e.errorCode, e);
459 }
460}