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

  • Before first use, the application generates a key-pair with the CONFIRMATION tag set. Device attestation, e.g. getCertificateChain(), is used to generate a certificate chain that includes the public key (Kpub in the following) of the newly generated key.
  • The application sends Kpub and the certificate chain resulting from device attestation to the Relying Party.
  • The Relying Party 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 Kpub, and that the attestation certificate asserts that Kpub has the CONFIRMATION tag set. Additionally the relying party stores Kpub and associates it with the device it was received from.

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:

  • The application gets a cryptographic nonce from the Relying Party and passes this as the extraData (via the Builder helper class) to the presentPrompt() method. The Relying Party stores the nonce locally since it'll use it in a later step.
  • If the user approves the prompt a Confirmation Response is returned in the onConfirmed(byte[]) callback as the dataThatWasConfirmed parameter. This blob contains the text that was shown to the user, the extraData parameter, and possibly other data.
  • The application signs the Confirmation Response with the previously created key and sends the blob and the signature to the Relying Party.
  • The Relying Party checks that the signature was made with Kpub and then extracts promptText matches what is expected and extraData matches the previously created nonce. If all checks passes, the transaction is executed.

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.

/** * 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#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>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 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.
/** * 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.
Params:
  • context – the application context
/** * 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.
Params:
  • promptText – the text to present in the prompt.
Returns:the builder.
/** * 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.
Params:
  • extraData – data to include in the response data.
Returns:the builder.
/** * 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 ConfirmationPrompt with the arguments supplied to this builder.
Throws:
Returns:a ConfirmationPrompt
/** * 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 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 ConfirmationNotAvailableException exception even if isSupported returns true.
Params:
  • executor – the executor identifying the thread that will receive the callback.
  • callback – the callback to use when the prompt is done showing.
Throws:
/** * 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 onCanceled() method on the supplied callback object will be called asynchronously.
Throws:
/** * 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.
Params:
  • context – the application context.
Returns:true if confirmation prompts are supported by the device.
/** * 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(); } }