blob: 735f90faee5b87e966ed72c99080846af11a0336 [file] [log] [blame]
/*
* Copyright (C) 2020 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 com.android.server.pm.verify.domain;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.compat.annotation.ChangeId;
import android.compat.annotation.EnabledSince;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Patterns;
import com.android.internal.pm.pkg.component.ParsedActivity;
import com.android.internal.pm.pkg.component.ParsedIntentInfo;
import com.android.server.SystemConfig;
import com.android.server.compat.PlatformCompat;
import com.android.server.pm.pkg.AndroidPackage;
import java.util.List;
import java.util.Objects;
import java.util.function.BiFunction;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class DomainVerificationCollector {
// The default domain name matcher doesn't account for wildcards, so prefix with *.
private static final Pattern DOMAIN_NAME_WITH_WILDCARD =
Pattern.compile("(\\*\\.)?" + Patterns.DOMAIN_NAME.pattern());
private static final int MAX_DOMAINS_BYTE_SIZE = 1024 * 1024;
private static final BiFunction<ArraySet<String>, String, Boolean> ARRAY_SET_COLLECTOR =
(set, domain) -> {
set.add(domain);
return null;
};
@NonNull
private final PlatformCompat mPlatformCompat;
@NonNull
private final SystemConfig mSystemConfig;
@NonNull
private final Matcher mDomainMatcher;
public DomainVerificationCollector(@NonNull PlatformCompat platformCompat,
@NonNull SystemConfig systemConfig) {
mPlatformCompat = platformCompat;
mSystemConfig = systemConfig;
// Cache the matcher to avoid calling into native on each check
mDomainMatcher = DOMAIN_NAME_WITH_WILDCARD.matcher("");
}
/**
* With the updated form of the app links verification APIs, an app will be required to declare
* domains inside an intent filter which includes all of the following:
* <ul>
* <li>- android:autoVerify="true"</li>
* <li>- Intent.ACTION_VIEW</li>
* <li>- Intent.CATEGORY_BROWSABLE</li>
* <li>- Intent.CATEGORY_DEFAULT</li>
* <li>- Only IntentFilter.SCHEME_HTTP and/or IntentFilter.SCHEME_HTTPS,
* with no other schemes</li>
* </ul>
* <p>
* On prior versions of Android, Intent.CATEGORY_BROWSABLE was not a requirement, other
* schemes were allowed, and setting autoVerify to true in any intent filter would implicitly
* pretend that all intent filters were set to autoVerify="true".
*/
@ChangeId
@EnabledSince(targetSdkVersion = Build.VERSION_CODES.S)
public static final long RESTRICT_DOMAINS = 175408749L;
@NonNull
public ArraySet<String> collectAllWebDomains(@NonNull AndroidPackage pkg) {
return collectDomains(pkg, false /* checkAutoVerify */, true /* valid */);
}
/**
* Effectively {@link #collectAllWebDomains(AndroidPackage)}, but requires {@link
* IntentFilter#getAutoVerify()} == true.
*/
@NonNull
public ArraySet<String> collectValidAutoVerifyDomains(@NonNull AndroidPackage pkg) {
return collectDomains(pkg, true /* checkAutoVerify */, true /* valid */);
}
/**
* Returns all the domains that are configured to be auto verified, but aren't actually valid
* HTTP domains, per {@link #DOMAIN_NAME_WITH_WILDCARD}.
*/
@NonNull
public ArraySet<String> collectInvalidAutoVerifyDomains(@NonNull AndroidPackage pkg) {
return collectDomains(pkg, true /* checkAutoVerify */, false /* valid */);
}
public boolean containsWebDomain(@NonNull AndroidPackage pkg, @NonNull String targetDomain) {
return collectDomains(pkg, false /* checkAutoVerify */, true /* valid */, null,
(BiFunction<Void, String, Boolean>) (unused, domain) -> {
if (Objects.equals(targetDomain, domain)) {
return true;
}
return null;
}) != null;
}
public boolean containsAutoVerifyDomain(@NonNull AndroidPackage pkg,
@NonNull String targetDomain) {
return collectDomains(pkg, true /* checkAutoVerify */, true /* valid */, null,
(BiFunction<Void, String, Boolean>) (unused, domain) -> {
if (Objects.equals(targetDomain, domain)) {
return true;
}
return null;
}) != null;
}
@NonNull
private ArraySet<String> collectDomains(@NonNull AndroidPackage pkg,
boolean checkAutoVerify, boolean valid) {
ArraySet<String> domains = new ArraySet<>();
collectDomains(pkg, checkAutoVerify, valid, domains, ARRAY_SET_COLLECTOR);
return domains;
}
@NonNull
private <InitialValue, ReturnValue> ReturnValue collectDomains(@NonNull AndroidPackage pkg,
boolean checkAutoVerify, boolean valid, @Nullable InitialValue initialValue,
@NonNull BiFunction<InitialValue, String, ReturnValue> domainCollector) {
boolean restrictDomains =
DomainVerificationUtils.isChangeEnabled(mPlatformCompat, pkg, RESTRICT_DOMAINS);
if (restrictDomains) {
return collectDomainsInternal(pkg, checkAutoVerify, valid, initialValue,
domainCollector);
} else {
return collectDomainsLegacy(pkg, checkAutoVerify, valid, initialValue, domainCollector);
}
}
/**
* @see #RESTRICT_DOMAINS
*/
@Nullable
private <InitialValue, ReturnValue> ReturnValue collectDomainsLegacy(
@NonNull AndroidPackage pkg, boolean checkAutoVerify, boolean valid,
@Nullable InitialValue initialValue,
@NonNull BiFunction<InitialValue, String, ReturnValue> domainCollector) {
if (!checkAutoVerify) {
// Per-domain user selection state doesn't have a V1 equivalent on S, so just use V2
return collectDomainsInternal(pkg, false /* checkAutoVerify */, true /* valid */,
initialValue, domainCollector);
}
List<ParsedActivity> activities = pkg.getActivities();
int activitiesSize = activities.size();
// Due to a bug in the platform, for backwards compatibility, assume that all linked apps
// require auto verification, even if they forget to mark their manifest as such.
boolean needsAutoVerify = mSystemConfig.getLinkedApps().contains(pkg.getPackageName());
if (!needsAutoVerify) {
for (int activityIndex = 0; activityIndex < activitiesSize && !needsAutoVerify;
activityIndex++) {
ParsedActivity activity = activities.get(activityIndex);
List<ParsedIntentInfo> intents = activity.getIntents();
int intentsSize = intents.size();
for (int intentIndex = 0; intentIndex < intentsSize && !needsAutoVerify;
intentIndex++) {
ParsedIntentInfo intent = intents.get(intentIndex);
needsAutoVerify = intent.getIntentFilter().needsVerification();
}
}
if (!needsAutoVerify) {
return null;
}
}
int totalSize = 0;
boolean underMaxSize = true;
for (int activityIndex = 0; activityIndex < activitiesSize && underMaxSize;
activityIndex++) {
ParsedActivity activity = activities.get(activityIndex);
List<ParsedIntentInfo> intents = activity.getIntents();
int intentsSize = intents.size();
for (int intentIndex = 0; intentIndex < intentsSize && underMaxSize; intentIndex++) {
ParsedIntentInfo intent = intents.get(intentIndex);
IntentFilter intentFilter = intent.getIntentFilter();
if (intentFilter.handlesWebUris(false)) {
int authorityCount = intentFilter.countDataAuthorities();
for (int index = 0; index < authorityCount; index++) {
String host = intentFilter.getDataAuthority(index).getHost();
if (isValidHost(host) == valid) {
totalSize += byteSizeOf(host);
underMaxSize = totalSize < MAX_DOMAINS_BYTE_SIZE;
ReturnValue returnValue = domainCollector.apply(initialValue, host);
if (returnValue != null) {
return returnValue;
}
}
}
}
}
}
return null;
}
/**
* @see #RESTRICT_DOMAINS
* @param domainCollector Function to call with initialValue and a valid host. Should return
* a non-null value if the function should return immediately
* after the currently processed host.
*/
@Nullable
private <InitialValue, ReturnValue> ReturnValue collectDomainsInternal(
@NonNull AndroidPackage pkg, boolean checkAutoVerify, boolean valid,
@Nullable InitialValue initialValue,
@NonNull BiFunction<InitialValue, String, ReturnValue> domainCollector) {
int totalSize = 0;
boolean underMaxSize = true;
List<ParsedActivity> activities = pkg.getActivities();
int activitiesSize = activities.size();
for (int activityIndex = 0; activityIndex < activitiesSize && underMaxSize;
activityIndex++) {
ParsedActivity activity = activities.get(activityIndex);
List<ParsedIntentInfo> intents = activity.getIntents();
int intentsSize = intents.size();
for (int intentIndex = 0; intentIndex < intentsSize && underMaxSize; intentIndex++) {
ParsedIntentInfo intent = intents.get(intentIndex);
IntentFilter intentFilter = intent.getIntentFilter();
if (checkAutoVerify && !intentFilter.getAutoVerify()) {
continue;
}
if (!intentFilter.hasCategory(Intent.CATEGORY_DEFAULT)
|| !intentFilter.handlesWebUris(checkAutoVerify)) {
continue;
}
// TODO(b/159952358): There seems to be no way to associate the exact host
// with its scheme, meaning all hosts have to be verified as if they were
// web schemes. This means that given the following:
// <intent-filter android:autoVerify="true">
// ...
// <data android:scheme="https" android:host="one.example.com"/>
// <data android:scheme="https" android:host="two.example.com"/>
// <data android:host="three.example.com"/>
// <data android:scheme="nonWeb" android:host="four.example.com"/>
// </intent-filter>
// The verification agent will be asked to verify four.example.com, which the
// app will probably fail. This can be re-configured to work properly by the
// app developer by declaring a separate intent-filter. This may not be worth
// fixing.
int authorityCount = intentFilter.countDataAuthorities();
for (int index = 0; index < authorityCount && underMaxSize; index++) {
String host = intentFilter.getDataAuthority(index).getHost();
if (isValidHost(host) == valid) {
totalSize += byteSizeOf(host);
underMaxSize = totalSize < MAX_DOMAINS_BYTE_SIZE;
ReturnValue returnValue = domainCollector.apply(initialValue, host);
if (returnValue != null) {
return returnValue;
}
}
}
}
}
return null;
}
/**
* Ballpark the size of domains to avoid a ridiculous amount of domains that could slow
* down client-server communication.
*/
private int byteSizeOf(String string) {
// Use the same method from core for the data objects so that restrictions are consistent
return android.content.pm.verify.domain.DomainVerificationUtils.estimatedByteSizeOf(string);
}
/**
* It's easy to mis-configure autoVerify intent filters, so to avoid adding unintended hosts,
* check if the host is an HTTP domain. This applies for both legacy and modern versions of
* the API, which will strip invalid hosts from the legacy parsing result. This is done to
* improve the reliability of any legacy verifiers.
*/
private boolean isValidHost(String host) {
if (TextUtils.isEmpty(host)) {
return false;
}
mDomainMatcher.reset(host);
return mDomainMatcher.matches();
}
}