1 /* 2 * Copyright (C) 2020 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 android.content.integrity; 18 19 import static com.android.internal.util.Preconditions.checkArgument; 20 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 26 import com.android.internal.annotations.VisibleForTesting; 27 28 import java.lang.annotation.Retention; 29 import java.lang.annotation.RetentionPolicy; 30 import java.nio.charset.StandardCharsets; 31 import java.security.MessageDigest; 32 import java.security.NoSuchAlgorithmException; 33 import java.util.Collections; 34 import java.util.List; 35 import java.util.Objects; 36 37 /** 38 * Represents a simple formula consisting of an app install metadata field and a value. 39 * 40 * <p>Instances of this class are immutable. 41 * 42 * @hide 43 */ 44 @VisibleForTesting 45 public abstract class AtomicFormula extends IntegrityFormula { 46 47 /** @hide */ 48 @IntDef( 49 value = { 50 PACKAGE_NAME, 51 APP_CERTIFICATE, 52 INSTALLER_NAME, 53 INSTALLER_CERTIFICATE, 54 VERSION_CODE, 55 PRE_INSTALLED, 56 STAMP_TRUSTED, 57 STAMP_CERTIFICATE_HASH, 58 }) 59 @Retention(RetentionPolicy.SOURCE) 60 public @interface Key {} 61 62 /** @hide */ 63 @IntDef(value = {EQ, GT, GTE}) 64 @Retention(RetentionPolicy.SOURCE) 65 public @interface Operator {} 66 67 /** 68 * Package name of the app. 69 * 70 * <p>Can only be used in {@link StringAtomicFormula}. 71 */ 72 public static final int PACKAGE_NAME = 0; 73 74 /** 75 * SHA-256 of the app certificate of the app. 76 * 77 * <p>Can only be used in {@link StringAtomicFormula}. 78 */ 79 public static final int APP_CERTIFICATE = 1; 80 81 /** 82 * Package name of the installer. Will be empty string if installed by the system (e.g., adb). 83 * 84 * <p>Can only be used in {@link StringAtomicFormula}. 85 */ 86 public static final int INSTALLER_NAME = 2; 87 88 /** 89 * SHA-256 of the cert of the installer. Will be empty string if installed by the system (e.g., 90 * adb). 91 * 92 * <p>Can only be used in {@link StringAtomicFormula}. 93 */ 94 public static final int INSTALLER_CERTIFICATE = 3; 95 96 /** 97 * Version code of the app. 98 * 99 * <p>Can only be used in {@link LongAtomicFormula}. 100 */ 101 public static final int VERSION_CODE = 4; 102 103 /** 104 * If the app is pre-installed on the device. 105 * 106 * <p>Can only be used in {@link BooleanAtomicFormula}. 107 */ 108 public static final int PRE_INSTALLED = 5; 109 110 /** 111 * If the APK has an embedded trusted stamp. 112 * 113 * <p>Can only be used in {@link BooleanAtomicFormula}. 114 */ 115 public static final int STAMP_TRUSTED = 6; 116 117 /** 118 * SHA-256 of the certificate used to sign the stamp embedded in the APK. 119 * 120 * <p>Can only be used in {@link StringAtomicFormula}. 121 */ 122 public static final int STAMP_CERTIFICATE_HASH = 7; 123 124 public static final int EQ = 0; 125 public static final int GT = 1; 126 public static final int GTE = 2; 127 128 private final @Key int mKey; 129 AtomicFormula(@ey int key)130 public AtomicFormula(@Key int key) { 131 checkArgument(isValidKey(key), String.format("Unknown key: %d", key)); 132 mKey = key; 133 } 134 135 /** An {@link AtomicFormula} with an key and long value. */ 136 public static final class LongAtomicFormula extends AtomicFormula implements Parcelable { 137 private final Long mValue; 138 private final @Operator Integer mOperator; 139 140 /** 141 * Constructs an empty {@link LongAtomicFormula}. This should only be used as a base. 142 * 143 * <p>This formula will always return false. 144 * 145 * @throws IllegalArgumentException if {@code key} cannot be used with long value 146 */ LongAtomicFormula(@ey int key)147 public LongAtomicFormula(@Key int key) { 148 super(key); 149 checkArgument( 150 key == VERSION_CODE, 151 String.format( 152 "Key %s cannot be used with LongAtomicFormula", keyToString(key))); 153 mValue = null; 154 mOperator = null; 155 } 156 157 /** 158 * Constructs a new {@link LongAtomicFormula}. 159 * 160 * <p>This formula will hold if and only if the corresponding information of an install 161 * specified by {@code key} is of the correct relationship to {@code value} as specified by 162 * {@code operator}. 163 * 164 * @throws IllegalArgumentException if {@code key} cannot be used with long value 165 */ LongAtomicFormula(@ey int key, @Operator int operator, long value)166 public LongAtomicFormula(@Key int key, @Operator int operator, long value) { 167 super(key); 168 checkArgument( 169 key == VERSION_CODE, 170 String.format( 171 "Key %s cannot be used with LongAtomicFormula", keyToString(key))); 172 checkArgument( 173 isValidOperator(operator), String.format("Unknown operator: %d", operator)); 174 mOperator = operator; 175 mValue = value; 176 } 177 LongAtomicFormula(Parcel in)178 LongAtomicFormula(Parcel in) { 179 super(in.readInt()); 180 mValue = in.readLong(); 181 mOperator = in.readInt(); 182 } 183 184 @NonNull 185 public static final Creator<LongAtomicFormula> CREATOR = 186 new Creator<LongAtomicFormula>() { 187 @Override 188 public LongAtomicFormula createFromParcel(Parcel in) { 189 return new LongAtomicFormula(in); 190 } 191 192 @Override 193 public LongAtomicFormula[] newArray(int size) { 194 return new LongAtomicFormula[size]; 195 } 196 }; 197 198 @Override getTag()199 public int getTag() { 200 return IntegrityFormula.LONG_ATOMIC_FORMULA_TAG; 201 } 202 203 @Override matches(AppInstallMetadata appInstallMetadata)204 public boolean matches(AppInstallMetadata appInstallMetadata) { 205 if (mValue == null || mOperator == null) { 206 return false; 207 } 208 209 long metadataValue = getLongMetadataValue(appInstallMetadata, getKey()); 210 switch (mOperator) { 211 case EQ: 212 return metadataValue == mValue; 213 case GT: 214 return metadataValue > mValue; 215 case GTE: 216 return metadataValue >= mValue; 217 default: 218 throw new IllegalArgumentException( 219 String.format("Unexpected operator %d", mOperator)); 220 } 221 } 222 223 @Override isAppCertificateFormula()224 public boolean isAppCertificateFormula() { 225 return false; 226 } 227 228 @Override isInstallerFormula()229 public boolean isInstallerFormula() { 230 return false; 231 } 232 233 @Override toString()234 public String toString() { 235 if (mValue == null || mOperator == null) { 236 return String.format("(%s)", keyToString(getKey())); 237 } 238 return String.format( 239 "(%s %s %s)", keyToString(getKey()), operatorToString(mOperator), mValue); 240 } 241 242 @Override equals(Object o)243 public boolean equals(Object o) { 244 if (this == o) { 245 return true; 246 } 247 if (o == null || getClass() != o.getClass()) { 248 return false; 249 } 250 LongAtomicFormula that = (LongAtomicFormula) o; 251 return getKey() == that.getKey() 252 && mValue == that.mValue 253 && mOperator == that.mOperator; 254 } 255 256 @Override hashCode()257 public int hashCode() { 258 return Objects.hash(getKey(), mOperator, mValue); 259 } 260 261 @Override describeContents()262 public int describeContents() { 263 return 0; 264 } 265 266 @Override writeToParcel(@onNull Parcel dest, int flags)267 public void writeToParcel(@NonNull Parcel dest, int flags) { 268 if (mValue == null || mOperator == null) { 269 throw new IllegalStateException("Cannot write an empty LongAtomicFormula."); 270 } 271 dest.writeInt(getKey()); 272 dest.writeLong(mValue); 273 dest.writeInt(mOperator); 274 } 275 getValue()276 public Long getValue() { 277 return mValue; 278 } 279 getOperator()280 public Integer getOperator() { 281 return mOperator; 282 } 283 isValidOperator(int operator)284 private static boolean isValidOperator(int operator) { 285 return operator == EQ || operator == GT || operator == GTE; 286 } 287 getLongMetadataValue(AppInstallMetadata appInstallMetadata, int key)288 private static long getLongMetadataValue(AppInstallMetadata appInstallMetadata, int key) { 289 switch (key) { 290 case AtomicFormula.VERSION_CODE: 291 return appInstallMetadata.getVersionCode(); 292 default: 293 throw new IllegalStateException("Unexpected key in IntAtomicFormula" + key); 294 } 295 } 296 } 297 298 /** An {@link AtomicFormula} with a key and string value. */ 299 public static final class StringAtomicFormula extends AtomicFormula implements Parcelable { 300 private final String mValue; 301 // Indicates whether the value is the actual value or the hashed value. 302 private final Boolean mIsHashedValue; 303 304 /** 305 * Constructs an empty {@link StringAtomicFormula}. This should only be used as a base. 306 * 307 * <p>An empty formula will always match to false. 308 * 309 * @throws IllegalArgumentException if {@code key} cannot be used with string value 310 */ StringAtomicFormula(@ey int key)311 public StringAtomicFormula(@Key int key) { 312 super(key); 313 checkArgument( 314 key == PACKAGE_NAME 315 || key == APP_CERTIFICATE 316 || key == INSTALLER_CERTIFICATE 317 || key == INSTALLER_NAME 318 || key == STAMP_CERTIFICATE_HASH, 319 String.format( 320 "Key %s cannot be used with StringAtomicFormula", keyToString(key))); 321 mValue = null; 322 mIsHashedValue = null; 323 } 324 325 /** 326 * Constructs a new {@link StringAtomicFormula}. 327 * 328 * <p>This formula will hold if and only if the corresponding information of an install 329 * specified by {@code key} equals {@code value}. 330 * 331 * @throws IllegalArgumentException if {@code key} cannot be used with string value 332 */ StringAtomicFormula(@ey int key, @NonNull String value, boolean isHashed)333 public StringAtomicFormula(@Key int key, @NonNull String value, boolean isHashed) { 334 super(key); 335 checkArgument( 336 key == PACKAGE_NAME 337 || key == APP_CERTIFICATE 338 || key == INSTALLER_CERTIFICATE 339 || key == INSTALLER_NAME 340 || key == STAMP_CERTIFICATE_HASH, 341 String.format( 342 "Key %s cannot be used with StringAtomicFormula", keyToString(key))); 343 mValue = value; 344 mIsHashedValue = isHashed; 345 } 346 347 /** 348 * Constructs a new {@link StringAtomicFormula} together with handling the necessary hashing 349 * for the given key. 350 * 351 * <p>The value will be automatically hashed with SHA256 and the hex digest will be computed 352 * when the key is PACKAGE_NAME or INSTALLER_NAME and the value is more than 32 characters. 353 * 354 * <p>The APP_CERTIFICATES, INSTALLER_CERTIFICATES, and STAMP_CERTIFICATE_HASH are always 355 * delivered in hashed form. So the isHashedValue is set to true by default. 356 * 357 * @throws IllegalArgumentException if {@code key} cannot be used with string value. 358 */ StringAtomicFormula(@ey int key, @NonNull String value)359 public StringAtomicFormula(@Key int key, @NonNull String value) { 360 super(key); 361 checkArgument( 362 key == PACKAGE_NAME 363 || key == APP_CERTIFICATE 364 || key == INSTALLER_CERTIFICATE 365 || key == INSTALLER_NAME 366 || key == STAMP_CERTIFICATE_HASH, 367 String.format( 368 "Key %s cannot be used with StringAtomicFormula", keyToString(key))); 369 mValue = hashValue(key, value); 370 mIsHashedValue = 371 (key == APP_CERTIFICATE 372 || key == INSTALLER_CERTIFICATE 373 || key == STAMP_CERTIFICATE_HASH) 374 || !mValue.equals(value); 375 } 376 StringAtomicFormula(Parcel in)377 StringAtomicFormula(Parcel in) { 378 super(in.readInt()); 379 mValue = in.readStringNoHelper(); 380 mIsHashedValue = in.readByte() != 0; 381 } 382 383 @NonNull 384 public static final Creator<StringAtomicFormula> CREATOR = 385 new Creator<StringAtomicFormula>() { 386 @Override 387 public StringAtomicFormula createFromParcel(Parcel in) { 388 return new StringAtomicFormula(in); 389 } 390 391 @Override 392 public StringAtomicFormula[] newArray(int size) { 393 return new StringAtomicFormula[size]; 394 } 395 }; 396 397 @Override getTag()398 public int getTag() { 399 return IntegrityFormula.STRING_ATOMIC_FORMULA_TAG; 400 } 401 402 @Override matches(AppInstallMetadata appInstallMetadata)403 public boolean matches(AppInstallMetadata appInstallMetadata) { 404 if (mValue == null || mIsHashedValue == null) { 405 return false; 406 } 407 return getMetadataValue(appInstallMetadata, getKey()).contains(mValue); 408 } 409 410 @Override isAppCertificateFormula()411 public boolean isAppCertificateFormula() { 412 return getKey() == APP_CERTIFICATE; 413 } 414 415 @Override isInstallerFormula()416 public boolean isInstallerFormula() { 417 return getKey() == INSTALLER_NAME || getKey() == INSTALLER_CERTIFICATE; 418 } 419 420 @Override toString()421 public String toString() { 422 if (mValue == null || mIsHashedValue == null) { 423 return String.format("(%s)", keyToString(getKey())); 424 } 425 return String.format("(%s %s %s)", keyToString(getKey()), operatorToString(EQ), mValue); 426 } 427 428 @Override equals(Object o)429 public boolean equals(Object o) { 430 if (this == o) { 431 return true; 432 } 433 if (o == null || getClass() != o.getClass()) { 434 return false; 435 } 436 StringAtomicFormula that = (StringAtomicFormula) o; 437 return getKey() == that.getKey() && Objects.equals(mValue, that.mValue); 438 } 439 440 @Override hashCode()441 public int hashCode() { 442 return Objects.hash(getKey(), mValue); 443 } 444 445 @Override describeContents()446 public int describeContents() { 447 return 0; 448 } 449 450 @Override writeToParcel(@onNull Parcel dest, int flags)451 public void writeToParcel(@NonNull Parcel dest, int flags) { 452 if (mValue == null || mIsHashedValue == null) { 453 throw new IllegalStateException("Cannot write an empty StringAtomicFormula."); 454 } 455 dest.writeInt(getKey()); 456 dest.writeStringNoHelper(mValue); 457 dest.writeByte((byte) (mIsHashedValue ? 1 : 0)); 458 } 459 getValue()460 public String getValue() { 461 return mValue; 462 } 463 getIsHashedValue()464 public Boolean getIsHashedValue() { 465 return mIsHashedValue; 466 } 467 getMetadataValue( AppInstallMetadata appInstallMetadata, int key)468 private static List<String> getMetadataValue( 469 AppInstallMetadata appInstallMetadata, int key) { 470 switch (key) { 471 case AtomicFormula.PACKAGE_NAME: 472 return Collections.singletonList(appInstallMetadata.getPackageName()); 473 case AtomicFormula.APP_CERTIFICATE: 474 return appInstallMetadata.getAppCertificates(); 475 case AtomicFormula.INSTALLER_CERTIFICATE: 476 return appInstallMetadata.getInstallerCertificates(); 477 case AtomicFormula.INSTALLER_NAME: 478 return Collections.singletonList(appInstallMetadata.getInstallerName()); 479 case AtomicFormula.STAMP_CERTIFICATE_HASH: 480 return Collections.singletonList(appInstallMetadata.getStampCertificateHash()); 481 default: 482 throw new IllegalStateException( 483 "Unexpected key in StringAtomicFormula: " + key); 484 } 485 } 486 hashValue(@ey int key, String value)487 private static String hashValue(@Key int key, String value) { 488 // Hash the string value if it is a PACKAGE_NAME or INSTALLER_NAME and the value is 489 // greater than 32 characters. 490 if (value.length() > 32) { 491 if (key == PACKAGE_NAME || key == INSTALLER_NAME) { 492 return hash(value); 493 } 494 } 495 return value; 496 } 497 hash(String value)498 private static String hash(String value) { 499 try { 500 MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); 501 byte[] hashBytes = messageDigest.digest(value.getBytes(StandardCharsets.UTF_8)); 502 return IntegrityUtils.getHexDigest(hashBytes); 503 } catch (NoSuchAlgorithmException e) { 504 throw new RuntimeException("SHA-256 algorithm not found", e); 505 } 506 } 507 } 508 509 /** An {@link AtomicFormula} with a key and boolean value. */ 510 public static final class BooleanAtomicFormula extends AtomicFormula implements Parcelable { 511 private final Boolean mValue; 512 513 /** 514 * Constructs an empty {@link BooleanAtomicFormula}. This should only be used as a base. 515 * 516 * <p>An empty formula will always match to false. 517 * 518 * @throws IllegalArgumentException if {@code key} cannot be used with boolean value 519 */ BooleanAtomicFormula(@ey int key)520 public BooleanAtomicFormula(@Key int key) { 521 super(key); 522 checkArgument( 523 key == PRE_INSTALLED || key == STAMP_TRUSTED, 524 String.format( 525 "Key %s cannot be used with BooleanAtomicFormula", keyToString(key))); 526 mValue = null; 527 } 528 529 /** 530 * Constructs a new {@link BooleanAtomicFormula}. 531 * 532 * <p>This formula will hold if and only if the corresponding information of an install 533 * specified by {@code key} equals {@code value}. 534 * 535 * @throws IllegalArgumentException if {@code key} cannot be used with boolean value 536 */ BooleanAtomicFormula(@ey int key, boolean value)537 public BooleanAtomicFormula(@Key int key, boolean value) { 538 super(key); 539 checkArgument( 540 key == PRE_INSTALLED || key == STAMP_TRUSTED, 541 String.format( 542 "Key %s cannot be used with BooleanAtomicFormula", keyToString(key))); 543 mValue = value; 544 } 545 BooleanAtomicFormula(Parcel in)546 BooleanAtomicFormula(Parcel in) { 547 super(in.readInt()); 548 mValue = in.readByte() != 0; 549 } 550 551 @NonNull 552 public static final Creator<BooleanAtomicFormula> CREATOR = 553 new Creator<BooleanAtomicFormula>() { 554 @Override 555 public BooleanAtomicFormula createFromParcel(Parcel in) { 556 return new BooleanAtomicFormula(in); 557 } 558 559 @Override 560 public BooleanAtomicFormula[] newArray(int size) { 561 return new BooleanAtomicFormula[size]; 562 } 563 }; 564 565 @Override getTag()566 public int getTag() { 567 return IntegrityFormula.BOOLEAN_ATOMIC_FORMULA_TAG; 568 } 569 570 @Override matches(AppInstallMetadata appInstallMetadata)571 public boolean matches(AppInstallMetadata appInstallMetadata) { 572 if (mValue == null) { 573 return false; 574 } 575 return getBooleanMetadataValue(appInstallMetadata, getKey()) == mValue; 576 } 577 578 @Override isAppCertificateFormula()579 public boolean isAppCertificateFormula() { 580 return false; 581 } 582 583 @Override isInstallerFormula()584 public boolean isInstallerFormula() { 585 return false; 586 } 587 588 @Override toString()589 public String toString() { 590 if (mValue == null) { 591 return String.format("(%s)", keyToString(getKey())); 592 } 593 return String.format("(%s %s %s)", keyToString(getKey()), operatorToString(EQ), mValue); 594 } 595 596 @Override equals(Object o)597 public boolean equals(Object o) { 598 if (this == o) { 599 return true; 600 } 601 if (o == null || getClass() != o.getClass()) { 602 return false; 603 } 604 BooleanAtomicFormula that = (BooleanAtomicFormula) o; 605 return getKey() == that.getKey() && mValue == that.mValue; 606 } 607 608 @Override hashCode()609 public int hashCode() { 610 return Objects.hash(getKey(), mValue); 611 } 612 613 @Override describeContents()614 public int describeContents() { 615 return 0; 616 } 617 618 @Override writeToParcel(@onNull Parcel dest, int flags)619 public void writeToParcel(@NonNull Parcel dest, int flags) { 620 if (mValue == null) { 621 throw new IllegalStateException("Cannot write an empty BooleanAtomicFormula."); 622 } 623 dest.writeInt(getKey()); 624 dest.writeByte((byte) (mValue ? 1 : 0)); 625 } 626 getValue()627 public Boolean getValue() { 628 return mValue; 629 } 630 getBooleanMetadataValue( AppInstallMetadata appInstallMetadata, int key)631 private static boolean getBooleanMetadataValue( 632 AppInstallMetadata appInstallMetadata, int key) { 633 switch (key) { 634 case AtomicFormula.PRE_INSTALLED: 635 return appInstallMetadata.isPreInstalled(); 636 case AtomicFormula.STAMP_TRUSTED: 637 return appInstallMetadata.isStampTrusted(); 638 default: 639 throw new IllegalStateException( 640 "Unexpected key in BooleanAtomicFormula: " + key); 641 } 642 } 643 } 644 getKey()645 public int getKey() { 646 return mKey; 647 } 648 keyToString(int key)649 static String keyToString(int key) { 650 switch (key) { 651 case PACKAGE_NAME: 652 return "PACKAGE_NAME"; 653 case APP_CERTIFICATE: 654 return "APP_CERTIFICATE"; 655 case VERSION_CODE: 656 return "VERSION_CODE"; 657 case INSTALLER_NAME: 658 return "INSTALLER_NAME"; 659 case INSTALLER_CERTIFICATE: 660 return "INSTALLER_CERTIFICATE"; 661 case PRE_INSTALLED: 662 return "PRE_INSTALLED"; 663 case STAMP_TRUSTED: 664 return "STAMP_TRUSTED"; 665 case STAMP_CERTIFICATE_HASH: 666 return "STAMP_CERTIFICATE_HASH"; 667 default: 668 throw new IllegalArgumentException("Unknown key " + key); 669 } 670 } 671 operatorToString(int op)672 static String operatorToString(int op) { 673 switch (op) { 674 case EQ: 675 return "EQ"; 676 case GT: 677 return "GT"; 678 case GTE: 679 return "GTE"; 680 default: 681 throw new IllegalArgumentException("Unknown operator " + op); 682 } 683 } 684 isValidKey(int key)685 private static boolean isValidKey(int key) { 686 return key == PACKAGE_NAME 687 || key == APP_CERTIFICATE 688 || key == VERSION_CODE 689 || key == INSTALLER_NAME 690 || key == INSTALLER_CERTIFICATE 691 || key == PRE_INSTALLED 692 || key == STAMP_TRUSTED 693 || key == STAMP_CERTIFICATE_HASH; 694 } 695 } 696