summaryrefslogtreecommitdiff
path: root/core/java/android/security/ConfirmationPrompt.java
blob: f626149b03c48edfa4a5f5efa9589572974e2a4b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
/*
 * Copyright 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.security;

import android.annotation.NonNull;
import android.content.ContentResolver;
import android.content.Context;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.text.TextUtils;
import android.util.Log;

import java.util.Locale;
import java.util.concurrent.Executor;

/**
 * Class used for displaying confirmation prompts.
 *
 * <p>Confirmation prompts are prompts shown to the user to confirm a given text and are
 * implemented in a way that a positive response indicates with high confidence that the user has
 * seen the given text, even if the Android framework (including the kernel) was
 * compromised. Implementing confirmation prompts with these guarantees requires dedicated
 * hardware-support and may not always be available.
 *
 * <p>Confirmation prompts are typically used with an external entity - the <i>Relying Party</i> -
 * in the following way. The setup steps are as follows:
 * <ul>
 * <li> Before first use, the application generates a key-pair with the
 * {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired
 * CONFIRMATION tag} set. AndroidKeyStore key attestation, e.g.,
 * {@link android.security.keystore.KeyGenParameterSpec.Builder#setAttestationChallenge(byte[])}
 * is used to generate a certificate chain that includes the public key (<code>Kpub</code> in the
 * following) of the newly generated key.
 * <li> The application sends <code>Kpub</code> and the certificate chain resulting from device
 * attestation to the <i>Relying Party</i>.
 * <li> The <i>Relying Party</i> validates the certificate chain which involves checking the root
 * certificate is what is expected (e.g. a certificate from Google), each certificate signs the
 * next one in the chain, ending with <code>Kpub</code>, and that the attestation certificate
 * asserts that <code>Kpub</code> has the
 * {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired
 * CONFIRMATION tag} set.
 * Additionally the relying party stores <code>Kpub</code> and associates it with the device
 * it was received from.
 * </ul>
 *
 * <p>The <i>Relying Party</i> is typically an external device (for example connected via
 * Bluetooth) or application server.
 *
 * <p>Before executing a transaction which requires a high assurance of user content, the
 * application does the following:
 * <ul>
 * <li> The application gets a cryptographic nonce from the <i>Relying Party</i> and passes this as
 * the <code>extraData</code> (via the Builder helper class) to the
 * {@link #presentPrompt presentPrompt()} method. The <i>Relying Party</i> stores the nonce locally
 * since it'll use it in a later step.
 * <li> If the user approves the prompt a <i>Confirmation Response</i> is returned in the
 * {@link ConfirmationCallback#onConfirmed onConfirmed(byte[])} callback as the
 * <code>dataThatWasConfirmed</code> parameter. This blob contains the text that was shown to the
 * user, the <code>extraData</code> parameter, and possibly other data.
 * <li> The application signs the <i>Confirmation Response</i> with the previously created key and
 * sends the blob and the signature to the <i>Relying Party</i>.
 * <li> The <i>Relying Party</i> checks that the signature was made with <code>Kpub</code> and then
 * extracts <code>promptText</code> matches what is expected and <code>extraData</code> matches the
 * previously created nonce. If all checks passes, the transaction is executed.
 * </ul>
 *
 * <p>Note: It is vital to check the <code>promptText</code> because this is the only part that
 * the user has approved. To avoid writing parsers for all of the possible locales, it is
 * recommended that the <i>Relying Party</i> uses the same string generator as used on the device
 * and performs a simple string comparison.
 */
public class ConfirmationPrompt {
    private static final String TAG = "ConfirmationPrompt";

    private CharSequence mPromptText;
    private byte[] mExtraData;
    private ConfirmationCallback mCallback;
    private Executor mExecutor;
    private Context mContext;

    private AndroidProtectedConfirmation mProtectedConfirmation;

