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