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