1 /*
2  * Copyright (C) 2012 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;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.compat.annotation.ChangeId;
22 import android.compat.annotation.Disabled;
23 import android.compat.annotation.EnabledAfter;
24 import android.content.pm.ApplicationInfo;
25 import android.content.pm.Signature;
26 import android.content.pm.SigningDetails;
27 import android.os.Environment;
28 import android.util.Slog;
29 import android.util.Xml;
30 
31 import com.android.server.compat.PlatformCompat;
32 import com.android.server.pm.parsing.pkg.AndroidPackageUtils;
33 import com.android.server.pm.pkg.AndroidPackage;
34 import com.android.server.pm.pkg.PackageState;
35 import com.android.server.pm.pkg.SharedUserApi;
36 
37 import libcore.io.IoUtils;
38 
39 import org.xmlpull.v1.XmlPullParser;
40 import org.xmlpull.v1.XmlPullParserException;
41 
42 import java.io.File;
43 import java.io.FileReader;
44 import java.io.IOException;
45 import java.util.ArrayList;
46 import java.util.Collections;
47 import java.util.Comparator;
48 import java.util.HashMap;
49 import java.util.HashSet;
50 import java.util.List;
51 import java.util.Map;
52 import java.util.Set;
53 
54 /**
55  * Centralized access to SELinux MMAC (middleware MAC) implementation. This
56  * class is responsible for loading the appropriate mac_permissions.xml file
57  * as well as providing an interface for assigning seinfo values to apks.
58  *
59  * {@hide}
60  */
61 public final class SELinuxMMAC {
62 
63     static final String TAG = "SELinuxMMAC";
64 
65     private static final boolean DEBUG_POLICY = false;
66     private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false;
67     private static final boolean DEBUG_POLICY_ORDER = DEBUG_POLICY || false;
68 
69     // All policy stanzas read from mac_permissions.xml. This is also the lock
70     // to synchronize access during policy load and access attempts.
71     private static final List<Policy> sPolicies = new ArrayList<>();
72     /** Whether or not the policy files have been read */
73     private static boolean sPolicyRead;
74 
75     /** Required MAC permissions files */
76     private static final List<File> sMacPermissions = new ArrayList<>();
77 
78     private static final String DEFAULT_SEINFO = "default";
79 
80     // Append privapp to existing seinfo label
81     private static final String PRIVILEGED_APP_STR = ":privapp";
82 
83     // Append targetSdkVersion=n to existing seinfo label where n is the app's targetSdkVersion
84     private static final String TARGETSDKVERSION_STR = ":targetSdkVersion=";
85 
86     private static final String PARTITION_STR = ":partition=";
87 
88     /**
89      * Allows opt-in to the latest targetSdkVersion enforced changes without changing target SDK.
90      * Turning this change on for an app targeting the latest SDK or higher is a no-op.
91      *
92      * <p>Has no effect for apps using shared user id.
93      *
94      * TODO(b/143539591): Update description with relevant SELINUX changes this opts in to.
95      */
96     @Disabled
97     @ChangeId
98     static final long SELINUX_LATEST_CHANGES = 143539591L;
99 
100     /**
101      * This change gates apps access to untrusted_app_R-targetSDK SELinux domain. Allows opt-in
102      * to R targetSdkVersion enforced changes without changing target SDK. Turning this change
103      * off for an app targeting {@code >= android.os.Build.VERSION_CODES.R} is a no-op.
104      *
105      * <p>Has no effect for apps using shared user id.
106      *
107      * TODO(b/143539591): Update description with relevant SELINUX changes this opts in to.
108      */
109     @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.Q)
110     @ChangeId
111     static final long SELINUX_R_CHANGES = 168782947L;
112 
113     // Only initialize sMacPermissions once.
114     static {
115         // Platform mac permissions.
sMacPermissions.add(new File( Environment.getRootDirectory(), "/etc/selinux/plat_mac_permissions.xml"))116         sMacPermissions.add(new File(
117             Environment.getRootDirectory(), "/etc/selinux/plat_mac_permissions.xml"));
118 
119         // SystemExt mac permissions (optional).
120         final File systemExtMacPermission = new File(
121                 Environment.getSystemExtDirectory(), "/etc/selinux/system_ext_mac_permissions.xml");
122         if (systemExtMacPermission.exists()) {
123             sMacPermissions.add(systemExtMacPermission);
124         }
125 
126         // Product mac permissions (optional).
127         final File productMacPermission = new File(
128                 Environment.getProductDirectory(), "/etc/selinux/product_mac_permissions.xml");
129         if (productMacPermission.exists()) {
130             sMacPermissions.add(productMacPermission);
131         }
132 
133         // Vendor mac permissions.
134         final File vendorMacPermission = new File(
135             Environment.getVendorDirectory(), "/etc/selinux/vendor_mac_permissions.xml");
136         if (vendorMacPermission.exists()) {
137             sMacPermissions.add(vendorMacPermission);
138         }
139 
140         // ODM mac permissions (optional).
141         final File odmMacPermission = new File(
142             Environment.getOdmDirectory(), "/etc/selinux/odm_mac_permissions.xml");
143         if (odmMacPermission.exists()) {
144             sMacPermissions.add(odmMacPermission);
145         }
146     }
147 
148     /**
149      * Load the mac_permissions.xml file containing all seinfo assignments used to
150      * label apps. The loaded mac_permissions.xml files are plat_mac_permissions.xml and
151      * vendor_mac_permissions.xml, on /system and /vendor partitions, respectively.
152      * odm_mac_permissions.xml on /odm partition is optional. For further guidance on
153      * the proper structure of a mac_permissions.xml file consult the source code
154      * located at system/sepolicy/private/mac_permissions.xml.
155      *
156      * @return boolean indicating if policy was correctly loaded. A value of false
157      *         typically indicates a structural problem with the xml or incorrectly
158      *         constructed policy stanzas. A value of true means that all stanzas
159      *         were loaded successfully; no partial loading is possible.
160      */
readInstallPolicy()161     public static boolean readInstallPolicy() {
162         synchronized (sPolicies) {
163             if (sPolicyRead) {
164                 return true;
165             }
166         }
167 
168         // Temp structure to hold the rules while we parse the xml file
169         List<Policy> policies = new ArrayList<>();
170 
171         FileReader policyFile = null;
172         XmlPullParser parser = Xml.newPullParser();
173 
174         final int count = sMacPermissions.size();
175         for (int i = 0; i < count; ++i) {
176             final File macPermission = sMacPermissions.get(i);
177             try {
178                 policyFile = new FileReader(macPermission);
179                 Slog.d(TAG, "Using policy file " + macPermission);
180 
181                 parser.setInput(policyFile);
182                 parser.nextTag();
183                 parser.require(XmlPullParser.START_TAG, null, "policy");
184 
185                 while (parser.next() != XmlPullParser.END_TAG) {
186                     if (parser.getEventType() != XmlPullParser.START_TAG) {
187                         continue;
188                     }
189 
190                     switch (parser.getName()) {
191                         case "signer":
192                             policies.add(readSignerOrThrow(parser));
193                             break;
194                         default:
195                             skip(parser);
196                     }
197                 }
198             } catch (IllegalStateException | IllegalArgumentException |
199                      XmlPullParserException ex) {
200                 StringBuilder sb = new StringBuilder("Exception @");
201                 sb.append(parser.getPositionDescription());
202                 sb.append(" while parsing ");
203                 sb.append(macPermission);
204                 sb.append(":");
205                 sb.append(ex);
206                 Slog.w(TAG, sb.toString());
207                 return false;
208             } catch (IOException ioe) {
209                 Slog.w(TAG, "Exception parsing " + macPermission, ioe);
210                 return false;
211             } finally {
212                 IoUtils.closeQuietly(policyFile);
213             }
214         }
215 
216         // Now sort the policy stanzas
217         PolicyComparator policySort = new PolicyComparator();
218         Collections.sort(policies, policySort);
219         if (policySort.foundDuplicate()) {
220             Slog.w(TAG, "ERROR! Duplicate entries found parsing mac_permissions.xml files");
221             return false;
222         }
223 
224         synchronized (sPolicies) {
225             sPolicies.clear();
226             sPolicies.addAll(policies);
227             sPolicyRead = true;
228 
229             if (DEBUG_POLICY_ORDER) {
230                 for (Policy policy : sPolicies) {
231                     Slog.d(TAG, "Policy: " + policy.toString());
232                 }
233             }
234         }
235 
236         return true;
237     }
238 
239     /**
240      * Loop over a signer tag looking for seinfo, package and cert tags. A {@link Policy}
241      * instance will be created and returned in the process. During the pass all other
242      * tag elements will be skipped.
243      *
244      * @param parser an XmlPullParser object representing a signer element.
245      * @return the constructed {@link Policy} instance
246      * @throws IOException
247      * @throws XmlPullParserException
248      * @throws IllegalArgumentException if any of the validation checks fail while
249      *         parsing tag values.
250      * @throws IllegalStateException if any of the invariants fail when constructing
251      *         the {@link Policy} instance.
252      */
readSignerOrThrow(XmlPullParser parser)253     private static Policy readSignerOrThrow(XmlPullParser parser) throws IOException,
254             XmlPullParserException {
255 
256         parser.require(XmlPullParser.START_TAG, null, "signer");
257         Policy.PolicyBuilder pb = new Policy.PolicyBuilder();
258 
259         // Check for a cert attached to the signer tag. We allow a signature
260         // to appear as an attribute as well as those attached to cert tags.
261         String cert = parser.getAttributeValue(null, "signature");
262         if (cert != null) {
263             pb.addSignature(cert);
264         }
265 
266         while (parser.next() != XmlPullParser.END_TAG) {
267             if (parser.getEventType() != XmlPullParser.START_TAG) {
268                 continue;
269             }
270 
271             String tagName = parser.getName();
272             if ("seinfo".equals(tagName)) {
273                 String seinfo = parser.getAttributeValue(null, "value");
274                 pb.setGlobalSeinfoOrThrow(seinfo);
275                 readSeinfo(parser);
276             } else if ("package".equals(tagName)) {
277                 readPackageOrThrow(parser, pb);
278             } else if ("cert".equals(tagName)) {
279                 String sig = parser.getAttributeValue(null, "signature");
280                 pb.addSignature(sig);
281                 readCert(parser);
282             } else {
283                 skip(parser);
284             }
285         }
286 
287         return pb.build();
288     }
289 
290     /**
291      * Loop over a package element looking for seinfo child tags. If found return the
292      * value attribute of the seinfo tag, otherwise return null. All other tags encountered
293      * will be skipped.
294      *
295      * @param parser an XmlPullParser object representing a package element.
296      * @param pb a Policy.PolicyBuilder instance to build
297      * @throws IOException
298      * @throws XmlPullParserException
299      * @throws IllegalArgumentException if any of the validation checks fail while
300      *         parsing tag values.
301      * @throws IllegalStateException if there is a duplicate seinfo tag for the current
302      *         package tag.
303      */
readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb)304     private static void readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb) throws
305             IOException, XmlPullParserException {
306         parser.require(XmlPullParser.START_TAG, null, "package");
307         String pkgName = parser.getAttributeValue(null, "name");
308 
309         while (parser.next() != XmlPullParser.END_TAG) {
310             if (parser.getEventType() != XmlPullParser.START_TAG) {
311                 continue;
312             }
313 
314             String tagName = parser.getName();
315             if ("seinfo".equals(tagName)) {
316                 String seinfo = parser.getAttributeValue(null, "value");
317                 pb.addInnerPackageMapOrThrow(pkgName, seinfo);
318                 readSeinfo(parser);
319             } else {
320                 skip(parser);
321             }
322         }
323     }
324 
readCert(XmlPullParser parser)325     private static void readCert(XmlPullParser parser) throws IOException,
326             XmlPullParserException {
327         parser.require(XmlPullParser.START_TAG, null, "cert");
328         parser.nextTag();
329     }
330 
readSeinfo(XmlPullParser parser)331     private static void readSeinfo(XmlPullParser parser) throws IOException,
332             XmlPullParserException {
333         parser.require(XmlPullParser.START_TAG, null, "seinfo");
334         parser.nextTag();
335     }
336 
skip(XmlPullParser p)337     private static void skip(XmlPullParser p) throws IOException, XmlPullParserException {
338         if (p.getEventType() != XmlPullParser.START_TAG) {
339             throw new IllegalStateException();
340         }
341         int depth = 1;
342         while (depth != 0) {
343             switch (p.next()) {
344             case XmlPullParser.END_TAG:
345                 depth--;
346                 break;
347             case XmlPullParser.START_TAG:
348                 depth++;
349                 break;
350             }
351         }
352     }
353 
getTargetSdkVersionForSeInfo(AndroidPackage pkg, SharedUserApi sharedUser, PlatformCompat compatibility)354     private static int getTargetSdkVersionForSeInfo(AndroidPackage pkg,
355             SharedUserApi sharedUser, PlatformCompat compatibility) {
356         // Apps which share a sharedUserId must be placed in the same selinux domain. If this
357         // package is the first app installed as this shared user, set seInfoTargetSdkVersion to its
358         // targetSdkVersion. These are later adjusted in PackageManagerService's constructor to be
359         // the lowest targetSdkVersion of all apps within the shared user, which corresponds to the
360         // least restrictive selinux domain.
361         // NOTE: As new packages are installed / updated, the shared user's seinfoTargetSdkVersion
362         // will NOT be modified until next boot, even if a lower targetSdkVersion is used. This
363         // ensures that all packages continue to run in the same selinux domain.
364         if ((sharedUser != null) && (sharedUser.getPackages().size() != 0)) {
365             return sharedUser.getSeInfoTargetSdkVersion();
366         }
367         final ApplicationInfo appInfo = AndroidPackageUtils.generateAppInfoWithoutState(pkg);
368         if (compatibility.isChangeEnabledInternal(SELINUX_LATEST_CHANGES, appInfo)) {
369             return Math.max(
370                     android.os.Build.VERSION_CODES.CUR_DEVELOPMENT, pkg.getTargetSdkVersion());
371         } else if (compatibility.isChangeEnabledInternal(SELINUX_R_CHANGES, appInfo)) {
372             return Math.max(android.os.Build.VERSION_CODES.R, pkg.getTargetSdkVersion());
373         }
374 
375         return pkg.getTargetSdkVersion();
376     }
377 
getPartition(PackageState state)378     private static String getPartition(PackageState state) {
379         if (state.isSystemExt()) {
380             return "system_ext";
381         } else if (state.isProduct()) {
382             return "product";
383         } else if (state.isVendor()) {
384             return "vendor";
385         } else if (state.isOem()) {
386             return "oem";
387         } else if (state.isOdm()) {
388             return "odm";
389         } else if (state.isSystem()) {
390             return "system";
391         }
392         return "";
393     }
394 
395     /**
396      * Selects a security label to a package based on input parameters and the seinfo tag taken
397      * from a matched policy. All signature based policy stanzas are consulted and, if no match
398      * is found, the default seinfo label of 'default' is used. The security label is attached to
399      * the ApplicationInfo instance of the package.
400      *
401      * @param packageState  {@link PackageState} object representing the package to be labeled.
402      * @param pkg           {@link AndroidPackage} object representing the package to be labeled.
403      * @param sharedUser    if the app shares a sharedUserId, then this has the shared setting.
404      * @param compatibility the PlatformCompat service to ask about state of compat changes.
405      * @return String representing the resulting seinfo.
406      */
getSeInfo(@onNull PackageState packageState, @NonNull AndroidPackage pkg, @Nullable SharedUserApi sharedUser, @NonNull PlatformCompat compatibility)407     public static String getSeInfo(@NonNull PackageState packageState, @NonNull AndroidPackage pkg,
408             @Nullable SharedUserApi sharedUser, @NonNull PlatformCompat compatibility) {
409         final int targetSdkVersion = getTargetSdkVersionForSeInfo(pkg, sharedUser,
410                 compatibility);
411         // TODO(b/71593002): isPrivileged for sharedUser and appInfo should never be out of sync.
412         // They currently can be if the sharedUser apps are signed with the platform key.
413         final boolean isPrivileged =
414                 (sharedUser != null) ? sharedUser.isPrivileged() | packageState.isPrivileged()
415                         : packageState.isPrivileged();
416         return getSeInfo(packageState, pkg, isPrivileged, targetSdkVersion);
417     }
418 
419     /**
420      * Selects a security label to a package based on input parameters and the seinfo tag taken
421      * from a matched policy. All signature based policy stanzas are consulted and, if no match
422      * is found, the default seinfo label of 'default' is used. The security label is attached to
423      * the ApplicationInfo instance of the package.
424      *
425      * @param packageState     {@link PackageState} object representing the package to be labeled.
426      * @param pkg              {@link AndroidPackage} object representing the package to be labeled.
427      * @param isPrivileged     boolean.
428      * @param targetSdkVersion int. If this pkg runs as a sharedUser, targetSdkVersion is the
429      *        greater of: lowest targetSdk for all pkgs in the sharedUser, or
430      *        MINIMUM_TARGETSDKVERSION.
431      * @return String representing the resulting seinfo.
432      */
getSeInfo(PackageState packageState, AndroidPackage pkg, boolean isPrivileged, int targetSdkVersion)433     public static String getSeInfo(PackageState packageState, AndroidPackage pkg,
434             boolean isPrivileged, int targetSdkVersion) {
435         String seInfo = null;
436         synchronized (sPolicies) {
437             if (!sPolicyRead) {
438                 if (DEBUG_POLICY) {
439                     Slog.d(TAG, "Policy not read");
440                 }
441             } else {
442                 for (Policy policy : sPolicies) {
443                     seInfo = policy.getMatchedSeInfo(pkg);
444                     if (seInfo != null) {
445                         break;
446                     }
447                 }
448             }
449         }
450 
451         if (seInfo == null) {
452             seInfo = DEFAULT_SEINFO;
453         }
454 
455         if (isPrivileged) {
456             seInfo += PRIVILEGED_APP_STR;
457         }
458 
459         seInfo += TARGETSDKVERSION_STR + targetSdkVersion;
460 
461         String partition = getPartition(packageState);
462         if (!partition.isEmpty()) {
463             seInfo += PARTITION_STR + partition;
464         }
465 
466         if (DEBUG_POLICY_INSTALL) {
467             Slog.i(TAG, "package (" + packageState.getPackageName() + ") labeled with "
468                     + "seinfo=" + seInfo);
469         }
470         return seInfo;
471     }
472 }
473 
474 /**
475  * Holds valid policy representations of individual stanzas from a mac_permissions.xml
476  * file. Each instance can further be used to assign seinfo values to apks using the
477  * {@link Policy#getMatchedSeInfo(AndroidPackage)} method. To create an instance of this use the
478  * {@link PolicyBuilder} pattern class, where each instance is validated against a set
479  * of invariants before being built and returned. Each instance can be guaranteed to
480  * hold one valid policy stanza as outlined in the system/sepolicy/mac_permissions.xml
481  * file.
482  * <p>
483  * The following is an example of how to use {@link Policy.PolicyBuilder} to create a
484  * signer based Policy instance with only inner package name refinements.
485  * </p>
486  * <pre>
487  * {@code
488  * Policy policy = new Policy.PolicyBuilder()
489  *         .addSignature("308204a8...")
490  *         .addSignature("483538c8...")
491  *         .addInnerPackageMapOrThrow("com.foo.", "bar")
492  *         .addInnerPackageMapOrThrow("com.foo.other", "bar")
493  *         .build();
494  * }
495  * </pre>
496  * <p>
497  * The following is an example of how to use {@link Policy.PolicyBuilder} to create a
498  * signer based Policy instance with only a global seinfo tag.
499  * </p>
500  * <pre>
501  * {@code
502  * Policy policy = new Policy.PolicyBuilder()
503  *         .addSignature("308204a8...")
504  *         .addSignature("483538c8...")
505  *         .setGlobalSeinfoOrThrow("paltform")
506  *         .build();
507  * }
508  * </pre>
509  */
510 final class Policy {
511 
512     private final String mSeinfo;
513     private final Set<Signature> mCerts;
514     private final Map<String, String> mPkgMap;
515 
516     // Use the PolicyBuilder pattern to instantiate
Policy(PolicyBuilder builder)517     private Policy(PolicyBuilder builder) {
518         mSeinfo = builder.mSeinfo;
519         mCerts = Collections.unmodifiableSet(builder.mCerts);
520         mPkgMap = Collections.unmodifiableMap(builder.mPkgMap);
521     }
522 
523     /**
524      * Return all the certs stored with this policy stanza.
525      *
526      * @return A set of Signature objects representing all the certs stored
527      *         with the policy.
528      */
getSignatures()529     public Set<Signature> getSignatures() {
530         return mCerts;
531     }
532 
533     /**
534      * Return whether this policy object contains package name mapping refinements.
535      *
536      * @return A boolean indicating if this object has inner package name mappings.
537      */
hasInnerPackages()538     public boolean hasInnerPackages() {
539         return !mPkgMap.isEmpty();
540     }
541 
542     /**
543      * Return the mapping of all package name refinements.
544      *
545      * @return A Map object whose keys are the package names and whose values are
546      *         the seinfo assignments.
547      */
getInnerPackages()548     public Map<String, String> getInnerPackages() {
549         return mPkgMap;
550     }
551 
552     /**
553      * Return whether the policy object has a global seinfo tag attached.
554      *
555      * @return A boolean indicating if this stanza has a global seinfo tag.
556      */
hasGlobalSeinfo()557     public boolean hasGlobalSeinfo() {
558         return mSeinfo != null;
559     }
560 
561     @Override
toString()562     public String toString() {
563         StringBuilder sb = new StringBuilder();
564         for (Signature cert : mCerts) {
565             sb.append("cert=" + cert.toCharsString().substring(0, 11) + "... ");
566         }
567 
568         if (mSeinfo != null) {
569             sb.append("seinfo=" + mSeinfo);
570         }
571 
572         for (String name : mPkgMap.keySet()) {
573             sb.append(" " + name + "=" + mPkgMap.get(name));
574         }
575 
576         return sb.toString();
577     }
578 
579     /**
580      * <p>
581      * Determine the seinfo value to assign to an apk. The appropriate seinfo value
582      * is determined using the following steps:
583      * </p>
584      * <ul>
585      *   <li> All certs used to sign the apk and all certs stored with this policy
586      *     instance are tested for set equality. If this fails then null is returned.
587      *   </li>
588      *   <li> If all certs match then an appropriate inner package stanza is
589      *     searched based on package name alone. If matched, the stored seinfo
590      *     value for that mapping is returned.
591      *   </li>
592      *   <li> If all certs matched and no inner package stanza matches then return
593      *     the global seinfo value. The returned value can be null in this case.
594      *   </li>
595      * </ul>
596      * <p>
597      * In all cases, a return value of null should be interpreted as the apk failing
598      * to match this Policy instance; i.e. failing this policy stanza.
599      * </p>
600      * @param pkg the apk to check given as a AndroidPackage object
601      * @return A string representing the seinfo matched during policy lookup.
602      *         A value of null can also be returned if no match occured.
603      */
getMatchedSeInfo(AndroidPackage pkg)604     public String getMatchedSeInfo(AndroidPackage pkg) {
605         // Check for exact signature matches across all certs.
606         Signature[] certs = mCerts.toArray(new Signature[0]);
607         if (pkg.getSigningDetails() != SigningDetails.UNKNOWN
608                 && !Signature.areExactMatch(pkg.getSigningDetails(), certs)) {
609 
610             // certs aren't exact match, but the package may have rotated from the known system cert
611             if (certs.length > 1 || !pkg.getSigningDetails().hasCertificate(certs[0])) {
612                 return null;
613             }
614         }
615 
616         // Check for inner package name matches given that the
617         // signature checks already passed.
618         String seinfoValue = mPkgMap.get(pkg.getPackageName());
619         if (seinfoValue != null) {
620             return seinfoValue;
621         }
622 
623         // Return the global seinfo value.
624         return mSeinfo;
625     }
626 
627     /**
628      * A nested builder class to create {@link Policy} instances. A {@link Policy}
629      * class instance represents one valid policy stanza found in a mac_permissions.xml
630      * file. A valid policy stanza is defined to be a signer stanza which obeys the rules
631      * outlined in system/sepolicy/mac_permissions.xml. The {@link #build} method
632      * ensures a set of invariants are upheld enforcing the correct stanza structure
633      * before returning a valid Policy object.
634      */
635     public static final class PolicyBuilder {
636 
637         private String mSeinfo;
638         private final Set<Signature> mCerts;
639         private final Map<String, String> mPkgMap;
640 
PolicyBuilder()641         public PolicyBuilder() {
642             mCerts = new HashSet<Signature>(2);
643             mPkgMap = new HashMap<String, String>(2);
644         }
645 
646         /**
647          * Adds a signature to the set of certs used for validation checks. The purpose
648          * being that all contained certs will need to be matched against all certs
649          * contained with an apk.
650          *
651          * @param cert the signature to add given as a String.
652          * @return The reference to this PolicyBuilder.
653          * @throws IllegalArgumentException if the cert value fails validation;
654          *         null or is an invalid hex-encoded ASCII string.
655          */
addSignature(String cert)656         public PolicyBuilder addSignature(String cert) {
657             if (cert == null) {
658                 String err = "Invalid signature value " + cert;
659                 throw new IllegalArgumentException(err);
660             }
661 
662             mCerts.add(new Signature(cert));
663             return this;
664         }
665 
666         /**
667          * Set the global seinfo tag for this policy stanza. The global seinfo tag
668          * when attached to a signer tag represents the assignment when there isn't a
669          * further inner package refinement in policy.
670          *
671          * @param seinfo the seinfo value given as a String.
672          * @return The reference to this PolicyBuilder.
673          * @throws IllegalArgumentException if the seinfo value fails validation;
674          *         null, zero length or contains non-valid characters [^a-zA-Z_\._0-9].
675          * @throws IllegalStateException if an seinfo value has already been found
676          */
setGlobalSeinfoOrThrow(String seinfo)677         public PolicyBuilder setGlobalSeinfoOrThrow(String seinfo) {
678             if (!validateValue(seinfo)) {
679                 String err = "Invalid seinfo value " + seinfo;
680                 throw new IllegalArgumentException(err);
681             }
682 
683             if (mSeinfo != null && !mSeinfo.equals(seinfo)) {
684                 String err = "Duplicate seinfo tag found";
685                 throw new IllegalStateException(err);
686             }
687 
688             mSeinfo = seinfo;
689             return this;
690         }
691 
692         /**
693          * Create a package name to seinfo value mapping. Each mapping represents
694          * the seinfo value that will be assigned to the described package name.
695          * These localized mappings allow the global seinfo to be overriden.
696          *
697          * @param pkgName the android package name given to the app
698          * @param seinfo the seinfo value that will be assigned to the passed pkgName
699          * @return The reference to this PolicyBuilder.
700          * @throws IllegalArgumentException if the seinfo value fails validation;
701          *         null, zero length or contains non-valid characters [^a-zA-Z_\.0-9].
702          *         Or, if the package name isn't a valid android package name.
703          * @throws IllegalStateException if trying to reset a package mapping with a
704          *         different seinfo value.
705          */
addInnerPackageMapOrThrow(String pkgName, String seinfo)706         public PolicyBuilder addInnerPackageMapOrThrow(String pkgName, String seinfo) {
707             if (!validateValue(pkgName)) {
708                 String err = "Invalid package name " + pkgName;
709                 throw new IllegalArgumentException(err);
710             }
711             if (!validateValue(seinfo)) {
712                 String err = "Invalid seinfo value " + seinfo;
713                 throw new IllegalArgumentException(err);
714             }
715 
716             String pkgValue = mPkgMap.get(pkgName);
717             if (pkgValue != null && !pkgValue.equals(seinfo)) {
718                 String err = "Conflicting seinfo value found";
719                 throw new IllegalStateException(err);
720             }
721 
722             mPkgMap.put(pkgName, seinfo);
723             return this;
724         }
725 
726         /**
727          * General validation routine for the attribute strings of an element. Checks
728          * if the string is non-null, positive length and only contains [a-zA-Z_\.0-9].
729          *
730          * @param name the string to validate.
731          * @return boolean indicating if the string was valid.
732          */
validateValue(String name)733         private boolean validateValue(String name) {
734             if (name == null)
735                 return false;
736 
737             // Want to match on [0-9a-zA-Z_.]
738             if (!name.matches("\\A[\\.\\w]+\\z")) {
739                 return false;
740             }
741 
742             return true;
743         }
744 
745         /**
746          * <p>
747          * Create a {@link Policy} instance based on the current configuration. This
748          * method checks for certain policy invariants used to enforce certain guarantees
749          * about the expected structure of a policy stanza.
750          * Those invariants are:
751          * </p>
752          * <ul>
753          *   <li> at least one cert must be found </li>
754          *   <li> either a global seinfo value is present OR at least one
755          *     inner package mapping must be present BUT not both. </li>
756          * </ul>
757          * @return an instance of {@link Policy} with the options set from this builder
758          * @throws IllegalStateException if an invariant is violated.
759          */
build()760         public Policy build() {
761             Policy p = new Policy(this);
762 
763             if (p.mCerts.isEmpty()) {
764                 String err = "Missing certs with signer tag. Expecting at least one.";
765                 throw new IllegalStateException(err);
766             }
767             if (!(p.mSeinfo == null ^ p.mPkgMap.isEmpty())) {
768                 String err = "Only seinfo tag XOR package tags are allowed within " +
769                         "a signer stanza.";
770                 throw new IllegalStateException(err);
771             }
772 
773             return p;
774         }
775     }
776 }
777 
778 /**
779  * Comparision imposing an ordering on Policy objects. It is understood that Policy
780  * objects can only take one of three forms and ordered according to the following
781  * set of rules most specific to least.
782  * <ul>
783  *   <li> signer stanzas with inner package mappings </li>
784  *   <li> signer stanzas with global seinfo tags </li>
785  * </ul>
786  * This comparison also checks for duplicate entries on the input selectors. Any
787  * found duplicates will be flagged and can be checked with {@link #foundDuplicate}.
788  */
789 
790 final class PolicyComparator implements Comparator<Policy> {
791 
792     private boolean duplicateFound = false;
793 
foundDuplicate()794     public boolean foundDuplicate() {
795         return duplicateFound;
796     }
797 
798     @Override
compare(Policy p1, Policy p2)799     public int compare(Policy p1, Policy p2) {
800 
801         // Give precedence to stanzas with inner package mappings
802         if (p1.hasInnerPackages() != p2.hasInnerPackages()) {
803             return p1.hasInnerPackages() ? -1 : 1;
804         }
805 
806         // Check for duplicate entries
807         if (p1.getSignatures().equals(p2.getSignatures())) {
808             // Checks if signer w/o inner package names
809             if (p1.hasGlobalSeinfo()) {
810                 duplicateFound = true;
811                 Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString());
812             }
813 
814             // Look for common inner package name mappings
815             final Map<String, String> p1Packages = p1.getInnerPackages();
816             final Map<String, String> p2Packages = p2.getInnerPackages();
817             if (!Collections.disjoint(p1Packages.keySet(), p2Packages.keySet())) {
818                 duplicateFound = true;
819                 Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString());
820             }
821         }
822 
823         return 0;
824     }
825 }
826