338 lines
14 KiB
Java
338 lines
14 KiB
Java
/*
|
|
* 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();
|
|
}
|
|
}
|