SELinuxMMAC.java revision 2e1f052f45cd0f3b0b52a7eae2f05da770702cb0
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.ArrayList; 39import java.util.Collections; 40import java.util.HashMap; 41import java.util.HashSet; 42import java.util.List; 43import java.util.Map; 44import java.util.Set; 45 46import org.xmlpull.v1.XmlPullParser; 47import org.xmlpull.v1.XmlPullParserException; 48 49/** 50 * Centralized access to SELinux MMAC (middleware MAC) implementation. This 51 * class is responsible for loading the appropriate mac_permissions.xml file 52 * as well as providing an interface for assigning seinfo values to apks. 53 * 54 * {@hide} 55 */ 56public final class SELinuxMMAC { 57 58 private static final String TAG = "SELinuxMMAC"; 59 60 private static final boolean DEBUG_POLICY = false; 61 private static final boolean DEBUG_POLICY_INSTALL = DEBUG_POLICY || false; 62 63 // All policy stanzas read from mac_permissions.xml. This is also the lock 64 // to synchronize access during policy load and access attempts. 65 private static final List<Policy> sPolicies = new ArrayList<Policy>(); 66 67 // Data policy override version file. 68 private static final String DATA_VERSION_FILE = 69 Environment.getDataDirectory() + "/security/current/selinux_version"; 70 71 // Base policy version file. 72 private static final String BASE_VERSION_FILE = "/selinux_version"; 73 74 // Whether override security policies should be loaded. 75 private static final boolean USE_OVERRIDE_POLICY = useOverridePolicy(); 76 77 // Data override mac_permissions.xml policy file. 78 private static final String DATA_MAC_PERMISSIONS = 79 Environment.getDataDirectory() + "/security/current/mac_permissions.xml"; 80 81 // Base mac_permissions.xml policy file. 82 private static final String BASE_MAC_PERMISSIONS = 83 Environment.getRootDirectory() + "/etc/security/mac_permissions.xml"; 84 85 // Determine which mac_permissions.xml file to use. 86 private static final String MAC_PERMISSIONS = USE_OVERRIDE_POLICY ? 87 DATA_MAC_PERMISSIONS : BASE_MAC_PERMISSIONS; 88 89 // Data override seapp_contexts policy file. 90 private static final String DATA_SEAPP_CONTEXTS = 91 Environment.getDataDirectory() + "/security/current/seapp_contexts"; 92 93 // Base seapp_contexts policy file. 94 private static final String BASE_SEAPP_CONTEXTS = "/seapp_contexts"; 95 96 // Determine which seapp_contexts file to use. 97 private static final String SEAPP_CONTEXTS = USE_OVERRIDE_POLICY ? 98 DATA_SEAPP_CONTEXTS : BASE_SEAPP_CONTEXTS; 99 100 // Stores the hash of the last used seapp_contexts file. 101 private static final String SEAPP_HASH_FILE = 102 Environment.getDataDirectory().toString() + "/system/seapp_hash"; 103 104 /** 105 * Load the mac_permissions.xml file containing all seinfo assignments used to 106 * label apps. The loaded mac_permissions.xml file is determined by the 107 * MAC_PERMISSIONS class variable which is set at class load time which itself 108 * is based on the USE_OVERRIDE_POLICY class variable. For further guidance on 109 * the proper structure of a mac_permissions.xml file consult the source code 110 * located at external/sepolicy/mac_permissions.xml. 111 * 112 * @return boolean indicating if policy was correctly loaded. A value of false 113 * typically indicates a structural problem with the xml or incorrectly 114 * constructed policy stanzas. A value of true means that all stanzas 115 * were loaded successfully; no partial loading is possible. 116 */ 117 public static boolean readInstallPolicy() { 118 // Temp structure to hold the rules while we parse the xml file. We add 119 // all the rules once we know there's no problems. 120 List<Policy> policies = new ArrayList<>(); 121 122 // A separate structure to hold the default stanza. We need to add this to 123 // the end of the policies list structure. 124 Policy defaultPolicy = null; 125 126 // Track sets of known policy certs so we can enforce rules across stanzas. 127 Set<Set<Signature>> knownCerts = new HashSet<>(); 128 129 FileReader policyFile = null; 130 XmlPullParser parser = Xml.newPullParser(); 131 try { 132 policyFile = new FileReader(MAC_PERMISSIONS); 133 Slog.d(TAG, "Using policy file " + MAC_PERMISSIONS); 134 135 parser.setInput(policyFile); 136 parser.nextTag(); 137 parser.require(XmlPullParser.START_TAG, null, "policy"); 138 139 while (parser.next() != XmlPullParser.END_TAG) { 140 if (parser.getEventType() != XmlPullParser.START_TAG) { 141 continue; 142 } 143 144 String tagName = parser.getName(); 145 if ("signer".equals(tagName)) { 146 Policy signerPolicy = readSignerOrThrow(parser); 147 // Return of a Policy instance ensures certain invariants have 148 // passed, however, we still want to do some cross policy checking. 149 // Thus, check that we haven't seen the certs in another stanza. 150 Set<Signature> certs = signerPolicy.getSignatures(); 151 if (knownCerts.contains(certs)) { 152 String msg = "Separate stanzas have identical certs"; 153 throw new IllegalStateException(msg); 154 } 155 knownCerts.add(certs); 156 policies.add(signerPolicy); 157 } else if ("default".equals(tagName)) { 158 Policy defPolicy = readDefaultOrThrow(parser); 159 // Return of a Policy instance ensures certain invariants have 160 // passed, however, we still want to do some cross policy checking. 161 // Thus, check that we haven't already seen a default stanza. 162 if (defaultPolicy != null) { 163 String msg = "Multiple default stanzas identified"; 164 throw new IllegalStateException(msg); 165 } 166 defaultPolicy = defPolicy; 167 } else { 168 skip(parser); 169 } 170 } 171 } catch (IllegalStateException | IllegalArgumentException | 172 XmlPullParserException ex) { 173 StringBuilder sb = new StringBuilder("Exception @"); 174 sb.append(parser.getPositionDescription()); 175 sb.append(" while parsing "); 176 sb.append(MAC_PERMISSIONS); 177 sb.append(":"); 178 sb.append(ex); 179 Slog.w(TAG, sb.toString()); 180 return false; 181 } catch (IOException ioe) { 182 Slog.w(TAG, "Exception parsing " + MAC_PERMISSIONS, ioe); 183 return false; 184 } finally { 185 IoUtils.closeQuietly(policyFile); 186 } 187 188 // Add the default policy to the end if there is one. This will ensure that 189 // the default stanza is consulted last when performing policy lookups. 190 if (defaultPolicy != null) { 191 policies.add(defaultPolicy); 192 } 193 194 synchronized (sPolicies) { 195 sPolicies.clear(); 196 sPolicies.addAll(policies); 197 } 198 199 return true; 200 } 201 202 /** 203 * Loop over a signer tag looking for seinfo, package and cert tags. A {@link Policy} 204 * instance will be created and returned in the process. During the pass all other 205 * tag elements will be skipped. 206 * 207 * @param parser an XmlPullParser object representing a signer element. 208 * @return the constructed {@link Policy} instance 209 * @throws IOException 210 * @throws XmlPullParserException 211 * @throws IllegalArgumentException if any of the validation checks fail while 212 * parsing tag values. 213 * @throws IllegalStateException if any of the invariants fail when constructing 214 * the {@link Policy} instance. 215 */ 216 private static Policy readSignerOrThrow(XmlPullParser parser) throws IOException, 217 XmlPullParserException { 218 219 parser.require(XmlPullParser.START_TAG, null, "signer"); 220 Policy.PolicyBuilder pb = new Policy.PolicyBuilder(); 221 222 // Check for a cert attached to the signer tag. We allow a signature 223 // to appear as an attribute as well as those attached to cert tags. 224 String cert = parser.getAttributeValue(null, "signature"); 225 if (cert != null) { 226 pb.addSignature(cert); 227 } 228 229 while (parser.next() != XmlPullParser.END_TAG) { 230 if (parser.getEventType() != XmlPullParser.START_TAG) { 231 continue; 232 } 233 234 String tagName = parser.getName(); 235 if ("seinfo".equals(tagName)) { 236 String seinfo = parser.getAttributeValue(null, "value"); 237 pb.setGlobalSeinfoOrThrow(seinfo); 238 readSeinfo(parser); 239 } else if ("package".equals(tagName)) { 240 readPackageOrThrow(parser, pb); 241 } else if ("cert".equals(tagName)) { 242 String sig = parser.getAttributeValue(null, "signature"); 243 pb.addSignature(sig); 244 readCert(parser); 245 } else { 246 skip(parser); 247 } 248 } 249 250 return pb.build(); 251 } 252 253 /** 254 * Loop over a default element looking for seinfo child tags. A {@link Policy} 255 * instance will be created and returned in the process. All other tags encountered 256 * will be skipped. 257 * 258 * @param parser an XmlPullParser object representing a default element. 259 * @return the constructed {@link Policy} instance 260 * @throws IOException 261 * @throws XmlPullParserException 262 * @throws IllegalArgumentException if any of the validation checks fail while 263 * parsing tag values. 264 * @throws IllegalStateException if any of the invariants fail when constructing 265 * the {@link Policy} instance. 266 */ 267 private static Policy readDefaultOrThrow(XmlPullParser parser) throws IOException, 268 XmlPullParserException { 269 270 parser.require(XmlPullParser.START_TAG, null, "default"); 271 Policy.PolicyBuilder pb = new Policy.PolicyBuilder(); 272 pb.setAsDefaultPolicy(); 273 274 while (parser.next() != XmlPullParser.END_TAG) { 275 if (parser.getEventType() != XmlPullParser.START_TAG) { 276 continue; 277 } 278 279 String tagName = parser.getName(); 280 if ("seinfo".equals(tagName)) { 281 String seinfo = parser.getAttributeValue(null, "value"); 282 pb.setGlobalSeinfoOrThrow(seinfo); 283 readSeinfo(parser); 284 } else { 285 skip(parser); 286 } 287 } 288 289 return pb.build(); 290 } 291 292 /** 293 * Loop over a package element looking for seinfo child tags. If found return the 294 * value attribute of the seinfo tag, otherwise return null. All other tags encountered 295 * will be skipped. 296 * 297 * @param parser an XmlPullParser object representing a package element. 298 * @param pb a Policy.PolicyBuilder instance to build 299 * @throws IOException 300 * @throws XmlPullParserException 301 * @throws IllegalArgumentException if any of the validation checks fail while 302 * parsing tag values. 303 * @throws IllegalStateException if there is a duplicate seinfo tag for the current 304 * package tag. 305 */ 306 private static void readPackageOrThrow(XmlPullParser parser, Policy.PolicyBuilder pb) throws 307 IOException, XmlPullParserException { 308 parser.require(XmlPullParser.START_TAG, null, "package"); 309 String pkgName = parser.getAttributeValue(null, "name"); 310 311 while (parser.next() != XmlPullParser.END_TAG) { 312 if (parser.getEventType() != XmlPullParser.START_TAG) { 313 continue; 314 } 315 316 String tagName = parser.getName(); 317 if ("seinfo".equals(tagName)) { 318 String seinfo = parser.getAttributeValue(null, "value"); 319 pb.addInnerPackageMapOrThrow(pkgName, seinfo); 320 readSeinfo(parser); 321 } else { 322 skip(parser); 323 } 324 } 325 } 326 327 private static void readCert(XmlPullParser parser) throws IOException, 328 XmlPullParserException { 329 parser.require(XmlPullParser.START_TAG, null, "cert"); 330 parser.nextTag(); 331 } 332 333 private static void readSeinfo(XmlPullParser parser) throws IOException, 334 XmlPullParserException { 335 parser.require(XmlPullParser.START_TAG, null, "seinfo"); 336 parser.nextTag(); 337 } 338 339 private static void skip(XmlPullParser p) throws IOException, XmlPullParserException { 340 if (p.getEventType() != XmlPullParser.START_TAG) { 341 throw new IllegalStateException(); 342 } 343 int depth = 1; 344 while (depth != 0) { 345 switch (p.next()) { 346 case XmlPullParser.END_TAG: 347 depth--; 348 break; 349 case XmlPullParser.START_TAG: 350 depth++; 351 break; 352 } 353 } 354 } 355 356 /** 357 * Applies a security label to a package based on an seinfo tag taken from a matched 358 * policy. All signature based policy stanzas are consulted first and, if no match 359 * is found, the default policy stanza is then consulted. The security label is 360 * attached to the ApplicationInfo instance of the package in the event that a matching 361 * policy was found. 362 * 363 * @param pkg object representing the package to be labeled. 364 * @return boolean which determines whether a non null seinfo label was assigned 365 * to the package. A null value simply represents that no policy matched. 366 */ 367 public static boolean assignSeinfoValue(PackageParser.Package pkg) { 368 synchronized (sPolicies) { 369 for (Policy policy : sPolicies) { 370 String seinfo = policy.getMatchedSeinfo(pkg); 371 if (seinfo != null) { 372 pkg.applicationInfo.seinfo = seinfo; 373 if (DEBUG_POLICY_INSTALL) { 374 Slog.i(TAG, "package (" + pkg.packageName + ") labeled with " + 375 "seinfo=" + seinfo); 376 } 377 return true; 378 } 379 } 380 } 381 382 if (DEBUG_POLICY_INSTALL) { 383 Slog.i(TAG, "package (" + pkg.packageName + ") doesn't match any policy; " + 384 "seinfo will remain null"); 385 } 386 return false; 387 } 388 389 /** 390 * Determines if a recursive restorecon on /data/data and /data/user is needed. 391 * It does this by comparing the SHA-1 of the seapp_contexts file against the 392 * stored hash at /data/system/seapp_hash. 393 * 394 * @return Returns true if the restorecon should occur or false otherwise. 395 */ 396 public static boolean shouldRestorecon() { 397 // Any error with the seapp_contexts file should be fatal 398 byte[] currentHash = null; 399 try { 400 currentHash = returnHash(SEAPP_CONTEXTS); 401 } catch (IOException ioe) { 402 Slog.e(TAG, "Error with hashing seapp_contexts.", ioe); 403 return false; 404 } 405 406 // Push past any error with the stored hash file 407 byte[] storedHash = null; 408 try { 409 storedHash = IoUtils.readFileAsByteArray(SEAPP_HASH_FILE); 410 } catch (IOException ioe) { 411 Slog.w(TAG, "Error opening " + SEAPP_HASH_FILE + ". Assuming first boot."); 412 } 413 414 return (storedHash == null || !MessageDigest.isEqual(storedHash, currentHash)); 415 } 416 417 /** 418 * Stores the SHA-1 of the seapp_contexts to /data/system/seapp_hash. 419 */ 420 public static void setRestoreconDone() { 421 try { 422 final byte[] currentHash = returnHash(SEAPP_CONTEXTS); 423 dumpHash(new File(SEAPP_HASH_FILE), currentHash); 424 } catch (IOException ioe) { 425 Slog.e(TAG, "Error with saving hash to " + SEAPP_HASH_FILE, ioe); 426 } 427 } 428 429 /** 430 * Dump the contents of a byte array to a specified file. 431 * 432 * @param file The file that receives the byte array content. 433 * @param content A byte array that will be written to the specified file. 434 * @throws IOException if any failed I/O operation occured. 435 * Included is the failure to atomically rename the tmp 436 * file used in the process. 437 */ 438 private static void dumpHash(File file, byte[] content) throws IOException { 439 FileOutputStream fos = null; 440 File tmp = null; 441 try { 442 tmp = File.createTempFile("seapp_hash", ".journal", file.getParentFile()); 443 tmp.setReadable(true); 444 fos = new FileOutputStream(tmp); 445 fos.write(content); 446 fos.getFD().sync(); 447 if (!tmp.renameTo(file)) { 448 throw new IOException("Failure renaming " + file.getCanonicalPath()); 449 } 450 } finally { 451 if (tmp != null) { 452 tmp.delete(); 453 } 454 IoUtils.closeQuietly(fos); 455 } 456 } 457 458 /** 459 * Return the SHA-1 of a file. 460 * 461 * @param file The path to the file given as a string. 462 * @return Returns the SHA-1 of the file as a byte array. 463 * @throws IOException if any failed I/O operations occured. 464 */ 465 private static byte[] returnHash(String file) throws IOException { 466 try { 467 final byte[] contents = IoUtils.readFileAsByteArray(file); 468 return MessageDigest.getInstance("SHA-1").digest(contents); 469 } catch (NoSuchAlgorithmException nsae) { 470 throw new RuntimeException(nsae); // impossible 471 } 472 } 473 474 private static boolean useOverridePolicy() { 475 try { 476 final String overrideVersion = IoUtils.readFileAsString(DATA_VERSION_FILE); 477 final String baseVersion = IoUtils.readFileAsString(BASE_VERSION_FILE); 478 if (overrideVersion.equals(baseVersion)) { 479 return true; 480 } 481 Slog.e(TAG, "Override policy version '" + overrideVersion + "' doesn't match " + 482 "base version '" + baseVersion + "'. Skipping override policy files."); 483 } catch (FileNotFoundException fnfe) { 484 // Override version file doesn't have to exist so silently ignore. 485 } catch (IOException ioe) { 486 Slog.w(TAG, "Skipping override policy files.", ioe); 487 } 488 return false; 489 } 490} 491 492/** 493 * Holds valid policy representations of individual stanzas from a mac_permissions.xml 494 * file. Each instance can further be used to assign seinfo values to apks using the 495 * {@link Policy#getMatchedSeinfo} method. To create an instance of this use the 496 * {@link PolicyBuilder} pattern class, where each instance is validated against a set 497 * of invariants before being built and returned. Each instance can be guaranteed to 498 * hold one valid policy stanza as outlined in the external/sepolicy/mac_permissions.xml 499 * file. 500 * </p> 501 * The following is an example of how to use {@link Policy.PolicyBuilder} to create a 502 * signer based Policy instance. 503 * </p> 504 * <pre> 505 * {@code 506 * Policy policy = new Policy.PolicyBuilder() 507 * .addSignature("308204a8...") 508 * .addSignature("483538c8...") 509 * .setGlobalSeinfoOrThrow("paltform") 510 * .addInnerPackageMapOrThrow("com.foo.", "bar") 511 * .addInnerPackageMapOrThrow("com.foo.other", "bar") 512 * .build(); 513 * } 514 * </pre> 515 * <p> 516 * An example of how to use {@link Policy.PolicyBuilder} to create a default based Policy 517 * instance. 518 * </p> 519 * <pre> 520 * {@code 521 * Policy policy = new Policy.PolicyBuilder() 522 * .setAsDefaultPolicy() 523 * .setGlobalSeinfoOrThrow("defualt") 524 * .build(); 525 * } 526 * </pre> 527 */ 528final class Policy { 529 530 private final String mSeinfo; 531 private final boolean mDefaultStanza; 532 private final Set<Signature> mCerts; 533 private final Map<String, String> mPkgMap; 534 535 // Use the PolicyBuilder pattern to instantiate 536 private Policy(PolicyBuilder builder) { 537 mSeinfo = builder.mSeinfo; 538 mDefaultStanza = builder.mDefaultStanza; 539 mCerts = Collections.unmodifiableSet(builder.mCerts); 540 mPkgMap = Collections.unmodifiableMap(builder.mPkgMap); 541 } 542 543 /** 544 * Return all the certs stored with this policy stanza. 545 * 546 * @return A set of Signature objects representing all the certs stored 547 * with the policy. 548 */ 549 public Set<Signature> getSignatures() { 550 return mCerts; 551 } 552 553 /** 554 * <p> 555 * Determine the seinfo value to assign to an apk. The appropriate seinfo value 556 * is determined using the following steps: 557 * </p> 558 * <ul> 559 * <li> If this Policy instance is defined as a default stanza: 560 * <ul><li>Return the global seinfo value</li></ul> 561 * </li> 562 * <li> If this Policy instance is defined as a signer stanza: 563 * <ul> 564 * <li> All certs used to sign the apk and all certs stored with this policy 565 * instance are tested for set equality. If this fails then null is returned. 566 * </li> 567 * <li> If all certs match then an appropriate inner package stanza is 568 * searched based on package name alone. If matched, the stored seinfo 569 * value for that mapping is returned. 570 * </li> 571 * <li> If all certs matched and no inner package stanza matches then return 572 * the global seinfo value. The returned value can be null in this case. 573 * </li> 574 * </ul> 575 * </li> 576 * </ul> 577 * <p> 578 * In all cases, a return value of null should be interpreted as the apk failing 579 * to match this Policy instance; i.e. failing this policy stanza. 580 * </p> 581 * @param pkg the apk to check given as a PackageParser.Package object 582 * @return A string representing the seinfo matched during policy lookup. 583 * A value of null can also be returned if no match occured. 584 */ 585 public String getMatchedSeinfo(PackageParser.Package pkg) { 586 if (!mDefaultStanza) { 587 // Check for exact signature matches across all certs. 588 Signature[] certs = mCerts.toArray(new Signature[0]); 589 if (!Signature.areExactMatch(certs, pkg.mSignatures)) { 590 return null; 591 } 592 593 // Check for inner package name matches given that the 594 // signature checks already passed. 595 String seinfoValue = mPkgMap.get(pkg.packageName); 596 if (seinfoValue != null) { 597 return seinfoValue; 598 } 599 } 600 601 // Return the global seinfo value (even if it's null). 602 return mSeinfo; 603 } 604 605 /** 606 * A nested builder class to create {@link Policy} instances. A {@link Policy} 607 * class instance represents one valid policy stanza found in a mac_permissions.xml 608 * file. A valid policy stanza is defined to be either a signer or default stanza 609 * which obeys the rules outlined in external/sepolicy/mac_permissions.xml. The 610 * {@link #build} method ensures a set of invariants are upheld enforcing the correct 611 * stanza structure before returning a valid Policy object. 612 */ 613 public static final class PolicyBuilder { 614 615 private String mSeinfo; 616 private boolean mDefaultStanza; 617 private final Set<Signature> mCerts; 618 private final Map<String, String> mPkgMap; 619 620 public PolicyBuilder() { 621 mCerts = new HashSet<Signature>(2); 622 mPkgMap = new HashMap<String, String>(2); 623 } 624 625 /** 626 * Sets this stanza as a defualt stanza. All policy stanzas are assumed to 627 * be signer stanzas unless this method is explicitly called. Default stanzas 628 * are treated differently with respect to allowable child tags, ordering and 629 * when and how policy decisions are enforced. 630 * 631 * @return The reference to this PolicyBuilder. 632 */ 633 public PolicyBuilder setAsDefaultPolicy() { 634 mDefaultStanza = true; 635 return this; 636 } 637 638 /** 639 * Adds a signature to the set of certs used for validation checks. The purpose 640 * being that all contained certs will need to be matched against all certs 641 * contained with an apk. 642 * 643 * @param cert the signature to add given as a String. 644 * @return The reference to this PolicyBuilder. 645 * @throws IllegalArgumentException if the cert value fails validation; 646 * null or is an invalid hex-encoded ASCII string. 647 */ 648 public PolicyBuilder addSignature(String cert) { 649 if (cert == null) { 650 String err = "Invalid signature value " + cert; 651 throw new IllegalArgumentException(err); 652 } 653 654 mCerts.add(new Signature(cert)); 655 return this; 656 } 657 658 /** 659 * Set the global seinfo tag for this policy stanza. The global seinfo tag 660 * represents the seinfo element that is used in one of two ways depending on 661 * its context. When attached to a signer tag the global seinfo represents an 662 * assignment when there isn't a further inner package refinement in policy. 663 * When used with a default tag, it represents the only allowable assignment 664 * value. 665 * 666 * @param seinfo the seinfo value given as a String. 667 * @return The reference to this PolicyBuilder. 668 * @throws IllegalArgumentException if the seinfo value fails validation; 669 * null, zero length or contains non-valid characters [^a-zA-Z_\._0-9]. 670 * @throws IllegalStateException if an seinfo value has already been found 671 */ 672 public PolicyBuilder setGlobalSeinfoOrThrow(String seinfo) { 673 if (!validateValue(seinfo)) { 674 String err = "Invalid seinfo value " + seinfo; 675 throw new IllegalArgumentException(err); 676 } 677 678 if (mSeinfo != null && !mSeinfo.equals(seinfo)) { 679 String err = "Duplicate seinfo tag found"; 680 throw new IllegalStateException(err); 681 } 682 683 mSeinfo = seinfo; 684 return this; 685 } 686 687 /** 688 * Create a package name to seinfo value mapping. Each mapping represents 689 * the seinfo value that will be assigned to the described package name. 690 * These localized mappings allow the global seinfo to be overriden. This 691 * mapping provides no value when used in conjunction with a default stanza; 692 * enforced through the {@link #build} method. 693 * 694 * @param pkgName the android package name given to the app 695 * @param seinfo the seinfo value that will be assigned to the passed pkgName 696 * @return The reference to this PolicyBuilder. 697 * @throws IllegalArgumentException if the seinfo value fails validation; 698 * null, zero length or contains non-valid characters [^a-zA-Z_\.0-9]. 699 * Or, if the package name isn't a valid android package name. 700 * @throws IllegalStateException if trying to reset a package mapping with a 701 * different seinfo value. 702 */ 703 public PolicyBuilder addInnerPackageMapOrThrow(String pkgName, String seinfo) { 704 if (!validateValue(pkgName)) { 705 String err = "Invalid package name " + pkgName; 706 throw new IllegalArgumentException(err); 707 } 708 if (!validateValue(seinfo)) { 709 String err = "Invalid seinfo value " + seinfo; 710 throw new IllegalArgumentException(err); 711 } 712 713 String pkgValue = mPkgMap.get(pkgName); 714 if (pkgValue != null && !pkgValue.equals(seinfo)) { 715 String err = "Conflicting seinfo value found"; 716 throw new IllegalStateException(err); 717 } 718 719 mPkgMap.put(pkgName, seinfo); 720 return this; 721 } 722 723 /** 724 * General validation routine for the attribute strings of an element. Checks 725 * if the string is non-null, positive length and only contains [a-zA-Z_\.0-9]. 726 * 727 * @param name the string to validate. 728 * @return boolean indicating if the string was valid. 729 */ 730 private boolean validateValue(String name) { 731 if (name == null) 732 return false; 733 734 // Want to match on [0-9a-zA-Z_.] 735 if (!name.matches("\\A[\\.\\w]+\\z")) { 736 return false; 737 } 738 739 return true; 740 } 741 742 /** 743 * <p> 744 * Create a {@link Policy} instance based on the current configuration. This 745 * method checks for certain policy invariants used to enforce certain guarantees 746 * about the expected structure of a policy stanza. 747 * Those invariants are: 748 * </p> 749 * <ul> 750 * <li> If a default stanza 751 * <ul> 752 * <li> an attached global seinfo tag must be present </li> 753 * <li> no signatures and no package names can be present </li> 754 * </ul> 755 * </li> 756 * <li> If a signer stanza 757 * <ul> 758 * <li> at least one cert must be found </li> 759 * <li> either a global seinfo value is present OR at least one 760 * inner package mapping must be present. </li> 761 * </ul> 762 * </li> 763 * </ul> 764 * 765 * @return an instance of {@link Policy} with the options set from this builder 766 * @throws IllegalStateException if an invariant is violated. 767 */ 768 public Policy build() { 769 Policy p = new Policy(this); 770 771 if (p.mDefaultStanza) { 772 if (p.mSeinfo == null) { 773 String err = "Missing global seinfo tag with default stanza."; 774 throw new IllegalStateException(err); 775 } 776 if (p.mCerts.size() != 0) { 777 String err = "Certs not allowed with default stanza."; 778 throw new IllegalStateException(err); 779 } 780 if (!p.mPkgMap.isEmpty()) { 781 String err = "Inner package mappings not allowed with default stanza."; 782 throw new IllegalStateException(err); 783 } 784 } else { 785 if (p.mCerts.size() == 0) { 786 String err = "Missing certs with signer tag. Expecting at least one."; 787 throw new IllegalStateException(err); 788 } 789 if ((p.mSeinfo == null) && (p.mPkgMap.isEmpty())) { 790 String err = "Missing seinfo OR package tags with signer tag. At " + 791 "least one must be present."; 792 throw new IllegalStateException(err); 793 } 794 } 795 796 return p; 797 } 798 } 799} 800