1 /* 2 * Copyright (C) 2021 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.compat.overrides; 18 19 import static android.content.pm.PackageManager.CERT_INPUT_SHA256; 20 import static android.content.pm.PackageManager.MATCH_ANY_USER; 21 22 import static java.util.Collections.emptyMap; 23 import static java.util.Collections.emptySet; 24 25 import android.annotation.Nullable; 26 import android.app.compat.PackageOverride; 27 import android.content.pm.ApplicationInfo; 28 import android.content.pm.PackageManager; 29 import android.util.ArrayMap; 30 import android.util.ArraySet; 31 import android.util.KeyValueListParser; 32 import android.util.Pair; 33 import android.util.Slog; 34 35 import libcore.util.HexEncoding; 36 37 import java.util.Arrays; 38 import java.util.Comparator; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Set; 42 import java.util.regex.Pattern; 43 44 /** 45 * A utility class for parsing App Compat Overrides flags. 46 * 47 * @hide 48 */ 49 final class AppCompatOverridesParser { 50 /** 51 * Flag for specifying all compat change IDs owned by a namespace. See {@link 52 * #parseOwnedChangeIds} for information on how this flag is parsed. 53 */ 54 static final String FLAG_OWNED_CHANGE_IDS = "owned_change_ids"; 55 56 /** 57 * Flag for immediately removing overrides for certain packages and change IDs (from the compat 58 * platform), as well as stopping to apply them, in case of an emergency. See {@link 59 * #parseRemoveOverrides} for information on how this flag is parsed. 60 */ 61 static final String FLAG_REMOVE_OVERRIDES = "remove_overrides"; 62 63 private static final String TAG = "AppCompatOverridesParser"; 64 65 private static final String WILDCARD_SYMBOL = "*"; 66 67 private static final Pattern BOOLEAN_PATTERN = 68 Pattern.compile("true|false", Pattern.CASE_INSENSITIVE); 69 70 private static final String WILDCARD_NO_OWNED_CHANGE_IDS_WARNING = 71 "Wildcard can't be used in '" + FLAG_REMOVE_OVERRIDES + "' flag with an empty " 72 + FLAG_OWNED_CHANGE_IDS + "' flag"; 73 74 private final PackageManager mPackageManager; 75 AppCompatOverridesParser(PackageManager packageManager)76 AppCompatOverridesParser(PackageManager packageManager) { 77 mPackageManager = packageManager; 78 } 79 80 /** 81 * Parses the given {@code configStr} and returns a map from package name to a set of change 82 * IDs to remove for that package. 83 * 84 * <p>The given {@code configStr} is expected to either be: 85 * 86 * <ul> 87 * <li>'*' (wildcard), to indicate that all owned overrides, specified in {@code 88 * ownedChangeIds}, for all installed packages should be removed. 89 * <li>A comma separated key value list, where the key is a package name and the value is 90 * either: 91 * <ul> 92 * <li>'*' (wildcard), to indicate that all owned overrides, specified in {@code 93 * ownedChangeIds} for that package should be removed. 94 * <li>A colon separated list of change IDs to remove for that package. 95 * </ul> 96 * </ul> 97 * 98 * <p>If the given {@code configStr} doesn't match the expected format, an empty map will be 99 * returned. If a specific change ID isn't a valid long, it will be ignored. 100 */ parseRemoveOverrides(String configStr, Set<Long> ownedChangeIds)101 Map<String, Set<Long>> parseRemoveOverrides(String configStr, Set<Long> ownedChangeIds) { 102 if (configStr.isEmpty()) { 103 return emptyMap(); 104 } 105 106 Map<String, Set<Long>> result = new ArrayMap<>(); 107 if (configStr.equals(WILDCARD_SYMBOL)) { 108 if (ownedChangeIds.isEmpty()) { 109 Slog.w(TAG, WILDCARD_NO_OWNED_CHANGE_IDS_WARNING); 110 return emptyMap(); 111 } 112 List<ApplicationInfo> installedApps = mPackageManager.getInstalledApplications( 113 MATCH_ANY_USER); 114 for (ApplicationInfo appInfo : installedApps) { 115 result.put(appInfo.packageName, ownedChangeIds); 116 } 117 return result; 118 } 119 120 KeyValueListParser parser = new KeyValueListParser(','); 121 try { 122 parser.setString(configStr); 123 } catch (IllegalArgumentException e) { 124 Slog.w( 125 TAG, 126 "Invalid format in '" + FLAG_REMOVE_OVERRIDES + "' flag: " + configStr, e); 127 return emptyMap(); 128 } 129 for (int i = 0; i < parser.size(); i++) { 130 String packageName = parser.keyAt(i); 131 String changeIdsStr = parser.getString(packageName, /* def= */ ""); 132 if (changeIdsStr.equals(WILDCARD_SYMBOL)) { 133 if (ownedChangeIds.isEmpty()) { 134 Slog.w(TAG, WILDCARD_NO_OWNED_CHANGE_IDS_WARNING); 135 continue; 136 } 137 result.put(packageName, ownedChangeIds); 138 } else { 139 for (String changeIdStr : changeIdsStr.split(":")) { 140 try { 141 long changeId = Long.parseLong(changeIdStr); 142 result.computeIfAbsent(packageName, k -> new ArraySet<>()).add(changeId); 143 } catch (NumberFormatException e) { 144 Slog.w( 145 TAG, 146 "Invalid change ID in '" + FLAG_REMOVE_OVERRIDES + "' flag: " 147 + changeIdStr, e); 148 } 149 } 150 } 151 } 152 153 return result; 154 } 155 156 157 /** 158 * Parses the given {@code configStr}, that is expected to be a comma separated list of change 159 * IDs, into a set. 160 * 161 * <p>If any of the change IDs isn't a valid long, it will be ignored. 162 */ parseOwnedChangeIds(String configStr)163 static Set<Long> parseOwnedChangeIds(String configStr) { 164 if (configStr.isEmpty()) { 165 return emptySet(); 166 } 167 168 Set<Long> result = new ArraySet<>(); 169 for (String changeIdStr : configStr.split(",")) { 170 try { 171 result.add(Long.parseLong(changeIdStr)); 172 } catch (NumberFormatException e) { 173 Slog.w(TAG, 174 "Invalid change ID in '" + FLAG_OWNED_CHANGE_IDS + "' flag: " + changeIdStr, 175 e); 176 } 177 } 178 return result; 179 } 180 181 /** 182 * Parses the given {@code configStr}, that is expected to be a comma separated list of changes 183 * overrides, and returns a map from change ID to {@link PackageOverride} instances to add. 184 * 185 * <p>Each change override is in the following format: 186 * '<signature?>~<change-id>:<min-version-code?>:<max-version-code?>:<enabled>'. 187 * 188 * <p>The signature is optional, and will only be enforced if included. 189 * 190 * <p>If there are multiple overrides that should be added with the same change ID, the one 191 * that best fits the given {@code versionCode} is added. 192 * 193 * <p>Any overrides whose change ID is in {@code changeIdsToSkip} are ignored. 194 * 195 * <p>If a change override entry in {@code configStr} is invalid, it will be ignored. 196 */ parsePackageOverrides(String configStr, String packageName, long versionCode, Set<Long> changeIdsToSkip)197 Map<Long, PackageOverride> parsePackageOverrides(String configStr, String packageName, 198 long versionCode, 199 Set<Long> changeIdsToSkip) { 200 if (configStr.isEmpty()) { 201 return emptyMap(); 202 } 203 PackageOverrideComparator comparator = new PackageOverrideComparator(versionCode); 204 Map<Long, PackageOverride> overridesToAdd = new ArrayMap<>(); 205 206 Pair<String, String> signatureAndConfig = extractSignatureFromConfig(configStr); 207 if (signatureAndConfig == null) { 208 return emptyMap(); 209 } 210 final String signature = signatureAndConfig.first; 211 final String overridesConfig = signatureAndConfig.second; 212 213 if (!verifySignature(packageName, signature)) { 214 return emptyMap(); 215 } 216 217 for (String overrideEntryString : overridesConfig.split(",")) { 218 List<String> changeIdAndVersions = Arrays.asList(overrideEntryString.split(":", 4)); 219 if (changeIdAndVersions.size() != 4) { 220 Slog.w(TAG, "Invalid change override entry: " + overrideEntryString); 221 continue; 222 } 223 long changeId; 224 try { 225 changeId = Long.parseLong(changeIdAndVersions.get(0)); 226 } catch (NumberFormatException e) { 227 Slog.w(TAG, "Invalid change ID in override entry: " + overrideEntryString, e); 228 continue; 229 } 230 231 if (changeIdsToSkip.contains(changeId)) { 232 continue; 233 } 234 235 String minVersionCodeStr = changeIdAndVersions.get(1); 236 String maxVersionCodeStr = changeIdAndVersions.get(2); 237 238 String enabledStr = changeIdAndVersions.get(3); 239 if (!BOOLEAN_PATTERN.matcher(enabledStr).matches()) { 240 Slog.w(TAG, "Invalid enabled string in override entry: " + overrideEntryString); 241 continue; 242 } 243 boolean enabled = Boolean.parseBoolean(enabledStr); 244 PackageOverride.Builder overrideBuilder = new PackageOverride.Builder().setEnabled( 245 enabled); 246 try { 247 if (!minVersionCodeStr.isEmpty()) { 248 overrideBuilder.setMinVersionCode(Long.parseLong(minVersionCodeStr)); 249 } 250 if (!maxVersionCodeStr.isEmpty()) { 251 overrideBuilder.setMaxVersionCode(Long.parseLong(maxVersionCodeStr)); 252 } 253 } catch (NumberFormatException e) { 254 Slog.w(TAG, 255 "Invalid min/max version code in override entry: " + overrideEntryString, 256 e); 257 continue; 258 } 259 260 try { 261 PackageOverride override = overrideBuilder.build(); 262 if (!overridesToAdd.containsKey(changeId) 263 || comparator.compare(override, overridesToAdd.get(changeId)) < 0) { 264 overridesToAdd.put(changeId, override); 265 } 266 } catch (IllegalArgumentException e) { 267 Slog.w(TAG, "Failed to build PackageOverride", e); 268 } 269 } 270 271 return overridesToAdd; 272 } 273 274 /** 275 * Extracts the signature from the config string if one exists. 276 * 277 * @param configStr String in the form of <signature?>~<overrideConfig> 278 */ 279 @Nullable extractSignatureFromConfig(String configStr)280 private static Pair<String, String> extractSignatureFromConfig(String configStr) { 281 final List<String> signatureAndConfig = Arrays.asList(configStr.split("~")); 282 283 if (signatureAndConfig.size() == 1) { 284 // The config string doesn't contain a signature. 285 return Pair.create("", configStr); 286 } 287 288 if (signatureAndConfig.size() > 2) { 289 Slog.w(TAG, "Only one signature per config is supported. Config: " + configStr); 290 return null; 291 } 292 293 return Pair.create(signatureAndConfig.get(0), signatureAndConfig.get(1)); 294 } 295 296 /** 297 * Verifies that the specified package was signed with a particular signature. 298 * 299 * @param packageName The package to check. 300 * @param signature The optional signature to verify. If empty, we return true. 301 * @return Whether the package is signed with that signature. 302 */ verifySignature(String packageName, String signature)303 private boolean verifySignature(String packageName, String signature) { 304 try { 305 final boolean signatureValid = signature.isEmpty() 306 || mPackageManager.hasSigningCertificate(packageName, 307 HexEncoding.decode(signature), CERT_INPUT_SHA256); 308 309 if (!signatureValid) { 310 Slog.w(TAG, packageName + " did not have expected signature: " + signature); 311 } 312 return signatureValid; 313 } catch (IllegalArgumentException e) { 314 Slog.w(TAG, "Unable to verify signature " + signature + " for " + packageName, e); 315 return false; 316 } 317 } 318 319 /** 320 * A {@link Comparator} that compares @link PackageOverride} instances with respect to a 321 * specified {@code versionCode} as follows: 322 * 323 * <ul> 324 * <li>Prefer the {@link PackageOverride} whose version range contains {@code versionCode}. 325 * <li>Otherwise, prefer the {@link PackageOverride} whose version range is closest to {@code 326 * versionCode} from below. 327 * <li>Otherwise, prefer the {@link PackageOverride} whose version range is closest to {@code 328 * versionCode} from above. 329 * </ul> 330 */ 331 private static final class PackageOverrideComparator implements Comparator<PackageOverride> { 332 private final long mVersionCode; 333 PackageOverrideComparator(long versionCode)334 PackageOverrideComparator(long versionCode) { 335 this.mVersionCode = versionCode; 336 } 337 338 @Override compare(PackageOverride o1, PackageOverride o2)339 public int compare(PackageOverride o1, PackageOverride o2) { 340 // Prefer overrides whose version range contains versionCode. 341 boolean isVersionInRange1 = isVersionInRange(o1, mVersionCode); 342 boolean isVersionInRange2 = isVersionInRange(o2, mVersionCode); 343 if (isVersionInRange1 != isVersionInRange2) { 344 return isVersionInRange1 ? -1 : 1; 345 } 346 347 // Otherwise, prefer overrides whose version range is before versionCode. 348 boolean isVersionAfterRange1 = isVersionAfterRange(o1, mVersionCode); 349 boolean isVersionAfterRange2 = isVersionAfterRange(o2, mVersionCode); 350 if (isVersionAfterRange1 != isVersionAfterRange2) { 351 return isVersionAfterRange1 ? -1 : 1; 352 } 353 354 // If both overrides' version ranges are either before or after versionCode, prefer 355 // those whose version range is closer to versionCode. 356 return Long.compare( 357 getVersionProximity(o1, mVersionCode), getVersionProximity(o2, mVersionCode)); 358 } 359 360 /** 361 * Returns true if the version range in the given {@code override} contains {@code 362 * versionCode}. 363 */ isVersionInRange(PackageOverride override, long versionCode)364 private static boolean isVersionInRange(PackageOverride override, long versionCode) { 365 return override.getMinVersionCode() <= versionCode 366 && versionCode <= override.getMaxVersionCode(); 367 } 368 369 /** 370 * Returns true if the given {@code versionCode} is strictly after the version range in the 371 * given {@code override}. 372 */ isVersionAfterRange(PackageOverride override, long versionCode)373 private static boolean isVersionAfterRange(PackageOverride override, long versionCode) { 374 return override.getMaxVersionCode() < versionCode; 375 } 376 377 /** 378 * Returns true if the given {@code versionCode} is strictly before the version range in the 379 * given {@code override}. 380 */ isVersionBeforeRange(PackageOverride override, long versionCode)381 private static boolean isVersionBeforeRange(PackageOverride override, long versionCode) { 382 return override.getMinVersionCode() > versionCode; 383 } 384 385 /** 386 * In case the given {@code versionCode} is strictly before or after the version range in 387 * the given {@code override}, returns the distance from it, otherwise returns zero. 388 */ getVersionProximity(PackageOverride override, long versionCode)389 private static long getVersionProximity(PackageOverride override, long versionCode) { 390 if (isVersionAfterRange(override, versionCode)) { 391 return versionCode - override.getMaxVersionCode(); 392 } 393 if (isVersionBeforeRange(override, versionCode)) { 394 return override.getMinVersionCode() - versionCode; 395 } 396 397 // Version is in range. Note that when two overrides have a zero version proximity 398 // they will be ordered arbitrarily. 399 return 0; 400 } 401 } 402 } 403