| /* |
| * Copyright (C) 2012 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.pm; |
| |
| import android.compat.annotation.ChangeId; |
| import android.compat.annotation.EnabledAfter; |
| import android.content.pm.PackageParser; |
| import android.content.pm.PackageParser.SigningDetails; |
| import android.content.pm.Signature; |
| import android.content.pm.parsing.AndroidPackage; |
| import android.os.Environment; |
| import android.util.Slog; |
| import android.util.Xml; |
| |
| import com.android.server.compat.PlatformCompat; |
| |
| import libcore.io.IoUtils; |
| |
| import org.xmlpull.v1.XmlPullParser; |
| import org.xmlpull.v1.XmlPullParserException; |
| |
| import java.io.File; |
| import java.io.FileReader; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.Comparator; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| |
| /** |
| * Centralized access to SELinux MMAC (middleware MAC) implementation. This |
| * class is responsible for loading the appropriate mac_permissions.xml file |
| * as well as providing an interface for assigning seinfo values to apks. |
| * |
| * {@hide} |
| */ |
| public final class SELinuxMMAC { |
| |
| static final String TAG = "SELinuxMMAC"; |
| |
| private static final boolean DEBUG_POLICY = false; |
| private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false; |
| private static final boolean DEBUG_POLICY_ORDER = DEBUG_POLICY || false; |
| |
| // All policy stanzas read from mac_permissions.xml. This is also the lock |
| // to synchronize access during policy load and access attempts. |
| private static List<Policy> sPolicies = new ArrayList<>(); |
| /** Whether or not the policy files have been read */ |
| private static boolean sPolicyRead; |
| |
| /** Required MAC permissions files */ |
| private static List<File> sMacPermissions = new ArrayList<>(); |
| |
| private static final String DEFAULT_SEINFO = "default"; |
| |
| // Append privapp to existing seinfo label |
| private static final String PRIVILEGED_APP_STR = ":privapp"; |
| |
| // Append targetSdkVersion=n to existing seinfo label where n is the app's targetSdkVersion |
| private static final String TARGETSDKVERSION_STR = ":targetSdkVersion="; |
| |
| /** |
| * This change gates apps access to untrusted_app_R-targetSDk SELinux domain. Allows opt-in |
| * to R targetSdkVersion enforced changes without changing target SDK. Turning this change |
| * off for an app targeting R is a no-op. |
| * |
| * <p>Has no effect for apps using shared user id. |
| * |
| * TODO(b/143539591): Update description with relevant SELINUX changes this opts in to. |
| */ |
| @EnabledAfter(targetSdkVersion = android.os.Build.VERSION_CODES.Q) |
| @ChangeId |
| static final long SELINUX_LATEST_CHANGES = 143539591L; |
| |
| // Only initialize sMacPermissions once. |
| static { |
| // Platform mac permissions. |
| sMacPermissions.add(new File( |
| Environment.getRootDirectory(), "/etc/selinux/plat_mac_permissions.xml")); |
| |
| // SystemExt mac permissions (optional). |
| final File systemExtMacPermission = new File( |
| Environment.getSystemExtDirectory(), "/etc/selinux/system_ext_mac_permissions.xml"); |
| if (systemExtMacPermission.exists()) { |
| sMacPermissions.add(systemExtMacPermission); |
| } |
| |
| // Product mac permissions (optional). |
| final File productMacPermission = new File( |
| Environment.getProductDirectory(), "/etc/selinux/product_mac_permissions.xml"); |
| if (productMacPermission.exists()) { |
| sMacPermissions.add(productMacPermission); |
| } |
| |
| // Vendor mac permissions. |
| // The filename has been renamed from nonplat_mac_permissions to |
| // vendor_mac_permissions. Either of them should exist. |
| final File vendorMacPermission = new File( |
| Environment.getVendorDirectory(), "/etc/selinux/vendor_mac_permissions.xml"); |
| if (vendorMacPermission.exists()) { |
| sMacPermissions.add(vendorMacPermission); |
| } else { |
| // For backward compatibility. |
| sMacPermissions.add(new File(Environment.getVendorDirectory(), |
| "/etc/selinux/nonplat_mac_permissions.xml")); |
| } |
| |
| // ODM mac permissions (optional). |
| final File odmMacPermission = new File( |
| Environment.getOdmDirectory(), "/etc/selinux/odm_mac_permissions.xml"); |
| if (odmMacPermission.exists()) { |
| sMacPermissions.add(odmMacPermission); |
| } |
| } |
| |
| /** |
| * Load the mac_permissions.xml file containing all seinfo assignments used to |
| * label apps. The loaded mac_permissions.xml files are plat_mac_permissions.xml and |
| * vendor_mac_permissions.xml, on /system and /vendor partitions, respectively. |
| * odm_mac_permissions.xml on /odm partition is optional. For further guidance on |
| * the proper structure of a mac_permissions.xml file consult the source code |
| * located at system/sepolicy/private/mac_permissions.xml. |
| * |
| * @return boolean indicating if policy was correctly loaded. A value of false |
| * typically indicates a structural problem with the xml or incorrectly |
| * constructed policy stanzas. A value of true means that all stanzas |
| * were loaded successfully; no partial loading is possible. |
| */ |
| public static boolean readInstallPolicy() { |
| synchronized (sPolicies) { |
| if (sPolicyRead) { |
| return true; |
| } |
| } |
| |
| // Temp structure to hold the rules while we parse the xml file |
| List<Policy> policies = new ArrayList<>(); |
| |
| FileReader policyFile = null; |
| XmlPullParser parser = Xml.newPullParser(); |
| |
| final int count = sMacPermissions.size(); |
| for (int i = 0; i < count; ++i) { |
| final File macPermission = sMacPermissions.get(i); |
| try { |
| policyFile = new FileReader(macPermission); |
| Slog.d(TAG, "Using policy file " + macPermission); |
| |
| parser.setInput(policyFile); |
| parser.nextTag(); |
| parser.require(XmlPullParser.START_TAG, null, "policy"); |
| |
| while (parser.next() != XmlPullParser.END_TAG) { |
| if (parser.getEventType() != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| switch (parser.getName()) { |
| case "signer": |
| policies.add(readSignerOrThrow(parser)); |
| break; |
| default: |
| skip(parser); |
| } |
| } |
| } catch (IllegalStateException | IllegalArgumentException | |
| XmlPullParserException ex) { |
| StringBuilder sb = new StringBuilder("Exception @"); |
| sb.append(parser.getPositionDescription()); |
| sb.append(" while parsing "); |
| sb.append(macPermission); |
| sb.append(":"); |
| sb.append(ex); |
| Slog.w(TAG, sb.toString()); |
| return false; |
| } catch (IOException ioe) { |
| Slog.w(TAG, "Exception parsing " + macPermission, ioe); |
| return false; |
| } finally { |
| IoUtils.closeQuietly(policyFile); |
| } |
| } |
| |
| // Now sort the policy stanzas |
| PolicyComparator policySort = new PolicyComparator(); |
| Collections.sort(policies, policySort); |
| if (policySort.foundDuplicate()) { |
| Slog.w(TAG, "ERROR! Duplicate entries found parsing mac_permissions.xml files"); |
| return false; |
| } |
| |
| synchronized (sPolicies) { |
| sPolicies.clear(); |
| sPolicies.addAll(policies); |
| sPolicyRead = true; |
| |
| if (DEBUG_POLICY_ORDER) { |
| for (Policy policy : sPolicies) { |
| Slog.d(TAG, "Policy: " + policy.toString()); |
| } |
| } |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Loop over a signer tag looking for seinfo, package and cert tags. A {@link Policy} |
| * instance will be created and returned in the process. During the pass all other |
| * tag elements will be skipped. |
| * |
| * @param parser an XmlPullParser object representing a signer element. |
| * @return the constructed {@link Policy} instance |
| * @throws IOException |
| * @throws XmlPullParserException |
| * @throws IllegalArgumentException if any of the validation checks fail while |
| * parsing tag values. |
| * @throws IllegalStateException if any of the invariants fail when constructing |
| * the {@link Policy} instance. |
| */ |
| private static Policy readSignerOrThrow(XmlPullParser parser) throws IOException, |
| XmlPullParserException { |
| |
| parser.require(XmlPullParser.START_TAG, null, "signer"); |
| Policy.PolicyBuilder pb = new Policy.PolicyBuilder(); |
| |
| // Check for a cert attached to the signer tag. We allow a signature |
| // to appear as an attribute as well as those attached to cert tags. |
| String cert = parser.getAttributeValue(null, "signature"); |
| if (cert != null) { |
| pb.addSignature(cert); |
| } |
| |
| while (parser.next() != XmlPullParser.END_TAG) { |
| if (parser.getEventType() != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| String tagName = parser.getName(); |
| if ("seinfo".equals(tagName)) { |
| String seinfo = parser.getAttributeValue(null, "value"); |
| pb.setGlobalSeinfoOrThrow(seinfo); |
| readSeinfo(parser); |
| } else if ("package".equals(tagName)) { |
| readPackageOrThrow(parser, pb); |
| } else if ("cert".equals(tagName)) { |
| String sig = parser.getAttributeValue(null, "signature"); |
| pb.addSignature(sig); |
| readCert(parser); |
| } else { |
| skip(parser); |
| } |
| } |
| |
| return pb.build(); |
| } |
| |
| /** |
| * Loop over a package element looking for seinfo child tags. If found return the |
| * value attribute of the seinfo tag, otherwise return null. All other tags encountered |
| * will be skipped. |
| * |
| * @param parser an XmlPullParser object representing a package element. |
| * @param pb a Policy.PolicyBuilder instance to build |
| * @throws IOException |
| * @throws XmlPullParserException |
| * @throws IllegalArgumentException if any of the validation checks fail while |
| * parsing tag values. |
| * @throws IllegalStateException if there is a duplicate seinfo tag for the current |
| * package tag. |
| */ |
| private static void readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb) throws |
| IOException, XmlPullParserException { |
| parser.require(XmlPullParser.START_TAG, null, "package"); |
| String pkgName = parser.getAttributeValue(null, "name"); |
| |
| while (parser.next() != XmlPullParser.END_TAG) { |
| if (parser.getEventType() != XmlPullParser.START_TAG) { |
| continue; |
| } |
| |
| String tagName = parser.getName(); |
| if ("seinfo".equals(tagName)) { |
| String seinfo = parser.getAttributeValue(null, "value"); |
| pb.addInnerPackageMapOrThrow(pkgName, seinfo); |
| readSeinfo(parser); |
| } else { |
| skip(parser); |
| } |
| } |
| } |
| |
| private static void readCert(XmlPullParser parser) throws IOException, |
| XmlPullParserException { |
| parser.require(XmlPullParser.START_TAG, null, "cert"); |
| parser.nextTag(); |
| } |
| |
| private static void readSeinfo(XmlPullParser parser) throws IOException, |
| XmlPullParserException { |
| parser.require(XmlPullParser.START_TAG, null, "seinfo"); |
| parser.nextTag(); |
| } |
| |
| private static void skip(XmlPullParser p) throws IOException, XmlPullParserException { |
| if (p.getEventType() != XmlPullParser.START_TAG) { |
| throw new IllegalStateException(); |
| } |
| int depth = 1; |
| while (depth != 0) { |
| switch (p.next()) { |
| case XmlPullParser.END_TAG: |
| depth--; |
| break; |
| case XmlPullParser.START_TAG: |
| depth++; |
| break; |
| } |
| } |
| } |
| |
| private static int getTargetSdkVersionForSeInfo(AndroidPackage pkg, |
| SharedUserSetting sharedUserSetting, PlatformCompat compatibility) { |
| // Apps which share a sharedUserId must be placed in the same selinux domain. If this |
| // package is the first app installed as this shared user, set seInfoTargetSdkVersion to its |
| // targetSdkVersion. These are later adjusted in PackageManagerService's constructor to be |
| // the lowest targetSdkVersion of all apps within the shared user, which corresponds to the |
| // least restrictive selinux domain. |
| // NOTE: As new packages are installed / updated, the shared user's seinfoTargetSdkVersion |
| // will NOT be modified until next boot, even if a lower targetSdkVersion is used. This |
| // ensures that all packages continue to run in the same selinux domain. |
| if ((sharedUserSetting != null) && (sharedUserSetting.packages.size() != 0)) { |
| return sharedUserSetting.seInfoTargetSdkVersion; |
| } |
| if (compatibility.isChangeEnabled(SELINUX_LATEST_CHANGES, pkg.toAppInfoWithoutState())) { |
| return android.os.Build.VERSION_CODES.R; |
| } |
| |
| return pkg.getTargetSdkVersion(); |
| } |
| |
| /** |
| * Selects a security label to a package based on input parameters and the seinfo tag taken |
| * from a matched policy. All signature based policy stanzas are consulted and, if no match |
| * is found, the default seinfo label of 'default' is used. The security label is attached to |
| * the ApplicationInfo instance of the package. |
| * |
| * @param pkg object representing the package to be labeled. |
| * @param sharedUserSetting if the app shares a sharedUserId, then this has the shared setting. |
| * @param compatibility the PlatformCompat service to ask about state of compat changes. |
| * @return String representing the resulting seinfo. |
| */ |
| public static String getSeInfo(AndroidPackage pkg, SharedUserSetting sharedUserSetting, |
| PlatformCompat compatibility) { |
| final int targetSdkVersion = getTargetSdkVersionForSeInfo(pkg, sharedUserSetting, |
| compatibility); |
| // TODO(b/71593002): isPrivileged for sharedUser and appInfo should never be out of sync. |
| // They currently can be if the sharedUser apps are signed with the platform key. |
| final boolean isPrivileged = (sharedUserSetting != null) |
| ? sharedUserSetting.isPrivileged() | pkg.isPrivileged() : pkg.isPrivileged(); |
| return getSeInfo(pkg, isPrivileged, targetSdkVersion); |
| } |
| |
| /** |
| * Selects a security label to a package based on input parameters and the seinfo tag taken |
| * from a matched policy. All signature based policy stanzas are consulted and, if no match |
| * is found, the default seinfo label of 'default' is used. The security label is attached to |
| * the ApplicationInfo instance of the package. |
| * |
| * @param pkg object representing the package to be labeled. |
| * @param isPrivileged boolean. |
| * @param targetSdkVersion int. If this pkg runs as a sharedUser, targetSdkVersion is the |
| * greater of: lowest targetSdk for all pkgs in the sharedUser, or |
| * MINIMUM_TARGETSDKVERSION. |
| * @return String representing the resulting seinfo. |
| */ |
| public static String getSeInfo(AndroidPackage pkg, boolean isPrivileged, |
| int targetSdkVersion) { |
| String seInfo = null; |
| synchronized (sPolicies) { |
| if (!sPolicyRead) { |
| if (DEBUG_POLICY) { |
| Slog.d(TAG, "Policy not read"); |
| } |
| } else { |
| for (Policy policy : sPolicies) { |
| seInfo = policy.getMatchedSeInfo(pkg); |
| if (seInfo != null) { |
| break; |
| } |
| } |
| } |
| } |
| |
| if (seInfo == null) { |
| seInfo = DEFAULT_SEINFO; |
| } |
| |
| if (isPrivileged) { |
| seInfo += PRIVILEGED_APP_STR; |
| } |
| |
| seInfo += TARGETSDKVERSION_STR + targetSdkVersion; |
| |
| if (DEBUG_POLICY_INSTALL) { |
| Slog.i(TAG, "package (" + pkg.getPackageName() + ") labeled with " |
| + "seinfo=" + seInfo); |
| } |
| return seInfo; |
| } |
| } |
| |
| /** |
| * Holds valid policy representations of individual stanzas from a mac_permissions.xml |
| * file. Each instance can further be used to assign seinfo values to apks using the |
| * {@link Policy#getMatchedSeInfo(AndroidPackage)} method. To create an instance of this use the |
| * {@link PolicyBuilder} pattern class, where each instance is validated against a set |
| * of invariants before being built and returned. Each instance can be guaranteed to |
| * hold one valid policy stanza as outlined in the system/sepolicy/mac_permissions.xml |
| * file. |
| * <p> |
| * The following is an example of how to use {@link Policy.PolicyBuilder} to create a |
| * signer based Policy instance with only inner package name refinements. |
| * </p> |
| * <pre> |
| * {@code |
| * Policy policy = new Policy.PolicyBuilder() |
| * .addSignature("308204a8...") |
| * .addSignature("483538c8...") |
| * .addInnerPackageMapOrThrow("com.foo.", "bar") |
| * .addInnerPackageMapOrThrow("com.foo.other", "bar") |
| * .build(); |
| * } |
| * </pre> |
| * <p> |
| * The following is an example of how to use {@link Policy.PolicyBuilder} to create a |
| * signer based Policy instance with only a global seinfo tag. |
| * </p> |
| * <pre> |
| * {@code |
| * Policy policy = new Policy.PolicyBuilder() |
| * .addSignature("308204a8...") |
| * .addSignature("483538c8...") |
| * .setGlobalSeinfoOrThrow("paltform") |
| * .build(); |
| * } |
| * </pre> |
| */ |
| final class Policy { |
| |
| private final String mSeinfo; |
| private final Set<Signature> mCerts; |
| private final Map<String, String> mPkgMap; |
| |
| // Use the PolicyBuilder pattern to instantiate |
| private Policy(PolicyBuilder builder) { |
| mSeinfo = builder.mSeinfo; |
| mCerts = Collections.unmodifiableSet(builder.mCerts); |
| mPkgMap = Collections.unmodifiableMap(builder.mPkgMap); |
| } |
| |
| /** |
| * Return all the certs stored with this policy stanza. |
| * |
| * @return A set of Signature objects representing all the certs stored |
| * with the policy. |
| */ |
| public Set<Signature> getSignatures() { |
| return mCerts; |
| } |
| |
| /** |
| * Return whether this policy object contains package name mapping refinements. |
| * |
| * @return A boolean indicating if this object has inner package name mappings. |
| */ |
| public boolean hasInnerPackages() { |
| return !mPkgMap.isEmpty(); |
| } |
| |
| /** |
| * Return the mapping of all package name refinements. |
| * |
| * @return A Map object whose keys are the package names and whose values are |
| * the seinfo assignments. |
| */ |
| public Map<String, String> getInnerPackages() { |
| return mPkgMap; |
| } |
| |
| /** |
| * Return whether the policy object has a global seinfo tag attached. |
| * |
| * @return A boolean indicating if this stanza has a global seinfo tag. |
| */ |
| public boolean hasGlobalSeinfo() { |
| return mSeinfo != null; |
| } |
| |
| @Override |
| public String toString() { |
| StringBuilder sb = new StringBuilder(); |
| for (Signature cert : mCerts) { |
| sb.append("cert=" + cert.toCharsString().substring(0, 11) + "... "); |
| } |
| |
| if (mSeinfo != null) { |
| sb.append("seinfo=" + mSeinfo); |
| } |
| |
| for (String name : mPkgMap.keySet()) { |
| sb.append(" " + name + "=" + mPkgMap.get(name)); |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * <p> |
| * Determine the seinfo value to assign to an apk. The appropriate seinfo value |
| * is determined using the following steps: |
| * </p> |
| * <ul> |
| * <li> All certs used to sign the apk and all certs stored with this policy |
| * instance are tested for set equality. If this fails then null is returned. |
| * </li> |
| * <li> If all certs match then an appropriate inner package stanza is |
| * searched based on package name alone. If matched, the stored seinfo |
| * value for that mapping is returned. |
| * </li> |
| * <li> If all certs matched and no inner package stanza matches then return |
| * the global seinfo value. The returned value can be null in this case. |
| * </li> |
| * </ul> |
| * <p> |
| * In all cases, a return value of null should be interpreted as the apk failing |
| * to match this Policy instance; i.e. failing this policy stanza. |
| * </p> |
| * @param pkg the apk to check given as a PackageParser.Package object |
| * @return A string representing the seinfo matched during policy lookup. |
| * A value of null can also be returned if no match occured. |
| */ |
| public String getMatchedSeInfo(AndroidPackage pkg) { |
| // Check for exact signature matches across all certs. |
| Signature[] certs = mCerts.toArray(new Signature[0]); |
| if (pkg.getSigningDetails() != SigningDetails.UNKNOWN |
| && !Signature.areExactMatch(certs, pkg.getSigningDetails().signatures)) { |
| |
| // certs aren't exact match, but the package may have rotated from the known system cert |
| if (certs.length > 1 || !pkg.getSigningDetails().hasCertificate(certs[0])) { |
| return null; |
| } |
| } |
| |
| // Check for inner package name matches given that the |
| // signature checks already passed. |
| String seinfoValue = mPkgMap.get(pkg.getPackageName()); |
| if (seinfoValue != null) { |
| return seinfoValue; |
| } |
| |
| // Return the global seinfo value. |
| return mSeinfo; |
| } |
| |
| /** |
| * A nested builder class to create {@link Policy} instances. A {@link Policy} |
| * class instance represents one valid policy stanza found in a mac_permissions.xml |
| * file. A valid policy stanza is defined to be a signer stanza which obeys the rules |
| * outlined in system/sepolicy/mac_permissions.xml. The {@link #build} method |
| * ensures a set of invariants are upheld enforcing the correct stanza structure |
| * before returning a valid Policy object. |
| */ |
| public static final class PolicyBuilder { |
| |
| private String mSeinfo; |
| private final Set<Signature> mCerts; |
| private final Map<String, String> mPkgMap; |
| |
| public PolicyBuilder() { |
| mCerts = new HashSet<Signature>(2); |
| mPkgMap = new HashMap<String, String>(2); |
| } |
| |
| /** |
| * Adds a signature to the set of certs used for validation checks. The purpose |
| * being that all contained certs will need to be matched against all certs |
| * contained with an apk. |
| * |
| * @param cert the signature to add given as a String. |
| * @return The reference to this PolicyBuilder. |
| * @throws IllegalArgumentException if the cert value fails validation; |
| * null or is an invalid hex-encoded ASCII string. |
| */ |
| public PolicyBuilder addSignature(String cert) { |
| if (cert == null) { |
| String err = "Invalid signature value " + cert; |
| throw new IllegalArgumentException(err); |
| } |
| |
| mCerts.add(new Signature(cert)); |
| return this; |
| } |
| |
| /** |
| * Set the global seinfo tag for this policy stanza. The global seinfo tag |
| * when attached to a signer tag represents the assignment when there isn't a |
| * further inner package refinement in policy. |
| * |
| * @param seinfo the seinfo value given as a String. |
| * @return The reference to this PolicyBuilder. |
| * @throws IllegalArgumentException if the seinfo value fails validation; |
| * null, zero length or contains non-valid characters [^a-zA-Z_\._0-9]. |
| * @throws IllegalStateException if an seinfo value has already been found |
| */ |
| public PolicyBuilder setGlobalSeinfoOrThrow(String seinfo) { |
| if (!validateValue(seinfo)) { |
| String err = "Invalid seinfo value " + seinfo; |
| throw new IllegalArgumentException(err); |
| } |
| |
| if (mSeinfo != null && !mSeinfo.equals(seinfo)) { |
| String err = "Duplicate seinfo tag found"; |
| throw new IllegalStateException(err); |
| } |
| |
| mSeinfo = seinfo; |
| return this; |
| } |
| |
| /** |
| * Create a package name to seinfo value mapping. Each mapping represents |
| * the seinfo value that will be assigned to the described package name. |
| * These localized mappings allow the global seinfo to be overriden. |
| * |
| * @param pkgName the android package name given to the app |
| * @param seinfo the seinfo value that will be assigned to the passed pkgName |
| * @return The reference to this PolicyBuilder. |
| * @throws IllegalArgumentException if the seinfo value fails validation; |
| * null, zero length or contains non-valid characters [^a-zA-Z_\.0-9]. |
| * Or, if the package name isn't a valid android package name. |
| * @throws IllegalStateException if trying to reset a package mapping with a |
| * different seinfo value. |
| */ |
| public PolicyBuilder addInnerPackageMapOrThrow(String pkgName, String seinfo) { |
| if (!validateValue(pkgName)) { |
| String err = "Invalid package name " + pkgName; |
| throw new IllegalArgumentException(err); |
| } |
| if (!validateValue(seinfo)) { |
| String err = "Invalid seinfo value " + seinfo; |
| throw new IllegalArgumentException(err); |
| } |
| |
| String pkgValue = mPkgMap.get(pkgName); |
| if (pkgValue != null && !pkgValue.equals(seinfo)) { |
| String err = "Conflicting seinfo value found"; |
| throw new IllegalStateException(err); |
| } |
| |
| mPkgMap.put(pkgName, seinfo); |
| return this; |
| } |
| |
| /** |
| * General validation routine for the attribute strings of an element. Checks |
| * if the string is non-null, positive length and only contains [a-zA-Z_\.0-9]. |
| * |
| * @param name the string to validate. |
| * @return boolean indicating if the string was valid. |
| */ |
| private boolean validateValue(String name) { |
| if (name == null) |
| return false; |
| |
| // Want to match on [0-9a-zA-Z_.] |
| if (!name.matches("\\A[\\.\\w]+\\z")) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * <p> |
| * Create a {@link Policy} instance based on the current configuration. This |
| * method checks for certain policy invariants used to enforce certain guarantees |
| * about the expected structure of a policy stanza. |
| * Those invariants are: |
| * </p> |
| * <ul> |
| * <li> at least one cert must be found </li> |
| * <li> either a global seinfo value is present OR at least one |
| * inner package mapping must be present BUT not both. </li> |
| * </ul> |
| * @return an instance of {@link Policy} with the options set from this builder |
| * @throws IllegalStateException if an invariant is violated. |
| */ |
| public Policy build() { |
| Policy p = new Policy(this); |
| |
| if (p.mCerts.isEmpty()) { |
| String err = "Missing certs with signer tag. Expecting at least one."; |
| throw new IllegalStateException(err); |
| } |
| if (!(p.mSeinfo == null ^ p.mPkgMap.isEmpty())) { |
| String err = "Only seinfo tag XOR package tags are allowed within " + |
| "a signer stanza."; |
| throw new IllegalStateException(err); |
| } |
| |
| return p; |
| } |
| } |
| } |
| |
| /** |
| * Comparision imposing an ordering on Policy objects. It is understood that Policy |
| * objects can only take one of three forms and ordered according to the following |
| * set of rules most specific to least. |
| * <ul> |
| * <li> signer stanzas with inner package mappings </li> |
| * <li> signer stanzas with global seinfo tags </li> |
| * </ul> |
| * This comparison also checks for duplicate entries on the input selectors. Any |
| * found duplicates will be flagged and can be checked with {@link #foundDuplicate}. |
| */ |
| |
| final class PolicyComparator implements Comparator<Policy> { |
| |
| private boolean duplicateFound = false; |
| |
| public boolean foundDuplicate() { |
| return duplicateFound; |
| } |
| |
| @Override |
| public int compare(Policy p1, Policy p2) { |
| |
| // Give precedence to stanzas with inner package mappings |
| if (p1.hasInnerPackages() != p2.hasInnerPackages()) { |
| return p1.hasInnerPackages() ? -1 : 1; |
| } |
| |
| // Check for duplicate entries |
| if (p1.getSignatures().equals(p2.getSignatures())) { |
| // Checks if signer w/o inner package names |
| if (p1.hasGlobalSeinfo()) { |
| duplicateFound = true; |
| Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString()); |
| } |
| |
| // Look for common inner package name mappings |
| final Map<String, String> p1Packages = p1.getInnerPackages(); |
| final Map<String, String> p2Packages = p2.getInnerPackages(); |
| if (!Collections.disjoint(p1Packages.keySet(), p2Packages.keySet())) { |
| duplicateFound = true; |
| Slog.e(SELinuxMMAC.TAG, "Duplicate policy entry: " + p1.toString()); |
| } |
| } |
| |
| return 0; |
| } |
| } |