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.HashMap;
39
40import org.xmlpull.v1.XmlPullParser;
41import org.xmlpull.v1.XmlPullParserException;
42
43/**
44 * Centralized access to SELinux MMAC (middleware MAC) implementation.
45 * {@hide}
46 */
47public 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