David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 1 | /* |
| 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 | |
| 17 | package android.security; |
| 18 | |
| 19 | import android.annotation.NonNull; |
David Zeuthen | bbb7f65 | 2018-02-26 11:04:18 -0500 | [diff] [blame] | 20 | import android.content.ContentResolver; |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 21 | import android.content.Context; |
David Zeuthen | bbb7f65 | 2018-02-26 11:04:18 -0500 | [diff] [blame] | 22 | import android.provider.Settings; |
| 23 | import android.provider.Settings.SettingNotFoundException; |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 24 | import android.text.TextUtils; |
| 25 | import android.util.Log; |
| 26 | |
| 27 | import java.util.Locale; |
| 28 | import 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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 71 | * {@link ConfirmationCallback#onConfirmed onConfirmed(byte[])} callback as the |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 72 | * <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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 85 | public class ConfirmationPrompt { |
| 86 | private static final String TAG = "ConfirmationPrompt"; |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 87 | |
| 88 | private CharSequence mPromptText; |
| 89 | private byte[] mExtraData; |
| 90 | private ConfirmationCallback mCallback; |
| 91 | private Executor mExecutor; |
David Zeuthen | bbb7f65 | 2018-02-26 11:04:18 -0500 | [diff] [blame] | 92 | private Context mContext; |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 93 | |
| 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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 100 | callback.onConfirmed(dataThatWasConfirmed); |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 101 | break; |
| 102 | |
| 103 | case KeyStore.CONFIRMATIONUI_CANCELED: |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 104 | callback.onDismissed(); |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 105 | break; |
| 106 | |
| 107 | case KeyStore.CONFIRMATIONUI_ABORTED: |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 108 | callback.onCanceled(); |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 109 | 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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 148 | * A builder that collects arguments, to be shown on the system-provided confirmation prompt. |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 149 | */ |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 150 | public static final class Builder { |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 151 | |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 152 | private Context mContext; |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 153 | private CharSequence mPromptText; |
| 154 | private byte[] mExtraData; |
| 155 | |
| 156 | /** |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 157 | * Creates a builder for the confirmation prompt. |
| 158 | * |
| 159 | * @param context the application context |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 160 | */ |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 161 | public Builder(Context context) { |
| 162 | mContext = context; |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 163 | } |
| 164 | |
| 165 | /** |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 166 | * Sets the prompt text for the prompt. |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 167 | * |
| 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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 177 | * Sets the extra data for the prompt. |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 178 | * |
| 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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 188 | * Creates a {@link ConfirmationPrompt} with the arguments supplied to this builder. |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 189 | * |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 190 | * @return a {@link ConfirmationPrompt} |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 191 | * @throws IllegalArgumentException if any of the required fields are not set. |
| 192 | */ |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 193 | public ConfirmationPrompt build() { |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 194 | 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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 200 | return new ConfirmationPrompt(mContext, mPromptText, mExtraData); |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 201 | } |
| 202 | } |
| 203 | |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 204 | private ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData) { |
David Zeuthen | bbb7f65 | 2018-02-26 11:04:18 -0500 | [diff] [blame] | 205 | mContext = context; |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 206 | mPromptText = promptText; |
| 207 | mExtraData = extraData; |
| 208 | } |
| 209 | |
David Zeuthen | bbb7f65 | 2018-02-26 11:04:18 -0500 | [diff] [blame] | 210 | 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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 233 | private static boolean isAccessibilityServiceRunning(Context context) { |
David Zeuthen | e3aad1c | 2018-03-12 16:47:03 -0400 | [diff] [blame] | 234 | boolean serviceRunning = false; |
| 235 | try { |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 236 | ContentResolver contentResolver = context.getContentResolver(); |
David Zeuthen | e3aad1c | 2018-03-12 16:47:03 -0400 | [diff] [blame] | 237 | 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 Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 249 | /** |
| 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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 255 | * Confirmation prompts may not be available when accessibility services are running so this |
David Zeuthen | e3aad1c | 2018-03-12 16:47:03 -0400 | [diff] [blame] | 256 | * may fail with a {@link ConfirmationNotAvailableException} exception even if |
| 257 | * {@link #isSupported} returns {@code true}. |
| 258 | * |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 259 | * @param executor the executor identifying the thread that will receive the callback. |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 260 | * @param callback the callback to use when the prompt is done showing. |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 261 | * @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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 271 | if (isAccessibilityServiceRunning(mContext)) { |
David Zeuthen | e3aad1c | 2018-03-12 16:47:03 -0400 | [diff] [blame] | 272 | throw new ConfirmationNotAvailableException(); |
| 273 | } |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 274 | mCallback = callback; |
| 275 | mExecutor = executor; |
| 276 | |
David Zeuthen | bbb7f65 | 2018-02-26 11:04:18 -0500 | [diff] [blame] | 277 | int uiOptionsAsFlags = getUiOptionsAsFlags(); |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 278 | 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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 307 | * {@link ConfirmationCallback#onCanceled onCanceled()} method on |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 308 | * 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 Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 330 | * @param context the application context. |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 331 | * @return true if confirmation prompts are supported by the device. |
| 332 | */ |
David Zeuthen | 1870e2d | 2018-04-02 14:34:21 -0400 | [diff] [blame] | 333 | public static boolean isSupported(Context context) { |
| 334 | if (isAccessibilityServiceRunning(context)) { |
| 335 | return false; |
| 336 | } |
David Zeuthen | bbb7f65 | 2018-02-26 11:04:18 -0500 | [diff] [blame] | 337 | return KeyStore.getInstance().isConfirmationPromptSupported(); |
David Zeuthen | a8e8b65 | 2017-10-25 14:25:55 -0400 | [diff] [blame] | 338 | } |
| 339 | } |