1 // Copyright 2015 The Chromium Authors 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.base; 6 7 import android.app.PendingIntent; 8 import android.content.ActivityNotFoundException; 9 import android.content.ComponentName; 10 import android.content.Context; 11 import android.content.Intent; 12 import android.os.BadParcelableException; 13 import android.os.Binder; 14 import android.os.Build; 15 import android.os.Bundle; 16 import android.os.IBinder; 17 import android.os.Parcel; 18 import android.os.Parcelable; 19 import android.os.TransactionTooLargeException; 20 import android.text.TextUtils; 21 22 import androidx.annotation.Nullable; 23 24 import org.chromium.base.compat.ApiHelperForM; 25 import org.chromium.base.compat.ApiHelperForS; 26 27 import java.io.Serializable; 28 import java.util.ArrayList; 29 30 /** Utilities dealing with extracting information from intents and creating common intents. */ 31 public class IntentUtils { 32 private static final String TAG = "IntentUtils"; 33 34 /** The scheme for referrer coming from an application. */ 35 public static final String ANDROID_APP_REFERRER_SCHEME = "android-app"; 36 37 /** Intent extra used to identify the sending application. */ 38 public static final String TRUSTED_APPLICATION_CODE_EXTRA = "trusted_application_code_extra"; 39 40 /** Fake ComponentName used in constructing TRUSTED_APPLICATION_CODE_EXTRA. */ 41 private static ComponentName sFakeComponentName; 42 43 private static final Object COMPONENT_NAME_LOCK = new Object(); 44 45 private static boolean sForceTrustedIntentForTesting; 46 47 /** Just like {@link Intent#hasExtra(String)} but doesn't throw exceptions. */ safeHasExtra(Intent intent, String name)48 public static boolean safeHasExtra(Intent intent, String name) { 49 try { 50 return intent.hasExtra(name); 51 } catch (Throwable t) { 52 // Catches un-parceling exceptions. 53 Log.e(TAG, "hasExtra failed on intent " + intent); 54 return false; 55 } 56 } 57 58 /** Just like {@link Intent#removeExtra(String)} but doesn't throw exceptions. */ safeRemoveExtra(Intent intent, String name)59 public static void safeRemoveExtra(Intent intent, String name) { 60 try { 61 intent.removeExtra(name); 62 } catch (Throwable t) { 63 // Catches un-parceling exceptions. 64 Log.e(TAG, "removeExtra failed on intent " + intent); 65 } 66 } 67 68 /** Just like {@link Intent#getBooleanExtra(String, boolean)} but doesn't throw exceptions. */ safeGetBooleanExtra(Intent intent, String name, boolean defaultValue)69 public static boolean safeGetBooleanExtra(Intent intent, String name, boolean defaultValue) { 70 try { 71 return intent.getBooleanExtra(name, defaultValue); 72 } catch (Throwable t) { 73 // Catches un-parceling exceptions. 74 Log.e(TAG, "getBooleanExtra failed on intent " + intent); 75 return defaultValue; 76 } 77 } 78 79 /** Just like {@link Bundle#getBoolean(String, boolean)} but doesn't throw exceptions. */ safeGetBoolean(Bundle bundle, String name, boolean defaultValue)80 public static boolean safeGetBoolean(Bundle bundle, String name, boolean defaultValue) { 81 try { 82 return bundle.getBoolean(name, defaultValue); 83 } catch (Throwable t) { 84 // Catches un-parceling exceptions. 85 Log.e(TAG, "getBoolean failed on bundle " + bundle); 86 return defaultValue; 87 } 88 } 89 90 /** Just like {@link Intent#getIntExtra(String, int)} but doesn't throw exceptions. */ safeGetIntExtra(Intent intent, String name, int defaultValue)91 public static int safeGetIntExtra(Intent intent, String name, int defaultValue) { 92 try { 93 return intent.getIntExtra(name, defaultValue); 94 } catch (Throwable t) { 95 // Catches un-parceling exceptions. 96 Log.e(TAG, "getIntExtra failed on intent " + intent); 97 return defaultValue; 98 } 99 } 100 101 /** Just like {@link Bundle#getInt(String, int)} but doesn't throw exceptions. */ safeGetInt(Bundle bundle, String name, int defaultValue)102 public static int safeGetInt(Bundle bundle, String name, int defaultValue) { 103 try { 104 return bundle.getInt(name, defaultValue); 105 } catch (Throwable t) { 106 // Catches un-parceling exceptions. 107 Log.e(TAG, "getInt failed on bundle " + bundle); 108 return defaultValue; 109 } 110 } 111 112 /** Just like {@link Intent#getIntArrayExtra(String)} but doesn't throw exceptions. */ safeGetIntArrayExtra(Intent intent, String name)113 public static int[] safeGetIntArrayExtra(Intent intent, String name) { 114 try { 115 return intent.getIntArrayExtra(name); 116 } catch (Throwable t) { 117 // Catches un-parceling exceptions. 118 Log.e(TAG, "getIntArrayExtra failed on intent " + intent); 119 return null; 120 } 121 } 122 123 /** Just like {@link Bundle#getIntArray(String)} but doesn't throw exceptions. */ safeGetIntArray(Bundle bundle, String name)124 public static int[] safeGetIntArray(Bundle bundle, String name) { 125 try { 126 return bundle.getIntArray(name); 127 } catch (Throwable t) { 128 // Catches un-parceling exceptions. 129 Log.e(TAG, "getIntArray failed on bundle " + bundle); 130 return null; 131 } 132 } 133 134 /** Just like {@link Bundle#getFloatArray(String)} but doesn't throw exceptions. */ safeGetFloatArray(Bundle bundle, String name)135 public static float[] safeGetFloatArray(Bundle bundle, String name) { 136 try { 137 return bundle.getFloatArray(name); 138 } catch (Throwable t) { 139 // Catches un-parceling exceptions. 140 Log.e(TAG, "getFloatArray failed on bundle " + bundle); 141 return null; 142 } 143 } 144 145 /** Just like {@link Intent#getLongExtra(String, long)} but doesn't throw exceptions. */ safeGetLongExtra(Intent intent, String name, long defaultValue)146 public static long safeGetLongExtra(Intent intent, String name, long defaultValue) { 147 try { 148 return intent.getLongExtra(name, defaultValue); 149 } catch (Throwable t) { 150 // Catches un-parceling exceptions. 151 Log.e(TAG, "getLongExtra failed on intent " + intent); 152 return defaultValue; 153 } 154 } 155 156 /** Just like {@link Intent#getStringExtra(String)} but doesn't throw exceptions. */ safeGetStringExtra(Intent intent, String name)157 public static String safeGetStringExtra(Intent intent, String name) { 158 try { 159 return intent.getStringExtra(name); 160 } catch (Throwable t) { 161 // Catches un-parceling exceptions. 162 Log.e(TAG, "getStringExtra failed on intent " + intent); 163 return null; 164 } 165 } 166 167 /** Just like {@link Bundle#getString(String)} but doesn't throw exceptions. */ safeGetString(Bundle bundle, String name)168 public static String safeGetString(Bundle bundle, String name) { 169 try { 170 return bundle.getString(name); 171 } catch (Throwable t) { 172 // Catches un-parceling exceptions. 173 Log.e(TAG, "getString failed on bundle " + bundle); 174 return null; 175 } 176 } 177 178 /** Just like {@link Intent#getBundleExtra(String)} but doesn't throw exceptions. */ safeGetBundleExtra(Intent intent, String name)179 public static Bundle safeGetBundleExtra(Intent intent, String name) { 180 try { 181 return intent.getBundleExtra(name); 182 } catch (Throwable t) { 183 // Catches un-parceling exceptions. 184 Log.e(TAG, "getBundleExtra failed on intent " + intent); 185 return null; 186 } 187 } 188 189 /** Just like {@link Bundle#getBundle(String)} but doesn't throw exceptions. */ safeGetBundle(Bundle bundle, String name)190 public static Bundle safeGetBundle(Bundle bundle, String name) { 191 try { 192 return bundle.getBundle(name); 193 } catch (Throwable t) { 194 // Catches un-parceling exceptions. 195 Log.e(TAG, "getBundle failed on bundle " + bundle); 196 return null; 197 } 198 } 199 200 /** Just like {@link Bundle#getParcelable(String)} but doesn't throw exceptions. */ safeGetParcelable(Bundle bundle, String name)201 public static <T extends Parcelable> T safeGetParcelable(Bundle bundle, String name) { 202 try { 203 return bundle.getParcelable(name); 204 } catch (Throwable t) { 205 // Catches un-parceling exceptions. 206 Log.e(TAG, "getParcelable failed on bundle " + bundle); 207 return null; 208 } 209 } 210 211 /** Just like {@link Intent#getParcelableExtra(String)} but doesn't throw exceptions. */ safeGetParcelableExtra(Intent intent, String name)212 public static <T extends Parcelable> T safeGetParcelableExtra(Intent intent, String name) { 213 try { 214 return intent.getParcelableExtra(name); 215 } catch (Throwable t) { 216 // Catches un-parceling exceptions. 217 Log.e(TAG, "getParcelableExtra failed on intent " + intent); 218 return null; 219 } 220 } 221 222 /** 223 * Just link {@link Intent#getParcelableArrayListExtra(String)} but doesn't throw exceptions. 224 */ getParcelableArrayListExtra( Intent intent, String name)225 public static <T extends Parcelable> ArrayList<T> getParcelableArrayListExtra( 226 Intent intent, String name) { 227 try { 228 return intent.getParcelableArrayListExtra(name); 229 } catch (Throwable t) { 230 // Catches un-parceling exceptions. 231 Log.e(TAG, "getParcelableArrayListExtra failed on intent " + intent); 232 return null; 233 } 234 } 235 236 /** Just link {@link Bundle#getParcelableArrayList(String)} but doesn't throw exceptions. */ safeGetParcelableArrayList( Bundle bundle, String name)237 public static <T extends Parcelable> ArrayList<T> safeGetParcelableArrayList( 238 Bundle bundle, String name) { 239 try { 240 return bundle.getParcelableArrayList(name); 241 } catch (Throwable t) { 242 // Catches un-parceling exceptions. 243 Log.e(TAG, "getParcelableArrayList failed on bundle " + bundle); 244 return null; 245 } 246 } 247 248 /** Just like {@link Intent#getParcelableArrayExtra(String)} but doesn't throw exceptions. */ safeGetParcelableArrayExtra(Intent intent, String name)249 public static Parcelable[] safeGetParcelableArrayExtra(Intent intent, String name) { 250 try { 251 return intent.getParcelableArrayExtra(name); 252 } catch (Throwable t) { 253 Log.e(TAG, "getParcelableArrayExtra failed on intent " + intent); 254 return null; 255 } 256 } 257 258 /** Just like {@link Intent#getStringArrayListExtra(String)} but doesn't throw exceptions. */ safeGetStringArrayListExtra(Intent intent, String name)259 public static ArrayList<String> safeGetStringArrayListExtra(Intent intent, String name) { 260 try { 261 return intent.getStringArrayListExtra(name); 262 } catch (Throwable t) { 263 // Catches un-parceling exceptions. 264 Log.e(TAG, "getStringArrayListExtra failed on intent " + intent); 265 return null; 266 } 267 } 268 269 /** Just like {@link Intent#getByteArrayExtra(String)} but doesn't throw exceptions. */ safeGetByteArrayExtra(Intent intent, String name)270 public static byte[] safeGetByteArrayExtra(Intent intent, String name) { 271 try { 272 return intent.getByteArrayExtra(name); 273 } catch (Throwable t) { 274 // Catches un-parceling exceptions. 275 Log.e(TAG, "getByteArrayExtra failed on intent " + intent); 276 return null; 277 } 278 } 279 280 /** Just like {@link Intent#getSerializableExtra(String)} but doesn't throw exceptions. */ 281 @SuppressWarnings("unchecked") safeGetSerializableExtra(Intent intent, String name)282 public static <T extends Serializable> T safeGetSerializableExtra(Intent intent, String name) { 283 try { 284 return (T) intent.getSerializableExtra(name); 285 } catch (ClassCastException ex) { 286 Log.e(TAG, "Invalide class for Serializable: " + name, ex); 287 return null; 288 } catch (Throwable t) { 289 // Catches un-serializable exceptions. 290 Log.e(TAG, "getSerializableExtra failed on intent " + intent); 291 return null; 292 } 293 } 294 295 /** 296 * Returns the value associated with the given name, or null if no mapping of the desired type 297 * exists for the given name or a null value is explicitly associated with the name. 298 * 299 * @param name a key string 300 * @return an IBinder value, or null 301 */ safeGetBinder(Bundle bundle, String name)302 public static IBinder safeGetBinder(Bundle bundle, String name) { 303 if (bundle == null) return null; 304 try { 305 return bundle.getBinder(name); 306 } catch (Throwable t) { 307 // Catches un-parceling exceptions. 308 Log.e(TAG, "getBinder failed on bundle " + bundle); 309 return null; 310 } 311 } 312 313 /** 314 * @return a Binder from an Intent, or null. 315 * 316 * Creates a temporary copy of the extra Bundle, which is required as 317 * Intent#getBinderExtra() doesn't exist, but Bundle.getBinder() does. 318 */ safeGetBinderExtra(Intent intent, String name)319 public static IBinder safeGetBinderExtra(Intent intent, String name) { 320 if (!intent.hasExtra(name)) return null; 321 Bundle extras = intent.getExtras(); 322 return safeGetBinder(extras, name); 323 } 324 325 /** 326 * Inserts a {@link Binder} value into an Intent as an extra. 327 * 328 * @param intent Intent to put the binder into. 329 * @param name Key. 330 * @param binder Binder object. 331 */ safePutBinderExtra(Intent intent, String name, IBinder binder)332 public static void safePutBinderExtra(Intent intent, String name, IBinder binder) { 333 if (intent == null) return; 334 Bundle bundle = new Bundle(); 335 try { 336 bundle.putBinder(name, binder); 337 } catch (Throwable t) { 338 // Catches parceling exceptions. 339 Log.e(TAG, "putBinder failed on bundle " + bundle); 340 } 341 intent.putExtras(bundle); 342 } 343 344 /** See {@link #safeStartActivity(Context, Intent, Bundle)}. */ safeStartActivity(Context context, Intent intent)345 public static boolean safeStartActivity(Context context, Intent intent) { 346 return safeStartActivity(context, intent, null); 347 } 348 349 /** 350 * Catches any failures to start an Activity. 351 * @param context Context to use when starting the Activity. 352 * @param intent Intent to fire. 353 * @param bundle Bundle of launch options. 354 * @return Whether or not Android accepted the Intent. 355 */ safeStartActivity( Context context, Intent intent, @Nullable Bundle bundle)356 public static boolean safeStartActivity( 357 Context context, Intent intent, @Nullable Bundle bundle) { 358 try { 359 context.startActivity(intent, bundle); 360 return true; 361 } catch (ActivityNotFoundException e) { 362 return false; 363 } 364 } 365 366 /** Returns whether the intent starts an activity in a new task or a new document. */ isIntentForNewTaskOrNewDocument(Intent intent)367 public static boolean isIntentForNewTaskOrNewDocument(Intent intent) { 368 int testFlags = Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT; 369 return (intent.getFlags() & testFlags) != 0; 370 } 371 372 /** 373 * Returns how large the Intent will be in Parcel form, which is helpful for gauging whether 374 * Android will deliver the Intent instead of throwing a TransactionTooLargeException. 375 * 376 * @param intent Intent to get the size of. 377 * @return Number of bytes required to parcel the Intent. 378 */ getParceledIntentSize(Intent intent)379 public static int getParceledIntentSize(Intent intent) { 380 Parcel parcel = Parcel.obtain(); 381 intent.writeToParcel(parcel, 0); 382 return parcel.dataSize(); 383 } 384 385 /** 386 * Given an exception, check whether it wrapped a {@link TransactionTooLargeException}. If it 387 * does, then log the underlying error. If not, throw the original exception again. 388 * 389 * @param e The caught RuntimeException. 390 * @param intent The intent that triggered the RuntimeException to be thrown. 391 */ logTransactionTooLargeOrRethrow(RuntimeException e, Intent intent)392 public static void logTransactionTooLargeOrRethrow(RuntimeException e, Intent intent) { 393 // See http://crbug.com/369574. 394 if (e.getCause() instanceof TransactionTooLargeException) { 395 Log.e(TAG, "Could not resolve Activity for intent " + intent.toString(), e); 396 } else { 397 throw e; 398 } 399 } 400 logInvalidIntent(Intent intent, Exception e)401 private static Intent logInvalidIntent(Intent intent, Exception e) { 402 Log.e(TAG, "Invalid incoming intent.", e); 403 return intent.replaceExtras((Bundle) null); 404 } 405 406 /** 407 * Sanitizes an intent. In case the intent cannot be unparcelled, all extras will be removed to 408 * make it safe to use. 409 * @return A safe to use version of this intent. 410 */ sanitizeIntent(final Intent incomingIntent)411 public static Intent sanitizeIntent(final Intent incomingIntent) { 412 // On Android T+, items are only deserialized when the items themselves are queried, so the 413 // code below is a no-op. 414 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) return incomingIntent; 415 if (incomingIntent == null) return null; 416 try { 417 incomingIntent.getBooleanExtra("TriggerUnparcel", false); 418 return incomingIntent; 419 } catch (BadParcelableException e) { 420 return logInvalidIntent(incomingIntent, e); 421 } catch (RuntimeException e) { 422 if (e.getCause() instanceof ClassNotFoundException) { 423 return logInvalidIntent(incomingIntent, e); 424 } 425 throw e; 426 } 427 } 428 429 /** 430 * @return True if the intent is a MAIN intent a launcher would send. 431 */ isMainIntentFromLauncher(Intent intent)432 public static boolean isMainIntentFromLauncher(Intent intent) { 433 return intent != null 434 && TextUtils.equals(intent.getAction(), Intent.ACTION_MAIN) 435 && intent.hasCategory(Intent.CATEGORY_LAUNCHER) 436 && 0 == (intent.getFlags() & Intent.FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY); 437 } 438 439 /** 440 * Gets the PendingIntent flag for the specified mutability. 441 * PendingIntent.FLAG_IMMUTABLE was added in API level 23 (M), and FLAG_MUTABLE was added in 442 * Android S. 443 * 444 * Unless mutability is required, PendingIntents should always be marked as Immutable as this 445 * is the more secure default. 446 */ getPendingIntentMutabilityFlag(boolean mutable)447 public static int getPendingIntentMutabilityFlag(boolean mutable) { 448 if (!mutable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 449 return ApiHelperForM.getPendingIntentImmutableFlag(); 450 } else if (mutable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 451 return ApiHelperForS.getPendingIntentMutableFlag(); 452 } 453 return 0; 454 } 455 456 /** 457 * Determines whether this app is the only possible handler for this Intent. 458 * 459 * @param context Any context for this app. 460 * @param intent The intent to check. 461 * @return True if the intent targets this app. 462 */ intentTargetsSelf(Context context, Intent intent)463 public static boolean intentTargetsSelf(Context context, Intent intent) { 464 boolean hasPackage = !TextUtils.isEmpty(intent.getPackage()); 465 boolean matchesPackage = hasPackage && context.getPackageName().equals(intent.getPackage()); 466 boolean hasComponent = intent.getComponent() != null; 467 boolean matchesComponent = 468 hasComponent 469 && context.getPackageName().equals(intent.getComponent().getPackageName()); 470 471 // Component takes precedence over PackageName when routing Intents if both are set, but to 472 // be on the safe side, ensure that if we have both package and component set, that they 473 // agree. 474 if (matchesComponent) { 475 if (hasPackage) { 476 // We should not create intents that disagree on package/component, but for security 477 // purposes we should handle this case. 478 assert matchesPackage; 479 return matchesPackage; 480 } 481 return true; 482 } 483 if (matchesPackage) { 484 assert !hasComponent; 485 return !hasComponent; 486 } 487 return false; 488 } 489 getFakeComponentName(String packageName)490 private static ComponentName getFakeComponentName(String packageName) { 491 synchronized (COMPONENT_NAME_LOCK) { 492 if (sFakeComponentName == null) { 493 sFakeComponentName = new ComponentName(packageName, "FakeClass"); 494 } 495 } 496 497 return sFakeComponentName; 498 } 499 getAuthenticationToken()500 private static PendingIntent getAuthenticationToken() { 501 Intent fakeIntent = new Intent(); 502 Context appContext = ContextUtils.getApplicationContext(); 503 fakeIntent.setComponent(getFakeComponentName(appContext.getPackageName())); 504 return PendingIntent.getActivity( 505 appContext, 0, fakeIntent, getPendingIntentMutabilityFlag(false)); 506 } 507 508 /** 509 * Sets TRUSTED_APPLICATION_CODE_EXTRA on the provided intent to identify it as coming from 510 * a trusted source. 511 * 512 * @param intent An Intent that targets either current package, or explicitly targets a 513 * component of the current package. 514 */ addTrustedIntentExtras(Intent intent)515 public static void addTrustedIntentExtras(Intent intent) { 516 // It is crucial that we never leak the authentication token to other packages, because 517 // then the other package could be used to impersonate us/do things as us. 518 boolean toSelf = 519 IntentUtils.intentTargetsSelf(ContextUtils.getApplicationContext(), intent); 520 assert toSelf; 521 // For security reasons we have to check the asserted condition anyways. 522 if (!toSelf) return; 523 524 // The PendingIntent functions as an authentication token --- it could only have come 525 // from us. Stash it in the real Intent as an extra we can validate upon receiving it. 526 intent.putExtra(TRUSTED_APPLICATION_CODE_EXTRA, getAuthenticationToken()); 527 } 528 529 /** 530 * @param intent An Intent to be checked. 531 * @return Whether an intent originates from the current app. 532 */ isTrustedIntentFromSelf(@ullable Intent intent)533 public static boolean isTrustedIntentFromSelf(@Nullable Intent intent) { 534 if (intent == null) return false; 535 536 if (sForceTrustedIntentForTesting) return true; 537 538 // Fetch the authentication token (a PendingIntent) created by 539 // addTrustedIntentExtras, if any. If anything goes wrong trying to retrieve the 540 // token (examples include BadParcelableException or ClassNotFoundException), fail closed. 541 PendingIntent token = 542 IntentUtils.safeGetParcelableExtra(intent, TRUSTED_APPLICATION_CODE_EXTRA); 543 if (token == null) return false; 544 545 // Fetch what should be a matching token. If the PendingIntents are equal, we know that the 546 // sender was us. 547 PendingIntent pending = getAuthenticationToken(); 548 return pending.equals(token); 549 } 550 setForceIsTrustedIntentForTesting(boolean isTrusted)551 public static void setForceIsTrustedIntentForTesting(boolean isTrusted) { 552 sForceTrustedIntentForTesting = isTrusted; 553 ResettersForTesting.register(() -> sForceTrustedIntentForTesting = false); 554 } 555 } 556