RecoverySession.java revision 5af199c56e2723f0eb98027644a1e3486f22ef18
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 {@code recoveryManager}.
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     * Starts a recovery session and returns a blob with proof of recovery secret possession.
76     * The method generates a symmetric key for a session, which trusted remote device can use to
77     * return recovery key.
78     *
79     * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key
80     *     used to create the recovery blob on the source device.
81     *     Keystore will verify the certificate using root of trust.
82     * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
83     *     Used to limit number of guesses.
84     * @param vaultChallenge Data passed from server for this recovery session and used to prevent
85     *     replay attacks
86     * @param secrets Secrets provided by user, the method only uses type and secret fields.
87     * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
88     *     encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
89     *     key and parameters necessary to identify the counter with the number of failed recovery
90     *     attempts.
91     * @throws CertificateException if the {@code verifierPublicKey} is in an incorrect
92     *     format.
93     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
94     *     service.
95     * @deprecated Use {@link #start(CertPath, byte[], byte[], List)} instead.
96     */
97    @Deprecated
98    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
99    @NonNull public byte[] start(
100            @NonNull byte[] verifierPublicKey,
101            @NonNull byte[] vaultParams,
102            @NonNull byte[] vaultChallenge,
103            @NonNull List<KeyChainProtectionParams> secrets)
104            throws CertificateException, InternalRecoveryServiceException {
105        try {
106            byte[] recoveryClaim =
107                    mRecoveryController.getBinder().startRecoverySession(
108                            mSessionId,
109                            verifierPublicKey,
110                            vaultParams,
111                            vaultChallenge,
112                            secrets);
113            return recoveryClaim;
114        } catch (RemoteException e) {
115            throw e.rethrowFromSystemServer();
116        } catch (ServiceSpecificException e) {
117            if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT) {
118                throw new CertificateException(e.getMessage());
119            }
120            throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
121        }
122    }
123
124    /**
125     * Starts a recovery session and returns a blob with proof of recovery secret possession.
126     * The method generates a symmetric key for a session, which trusted remote device can use to
127     * return recovery key.
128     *
129     * @param verifierCertPath The certificate path used to create the recovery blob on the source
130     *     device. Keystore will verify the certificate path by using the root of trust.
131     * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
132     *     Used to limit number of guesses.
133     * @param vaultChallenge Data passed from server for this recovery session and used to prevent
134     *     replay attacks
135     * @param secrets Secrets provided by user, the method only uses type and secret fields.
136     * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
137     *     encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
138     *     key and parameters necessary to identify the counter with the number of failed recovery
139     *     attempts.
140     * @throws CertificateException if the {@code verifierCertPath} is invalid.
141     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
142     *     service.
143     */
144    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
145    @NonNull public byte[] start(
146            @NonNull CertPath verifierCertPath,
147            @NonNull byte[] vaultParams,
148            @NonNull byte[] vaultChallenge,
149            @NonNull List<KeyChainProtectionParams> secrets)
150            throws CertificateException, InternalRecoveryServiceException {
151        // Wrap the CertPath in a Parcelable so it can be passed via Binder calls.
152        RecoveryCertPath recoveryCertPath =
153                RecoveryCertPath.createRecoveryCertPath(verifierCertPath);
154        try {
155            byte[] recoveryClaim =
156                    mRecoveryController.getBinder().startRecoverySessionWithCertPath(
157                            mSessionId,
158                            recoveryCertPath,
159                            vaultParams,
160                            vaultChallenge,
161                            secrets);
162            return recoveryClaim;
163        } catch (RemoteException e) {
164            throw e.rethrowFromSystemServer();
165        } catch (ServiceSpecificException e) {
166            if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT) {
167                throw new CertificateException(e.getMessage());
168            }
169            throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
170        }
171    }
172
173    /**
174     * Imports keys.
175     *
176     * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
177     * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
178     *     and session. KeyStore only uses package names from the application info in {@link
179     *     WrappedApplicationKey}. Caller is responsibility to perform certificates check.
180     * @return Map from alias to raw key material.
181     * @throws SessionExpiredException if {@code session} has since been closed.
182     * @throws DecryptionFailedException if unable to decrypt the snapshot.
183     * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
184     */
185    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
186    public Map<String, byte[]> recoverKeys(
187            @NonNull byte[] recoveryKeyBlob,
188            @NonNull List<WrappedApplicationKey> applicationKeys)
189            throws SessionExpiredException, DecryptionFailedException,
190            InternalRecoveryServiceException {
191        try {
192            return (Map<String, byte[]>) mRecoveryController.getBinder().recoverKeys(
193                    mSessionId, recoveryKeyBlob, applicationKeys);
194        } catch (RemoteException e) {
195            throw e.rethrowFromSystemServer();
196        } catch (ServiceSpecificException e) {
197            if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) {
198                throw new DecryptionFailedException(e.getMessage());
199            }
200            if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) {
201                throw new SessionExpiredException(e.getMessage());
202            }
203            throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
204        }
205    }
206
207    /**
208     * An internal session ID, used by the framework to match recovery claims to snapshot responses.
209     *
210     * @hide
211     */
212    String getSessionId() {
213        return mSessionId;
214    }
215
216    /**
217     * Deletes all data associated with {@code session}. Should not be invoked directly but via
218     * {@link RecoverySession#close()}.
219     */
220    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
221    @Override
222    public void close() {
223        try {
224            mRecoveryController.getBinder().closeSession(mSessionId);
225        } catch (RemoteException | ServiceSpecificException e) {
226            Log.e(TAG, "Unexpected error trying to close session", e);
227        }
228    }
229}
230