1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.security.keystore.recovery;
18 
19 import android.Manifest;
20 import android.annotation.NonNull;
21 import android.annotation.RequiresPermission;
22 import android.annotation.SystemApi;
23 import android.os.RemoteException;
24 import android.os.ServiceSpecificException;
25 import android.security.keystore.KeyPermanentlyInvalidatedException;
26 import android.util.ArrayMap;
27 import android.util.Log;
28 
29 import libcore.util.HexEncoding;
30 
31 import java.security.Key;
32 import java.security.SecureRandom;
33 import java.security.UnrecoverableKeyException;
34 import java.security.cert.CertPath;
35 import java.security.cert.CertificateException;
36 import java.util.List;
37 import java.util.Locale;
38 import java.util.Map;
39 
40 /**
41  * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a
42  * recovery agent.
43  *
44  * @hide
45  */
46 @SystemApi
47 public class RecoverySession implements AutoCloseable {
48     private static final String TAG = "RecoverySession";
49 
50     private static final int SESSION_ID_LENGTH_BYTES = 16;
51 
52     private final String mSessionId;
53     private final RecoveryController mRecoveryController;
54 
RecoverySession(@onNull RecoveryController recoveryController, @NonNull String sessionId)55     private RecoverySession(@NonNull RecoveryController recoveryController,
56             @NonNull String sessionId) {
57         mRecoveryController = recoveryController;
58         mSessionId = sessionId;
59     }
60 
61     /**
62      * A new session, started by the {@link RecoveryController}.
63      */
64     @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
newInstance(RecoveryController recoveryController)65     static @NonNull RecoverySession newInstance(RecoveryController recoveryController) {
66         return new RecoverySession(recoveryController, newSessionId());
67     }
68 
69     /**
70      * Returns a new random session ID.
71      */
newSessionId()72     private static @NonNull String newSessionId() {
73         SecureRandom secureRandom = new SecureRandom();
74         byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
75         secureRandom.nextBytes(sessionId);
76         return HexEncoding.encodeToString(sessionId, /*upperCase=*/ false);
77     }
78 
79     /**
80      * Starts a recovery session and returns a blob with proof of recovery secret possession.
81      * The method generates a symmetric key for a session, which trusted remote device can use to
82      * return recovery key.
83      *
84      * @param rootCertificateAlias The alias of the root certificate that is already in the Android
85      *     OS. The root certificate will be used for validating {@code verifierCertPath}.
86      * @param verifierCertPath The certificate path used to create the recovery blob on the source
87      *     device. Keystore will verify the certificate path by using the root of trust.
88      * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
89      *     Used to limit number of guesses.
90      * @param vaultChallenge Data passed from server for this recovery session and used to prevent
91      *     replay attacks.
92      * @param secrets Secrets provided by user, the method only uses type and secret fields.
93      * @return The binary blob with recovery claim. It is encrypted with verifierPublicKey
94      * and contains a proof of user secrets possession, session symmetric
95      *     key and parameters necessary to identify the counter with the number of failed recovery
96      *     attempts.
97      * @throws CertificateException if the {@code verifierCertPath} is invalid.
98      * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
99      *     service.
100      */
101     @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
start( @onNull String rootCertificateAlias, @NonNull CertPath verifierCertPath, @NonNull byte[] vaultParams, @NonNull byte[] vaultChallenge, @NonNull List<KeyChainProtectionParams> secrets)102     @NonNull public byte[] start(
103             @NonNull String rootCertificateAlias,
104             @NonNull CertPath verifierCertPath,
105             @NonNull byte[] vaultParams,
106             @NonNull byte[] vaultChallenge,
107             @NonNull List<KeyChainProtectionParams> secrets)
108             throws CertificateException, InternalRecoveryServiceException {
109         // Wrap the CertPath in a Parcelable so it can be passed via Binder calls.
110         RecoveryCertPath recoveryCertPath =
111                 RecoveryCertPath.createRecoveryCertPath(verifierCertPath);
112         try {
113             byte[] recoveryClaim =
114                     mRecoveryController.getBinder().startRecoverySessionWithCertPath(
115                             mSessionId,
116                             rootCertificateAlias,
117                             recoveryCertPath,
118                             vaultParams,
119                             vaultChallenge,
120                             secrets);
121             return recoveryClaim;
122         } catch (RemoteException e) {
123             throw e.rethrowFromSystemServer();
124         } catch (ServiceSpecificException e) {
125             if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT
126                     || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) {
127                 throw new CertificateException("Invalid certificate for recovery session", e);
128             }
129             throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
130         }
131     }
132 
133     /**
134      * Imports key chain snapshot recovered from a remote vault.
135      *
136      * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
137      * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
138      *     and session key generated by {@link #start}.
139      * @return {@code Map} from recovered keys aliases to their references.
140      * @throws SessionExpiredException if {@code session} has since been closed.
141      * @throws DecryptionFailedException if unable to decrypt the snapshot.
142      * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
143      */
144     @RequiresPermission(Manifest.permission.RECOVER_KEYSTORE)
recoverKeyChainSnapshot( @onNull byte[] recoveryKeyBlob, @NonNull List<WrappedApplicationKey> applicationKeys )145     @NonNull public Map<String, Key> recoverKeyChainSnapshot(
146             @NonNull byte[] recoveryKeyBlob,
147             @NonNull List<WrappedApplicationKey> applicationKeys
148     ) throws SessionExpiredException, DecryptionFailedException, InternalRecoveryServiceException {
149         try {
150             Map<String, String> grantAliases = mRecoveryController
151                     .getBinder()
152                     .recoverKeyChainSnapshot(mSessionId, recoveryKeyBlob, applicationKeys);
153             return getKeysFromGrants(grantAliases);
154         } catch (RemoteException e) {
155             throw e.rethrowFromSystemServer();
156         } catch (ServiceSpecificException e) {
157             if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) {
158                 throw new DecryptionFailedException(e.getMessage());
159             }
160             if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) {
161                 throw new SessionExpiredException(e.getMessage());
162             }
163             throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
164         }
165     }
166 
167     /** Given a map from alias to grant alias, returns a map from alias to a {@link Key} handle. */
getKeysFromGrants(@onNull Map<String, String> grantAliases)168     private @NonNull Map<String, Key> getKeysFromGrants(@NonNull Map<String, String> grantAliases)
169             throws InternalRecoveryServiceException {
170         ArrayMap<String, Key> keysByAlias = new ArrayMap<>(grantAliases.size());
171         for (String alias : grantAliases.keySet()) {
172             String grantAlias = grantAliases.get(alias);
173             Key key;
174             try {
175                 key = mRecoveryController.getKeyFromGrant(grantAlias);
176             } catch (KeyPermanentlyInvalidatedException | UnrecoverableKeyException e) {
177                 throw new InternalRecoveryServiceException(
178                         String.format(
179                                 Locale.US,
180                                 "Failed to get key '%s' from grant '%s'",
181                                 alias,
182                                 grantAlias), e);
183             }
184             keysByAlias.put(alias, key);
185         }
186         return keysByAlias;
187     }
188 
189     /**
190      * An internal session ID, used by the framework to match recovery claims to snapshot responses.
191      *
192      * @hide
193      */
getSessionId()194     @NonNull String getSessionId() {
195         return mSessionId;
196     }
197 
198     /**
199      * Deletes all data associated with {@code session}.
200      */
201     @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
202     @Override
close()203     public void close() {
204         try {
205             mRecoveryController.getBinder().closeSession(mSessionId);
206         } catch (RemoteException | ServiceSpecificException e) {
207             Log.e(TAG, "Unexpected error trying to close session", e);
208         }
209     }
210 }
211