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