1 /* 2 * Copyright 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.security; 18 19 import android.annotation.NonNull; 20 import android.content.ContentResolver; 21 import android.content.Context; 22 import android.provider.Settings; 23 import android.provider.Settings.SettingNotFoundException; 24 import android.text.TextUtils; 25 import android.util.Log; 26 27 import java.util.Locale; 28 import java.util.concurrent.Executor; 29 30 /** 31 * Class used for displaying confirmation prompts. 32 * 33 * <p>Confirmation prompts are prompts shown to the user to confirm a given text and are 34 * implemented in a way that a positive response indicates with high confidence that the user has 35 * seen the given text, even if the Android framework (including the kernel) was 36 * compromised. Implementing confirmation prompts with these guarantees requires dedicated 37 * hardware-support and may not always be available. 38 * 39 * <p>Confirmation prompts are typically used with an external entitity - the <i>Relying Party</i> - 40 * in the following way. The setup steps are as follows: 41 * <ul> 42 * <li> Before first use, the application generates a key-pair with the 43 * {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired 44 * CONFIRMATION tag} set. Device attestation, 45 * e.g. {@link java.security.KeyStore#getCertificateChain getCertificateChain()}, is used to 46 * generate a certificate chain that includes the public key (<code>Kpub</code> in the following) 47 * of the newly generated key. 48 * <li> The application sends <code>Kpub</code> and the certificate chain resulting from device 49 * attestation to the <i>Relying Party</i>. 50 * <li> The <i>Relying Party</i> validates the certificate chain which involves checking the root 51 * certificate is what is expected (e.g. a certificate from Google), each certificate signs the 52 * next one in the chain, ending with <code>Kpub</code>, and that the attestation certificate 53 * asserts that <code>Kpub</code> has the 54 * {@link android.security.keystore.KeyGenParameterSpec.Builder#setUserConfirmationRequired 55 * CONFIRMATION tag} set. 56 * Additionally the relying party stores <code>Kpub</code> and associates it with the device 57 * it was received from. 58 * </ul> 59 * 60 * <p>The <i>Relying Party</i> is typically an external device (for example connected via 61 * Bluetooth) or application server. 62 * 63 * <p>Before executing a transaction which requires a high assurance of user content, the 64 * application does the following: 65 * <ul> 66 * <li> The application gets a cryptographic nonce from the <i>Relying Party</i> and passes this as 67 * the <code>extraData</code> (via the Builder helper class) to the 68 * {@link #presentPrompt presentPrompt()} method. The <i>Relying Party</i> stores the nonce locally 69 * since it'll use it in a later step. 70 * <li> If the user approves the prompt a <i>Confirmation Response</i> is returned in the 71 * {@link ConfirmationCallback#onConfirmed onConfirmed(byte[])} callback as the 72 * <code>dataThatWasConfirmed</code> parameter. This blob contains the text that was shown to the 73 * user, the <code>extraData</code> parameter, and possibly other data. 74 * <li> The application signs the <i>Confirmation Response</i> with the previously created key and 75 * sends the blob and the signature to the <i>Relying Party</i>. 76 * <li> The <i>Relying Party</i> checks that the signature was made with <code>Kpub</code> and then 77 * extracts <code>promptText</code> matches what is expected and <code>extraData</code> matches the 78 * previously created nonce. If all checks passes, the transaction is executed. 79 * </ul> 80 * 81 * <p>A common way of implementing the "<code>promptText</code> is what is expected" check in the 82 * last bullet, is to have the <i>Relying Party</i> generate <code>promptText</code> and store it 83 * along the nonce in the <code>extraData</code> blob. 84 */ 85 public class ConfirmationPrompt { 86 private static final String TAG = "ConfirmationPrompt"; 87 88 private CharSequence mPromptText; 89 private byte[] mExtraData; 90 private ConfirmationCallback mCallback; 91 private Executor mExecutor; 92 private Context mContext; 93 94 private final KeyStore mKeyStore = KeyStore.getInstance(); 95 doCallback(int responseCode, byte[] dataThatWasConfirmed, ConfirmationCallback callback)96 private void doCallback(int responseCode, byte[] dataThatWasConfirmed, 97 ConfirmationCallback callback) { 98 switch (responseCode) { 99 case KeyStore.CONFIRMATIONUI_OK: 100 callback.onConfirmed(dataThatWasConfirmed); 101 break; 102 103 case KeyStore.CONFIRMATIONUI_CANCELED: 104 callback.onDismissed(); 105 break; 106 107 case KeyStore.CONFIRMATIONUI_ABORTED: 108 callback.onCanceled(); 109 break; 110 111 case KeyStore.CONFIRMATIONUI_SYSTEM_ERROR: 112 callback.onError(new Exception("System error returned by ConfirmationUI.")); 113 break; 114 115 default: 116 callback.onError(new Exception("Unexpected responseCode=" + responseCode 117 + " from onConfirmtionPromptCompleted() callback.")); 118 break; 119 } 120 } 121 122 private final android.os.IBinder mCallbackBinder = 123 new android.security.IConfirmationPromptCallback.Stub() { 124 @Override 125 public void onConfirmationPromptCompleted( 126 int responseCode, final byte[] dataThatWasConfirmed) 127 throws android.os.RemoteException { 128 if (mCallback != null) { 129 ConfirmationCallback callback = mCallback; 130 Executor executor = mExecutor; 131 mCallback = null; 132 mExecutor = null; 133 if (executor == null) { 134 doCallback(responseCode, dataThatWasConfirmed, callback); 135 } else { 136 executor.execute(new Runnable() { 137 @Override 138 public void run() { 139 doCallback(responseCode, dataThatWasConfirmed, callback); 140 } 141 }); 142 } 143 } 144 } 145 }; 146 147 /** 148 * A builder that collects arguments, to be shown on the system-provided confirmation prompt. 149 */ 150 public static final class Builder { 151 152 private Context mContext; 153 private CharSequence mPromptText; 154 private byte[] mExtraData; 155 156 /** 157 * Creates a builder for the confirmation prompt. 158 * 159 * @param context the application context 160 */ Builder(Context context)161 public Builder(Context context) { 162 mContext = context; 163 } 164 165 /** 166 * Sets the prompt text for the prompt. 167 * 168 * @param promptText the text to present in the prompt. 169 * @return the builder. 170 */ setPromptText(CharSequence promptText)171 public Builder setPromptText(CharSequence promptText) { 172 mPromptText = promptText; 173 return this; 174 } 175 176 /** 177 * Sets the extra data for the prompt. 178 * 179 * @param extraData data to include in the response data. 180 * @return the builder. 181 */ setExtraData(byte[] extraData)182 public Builder setExtraData(byte[] extraData) { 183 mExtraData = extraData; 184 return this; 185 } 186 187 /** 188 * Creates a {@link ConfirmationPrompt} with the arguments supplied to this builder. 189 * 190 * @return a {@link ConfirmationPrompt} 191 * @throws IllegalArgumentException if any of the required fields are not set. 192 */ build()193 public ConfirmationPrompt build() { 194 if (TextUtils.isEmpty(mPromptText)) { 195 throw new IllegalArgumentException("prompt text must be set and non-empty"); 196 } 197 if (mExtraData == null) { 198 throw new IllegalArgumentException("extraData must be set"); 199 } 200 return new ConfirmationPrompt(mContext, mPromptText, mExtraData); 201 } 202 } 203 ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData)204 private ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData) { 205 mContext = context; 206 mPromptText = promptText; 207 mExtraData = extraData; 208 } 209 210 private static final int UI_OPTION_ACCESSIBILITY_INVERTED_FLAG = 1 << 0; 211 private static final int UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG = 1 << 1; 212 getUiOptionsAsFlags()213 private int getUiOptionsAsFlags() { 214 int uiOptionsAsFlags = 0; 215 ContentResolver contentResolver = mContext.getContentResolver(); 216 int inversionEnabled = Settings.Secure.getInt(contentResolver, 217 Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0); 218 if (inversionEnabled == 1) { 219 uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_INVERTED_FLAG; 220 } 221 float fontScale = Settings.System.getFloat(contentResolver, 222 Settings.System.FONT_SCALE, (float) 1.0); 223 if (fontScale > 1.0) { 224 uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG; 225 } 226 return uiOptionsAsFlags; 227 } 228 isAccessibilityServiceRunning(Context context)229 private static boolean isAccessibilityServiceRunning(Context context) { 230 boolean serviceRunning = false; 231 try { 232 ContentResolver contentResolver = context.getContentResolver(); 233 int a11yEnabled = Settings.Secure.getInt(contentResolver, 234 Settings.Secure.ACCESSIBILITY_ENABLED); 235 if (a11yEnabled == 1) { 236 serviceRunning = true; 237 } 238 } catch (SettingNotFoundException e) { 239 Log.w(TAG, "Unexpected SettingNotFoundException"); 240 e.printStackTrace(); 241 } 242 return serviceRunning; 243 } 244 245 /** 246 * Requests a confirmation prompt to be presented to the user. 247 * 248 * When the prompt is no longer being presented, one of the methods in 249 * {@link ConfirmationCallback} is called on the supplied callback object. 250 * 251 * Confirmation prompts may not be available when accessibility services are running so this 252 * may fail with a {@link ConfirmationNotAvailableException} exception even if 253 * {@link #isSupported} returns {@code true}. 254 * 255 * @param executor the executor identifying the thread that will receive the callback. 256 * @param callback the callback to use when the prompt is done showing. 257 * @throws IllegalArgumentException if the prompt text is too long or malfomed. 258 * @throws ConfirmationAlreadyPresentingException if another prompt is being presented. 259 * @throws ConfirmationNotAvailableException if confirmation prompts are not supported. 260 */ presentPrompt(@onNull Executor executor, @NonNull ConfirmationCallback callback)261 public void presentPrompt(@NonNull Executor executor, @NonNull ConfirmationCallback callback) 262 throws ConfirmationAlreadyPresentingException, 263 ConfirmationNotAvailableException { 264 if (mCallback != null) { 265 throw new ConfirmationAlreadyPresentingException(); 266 } 267 if (isAccessibilityServiceRunning(mContext)) { 268 throw new ConfirmationNotAvailableException(); 269 } 270 mCallback = callback; 271 mExecutor = executor; 272 273 int uiOptionsAsFlags = getUiOptionsAsFlags(); 274 String locale = Locale.getDefault().toLanguageTag(); 275 int responseCode = mKeyStore.presentConfirmationPrompt( 276 mCallbackBinder, mPromptText.toString(), mExtraData, locale, uiOptionsAsFlags); 277 switch (responseCode) { 278 case KeyStore.CONFIRMATIONUI_OK: 279 return; 280 281 case KeyStore.CONFIRMATIONUI_OPERATION_PENDING: 282 throw new ConfirmationAlreadyPresentingException(); 283 284 case KeyStore.CONFIRMATIONUI_UNIMPLEMENTED: 285 throw new ConfirmationNotAvailableException(); 286 287 case KeyStore.CONFIRMATIONUI_UIERROR: 288 throw new IllegalArgumentException(); 289 290 default: 291 // Unexpected error code. 292 Log.w(TAG, 293 "Unexpected responseCode=" + responseCode 294 + " from presentConfirmationPrompt() call."); 295 throw new IllegalArgumentException(); 296 } 297 } 298 299 /** 300 * Cancels a prompt currently being displayed. 301 * 302 * On success, the 303 * {@link ConfirmationCallback#onCanceled onCanceled()} method on 304 * the supplied callback object will be called asynchronously. 305 * 306 * @throws IllegalStateException if no prompt is currently being presented. 307 */ cancelPrompt()308 public void cancelPrompt() { 309 int responseCode = mKeyStore.cancelConfirmationPrompt(mCallbackBinder); 310 if (responseCode == KeyStore.CONFIRMATIONUI_OK) { 311 return; 312 } else if (responseCode == KeyStore.CONFIRMATIONUI_OPERATION_PENDING) { 313 throw new IllegalStateException(); 314 } else { 315 // Unexpected error code. 316 Log.w(TAG, 317 "Unexpected responseCode=" + responseCode 318 + " from cancelConfirmationPrompt() call."); 319 throw new IllegalStateException(); 320 } 321 } 322 323 /** 324 * Checks if the device supports confirmation prompts. 325 * 326 * @param context the application context. 327 * @return true if confirmation prompts are supported by the device. 328 */ isSupported(Context context)329 public static boolean isSupported(Context context) { 330 if (isAccessibilityServiceRunning(context)) { 331 return false; 332 } 333 return KeyStore.getInstance().isConfirmationPromptSupported(); 334 } 335 } 336