• Home
  • History
  • Annotate
  • 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.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 
Policy()103         Policy() {
104             seinfo = null;
105             pkgMap = new HashMap<String, String>();
106         }
107 
putSeinfo(String seinfoValue)108         void putSeinfo(String seinfoValue) {
109             seinfo = seinfoValue;
110         }
111 
putPkg(String pkg, String seinfoValue)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.
isValid()118         boolean isValid() {
119             return (seinfo != null) || (!pkgMap.isEmpty());
120         }
121 
checkPolicy(String pkgName)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 
flushInstallPolicy()134     private static void flushInstallPolicy() {
135         sSigSeinfo.clear();
136         sDefaultSeinfo = null;
137     }
138 
readInstallPolicy()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 
readPolicyTags(XmlPullParser parser)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 
readSeinfoTag(XmlPullParser parser)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 
parseSeinfo(XmlPullParser parser)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      */
validatePackageName(String name)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      */
validateValue(String name)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      */
assignSeinfoValue(PackageParser.Package pkg)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      */
shouldRestorecon()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      */
setRestoreconDone()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      */
dumpHash(File file, byte[] content)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      */
returnHash(String file)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 
useOverridePolicy()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