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