    private AndroidProtectedConfirmation getService() {
        if (mProtectedConfirmation == null) {
            mProtectedConfirmation = new AndroidProtectedConfirmation();
        }
        return mProtectedConfirmation;
    }

    private void doCallback(int responseCode, byte[] dataThatWasConfirmed,
            ConfirmationCallback callback) {
        switch (responseCode) {
            case AndroidProtectedConfirmation.ERROR_OK:
                callback.onConfirmed(dataThatWasConfirmed);
                break;

            case AndroidProtectedConfirmation.ERROR_CANCELED:
                callback.onDismissed();
                break;

            case AndroidProtectedConfirmation.ERROR_ABORTED:
                callback.onCanceled();
                break;

            case AndroidProtectedConfirmation.ERROR_SYSTEM_ERROR:
                callback.onError(new Exception("System error returned by ConfirmationUI."));
                break;

            default:
                callback.onError(new Exception("Unexpected responseCode=" + responseCode
                        + " from onConfirmtionPromptCompleted() callback."));
                break;
        }
    }

    private final android.security.apc.IConfirmationCallback mConfirmationCallback =
            new android.security.apc.IConfirmationCallback.Stub() {
                @Override
                public void onCompleted(int result, byte[] dataThatWasConfirmed)
                        throws android.os.RemoteException {
                    if (mCallback != null) {
                        ConfirmationCallback callback = mCallback;
                        Executor executor = mExecutor;
                        mCallback = null;
                        mExecutor = null;
                        if (executor == null) {
                            doCallback(result, dataThatWasConfirmed, callback);
                        } else {
                            executor.execute(new Runnable() {
                                @Override public void run() {
                                    doCallback(result, dataThatWasConfirmed, callback);
                                }
                            });
                        }
                    }
                }
            };

    /**
     * A builder that collects arguments, to be shown on the system-provided confirmation prompt.
     */
    public static final class Builder {

        private Context mContext;
        private CharSequence mPromptText;
        private byte[] mExtraData;

        /**
         * Creates a builder for the confirmation prompt.
         *
         * @param context the application context
         */
        public Builder(Context context) {
            mContext = context;
        }

        /**
         * Sets the prompt text for the prompt.
         *
         * @param promptText the text to present in the prompt.
         * @return the builder.
         */
        public Builder setPromptText(CharSequence promptText) {
            mPromptText = promptText;
            return this;
        }

        /**
         * Sets the extra data for the prompt.
         *
         * @param extraData data to include in the response data.
         * @return the builder.
         */
        public Builder setExtraData(byte[] extraData) {
            mExtraData = extraData;
            return this;
        }

        /**
         * Creates a {@link ConfirmationPrompt} with the arguments supplied to this builder.
         *
         * @return a {@link ConfirmationPrompt}
         * @throws IllegalArgumentException if any of the required fields are not set.
         */
        public ConfirmationPrompt build() {
            if (TextUtils.isEmpty(mPromptText)) {
                throw new IllegalArgumentException("prompt text must be set and non-empty");
            }
            if (mExtraData == null) {
                throw new IllegalArgumentException("extraData must be set");
            }
            return new ConfirmationPrompt(mContext, mPromptText, mExtraData);
        }
    }

