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