• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.content.pm.PackageParser;
20 import android.content.pm.Signature;
21 import android.os.Environment;
22 import android.util.Slog;
23 import android.util.Xml;
24 
25 import libcore.io.IoUtils;
26 
27 import java.io.File;
28 import java.io.FileNotFoundException;
29 import java.io.FileOutputStream;
30 import java.io.FileReader;
31 import java.io.IOException;
32 import java.security.MessageDigest;
33 import java.security.NoSuchAlgorithmException;
34 
35 import java.util.ArrayList;
36 import java.util.Collections;
37 import java.util.Comparator;
38 import java.util.HashMap;
39 import java.util.HashSet;
40 import java.util.List;
41 import java.util.Map;
42 import java.util.Set;
43 
44 import org.xmlpull.v1.XmlPullParser;
45 import org.xmlpull.v1.XmlPullParserException;
46 
47 /**
48  * Centralized access to SELinux MMAC (middleware MAC) implementation. This
49  * class is responsible for loading the appropriate mac_permissions.xml file
50  * as well as providing an interface for assigning seinfo values to apks.
51  *
52  * {@hide}
53  */
54 public final class SELinuxMMAC {
55 
56     static final String TAG = "SELinuxMMAC";
57 
58     private static final boolean DEBUG_POLICY = false;
59     private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false;
60     private static final boolean DEBUG_POLICY_ORDER = DEBUG_POLICY || false;
61 
62     // All policy stanzas read from mac_permissions.xml. This is also the lock
63     // to synchronize access during policy load and access attempts.
64     private static List<Policy> sPolicies = new ArrayList<>();
65 
66     // Data policy override version file.
67     private static final String DATA_VERSION_FILE =
68             Environment.getDataDirectory() + "/security/current/selinux_version";
69 
70     // Base policy version file.
71     private static final String BASE_VERSION_FILE = "/selinux_version";
72 
73     // Whether override security policies should be loaded.
74     private static final boolean USE_OVERRIDE_POLICY = useOverridePolicy();
75 
76     // Data override mac_permissions.xml policy file.
77     private static final String DATA_MAC_PERMISSIONS =
78             Environment.getDataDirectory() + "/security/current/mac_permissions.xml";
79 
80     // Base mac_permissions.xml policy file.
81     private static final String BASE_MAC_PERMISSIONS =
82             Environment.getRootDirectory() + "/etc/security/mac_permissions.xml";
83 
84     // Determine which mac_permissions.xml file to use.
85     private static final String MAC_PERMISSIONS = USE_OVERRIDE_POLICY ?
86             DATA_MAC_PERMISSIONS : BASE_MAC_PERMISSIONS;
87 
88     // Data override seapp_contexts policy file.
89     private static final String DATA_SEAPP_CONTEXTS =
90             Environment.getDataDirectory() + "/security/current/seapp_contexts";
91 
92     // Base seapp_contexts policy file.
93     private static final String BASE_SEAPP_CONTEXTS = "/seapp_contexts";
94 
95     // Determine which seapp_contexts file to use.
96     private static final String SEAPP_CONTEXTS = USE_OVERRIDE_POLICY ?
97             DATA_SEAPP_CONTEXTS : BASE_SEAPP_CONTEXTS;
98 
99     // Stores the hash of the last used seapp_contexts file.
100     private static final String SEAPP_HASH_FILE =
101             Environment.getDataDirectory().toString() + "/system/seapp_hash";
102 
103     /**
104      * Load the mac_permissions.xml file containing all seinfo assignments used to
105      * label apps. The loaded mac_permissions.xml file is determined by the
106      * MAC_PERMISSIONS class variable which is set at class load time which itself
107      * is based on the USE_OVERRIDE_POLICY class variable. For further guidance on
108      * the proper structure of a mac_permissions.xml file consult the source code
109      * located at external/sepolicy/mac_permissions.xml.
110      *
111      * @return boolean indicating if policy was correctly loaded. A value of false
112      *         typically indicates a structural problem with the xml or incorrectly
113      *         constructed policy stanzas. A value of true means that all stanzas
114      *         were loaded successfully; no partial loading is possible.
115      */
readInstallPolicy()116     public static boolean readInstallPolicy() {
117         // Temp structure to hold the rules while we parse the xml file
118         List<Policy> policies = new ArrayList<>();
119 
120         FileReader policyFile = null;
121         XmlPullParser parser = Xml.newPullParser();
122         try {
123             policyFile = new FileReader(MAC_PERMISSIONS);
124             Slog.d(TAG, "Using policy file " + MAC_PERMISSIONS);
125 
126             parser.setInput(policyFile);
127             parser.nextTag();
128             parser.require(XmlPullParser.START_TAG, null, "policy");
129 
130             while (parser.next() != XmlPullParser.END_TAG) {
131                 if (parser.getEventType() != XmlPullParser.START_TAG) {
132                     continue;
133                 }
134 
135                 switch (parser.getName()) {
136                     case "signer":
137                         policies.add(readSignerOrThrow(parser));
138                         break;
139                     case "default":
140                         policies.add(readDefaultOrThrow(parser));
141                         break;
142                     default:
143                         skip(parser);
144                 }
145             }
146         } catch (IllegalStateException | IllegalArgumentException |
147                 XmlPullParserException ex) {
148             StringBuilder sb = new StringBuilder("Exception @");
149             sb.append(parser.getPositionDescription());
150             sb.append(" while parsing ");
151             sb.append(MAC_PERMISSIONS);
152             sb.append(":");
153             sb.append(ex);
154             Slog.w(TAG, sb.toString());
155             return false;
156         } catch (IOException ioe) {
157             Slog.w(TAG, "Exception parsing " + MAC_PERMISSIONS, ioe);
158             return false;
159         } finally {
160             IoUtils.closeQuietly(policyFile);
161         }
162 
163         // Now sort the policy stanzas
164         PolicyComparator policySort = new PolicyComparator();
165         Collections.sort(policies, policySort);
166         if (policySort.foundDuplicate()) {
167             Slog.w(TAG, "ERROR! Duplicate entries found parsing " + MAC_PERMISSIONS);
168             return false;
169         }
170 
171         synchronized (sPolicies) {
172             sPolicies = policies;
173 
174             if (DEBUG_POLICY_ORDER) {
175                 for (Policy policy : sPolicies) {
176                     Slog.d(TAG, "Policy: " + policy.toString());
177                 }
178             }
179         }
180 
181         return true;
182     }
183 
184     /**
185      * Loop over a signer tag looking for seinfo, package and cert tags. A {@link Policy}
186      * instance will be created and returned in the process. During the pass all other
187      * tag elements will be skipped.
188      *
189      * @param parser an XmlPullParser object representing a signer element.
190      * @return the constructed {@link Policy} instance
191      * @throws IOException
192      * @throws XmlPullParserException
193      * @throws IllegalArgumentException if any of the validation checks fail while
194      *         parsing tag values.
195      * @throws IllegalStateException if any of the invariants fail when constructing
196      *         the {@link Policy} instance.
197      */
readSignerOrThrow(XmlPullParser parser)198     private static Policy readSignerOrThrow(XmlPullParser parser) throws IOException,
199             XmlPullParserException {
200 
201         parser.require(XmlPullParser.START_TAG, null, "signer");
202         Policy.PolicyBuilder pb = new Policy.PolicyBuilder();
203 
204         // Check for a cert attached to the signer tag. We allow a signature
205         // to appear as an attribute as well as those attached to cert tags.
206         String cert = parser.getAttributeValue(null, "signature");
207         if (cert != null) {
208             pb.addSignature(cert);
209         }
210 
211         while (parser.next() != XmlPullParser.END_TAG) {
212             if (parser.getEventType() != XmlPullParser.START_TAG) {
213                 continue;
214             }
215 
216             String tagName = parser.getName();
217             if ("seinfo".equals(tagName)) {
218                 String seinfo = parser.getAttributeValue(null, "value");
219                 pb.setGlobalSeinfoOrThrow(seinfo);
220                 readSeinfo(parser);
221             } else if ("package".equals(tagName)) {
222                 readPackageOrThrow(parser, pb);
223             } else if ("cert".equals(tagName)) {
224                 String sig = parser.getAttributeValue(null, "signature");
225                 pb.addSignature(sig);
226                 readCert(parser);
227             } else {
228                 skip(parser);
229             }
230         }
231 
232         return pb.build();
233     }
234 
235     /**
236      * Loop over a default element looking for seinfo child tags. A {@link Policy}
237      * instance will be created and returned in the process. All other tags encountered
238      * will be skipped.
239      *
240      * @param parser an XmlPullParser object representing a default element.
241      * @return the constructed {@link Policy} instance
242      * @throws IOException
243      * @throws XmlPullParserException
244      * @throws IllegalArgumentException if any of the validation checks fail while
245      *         parsing tag values.
246      * @throws IllegalStateException if any of the invariants fail when constructing
247      *         the {@link Policy} instance.
248      */
readDefaultOrThrow(XmlPullParser parser)249     private static Policy readDefaultOrThrow(XmlPullParser parser) throws IOException,
250             XmlPullParserException {
251 
252         parser.require(XmlPullParser.START_TAG, null, "default");
253         Policy.PolicyBuilder pb = new Policy.PolicyBuilder();
254         pb.setAsDefaultPolicy();
255 
256         while (parser.next() != XmlPullParser.END_TAG) {
257             if (parser.getEventType() != XmlPullParser.START_TAG) {
258                 continue;
259             }
260 
261             String tagName = parser.getName();
262             if ("seinfo".equals(tagName)) {
263                 String seinfo = parser.getAttributeValue(null, "value");
264                 pb.setGlobalSeinfoOrThrow(seinfo);
265                 readSeinfo(parser);
266             } else {
267                 skip(parser);
268             }
269         }
270 
271         return pb.build();
272     }
273 
274     /**
275      * Loop over a package element looking for seinfo child tags. If found return the
276      * value attribute of the seinfo tag, otherwise return null. All other tags encountered
277      * will be skipped.
278      *
279      * @param parser an XmlPullParser object representing a package element.
280      * @param pb a Policy.PolicyBuilder instance to build
281      * @throws IOException
282      * @throws XmlPullParserException
283      * @throws IllegalArgumentException if any of the validation checks fail while
284      *         parsing tag values.
285      * @throws IllegalStateException if there is a duplicate seinfo tag for the current
286      *         package tag.
287      */
readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb)288     private static void readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb) throws
289             IOException, XmlPullParserException {
290         parser.require(XmlPullParser.START_TAG, null, "package");
291         String pkgName = parser.getAttributeValue(null, "name");
292 
293         while (parser.next() != XmlPullParser.END_TAG) {
294             if (parser.getEventType() != XmlPullParser.START_TAG) {
295                 continue;
296             }
297 
298             String tagName = parser.getName();
299             if ("seinfo".equals(tagName)) {
300                 String seinfo = parser.getAttributeValue(null, "value");
301                 pb.addInnerPackageMapOrThrow(pkgName, seinfo);
302                 readSeinfo(parser);
303             } else {
304                 skip(parser);
305             }
306         }
307     }
308 
readCert(XmlPullParser parser)309     private static void readCert(XmlPullParser parser) throws IOException,
310             XmlPullParserException {
311         parser.require(XmlPullParser.START_TAG, null, "cert");
312         parser.nextTag();
313     }
314 
readSeinfo(XmlPullParser parser)315     private static void readSeinfo(XmlPullParser parser) throws IOException,
316             XmlPullParserException {
317         parser.require(XmlPullParser.START_TAG, null, "seinfo");
318         parser.nextTag();
319     }
320 
skip(XmlPullParser p)321     private static void skip(XmlPullParser p) throws IOException, XmlPullParserException {
322         if (p.getEventType() != XmlPullParser.START_TAG) {
323             throw new IllegalStateException();
324         }
325         int depth = 1;
326         while (depth != 0) {
327             switch (p.next()) {
328             case XmlPullParser.END_TAG:
329                 depth--;
330                 break;
331             case XmlPullParser.START_TAG:
332                 depth++;
333                 break;
334             }
335         }
336     }
337 
338     /**
339      * Applies a security label to a package based on an seinfo tag taken from a matched
340      * policy. All signature based policy stanzas are consulted first and, if no match
341      * is found, the default policy stanza is then consulted. The security label is
342      * attached to the ApplicationInfo instance of the package in the event that a matching
343      * policy was found.
344      *
345      * @param pkg object representing the package to be labeled.
346      * @return boolean which determines whether a non null seinfo label was assigned
347      *         to the package. A null value simply represents that no policy matched.
348      */
assignSeinfoValue(PackageParser.Package pkg)349     public static boolean assignSeinfoValue(PackageParser.Package pkg) {
350         synchronized (sPolicies) {
351             for (Policy policy : sPolicies) {
352                 String seinfo = policy.getMatchedSeinfo(pkg);
353                 if (seinfo != null) {
354                     pkg.applicationInfo.seinfo = seinfo;
355                     if (DEBUG_POLICY_INSTALL) {
356                         Slog.i(TAG, "package (" + pkg.packageName + ") labeled with " +
357                                "seinfo=" + seinfo);
358                     }
359                     return true;
360                 }
361             }
362         }
363 
364         if (DEBUG_POLICY_INSTALL) {
365             Slog.i(TAG, "package (" + pkg.packageName + ") doesn't match any policy; " +
366                    "seinfo will remain null");
367         }
368         return false;
369     }
370 
371     /**
372      * Determines if a recursive restorecon on /data/data and /data/user is needed.
373      * It does this by comparing the SHA-1 of the seapp_contexts file against the
374      * stored hash at /data/system/seapp_hash.
375      *
376      * @return Returns true if the restorecon should occur or false otherwise.
377      */
shouldRestorecon()378     public static boolean shouldRestorecon() {
379         // Any error with the seapp_contexts file should be fatal
380         byte[] currentHash = null;
381         try {
382             currentHash = returnHash(SEAPP_CONTEXTS);
383         } catch (IOException ioe) {
384             Slog.e(TAG, "Error with hashing seapp_contexts.", ioe);
385             return false;
386         }
387 
388         // Push past any error with the stored hash file
389         byte[] storedHash = null;
390         try {
391             storedHash = IoUtils.readFileAsByteArray(SEAPP_HASH_FILE);
392         } catch (IOException ioe) {
393             Slog.w(TAG, "Error opening " + SEAPP_HASH_FILE + ". Assuming first boot.");
394         }
395 
396         return (storedHash == null || !MessageDigest.isEqual(storedHash, currentHash));
397     }
398 
399     /**
400      * Stores the SHA-1 of the seapp_contexts to /data/system/seapp_hash.
401      */
setRestoreconDone()402     public static void setRestoreconDone() {
403         try {
404             final byte[] currentHash = returnHash(SEAPP_CONTEXTS);
405             dumpHash(new File(SEAPP_HASH_FILE), currentHash);
406         } catch (IOException ioe) {
407             Slog.e(TAG, "Error with saving hash to " + SEAPP_HASH_FILE, ioe);
408         }
409     }
410 
411     /**
412      * Dump the contents of a byte array to a specified file.
413      *
414      * @param file The file that receives the byte array content.
415      * @param content A byte array that will be written to the specified file.
416      * @throws IOException if any failed I/O operation occured.
417      *         Included is the failure to atomically rename the tmp
418      *         file used in the process.
419      */
dumpHash(File file, byte[] content)420     private static void dumpHash(File file, byte[] content) throws IOException {
421         FileOutputStream fos = null;
422         File tmp = null;
423         try {
424             tmp = File.createTempFile("seapp_hash", ".journal", file.getParentFile());
425             tmp.setReadable(true);
426             fos = new FileOutputStream(tmp);
427             fos.write(content);
428             fos.getFD().sync();
429             if (!tmp.renameTo(file)) {
430                 throw new IOException("Failure renaming " + file.getCanonicalPath());
431             }
432         } finally {
433             if (tmp != null) {
434                 tmp.delete();
435             }
436             IoUtils.closeQuietly(fos);
437         }
438     }
439 
440     /**
441      * Return the SHA-1 of a file.
442      *
443      * @param file The path to the file given as a string.
444      * @return Returns the SHA-1 of the file as a byte array.
445      * @throws IOException if any failed I/O operations occured.
446      */
returnHash(String file)447     private static byte[] returnHash(String file) throws IOException {
448         try {
449             final byte[] contents = IoUtils.readFileAsByteArray(file);
450             return MessageDigest.getInstance("SHA-1").digest(contents);
451         } catch (NoSuchAlgorithmException nsae) {
452             throw new RuntimeException(nsae);  // impossible
453         }
454     }
455 
useOverridePolicy()456     private static boolean useOverridePolicy() {
457         try {
458             final String overrideVersion = IoUtils.readFileAsString(DATA_VERSION_FILE);
459             final String baseVersion = IoUtils.readFileAsString(BASE_VERSION_FILE);
460             if (overrideVersion.equals(baseVersion)) {
461                 return true;
462             }
463             Slog.e(TAG, "Override policy version '" + overrideVersion + "' doesn't match " +
464                    "base version '" + baseVersion + "'. Skipping override policy files.");
465         } catch (FileNotFoundException fnfe) {
466             // Override version file doesn't have to exist so silently ignore.
467         } catch (IOException ioe) {
468             Slog.w(TAG, "Skipping override policy files.", ioe);
469         }
470         return false;
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} 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 external/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  * <p>
510  * The following is an example of how to use {@link Policy.PolicyBuilder} to create a
511  * default based Policy instance.
512  * </p>
513  * <pre>
514  * {@code
515  * Policy policy = new Policy.PolicyBuilder()
516  *         .setAsDefaultPolicy()
517  *         .setGlobalSeinfoOrThrow("default")
518  *         .build();
519  * }
520  * </pre>
521  */
522 final class Policy {
523 
524     private final String mSeinfo;
525     private final boolean mDefaultStanza;
526     private final Set<Signature> mCerts;
527     private final Map<String, String> mPkgMap;
528 
529     // Use the PolicyBuilder pattern to instantiate
Policy(PolicyBuilder builder)530     private Policy(PolicyBuilder builder) {
531         mSeinfo = builder.mSeinfo;
532         mDefaultStanza = builder.mDefaultStanza;
533         mCerts = Collections.unmodifiableSet(builder.mCerts);
534         mPkgMap = Collections.unmodifiableMap(builder.mPkgMap);
535     }
536 
537     /**
538      * Return all the certs stored with this policy stanza.
539      *
540      * @return A set of Signature objects representing all the certs stored
541      *         with the policy.
542      */
getSignatures()543     public Set<Signature> getSignatures() {
544         return mCerts;
545     }
546 
547     /**
548      * Return whether this policy object represents a default stanza.
549      *
550      * @return A boolean indicating if this object represents a default policy stanza.
551      */
isDefaultStanza()552     public boolean isDefaultStanza() {
553         return mDefaultStanza;
554     }
555 
556     /**
557      * Return whether this policy object contains package name mapping refinements.
558      *
559      * @return A boolean indicating if this object has inner package name mappings.
560      */
hasInnerPackages()561     public boolean hasInnerPackages() {
562         return !mPkgMap.isEmpty();
563     }
564 
565     /**
566      * Return the mapping of all package name refinements.
567      *
568      * @return A Map object whose keys are the package names and whose values are
569      *         the seinfo assignments.
570      */
getInnerPackages()571     public Map<String, String> getInnerPackages() {
572         return mPkgMap;
573     }
574 
575     /**
576      * Return whether the policy object has a global seinfo tag attached.
577      *
578      * @return A boolean indicating if this stanza has a global seinfo tag.
579      */
hasGlobalSeinfo()580     public boolean hasGlobalSeinfo() {
581         return mSeinfo != null;
582     }
583 
584     @Override
toString()585     public String toString() {
586         StringBuilder sb = new StringBuilder();
587         if (mDefaultStanza) {
588             sb.append("defaultStanza=true ");
589         }
590 
591         for (Signature cert : mCerts) {
592             sb.append("cert=" + cert.toCharsString().substring(0, 11) + "... ");
593         }
594 
595         if (mSeinfo != null) {
596             sb.append("seinfo=" + mSeinfo);
597         }
598 
599         for (String name : mPkgMap.keySet()) {
600             sb.append(" " + name + "=" + mPkgMap.get(name));
601         }
602 
603         return sb.toString();
604     }
605 
606     /**
607      * <p>
608      * Determine the seinfo value to assign to an apk. The appropriate seinfo value
609      * is determined using the following steps:
610      * </p>
611      * <ul>
612      *   <li> If this Policy instance is defined as a default stanza:
613      *       <ul><li>Return the global seinfo value</li></ul>
614      *   </li>
615      *   <li> If this Policy instance is defined as a signer stanza:
616      *     <ul>
617      *       <li> All certs used to sign the apk and all certs stored with this policy
618      *         instance are tested for set equality. If this fails then null is returned.
619      *       </li>
620      *       <li> If all certs match then an appropriate inner package stanza is
621      *         searched based on package name alone. If matched, the stored seinfo
622      *         value for that mapping is returned.
623      *       </li>
624      *       <li> If all certs matched and no inner package stanza matches then return
625      *         the global seinfo value. The returned value can be null in this case.
626      *       </li>
627      *     </ul>
628      *   </li>
629      * </ul>
630      * <p>
631      * In all cases, a return value of null should be interpreted as the apk failing
632      * to match this Policy instance; i.e. failing this policy stanza.
633      * </p>
634      * @param pkg the apk to check given as a PackageParser.Package object
635      * @return A string representing the seinfo matched during policy lookup.
636      *         A value of null can also be returned if no match occured.
637      */
getMatchedSeinfo(PackageParser.Package pkg)638     public String getMatchedSeinfo(PackageParser.Package pkg) {
639         if (!mDefaultStanza) {
640             // Check for exact signature matches across all certs.
641             Signature[] certs = mCerts.toArray(new Signature[0]);
642             if (!Signature.areExactMatch(certs, pkg.mSignatures)) {
643                 return null;
644             }
645 
646             // Check for inner package name matches given that the
647             // signature checks already passed.
648             String seinfoValue = mPkgMap.get(pkg.packageName);
649             if (seinfoValue != null) {
650                 return seinfoValue;
651             }
652         }
653 
654         // Return the global seinfo value (even if it's null).
655         return mSeinfo;
656     }
657 
658     /**
659      * A nested builder class to create {@link Policy} instances. A {@link Policy}
660      * class instance represents one valid policy stanza found in a mac_permissions.xml
661      * file. A valid policy stanza is defined to be either a signer or default stanza
662      * which obeys the rules outlined in external/sepolicy/mac_permissions.xml. The
663      * {@link #build} method ensures a set of invariants are upheld enforcing the correct
664      * stanza structure before returning a valid Policy object.
665      */
666     public static final class PolicyBuilder {
667 
668         private String mSeinfo;
669         private boolean mDefaultStanza;
670         private final Set<Signature> mCerts;
671         private final Map<String, String> mPkgMap;
672 
PolicyBuilder()673         public PolicyBuilder() {
674             mCerts = new HashSet<Signature>(2);
675             mPkgMap = new HashMap<String, String>(2);
676         }
677 
678         /**
679          * Sets this stanza as a default stanza. All policy stanzas are assumed to
680          * be signer stanzas unless this method is explicitly called. Default stanzas
681          * are treated differently with respect to allowable child tags, ordering and
682          * when and how policy decisions are enforced.
683          *
684          * @return The reference to this PolicyBuilder.
685          */
setAsDefaultPolicy()686         public PolicyBuilder setAsDefaultPolicy() {
687             mDefaultStanza = true;
688             return this;
689         }
690 
691         /**
692          * Adds a signature to the set of certs used for validation checks. The purpose
693          * being that all contained certs will need to be matched against all certs
694          * contained with an apk.
695          *
696          * @param cert the signature to add given as a String.
697          * @return The reference to this PolicyBuilder.
698          * @throws IllegalArgumentException if the cert value fails validation;
699          *         null or is an invalid hex-encoded ASCII string.
700          */
addSignature(String cert)701         public PolicyBuilder addSignature(String cert) {
702             if (cert == null) {
703                 String err = "Invalid signature value " + cert;
704                 throw new IllegalArgumentException(err);
705             }
706 
707             mCerts.add(new Signature(cert));
708             return this;
709         }
710 
711         /**
712          * Set the global seinfo tag for this policy stanza. The global seinfo tag
713          * represents the seinfo element that is used in one of two ways depending on
714          * its context. When attached to a signer tag the global seinfo represents an
715          * assignment when there isn't a further inner package refinement in policy.
716          * When used with a default tag, it represents the only allowable assignment
717          * value.
718          *
719          * @param seinfo the seinfo value given as a String.
720          * @return The reference to this PolicyBuilder.
721          * @throws IllegalArgumentException if the seinfo value fails validation;
722          *         null, zero length or contains non-valid characters [^a-zA-Z_\._0-9].
723          * @throws IllegalStateException if an seinfo value has already been found
724          */
setGlobalSeinfoOrThrow(String seinfo)725         public PolicyBuilder setGlobalSeinfoOrThrow(String seinfo) {
726             if (!validateValue(seinfo)) {
727                 String err = "Invalid seinfo value " + seinfo;
728                 throw new IllegalArgumentException(err);
729             }
730 
731             if (mSeinfo != null && !mSeinfo.equals(seinfo)) {
732                 String err = "Duplicate seinfo tag found";
733                 throw new IllegalStateException(err);
734             }
735 
736             mSeinfo = seinfo;
737             return this;
738         }
739 
740         /**
741          * Create a package name to seinfo value mapping. Each mapping represents
742          * the seinfo value that will be assigned to the described package name.
743          * These localized mappings allow the global seinfo to be overriden. This
744          * mapping provides no value when used in conjunction with a default stanza;
745          * enforced through the {@link #build} method.
746          *
747          * @param pkgName the android package name given to the app
748          * @param seinfo the seinfo value that will be assigned to the passed pkgName
749          * @return The reference to this PolicyBuilder.
750          * @throws IllegalArgumentException if the seinfo value fails validation;
751          *         null, zero length or contains non-valid characters [^a-zA-Z_\.0-9].
752          *         Or, if the package name isn't a valid android package name.
753          * @throws IllegalStateException if trying to reset a package mapping with a
754          *         different seinfo value.
755          */
addInnerPackageMapOrThrow(String pkgName, String seinfo)756         public PolicyBuilder addInnerPackageMapOrThrow(String pkgName, String seinfo) {
757             if (!validateValue(pkgName)) {
758                 String err = "Invalid package name " + pkgName;
759                 throw new IllegalArgumentException(err);
760             }
761             if (!validateValue(seinfo)) {
762                 String err = "Invalid seinfo value " + seinfo;
763                 throw new IllegalArgumentException(err);
764             }
765 
766             String pkgValue = mPkgMap.get(pkgName);
767             if (pkgValue != null && !pkgValue.equals(seinfo)) {
768                 String err = "Conflicting seinfo value found";
769                 throw new IllegalStateException(err);
770             }
771 
772             mPkgMap.put(pkgName, seinfo);
773             return this;
774         }
775 
776         /**
777          * General validation routine for the attribute strings of an element. Checks
778          * if the string is non-null, positive length and only contains [a-zA-Z_\.0-9].
779          *
780          * @param name the string to validate.
781          * @return boolean indicating if the string was valid.
782          */
validateValue(String name)783         private boolean validateValue(String name) {
784             if (name == null)
785                 return false;
786 
787             // Want to match on [0-9a-zA-Z_.]
788             if (!name.matches("\\A[\\.\\w]+\\z")) {
789                 return false;
790             }
791 
792             return true;
793         }
794 
795         /**
796          * <p>
797          * Create a {@link Policy} instance based on the current configuration. This
798          * method checks for certain policy invariants used to enforce certain guarantees
799          * about the expected structure of a policy stanza.
800          * Those invariants are:
801          * </p>
802          *    <ul>
803          *      <li> If a default stanza
804          *        <ul>
805          *          <li> an attached global seinfo tag must be present </li>
806          *          <li> no signatures and no package names can be present </li>
807          *        </ul>
808          *      </li>
809          *      <li> If a signer stanza
810          *        <ul>
811          *           <li> at least one cert must be found </li>
812          *           <li> either a global seinfo value is present OR at least one
813          *           inner package mapping must be present BUT not both. </li>
814          *        </ul>
815          *      </li>
816          *    </ul>
817          *
818          * @return an instance of {@link Policy} with the options set from this builder
819          * @throws IllegalStateException if an invariant is violated.
820          */
build()821         public Policy build() {
822             Policy p = new Policy(this);
823 
824             if (p.mDefaultStanza) {
825                 if (p.mSeinfo == null) {
826                     String err = "Missing global seinfo tag with default stanza.";
827                     throw new IllegalStateException(err);
828                 }
829                 if (p.mCerts.size() != 0) {
830                     String err = "Certs not allowed with default stanza.";
831                     throw new IllegalStateException(err);
832                 }
833                 if (!p.mPkgMap.isEmpty()) {
834                     String err = "Inner package mappings not allowed with default stanza.";
835                     throw new IllegalStateException(err);
836                 }
837             } else {
838                 if (p.mCerts.size() == 0) {
839                     String err = "Missing certs with signer tag. Expecting at least one.";
840                     throw new IllegalStateException(err);
841                 }
842                 if (!(p.mSeinfo == null ^ p.mPkgMap.isEmpty())) {
843                     String err = "Only seinfo tag XOR package tags are allowed within " +
844                             "a signer stanza.";
845                     throw new IllegalStateException(err);
846                 }
847             }
848 
849             return p;
850         }
851     }
852 }
853 
854 /**
855  * Comparision imposing an ordering on Policy objects. It is understood that Policy
856  * objects can only take one of three forms and ordered according to the following
857  * set of rules most specific to least.
858  * <ul>
859  *   <li> signer stanzas with inner package mappings </li>
860  *   <li> signer stanzas with global seinfo tags </li>
861  *   <li> default stanza </li>
862  * </ul>
863  * This comparison also checks for duplicate entries on the input selectors. Any
864  * found duplicates will be flagged and can be checked with {@link #foundDuplicate}.
865  */
866 
867 final class PolicyComparator implements Comparator<Policy> {
868 
869     private boolean duplicateFound = false;
870 
foundDuplicate()871     public boolean foundDuplicate() {
872         return duplicateFound;
873     }
874 
875     @Override
compare(Policy p1, Policy p2)876     public int compare(Policy p1, Policy p2) {
877 
878         // Give precedence to signature stanzas over default stanzas
879         if (p1.isDefaultStanza() != p2.isDefaultStanza()) {
880             return p1.isDefaultStanza() ? 1 : -1;
881         }
882 
883         // Give precedence to stanzas with inner package mappings
884         if (p1.hasInnerPackages() != p2.hasInnerPackages()) {
885             return p1.hasInnerPackages() ? -1 : 1;
886         }
887 
888         // Check for duplicate entries
889         if (p1.getSignatures().equals(p2.getSignatures())) {
890             // Checks if default stanza or a signer w/o inner package names
891             if (p1.hasGlobalSeinfo()) {
892                 duplicateFound = true;
893                 Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString());
894             }
895 
896             // Look for common inner package name mappings
897             final Map<String, String> p1Packages = p1.getInnerPackages();
898             final Map<String, String> p2Packages = p2.getInnerPackages();
899             if (!Collections.disjoint(p1Packages.keySet(), p2Packages.keySet())) {
900                 duplicateFound = true;
901                 Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString());
902             }
903         }
904 
905         return 0;
906     }
907 }
908