1 /* 2 * Copyright (C) 2020 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 com.android.server.pm.verify.domain; 18 19 import android.annotation.CheckResult; 20 import android.annotation.NonNull; 21 import android.content.Intent; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.content.pm.PackageManager.NameNotFoundException; 25 import android.text.TextUtils; 26 import android.util.Patterns; 27 28 import com.android.internal.util.CollectionUtils; 29 import com.android.server.compat.PlatformCompat; 30 import com.android.server.pm.PackageManagerService; 31 import com.android.server.pm.pkg.AndroidPackage; 32 33 import java.util.Set; 34 import java.util.regex.Matcher; 35 36 public final class DomainVerificationUtils { 37 38 public static final int MAX_DOMAIN_LENGTH = 254; 39 public static final int MAX_DOMAIN_LABEL_LENGTH = 63; 40 41 private static final ThreadLocal<Matcher> sCachedMatcher = ThreadLocal.withInitial( 42 () -> Patterns.DOMAIN_NAME.matcher("")); 43 44 /** 45 * Consolidates package exception messages. A generic unavailable message is included since the 46 * caller doesn't bother to check why the package isn't available. 47 */ 48 @CheckResult throwPackageUnavailable(@onNull String packageName)49 static NameNotFoundException throwPackageUnavailable(@NonNull String packageName) 50 throws NameNotFoundException { 51 throw new NameNotFoundException("Package " + packageName + " unavailable"); 52 } 53 isDomainVerificationIntent(Intent intent, @PackageManager.ResolveInfoFlagsBits long resolveInfoFlags)54 public static boolean isDomainVerificationIntent(Intent intent, 55 @PackageManager.ResolveInfoFlagsBits long resolveInfoFlags) { 56 if (!intent.isWebIntent()) { 57 return false; 58 } 59 60 String host = intent.getData().getHost(); 61 if (TextUtils.isEmpty(host)) { 62 return false; 63 } 64 65 if (!sCachedMatcher.get().reset(host).matches()) { 66 return false; 67 } 68 69 Set<String> categories = intent.getCategories(); 70 int categoriesSize = CollectionUtils.size(categories); 71 if (categoriesSize > 2) { 72 // Specifying at least one non-app-link category 73 return false; 74 } else if (categoriesSize == 2) { 75 // Check for explicit app link intent with exactly BROWSABLE && DEFAULT 76 return intent.hasCategory(Intent.CATEGORY_DEFAULT) 77 && intent.hasCategory(Intent.CATEGORY_BROWSABLE); 78 } 79 80 boolean matchDefaultByFlags = (resolveInfoFlags & PackageManager.MATCH_DEFAULT_ONLY) != 0; 81 82 // Check if matches (BROWSABLE || none) && DEFAULT 83 if (categoriesSize == 0) { 84 // No categories, only allow matching DEFAULT by flags 85 return matchDefaultByFlags; 86 } else if (intent.hasCategory(Intent.CATEGORY_BROWSABLE)) { 87 // Intent matches BROWSABLE, must match DEFAULT by flags 88 return matchDefaultByFlags; 89 } else { 90 // Otherwise only needs to have DEFAULT 91 return intent.hasCategory(Intent.CATEGORY_DEFAULT); 92 } 93 } 94 isChangeEnabled(PlatformCompat platformCompat, AndroidPackage pkg, long changeId)95 static boolean isChangeEnabled(PlatformCompat platformCompat, AndroidPackage pkg, 96 long changeId) { 97 return platformCompat.isChangeEnabledInternalNoLogging(changeId, buildMockAppInfo(pkg)); 98 } 99 100 /** 101 * Passed to {@link PlatformCompat} because this can be invoked mid-install process or when 102 * {@link PackageManagerService#mLock} is being held, and {@link PlatformCompat} will not be 103 * able to query the pending {@link ApplicationInfo} from {@link PackageManager}. 104 * <p> 105 * TODO(b/177613575): Can a different API be used? 106 */ 107 @NonNull buildMockAppInfo(@onNull AndroidPackage pkg)108 private static ApplicationInfo buildMockAppInfo(@NonNull AndroidPackage pkg) { 109 ApplicationInfo appInfo = new ApplicationInfo(); 110 appInfo.packageName = pkg.getPackageName(); 111 appInfo.targetSdkVersion = pkg.getTargetSdkVersion(); 112 return appInfo; 113 } 114 isValidDomain(String domain)115 static boolean isValidDomain(String domain) { 116 if (domain.length() > MAX_DOMAIN_LENGTH || domain.equals("*")) { 117 return false; 118 } 119 if (domain.charAt(0) == '*') { 120 if (domain.charAt(1) != '.') { 121 return false; 122 } 123 domain = domain.substring(2); 124 } 125 int labels = 1; 126 int labelStart = -1; 127 for (int i = 0; i < domain.length(); i++) { 128 char c = domain.charAt(i); 129 if (c == '.') { 130 int labelLength = i - labelStart - 1; 131 if (labelLength == 0 || labelLength > MAX_DOMAIN_LABEL_LENGTH) { 132 return false; 133 } 134 labelStart = i; 135 labels += 1; 136 } else if (!isValidDomainChar(c)) { 137 return false; 138 } 139 } 140 int lastLabelLength = domain.length() - labelStart - 1; 141 if (lastLabelLength == 0 || lastLabelLength > 63) { 142 return false; 143 } 144 return labels > 1; 145 } 146 isValidDomainChar(char c)147 private static boolean isValidDomainChar(char c) { 148 return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') 149 || (c >= '0' && c <= '9') || c == '-'; 150 } 151 } 152