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