/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.service.credentials; import static java.util.Objects.requireNonNull; import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.AppGlobals; import android.app.admin.DevicePolicyManager; import android.app.admin.PackagePolicy; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; import android.credentials.CredentialManager; import android.credentials.CredentialProviderInfo; import android.os.Bundle; import android.os.RemoteException; import android.os.UserHandle; import android.text.TextUtils; import android.util.AttributeSet; import android.util.Slog; import android.util.Xml; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; /** * {@link CredentialProviderInfo} generator. * * @hide */ public final class CredentialProviderInfoFactory { private static final String TAG = CredentialManager.TAG; private static final String TAG_CREDENTIAL_PROVIDER = "credential-provider"; private static final String TAG_CAPABILITIES = "capabilities"; private static final String TAG_CAPABILITY = "capability"; private static final String ATTR_NAME = "name"; /** * Constructs an information instance of the credential provider. * * @param context the context object * @param serviceComponent the serviceComponent of the provider service * @param userId the android userId for which the current process is running * @param isSystemProvider whether this provider is a system provider * @throws PackageManager.NameNotFoundException If provider service is not found * @throws SecurityException If provider does not require the relevant permission */ public static CredentialProviderInfo create( @NonNull Context context, @NonNull ComponentName serviceComponent, int userId, boolean isSystemProvider, boolean isPrimary) throws PackageManager.NameNotFoundException { return create( context, getServiceInfoOrThrow(serviceComponent, userId), isSystemProvider, /* disableSystemAppVerificationForTests= */ false, /* isEnabled= */ false, isPrimary); } /** * Constructs an information instance of the credential provider. * * @param context the context object * @param serviceInfo the service info for the provider app. This must be retrieved from the * {@code PackageManager} * @param isSystemProvider whether the provider app is a system provider * @param disableSystemAppVerificationForTests whether to disable system app permission * verification so that tests can install system providers * @param isEnabled whether the user enabled this provider * @throws SecurityException If provider does not require the relevant permission */ public static CredentialProviderInfo create( @NonNull Context context, @NonNull ServiceInfo serviceInfo, boolean isSystemProvider, boolean disableSystemAppVerificationForTests, boolean isEnabled, boolean isPrimary) throws SecurityException { verifyProviderPermission(serviceInfo); if (isSystemProvider) { if (!isValidSystemProvider( context, serviceInfo, disableSystemAppVerificationForTests)) { Slog.e(TAG, "Provider is not a valid system provider: " + serviceInfo); throw new SecurityException( "Provider is not a valid system provider: " + serviceInfo); } } return populateMetadata(context, serviceInfo) .setSystemProvider(isSystemProvider) .setEnabled(isEnabled) .setPrimary(isPrimary) .build(); } /** * Constructs an information instance of the credential provider for testing purposes. Does not * run any verifications and passes parameters as is. */ @VisibleForTesting public static CredentialProviderInfo createForTests( @NonNull ServiceInfo serviceInfo, @NonNull CharSequence overrideLabel, boolean isSystemProvider, boolean isEnabled, @NonNull List capabilities) { return new CredentialProviderInfo.Builder(serviceInfo) .setEnabled(isEnabled) .setOverrideLabel(overrideLabel) .setSystemProvider(isSystemProvider) .addCapabilities(capabilities) .build(); } private static void verifyProviderPermission(ServiceInfo serviceInfo) throws SecurityException { final String permission = Manifest.permission.BIND_CREDENTIAL_PROVIDER_SERVICE; if (permission.equals(serviceInfo.permission)) { return; } throw new SecurityException( "Service does not require the expected permission : " + permission); } private static boolean isSystemProviderWithValidPermission( ServiceInfo serviceInfo, Context context) { if (context == null) { Slog.w(TAG, "Context is null in isSystemProviderWithValidPermission"); return false; } return PermissionUtils.hasPermission( context, serviceInfo.packageName, Manifest.permission.PROVIDE_DEFAULT_ENABLED_CREDENTIAL_SERVICE); } private static boolean isValidSystemProvider( Context context, ServiceInfo serviceInfo, boolean disableSystemAppVerificationForTests) { requireNonNull(context, "context must not be null"); if (disableSystemAppVerificationForTests) { Bundle metadata = serviceInfo.metaData; if (metadata == null) { Slog.w( TAG, "metadata is null while reading " + "TEST_SYSTEM_PROVIDER_META_DATA_KEY: " + serviceInfo); return false; } return metadata.getBoolean( CredentialProviderService.TEST_SYSTEM_PROVIDER_META_DATA_KEY); } return isSystemProviderWithValidPermission(serviceInfo, context); } private static CredentialProviderInfo.Builder populateMetadata( @NonNull Context context, ServiceInfo serviceInfo) { requireNonNull(context, "context must not be null"); final PackageManager pm = context.getPackageManager(); CredentialProviderInfo.Builder builder = new CredentialProviderInfo.Builder(serviceInfo); // 1. Get the metadata for the service. final Bundle metadata = serviceInfo.metaData; if (metadata == null) { Slog.w(TAG, "Metadata is null for provider: " + serviceInfo.getComponentName()); return builder; } // 2. Get the resources for the application. Resources resources = null; try { resources = pm.getResourcesForApplication(serviceInfo.applicationInfo); } catch (PackageManager.NameNotFoundException e) { Slog.e(TAG, "Failed to get app resources", e); } // 3. Stop if we are missing data. if (resources == null) { Slog.w( TAG, "Resources are null for the serviceInfo being processed: " + serviceInfo.getComponentName()); return builder; } // 4. Extract the XML metadata. try { builder = extractXmlMetadata(context, serviceInfo, pm, resources); } catch (Exception e) { Slog.e(TAG, "Failed to get XML metadata", e); } return builder; } private static CredentialProviderInfo.Builder extractXmlMetadata( @NonNull Context context, @NonNull ServiceInfo serviceInfo, @NonNull PackageManager pm, @NonNull Resources resources) { final CredentialProviderInfo.Builder builder = new CredentialProviderInfo.Builder(serviceInfo); final XmlResourceParser parser = serviceInfo.loadXmlMetaData(pm, CredentialProviderService.SERVICE_META_DATA); if (parser == null) { return builder; } try { int type = 0; while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) { type = parser.next(); } // This is matching a tag in the XML. if (TAG_CREDENTIAL_PROVIDER.equals(parser.getName())) { final AttributeSet allAttributes = Xml.asAttributeSet(parser); TypedArray afsAttributes = null; try { afsAttributes = resources.obtainAttributes( allAttributes, com.android.internal.R.styleable.CredentialProvider); builder.setSettingsSubtitle( getAfsAttributeSafe( afsAttributes, R.styleable.CredentialProvider_settingsSubtitle)); builder.setSettingsActivity( getAfsAttributeSafe( afsAttributes, R.styleable.CredentialProvider_settingsActivity)); } catch (Exception e) { Slog.w(TAG, "Failed to get XML attr for metadata", e); } finally { if (afsAttributes != null) { afsAttributes.recycle(); } } builder.addCapabilities(parseXmlProviderOuterCapabilities(parser, resources)); } else { Slog.w(TAG, "Meta-data does not start with credential-provider-service tag"); } } catch (IOException | XmlPullParserException e) { Slog.e(TAG, "Error parsing credential provider service meta-data", e); } return builder; } private static @Nullable String getAfsAttributeSafe( @Nullable TypedArray afsAttributes, int resId) { if (afsAttributes == null) { return null; } try { return afsAttributes.getString(resId); } catch (Exception e) { Slog.w(TAG, "Failed to get XML attr from afs attributes", e); } return null; } private static List parseXmlProviderOuterCapabilities( XmlPullParser parser, Resources resources) throws IOException, XmlPullParserException { final List capabilities = new ArrayList<>(); final int outerDepth = parser.getDepth(); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue; } if (TAG_CAPABILITIES.equals(parser.getName())) { capabilities.addAll(parseXmlProviderInnerCapabilities(parser, resources)); } } return capabilities; } private static List parseXmlProviderInnerCapabilities( XmlPullParser parser, Resources resources) throws IOException, XmlPullParserException { List capabilities = new ArrayList<>(); final int outerDepth = parser.getDepth(); int type; while ((type = parser.next()) != XmlPullParser.END_DOCUMENT && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) { if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) { continue; } if (TAG_CAPABILITY.equals(parser.getName())) { String name = parser.getAttributeValue(null, ATTR_NAME); if (name != null && !TextUtils.isEmpty(name)) { capabilities.add(name); } } } return capabilities; } private static ServiceInfo getServiceInfoOrThrow( @NonNull ComponentName serviceComponent, int userId) throws PackageManager.NameNotFoundException { try { ServiceInfo si = AppGlobals.getPackageManager() .getServiceInfo(serviceComponent, PackageManager.GET_META_DATA, userId); if (si != null) { return si; } } catch (RemoteException e) { Slog.e(TAG, "Unable to get serviceInfo", e); } throw new PackageManager.NameNotFoundException(serviceComponent.toString()); } /** * Returns the valid credential provider services available for the user with the given {@code * userId}. */ @NonNull private static List getAvailableSystemServiceInfos( @NonNull Context context, @UserIdInt int userId, boolean disableSystemAppVerificationForTests) { requireNonNull(context, "context must not be null"); final List services = new ArrayList<>(); final List resolveInfos = new ArrayList<>(); resolveInfos.addAll( context.getPackageManager() .queryIntentServicesAsUser( new Intent(CredentialProviderService.SYSTEM_SERVICE_INTERFACE), PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA), userId)); for (ResolveInfo resolveInfo : resolveInfos) { final ServiceInfo serviceInfo = resolveInfo.serviceInfo; if (disableSystemAppVerificationForTests) { if (serviceInfo != null) { services.add(serviceInfo); } continue; } try { ApplicationInfo appInfo = context.getPackageManager() .getApplicationInfo( serviceInfo.packageName, PackageManager.ApplicationInfoFlags.of( PackageManager.MATCH_SYSTEM_ONLY)); if (appInfo == null || serviceInfo == null) { continue; } services.add(serviceInfo); } catch (SecurityException | PackageManager.NameNotFoundException e) { Slog.e(TAG, "Error getting info for " + serviceInfo, e); } } return services; } /** * Returns the valid credential provider services available for the user with the given {@code * userId}. */ @NonNull public static List getAvailableSystemServices( @NonNull Context context, @UserIdInt int userId, boolean disableSystemAppVerificationForTests, Set enabledServices) { requireNonNull(context, "context must not be null"); final List providerInfos = new ArrayList<>(); for (ServiceInfo si : getAvailableSystemServiceInfos( context, userId, disableSystemAppVerificationForTests)) { try { CredentialProviderInfo cpi = CredentialProviderInfoFactory.create( context, si, /* isSystemProvider= */ true, disableSystemAppVerificationForTests, enabledServices.contains(si.getComponentName()), false); if (cpi.isSystemProvider()) { providerInfos.add(cpi); } else { Slog.e(TAG, "Non system provider was in system provider list."); } } catch (SecurityException e) { Slog.e(TAG, "Failed to create CredentialProviderInfo: " + e); } } return providerInfos; } private static @Nullable PackagePolicy getDeviceManagerPolicy( @NonNull Context context, int userId) { Context newContext = context.createContextAsUser(UserHandle.of(userId), 0); try { DevicePolicyManager dpm = newContext.getSystemService(DevicePolicyManager.class); PackagePolicy pp = dpm.getCredentialManagerPolicy(); return pp; } catch (SecurityException e) { // If the current user is not enrolled in DPM then this can throw a security error. Slog.e(TAG, "Failed to get device policy: " + e); } return null; } /** * Returns the valid credential provider services available for the user with the given {@code * userId}. */ @NonNull public static List getCredentialProviderServices( @NonNull Context context, int userId, int providerFilter, Set enabledServices, Set primaryServices) { requireNonNull(context, "context must not be null"); // Get the device policy. If the client has asked for all providers then we // should ignore the device policy. PackagePolicy pp = providerFilter != CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN ? getDeviceManagerPolicy(context, userId) : null; // Generate the provider list. final boolean disableSystemAppVerificationForTests = false; ProviderGenerator generator = new ProviderGenerator( context, pp, disableSystemAppVerificationForTests, providerFilter); generator.addUserProviders( getUserProviders( context, userId, disableSystemAppVerificationForTests, enabledServices, primaryServices)); generator.addSystemProviders( getAvailableSystemServices( context, userId, disableSystemAppVerificationForTests, enabledServices)); return generator.getProviders(); } /** * Returns the valid credential provider services available for the user with the given {@code * userId}. Includes test providers. */ @NonNull public static List getCredentialProviderServicesForTesting( @NonNull Context context, int userId, int providerFilter, Set enabledServices, Set primaryServices) { requireNonNull(context, "context must not be null"); // Get the device policy. If the client has asked for all providers then we // should ignore the device policy. PackagePolicy pp = providerFilter != CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN ? getDeviceManagerPolicy(context, userId) : null; // Generate the provider list. final boolean disableSystemAppVerificationForTests = true; ProviderGenerator generator = new ProviderGenerator( context, pp, disableSystemAppVerificationForTests, providerFilter); generator.addUserProviders( getUserProviders( context, userId, disableSystemAppVerificationForTests, enabledServices, primaryServices)); generator.addSystemProviders( getAvailableSystemServices( context, userId, disableSystemAppVerificationForTests, enabledServices)); return generator.getProviders(); } private static class ProviderGenerator { private final Context mContext; private final PackagePolicy mPp; private final boolean mDisableSystemAppVerificationForTests; private final Map mServices = new HashMap(); private final int mProviderFilter; ProviderGenerator( Context context, PackagePolicy pp, boolean disableSystemAppVerificationForTests, int providerFilter) { this.mContext = context; this.mPp = pp; this.mDisableSystemAppVerificationForTests = disableSystemAppVerificationForTests; this.mProviderFilter = providerFilter; } private boolean isPackageAllowed(boolean isSystemProvider, String packageName) { if (mPp == null) { return true; } if (isSystemProvider) { return mPp.getPolicyType() == PackagePolicy.PACKAGE_POLICY_ALLOWLIST_AND_SYSTEM; } return mPp.isPackageAllowed(packageName, new HashSet<>()); } public List getProviders() { return new ArrayList<>(mServices.values()); } public void addUserProviders(List providers) { for (CredentialProviderInfo cpi : providers) { if (!cpi.isSystemProvider()) { addProvider(cpi); } } } public void addSystemProviders(List providers) { for (CredentialProviderInfo cpi : providers) { if (cpi.isSystemProvider()) { addProvider(cpi); } } } private boolean isProviderAllowedWithFilter(CredentialProviderInfo cpi) { if (mProviderFilter == CredentialManager.PROVIDER_FILTER_ALL_PROVIDERS) { return true; } if (cpi.isSystemProvider()) { return mProviderFilter == CredentialManager.PROVIDER_FILTER_SYSTEM_PROVIDERS_ONLY; } else { return mProviderFilter == CredentialManager.PROVIDER_FILTER_USER_PROVIDERS_ONLY || mProviderFilter == CredentialManager .PROVIDER_FILTER_USER_PROVIDERS_INCLUDING_HIDDEN; } } private void addProvider(CredentialProviderInfo cpi) { final String componentNameString = cpi.getServiceInfo().getComponentName().flattenToString(); if (!isProviderAllowedWithFilter(cpi)) { return; } if (!isPackageAllowed(cpi.isSystemProvider(), cpi.getServiceInfo().packageName)) { return; } mServices.put(componentNameString, cpi); } } /** * Returns the valid credential provider services available for the user with the given {@code * userId}. */ @NonNull private static List getUserProviders( @NonNull Context context, @UserIdInt int userId, boolean disableSystemAppVerificationForTests, Set enabledServices, Set primaryServices) { final List services = new ArrayList<>(); final List resolveInfos = context.getPackageManager() .queryIntentServicesAsUser( new Intent(CredentialProviderService.SERVICE_INTERFACE), PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA), userId); for (ResolveInfo resolveInfo : resolveInfos) { final ServiceInfo serviceInfo = resolveInfo.serviceInfo; if (serviceInfo == null) { Slog.d(TAG, "No serviceInfo found for resolveInfo, so skipping provider"); continue; } try { CredentialProviderInfo cpi = CredentialProviderInfoFactory.create( context, serviceInfo, /* isSystemProvider= */ false, disableSystemAppVerificationForTests, enabledServices.contains(serviceInfo.getComponentName()), primaryServices.contains(serviceInfo.getComponentName())); if (!cpi.isSystemProvider()) { services.add(cpi); } } catch (Exception e) { Slog.e(TAG, "Error getting info for " + serviceInfo, e); } } return services; } }