Home | History | Annotate | Download | only in pm
      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.ApplicationInfo;
     20 import android.content.pm.PackageParser;
     21 import android.content.pm.Signature;
     22 import android.os.Environment;
     23 import android.util.Slog;
     24 import android.util.Xml;
     25 
     26 import com.android.internal.util.XmlUtils;
     27 
     28 import libcore.io.IoUtils;
     29 
     30 import java.io.File;
     31 import java.io.FileNotFoundException;
     32 import java.io.FileOutputStream;
     33 import java.io.FileReader;
     34 import java.io.IOException;
     35 import java.security.MessageDigest;
     36 import java.security.NoSuchAlgorithmException;
     37 
     38 import java.util.HashMap;
     39 
     40 import org.xmlpull.v1.XmlPullParser;
     41 import org.xmlpull.v1.XmlPullParserException;
     42 
     43 /**
     44  * Centralized access to SELinux MMAC (middleware MAC) implementation.
     45  * {@hide}
     46  */
     47 public final class SELinuxMMAC {
     48 
     49     private static final String TAG = "SELinuxMMAC";
     50 
     51     private static final boolean DEBUG_POLICY = false;
     52     private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false;
     53 
     54     // Signature seinfo values read from policy.
     55     private static HashMap<Signature, Policy> sSigSeinfo = new HashMap<Signature, Policy>();
     56 
     57     // Default seinfo read from policy.
     58     private static String sDefaultSeinfo = null;
     59 
     60     // Data policy override version file.
     61     private static final String DATA_VERSION_FILE =
     62             Environment.getDataDirectory() + "/security/current/selinux_version";
     63 
     64     // Base policy version file.
     65     private static final String BASE_VERSION_FILE = "/selinux_version";
     66 
     67     // Whether override security policies should be loaded.
     68     private static final boolean USE_OVERRIDE_POLICY = useOverridePolicy();
     69 
     70     // Data override mac_permissions.xml policy file.
     71     private static final String DATA_MAC_PERMISSIONS =
     72             Environment.getDataDirectory() + "/security/current/mac_permissions.xml";
     73 
     74     // Base mac_permissions.xml policy file.
     75     private static final String BASE_MAC_PERMISSIONS =
     76             Environment.getRootDirectory() + "/etc/security/mac_permissions.xml";
     77 
     78     // Determine which mac_permissions.xml file to use.
     79     private static final String MAC_PERMISSIONS = USE_OVERRIDE_POLICY ?
     80             DATA_MAC_PERMISSIONS : BASE_MAC_PERMISSIONS;
     81 
     82     // Data override seapp_contexts policy file.
     83     private static final String DATA_SEAPP_CONTEXTS =
     84             Environment.getDataDirectory() + "/security/current/seapp_contexts";
     85 
     86     // Base seapp_contexts policy file.
     87     private static final String BASE_SEAPP_CONTEXTS = "/seapp_contexts";
     88 
     89     // Determine which seapp_contexts file to use.
     90     private static final String SEAPP_CONTEXTS = USE_OVERRIDE_POLICY ?
     91             DATA_SEAPP_CONTEXTS : BASE_SEAPP_CONTEXTS;
     92 
     93     // Stores the hash of the last used seapp_contexts file.
     94     private static final String SEAPP_HASH_FILE =
     95             Environment.getDataDirectory().toString() + "/system/seapp_hash";
     96 
     97 
     98     // Signature policy stanzas
     99     static class Policy {
    100         private String seinfo;
    101         private final HashMap<String, String> pkgMap;
    102 
    103         Policy() {
    104             seinfo = null;
    105             pkgMap = new HashMap<String, String>();
    106         }
    107 
    108         void putSeinfo(String seinfoValue) {
    109             seinfo = seinfoValue;
    110         }
    111 
    112         void putPkg(String pkg, String seinfoValue) {
    113             pkgMap.put(pkg, seinfoValue);
    114         }
    115 
    116         // Valid policy stanza means there exists a global
    117         // seinfo value or at least one package policy.
    118         boolean isValid() {
    119             return (seinfo != null) || (!pkgMap.isEmpty());
    120         }
    121 
    122         String checkPolicy(String pkgName) {
    123             // Check for package name seinfo value first.
    124             String seinfoValue = pkgMap.get(pkgName);
    125             if (seinfoValue != null) {
    126                 return seinfoValue;
    127             }
    128 
    129             // Return the global seinfo value.
    130             return seinfo;
    131         }
    132     }
    133 
    134     private static void flushInstallPolicy() {
    135         sSigSeinfo.clear();
    136         sDefaultSeinfo = null;
    137     }
    138 
    139     public static boolean readInstallPolicy() {
    140         // Temp structures to hold the rules while we parse the xml file.
    141         // We add all the rules together once we know there's no structural problems.
    142         HashMap<Signature, Policy> sigSeinfo = new HashMap<Signature, Policy>();
    143         String defaultSeinfo = null;
    144 
    145         FileReader policyFile = null;
    146         try {
    147             policyFile = new FileReader(MAC_PERMISSIONS);
    148             Slog.d(TAG, "Using policy file " + MAC_PERMISSIONS);
    149 
    150             XmlPullParser parser = Xml.newPullParser();
    151             parser.setInput(policyFile);
    152 
    153             XmlUtils.beginDocument(parser, "policy");
    154             while (true) {
    155                 XmlUtils.nextElement(parser);
    156                 if (parser.getEventType() == XmlPullParser.END_DOCUMENT) {
    157                     break;
    158                 }
    159 
    160                 String tagName = parser.getName();
    161                 if ("signer".equals(tagName)) {
    162                     String cert = parser.getAttributeValue(null, "signature");
    163                     if (cert == null) {
    164                         Slog.w(TAG, "<signer> without signature at "
    165                                + parser.getPositionDescription());
    166                         XmlUtils.skipCurrentTag(parser);
    167                         continue;
    168                     }
    169                     Signature signature;
    170                     try {
    171                         signature = new Signature(cert);
    172                     } catch (IllegalArgumentException e) {
    173                         Slog.w(TAG, "<signer> with bad signature at "
    174                                + parser.getPositionDescription(), e);
    175                         XmlUtils.skipCurrentTag(parser);
    176                         continue;
    177                     }
    178                     Policy policy = readPolicyTags(parser);
    179                     if (policy.isValid()) {
    180                         sigSeinfo.put(signature, policy);
    181                     }
    182                 } else if ("default".equals(tagName)) {
    183                     // Value is null if default tag is absent or seinfo tag is malformed.
    184                     defaultSeinfo = readSeinfoTag(parser);
    185                     if (DEBUG_POLICY_INSTALL)
    186                         Slog.i(TAG, "<default> tag assigned seinfo=" + defaultSeinfo);
    187 
    188                 } else {
    189                     XmlUtils.skipCurrentTag(parser);
    190                 }
    191             }
    192         } catch (XmlPullParserException xpe) {
    193             Slog.w(TAG, "Got exception parsing " + MAC_PERMISSIONS, xpe);
    194             return false;
    195         } catch (IOException ioe) {
    196             Slog.w(TAG, "Got exception parsing " + MAC_PERMISSIONS, ioe);
    197             return false;
    198         } finally {
    199             IoUtils.closeQuietly(policyFile);
    200         }
    201 
    202         flushInstallPolicy();
    203         sSigSeinfo = sigSeinfo;
    204         sDefaultSeinfo = defaultSeinfo;
    205 
    206         return true;
    207     }
    208 
    209     private static Policy readPolicyTags(XmlPullParser parser) throws
    210             IOException, XmlPullParserException {
    211 
    212         int type;
    213         int outerDepth = parser.getDepth();
    214         Policy policy = new Policy();
    215         while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
    216                && (type != XmlPullParser.END_TAG
    217                    || parser.getDepth() > outerDepth)) {
    218             if (type == XmlPullParser.END_TAG
    219                 || type == XmlPullParser.TEXT) {
    220                 continue;
    221             }
    222 
    223             String tagName = parser.getName();
    224             if ("seinfo".equals(tagName)) {
    225                 String seinfo = parseSeinfo(parser);
    226                 if (seinfo != null) {
    227                     policy.putSeinfo(seinfo);
    228                 }
    229                 XmlUtils.skipCurrentTag(parser);
    230             } else if ("package".equals(tagName)) {
    231                 String pkg = parser.getAttributeValue(null, "name");
    232                 if (!validatePackageName(pkg)) {
    233                     Slog.w(TAG, "<package> without valid name at "
    234                            + parser.getPositionDescription());
    235                     XmlUtils.skipCurrentTag(parser);
    236                     continue;
    237                 }
    238 
    239                 String seinfo = readSeinfoTag(parser);
    240                 if (seinfo != null) {
    241                     policy.putPkg(pkg, seinfo);
    242                 }
    243             } else {
    244                 XmlUtils.skipCurrentTag(parser);
    245             }
    246         }
    247         return policy;
    248     }
    249 
    250     private static String readSeinfoTag(XmlPullParser parser) throws
    251             IOException, XmlPullParserException {
    252 
    253         int type;
    254         int outerDepth = parser.getDepth();
    255         String seinfo = null;
    256         while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
    257                && (type != XmlPullParser.END_TAG
    258                    || parser.getDepth() > outerDepth)) {
    259             if (type == XmlPullParser.END_TAG
    260                 || type == XmlPullParser.TEXT) {
    261                 continue;
    262             }
    263 
    264             String tagName = parser.getName();
    265             if ("seinfo".equals(tagName)) {
    266                 seinfo = parseSeinfo(parser);
    267             }
    268             XmlUtils.skipCurrentTag(parser);
    269         }
    270         return seinfo;
    271     }
    272 
    273     private static String parseSeinfo(XmlPullParser parser) {
    274 
    275         String seinfoValue = parser.getAttributeValue(null, "value");
    276         if (!validateValue(seinfoValue)) {
    277             Slog.w(TAG, "<seinfo> without valid value at "
    278                    + parser.getPositionDescription());
    279             seinfoValue = null;
    280         }
    281         return seinfoValue;
    282     }
    283 
    284     /**
    285      * General validation routine for package names.
    286      * Returns a boolean indicating if the passed string
    287      * is a valid android package name.
    288      */
    289     private static boolean validatePackageName(String name) {
    290         if (name == null)
    291             return false;
    292 
    293         final int N = name.length();
    294         boolean hasSep = false;
    295         boolean front = true;
    296         for (int i=0; i<N; i++) {
    297             final char c = name.charAt(i);
    298             if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
    299                 front = false;
    300                 continue;
    301             }
    302             if (!front) {
    303                 if ((c >= '0' && c <= '9') || c == '_') {
    304                     continue;
    305                 }
    306             }
    307             if (c == '.') {
    308                 hasSep = true;
    309                 front = true;
    310                 continue;
    311             }
    312             return false;
    313         }
    314         return hasSep;
    315     }
    316 
    317     /**
    318      * General validation routine for tag values.
    319      * Returns a boolean indicating if the passed string
    320      * contains only letters or underscores.
    321      */
    322     private static boolean validateValue(String name) {
    323         if (name == null)
    324             return false;
    325 
    326         final int N = name.length();
    327         if (N == 0)
    328             return false;
    329 
    330         for (int i = 0; i < N; i++) {
    331             final char c = name.charAt(i);
    332             if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c != '_')) {
    333                 return false;
    334             }
    335         }
    336         return true;
    337     }
    338 
    339     /**
    340      * Labels a package based on an seinfo tag from install policy.
    341      * The label is attached to the ApplicationInfo instance of the package.
    342      * @param pkg object representing the package to be labeled.
    343      * @return boolean which determines whether a non null seinfo label
    344      *         was assigned to the package. A null value simply meaning that
    345      *         no policy matched.
    346      */
    347     public static boolean assignSeinfoValue(PackageParser.Package pkg) {
    348 
    349         // We just want one of the signatures to match.
    350         for (Signature s : pkg.mSignatures) {
    351             if (s == null)
    352                 continue;
    353 
    354             Policy policy = sSigSeinfo.get(s);
    355             if (policy != null) {
    356                 String seinfo = policy.checkPolicy(pkg.packageName);
    357                 if (seinfo != null) {
    358                     pkg.applicationInfo.seinfo = seinfo;
    359                     if (DEBUG_POLICY_INSTALL)
    360                         Slog.i(TAG, "package (" + pkg.packageName +
    361                                ") labeled with seinfo=" + seinfo);
    362 
    363                     return true;
    364                 }
    365             }
    366         }
    367 
    368         // If we have a default seinfo value then great, otherwise
    369         // we set a null object and that is what we started with.
    370         pkg.applicationInfo.seinfo = sDefaultSeinfo;
    371         if (DEBUG_POLICY_INSTALL)
    372             Slog.i(TAG, "package (" + pkg.packageName + ") labeled with seinfo="
    373                    + (sDefaultSeinfo == null ? "null" : sDefaultSeinfo));
    374 
    375         return (sDefaultSeinfo != null);
    376     }
    377 
    378     /**
    379      * Determines if a recursive restorecon on /data/data and /data/user is needed.
    380      * It does this by comparing the SHA-1 of the seapp_contexts file against the
    381      * stored hash at /data/system/seapp_hash.
    382      *
    383      * @return Returns true if the restorecon should occur or false otherwise.
    384      */
    385     public static boolean shouldRestorecon() {
    386         // Any error with the seapp_contexts file should be fatal
    387         byte[] currentHash = null;
    388         try {
    389             currentHash = returnHash(SEAPP_CONTEXTS);
    390         } catch (IOException ioe) {
    391             Slog.e(TAG, "Error with hashing seapp_contexts.", ioe);
    392             return false;
    393         }
    394 
    395         // Push past any error with the stored hash file
    396         byte[] storedHash = null;
    397         try {
    398             storedHash = IoUtils.readFileAsByteArray(SEAPP_HASH_FILE);
    399         } catch (IOException ioe) {
    400             Slog.w(TAG, "Error opening " + SEAPP_HASH_FILE + ". Assuming first boot.");
    401         }
    402 
    403         return (storedHash == null || !MessageDigest.isEqual(storedHash, currentHash));
    404     }
    405 
    406     /**
    407      * Stores the SHA-1 of the seapp_contexts to /data/system/seapp_hash.
    408      */
    409     public static void setRestoreconDone() {
    410         try {
    411             final byte[] currentHash = returnHash(SEAPP_CONTEXTS);
    412             dumpHash(new File(SEAPP_HASH_FILE), currentHash);
    413         } catch (IOException ioe) {
    414             Slog.e(TAG, "Error with saving hash to " + SEAPP_HASH_FILE, ioe);
    415         }
    416     }
    417 
    418     /**
    419      * Dump the contents of a byte array to a specified file.
    420      *
    421      * @param file The file that receives the byte array content.
    422      * @param content A byte array that will be written to the specified file.
    423      * @throws IOException if any failed I/O operation occured.
    424      *         Included is the failure to atomically rename the tmp
    425      *         file used in the process.
    426      */
    427     private static void dumpHash(File file, byte[] content) throws IOException {
    428         FileOutputStream fos = null;
    429         File tmp = null;
    430         try {
    431             tmp = File.createTempFile("seapp_hash", ".journal", file.getParentFile());
    432             tmp.setReadable(true);
    433             fos = new FileOutputStream(tmp);
    434             fos.write(content);
    435             fos.getFD().sync();
    436             if (!tmp.renameTo(file)) {
    437                 throw new IOException("Failure renaming " + file.getCanonicalPath());
    438             }
    439         } finally {
    440             if (tmp != null) {
    441                 tmp.delete();
    442             }
    443             IoUtils.closeQuietly(fos);
    444         }
    445     }
    446 
    447     /**
    448      * Return the SHA-1 of a file.
    449      *
    450      * @param file The path to the file given as a string.
    451      * @return Returns the SHA-1 of the file as a byte array.
    452      * @throws IOException if any failed I/O operations occured.
    453      */
    454     private static byte[] returnHash(String file) throws IOException {
    455         try {
    456             final byte[] contents = IoUtils.readFileAsByteArray(file);
    457             return MessageDigest.getInstance("SHA-1").digest(contents);
    458         } catch (NoSuchAlgorithmException nsae) {
    459             throw new RuntimeException(nsae);  // impossible
    460         }
    461     }
    462 
    463     private static boolean useOverridePolicy() {
    464         try {
    465             final String overrideVersion = IoUtils.readFileAsString(DATA_VERSION_FILE);
    466             final String baseVersion = IoUtils.readFileAsString(BASE_VERSION_FILE);
    467             if (overrideVersion.equals(baseVersion)) {
    468                 return true;
    469             }
    470             Slog.e(TAG, "Override policy version '" + overrideVersion + "' doesn't match " +
    471                    "base version '" + baseVersion + "'. Skipping override policy files.");
    472         } catch (FileNotFoundException fnfe) {
    473             // Override version file doesn't have to exist so silently ignore.
    474         } catch (IOException ioe) {
    475             Slog.w(TAG, "Skipping override policy files.", ioe);
    476         }
    477         return false;
    478     }
    479 }
    480