RecoverySession.java revision 889e78cb28a59c678ce1310c94e25ba887e18571
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
17package android.security.keystore.recovery;
18
19import android.Manifest;
20import android.annotation.NonNull;
21import android.annotation.RequiresPermission;
22import android.annotation.SystemApi;
23import android.os.RemoteException;
24import android.os.ServiceSpecificException;
25import android.util.ArrayMap;
26import android.util.Log;
27
28import java.security.Key;
29import java.security.SecureRandom;
30import java.security.UnrecoverableKeyException;
31import java.security.cert.CertPath;
32import java.security.cert.CertificateException;
33import java.util.List;
34import java.util.Locale;
35import java.util.Map;
36
37/**
38 * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a
39 * recovery agent.
40 *
41 * @hide
42 */
43@SystemApi
44public class RecoverySession implements AutoCloseable {
45    private static final String TAG = "RecoverySession";
46
47    private static final int SESSION_ID_LENGTH_BYTES = 16;
48
49    private final String mSessionId;
50    private final RecoveryController mRecoveryController;
51
52    private RecoverySession(RecoveryController recoveryController, String sessionId) {
53        mRecoveryController = recoveryController;
54        mSessionId = sessionId;
55    }
56
57    /**
58     * A new session, started by the {@link RecoveryController}.
59     */
60    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
61    static RecoverySession newInstance(RecoveryController recoveryController) {
62        return new RecoverySession(recoveryController, newSessionId());
63    }
64
65    /**
66     * Returns a new random session ID.
67     */
68    private static String newSessionId() {
69        SecureRandom secureRandom = new SecureRandom();
70        byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
71        secureRandom.nextBytes(sessionId);
72        StringBuilder sb = new StringBuilder();
73        for (byte b : sessionId) {
74            sb.append(Byte.toHexString(b, /*upperCase=*/ false));
75        }
76        return sb.toString();
77    }
78
79    /**
80     * @deprecated Use {@link #start(CertPath, byte[], byte[], List)} instead.
81     */
82    @Deprecated
83    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
84    @NonNull public byte[] start(
85            @NonNull byte[] verifierPublicKey,
86            @NonNull byte[] vaultParams,
87            @NonNull byte[] vaultChallenge,
88            @NonNull List<KeyChainProtectionParams> secrets)
89            throws CertificateException, InternalRecoveryServiceException {
90        try {
91            byte[] recoveryClaim =
92                    mRecoveryController.getBinder().startRecoverySession(
93                            mSessionId,
94                            verifierPublicKey,
95                            vaultParams,
96                            vaultChallenge,
97                            secrets);
98            return recoveryClaim;
99        } catch (RemoteException e) {
100            throw e.rethrowFromSystemServer();
101        } catch (ServiceSpecificException e) {
102            if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT
103                    || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) {
104                throw new CertificateException(e.getMessage());
105            }
106            throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
107        }
108    }
109
110    /**
111     * Starts a recovery session and returns a blob with proof of recovery secret possession.
112     * The method generates a symmetric key for a session, which trusted remote device can use to
113     * return recovery key.
114     *
115     * @param verifierCertPath The certificate path used to create the recovery blob on the source
116     *     device. Keystore will verify the certificate path by using the root of trust.
117     * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
118     *     Used to limit number of guesses.
119     * @param vaultChallenge Data passed from server for this recovery session and used to prevent
120     *     replay attacks.
121     * @param secrets Secrets provided by user, the method only uses type and secret fields.
122     * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
123     *     encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
124     *     key and parameters necessary to identify the counter with the number of failed recovery
125     *     attempts.
126     * @throws CertificateException if the {@code verifierCertPath} is invalid.
127     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
128     *     service.
129     */
130    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
131    @NonNull public byte[] start(
132            @NonNull CertPath verifierCertPath,
133            @NonNull byte[] vaultParams,
134            @NonNull byte[] vaultChallenge,
135            @NonNull List<KeyChainProtectionParams> secrets)
136            throws CertificateException, InternalRecoveryServiceException {
137        // Wrap the CertPath in a Parcelable so it can be passed via Binder calls.
138        RecoveryCertPath recoveryCertPath =
139                RecoveryCertPath.createRecoveryCertPath(verifierCertPath);
140        try {
141            byte[] recoveryClaim =
142                    mRecoveryController.getBinder().startRecoverySessionWithCertPath(
143                            mSessionId,
144                            /*rootCertificateAlias=*/ "",  // Use the default root cert
145                            recoveryCertPath,
146                            vaultParams,
147                            vaultChallenge,
148                            secrets);
149            return recoveryClaim;
150        } catch (RemoteException e) {
151            throw e.rethrowFromSystemServer();
152        } catch (ServiceSpecificException e) {
153            if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT
154                    || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) {
155                throw new CertificateException(e.getMessage());
156            }
157            throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
158        }
159    }
160
161    /**
162     * Starts a recovery session and returns a blob with proof of recovery secret possession.
163     * The method generates a symmetric key for a session, which trusted remote device can use to
164     * return recovery key.
165     *
166     * @param rootCertificateAlias The alias of the root certificate that is already in the Android
167     *     OS. The root certificate will be used for validating {@code verifierCertPath}.
168     * @param verifierCertPath The certificate path used to create the recovery blob on the source
169     *     device. Keystore will verify the certificate path by using the root of trust.
170     * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
171     *     Used to limit number of guesses.
172     * @param vaultChallenge Data passed from server for this recovery session and used to prevent
173     *     replay attacks.
174     * @param secrets Secrets provided by user, the method only uses type and secret fields.
175     * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
176     *     encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
177     *     key and parameters necessary to identify the counter with the number of failed recovery
178     *     attempts.
179     * @throws CertificateException if the {@code verifierCertPath} is invalid.
180     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
181     *     service.
182     *
183     * @hide
184     */
185    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
186    @NonNull public byte[] start(
187            @NonNull String rootCertificateAlias,
188            @NonNull CertPath verifierCertPath,
189            @NonNull byte[] vaultParams,
190            @NonNull byte[] vaultChallenge,
191            @NonNull List<KeyChainProtectionParams> secrets)
192            throws CertificateException, InternalRecoveryServiceException {
193        // Wrap the CertPath in a Parcelable so it can be passed via Binder calls.
194        RecoveryCertPath recoveryCertPath =
195                RecoveryCertPath.createRecoveryCertPath(verifierCertPath);
196        try {
197            byte[] recoveryClaim =
198                    mRecoveryController.getBinder().startRecoverySessionWithCertPath(
199                            mSessionId,
200                            rootCertificateAlias,
201                            recoveryCertPath,
202                            vaultParams,
203                            vaultChallenge,
204                            secrets);
205            return recoveryClaim;
206        } catch (RemoteException e) {
207            throw e.rethrowFromSystemServer();
208        } catch (ServiceSpecificException e) {
209            if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT
210                    || e.errorCode == RecoveryController.ERROR_INVALID_CERTIFICATE) {
211                throw new CertificateException(e.getMessage());
212            }
213            throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
214        }
215    }
216
217    /**
218     * Imports keys.
219     *
220     * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
221     * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
222     *     and session. KeyStore only uses package names from the application info in {@link
223     *     WrappedApplicationKey}. Caller is responsibility to perform certificates check.
224     * @return Map from alias to raw key material.
225     * @throws SessionExpiredException if {@code session} has since been closed.
226     * @throws DecryptionFailedException if unable to decrypt the snapshot.
227     * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
228     */
229    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
230    public Map<String, byte[]> recoverKeys(
231            @NonNull byte[] recoveryKeyBlob,
232            @NonNull List<WrappedApplicationKey> applicationKeys)
233            throws SessionExpiredException, DecryptionFailedException,
234            InternalRecoveryServiceException {
235        try {
236            return (Map<String, byte[]>) mRecoveryController.getBinder().recoverKeys(
237                    mSessionId, recoveryKeyBlob, applicationKeys);
238        } catch (RemoteException e) {
239            throw e.rethrowFromSystemServer();
240        } catch (ServiceSpecificException e) {
241            if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) {
242                throw new DecryptionFailedException(e.getMessage());
243            }
244            if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) {
245                throw new SessionExpiredException(e.getMessage());
246            }
247            throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
248        }
249    }
250
251    /**
252     * Imports key chain snapshot recovered from a remote vault.
253     *
254     * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
255     * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
256     *     and session.
257     * @throws SessionExpiredException if {@code session} has since been closed.
258     * @throws DecryptionFailedException if unable to decrypt the snapshot.
259     * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
260     *
261     * @hide
262     */
263    @RequiresPermission(Manifest.permission.RECOVER_KEYSTORE)
264    public Map<String, Key> recoverKeyChainSnapshot(
265            @NonNull byte[] recoveryKeyBlob,
266            @NonNull List<WrappedApplicationKey> applicationKeys
267    ) throws SessionExpiredException, DecryptionFailedException, InternalRecoveryServiceException {
268        try {
269            Map<String, String> grantAliases = mRecoveryController
270                    .getBinder()
271                    .recoverKeyChainSnapshot(mSessionId, recoveryKeyBlob, applicationKeys);
272            return getKeysFromGrants(grantAliases);
273        } catch (RemoteException e) {
274            throw e.rethrowFromSystemServer();
275        } catch (ServiceSpecificException e) {
276            if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) {
277                throw new DecryptionFailedException(e.getMessage());
278            }
279            if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) {
280                throw new SessionExpiredException(e.getMessage());
281            }
282            throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
283        }
284    }
285
286    /** Given a map from alias to grant alias, returns a map from alias to a {@link Key} handle. */
287    private Map<String, Key> getKeysFromGrants(Map<String, String> grantAliases)
288            throws InternalRecoveryServiceException {
289        ArrayMap<String, Key> keysByAlias = new ArrayMap<>(grantAliases.size());
290        for (String alias : grantAliases.keySet()) {
291            String grantAlias = grantAliases.get(alias);
292            Key key;
293            try {
294                key = mRecoveryController.getKeyFromGrant(grantAlias);
295            } catch (UnrecoverableKeyException e) {
296                throw new InternalRecoveryServiceException(
297                        String.format(
298                                Locale.US,
299                                "Failed to get key '%s' from grant '%s'",
300                                alias,
301                                grantAlias), e);
302            }
303            keysByAlias.put(alias, key);
304        }
305        return keysByAlias;
306    }
307
308    /**
309     * An internal session ID, used by the framework to match recovery claims to snapshot responses.
310     *
311     * @hide
312     */
313    String getSessionId() {
314        return mSessionId;
315    }
316
317    /**
318     * Deletes all data associated with {@code session}. Should not be invoked directly but via
319     * {@link RecoverySession#close()}.
320     */
321    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
322    @Override
323    public void close() {
324        try {
325            mRecoveryController.getBinder().closeSession(mSessionId);
326        } catch (RemoteException | ServiceSpecificException e) {
327            Log.e(TAG, "Unexpected error trying to close session", e);
328        }
329    }
330}
331