blob: 4302defc6dadcc22c8fb19e1f85c058e489a7a9e [file] [log] [blame]
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.components.externalauth;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Binder;
import android.text.TextUtils;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import com.google.android.gms.common.ConnectionResult;
import com.google.android.gms.common.GoogleApiAvailability;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.task.PostTask;
import org.chromium.components.embedder_support.util.Origin;
import org.chromium.content_public.browser.UiThreadTaskTraits;
import org.chromium.gms.ChromiumPlayServicesAvailability;
/**
* Utility class for external authentication tools.
*
* This class is safe to use on any thread.
*/
public class ExternalAuthUtils {
public static final int FLAG_SHOULD_BE_GOOGLE_SIGNED = 1 << 0;
public static final int FLAG_SHOULD_BE_SYSTEM = 1 << 1;
private static final String TAG = "ExternalAuthUtils";
private static ExternalAuthUtils sInstance = new ExternalAuthUtils();
private final ExternalAuthGoogleDelegate mGoogleDelegate;
public ExternalAuthUtils() {
mGoogleDelegate = new ExternalAuthGoogleDelegateImpl();
}
/**
* @return The singleton instance of ExternalAuthUtils.
*/
public static ExternalAuthUtils getInstance() {
return sInstance;
}
/**
* Gets the calling package names for the current transaction.
* @param context The context to use for accessing the package manager.
* @return The calling package names.
*/
private static String[] getCallingPackages(Context context) {
int callingUid = Binder.getCallingUid();
PackageManager pm = context.getApplicationContext().getPackageManager();
return pm.getPackagesForUid(callingUid);
}
/**
* Returns whether the caller application is a part of the system build.
* @param pm Package manager to use for getting package related info.
* @param packageName The package name to inquire about.
*/
@VisibleForTesting
// TODO(crbug.com/635567): Fix this properly.
@SuppressLint("WrongConstant")
public boolean isSystemBuild(PackageManager pm, String packageName) {
try {
ApplicationInfo info = pm.getApplicationInfo(packageName, ApplicationInfo.FLAG_SYSTEM);
if ((info.flags & ApplicationInfo.FLAG_SYSTEM) == 0) throw new SecurityException();
} catch (NameNotFoundException e) {
Log.e(TAG, "Package with name " + packageName + " not found");
return false;
} catch (SecurityException e) {
Log.e(TAG, "Caller with package name " + packageName + " is not in the system build");
return false;
}
return true;
}
/**
* Returns whether the current build of Chrome is a Google-signed package.
* @return whether the currently running application is signed with Google keys.
*/
public boolean isChromeGoogleSigned() {
String packageName = ContextUtils.getApplicationContext().getPackageName();
return isGoogleSigned(packageName);
}
/**
* Returns whether the call is originating from a Google-signed package.
* @param packageName The package name to inquire about.
*/
public boolean isGoogleSigned(String packageName) {
return mGoogleDelegate.isGoogleSigned(packageName);
}
/**
* Returns whether the package can bypass TWA verification.
* @param packageName The package name to inquire about.
* @param origin The origin of the TWA.
*/
public boolean isAllowlistedForTwaVerification(String packageName, Origin origin) {
return false;
}
/**
* Returns whether the callers of the current transaction contains a package that matches
* the give authentication requirements.
* @param context The context to use for getting package information.
* @param authRequirements The requirements to be exercised on the caller.
* @param packageToMatch The package name to compare with the caller.
* @return Whether the caller meets the authentication requirements.
*/
private boolean isCallerValid(Context context, int authRequirements, String packageToMatch) {
boolean shouldBeGoogleSigned = (authRequirements & FLAG_SHOULD_BE_GOOGLE_SIGNED) != 0;
boolean shouldBeSystem = (authRequirements & FLAG_SHOULD_BE_SYSTEM) != 0;
String[] callingPackages = getCallingPackages(context);
PackageManager pm = context.getApplicationContext().getPackageManager();
boolean matchFound = false;
for (String packageName : callingPackages) {
if (!TextUtils.isEmpty(packageToMatch) && !packageName.equals(packageToMatch)) continue;
matchFound = true;
if ((shouldBeGoogleSigned && !isGoogleSigned(packageName))
|| (shouldBeSystem && !isSystemBuild(pm, packageName))) {
return false;
}
}
return matchFound;
}
/**
* Returns whether the callers of the current transaction contains a package that matches
* the give authentication requirements.
* @param context The context to use for getting package information.
* @param authRequirements The requirements to be exercised on the caller.
* @param packageToMatch The package name to compare with the caller. Should be non-empty.
* @return Whether the caller meets the authentication requirements.
*/
public boolean isCallerValidForPackage(
Context context, int authRequirements, String packageToMatch) {
assert !TextUtils.isEmpty(packageToMatch);
return isCallerValid(context, authRequirements, packageToMatch);
}
/**
* Returns whether the callers of the current transaction matches the given authentication
* requirements.
* @param context The context to use for getting package information.
* @param authRequirements The requirements to be exercised on the caller.
* @return Whether the caller meets the authentication requirements.
*/
public boolean isCallerValid(Context context, int authRequirements) {
return isCallerValid(context, authRequirements, "");
}
/**
* @return Whether the current device lacks proper Google Play Services. This will return true
* if the service is not authentic or it is totally missing. Return false otherwise.
* Note this method returns false if the service is only temporarily disabled, such as
* when it is updating.
*/
public boolean isGooglePlayServicesMissing(final Context context) {
final int resultCode = checkGooglePlayServicesAvailable(context);
return (resultCode == ConnectionResult.SERVICE_MISSING
|| resultCode == ConnectionResult.SERVICE_INVALID);
}
/**
* Checks whether Google Play Services can be used, applying the specified error-handling
* policy if a user-recoverable error occurs. This method is threadsafe. If the specified
* error-handling policy requires UI interaction, it will be run on the UI thread.
* Subclasses should generally not override this method; instead, they should override the
* helper methods {@link #checkGooglePlayServicesAvailable(Context)},
* {@link #describeError(int)}, and {@link #isUserRecoverableError(int)} instead, which are
* called in that order (as necessary) by this method.
* @param errorHandler How to handle user-recoverable errors; must be non-null.
* @return true if and only if Google Play Services can be used
*/
public boolean canUseGooglePlayServices(final UserRecoverableErrorHandler errorHandler) {
Context context = ContextUtils.getApplicationContext();
final int resultCode = checkGooglePlayServicesAvailable(context);
if (resultCode == ConnectionResult.SUCCESS) return true;
// resultCode is some kind of error.
Log.v(TAG, "Unable to use Google Play Services: %s", describeError(resultCode));
if (isUserRecoverableError(resultCode)) {
Runnable errorHandlerTask = new Runnable() {
@Override
public void run() {
errorHandler.handleError(context, resultCode);
}
};
PostTask.runOrPostTask(UiThreadTaskTraits.DEFAULT, errorHandlerTask);
}
return false;
}
/**
* Shortcut of {@link #canUseGooglePlayServices(UserRecoverableErrorHandler)}.
*
* @return true if and only if Google Play Services can be used
*/
public boolean canUseGooglePlayServices() {
return canUseGooglePlayServices(new UserRecoverableErrorHandler.Silent());
}
/**
* Same as {@link #canUseGooglePlayServices(UserRecoverableErrorHandler)}
* but also with the constraint that first-party APIs must be available. This check is
* implemented by verifying that the package is Google-signed; if not, first-party APIs will
* be unavailable at runtime.
* Nuance: The check on whether or not the package is Google-signed itself requires access to
* Google Play Services, so this method first checks for "normal" (non-first-party) access and,
* if successful, makes a second call to Google Play Services to determine the state of the
* package signature. The failure handling policy only applies to the first check, since Google
* Play Services provides "canned" ways to deal with failures; there is no special handling of
* the case where the Google Play Services check succeeds and the Google-signed package check
* fails (the method will simply return false).
* @param userRecoverableErrorHandler How to handle user-recoverable errors from Google
* Play Services; must be non-null.
* @return true if and only if first-party Google Play Services can be used
*/
@WorkerThread
public boolean canUseFirstPartyGooglePlayServices(
UserRecoverableErrorHandler userRecoverableErrorHandler) {
return canUseGooglePlayServices(userRecoverableErrorHandler) && isChromeGoogleSigned();
}
/**
* Shortcut of {@link #canUseFirstPartyGooglePlayServices(UserRecoverableErrorHandler)}.
*
* @return true if and only if first-party Google Play Services can be used
*/
public boolean canUseFirstPartyGooglePlayServices() {
return canUseFirstPartyGooglePlayServices(new UserRecoverableErrorHandler.Silent());
}
/**
* @return this object's {@link ExternalAuthGoogleDelegate} instance.
*/
@VisibleForTesting
public ExternalAuthGoogleDelegate getGoogleDelegateForTesting() {
return mGoogleDelegate;
}
/**
* Invokes whatever external code is necessary to check if Google Play Services is available
* and returns the code produced by the attempt. Subclasses can override to force the behavior
* one way or another, or to change the way that the check is performed.
* @param context The current context.
* @return The code produced by calling the external code
*/
protected int checkGooglePlayServicesAvailable(final Context context) {
// TODO(crbug.com/577190): Temporarily allowing disk access until more permanent fix is in.
try (StrictModeContext ignored = StrictModeContext.allowDiskWrites()) {
return ChromiumPlayServicesAvailability.getGooglePlayServicesConnectionResult(context);
}
}
/**
* Invokes whatever external code is necessary to check if the specified error code produced
* by {@link #checkGooglePlayServicesAvailable(Context)} represents a user-recoverable error.
* Subclasses can override to filter error codes as desired.
* @param errorCode The code to check
* @return true If the code represents a user-recoverable error
*/
protected boolean isUserRecoverableError(final int errorCode) {
return GoogleApiAvailability.getInstance().isUserResolvableError(errorCode);
}
/**
* Invokes whatever external code is necessary to obtain a textual description of an error
* code produced by {@link #checkGooglePlayServicesAvailable(Context)}.
* @param errorCode The code to check
* @return a textual description of the error code
*/
protected String describeError(final int errorCode) {
return GoogleApiAvailability.getInstance().getErrorString(errorCode);
}
}