1 /*
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 
17 package com.android.server.locksettings.recoverablekeystore;
18 
19 import static android.security.keystore.recovery.RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT;
20 import static android.security.keystore.recovery.RecoveryController.ERROR_DECRYPTION_FAILED;
21 import static android.security.keystore.recovery.RecoveryController.ERROR_DOWNGRADE_CERTIFICATE;
22 import static android.security.keystore.recovery.RecoveryController.ERROR_INSECURE_USER;
23 import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_CERTIFICATE;
24 import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_KEY_FORMAT;
25 import static android.security.keystore.recovery.RecoveryController.ERROR_NO_SNAPSHOT_PENDING;
26 import static android.security.keystore.recovery.RecoveryController.ERROR_SERVICE_INTERNAL_ERROR;
27 import static android.security.keystore.recovery.RecoveryController.ERROR_SESSION_EXPIRED;
28 
29 import android.Manifest;
30 import android.annotation.NonNull;
31 import android.annotation.Nullable;
32 import android.app.PendingIntent;
33 import android.content.Context;
34 import android.os.Binder;
35 import android.os.RemoteException;
36 import android.os.ServiceSpecificException;
37 import android.os.UserHandle;
38 import android.security.KeyStore;
39 import android.security.keystore.recovery.KeyChainProtectionParams;
40 import android.security.keystore.recovery.KeyChainSnapshot;
41 import android.security.keystore.recovery.RecoveryCertPath;
42 import android.security.keystore.recovery.RecoveryController;
43 import android.security.keystore.recovery.WrappedApplicationKey;
44 import android.util.ArrayMap;
45 import android.util.Log;
46 
47 import com.android.internal.annotations.VisibleForTesting;
48 import com.android.internal.util.HexDump;
49 import com.android.internal.util.Preconditions;
50 import com.android.server.locksettings.recoverablekeystore.certificate.CertParsingException;
51 import com.android.server.locksettings.recoverablekeystore.certificate.CertUtils;
52 import com.android.server.locksettings.recoverablekeystore.certificate.CertValidationException;
53 import com.android.server.locksettings.recoverablekeystore.certificate.CertXml;
54 import com.android.server.locksettings.recoverablekeystore.certificate.SigXml;
55 import com.android.server.locksettings.recoverablekeystore.storage.ApplicationKeyStorage;
56 import com.android.server.locksettings.recoverablekeystore.storage.CleanupManager;
57 import com.android.server.locksettings.recoverablekeystore.storage.RecoverableKeyStoreDb;
58 import com.android.server.locksettings.recoverablekeystore.storage.RecoverySessionStorage;
59 import com.android.server.locksettings.recoverablekeystore.storage.RecoverySnapshotStorage;
60 
61 import java.io.IOException;
62 import java.security.InvalidKeyException;
63 import java.security.KeyStoreException;
64 import java.security.NoSuchAlgorithmException;
65 import java.security.PublicKey;
66 import java.security.SecureRandom;
67 import java.security.UnrecoverableKeyException;
68 import java.security.cert.CertPath;
69 import java.security.cert.CertificateEncodingException;
70 import java.security.cert.CertificateException;
71 import java.security.cert.X509Certificate;
72 import java.security.spec.InvalidKeySpecException;
73 import java.util.Arrays;
74 import java.util.HashMap;
75 import java.util.List;
76 import java.util.Locale;
77 import java.util.Map;
78 import java.util.concurrent.ExecutorService;
79 import java.util.concurrent.Executors;
80 
81 import javax.crypto.AEADBadTagException;
82 
83 /**
84  * Class with {@link RecoveryController} API implementation and internal methods to interact
85  * with {@code LockSettingsService}.
86  *
87  * @hide
88  */
89 public class RecoverableKeyStoreManager {
90     private static final String TAG = "RecoverableKeyStoreMgr";
91 
92     private static RecoverableKeyStoreManager mInstance;
93 
94     private final Context mContext;
95     private final RecoverableKeyStoreDb mDatabase;
96     private final RecoverySessionStorage mRecoverySessionStorage;
97     private final ExecutorService mExecutorService;
98     private final RecoverySnapshotListenersStorage mListenersStorage;
99     private final RecoverableKeyGenerator mRecoverableKeyGenerator;
100     private final RecoverySnapshotStorage mSnapshotStorage;
101     private final PlatformKeyManager mPlatformKeyManager;
102     private final ApplicationKeyStorage mApplicationKeyStorage;
103     private final TestOnlyInsecureCertificateHelper mTestCertHelper;
104     private final CleanupManager mCleanupManager;
105 
106     /**
107      * Returns a new or existing instance.
108      *
109      * @hide
110      */
111     public static synchronized RecoverableKeyStoreManager
getInstance(Context context, KeyStore keystore)112             getInstance(Context context, KeyStore keystore) {
113         if (mInstance == null) {
114             RecoverableKeyStoreDb db = RecoverableKeyStoreDb.newInstance(context);
115             PlatformKeyManager platformKeyManager;
116             ApplicationKeyStorage applicationKeyStorage;
117             try {
118                 platformKeyManager = PlatformKeyManager.getInstance(context, db);
119                 applicationKeyStorage = ApplicationKeyStorage.getInstance(keystore);
120             } catch (NoSuchAlgorithmException e) {
121                 // Impossible: all algorithms must be supported by AOSP
122                 throw new RuntimeException(e);
123             } catch (KeyStoreException e) {
124                 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
125             }
126 
127             RecoverySnapshotStorage snapshotStorage =
128                     RecoverySnapshotStorage.newInstance();
129             CleanupManager cleanupManager = CleanupManager.getInstance(
130                     context.getApplicationContext(),
131                     snapshotStorage,
132                     db,
133                     applicationKeyStorage);
134             mInstance = new RecoverableKeyStoreManager(
135                     context.getApplicationContext(),
136                     db,
137                     new RecoverySessionStorage(),
138                     Executors.newSingleThreadExecutor(),
139                     snapshotStorage,
140                     new RecoverySnapshotListenersStorage(),
141                     platformKeyManager,
142                     applicationKeyStorage,
143                     new TestOnlyInsecureCertificateHelper(),
144                     cleanupManager);
145         }
146         return mInstance;
147     }
148 
149     @VisibleForTesting
RecoverableKeyStoreManager( Context context, RecoverableKeyStoreDb recoverableKeyStoreDb, RecoverySessionStorage recoverySessionStorage, ExecutorService executorService, RecoverySnapshotStorage snapshotStorage, RecoverySnapshotListenersStorage listenersStorage, PlatformKeyManager platformKeyManager, ApplicationKeyStorage applicationKeyStorage, TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper, CleanupManager cleanupManager)150     RecoverableKeyStoreManager(
151             Context context,
152             RecoverableKeyStoreDb recoverableKeyStoreDb,
153             RecoverySessionStorage recoverySessionStorage,
154             ExecutorService executorService,
155             RecoverySnapshotStorage snapshotStorage,
156             RecoverySnapshotListenersStorage listenersStorage,
157             PlatformKeyManager platformKeyManager,
158             ApplicationKeyStorage applicationKeyStorage,
159             TestOnlyInsecureCertificateHelper testOnlyInsecureCertificateHelper,
160             CleanupManager cleanupManager) {
161         mContext = context;
162         mDatabase = recoverableKeyStoreDb;
163         mRecoverySessionStorage = recoverySessionStorage;
164         mExecutorService = executorService;
165         mListenersStorage = listenersStorage;
166         mSnapshotStorage = snapshotStorage;
167         mPlatformKeyManager = platformKeyManager;
168         mApplicationKeyStorage = applicationKeyStorage;
169         mTestCertHelper = testOnlyInsecureCertificateHelper;
170         mCleanupManager = cleanupManager;
171         // Clears data for removed users.
172         mCleanupManager.verifyKnownUsers();
173         try {
174             mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase);
175         } catch (NoSuchAlgorithmException e) {
176             Log.wtf(TAG, "AES keygen algorithm not available. AOSP must support this.", e);
177             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
178         }
179     }
180 
181     /**
182      * Used by {@link #initRecoveryServiceWithSigFile(String, byte[], byte[])}.
183      */
184     @VisibleForTesting
initRecoveryService( @onNull String rootCertificateAlias, @NonNull byte[] recoveryServiceCertFile)185     void initRecoveryService(
186             @NonNull String rootCertificateAlias, @NonNull byte[] recoveryServiceCertFile)
187             throws RemoteException {
188         checkRecoverKeyStorePermission();
189         int userId = UserHandle.getCallingUserId();
190         int uid = Binder.getCallingUid();
191 
192         rootCertificateAlias
193                 = mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
194         if (!mTestCertHelper.isValidRootCertificateAlias(rootCertificateAlias)) {
195             throw new ServiceSpecificException(
196                     ERROR_INVALID_CERTIFICATE, "Invalid root certificate alias");
197         }
198         // Always set active alias to the argument of the last call to initRecoveryService method,
199         // even if cert file is incorrect.
200         String activeRootAlias = mDatabase.getActiveRootOfTrust(userId, uid);
201         if (activeRootAlias == null) {
202             Log.d(TAG, "Root of trust for recovery agent + " + uid
203                 + " is assigned for the first time to " + rootCertificateAlias);
204         } else if (!activeRootAlias.equals(rootCertificateAlias)) {
205             Log.i(TAG, "Root of trust for recovery agent " + uid + " is changed to "
206                     + rootCertificateAlias + " from  " + activeRootAlias);
207         }
208         long updatedRows = mDatabase.setActiveRootOfTrust(userId, uid, rootCertificateAlias);
209         if (updatedRows < 0) {
210             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR,
211                     "Failed to set the root of trust in the local DB.");
212         }
213 
214         CertXml certXml;
215         try {
216             certXml = CertXml.parse(recoveryServiceCertFile);
217         } catch (CertParsingException e) {
218             Log.d(TAG, "Failed to parse the input as a cert file: " + HexDump.toHexString(
219                     recoveryServiceCertFile));
220             throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
221         }
222 
223         // Check serial number
224         long newSerial = certXml.getSerial();
225         Long oldSerial = mDatabase.getRecoveryServiceCertSerial(userId, uid, rootCertificateAlias);
226         if (oldSerial != null && oldSerial >= newSerial
227                 && !mTestCertHelper.isTestOnlyCertificateAlias(rootCertificateAlias)) {
228             if (oldSerial == newSerial) {
229                 Log.i(TAG, "The cert file serial number is the same, so skip updating.");
230             } else {
231                 Log.e(TAG, "The cert file serial number is older than the one in database.");
232                 throw new ServiceSpecificException(ERROR_DOWNGRADE_CERTIFICATE,
233                         "The cert file serial number is older than the one in database.");
234             }
235             return;
236         }
237         Log.i(TAG, "Updating the certificate with the new serial number " + newSerial);
238 
239         // Randomly choose and validate an endpoint certificate from the list
240         CertPath certPath;
241         X509Certificate rootCert =
242                 mTestCertHelper.getRootCertificate(rootCertificateAlias);
243         try {
244             Log.d(TAG, "Getting and validating a random endpoint certificate");
245             certPath = certXml.getRandomEndpointCert(rootCert);
246         } catch (CertValidationException e) {
247             Log.e(TAG, "Invalid endpoint cert", e);
248             throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage());
249         }
250 
251         // Save the chosen and validated certificate into database
252         try {
253             Log.d(TAG, "Saving the randomly chosen endpoint certificate to database");
254             long updatedCertPathRows = mDatabase.setRecoveryServiceCertPath(userId, uid,
255                     rootCertificateAlias, certPath);
256             if (updatedCertPathRows > 0) {
257                 long updatedCertSerialRows = mDatabase.setRecoveryServiceCertSerial(userId, uid,
258                         rootCertificateAlias, newSerial);
259                 if (updatedCertSerialRows < 0) {
260                     // Ideally CertPath and CertSerial should be updated together in single
261                     // transaction, but since their mismatch doesn't create many problems
262                     // extra complexity is unnecessary.
263                     throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR,
264                         "Failed to set the certificate serial number in the local DB.");
265                 }
266                 if (mDatabase.getSnapshotVersion(userId, uid) != null) {
267                     mDatabase.setShouldCreateSnapshot(userId, uid, true);
268                     Log.i(TAG, "This is a certificate change. Snapshot must be updated");
269                 } else {
270                     Log.i(TAG, "This is a certificate change. Snapshot didn't exist");
271                 }
272                 long updatedCounterIdRows =
273                         mDatabase.setCounterId(userId, uid, new SecureRandom().nextLong());
274                 if (updatedCounterIdRows < 0) {
275                     Log.e(TAG, "Failed to set the counter id in the local DB.");
276                 }
277             } else if (updatedCertPathRows < 0) {
278                 throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR,
279                         "Failed to set the certificate path in the local DB.");
280             }
281         } catch (CertificateEncodingException e) {
282             Log.e(TAG, "Failed to encode CertPath", e);
283             throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
284         }
285     }
286 
287     /**
288      * Initializes the recovery service with the two files {@code recoveryServiceCertFile} and
289      * {@code recoveryServiceSigFile}.
290      *
291      * @param rootCertificateAlias the alias for the root certificate that is used for validating
292      *     the recovery service certificates.
293      * @param recoveryServiceCertFile the content of the XML file containing a list of certificates
294      *     for the recovery service.
295      * @param recoveryServiceSigFile the content of the XML file containing the public-key signature
296      *     over the entire content of {@code recoveryServiceCertFile}.
297      */
initRecoveryServiceWithSigFile( @onNull String rootCertificateAlias, @NonNull byte[] recoveryServiceCertFile, @NonNull byte[] recoveryServiceSigFile)298     public void initRecoveryServiceWithSigFile(
299             @NonNull String rootCertificateAlias, @NonNull byte[] recoveryServiceCertFile,
300             @NonNull byte[] recoveryServiceSigFile)
301             throws RemoteException {
302         checkRecoverKeyStorePermission();
303         rootCertificateAlias =
304                 mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
305         Preconditions.checkNotNull(recoveryServiceCertFile, "recoveryServiceCertFile is null");
306         Preconditions.checkNotNull(recoveryServiceSigFile, "recoveryServiceSigFile is null");
307 
308         SigXml sigXml;
309         try {
310             sigXml = SigXml.parse(recoveryServiceSigFile);
311         } catch (CertParsingException e) {
312             Log.d(TAG, "Failed to parse the sig file: " + HexDump.toHexString(
313                     recoveryServiceSigFile));
314             throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
315         }
316 
317         X509Certificate rootCert =
318                 mTestCertHelper.getRootCertificate(rootCertificateAlias);
319         try {
320             sigXml.verifyFileSignature(rootCert, recoveryServiceCertFile);
321         } catch (CertValidationException e) {
322             Log.d(TAG, "The signature over the cert file is invalid."
323                     + " Cert: " + HexDump.toHexString(recoveryServiceCertFile)
324                     + " Sig: " + HexDump.toHexString(recoveryServiceSigFile));
325             throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage());
326         }
327 
328         initRecoveryService(rootCertificateAlias, recoveryServiceCertFile);
329     }
330 
331     /**
332      * Gets all data necessary to recover application keys on new device.
333      *
334      * @return KeyChain Snapshot.
335      * @throws ServiceSpecificException if no snapshot is pending.
336      * @hide
337      */
getKeyChainSnapshot()338     public @NonNull KeyChainSnapshot getKeyChainSnapshot()
339             throws RemoteException {
340         checkRecoverKeyStorePermission();
341         int uid = Binder.getCallingUid();
342         KeyChainSnapshot snapshot = mSnapshotStorage.get(uid);
343         if (snapshot == null) {
344             throw new ServiceSpecificException(ERROR_NO_SNAPSHOT_PENDING);
345         }
346         return snapshot;
347     }
348 
setSnapshotCreatedPendingIntent(@ullable PendingIntent intent)349     public void setSnapshotCreatedPendingIntent(@Nullable PendingIntent intent)
350             throws RemoteException {
351         checkRecoverKeyStorePermission();
352         int uid = Binder.getCallingUid();
353         mListenersStorage.setSnapshotListener(uid, intent);
354     }
355 
356     /**
357      * Set the server params for the user's key chain. This is used to uniquely identify a key
358      * chain. Along with the counter ID, it is used to uniquely identify an instance of a vault.
359      */
setServerParams(@onNull byte[] serverParams)360     public void setServerParams(@NonNull byte[] serverParams) throws RemoteException {
361         checkRecoverKeyStorePermission();
362         int userId = UserHandle.getCallingUserId();
363         int uid = Binder.getCallingUid();
364 
365         byte[] currentServerParams = mDatabase.getServerParams(userId, uid);
366 
367         if (Arrays.equals(serverParams, currentServerParams)) {
368             Log.v(TAG, "Not updating server params - same as old value.");
369             return;
370         }
371 
372         long updatedRows = mDatabase.setServerParams(userId, uid, serverParams);
373         if (updatedRows < 0) {
374             throw new ServiceSpecificException(
375                     ERROR_SERVICE_INTERNAL_ERROR, "Database failure trying to set server params.");
376         }
377 
378         if (currentServerParams == null) {
379             Log.i(TAG, "Initialized server params.");
380             return;
381         }
382 
383         if (mDatabase.getSnapshotVersion(userId, uid) != null) {
384             mDatabase.setShouldCreateSnapshot(userId, uid, true);
385             Log.i(TAG, "Updated server params. Snapshot must be updated");
386         } else {
387             Log.i(TAG, "Updated server params. Snapshot didn't exist");
388         }
389     }
390 
391     /**
392      * Sets the recovery status of key with {@code alias} to {@code status}.
393      */
setRecoveryStatus(@onNull String alias, int status)394     public void setRecoveryStatus(@NonNull String alias, int status) throws RemoteException {
395         checkRecoverKeyStorePermission();
396         Preconditions.checkNotNull(alias, "alias is null");
397         long updatedRows = mDatabase.setRecoveryStatus(Binder.getCallingUid(), alias, status);
398         if (updatedRows < 0) {
399             throw new ServiceSpecificException(
400                     ERROR_SERVICE_INTERNAL_ERROR,
401                     "Failed to set the key recovery status in the local DB.");
402         }
403     }
404 
405     /**
406      * Returns recovery statuses for all keys belonging to the calling uid.
407      *
408      * @return {@link Map} from key alias to recovery status. Recovery status is one of
409      *     {@link RecoveryController#RECOVERY_STATUS_SYNCED},
410      *     {@link RecoveryController#RECOVERY_STATUS_SYNC_IN_PROGRESS} or
411      *     {@link RecoveryController#RECOVERY_STATUS_PERMANENT_FAILURE}.
412      */
getRecoveryStatus()413     public @NonNull Map<String, Integer> getRecoveryStatus() throws RemoteException {
414         checkRecoverKeyStorePermission();
415         return mDatabase.getStatusForAllKeys(Binder.getCallingUid());
416     }
417 
418     /**
419      * Sets recovery secrets list used by all recovery agents for given {@code userId}
420      *
421      * @hide
422      */
setRecoverySecretTypes( @onNull @eyChainProtectionParams.UserSecretType int[] secretTypes)423     public void setRecoverySecretTypes(
424             @NonNull @KeyChainProtectionParams.UserSecretType int[] secretTypes)
425             throws RemoteException {
426         checkRecoverKeyStorePermission();
427         Preconditions.checkNotNull(secretTypes, "secretTypes is null");
428         int userId = UserHandle.getCallingUserId();
429         int uid = Binder.getCallingUid();
430 
431         int[] currentSecretTypes = mDatabase.getRecoverySecretTypes(userId, uid);
432         if (Arrays.equals(secretTypes, currentSecretTypes)) {
433             Log.v(TAG, "Not updating secret types - same as old value.");
434             return;
435         }
436 
437         long updatedRows = mDatabase.setRecoverySecretTypes(userId, uid, secretTypes);
438         if (updatedRows < 0) {
439             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR,
440                     "Database error trying to set secret types.");
441         }
442 
443         if (currentSecretTypes.length == 0) {
444             Log.i(TAG, "Initialized secret types.");
445             return;
446         }
447 
448         Log.i(TAG, "Updated secret types. Snapshot pending.");
449         if (mDatabase.getSnapshotVersion(userId, uid) != null) {
450             mDatabase.setShouldCreateSnapshot(userId, uid, true);
451             Log.i(TAG, "Updated secret types. Snapshot must be updated");
452         } else {
453             Log.i(TAG, "Updated secret types. Snapshot didn't exist");
454         }
455     }
456 
457     /**
458      * Gets secret types necessary to create Recovery Data.
459      *
460      * @return secret types
461      * @hide
462      */
getRecoverySecretTypes()463     public @NonNull int[] getRecoverySecretTypes() throws RemoteException {
464         checkRecoverKeyStorePermission();
465         return mDatabase.getRecoverySecretTypes(UserHandle.getCallingUserId(),
466             Binder.getCallingUid());
467     }
468 
469     /**
470      * Initializes recovery session given the X509-encoded public key of the recovery service.
471      *
472      * @param sessionId A unique ID to identify the recovery session.
473      * @param verifierPublicKey X509-encoded public key.
474      * @param vaultParams Additional params associated with vault.
475      * @param vaultChallenge Challenge issued by vault service.
476      * @param secrets Lock-screen hashes. For now only a single secret is supported.
477      * @return Encrypted bytes of recovery claim. This can then be issued to the vault service.
478      * @deprecated Use {@link #startRecoverySessionWithCertPath(String, String, RecoveryCertPath,
479      *         byte[], byte[], List)} instead.
480      *
481      * @hide
482      */
483     @VisibleForTesting
startRecoverySession( @onNull String sessionId, @NonNull byte[] verifierPublicKey, @NonNull byte[] vaultParams, @NonNull byte[] vaultChallenge, @NonNull List<KeyChainProtectionParams> secrets)484     @NonNull byte[] startRecoverySession(
485             @NonNull String sessionId,
486             @NonNull byte[] verifierPublicKey,
487             @NonNull byte[] vaultParams,
488             @NonNull byte[] vaultChallenge,
489             @NonNull List<KeyChainProtectionParams> secrets)
490             throws RemoteException {
491         checkRecoverKeyStorePermission();
492         int uid = Binder.getCallingUid();
493 
494         if (secrets.size() != 1) {
495             throw new UnsupportedOperationException(
496                     "Only a single KeyChainProtectionParams is supported");
497         }
498 
499         PublicKey publicKey;
500         try {
501             publicKey = KeySyncUtils.deserializePublicKey(verifierPublicKey);
502         } catch (InvalidKeySpecException e) {
503             throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
504         }
505         // The raw public key bytes contained in vaultParams must match the ones given in
506         // verifierPublicKey; otherwise, the user secret may be decrypted by a key that is not owned
507         // by the original recovery service.
508         if (!publicKeysMatch(publicKey, vaultParams)) {
509             throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE,
510                     "The public keys given in verifierPublicKey and vaultParams do not match.");
511         }
512 
513         byte[] keyClaimant = KeySyncUtils.generateKeyClaimant();
514         byte[] kfHash = secrets.get(0).getSecret();
515         mRecoverySessionStorage.add(
516                 uid,
517                 new RecoverySessionStorage.Entry(sessionId, kfHash, keyClaimant, vaultParams));
518 
519         Log.i(TAG, "Received VaultParams for recovery: " + HexDump.toHexString(vaultParams));
520         try {
521             byte[] thmKfHash = KeySyncUtils.calculateThmKfHash(kfHash);
522             return KeySyncUtils.encryptRecoveryClaim(
523                     publicKey,
524                     vaultParams,
525                     vaultChallenge,
526                     thmKfHash,
527                     keyClaimant);
528         } catch (NoSuchAlgorithmException e) {
529             Log.wtf(TAG, "SecureBox algorithm missing. AOSP must support this.", e);
530             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
531         } catch (InvalidKeyException e) {
532             throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
533         }
534     }
535 
536     /**
537      * Initializes recovery session given the certificate path of the recovery service.
538      *
539      * @param sessionId A unique ID to identify the recovery session.
540      * @param verifierCertPath The certificate path of the recovery service.
541      * @param vaultParams Additional params associated with vault.
542      * @param vaultChallenge Challenge issued by vault service.
543      * @param secrets Lock-screen hashes. For now only a single secret is supported.
544      * @return Encrypted bytes of recovery claim. This can then be issued to the vault service.
545      *
546      * @hide
547      */
startRecoverySessionWithCertPath( @onNull String sessionId, @NonNull String rootCertificateAlias, @NonNull RecoveryCertPath verifierCertPath, @NonNull byte[] vaultParams, @NonNull byte[] vaultChallenge, @NonNull List<KeyChainProtectionParams> secrets)548     public @NonNull byte[] startRecoverySessionWithCertPath(
549             @NonNull String sessionId,
550             @NonNull String rootCertificateAlias,
551             @NonNull RecoveryCertPath verifierCertPath,
552             @NonNull byte[] vaultParams,
553             @NonNull byte[] vaultChallenge,
554             @NonNull List<KeyChainProtectionParams> secrets)
555             throws RemoteException {
556         checkRecoverKeyStorePermission();
557         rootCertificateAlias =
558                 mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
559         Preconditions.checkNotNull(sessionId, "invalid session");
560         Preconditions.checkNotNull(verifierCertPath, "verifierCertPath is null");
561         Preconditions.checkNotNull(vaultParams, "vaultParams is null");
562         Preconditions.checkNotNull(vaultChallenge, "vaultChallenge is null");
563         Preconditions.checkNotNull(secrets, "secrets is null");
564         CertPath certPath;
565         try {
566             certPath = verifierCertPath.getCertPath();
567         } catch (CertificateException e) {
568             throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT, e.getMessage());
569         }
570 
571         try {
572             CertUtils.validateCertPath(
573                     mTestCertHelper.getRootCertificate(rootCertificateAlias), certPath);
574         } catch (CertValidationException e) {
575             Log.e(TAG, "Failed to validate the given cert path", e);
576             throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage());
577         }
578 
579         byte[] verifierPublicKey = certPath.getCertificates().get(0).getPublicKey().getEncoded();
580         if (verifierPublicKey == null) {
581             Log.e(TAG, "Failed to encode verifierPublicKey");
582             throw new ServiceSpecificException(ERROR_BAD_CERTIFICATE_FORMAT,
583                     "Failed to encode verifierPublicKey");
584         }
585 
586         return startRecoverySession(
587                 sessionId, verifierPublicKey, vaultParams, vaultChallenge, secrets);
588     }
589 
590     /**
591      * Invoked by a recovery agent after a successful recovery claim is sent to the remote vault
592      * service.
593      *
594      * @param sessionId The session ID used to generate the claim. See
595      *     {@link #startRecoverySession(String, byte[], byte[], byte[], List)}.
596      * @param encryptedRecoveryKey The encrypted recovery key blob returned by the remote vault
597      *     service.
598      * @param applicationKeys The encrypted key blobs returned by the remote vault service. These
599      *     were wrapped with the recovery key.
600      * @throws RemoteException if an error occurred recovering the keys.
601      */
recoverKeyChainSnapshot( @onNull String sessionId, @NonNull byte[] encryptedRecoveryKey, @NonNull List<WrappedApplicationKey> applicationKeys)602     public @NonNull Map<String, String> recoverKeyChainSnapshot(
603             @NonNull String sessionId,
604             @NonNull byte[] encryptedRecoveryKey,
605             @NonNull List<WrappedApplicationKey> applicationKeys) throws RemoteException {
606         checkRecoverKeyStorePermission();
607         int userId = UserHandle.getCallingUserId();
608         int uid = Binder.getCallingUid();
609         RecoverySessionStorage.Entry sessionEntry = mRecoverySessionStorage.get(uid, sessionId);
610         if (sessionEntry == null) {
611             throw new ServiceSpecificException(ERROR_SESSION_EXPIRED,
612                     String.format(Locale.US,
613                             "Application uid=%d does not have pending session '%s'",
614                             uid,
615                             sessionId));
616         }
617 
618         try {
619             byte[] recoveryKey = decryptRecoveryKey(sessionEntry, encryptedRecoveryKey);
620             Map<String, byte[]> keysByAlias = recoverApplicationKeys(recoveryKey,
621                     applicationKeys);
622             return importKeyMaterials(userId, uid, keysByAlias);
623         } catch (KeyStoreException e) {
624             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
625         } finally {
626             sessionEntry.destroy();
627             mRecoverySessionStorage.remove(uid);
628         }
629     }
630 
631     /**
632      * Imports the key materials, returning a map from alias to grant alias for the calling user.
633      *
634      * @param userId The calling user ID.
635      * @param uid The calling uid.
636      * @param keysByAlias The key materials, keyed by alias.
637      * @throws KeyStoreException if an error occurs importing the key or getting the grant.
638      */
importKeyMaterials( int userId, int uid, Map<String, byte[]> keysByAlias)639     private @NonNull Map<String, String> importKeyMaterials(
640             int userId, int uid, Map<String, byte[]> keysByAlias)
641             throws KeyStoreException {
642         ArrayMap<String, String> grantAliasesByAlias = new ArrayMap<>(keysByAlias.size());
643         for (String alias : keysByAlias.keySet()) {
644             mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, keysByAlias.get(alias));
645             String grantAlias = getAlias(userId, uid, alias);
646             Log.i(TAG, String.format(Locale.US, "Import %s -> %s", alias, grantAlias));
647             grantAliasesByAlias.put(alias, grantAlias);
648         }
649         return grantAliasesByAlias;
650     }
651 
652     /**
653      * Returns an alias for the key.
654      *
655      * @param userId The user ID of the calling process.
656      * @param uid The uid of the calling process.
657      * @param alias The alias of the key.
658      * @return The alias in the calling process's keystore.
659      */
getAlias(int userId, int uid, String alias)660     private @Nullable String getAlias(int userId, int uid, String alias) {
661         return mApplicationKeyStorage.getGrantAlias(userId, uid, alias);
662     }
663 
664     /**
665      * Destroys the session with the given {@code sessionId}.
666      */
closeSession(@onNull String sessionId)667     public void closeSession(@NonNull String sessionId) throws RemoteException {
668         checkRecoverKeyStorePermission();
669         Preconditions.checkNotNull(sessionId, "invalid session");
670         mRecoverySessionStorage.remove(Binder.getCallingUid(), sessionId);
671     }
672 
removeKey(@onNull String alias)673     public void removeKey(@NonNull String alias) throws RemoteException {
674         checkRecoverKeyStorePermission();
675         Preconditions.checkNotNull(alias, "alias is null");
676         int uid = Binder.getCallingUid();
677         int userId = UserHandle.getCallingUserId();
678 
679         boolean wasRemoved = mDatabase.removeKey(uid, alias);
680         if (wasRemoved) {
681             mDatabase.setShouldCreateSnapshot(userId, uid, true);
682             mApplicationKeyStorage.deleteEntry(userId, uid, alias);
683         }
684     }
685 
686     /**
687      * Generates a key named {@code alias} in caller's namespace.
688      * The key is stored in system service keystore namespace.
689      *
690      * @param alias the alias provided by caller as a reference to the key.
691      * @return grant alias, which caller can use to access the key.
692      * @throws RemoteException if certain internal errors occur.
693      *
694      * @deprecated Use {@link #generateKeyWithMetadata(String, byte[])} instead.
695      */
696     @Deprecated
generateKey(@onNull String alias)697     public String generateKey(@NonNull String alias) throws RemoteException {
698         return generateKeyWithMetadata(alias, /*metadata=*/ null);
699     }
700 
701     /**
702      * Generates a key named {@code alias} with the {@code metadata} in caller's namespace.
703      * The key is stored in system service keystore namespace.
704      *
705      * @param alias the alias provided by caller as a reference to the key.
706      * @param metadata the optional metadata blob that will authenticated (but unencrypted) together
707      *         with the key material when the key is uploaded to cloud.
708      * @return grant alias, which caller can use to access the key.
709      * @throws RemoteException if certain internal errors occur.
710      */
generateKeyWithMetadata(@onNull String alias, @Nullable byte[] metadata)711     public String generateKeyWithMetadata(@NonNull String alias, @Nullable byte[] metadata)
712             throws RemoteException {
713         checkRecoverKeyStorePermission();
714         Preconditions.checkNotNull(alias, "alias is null");
715         int uid = Binder.getCallingUid();
716         int userId = UserHandle.getCallingUserId();
717 
718         PlatformEncryptionKey encryptionKey;
719         try {
720             encryptionKey = mPlatformKeyManager.getEncryptKey(userId);
721         } catch (NoSuchAlgorithmException e) {
722             // Impossible: all algorithms must be supported by AOSP
723             throw new RuntimeException(e);
724         } catch (KeyStoreException | UnrecoverableKeyException | IOException e) {
725             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
726         } catch (InsecureUserException e) {
727             throw new ServiceSpecificException(ERROR_INSECURE_USER, e.getMessage());
728         }
729 
730         try {
731             byte[] secretKey = mRecoverableKeyGenerator.generateAndStoreKey(encryptionKey, userId,
732                     uid, alias, metadata);
733             mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, secretKey);
734             return getAlias(userId, uid, alias);
735         } catch (KeyStoreException | InvalidKeyException | RecoverableKeyStorageException e) {
736             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
737         }
738     }
739 
740     /**
741      * Imports a 256-bit AES-GCM key named {@code alias}. The key is stored in system service
742      * keystore namespace.
743      *
744      * @param alias the alias provided by caller as a reference to the key.
745      * @param keyBytes the raw bytes of the 256-bit AES key.
746      * @return grant alias, which caller can use to access the key.
747      * @throws RemoteException if the given key is invalid or some internal errors occur.
748      *
749      * @deprecated Use {{@link #importKeyWithMetadata(String, byte[], byte[])}} instead.
750      *
751      * @hide
752      */
753     @Deprecated
importKey(@onNull String alias, @NonNull byte[] keyBytes)754     public @Nullable String importKey(@NonNull String alias, @NonNull byte[] keyBytes)
755             throws RemoteException {
756         return importKeyWithMetadata(alias, keyBytes, /*metadata=*/ null);
757     }
758 
759     /**
760      * Imports a 256-bit AES-GCM key named {@code alias} with the given {@code metadata}. The key is
761      * stored in system service keystore namespace.
762      *
763      * @param alias the alias provided by caller as a reference to the key.
764      * @param keyBytes the raw bytes of the 256-bit AES key.
765      * @param metadata the metadata to be authenticated (but unencrypted) together with the key.
766      * @return grant alias, which caller can use to access the key.
767      * @throws RemoteException if the given key is invalid or some internal errors occur.
768      *
769      * @hide
770      */
importKeyWithMetadata(@onNull String alias, @NonNull byte[] keyBytes, @Nullable byte[] metadata)771     public @Nullable String importKeyWithMetadata(@NonNull String alias, @NonNull byte[] keyBytes,
772             @Nullable byte[] metadata) throws RemoteException {
773         checkRecoverKeyStorePermission();
774         Preconditions.checkNotNull(alias, "alias is null");
775         Preconditions.checkNotNull(keyBytes, "keyBytes is null");
776         if (keyBytes.length != RecoverableKeyGenerator.KEY_SIZE_BITS / Byte.SIZE) {
777             Log.e(TAG, "The given key for import doesn't have the required length "
778                     + RecoverableKeyGenerator.KEY_SIZE_BITS);
779             throw new ServiceSpecificException(ERROR_INVALID_KEY_FORMAT,
780                     "The given key does not contain " + RecoverableKeyGenerator.KEY_SIZE_BITS
781                             + " bits.");
782         }
783 
784         int uid = Binder.getCallingUid();
785         int userId = UserHandle.getCallingUserId();
786 
787         PlatformEncryptionKey encryptionKey;
788         try {
789             encryptionKey = mPlatformKeyManager.getEncryptKey(userId);
790         } catch (NoSuchAlgorithmException e) {
791             // Impossible: all algorithms must be supported by AOSP
792             throw new RuntimeException(e);
793         } catch (KeyStoreException | UnrecoverableKeyException | IOException e) {
794             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
795         } catch (InsecureUserException e) {
796             throw new ServiceSpecificException(ERROR_INSECURE_USER, e.getMessage());
797         }
798 
799         try {
800             // Wrap the key by the platform key and store the wrapped key locally
801             mRecoverableKeyGenerator.importKey(encryptionKey, userId, uid, alias, keyBytes,
802                     metadata);
803 
804             // Import the key to Android KeyStore and get grant
805             mApplicationKeyStorage.setSymmetricKeyEntry(userId, uid, alias, keyBytes);
806             return getAlias(userId, uid, alias);
807         } catch (KeyStoreException | InvalidKeyException | RecoverableKeyStorageException e) {
808             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
809         }
810     }
811 
812     /**
813      * Gets a key named {@code alias} in caller's namespace.
814      *
815      * @return grant alias, which caller can use to access the key.
816      */
getKey(@onNull String alias)817     public @Nullable String getKey(@NonNull String alias) throws RemoteException {
818         checkRecoverKeyStorePermission();
819         Preconditions.checkNotNull(alias, "alias is null");
820         int uid = Binder.getCallingUid();
821         int userId = UserHandle.getCallingUserId();
822         return getAlias(userId, uid, alias);
823     }
824 
decryptRecoveryKey( RecoverySessionStorage.Entry sessionEntry, byte[] encryptedClaimResponse)825     private byte[] decryptRecoveryKey(
826             RecoverySessionStorage.Entry sessionEntry, byte[] encryptedClaimResponse)
827             throws RemoteException, ServiceSpecificException {
828         byte[] locallyEncryptedKey;
829         try {
830             locallyEncryptedKey = KeySyncUtils.decryptRecoveryClaimResponse(
831                     sessionEntry.getKeyClaimant(),
832                     sessionEntry.getVaultParams(),
833                     encryptedClaimResponse);
834         } catch (InvalidKeyException e) {
835             Log.e(TAG, "Got InvalidKeyException during decrypting recovery claim response", e);
836             throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
837                     "Failed to decrypt recovery key " + e.getMessage());
838         } catch (AEADBadTagException e) {
839             Log.e(TAG, "Got AEADBadTagException during decrypting recovery claim response", e);
840             throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
841                     "Failed to decrypt recovery key " + e.getMessage());
842         } catch (NoSuchAlgorithmException e) {
843             // Should never happen: all the algorithms used are required by AOSP implementations
844             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
845         }
846 
847         try {
848             return KeySyncUtils.decryptRecoveryKey(sessionEntry.getLskfHash(), locallyEncryptedKey);
849         } catch (InvalidKeyException e) {
850             Log.e(TAG, "Got InvalidKeyException during decrypting recovery key", e);
851             throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
852                     "Failed to decrypt recovery key " + e.getMessage());
853         } catch (AEADBadTagException e) {
854             Log.e(TAG, "Got AEADBadTagException during decrypting recovery key", e);
855             throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
856                     "Failed to decrypt recovery key " + e.getMessage());
857         } catch (NoSuchAlgorithmException e) {
858             // Should never happen: all the algorithms used are required by AOSP implementations
859             throw new ServiceSpecificException(ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
860         }
861     }
862 
863     /**
864      * Uses {@code recoveryKey} to decrypt {@code applicationKeys}.
865      *
866      * @return Map from alias to raw key material.
867      * @throws RemoteException if an error occurred decrypting the keys.
868      */
recoverApplicationKeys(@onNull byte[] recoveryKey, @NonNull List<WrappedApplicationKey> applicationKeys)869     private @NonNull Map<String, byte[]> recoverApplicationKeys(@NonNull byte[] recoveryKey,
870             @NonNull List<WrappedApplicationKey> applicationKeys) throws RemoteException {
871         HashMap<String, byte[]> keyMaterialByAlias = new HashMap<>();
872         for (WrappedApplicationKey applicationKey : applicationKeys) {
873             String alias = applicationKey.getAlias();
874             byte[] encryptedKeyMaterial = applicationKey.getEncryptedKeyMaterial();
875             byte[] keyMetadata = applicationKey.getMetadata();
876 
877             try {
878                 byte[] keyMaterial = KeySyncUtils.decryptApplicationKey(recoveryKey,
879                         encryptedKeyMaterial, keyMetadata);
880                 keyMaterialByAlias.put(alias, keyMaterial);
881             } catch (NoSuchAlgorithmException e) {
882                 Log.wtf(TAG, "Missing SecureBox algorithm. AOSP required to support this.", e);
883                 throw new ServiceSpecificException(
884                         ERROR_SERVICE_INTERNAL_ERROR, e.getMessage());
885             } catch (InvalidKeyException e) {
886                 Log.e(TAG, "Got InvalidKeyException during decrypting application key with alias: "
887                         + alias, e);
888                 throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
889                         "Failed to recover key with alias '" + alias + "': " + e.getMessage());
890             } catch (AEADBadTagException e) {
891                 Log.e(TAG, "Got AEADBadTagException during decrypting application key with alias: "
892                         + alias, e);
893                 // Ignore the exception to continue to recover the other application keys.
894             }
895         }
896         if (!applicationKeys.isEmpty() && keyMaterialByAlias.isEmpty()) {
897             Log.e(TAG, "Failed to recover any of the application keys.");
898             throw new ServiceSpecificException(ERROR_DECRYPTION_FAILED,
899                     "Failed to recover any of the application keys.");
900         }
901         return keyMaterialByAlias;
902     }
903 
904     /**
905      * This function can only be used inside LockSettingsService.
906      *
907      * @param storedHashType from {@code CredentialHash}
908      * @param credential - unencrypted byte array. Password length should be at most 16 symbols
909      *     {@code mPasswordMaxLength}
910      * @param userId for user who just unlocked the device.
911      * @hide
912      */
lockScreenSecretAvailable( int storedHashType, @NonNull byte[] credential, int userId)913     public void lockScreenSecretAvailable(
914             int storedHashType, @NonNull byte[] credential, int userId) {
915         // So as not to block the critical path unlocking the phone, defer to another thread.
916         try {
917             mExecutorService.execute(KeySyncTask.newInstance(
918                     mContext,
919                     mDatabase,
920                     mSnapshotStorage,
921                     mListenersStorage,
922                     userId,
923                     storedHashType,
924                     credential,
925                     /*credentialUpdated=*/ false));
926         } catch (NoSuchAlgorithmException e) {
927             Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e);
928         } catch (KeyStoreException e) {
929             Log.e(TAG, "Key store error encountered during recoverable key sync", e);
930         } catch (InsecureUserException e) {
931             Log.wtf(TAG, "Impossible - insecure user, but user just entered lock screen", e);
932         }
933     }
934 
935     /**
936      * This function can only be used inside LockSettingsService.
937      *
938      * @param storedHashType from {@code CredentialHash}
939      * @param credential - unencrypted byte array
940      * @param userId for the user whose lock screen credentials were changed.
941      * @hide
942      */
lockScreenSecretChanged( int storedHashType, @Nullable byte[] credential, int userId)943     public void lockScreenSecretChanged(
944             int storedHashType,
945             @Nullable byte[] credential,
946             int userId) {
947         // So as not to block the critical path unlocking the phone, defer to another thread.
948         try {
949             mExecutorService.execute(KeySyncTask.newInstance(
950                     mContext,
951                     mDatabase,
952                     mSnapshotStorage,
953                     mListenersStorage,
954                     userId,
955                     storedHashType,
956                     credential,
957                     /*credentialUpdated=*/ true));
958         } catch (NoSuchAlgorithmException e) {
959             Log.wtf(TAG, "Should never happen - algorithm unavailable for KeySync", e);
960         } catch (KeyStoreException e) {
961             Log.e(TAG, "Key store error encountered during recoverable key sync", e);
962         } catch (InsecureUserException e) {
963             Log.e(TAG, "InsecureUserException during lock screen secret update", e);
964         }
965     }
966 
checkRecoverKeyStorePermission()967     private void checkRecoverKeyStorePermission() {
968         mContext.enforceCallingOrSelfPermission(
969                 Manifest.permission.RECOVER_KEYSTORE,
970                 "Caller " + Binder.getCallingUid() + " doesn't have RecoverKeyStore permission.");
971         int userId = UserHandle.getCallingUserId();
972         int uid = Binder.getCallingUid();
973         mCleanupManager.registerRecoveryAgent(userId, uid);
974     }
975 
publicKeysMatch(PublicKey publicKey, byte[] vaultParams)976     private boolean publicKeysMatch(PublicKey publicKey, byte[] vaultParams) {
977         byte[] encodedPublicKey = SecureBox.encodePublicKey(publicKey);
978         return Arrays.equals(encodedPublicKey, Arrays.copyOf(vaultParams, encodedPublicKey.length));
979     }
980 }
981