blob: d70c725c899091563c8241a7375a7ee9ae34a7f7 [file] [log] [blame]
/*
* 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.content.pm.ApplicationInfo;
import android.content.pm.PackageParser;
import android.content.pm.Signature;
import android.os.Environment;
import android.util.Slog;
import android.util.Xml;
import com.android.internal.util.XmlUtils;
import libcore.io.IoUtils;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
/**
* Centralized access to SELinux MMAC (middleware MAC) implementation.
* {@hide}
*/
public final class SELinuxMMAC {
private static final String TAG = "SELinuxMMAC";
private static final boolean DEBUG_POLICY = false;
private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false;
// Signature seinfo values read from policy.
private static HashMap<Signature, Policy> sSigSeinfo =
new HashMap<Signature, Policy>();
// Default seinfo read from policy.
private static String sDefaultSeinfo = null;
// Locations of potential install policy files.
private static final File[] INSTALL_POLICY_FILE = {
new File(Environment.getDataDirectory(), "security/mac_permissions.xml"),
new File(Environment.getRootDirectory(), "etc/security/mac_permissions.xml"),
null};
// Location of seapp_contexts policy file.
private static final String SEAPP_CONTEXTS_FILE = "/seapp_contexts";
// Stores the hash of the last used seapp_contexts file.
private static final String SEAPP_HASH_FILE =
Environment.getDataDirectory().toString() + "/system/seapp_hash";
// Signature policy stanzas
static class Policy {
private String seinfo;
private final HashMap<String, String> pkgMap;
Policy() {
seinfo = null;
pkgMap = new HashMap<String, String>();
}
void putSeinfo(String seinfoValue) {
seinfo = seinfoValue;
}
void putPkg(String pkg, String seinfoValue) {
pkgMap.put(pkg, seinfoValue);
}
// Valid policy stanza means there exists a global
// seinfo value or at least one package policy.
boolean isValid() {
return (seinfo != null) || (!pkgMap.isEmpty());
}
String checkPolicy(String pkgName) {
// Check for package name seinfo value first.
String seinfoValue = pkgMap.get(pkgName);
if (seinfoValue != null) {
return seinfoValue;
}
// Return the global seinfo value.
return seinfo;
}
}
private static void flushInstallPolicy() {
sSigSeinfo.clear();
sDefaultSeinfo = null;
}
/**
* Parses an MMAC install policy from a predefined list of locations.
* @return boolean indicating whether an install policy was correctly parsed.
*/
public static boolean readInstallPolicy() {
return readInstallPolicy(INSTALL_POLICY_FILE);
}
/**
* Parses an MMAC install policy given as an argument.
* @param policyFile object representing the path of the policy.
* @return boolean indicating whether the install policy was correctly parsed.
*/
public static boolean readInstallPolicy(File policyFile) {
return readInstallPolicy(new File[]{policyFile,null});
}
private static boolean readInstallPolicy(File[] policyFiles) {
// Temp structures to hold the rules while we parse the xml file.
// We add all the rules together once we know there's no structural problems.
HashMap<Signature, Policy> sigSeinfo = new HashMap<Signature, Policy>();
String defaultSeinfo = null;
FileReader policyFile = null;
int i = 0;
while (policyFile == null && policyFiles != null && policyFiles[i] != null) {
try {
policyFile = new FileReader(policyFiles[i]);
break;
} catch (FileNotFoundException e) {
Slog.d(TAG,"Couldn't find install policy " + policyFiles[i].getPath());
}
i++;
}
if (policyFile == null) {
Slog.d(TAG, "No policy file found. All seinfo values will be null.");
return false;
}
Slog.d(TAG, "Using install policy file " + policyFiles[i].getPath());
try {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(policyFile);
XmlUtils.beginDocument(parser, "policy");
while (true) {
XmlUtils.nextElement(parser);
if (parser.getEventType() == XmlPullParser.END_DOCUMENT) {
break;
}
String tagName = parser.getName();
if ("signer".equals(tagName)) {
String cert = parser.getAttributeValue(null, "signature");
if (cert == null) {
Slog.w(TAG, "<signer> without signature at "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
Signature signature;
try {
signature = new Signature(cert);
} catch (IllegalArgumentException e) {
Slog.w(TAG, "<signer> with bad signature at "
+ parser.getPositionDescription(), e);
XmlUtils.skipCurrentTag(parser);
continue;
}
Policy policy = readPolicyTags(parser);
if (policy.isValid()) {
sigSeinfo.put(signature, policy);
}
} else if ("default".equals(tagName)) {
// Value is null if default tag is absent or seinfo tag is malformed.
defaultSeinfo = readSeinfoTag(parser);
if (DEBUG_POLICY_INSTALL)
Slog.i(TAG, "<default> tag assigned seinfo=" + defaultSeinfo);
} else {
XmlUtils.skipCurrentTag(parser);
}
}
} catch (XmlPullParserException e) {
// An error outside of a stanza means a structural problem
// with the xml file. So ignore it.
Slog.w(TAG, "Got exception parsing ", e);
return false;
} catch (IOException e) {
Slog.w(TAG, "Got exception parsing ", e);
return false;
} finally {
try {
policyFile.close();
} catch (IOException e) {
//omit
}
}
flushInstallPolicy();
sSigSeinfo = sigSeinfo;
sDefaultSeinfo = defaultSeinfo;
return true;
}
private static Policy readPolicyTags(XmlPullParser parser) throws
IOException, XmlPullParserException {
int type;
int outerDepth = parser.getDepth();
Policy policy = new Policy();
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG
|| parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG
|| type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if ("seinfo".equals(tagName)) {
String seinfo = parseSeinfo(parser);
if (seinfo != null) {
policy.putSeinfo(seinfo);
}
XmlUtils.skipCurrentTag(parser);
} else if ("package".equals(tagName)) {
String pkg = parser.getAttributeValue(null, "name");
if (!validatePackageName(pkg)) {
Slog.w(TAG, "<package> without valid name at "
+ parser.getPositionDescription());
XmlUtils.skipCurrentTag(parser);
continue;
}
String seinfo = readSeinfoTag(parser);
if (seinfo != null) {
policy.putPkg(pkg, seinfo);
}
} else {
XmlUtils.skipCurrentTag(parser);
}
}
return policy;
}
private static String readSeinfoTag(XmlPullParser parser) throws
IOException, XmlPullParserException {
int type;
int outerDepth = parser.getDepth();
String seinfo = null;
while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
&& (type != XmlPullParser.END_TAG
|| parser.getDepth() > outerDepth)) {
if (type == XmlPullParser.END_TAG
|| type == XmlPullParser.TEXT) {
continue;
}
String tagName = parser.getName();
if ("seinfo".equals(tagName)) {
seinfo = parseSeinfo(parser);
}
XmlUtils.skipCurrentTag(parser);
}
return seinfo;
}
private static String parseSeinfo(XmlPullParser parser) {
String seinfoValue = parser.getAttributeValue(null, "value");
if (!validateValue(seinfoValue)) {
Slog.w(TAG, "<seinfo> without valid value at "
+ parser.getPositionDescription());
seinfoValue = null;
}
return seinfoValue;
}
/**
* General validation routine for package names.
* Returns a boolean indicating if the passed string
* is a valid android package name.
*/
private static boolean validatePackageName(String name) {
if (name == null)
return false;
final int N = name.length();
boolean hasSep = false;
boolean front = true;
for (int i=0; i<N; i++) {
final char c = name.charAt(i);
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) {
front = false;
continue;
}
if (!front) {
if ((c >= '0' && c <= '9') || c == '_') {
continue;
}
}
if (c == '.') {
hasSep = true;
front = true;
continue;
}
return false;
}
return hasSep;
}
/**
* General validation routine for tag values.
* Returns a boolean indicating if the passed string
* contains only letters or underscores.
*/
private static boolean validateValue(String name) {
if (name == null)
return false;
final int N = name.length();
if (N == 0)
return false;
for (int i = 0; i < N; i++) {
final char c = name.charAt(i);
if ((c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c != '_')) {
return false;
}
}
return true;
}
/**
* Labels a package based on an seinfo tag from install policy.
* The label is attached to the ApplicationInfo instance of the package.
* @param pkg object representing the package to be labeled.
* @return boolean which determines whether a non null seinfo label
* was assigned to the package. A null value simply meaning that
* no policy matched.
*/
public static boolean assignSeinfoValue(PackageParser.Package pkg) {
/*
* Non system installed apps should be treated the same. This
* means that any post-loaded apk will be assigned the default
* tag, if one exists in the policy, else null, without respect
* to the signing key.
*/
if (((pkg.applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0) ||
((pkg.applicationInfo.flags & ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0)) {
// We just want one of the signatures to match.
for (Signature s : pkg.mSignatures) {
if (s == null)
continue;
Policy policy = sSigSeinfo.get(s);
if (policy != null) {
String seinfo = policy.checkPolicy(pkg.packageName);
if (seinfo != null) {
pkg.applicationInfo.seinfo = seinfo;
if (DEBUG_POLICY_INSTALL)
Slog.i(TAG, "package (" + pkg.packageName +
") labeled with seinfo=" + seinfo);
return true;
}
}
}
}
// If we have a default seinfo value then great, otherwise
// we set a null object and that is what we started with.
pkg.applicationInfo.seinfo = sDefaultSeinfo;
if (DEBUG_POLICY_INSTALL)
Slog.i(TAG, "package (" + pkg.packageName + ") labeled with seinfo="
+ (sDefaultSeinfo == null ? "null" : sDefaultSeinfo));
return (sDefaultSeinfo != null);
}
/**
* Determines if a recursive restorecon on /data/data and /data/user is needed.
* It does this by comparing the SHA-1 of the seapp_contexts file against the
* stored hash at /data/system/seapp_hash.
*
* @return Returns true if the restorecon should occur or false otherwise.
*/
public static boolean shouldRestorecon() {
// Any error with the seapp_contexts file should be fatal
byte[] currentHash = null;
try {
currentHash = returnHash(SEAPP_CONTEXTS_FILE);
} catch (IOException ioe) {
Slog.e(TAG, "Error with hashing seapp_contexts.", ioe);
return false;
}
// Push past any error with the stored hash file
byte[] storedHash = null;
try {
storedHash = IoUtils.readFileAsByteArray(SEAPP_HASH_FILE);
} catch (IOException ioe) {
Slog.w(TAG, "Error opening " + SEAPP_HASH_FILE + ". Assuming first boot.");
}
return (storedHash == null || !MessageDigest.isEqual(storedHash, currentHash));
}
/**
* Stores the SHA-1 of the seapp_contexts to /data/system/seapp_hash.
*/
public static void setRestoreconDone() {
try {
final byte[] currentHash = returnHash(SEAPP_CONTEXTS_FILE);
dumpHash(new File(SEAPP_HASH_FILE), currentHash);
} catch (IOException ioe) {
Slog.e(TAG, "Error with saving hash to " + SEAPP_HASH_FILE, ioe);
}
}
/**
* Dump the contents of a byte array to a specified file.
*
* @param file The file that receives the byte array content.
* @param content A byte array that will be written to the specified file.
* @throws IOException if any failed I/O operation occured.
* Included is the failure to atomically rename the tmp
* file used in the process.
*/
private static void dumpHash(File file, byte[] content) throws IOException {
FileOutputStream fos = null;
File tmp = null;
try {
tmp = File.createTempFile("seapp_hash", ".journal", file.getParentFile());
tmp.setReadable(true);
fos = new FileOutputStream(tmp);
fos.write(content);
fos.getFD().sync();
if (!tmp.renameTo(file)) {
throw new IOException("Failure renaming " + file.getCanonicalPath());
}
} finally {
if (tmp != null) {
tmp.delete();
}
IoUtils.closeQuietly(fos);
}
}
/**
* Return the SHA-1 of a file.
*
* @param file The path to the file given as a string.
* @return Returns the SHA-1 of the file as a byte array.
* @throws IOException if any failed I/O operations occured.
*/
private static byte[] returnHash(String file) throws IOException {
try {
final byte[] contents = IoUtils.readFileAsByteArray(file);
return MessageDigest.getInstance("SHA-1").digest(contents);
} catch (NoSuchAlgorithmException nsae) {
throw new RuntimeException(nsae); // impossible
}
}
}