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 entity - 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. AndroidKeyStore key attestation, e.g., 45 * {@link android.security.keystore.KeyGenParameterSpec.Builder#setAttestationChallenge(byte[])} 46 * is used to generate a certificate chain that includes the public key (<code>Kpub</code> in the 47 * following) 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>Note: It is vital to check the <code>promptText</code> because this is the only part that 82 * the user has approved. To avoid writing parsers for all of the possible locales, it is 83 * recommended that the <i>Relying Party</i> uses the same string generator as used on the device 84 * and performs a simple string comparison. 85 */ 86 public class ConfirmationPrompt { 87 private static final String TAG = "ConfirmationPrompt"; 88 89 private CharSequence mPromptText; 90 private byte[] mExtraData; 91 private ConfirmationCallback mCallback; 92 private Executor mExecutor; 93 private Context mContext; 94 95 private AndroidProtectedConfirmation mProtectedConfirmation; 96 getService()97 private AndroidProtectedConfirmation getService() { 98 if (mProtectedConfirmation == null) { 99 mProtectedConfirmation = new AndroidProtectedConfirmation(); 100 } 101 return mProtectedConfirmation; 102 } 103 doCallback(int responseCode, byte[] dataThatWasConfirmed, ConfirmationCallback callback)104 private void doCallback(int responseCode, byte[] dataThatWasConfirmed, 105 ConfirmationCallback callback) { 106 switch (responseCode) { 107 case AndroidProtectedConfirmation.ERROR_OK: 108 callback.onConfirmed(dataThatWasConfirmed); 109 break; 110 111 case AndroidProtectedConfirmation.ERROR_CANCELED: 112 callback.onDismissed(); 113 break; 114 115 case AndroidProtectedConfirmation.ERROR_ABORTED: 116 callback.onCanceled(); 117 break; 118 119 case AndroidProtectedConfirmation.ERROR_SYSTEM_ERROR: 120 callback.onError(new Exception("System error returned by ConfirmationUI.")); 121 break; 122 123 default: 124 callback.onError(new Exception("Unexpected responseCode=" + responseCode 125 + " from onConfirmtionPromptCompleted() callback.")); 126 break; 127 } 128 } 129 130 private final android.security.apc.IConfirmationCallback mConfirmationCallback = 131 new android.security.apc.IConfirmationCallback.Stub() { 132 @Override 133 public void onCompleted(int result, byte[] dataThatWasConfirmed) 134 throws android.os.RemoteException { 135 if (mCallback != null) { 136 ConfirmationCallback callback = mCallback; 137 Executor executor = mExecutor; 138 mCallback = null; 139 mExecutor = null; 140 if (executor == null) { 141 doCallback(result, dataThatWasConfirmed, callback); 142 } else { 143 executor.execute(new Runnable() { 144 @Override public void run() { 145 doCallback(result, dataThatWasConfirmed, callback); 146 } 147 }); 148 } 149 } 150 } 151 }; 152 153 /** 154 * A builder that collects arguments, to be shown on the system-provided confirmation prompt. 155 */ 156 public static final class Builder { 157 158 private Context mContext; 159 private CharSequence mPromptText; 160 private byte[] mExtraData; 161 162 /** 163 * Creates a builder for the confirmation prompt. 164 * 165 * @param context the application context 166 */ Builder(Context context)167 public Builder(Context context) { 168 mContext = context; 169 } 170 171 /** 172 * Sets the prompt text for the prompt. 173 * 174 * @param promptText the text to present in the prompt. 175 * @return the builder. 176 */ setPromptText(CharSequence promptText)177 public Builder setPromptText(CharSequence promptText) { 178 mPromptText = promptText; 179 return this; 180 } 181 182 /** 183 * Sets the extra data for the prompt. 184 * 185 * @param extraData data to include in the response data. 186 * @return the builder. 187 */ setExtraData(byte[] extraData)188 public Builder setExtraData(byte[] extraData) { 189 mExtraData = extraData; 190 return this; 191 } 192 193 /** 194 * Creates a {@link ConfirmationPrompt} with the arguments supplied to this builder. 195 * 196 * @return a {@link ConfirmationPrompt} 197 * @throws IllegalArgumentException if any of the required fields are not set. 198 */ build()199 public ConfirmationPrompt build() { 200 if (TextUtils.isEmpty(mPromptText)) { 201 throw new IllegalArgumentException("prompt text must be set and non-empty"); 202 } 203 if (mExtraData == null) { 204 throw new IllegalArgumentException("extraData must be set"); 205 } 206 return new ConfirmationPrompt(mContext, mPromptText, mExtraData); 207 } 208 } 209 ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData)210 private ConfirmationPrompt(Context context, CharSequence promptText, byte[] extraData) { 211 mContext = context; 212 mPromptText = promptText; 213 mExtraData = extraData; 214 } 215 getUiOptionsAsFlags()216 private int getUiOptionsAsFlags() { 217 int uiOptionsAsFlags = 0; 218 ContentResolver contentResolver = mContext.getContentResolver(); 219 int inversionEnabled = Settings.Secure.getInt(contentResolver, 220 Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED, 0); 221 if (inversionEnabled == 1) { 222 uiOptionsAsFlags |= AndroidProtectedConfirmation.FLAG_UI_OPTION_INVERTED; 223 } 224 float fontScale = Settings.System.getFloat(contentResolver, 225 Settings.System.FONT_SCALE, (float) 1.0); 226 if (fontScale > 1.0) { 227 uiOptionsAsFlags |= AndroidProtectedConfirmation.FLAG_UI_OPTION_MAGNIFIED; 228 } 229 return uiOptionsAsFlags; 230 } 231 isAccessibilityServiceRunning(Context context)232 private static boolean isAccessibilityServiceRunning(Context context) { 233 boolean serviceRunning = false; 234 try { 235 ContentResolver contentResolver = context.getContentResolver(); 236 int a11yEnabled = Settings.Secure.getInt(contentResolver, 237 Settings.Secure.ACCESSIBILITY_ENABLED); 238 if (a11yEnabled == 1) { 239 serviceRunning = true; 240 } 241 } catch (SettingNotFoundException e) { 242 Log.w(TAG, "Unexpected SettingNotFoundException"); 243 e.printStackTrace(); 244 } 245 return serviceRunning; 246 } 247 248 /** 249 * Requests a confirmation prompt to be presented to the user. 250 * 251 * When the prompt is no longer being presented, one of the methods in 252 * {@link ConfirmationCallback} is called on the supplied callback object. 253 * 254 * Confirmation prompts may not be available when accessibility services are running so this 255 * may fail with a {@link ConfirmationNotAvailableException} exception even if 256 * {@link #isSupported} returns {@code true}. 257 * 258 * @param executor the executor identifying the thread that will receive the callback. 259 * @param callback the callback to use when the prompt is done showing. 260 * @throws IllegalArgumentException if the prompt text is too long or malfomed. 261 * @throws ConfirmationAlreadyPresentingException if another prompt is being presented. 262 * @throws ConfirmationNotAvailableException if confirmation prompts are not supported. 263 */ presentPrompt(@onNull Executor executor, @NonNull ConfirmationCallback callback)264 public void presentPrompt(@NonNull Executor executor, @NonNull ConfirmationCallback callback) 265 throws ConfirmationAlreadyPresentingException, 266 ConfirmationNotAvailableException { 267 if (mCallback != null) { 268 throw new ConfirmationAlreadyPresentingException(); 269 } 270 if (isAccessibilityServiceRunning(mContext)) { 271 throw new ConfirmationNotAvailableException(); 272 } 273 mCallback = callback; 274 mExecutor = executor; 275 276 String locale = Locale.getDefault().toLanguageTag(); 277 int uiOptionsAsFlags = getUiOptionsAsFlags(); 278 int responseCode = getService().presentConfirmationPrompt( 279 mConfirmationCallback, mPromptText.toString(), mExtraData, locale, 280 uiOptionsAsFlags); 281 switch (responseCode) { 282 case AndroidProtectedConfirmation.ERROR_OK: 283 return; 284 285 case AndroidProtectedConfirmation.ERROR_OPERATION_PENDING: 286 throw new ConfirmationAlreadyPresentingException(); 287 288 case AndroidProtectedConfirmation.ERROR_UNIMPLEMENTED: 289 throw new ConfirmationNotAvailableException(); 290 291 default: 292 // Unexpected error code. 293 Log.w(TAG, 294 "Unexpected responseCode=" + responseCode 295 + " from presentConfirmationPrompt() call."); 296 throw new IllegalArgumentException(); 297 } 298 } 299 300 /** 301 * Cancels a prompt currently being displayed. 302 * 303 * On success, the 304 * {@link ConfirmationCallback#onCanceled onCanceled()} method on 305 * the supplied callback object will be called asynchronously. 306 * 307 * @throws IllegalStateException if no prompt is currently being presented. 308 */ cancelPrompt()309 public void cancelPrompt() { 310 int responseCode = 311 getService().cancelConfirmationPrompt(mConfirmationCallback); 312 if (responseCode == AndroidProtectedConfirmation.ERROR_OK) { 313 return; 314 } else if (responseCode == AndroidProtectedConfirmation.ERROR_OPERATION_PENDING) { 315 throw new IllegalStateException(); 316 } else { 317 // Unexpected error code. 318 Log.w(TAG, 319 "Unexpected responseCode=" + responseCode 320 + " from cancelConfirmationPrompt() call."); 321 throw new IllegalStateException(); 322 } 323 } 324 325 /** 326 * Checks if the device supports confirmation prompts. 327 * 328 * @param context the application context. 329 * @return true if confirmation prompts are supported by the device. 330 */ isSupported(Context context)331 public static boolean isSupported(Context context) { 332 if (isAccessibilityServiceRunning(context)) { 333 return false; 334 } 335 return new AndroidProtectedConfirmation().isConfirmationPromptSupported(); 336 } 337 } 338