/* * 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. * *

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. * *

Confirmation prompts are typically used with an external entitity - the Relying Party - * in the following way. The setup steps are as follows: *

* *

The Relying Party is typically an external device (for example connected via * Bluetooth) or application server. * *

Before executing a transaction which requires a high assurance of user content, the * application does the following: *

* *

A common way of implementing the "promptText is what is expected" check in the * last bullet, is to have the Relying Party generate promptText and store it * along the nonce in the extraData blob. */ 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 final KeyStore mKeyStore = KeyStore.getInstance(); private void doCallback(int responseCode, byte[] dataThatWasConfirmed, ConfirmationCallback callback) { switch (responseCode) { case KeyStore.CONFIRMATIONUI_OK: callback.onConfirmed(dataThatWasConfirmed); break; case KeyStore.CONFIRMATIONUI_CANCELED: callback.onDismissed(); break; case KeyStore.CONFIRMATIONUI_ABORTED: callback.onCanceled(); 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 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 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 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; 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#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 = 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. * * @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 KeyStore.getInstance().isConfirmationPromptSupported(); } }