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