/* * 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.Context; 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: *
Kpub in the following)
* of the newly generated key.
* Kpub and the certificate chain resulting from device
* attestation to the Relying Party.
* Kpub, and that the attestation certificate
* asserts that Kpub has the
* {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired
* 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: *
extraData (via the Builder helper class) to the
* {@link #presentPrompt presentPrompt()} method. The Relying Party stores the nonce locally
* since it'll use it in a later step.
* dataThatWasConfirmed parameter. This blob contains the text that was shown to the
* user, the extraData parameter, and possibly other data.
* 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.
*/
public class ConfirmationDialog {
private static final String TAG = "ConfirmationDialog";
private CharSequence mPromptText;
private byte[] mExtraData;
private ConfirmationCallback mCallback;
private Executor mExecutor;
private final KeyStore mKeyStore = KeyStore.getInstance();
private void doCallback(int responseCode, byte[] dataThatWasConfirmed,
ConfirmationCallback callback) {
switch (responseCode) {
case KeyStore.CONFIRMATIONUI_OK:
callback.onConfirmedByUser(dataThatWasConfirmed);
break;
case KeyStore.CONFIRMATIONUI_CANCELED:
callback.onDismissedByUser();
break;
case KeyStore.CONFIRMATIONUI_ABORTED:
callback.onDismissedByApplication();
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 dialog.
*/
public static class Builder {
private CharSequence mPromptText;
private byte[] mExtraData;
/**
* Creates a builder for the confirmation dialog.
*/
public Builder() {
}
/**
* Sets the prompt text for the dialog.
*
* @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 dialog.
*
* @param extraData data to include in the response data.
* @return the builder.
*/
public Builder setExtraData(byte[] extraData) {
mExtraData = extraData;
return this;
}
/**
* Creates a {@link ConfirmationDialog} with the arguments supplied to this builder.
*
* @param context the application context
* @return a {@link ConfirmationDialog}
* @throws IllegalArgumentException if any of the required fields are not set.
*/
public ConfirmationDialog build(Context context) {
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 ConfirmationDialog(mPromptText, mExtraData);
}
}
private ConfirmationDialog(CharSequence promptText, byte[] extraData) {
mPromptText = promptText;
mExtraData = extraData;
}
/**
* 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.
*
* @param executor the executor identifying the thread that will receive the callback.
* @param callback the callback to use when the dialog 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();
}
mCallback = callback;
mExecutor = executor;
int uiOptionsAsFlags = 0;
// TODO: set AccessibilityInverted, AccessibilityMagnified in uiOptionsAsFlags as needed.
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#onDismissedByApplication onDismissedByApplication()} 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.
*
* @return true if confirmation prompts are supported by the device.
*/
public static boolean isSupported() {
// TODO: read and return system property.
return true;
}
}