summaryrefslogtreecommitdiff
path: root/android/security/ConfirmationDialog.java
blob: 1697106c378221b1feccb6d6ed68d9fdbd943e56 (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
/*
 * 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 entitity - 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. Device attestation,
 * e.g. {@link java.security.KeyStore#getCertificateChain getCertificateChain()}, 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#onConfirmedByUser onConfirmedByUser(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>A common way of implementing the "<code>promptText</code> is what is expected" check in the
 * last bullet, is to have the <i>Relying Party</i> generate <code>promptText</code> and store it
 * along the nonce in the <code>extraData</code> blob.
 */
public class ConfirmationDialog {
    private static final String TAG = "ConfirmationDialog";

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

    private final KeyStore mKeyStore = KeyStore.getInstance();

    private void doCallback(int responseCode, byte[] dataThatWasConfirmed,
            ConfirmationCallback callback) {
        switch (responseCode) {
            case KeyStore.CONFIRMATIONUI_OK:
                callback.onConfirmedByUser(dataThatWasConfirmed);
                break;

            case KeyStore.CONFIRMATIONUI_CANCELED:
                callback.onDismissedByUser();
                break;

            case KeyStore.CONFIRMATIONUI_ABORTED:
                callback.onDismissedByApplication();
                break;

            case KeyStore.CONFIRMATIONUI_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.os.IBinder mCallbackBinder =
            new android.security.IConfirmationPromptCallback.Stub() {
                @Override
                public void onConfirmationPromptCompleted(
                        int responseCode, final byte[] dataThatWasConfirmed)
                        throws android.os.RemoteException {
                    if (mCallback != null) {
                        ConfirmationCallback callback = mCallback;
                        Executor executor = mExecutor;
                        mCallback = null;
                        mExecutor = null;
                        if (executor == null) {
                            doCallback(responseCode, dataThatWasConfirmed, callback);
                        } else {
                            executor.execute(new Runnable() {
                                    @Override
                                    public void run() {
                                        doCallback(responseCode, dataThatWasConfirmed, callback);
                                    }
                                });
                        }
                    }
                }
            };

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

        private CharSequence mPromptText;
        private byte[] mExtraData;

        /**
         * Creates a builder for the confirmation dialog.
         */
        public Builder() {
        }

        /**
         * Sets the prompt text for the dialog.
         *
         * @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 dialog.
         *
         * @param extraData data to include in the response data.
         * @return the builder.
         */
        public Builder setExtraData(byte[] extraData) {
            mExtraData = extraData;
            return this;
        }

        /**
         * Creates a {@link ConfirmationDialog} with the arguments supplied to this builder.
         *
         * @param context the application context
         * @return a {@link ConfirmationDialog}
         * @throws IllegalArgumentException if any of the required fields are not set.
         */
        public ConfirmationDialog build(Context context) {
            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 ConfirmationDialog(context, mPromptText, mExtraData);
        }
    }

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

    private static final int UI_OPTION_ACCESSIBILITY_INVERTED_FLAG = 1 << 0;
    private static final int UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG = 1 << 1;

    private int getUiOptionsAsFlags() {
        int uiOptionsAsFlags = 0;
        try {
            ContentResolver contentResolver = mContext.getContentResolver();
            int inversionEnabled = Settings.Secure.getInt(contentResolver,
                    Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED);
            if (inversionEnabled == 1) {
                uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_INVERTED_FLAG;
            }
            float fontScale = Settings.System.getFloat(contentResolver,
                    Settings.System.FONT_SCALE);
            if (fontScale > 1.0) {
                uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG;
            }
        } catch (SettingNotFoundException e) {
            Log.w(TAG, "Unexpected SettingNotFoundException");
        }
        return uiOptionsAsFlags;
    }

    private boolean isAccessibilityServiceRunning() {
        boolean serviceRunning = false;
        try {
            ContentResolver contentResolver = mContext.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 dialogs 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 dialog 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()) {
            throw new ConfirmationNotAvailableException();
        }
        mCallback = callback;
        mExecutor = executor;

        int uiOptionsAsFlags = getUiOptionsAsFlags();
        String locale = Locale.getDefault().toLanguageTag();
        int responseCode = mKeyStore.presentConfirmationPrompt(
                mCallbackBinder, mPromptText.toString(), mExtraData, locale, uiOptionsAsFlags);
        switch (responseCode) {
            case KeyStore.CONFIRMATIONUI_OK:
                return;

            case KeyStore.CONFIRMATIONUI_OPERATION_PENDING:
                throw new ConfirmationAlreadyPresentingException();

            case KeyStore.CONFIRMATIONUI_UNIMPLEMENTED:
                throw new ConfirmationNotAvailableException();

            case KeyStore.CONFIRMATIONUI_UIERROR:
                throw new IllegalArgumentException();

            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#onDismissedByApplication onDismissedByApplication()} method on
     * the supplied callback object will be called asynchronously.
     *
     * @throws IllegalStateException if no prompt is currently being presented.
     */
    public void cancelPrompt() {
        int responseCode = mKeyStore.cancelConfirmationPrompt(mCallbackBinder);
        if (responseCode == KeyStore.CONFIRMATIONUI_OK) {
            return;
        } else if (responseCode == KeyStore.CONFIRMATIONUI_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.
     *
     * @return true if confirmation prompts are supported by the device.
     */
    public static boolean isSupported() {
        return KeyStore.getInstance().isConfirmationPromptSupported();
    }
}