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