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