blob: 5330cffee3db49617b84ed3a512bc4de17dd3c3b [file] [log] [blame]
David Zeuthena8e8b652017-10-25 14:25:55 -04001/*
2 * Copyright 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;
18
19import android.annotation.NonNull;
David Zeuthenbbb7f652018-02-26 11:04:18 -050020import android.content.ContentResolver;
David Zeuthena8e8b652017-10-25 14:25:55 -040021import android.content.Context;
David Zeuthenbbb7f652018-02-26 11:04:18 -050022import android.provider.Settings;
23import android.provider.Settings.SettingNotFoundException;
David Zeuthena8e8b652017-10-25 14:25:55 -040024import android.text.TextUtils;
25import android.util.Log;
26
27import java.util.Locale;
28import java.util.concurrent.Executor;
29
30/**
31 * Class used for displaying confirmation prompts.
32 *
33 * <p>Confirmation prompts are prompts shown to the user to confirm a given text and are
34 * implemented in a way that a positive response indicates with high confidence that the user has
35 * seen the given text, even if the Android framework (including the kernel) was
36 * compromised. Implementing confirmation prompts with these guarantees requires dedicated
37 * hardware-support and may not always be available.
38 *
39 * <p>Confirmation prompts are typically used with an external entitity - the <i>Relying Party</i> -
40 * in the following way. The setup steps are as follows:
41 * <ul>
42 * <li> Before first use, the application generates a key-pair with the
43 * {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired
44 * CONFIRMATION tag} set. Device attestation,
45 * e.g. {@link java.security.KeyStore#getCertificateChain getCertificateChain()}, is used to
46 * generate a certificate chain that includes the public key (<code>Kpub</code> in the following)
47 * of the newly generated key.
48 * <li> The application sends <code>Kpub</code> and the certificate chain resulting from device
49 * attestation to the <i>Relying Party</i>.
50 * <li> The <i>Relying Party</i> validates the certificate chain which involves checking the root
51 * certificate is what is expected (e.g. a certificate from Google), each certificate signs the
52 * next one in the chain, ending with <code>Kpub</code>, and that the attestation certificate
53 * asserts that <code>Kpub</code> has the
54 * {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired
55 * CONFIRMATION tag} set.
56 * Additionally the relying party stores <code>Kpub</code> and associates it with the device
57 * it was received from.
58 * </ul>
59 *
60 * <p>The <i>Relying Party</i> is typically an external device (for example connected via
61 * Bluetooth) or application server.
62 *
63 * <p>Before executing a transaction which requires a high assurance of user content, the
64 * application does the following:
65 * <ul>
66 * <li> The application gets a cryptographic nonce from the <i>Relying Party</i> and passes this as
67 * the <code>extraData</code> (via the Builder helper class) to the
68 * {@link #presentPrompt presentPrompt()} method. The <i>Relying Party</i> stores the nonce locally
69 * since it'll use it in a later step.
70 * <li> If the user approves the prompt a <i>Confirmation Response</i> is returned in the
David Zeuthen1870e2d2018-04-02 14:34:21 -040071 * {@link ConfirmationCallback#onConfirmed onConfirmed(byte[])} callback as the
David Zeuthena8e8b652017-10-25 14:25:55 -040072 * <code>dataThatWasConfirmed</code> parameter. This blob contains the text that was shown to the
73 * user, the <code>extraData</code> parameter, and possibly other data.
74 * <li> The application signs the <i>Confirmation Response</i> with the previously created key and
75 * sends the blob and the signature to the <i>Relying Party</i>.
76 * <li> The <i>Relying Party</i> checks that the signature was made with <code>Kpub</code> and then
77 * extracts <code>promptText</code> matches what is expected and <code>extraData</code> matches the
78 * previously created nonce. If all checks passes, the transaction is executed.
79 * </ul>
80 *
81 * <p>A common way of implementing the "<code>promptText</code> is what is expected" check in the
82 * last bullet, is to have the <i>Relying Party</i> generate <code>promptText</code> and store it
83 * along the nonce in the <code>extraData</code> blob.
84 */
David Zeuthen1870e2d2018-04-02 14:34:21 -040085public class ConfirmationPrompt {
86 private static final String TAG = "ConfirmationPrompt";
David Zeuthena8e8b652017-10-25 14:25:55 -040087
88 private CharSequence mPromptText;
89 private byte[] mExtraData;
90 private ConfirmationCallback mCallback;
91 private Executor mExecutor;
David Zeuthenbbb7f652018-02-26 11:04:18 -050092 private Context mContext;
David Zeuthena8e8b652017-10-25 14:25:55 -040093
94 private final KeyStore mKeyStore = KeyStore.getInstance();
95
96 private void doCallback(int responseCode, byte[] dataThatWasConfirmed,
97 ConfirmationCallback callback) {
98 switch (responseCode) {
99 case KeyStore.CONFIRMATIONUI_OK:
David Zeuthen1870e2d2018-04-02 14:34:21 -0400100 callback.onConfirmed(dataThatWasConfirmed);
David Zeuthena8e8b652017-10-25 14:25:55 -0400101 break;
102
103 case KeyStore.CONFIRMATIONUI_CANCELED:
David Zeuthen1870e2d2018-04-02 14:34:21 -0400104 callback.onDismissed();
David Zeuthena8e8b652017-10-25 14:25:55 -0400105 break;
106
107 case KeyStore.CONFIRMATIONUI_ABORTED:
David Zeuthen1870e2d2018-04-02 14:34:21 -0400108 callback.onCanceled();
David Zeuthena8e8b652017-10-25 14:25:55 -0400109 break;
110
111 case KeyStore.CONFIRMATIONUI_SYSTEM_ERROR:
112 callback.onError(new Exception("System error returned by ConfirmationUI."));
113 break;
114
115 default:
116 callback.onError(new Exception("Unexpected responseCode=" + responseCode
117 + " from onConfirmtionPromptCompleted() callback."));
118 break;
119 }
120 }
121
122 private final android.os.IBinder mCallbackBinder =
123 new android.security.IConfirmationPromptCallback.Stub() {
124 @Override
125 public void onConfirmationPromptCompleted(
126 int responseCode, final byte[] dataThatWasConfirmed)
127 throws android.os.RemoteException {
128 if (mCallback != null) {
129 ConfirmationCallback callback = mCallback;
130 Executor executor = mExecutor;
131 mCallback = null;
132 mExecutor = null;
133 if (executor == null) {
134 doCallback(responseCode, dataThatWasConfirmed, callback);
135 } else {
136 executor.execute(new Runnable() {
137 @Override
138 public void run() {
139 doCallback(responseCode, dataThatWasConfirmed, callback);
140 }
141 });
142 }
143 }
144 }
145 };
146
147 /**
David Zeuthen1870e2d2018-04-02 14:34:21 -0400148 * A builder that collects arguments, to be shown on the system-provided confirmation prompt.
David Zeuthena8e8b652017-10-25 14:25:55 -0400149 */
David Zeuthen1870e2d2018-04-02 14:34:21 -0400150 public static final class Builder {
David Zeuthena8e8b652017-10-25 14:25:55 -0400151
David Zeuthen1870e2d2018-04-02 14:34:21 -0400152 private Context mContext;
David Zeuthena8e8b652017-10-25 14:25:55 -0400153 private CharSequence mPromptText;
154 private byte[] mExtraData;
155
156 /**
David Zeuthen1870e2d2018-04-02 14:34:21 -0400157 * Creates a builder for the confirmation prompt.
158 *
159 * @param context the application context
David Zeuthena8e8b652017-10-25 14:25:55 -0400160 */
David Zeuthen1870e2d2018-04-02 14:34:21 -0400161 public Builder(Context context) {
162 mContext = context;
David Zeuthena8e8b652017-10-25 14:25:55 -0400163 }
164
165 /**
David Zeuthen1870e2d2018-04-02 14:34:21 -0400166 * Sets the prompt text for the prompt.
David Zeuthena8e8b652017-10-25 14:25:55 -0400167 *
168 * @param promptText the text to present in the prompt.
169 * @return the builder.
170 */
171 public Builder setPromptText(CharSequence promptText) {
172 mPromptText = promptText;
173 return this;
174 }
175
176 /**
David Zeuthen1870e2d2018-04-02 14:34:21 -0400177 * Sets the extra data for the prompt.
David Zeuthena8e8b652017-10-25 14:25:55 -0400178 *
179 * @param extraData data to include in the response data.
180 * @return the builder.
181 */
182 public Builder setExtraData(byte[] extraData) {
183 mExtraData = extraData;
184 return this;
185 }
186
187 /**
David Zeuthen1870e2d2018-04-02 14:34:21 -0400188 * Creates a {@link ConfirmationPrompt} with the arguments supplied to this builder.
David Zeuthena8e8b652017-10-25 14:25:55 -0400189 *
David Zeuthen1870e2d2018-04-02 14:34:21 -0400190 * @return a {@link ConfirmationPrompt}
David Zeuthena8e8b652017-10-25 14:25:55 -0400191 * @throws IllegalArgumentException if any of the required fields are not set.
192 */
David Zeuthen1870e2d2018-04-02 14:34:21 -0400193 public ConfirmationPrompt build() {
David Zeuthena8e8b652017-10-25 14:25:55 -0400194 if (TextUtils.isEmpty(mPromptText)) {
195 throw new IllegalArgumentException("prompt text must be set and non-empty");
196 }
197 if (mExtraData == null) {
198 throw new IllegalArgumentException("extraData must be set");
199 }
David Zeuthen1870e2d2018-04-02 14:34:21 -0400200 return new ConfirmationPrompt(mContext, mPromptText, mExtraData);
David Zeuthena8e8b652017-10-25 14:25:55 -0400201 }
202 }
203
David Zeuthen1870e2d2018-04-02 14:34:21 -0400204 private ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData) {
David Zeuthenbbb7f652018-02-26 11:04:18 -0500205 mContext = context;
David Zeuthena8e8b652017-10-25 14:25:55 -0400206 mPromptText = promptText;
207 mExtraData = extraData;
208 }
209
David Zeuthenbbb7f652018-02-26 11:04:18 -0500210 private static final int UI_OPTION_ACCESSIBILITY_INVERTED_FLAG = 1 << 0;
211 private static final int UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG = 1 << 1;
212
213 private int getUiOptionsAsFlags() {
214 int uiOptionsAsFlags = 0;
215 try {
216 ContentResolver contentResolver = mContext.getContentResolver();
217 int inversionEnabled = Settings.Secure.getInt(contentResolver,
218 Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED);
219 if (inversionEnabled == 1) {
220 uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_INVERTED_FLAG;
221 }
222 float fontScale = Settings.System.getFloat(contentResolver,
223 Settings.System.FONT_SCALE);
224 if (fontScale > 1.0) {
225 uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG;
226 }
227 } catch (SettingNotFoundException e) {
228 Log.w(TAG, "Unexpected SettingNotFoundException");
229 }
230 return uiOptionsAsFlags;
231 }
232
David Zeuthen1870e2d2018-04-02 14:34:21 -0400233 private static boolean isAccessibilityServiceRunning(Context context) {
David Zeuthene3aad1c2018-03-12 16:47:03 -0400234 boolean serviceRunning = false;
235 try {
David Zeuthen1870e2d2018-04-02 14:34:21 -0400236 ContentResolver contentResolver = context.getContentResolver();
David Zeuthene3aad1c2018-03-12 16:47:03 -0400237 int a11yEnabled = Settings.Secure.getInt(contentResolver,
238 Settings.Secure.ACCESSIBILITY_ENABLED);
239 if (a11yEnabled == 1) {
240 serviceRunning = true;
241 }
242 } catch (SettingNotFoundException e) {
243 Log.w(TAG, "Unexpected SettingNotFoundException");
244 e.printStackTrace();
245 }
246 return serviceRunning;
247 }
248
David Zeuthena8e8b652017-10-25 14:25:55 -0400249 /**
250 * Requests a confirmation prompt to be presented to the user.
251 *
252 * When the prompt is no longer being presented, one of the methods in
253 * {@link ConfirmationCallback} is called on the supplied callback object.
254 *
David Zeuthen1870e2d2018-04-02 14:34:21 -0400255 * Confirmation prompts may not be available when accessibility services are running so this
David Zeuthene3aad1c2018-03-12 16:47:03 -0400256 * may fail with a {@link ConfirmationNotAvailableException} exception even if
257 * {@link #isSupported} returns {@code true}.
258 *
David Zeuthena8e8b652017-10-25 14:25:55 -0400259 * @param executor the executor identifying the thread that will receive the callback.
David Zeuthen1870e2d2018-04-02 14:34:21 -0400260 * @param callback the callback to use when the prompt is done showing.
David Zeuthena8e8b652017-10-25 14:25:55 -0400261 * @throws IllegalArgumentException if the prompt text is too long or malfomed.
262 * @throws ConfirmationAlreadyPresentingException if another prompt is being presented.
263 * @throws ConfirmationNotAvailableException if confirmation prompts are not supported.
264 */
265 public void presentPrompt(@NonNull Executor executor, @NonNull ConfirmationCallback callback)
266 throws ConfirmationAlreadyPresentingException,
267 ConfirmationNotAvailableException {
268 if (mCallback != null) {
269 throw new ConfirmationAlreadyPresentingException();
270 }
David Zeuthen1870e2d2018-04-02 14:34:21 -0400271 if (isAccessibilityServiceRunning(mContext)) {
David Zeuthene3aad1c2018-03-12 16:47:03 -0400272 throw new ConfirmationNotAvailableException();
273 }
David Zeuthena8e8b652017-10-25 14:25:55 -0400274 mCallback = callback;
275 mExecutor = executor;
276
David Zeuthenbbb7f652018-02-26 11:04:18 -0500277 int uiOptionsAsFlags = getUiOptionsAsFlags();
David Zeuthena8e8b652017-10-25 14:25:55 -0400278 String locale = Locale.getDefault().toLanguageTag();
279 int responseCode = mKeyStore.presentConfirmationPrompt(
280 mCallbackBinder, mPromptText.toString(), mExtraData, locale, uiOptionsAsFlags);
281 switch (responseCode) {
282 case KeyStore.CONFIRMATIONUI_OK:
283 return;
284
285 case KeyStore.CONFIRMATIONUI_OPERATION_PENDING:
286 throw new ConfirmationAlreadyPresentingException();
287
288 case KeyStore.CONFIRMATIONUI_UNIMPLEMENTED:
289 throw new ConfirmationNotAvailableException();
290
291 case KeyStore.CONFIRMATIONUI_UIERROR:
292 throw new IllegalArgumentException();
293
294 default:
295 // Unexpected error code.
296 Log.w(TAG,
297 "Unexpected responseCode=" + responseCode
298 + " from presentConfirmationPrompt() call.");
299 throw new IllegalArgumentException();
300 }
301 }
302
303 /**
304 * Cancels a prompt currently being displayed.
305 *
306 * On success, the
David Zeuthen1870e2d2018-04-02 14:34:21 -0400307 * {@link ConfirmationCallback#onCanceled onCanceled()} method on
David Zeuthena8e8b652017-10-25 14:25:55 -0400308 * the supplied callback object will be called asynchronously.
309 *
310 * @throws IllegalStateException if no prompt is currently being presented.
311 */
312 public void cancelPrompt() {
313 int responseCode = mKeyStore.cancelConfirmationPrompt(mCallbackBinder);
314 if (responseCode == KeyStore.CONFIRMATIONUI_OK) {
315 return;
316 } else if (responseCode == KeyStore.CONFIRMATIONUI_OPERATION_PENDING) {
317 throw new IllegalStateException();
318 } else {
319 // Unexpected error code.
320 Log.w(TAG,
321 "Unexpected responseCode=" + responseCode
322 + " from cancelConfirmationPrompt() call.");
323 throw new IllegalStateException();
324 }
325 }
326
327 /**
328 * Checks if the device supports confirmation prompts.
329 *
David Zeuthen1870e2d2018-04-02 14:34:21 -0400330 * @param context the application context.
David Zeuthena8e8b652017-10-25 14:25:55 -0400331 * @return true if confirmation prompts are supported by the device.
332 */
David Zeuthen1870e2d2018-04-02 14:34:21 -0400333 public static boolean isSupported(Context context) {
334 if (isAccessibilityServiceRunning(context)) {
335 return false;
336 }
David Zeuthenbbb7f652018-02-26 11:04:18 -0500337 return KeyStore.getInstance().isConfirmationPromptSupported();
David Zeuthena8e8b652017-10-25 14:25:55 -0400338 }
339}