    private ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData) {
        mContext = context;
        mPromptText = promptText;
        mExtraData = extraData;
    }

    private int getUiOptionsAsFlags() {
        int uiOptionsAsFlags = 0;
        ContentResolver contentResolver = mContext.getContentResolver();
        int inversionEnabled = Settings.Secure.getInt(contentResolver,
                Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0);
        if (inversionEnabled == 1) {
            uiOptionsAsFlags |= AndroidProtectedConfirmation.FLAG_UI_OPTION_INVERTED;
        }
        float fontScale = Settings.System.getFloat(contentResolver,
                Settings.System.FONT_SCALE, (float) 1.0);
        if (fontScale > 1.0) {
            uiOptionsAsFlags |= AndroidProtectedConfirmation.FLAG_UI_OPTION_MAGNIFIED;
        }
        return uiOptionsAsFlags;
    }

    private static boolean isAccessibilityServiceRunning(Context context) {
        boolean serviceRunning = false;
        try {
            ContentResolver contentResolver = context.getContentResolver();
            int a11yEnabled = Settings.Secure.getInt(contentResolver,
                    Settings.Secure.ACCESSIBILITY_ENABLED);
            if (a11yEnabled == 1) {
                serviceRunning = true;
            }
        } catch (SettingNotFoundException e) {
            Log.w(TAG, "Unexpected SettingNotFoundException");
            e.printStackTrace();
        }
        return serviceRunning;
    }

    /**
     * Requests a confirmation prompt to be presented to the user.
     *
     * When the prompt is no longer being presented, one of the methods in
     * {@link ConfirmationCallback} is called on the supplied callback object.
     *
     * Confirmation prompts may not be available when accessibility services are running so this
     * may fail with a {@link ConfirmationNotAvailableException} exception even if
     * {@link #isSupported} returns {@code true}.
     *
     * @param executor the executor identifying the thread that will receive the callback.
     * @param callback the callback to use when the prompt is done showing.
     * @throws IllegalArgumentException if the prompt text is too long or malfomed.
     * @throws ConfirmationAlreadyPresentingException if another prompt is being presented.
     * @throws ConfirmationNotAvailableException if confirmation prompts are not supported.
     */
    public void presentPrompt(@NonNull Executor executor, @NonNull ConfirmationCallback callback)
            throws ConfirmationAlreadyPresentingException,
            ConfirmationNotAvailableException {
        if (mCallback != null) {
            throw new ConfirmationAlreadyPresentingException();
        }
        if (isAccessibilityServiceRunning(mContext)) {
            throw new ConfirmationNotAvailableException();
        }
        mCallback = callback;
        mExecutor = executor;

        String locale = Locale.getDefault().toLanguageTag();
        int uiOptionsAsFlags = getUiOptionsAsFlags();
        int responseCode = getService().presentConfirmationPrompt(
                mConfirmationCallback, mPromptText.toString(), mExtraData, locale,
                uiOptionsAsFlags);
        switch (responseCode) {
            case AndroidProtectedConfirmation.ERROR_OK:
                return;

            case AndroidProtectedConfirmation.ERROR_OPERATION_PENDING:
                throw new ConfirmationAlreadyPresentingException();

            case AndroidProtectedConfirmation.ERROR_UNIMPLEMENTED:
                throw new ConfirmationNotAvailableException();

            default:
                // Unexpected error code.
                Log.w(TAG,
                        "Unexpected responseCode=" + responseCode
                                + " from presentConfirmationPrompt() call.");
                throw new IllegalArgumentException();
        }
    }

    /**
     * Cancels a prompt currently being displayed.
     *
     * On success, the
     * {@link ConfirmationCallback#onCanceled onCanceled()} method on
     * the supplied callback object will be called asynchronously.
     *
     * @throws IllegalStateException if no prompt is currently being presented.
     */
    public void cancelPrompt() {
        int responseCode =
                getService().cancelConfirmationPrompt(mConfirmationCallback);
        if (responseCode == AndroidProtectedConfirmation.ERROR_OK) {
            return;
        } else if (responseCode == AndroidProtectedConfirmation.ERROR_OPERATION_PENDING) {
            throw new IllegalStateException();
        } else {
            // Unexpected error code.
            Log.w(TAG,
                    "Unexpected responseCode=" + responseCode
                            + " from cancelConfirmationPrompt() call.");
            throw new IllegalStateException();
        }
    }

    /**
     * Checks if the device supports confirmation prompts.
     *
     * @param context the application context.
     * @return true if confirmation prompts are supported by the device.
     */
    public static boolean isSupported(Context context) {
        if (isAccessibilityServiceRunning(context)) {
            return false;
        }
        return new AndroidProtectedConfirmation().isConfirmationPromptSupported();
    }
}