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 try { 216 ContentResolver contentResolver = mContext.getContentResolver(); 217 int inversionEnabled = Settings.Secure.getInt(contentResolver, 218 Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED); 219 if (inversionEnabled == 1) { 220 uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_INVERTED_FLAG; 221 } 222 float fontScale = Settings.System.getFloat(contentResolver, 223 Settings.System.FONT_SCALE); 224 if (fontScale > 1.0) { 225 uiOptionsAsFlags |= UI_OPTION_ACCESSIBILITY_MAGNIFIED_FLAG; 226 } 227 } catch (SettingNotFoundException e) { 228 Log.w(TAG, "Unexpected SettingNotFoundException"); 229 } 230 return uiOptionsAsFlags; 231 } 232 isAccessibilityServiceRunning(Context context)233 private static boolean isAccessibilityServiceRunning(Context context) { 234 boolean serviceRunning = false; 235 try { 236 ContentResolver contentResolver = context.getContentResolver(); 237 int a11yEnabled = Settings.Secure.getInt(contentResolver, 238 Settings.Secure.ACCESSIBILITY_ENABLED); 239 if (a11yEnabled == 1) { 240 serviceRunning = true; 241 } 242 } catch (SettingNotFoundException e) { 243 Log.w(TAG, "Unexpected SettingNotFoundException"); 244 e.printStackTrace(); 245 } 246 return serviceRunning; 247 } 248 249 /** 250 * Requests a confirmation prompt to be presented to the user. 251 * 252 * When the prompt is no longer being presented, one of the methods in 253 * {@link ConfirmationCallback} is called on the supplied callback object. 254 * 255 * Confirmation prompts may not be available when accessibility services are running so this 256 * may fail with a {@link ConfirmationNotAvailableException} exception even if 257 * {@link #isSupported} returns {@code true}. 258 * 259 * @param executor the executor identifying the thread that will receive the callback. 260 * @param callback the callback to use when the prompt is done showing. 261 * @throws IllegalArgumentException if the prompt text is too long or malfomed. 262 * @throws ConfirmationAlreadyPresentingException if another prompt is being presented. 263 * @throws ConfirmationNotAvailableException if confirmation prompts are not supported. 264 */ presentPrompt(@onNull Executor executor, @NonNull ConfirmationCallback callback)265 public void presentPrompt(@NonNull Executor executor, @NonNull ConfirmationCallback callback) 266 throws ConfirmationAlreadyPresentingException, 267 ConfirmationNotAvailableException { 268 if (mCallback != null) { 269 throw new ConfirmationAlreadyPresentingException(); 270 } 271 if (isAccessibilityServiceRunning(mContext)) { 272 throw new ConfirmationNotAvailableException(); 273 } 274 mCallback = callback; 275 mExecutor = executor; 276 277 int uiOptionsAsFlags = getUiOptionsAsFlags(); 278 String locale = Locale.getDefault().toLanguageTag(); 279 int responseCode = mKeyStore.presentConfirmationPrompt( 280 mCallbackBinder, mPromptText.toString(), mExtraData, locale, uiOptionsAsFlags); 281 switch (responseCode) { 282 case KeyStore.CONFIRMATIONUI_OK: 283 return; 284 285 case KeyStore.CONFIRMATIONUI_OPERATION_PENDING: 286 throw new ConfirmationAlreadyPresentingException(); 287 288 case KeyStore.CONFIRMATIONUI_UNIMPLEMENTED: 289 throw new ConfirmationNotAvailableException(); 290 291 case KeyStore.CONFIRMATIONUI_UIERROR: 292 throw new IllegalArgumentException(); 293 294 default: 295 // Unexpected error code. 296 Log.w(TAG, 297 "Unexpected responseCode=" + responseCode 298 + " from presentConfirmationPrompt() call."); 299 throw new IllegalArgumentException(); 300 } 301 } 302 303 /** 304 * Cancels a prompt currently being displayed. 305 * 306 * On success, the 307 * {@link ConfirmationCallback#onCanceled onCanceled()} method on 308 * the supplied callback object will be called asynchronously. 309 * 310 * @throws IllegalStateException if no prompt is currently being presented. 311 */ cancelPrompt()312 public void cancelPrompt() { 313 int responseCode = mKeyStore.cancelConfirmationPrompt(mCallbackBinder); 314 if (responseCode == KeyStore.CONFIRMATIONUI_OK) { 315 return; 316 } else if (responseCode == KeyStore.CONFIRMATIONUI_OPERATION_PENDING) { 317 throw new IllegalStateException(); 318 } else { 319 // Unexpected error code. 320 Log.w(TAG, 321 "Unexpected responseCode=" + responseCode 322 + " from cancelConfirmationPrompt() call."); 323 throw new IllegalStateException(); 324 } 325 } 326 327 /** 328 * Checks if the device supports confirmation prompts. 329 * 330 * @param context the application context. 331 * @return true if confirmation prompts are supported by the device. 332 */ isSupported(Context context)333 public static boolean isSupported(Context context) { 334 if (isAccessibilityServiceRunning(context)) { 335 return false; 336 } 337 return KeyStore.getInstance().isConfirmationPromptSupported(); 338 } 339 } 340