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