1 /*
2  * Copyright (C) 2022 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.internal.pm.pkg.component;
18 
19 import static com.android.internal.pm.pkg.parsing.ParsingUtils.NOT_SET;
20 
21 import android.annotation.NonNull;
22 import android.content.pm.PermissionInfo;
23 import android.content.pm.parsing.result.ParseInput;
24 import android.content.pm.parsing.result.ParseResult;
25 import android.content.res.Resources;
26 import android.content.res.TypedArray;
27 import android.content.res.XmlResourceParser;
28 import android.os.Build;
29 import android.util.ArrayMap;
30 import android.util.EventLog;
31 import android.util.Slog;
32 
33 import com.android.internal.R;
34 import com.android.internal.pm.pkg.parsing.ParsingPackage;
35 import com.android.internal.pm.pkg.parsing.ParsingUtils;
36 
37 import org.xmlpull.v1.XmlPullParserException;
38 
39 import java.io.IOException;
40 import java.util.List;
41 import java.util.Objects;
42 
43 /**
44  * @hide
45  */
46 public class ParsedPermissionUtils {
47 
48     private static final String TAG = ParsingUtils.TAG;
49 
50     @NonNull
parsePermission(ParsingPackage pkg, Resources res, XmlResourceParser parser, boolean useRoundIcon, ParseInput input)51     public static ParseResult<ParsedPermission> parsePermission(ParsingPackage pkg, Resources res,
52             XmlResourceParser parser, boolean useRoundIcon, ParseInput input)
53             throws IOException, XmlPullParserException {
54         String packageName = pkg.getPackageName();
55         ParsedPermissionImpl permission = new ParsedPermissionImpl();
56         String tag = "<" + parser.getName() + ">";
57         ParseResult<ParsedPermissionImpl> result;
58 
59         try (TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestPermission)) {
60             result = ParsedComponentUtils.parseComponent(
61                     permission, tag, pkg, sa, useRoundIcon, input,
62                     R.styleable.AndroidManifestPermission_banner,
63                     R.styleable.AndroidManifestPermission_description,
64                     R.styleable.AndroidManifestPermission_icon,
65                     R.styleable.AndroidManifestPermission_label,
66                     R.styleable.AndroidManifestPermission_logo,
67                     R.styleable.AndroidManifestPermission_name,
68                     R.styleable.AndroidManifestPermission_roundIcon);
69             if (result.isError()) {
70                 return input.error(result);
71             }
72 
73             int maxSdkVersion = sa.getInt(R.styleable.AndroidManifestPermission_maxSdkVersion, -1);
74             if ((maxSdkVersion != -1) && (maxSdkVersion < Build.VERSION.SDK_INT)) {
75                 return input.success(null);
76             }
77 
78             if (sa.hasValue(
79                     R.styleable.AndroidManifestPermission_backgroundPermission)) {
80                 if ("android".equals(packageName)) {
81                     permission.setBackgroundPermission(sa.getNonResourceString(
82                             R.styleable.AndroidManifestPermission_backgroundPermission));
83                 } else {
84                     Slog.w(TAG, packageName + " defines a background permission. Only the "
85                             + "'android' package can do that.");
86                 }
87             }
88 
89             // Note: don't allow this value to be a reference to a resource
90             // that may change.
91             permission.setGroup(sa.getNonResourceString(
92                     R.styleable.AndroidManifestPermission_permissionGroup))
93                     .setRequestRes(sa.getResourceId(
94                             R.styleable.AndroidManifestPermission_request, 0))
95                     .setProtectionLevel(sa.getInt(
96                             R.styleable.AndroidManifestPermission_protectionLevel,
97                             PermissionInfo.PROTECTION_NORMAL))
98                     .setFlags(sa.getInt(
99                             R.styleable.AndroidManifestPermission_permissionFlags, 0));
100 
101             final int knownCertsResource = sa.getResourceId(
102                     R.styleable.AndroidManifestPermission_knownCerts, 0);
103             if (knownCertsResource != 0) {
104                 // The knownCerts attribute supports both a string array resource as well as a
105                 // string resource for the case where the permission should only be granted to a
106                 // single known signer.
107                 final String resourceType = res.getResourceTypeName(knownCertsResource);
108                 if (resourceType.equals("array")) {
109                     final String[] knownCerts = res.getStringArray(knownCertsResource);
110                     if (knownCerts != null) {
111                         permission.setKnownCerts(knownCerts);
112                     }
113                 } else {
114                     final String knownCert = res.getString(knownCertsResource);
115                     if (knownCert != null) {
116                         permission.setKnownCert(knownCert);
117                     }
118                 }
119                 if (permission.getKnownCerts().isEmpty()) {
120                     Slog.w(TAG, packageName + " defines a knownSigner permission but"
121                             + " the provided knownCerts resource is null");
122                 }
123             } else {
124                 // If the knownCerts resource ID is null check if the app specified a string
125                 // value for the attribute representing a single trusted signer.
126                 final String knownCert = sa.getString(
127                         R.styleable.AndroidManifestPermission_knownCerts);
128                 if (knownCert != null) {
129                     permission.setKnownCert(knownCert);
130                 }
131             }
132 
133             // For now only platform runtime permissions can be restricted
134             if (!isRuntime(permission) || !"android".equals(permission.getPackageName())) {
135                 permission.setFlags(permission.getFlags() & ~PermissionInfo.FLAG_HARD_RESTRICTED);
136                 permission.setFlags(permission.getFlags() & ~PermissionInfo.FLAG_SOFT_RESTRICTED);
137             } else {
138                 // The platform does not get to specify conflicting permissions
139                 if ((permission.getFlags() & PermissionInfo.FLAG_HARD_RESTRICTED) != 0
140                         && (permission.getFlags() & PermissionInfo.FLAG_SOFT_RESTRICTED) != 0) {
141                     throw new IllegalStateException("Permission cannot be both soft and hard"
142                             + " restricted: " + permission.getName());
143                 }
144             }
145         }
146 
147         permission.setProtectionLevel(
148                 PermissionInfo.fixProtectionLevel(permission.getProtectionLevel()));
149 
150         final int otherProtectionFlags = getProtectionFlags(permission)
151                 & ~(PermissionInfo.PROTECTION_FLAG_APPOP | PermissionInfo.PROTECTION_FLAG_INSTANT
152                 | PermissionInfo.PROTECTION_FLAG_RUNTIME_ONLY);
153         if (otherProtectionFlags != 0
154                 && getProtection(permission) != PermissionInfo.PROTECTION_SIGNATURE
155                 && getProtection(permission) != PermissionInfo.PROTECTION_INTERNAL) {
156             return input.error("<permission> protectionLevel specifies a non-instant, non-appop,"
157                     + " non-runtimeOnly flag but is not based on signature or internal type");
158         }
159 
160         result = ComponentParseUtils.parseAllMetaData(pkg, res, parser, tag, permission, input);
161         if (result.isError()) {
162             return input.error(result);
163         }
164 
165         return input.success(result.getResult());
166     }
167 
168     @NonNull
parsePermissionTree(ParsingPackage pkg, Resources res, XmlResourceParser parser, boolean useRoundIcon, ParseInput input)169     public static ParseResult<ParsedPermission> parsePermissionTree(ParsingPackage pkg, Resources res,
170             XmlResourceParser parser, boolean useRoundIcon, ParseInput input)
171             throws IOException, XmlPullParserException {
172         ParsedPermissionImpl permission = new ParsedPermissionImpl();
173         String tag = "<" + parser.getName() + ">";
174         ParseResult<ParsedPermissionImpl> result;
175 
176         TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestPermissionTree);
177         try {
178             result = ParsedComponentUtils.parseComponent(
179                     permission, tag, pkg, sa, useRoundIcon, input,
180                     R.styleable.AndroidManifestPermissionTree_banner,
181                     NOT_SET /*descriptionAttr*/,
182                     R.styleable.AndroidManifestPermissionTree_icon,
183                     R.styleable.AndroidManifestPermissionTree_label,
184                     R.styleable.AndroidManifestPermissionTree_logo,
185                     R.styleable.AndroidManifestPermissionTree_name,
186                     R.styleable.AndroidManifestPermissionTree_roundIcon);
187             if (result.isError()) {
188                 return input.error(result);
189             }
190         } finally {
191             sa.recycle();
192         }
193 
194         int index = permission.getName().indexOf('.');
195         if (index > 0) {
196             index = permission.getName().indexOf('.', index + 1);
197         }
198         if (index < 0) {
199             return input.error("<permission-tree> name has less than three segments: "
200                     + permission.getName());
201         }
202 
203         permission.setProtectionLevel(PermissionInfo.PROTECTION_NORMAL)
204                 .setTree(true);
205 
206         result = ComponentParseUtils.parseAllMetaData(pkg, res, parser, tag, permission, input);
207         if (result.isError()) {
208             return input.error(result);
209         }
210 
211         return input.success(result.getResult());
212     }
213 
214     @NonNull
parsePermissionGroup(ParsingPackage pkg, Resources res, XmlResourceParser parser, boolean useRoundIcon, ParseInput input)215     public static ParseResult<ParsedPermissionGroup> parsePermissionGroup(ParsingPackage pkg,
216             Resources res, XmlResourceParser parser, boolean useRoundIcon, ParseInput input)
217             throws IOException, XmlPullParserException {
218         ParsedPermissionGroupImpl
219                 permissionGroup = new ParsedPermissionGroupImpl();
220         String tag = "<" + parser.getName() + ">";
221 
222         TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestPermissionGroup);
223         try {
224             ParseResult<ParsedPermissionGroupImpl> result = ParsedComponentUtils.parseComponent(
225                     permissionGroup, tag, pkg, sa, useRoundIcon, input,
226                     R.styleable.AndroidManifestPermissionGroup_banner,
227                     R.styleable.AndroidManifestPermissionGroup_description,
228                     R.styleable.AndroidManifestPermissionGroup_icon,
229                     R.styleable.AndroidManifestPermissionGroup_label,
230                     R.styleable.AndroidManifestPermissionGroup_logo,
231                     R.styleable.AndroidManifestPermissionGroup_name,
232                     R.styleable.AndroidManifestPermissionGroup_roundIcon);
233             if (result.isError()) {
234                 return input.error(result);
235             }
236 
237             // @formatter:off
238             permissionGroup.setRequestDetailRes(sa.getResourceId(R.styleable.AndroidManifestPermissionGroup_requestDetail, 0))
239                     .setBackgroundRequestRes(sa.getResourceId(R.styleable.AndroidManifestPermissionGroup_backgroundRequest, 0))
240                     .setBackgroundRequestDetailRes(sa.getResourceId(R.styleable.AndroidManifestPermissionGroup_backgroundRequestDetail, 0))
241                     .setRequestRes(sa.getResourceId(R.styleable.AndroidManifestPermissionGroup_request, 0))
242                     .setPriority(sa.getInt(R.styleable.AndroidManifestPermissionGroup_priority, 0))
243                     .setFlags(sa.getInt(R.styleable.AndroidManifestPermissionGroup_permissionGroupFlags,0));
244             // @formatter:on
245         } finally {
246             sa.recycle();
247         }
248 
249         ParseResult<ParsedPermissionGroupImpl> result = ComponentParseUtils.parseAllMetaData(pkg,
250                 res, parser, tag, permissionGroup, input);
251         if (result.isError()) {
252             return input.error(result);
253         }
254 
255         return input.success(result.getResult());
256     }
257 
isRuntime(@onNull ParsedPermission permission)258     public static boolean isRuntime(@NonNull ParsedPermission permission) {
259         return getProtection(permission) == PermissionInfo.PROTECTION_DANGEROUS;
260     }
261 
isAppOp(@onNull ParsedPermission permission)262     public static boolean isAppOp(@NonNull ParsedPermission permission) {
263         return (permission.getProtectionLevel() & PermissionInfo.PROTECTION_FLAG_APPOP) != 0;
264     }
265 
266     @PermissionInfo.Protection
getProtection(@onNull ParsedPermission permission)267     public static int getProtection(@NonNull ParsedPermission permission) {
268         return permission.getProtectionLevel() & PermissionInfo.PROTECTION_MASK_BASE;
269     }
270 
getProtectionFlags(@onNull ParsedPermission permission)271     public static int getProtectionFlags(@NonNull ParsedPermission permission) {
272         return permission.getProtectionLevel() & ~PermissionInfo.PROTECTION_MASK_BASE;
273     }
274 
calculateFootprint(@onNull ParsedPermission permission)275     public static int calculateFootprint(@NonNull ParsedPermission permission) {
276         int size = permission.getName().length();
277         CharSequence nonLocalizedLabel = permission.getNonLocalizedLabel();
278         if (nonLocalizedLabel != null) {
279             size += nonLocalizedLabel.length();
280         }
281         return size;
282     }
283 
284     /**
285      * Determines if a duplicate permission is malformed .i.e. defines different protection level
286      * or group.
287      */
isMalformedDuplicate(ParsedPermission p1, ParsedPermission p2)288     private static boolean isMalformedDuplicate(ParsedPermission p1, ParsedPermission p2) {
289         // Since a permission tree is also added as a permission with normal protection
290         // level, we need to skip if the parsedPermission is a permission tree.
291         if (p1 == null || p2 == null || p1.isTree() || p2.isTree()) {
292             return false;
293         }
294 
295         if (p1.getProtectionLevel() != p2.getProtectionLevel()) {
296             return true;
297         }
298         if (!Objects.equals(p1.getGroup(), p2.getGroup())) {
299             return true;
300         }
301 
302         return false;
303     }
304 
305     /**
306      * @return {@code true} if the package declares malformed duplicate permissions.
307      */
declareDuplicatePermission(@onNull ParsingPackage pkg)308     public static boolean declareDuplicatePermission(@NonNull ParsingPackage pkg) {
309         final List<ParsedPermission> permissions = pkg.getPermissions();
310         final int size = permissions.size();
311         if (size > 0) {
312             final ArrayMap<String, ParsedPermission> checkDuplicatePerm = new ArrayMap<>(size);
313             for (int i = 0; i < size; i++) {
314                 final ParsedPermission parsedPermission = permissions.get(i);
315                 final String name = parsedPermission.getName();
316                 final ParsedPermission perm = checkDuplicatePerm.get(name);
317                 if (isMalformedDuplicate(parsedPermission, perm)) {
318                     // Fix for b/213323615
319                     EventLog.writeEvent(0x534e4554, "213323615");
320                     return true;
321                 }
322                 checkDuplicatePerm.put(name, parsedPermission);
323             }
324         }
325         return false;
326     }
327 }
328