1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339
|
/*
* 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 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.
*/
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();
}
}
|