| /* |
| * 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 entity - 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. AndroidKeyStore key attestation, e.g., |
| * {@link android.security.keystore.KeyGenParameterSpec.Builder#setAttestationChallenge(byte[])} |
| * 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>Note: It is vital to check the <code>promptText</code> because this is the only part that |
| * the user has approved. To avoid writing parsers for all of the possible locales, it is |
| * recommended that the <i>Relying Party</i> uses the same string generator as used on the device |
| * and performs a simple string comparison. |
| */ |
| 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 AndroidProtectedConfirmation mProtectedConfirmation; |
| |
| private AndroidProtectedConfirmation getService() { |
| if (mProtectedConfirmation == null) { |
| mProtectedConfirmation = new AndroidProtectedConfirmation(); |
| } |
| return mProtectedConfirmation; |
| } |
| |
| private void doCallback(int responseCode, byte[] dataThatWasConfirmed, |
| ConfirmationCallback callback) { |
| switch (responseCode) { |
| case AndroidProtectedConfirmation.ERROR_OK: |
| callback.onConfirmed(dataThatWasConfirmed); |
| break; |
| |
| case AndroidProtectedConfirmation.ERROR_CANCELED: |
| callback.onDismissed(); |
| break; |
| |
| case AndroidProtectedConfirmation.ERROR_ABORTED: |
| callback.onCanceled(); |
| break; |
| |
| case AndroidProtectedConfirmation.ERROR_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.security.apc.IConfirmationCallback mConfirmationCallback = |
| new android.security.apc.IConfirmationCallback.Stub() { |
| @Override |
| public void onCompleted(int result, byte[] dataThatWasConfirmed) |
| throws android.os.RemoteException { |
| if (mCallback != null) { |
| ConfirmationCallback callback = mCallback; |
| Executor executor = mExecutor; |
| mCallback = null; |
| mExecutor = null; |
| if (executor == null) { |
| doCallback(result, dataThatWasConfirmed, callback); |
| } else { |
| executor.execute(new Runnable() { |
| @Override public void run() { |
| doCallback(result, 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 int getUiOptionsAsFlags() { |
| int uiOptionsAsFlags = 0; |
| ContentResolver contentResolver = mContext.getContentResolver(); |
| int inversionEnabled = Settings.Secure.getInt(contentResolver, |
| Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0); |
| if (inversionEnabled == 1) { |
| uiOptionsAsFlags |= AndroidProtectedConfirmation.FLAG_UI_OPTION_INVERTED; |
| } |
| float fontScale = Settings.System.getFloat(contentResolver, |
| Settings.System.FONT_SCALE, (float) 1.0); |
| if (fontScale > 1.0) { |
| uiOptionsAsFlags |= AndroidProtectedConfirmation.FLAG_UI_OPTION_MAGNIFIED; |
| } |
| 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; |
| |
| String locale = Locale.getDefault().toLanguageTag(); |
| int uiOptionsAsFlags = getUiOptionsAsFlags(); |
| int responseCode = getService().presentConfirmationPrompt( |
| mConfirmationCallback, mPromptText.toString(), mExtraData, locale, |
| uiOptionsAsFlags); |
| switch (responseCode) { |
| case AndroidProtectedConfirmation.ERROR_OK: |
| return; |
| |
| case AndroidProtectedConfirmation.ERROR_OPERATION_PENDING: |
| throw new ConfirmationAlreadyPresentingException(); |
| |
| case AndroidProtectedConfirmation.ERROR_UNIMPLEMENTED: |
| throw new ConfirmationNotAvailableException(); |
| |
| 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 = |
| getService().cancelConfirmationPrompt(mConfirmationCallback); |
| if (responseCode == AndroidProtectedConfirmation.ERROR_OK) { |
| return; |
| } else if (responseCode == AndroidProtectedConfirmation.ERROR_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 new AndroidProtectedConfirmation().isConfirmationPromptSupported(); |
| } |
| } |