RecoverySession.java revision f8ae5deba2911b7bc8441df31c0504eaaa687add
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.CertificateException;
28import java.util.List;
29import java.util.Map;
30
31/**
32 * Session to recover a {@link KeyChainSnapshot} from the remote trusted hardware, initiated by a
33 * recovery agent.
34 *
35 * @hide
36 */
37@SystemApi
38public class RecoverySession implements AutoCloseable {
39    private static final String TAG = "RecoverySession";
40
41    private static final int SESSION_ID_LENGTH_BYTES = 16;
42
43    private final String mSessionId;
44    private final RecoveryController mRecoveryController;
45
46    private RecoverySession(RecoveryController recoveryController, String sessionId) {
47        mRecoveryController = recoveryController;
48        mSessionId = sessionId;
49    }
50
51    /**
52     * A new session, started by {@code recoveryManager}.
53     */
54    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
55    static RecoverySession newInstance(RecoveryController recoveryController) {
56        return new RecoverySession(recoveryController, newSessionId());
57    }
58
59    /**
60     * Returns a new random session ID.
61     */
62    private static String newSessionId() {
63        SecureRandom secureRandom = new SecureRandom();
64        byte[] sessionId = new byte[SESSION_ID_LENGTH_BYTES];
65        secureRandom.nextBytes(sessionId);
66        StringBuilder sb = new StringBuilder();
67        for (byte b : sessionId) {
68            sb.append(Byte.toHexString(b, /*upperCase=*/ false));
69        }
70        return sb.toString();
71    }
72
73    /**
74     * Starts a recovery session and returns a blob with proof of recovery secret possession.
75     * The method generates a symmetric key for a session, which trusted remote device can use to
76     * return recovery key.
77     *
78     * @param verifierPublicKey Encoded {@code java.security.cert.X509Certificate} with Public key
79     *     used to create the recovery blob on the source device.
80     *     Keystore will verify the certificate using root of trust.
81     * @param vaultParams Must match the parameters in the corresponding field in the recovery blob.
82     *     Used to limit number of guesses.
83     * @param vaultChallenge Data passed from server for this recovery session and used to prevent
84     *     replay attacks
85     * @param secrets Secrets provided by user, the method only uses type and secret fields.
86     * @return The recovery claim. Claim provides a b binary blob with recovery claim. It is
87     *     encrypted with verifierPublicKey and contains a proof of user secrets, session symmetric
88     *     key and parameters necessary to identify the counter with the number of failed recovery
89     *     attempts.
90     * @throws CertificateException if the {@code verifierPublicKey} is in an incorrect
91     *     format.
92     * @throws InternalRecoveryServiceException if an unexpected error occurred in the recovery
93     *     service.
94     */
95    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
96    @NonNull public byte[] start(
97            @NonNull byte[] verifierPublicKey,
98            @NonNull byte[] vaultParams,
99            @NonNull byte[] vaultChallenge,
100            @NonNull List<KeyChainProtectionParams> secrets)
101            throws CertificateException, InternalRecoveryServiceException {
102        try {
103            byte[] recoveryClaim =
104                    mRecoveryController.getBinder().startRecoverySession(
105                            mSessionId,
106                            verifierPublicKey,
107                            vaultParams,
108                            vaultChallenge,
109                            secrets);
110            return recoveryClaim;
111        } catch (RemoteException e) {
112            throw e.rethrowFromSystemServer();
113        } catch (ServiceSpecificException e) {
114            if (e.errorCode == RecoveryController.ERROR_BAD_CERTIFICATE_FORMAT) {
115                throw new CertificateException(e.getMessage());
116            }
117            throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
118        }
119    }
120
121    /**
122     * Imports keys.
123     *
124     * @param recoveryKeyBlob Recovery blob encrypted by symmetric key generated for this session.
125     * @param applicationKeys Application keys. Key material can be decrypted using recoveryKeyBlob
126     *     and session. KeyStore only uses package names from the application info in {@link
127     *     WrappedApplicationKey}. Caller is responsibility to perform certificates check.
128     * @return Map from alias to raw key material.
129     * @throws SessionExpiredException if {@code session} has since been closed.
130     * @throws DecryptionFailedException if unable to decrypt the snapshot.
131     * @throws InternalRecoveryServiceException if an error occurs internal to the recovery service.
132     */
133    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
134    public Map<String, byte[]> recoverKeys(
135            @NonNull byte[] recoveryKeyBlob,
136            @NonNull List<WrappedApplicationKey> applicationKeys)
137            throws SessionExpiredException, DecryptionFailedException,
138            InternalRecoveryServiceException {
139        try {
140            return (Map<String, byte[]>) mRecoveryController.getBinder().recoverKeys(
141                    mSessionId, recoveryKeyBlob, applicationKeys);
142        } catch (RemoteException e) {
143            throw e.rethrowFromSystemServer();
144        } catch (ServiceSpecificException e) {
145            if (e.errorCode == RecoveryController.ERROR_DECRYPTION_FAILED) {
146                throw new DecryptionFailedException(e.getMessage());
147            }
148            if (e.errorCode == RecoveryController.ERROR_SESSION_EXPIRED) {
149                throw new SessionExpiredException(e.getMessage());
150            }
151            throw mRecoveryController.wrapUnexpectedServiceSpecificException(e);
152        }
153    }
154
155    /**
156     * An internal session ID, used by the framework to match recovery claims to snapshot responses.
157     *
158     * @hide
159     */
160    String getSessionId() {
161        return mSessionId;
162    }
163
164    /**
165     * Deletes all data associated with {@code session}. Should not be invoked directly but via
166     * {@link RecoverySession#close()}.
167     */
168    @RequiresPermission(android.Manifest.permission.RECOVER_KEYSTORE)
169    @Override
170    public void close() {
171        try {
172            mRecoveryController.getBinder().closeSession(mSessionId);
173        } catch (RemoteException | ServiceSpecificException e) {
174            Log.e(TAG, "Unexpected error trying to close session", e);
175        }
176    }
177}
178