Add public API and stub implementations for app integrity manager (service).
App integrity manager will accept rules from verifier and evaluate these rules
during install (and accept/reject the install accordingly). For more
information, see the linked bug (and the blocked bug).
Design doc: go/integrity-component-android-infra
Bug: 143689885
Change-Id: Ie965c6cc77987e06e9dea4be8fb49785a7bd08c4
Test: moving only
diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java
index 0fdb513..0c6fdc0 100644
--- a/core/java/android/content/Context.java
+++ b/core/java/android/content/Context.java
@@ -4917,6 +4917,14 @@
public static final String APP_SEARCH_SERVICE = "app_search";
/**
+ * Use with {@link #getSystemService(String)} to retrieve an
+ * {@link android.content.integrity.AppIntegrityManager}.
+ * @hide
+ */
+ @SystemApi
+ public static final String APP_INTEGRITY_SERVICE = "app_integrity";
+
+ /**
* Determine whether the given permission is allowed for a particular
* process and user ID running in the system.
*
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index ca374f93..40aca0e 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -4417,6 +4417,22 @@
@SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
public static final String ACTION_VIEW_LOCUS = "android.intent.action.VIEW_LOCUS";
+ /**
+ * Broadcast Action: Sent to the integrity component when a package
+ * needs to be verified. The data contains the package URI along with other relevant
+ * information.
+ *
+ * <p class="note">
+ * This is a protected intent that can only be sent by the system.
+ * </p>
+ *
+ * @hide
+ */
+ @SystemApi
+ @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+ public static final String ACTION_PACKAGE_NEEDS_INTEGRITY_VERIFICATION =
+ "android.intent.action.PACKAGE_NEEDS_INTEGRITY_VERIFICATION";
+
// ---------------------------------------------------------------------
// ---------------------------------------------------------------------
// Standard intent categories (see addCategory()).
diff --git a/core/java/android/content/integrity/AppInstallMetadata.java b/core/java/android/content/integrity/AppInstallMetadata.java
new file mode 100644
index 0000000..c963475
--- /dev/null
+++ b/core/java/android/content/integrity/AppInstallMetadata.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2019 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 android.content.integrity;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * The app install metadata.
+ *
+ * <p>The integrity component retrieves metadata for app installs from package manager, passing it
+ * to the rule evaluation engine to evaluate the metadata against the rules.
+ *
+ * <p>Instances of this class are immutable.
+ *
+ * @hide
+ */
+@SystemApi
+@VisibleForTesting
+public final class AppInstallMetadata {
+ private final String mPackageName;
+ // Raw string encoding for the SHA-256 hash of the certificate of the app.
+ private final String mAppCertificate;
+ private final String mInstallerName;
+ // Raw string encoding for the SHA-256 hash of the certificate of the installer.
+ private final String mInstallerCertificate;
+ private final int mVersionCode;
+ private final boolean mIsPreInstalled;
+
+ private AppInstallMetadata(Builder builder) {
+ this.mPackageName = builder.mPackageName;
+ this.mAppCertificate = builder.mAppCertificate;
+ this.mInstallerName = builder.mInstallerName;
+ this.mInstallerCertificate = builder.mInstallerCertificate;
+ this.mVersionCode = builder.mVersionCode;
+ this.mIsPreInstalled = builder.mIsPreInstalled;
+ }
+
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ @NonNull
+ public String getAppCertificate() {
+ return mAppCertificate;
+ }
+
+ @Nullable
+ public String getInstallerName() {
+ return mInstallerName;
+ }
+
+ @Nullable
+ public String getInstallerCertificate() {
+ return mInstallerCertificate;
+ }
+
+ /** @see AppInstallMetadata.Builder#setVersionCode(int) */
+ public int getVersionCode() {
+ return mVersionCode;
+ }
+
+ /** @see AppInstallMetadata.Builder#setIsPreInstalled(boolean) */
+ public boolean isPreInstalled() {
+ return mIsPreInstalled;
+ }
+
+ /** Builder class for constructing {@link AppInstallMetadata} objects. */
+ public static final class Builder {
+ private String mPackageName;
+ private String mAppCertificate;
+ private String mInstallerName;
+ private String mInstallerCertificate;
+ private int mVersionCode;
+ private boolean mIsPreInstalled;
+
+ /**
+ * Set package name of the app to be installed.
+ *
+ * @see AppInstallMetadata#getPackageName()
+ */
+ @NonNull
+ public Builder setPackageName(@NonNull String packageName) {
+ this.mPackageName = checkNotNull(packageName);
+ return this;
+ }
+
+ /**
+ * Set certificate of the app to be installed.
+ *
+ * <p>It is represented as the raw string encoding for the SHA-256 hash of the certificate
+ * of the app.
+ *
+ * @see AppInstallMetadata#getAppCertificate()
+ */
+ @NonNull
+ public Builder setAppCertificate(@NonNull String appCertificate) {
+ this.mAppCertificate = checkNotNull(appCertificate);
+ return this;
+ }
+
+ /**
+ * Set name of the installer installing the app.
+ *
+ * @see AppInstallMetadata#getInstallerName()
+ */
+ @NonNull
+ public Builder setInstallerName(@NonNull String installerName) {
+ this.mInstallerName = checkNotNull(installerName);
+ return this;
+ }
+
+ /**
+ * Set certificate of the installer installing the app.
+ *
+ * <p>It is represented as the raw string encoding for the SHA-256 hash of the certificate
+ * of the installer.
+ *
+ * @see AppInstallMetadata#getInstallerCertificate()
+ */
+ @NonNull
+ public Builder setInstallerCertificate(@NonNull String installerCertificate) {
+ this.mInstallerCertificate = checkNotNull(installerCertificate);
+ return this;
+ }
+
+ /**
+ * Set version code of the app to be installed.
+ *
+ * @see AppInstallMetadata#getVersionCode()
+ */
+ @NonNull
+ public Builder setVersionCode(int versionCode) {
+ this.mVersionCode = versionCode;
+ return this;
+ }
+
+ /**
+ * Set whether the app is pre-installed on the device or not.
+ *
+ * @see AppInstallMetadata#isPreInstalled()
+ */
+ @NonNull
+ public Builder setIsPreInstalled(boolean isPreInstalled) {
+ this.mIsPreInstalled = isPreInstalled;
+ return this;
+ }
+
+ /**
+ * Build {@link AppInstallMetadata}.
+ *
+ * @throws IllegalArgumentException if package name or app certificate is null
+ */
+ @NonNull
+ public AppInstallMetadata build() {
+ checkArgument(mPackageName != null);
+ checkArgument(mAppCertificate != null);
+ return new AppInstallMetadata(this);
+ }
+ }
+}
diff --git a/core/java/android/content/integrity/AppIntegrityManager.java b/core/java/android/content/integrity/AppIntegrityManager.java
new file mode 100644
index 0000000..e53ef66
--- /dev/null
+++ b/core/java/android/content/integrity/AppIntegrityManager.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2019 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 android.content.integrity;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.content.IntentSender;
+import android.content.pm.ParceledListSlice;
+import android.os.RemoteException;
+
+/**
+ * Class for pushing rules used to check the integrity of app installs.
+ *
+ * <p>Note: applications using methods of this class must be a system app and have their package
+ * name whitelisted as an integrity rule provider. Otherwise a {@link SecurityException} will be
+ * thrown.
+ *
+ * @hide
+ */
+@SystemApi
+@SystemService(Context.APP_INTEGRITY_SERVICE)
+public class AppIntegrityManager {
+
+ /** The operation succeeded. */
+ public static final int STATUS_SUCCESS = 0;
+
+ /** The operation failed. */
+ public static final int STATUS_FAILURE = 1;
+
+ /**
+ * Current status of an operation. Will be one of {@link #STATUS_SUCCESS}, {@link
+ * #STATUS_FAILURE}.
+ *
+ * <p>More information about a status may be available through additional extras; see the
+ * individual status documentation for details.
+ *
+ * @see android.content.Intent#getIntExtra(String, int)
+ */
+ public static final String EXTRA_STATUS = "android.content.integrity.extra.STATUS";
+
+ IAppIntegrityManager mManager;
+
+ /** @hide */
+ public AppIntegrityManager(IAppIntegrityManager manager) {
+ mManager = manager;
+ }
+
+ /**
+ * Update the rules to evaluate during install time.
+ *
+ * @param updateRequest request containing the data of the rule set update
+ * @param statusReceiver Called when the state of the session changes. Intents sent to this
+ * receiver contain {@link #EXTRA_STATUS}. Refer to the individual status codes on how to
+ * handle them.
+ */
+ public void updateRuleSet(
+ @NonNull RuleSet updateRequest, @NonNull IntentSender statusReceiver) {
+ try {
+ mManager.updateRuleSet(
+ updateRequest.getVersion(),
+ new ParceledListSlice<>(updateRequest.getRules()),
+ statusReceiver);
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /** Get the current version of the rule set. */
+ @NonNull
+ public String getCurrentRuleSetVersion() {
+ try {
+ return mManager.getCurrentRuleSetVersion();
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+
+ /** Get the name of the package that provided the current rule set. */
+ @NonNull
+ public String getCurrentRuleSetProvider() {
+ try {
+ return mManager.getCurrentRuleSetProvider();
+ } catch (RemoteException e) {
+ throw e.rethrowAsRuntimeException();
+ }
+ }
+}
diff --git a/core/java/android/content/integrity/AtomicFormula.java b/core/java/android/content/integrity/AtomicFormula.java
new file mode 100644
index 0000000..c8e164f
--- /dev/null
+++ b/core/java/android/content/integrity/AtomicFormula.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2019 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 android.content.integrity;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represents a simple formula consisting of an app install metadata field and a value.
+ *
+ * <p>Instances of this class are immutable.
+ *
+ * @hide
+ */
+@SystemApi
+@VisibleForTesting
+public abstract class AtomicFormula implements Formula {
+
+ private static final String TAG = "AtomicFormula";
+
+ @IntDef(
+ value = {
+ PACKAGE_NAME,
+ APP_CERTIFICATE,
+ INSTALLER_NAME,
+ INSTALLER_CERTIFICATE,
+ VERSION_CODE,
+ PRE_INSTALLED,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Key {}
+
+ @IntDef(value = {EQ, LT, LE, GT, GE})
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Operator {}
+
+ /**
+ * Package name of the app.
+ *
+ * <p>Can only be used in {@link StringAtomicFormula}.
+ */
+ public static final int PACKAGE_NAME = 0;
+
+ /**
+ * SHA-256 of the app certificate of the app.
+ *
+ * <p>Can only be used in {@link StringAtomicFormula}.
+ */
+ public static final int APP_CERTIFICATE = 1;
+
+ /**
+ * Package name of the installer. Will be empty string if installed by the system (e.g., adb).
+ *
+ * <p>Can only be used in {@link StringAtomicFormula}.
+ */
+ public static final int INSTALLER_NAME = 2;
+
+ /**
+ * SHA-256 of the cert of the installer. Will be empty string if installed by the system (e.g.,
+ * adb).
+ *
+ * <p>Can only be used in {@link StringAtomicFormula}.
+ */
+ public static final int INSTALLER_CERTIFICATE = 3;
+
+ /**
+ * Version code of the app.
+ *
+ * <p>Can only be used in {@link IntAtomicFormula}.
+ */
+ public static final int VERSION_CODE = 4;
+
+ /**
+ * If the app is pre-installed on the device.
+ *
+ * <p>Can only be used in {@link BooleanAtomicFormula}.
+ */
+ public static final int PRE_INSTALLED = 5;
+
+ public static final int EQ = 0;
+ public static final int LT = 1;
+ public static final int LE = 2;
+ public static final int GT = 3;
+ public static final int GE = 4;
+
+ private final @Key int mKey;
+
+ public AtomicFormula(@Key int key) {
+ checkArgument(isValidKey(key), String.format("Unknown key: %d", key));
+ mKey = key;
+ }
+
+ /** An {@link AtomicFormula} with an key and int value. */
+ public static final class IntAtomicFormula extends AtomicFormula implements Parcelable {
+ private final int mValue;
+ private final @Operator int mOperator;
+
+ /**
+ * Constructs a new {@link IntAtomicFormula}.
+ *
+ * <p>This formula will hold if and only if the corresponding information of an install
+ * specified by {@code key} is of the correct relationship to {@code value} as specified by
+ * {@code operator}.
+ *
+ * @throws IllegalArgumentException if {@code key} cannot be used with integer value
+ */
+ public IntAtomicFormula(@Key int key, @Operator int operator, int value) {
+ super(key);
+ checkArgument(
+ key == VERSION_CODE,
+ String.format("Key %s cannot be used with IntAtomicFormula", keyToString(key)));
+ checkArgument(isValidOperator(operator),
+ String.format("Unknown operator: %d", operator));
+ mOperator = operator;
+ mValue = value;
+ }
+
+ IntAtomicFormula(Parcel in) {
+ super(in.readInt());
+ mValue = in.readInt();
+ mOperator = in.readInt();
+ }
+
+ @NonNull
+ public static final Creator<IntAtomicFormula> CREATOR =
+ new Creator<IntAtomicFormula>() {
+ @Override
+ public IntAtomicFormula createFromParcel(Parcel in) {
+ return new IntAtomicFormula(in);
+ }
+
+ @Override
+ public IntAtomicFormula[] newArray(int size) {
+ return new IntAtomicFormula[size];
+ }
+ };
+
+ @Override
+ public boolean isSatisfied(@NonNull AppInstallMetadata appInstallMetadata) {
+ int metadataValue = getMetadataValueByKey(appInstallMetadata);
+ switch (mOperator) {
+ case EQ:
+ return metadataValue == mValue;
+ case LE:
+ return metadataValue <= mValue;
+ case LT:
+ return metadataValue < mValue;
+ case GE:
+ return metadataValue >= mValue;
+ case GT:
+ return metadataValue > mValue;
+ default:
+ Slog.i(TAG, String.format("Unexpected operator %d", mOperator));
+ return false;
+ }
+ }
+
+ @Override
+ public int getTag() {
+ return Formula.INT_ATOMIC_FORMULA_TAG;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "(%s %s %s)", keyToString(getKey()), operatorToString(mOperator), mValue);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ IntAtomicFormula that = (IntAtomicFormula) o;
+ return getKey() == that.getKey()
+ && mValue == that.mValue
+ && mOperator == that.mOperator;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getKey(), mOperator, mValue);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(getKey());
+ dest.writeInt(mValue);
+ dest.writeInt(mOperator);
+ }
+
+ public int getValue() {
+ return mValue;
+ }
+
+ public int getOperator() {
+ return mOperator;
+ }
+
+ private int getMetadataValueByKey(AppInstallMetadata appInstallMetadata) {
+ switch (getKey()) {
+ case VERSION_CODE:
+ return appInstallMetadata.getVersionCode();
+ default:
+ throw new IllegalStateException(
+ "Unexpected key in IntAtomicFormula" + getKey());
+ }
+ }
+
+ private static boolean isValidOperator(int operator) {
+ return operator == EQ
+ || operator == LT
+ || operator == LE
+ || operator == GT
+ || operator == GE;
+ }
+ }
+
+ /** An {@link AtomicFormula} with a key and string value. */
+ public static final class StringAtomicFormula extends AtomicFormula implements Parcelable {
+ private final String mValue;
+ // Indicates whether the value is the actual value or the hashed value.
+ private final boolean mIsHashedValue;
+
+ /**
+ * Constructs a new {@link StringAtomicFormula}.
+ *
+ * <p>This formula will hold if and only if the corresponding information of an install
+ * specified by {@code key} equals {@code value}.
+ *
+ * @throws IllegalArgumentException if {@code key} cannot be used with string value
+ */
+ public StringAtomicFormula(@Key int key, @NonNull String value, boolean isHashedValue) {
+ super(key);
+ mIsHashedValue = isHashedValue;
+ checkArgument(
+ key == PACKAGE_NAME
+ || key == APP_CERTIFICATE
+ || key == INSTALLER_CERTIFICATE
+ || key == INSTALLER_NAME,
+ String.format(
+ "Key %s cannot be used with StringAtomicFormula", keyToString(key)));
+ mValue = value;
+ }
+
+ StringAtomicFormula(Parcel in) {
+ super(in.readInt());
+ mValue = in.readStringNoHelper();
+ mIsHashedValue = in.readByte() != 0;
+ }
+
+ @NonNull
+ public static final Creator<StringAtomicFormula> CREATOR =
+ new Creator<StringAtomicFormula>() {
+ @Override
+ public StringAtomicFormula createFromParcel(Parcel in) {
+ return new StringAtomicFormula(in);
+ }
+
+ @Override
+ public StringAtomicFormula[] newArray(int size) {
+ return new StringAtomicFormula[size];
+ }
+ };
+
+ @Override
+ public boolean isSatisfied(@NonNull AppInstallMetadata appInstallMetadata) {
+ String metadataValue = getMetadataValueByKey(appInstallMetadata);
+ return metadataValue.equals(mValue);
+ }
+
+ @Override
+ public int getTag() {
+ return Formula.STRING_ATOMIC_FORMULA_TAG;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s %s %s)", keyToString(getKey()), operatorToString(EQ), mValue);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ StringAtomicFormula that = (StringAtomicFormula) o;
+ return getKey() == that.getKey() && Objects.equals(mValue, that.mValue);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getKey(), mValue);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(getKey());
+ dest.writeStringNoHelper(mValue);
+ dest.writeByte((byte) (mIsHashedValue ? 1 : 0));
+ }
+
+ @NonNull
+ public String getValue() {
+ return mValue;
+ }
+
+ public boolean getIsHashedValue() {
+ return mIsHashedValue;
+ }
+
+ private String getMetadataValueByKey(AppInstallMetadata appInstallMetadata) {
+ switch (getKey()) {
+ case PACKAGE_NAME:
+ return appInstallMetadata.getPackageName();
+ case APP_CERTIFICATE:
+ return appInstallMetadata.getAppCertificate();
+ case INSTALLER_CERTIFICATE:
+ return appInstallMetadata.getInstallerCertificate();
+ case INSTALLER_NAME:
+ return appInstallMetadata.getInstallerName();
+ default:
+ throw new IllegalStateException(
+ "Unexpected key in StringAtomicFormula: " + getKey());
+ }
+ }
+ }
+
+ /** An {@link AtomicFormula} with a key and boolean value. */
+ public static final class BooleanAtomicFormula extends AtomicFormula implements Parcelable {
+ private final boolean mValue;
+
+ /**
+ * Constructs a new {@link BooleanAtomicFormula}.
+ *
+ * <p>This formula will hold if and only if the corresponding information of an install
+ * specified by {@code key} equals {@code value}.
+ *
+ * @throws IllegalArgumentException if {@code key} cannot be used with boolean value
+ */
+ public BooleanAtomicFormula(@Key int key, boolean value) {
+ super(key);
+ checkArgument(
+ key == PRE_INSTALLED,
+ String.format(
+ "Key %s cannot be used with BooleanAtomicFormula", keyToString(key)));
+ mValue = value;
+ }
+
+ BooleanAtomicFormula(Parcel in) {
+ super(in.readInt());
+ mValue = in.readByte() != 0;
+ }
+
+ @NonNull
+ public static final Creator<BooleanAtomicFormula> CREATOR =
+ new Creator<BooleanAtomicFormula>() {
+ @Override
+ public BooleanAtomicFormula createFromParcel(Parcel in) {
+ return new BooleanAtomicFormula(in);
+ }
+
+ @Override
+ public BooleanAtomicFormula[] newArray(int size) {
+ return new BooleanAtomicFormula[size];
+ }
+ };
+
+ @Override
+ public boolean isSatisfied(@NonNull AppInstallMetadata appInstallMetadata) {
+ boolean metadataValue = getMetadataValueByKey(appInstallMetadata);
+ return metadataValue == mValue;
+ }
+
+ @Override
+ public int getTag() {
+ return Formula.BOOLEAN_ATOMIC_FORMULA_TAG;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("(%s %s %s)", keyToString(getKey()), operatorToString(EQ), mValue);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ BooleanAtomicFormula that = (BooleanAtomicFormula) o;
+ return getKey() == that.getKey() && mValue == that.mValue;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(getKey(), mValue);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(getKey());
+ dest.writeByte((byte) (mValue ? 1 : 0));
+ }
+
+ public boolean getValue() {
+ return mValue;
+ }
+
+ private boolean getMetadataValueByKey(AppInstallMetadata appInstallMetadata) {
+ switch (getKey()) {
+ case PRE_INSTALLED:
+ return appInstallMetadata.isPreInstalled();
+ default:
+ throw new IllegalStateException(
+ "Unexpected key in BooleanAtomicFormula: " + getKey());
+ }
+ }
+ }
+
+ public int getKey() {
+ return mKey;
+ }
+
+ static String keyToString(int key) {
+ switch (key) {
+ case PACKAGE_NAME:
+ return "PACKAGE_NAME";
+ case APP_CERTIFICATE:
+ return "APP_CERTIFICATE";
+ case VERSION_CODE:
+ return "VERSION_CODE";
+ case INSTALLER_NAME:
+ return "INSTALLER_NAME";
+ case INSTALLER_CERTIFICATE:
+ return "INSTALLER_CERTIFICATE";
+ case PRE_INSTALLED:
+ return "PRE_INSTALLED";
+ default:
+ throw new IllegalArgumentException("Unknown key " + key);
+ }
+ }
+
+ static String operatorToString(int op) {
+ switch (op) {
+ case EQ:
+ return "EQ";
+ case LT:
+ return "LT";
+ case LE:
+ return "LE";
+ case GT:
+ return "GT";
+ case GE:
+ return "GE";
+ default:
+ throw new IllegalArgumentException("Unknown operator " + op);
+ }
+ }
+
+ private static boolean isValidKey(int key) {
+ return key == PACKAGE_NAME
+ || key == APP_CERTIFICATE
+ || key == VERSION_CODE
+ || key == INSTALLER_NAME
+ || key == INSTALLER_CERTIFICATE
+ || key == PRE_INSTALLED;
+ }
+}
diff --git a/core/java/android/content/integrity/CompoundFormula.java b/core/java/android/content/integrity/CompoundFormula.java
new file mode 100644
index 0000000..53a9953
--- /dev/null
+++ b/core/java/android/content/integrity/CompoundFormula.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2019 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 android.content.integrity;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Slog;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Represents a compound formula formed by joining other simple and complex formulas with boolean
+ * connectors.
+ *
+ * <p>Instances of this class are immutable.
+ *
+ * @hide
+ */
+@SystemApi
+@VisibleForTesting
+public final class CompoundFormula implements Formula, Parcelable {
+ private static final String TAG = "OpenFormula";
+
+ @IntDef(
+ value = {
+ AND, OR, NOT,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Connector {}
+
+ /** Boolean AND operator. */
+ public static final int AND = 0;
+
+ /** Boolean OR operator. */
+ public static final int OR = 1;
+
+ /** Boolean NOT operator. */
+ public static final int NOT = 2;
+
+ private final @Connector int mConnector;
+ private final @NonNull List<Formula> mFormulas;
+
+ @NonNull
+ public static final Creator<CompoundFormula> CREATOR =
+ new Creator<CompoundFormula>() {
+ @Override
+ public CompoundFormula createFromParcel(Parcel in) {
+ return new CompoundFormula(in);
+ }
+
+ @Override
+ public CompoundFormula[] newArray(int size) {
+ return new CompoundFormula[size];
+ }
+ };
+
+ /**
+ * Create a new formula from operator and operands.
+ *
+ * @throws IllegalArgumentException if the number of operands is not matching the requirements
+ * for that operator (at least 2 for {@link #AND} and {@link #OR}, 1 for {@link #NOT}).
+ */
+ public CompoundFormula(@Connector int connector, @NonNull List<Formula> formulas) {
+ checkArgument(
+ isValidConnector(connector), String.format("Unknown connector: %d", connector));
+ validateFormulas(connector, formulas);
+ this.mConnector = connector;
+ this.mFormulas = Collections.unmodifiableList(formulas);
+ }
+
+ CompoundFormula(Parcel in) {
+ mConnector = in.readInt();
+ int length = in.readInt();
+ checkArgument(length >= 0, "Must have non-negative length. Got " + length);
+ mFormulas = new ArrayList<>(length);
+ for (int i = 0; i < length; i++) {
+ mFormulas.add(Formula.readFromParcel(in));
+ }
+ validateFormulas(mConnector, mFormulas);
+ }
+
+ public @Connector int getConnector() {
+ return mConnector;
+ }
+
+ @NonNull
+ public List<Formula> getFormulas() {
+ return mFormulas;
+ }
+
+ @Override
+ public boolean isSatisfied(@NonNull AppInstallMetadata appInstallMetadata) {
+ switch (mConnector) {
+ case NOT:
+ return !mFormulas.get(0).isSatisfied(appInstallMetadata);
+ case AND:
+ return mFormulas.stream()
+ .allMatch(formula -> formula.isSatisfied(appInstallMetadata));
+ case OR:
+ return mFormulas.stream()
+ .anyMatch(formula -> formula.isSatisfied(appInstallMetadata));
+ default:
+ Slog.i(TAG, "Unknown connector " + mConnector);
+ return false;
+ }
+ }
+
+ @Override
+ public int getTag() {
+ return Formula.COMPOUND_FORMULA_TAG;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ if (mFormulas.size() == 1) {
+ sb.append(String.format("%s ", connectorToString(mConnector)));
+ sb.append(mFormulas.get(0).toString());
+ } else {
+ for (int i = 0; i < mFormulas.size(); i++) {
+ if (i > 0) {
+ sb.append(String.format(" %s ", connectorToString(mConnector)));
+ }
+ sb.append(mFormulas.get(i).toString());
+ }
+ }
+ return sb.toString();
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ CompoundFormula that = (CompoundFormula) o;
+ return mConnector == that.mConnector && mFormulas.equals(that.mFormulas);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mConnector, mFormulas);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ dest.writeInt(mConnector);
+ dest.writeInt(mFormulas.size());
+ for (Formula formula : mFormulas) {
+ Formula.writeToParcel(formula, dest, flags);
+ }
+ }
+
+ private static void validateFormulas(@Connector int connector, List<Formula> formulas) {
+ switch (connector) {
+ case AND:
+ case OR:
+ checkArgument(
+ formulas.size() >= 2,
+ String.format(
+ "Connector %s must have at least 2 formulas",
+ connectorToString(connector)));
+ break;
+ case NOT:
+ checkArgument(
+ formulas.size() == 1,
+ String.format(
+ "Connector %s must have 1 formula only",
+ connectorToString(connector)));
+ break;
+ }
+ }
+
+ private static String connectorToString(int connector) {
+ switch (connector) {
+ case AND:
+ return "AND";
+ case OR:
+ return "OR";
+ case NOT:
+ return "NOT";
+ default:
+ throw new IllegalArgumentException("Unknown connector " + connector);
+ }
+ }
+
+ private static boolean isValidConnector(int connector) {
+ return connector == AND || connector == OR || connector == NOT;
+ }
+}
diff --git a/core/java/android/content/integrity/Formula.java b/core/java/android/content/integrity/Formula.java
new file mode 100644
index 0000000..030ff6b
--- /dev/null
+++ b/core/java/android/content/integrity/Formula.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2019 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 android.content.integrity;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.content.integrity.AtomicFormula.BooleanAtomicFormula;
+import android.content.integrity.AtomicFormula.IntAtomicFormula;
+import android.content.integrity.AtomicFormula.StringAtomicFormula;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Represents a rule logic/content.
+ *
+ * @hide
+ */
+@SystemApi
+@VisibleForTesting
+public interface Formula {
+ @IntDef(
+ value = {
+ COMPOUND_FORMULA_TAG,
+ STRING_ATOMIC_FORMULA_TAG,
+ INT_ATOMIC_FORMULA_TAG,
+ BOOLEAN_ATOMIC_FORMULA_TAG
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Tag {}
+
+ int COMPOUND_FORMULA_TAG = 0;
+ int STRING_ATOMIC_FORMULA_TAG = 1;
+ int INT_ATOMIC_FORMULA_TAG = 2;
+ int BOOLEAN_ATOMIC_FORMULA_TAG = 3;
+
+ /**
+ * Returns if this formula can be satisfied by substituting the corresponding information of
+ * {@code appInstallMetadata} into the formula.
+ */
+ boolean isSatisfied(@NonNull AppInstallMetadata appInstallMetadata);
+
+ /** Returns the tag that identifies the current class. */
+ @Tag int getTag();
+
+ /**
+ * Write a {@link Formula} to {@link android.os.Parcel}.
+ *
+ * <p>This helper method is needed because non-final class/interface are not allowed to be
+ * {@link Parcelable}.
+ *
+ * @throws IllegalArgumentException if {@link Formula} is not a recognized subclass
+ */
+ static void writeToParcel(@NonNull Formula formula, @NonNull Parcel dest, int flags) {
+ dest.writeInt(formula.getTag());
+ ((Parcelable) formula).writeToParcel(dest, flags);
+ }
+
+ /**
+ * Read a {@link Formula} from a {@link android.os.Parcel}.
+ *
+ * <p>We need this (hacky) helper method because non-final class/interface cannot be {@link
+ * Parcelable} (api lint error).
+ *
+ * @throws IllegalArgumentException if the parcel cannot be parsed
+ */
+ @NonNull
+ static Formula readFromParcel(@NonNull Parcel in) {
+ int tag = in.readInt();
+ switch (tag) {
+ case COMPOUND_FORMULA_TAG:
+ return CompoundFormula.CREATOR.createFromParcel(in);
+ case STRING_ATOMIC_FORMULA_TAG:
+ return StringAtomicFormula.CREATOR.createFromParcel(in);
+ case INT_ATOMIC_FORMULA_TAG:
+ return IntAtomicFormula.CREATOR.createFromParcel(in);
+ case BOOLEAN_ATOMIC_FORMULA_TAG:
+ return BooleanAtomicFormula.CREATOR.createFromParcel(in);
+ default:
+ throw new IllegalArgumentException("Unknown formula tag " + tag);
+ }
+ }
+}
diff --git a/core/java/android/content/integrity/IAppIntegrityManager.aidl b/core/java/android/content/integrity/IAppIntegrityManager.aidl
new file mode 100644
index 0000000..6b73fd7
--- /dev/null
+++ b/core/java/android/content/integrity/IAppIntegrityManager.aidl
@@ -0,0 +1,28 @@
+/*
+**
+** Copyright 2019, 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 android.content.integrity;
+
+import android.content.integrity.Rule;
+import android.content.IntentSender;
+import android.content.pm.ParceledListSlice;
+
+/** @hide */
+interface IAppIntegrityManager {
+ void updateRuleSet(String version, in ParceledListSlice<Rule> rules, in IntentSender statusReceiver);
+ String getCurrentRuleSetVersion();
+ String getCurrentRuleSetProvider();
+}
diff --git a/core/java/android/content/integrity/Rule.aidl b/core/java/android/content/integrity/Rule.aidl
new file mode 100644
index 0000000..a6634ee
--- /dev/null
+++ b/core/java/android/content/integrity/Rule.aidl
@@ -0,0 +1,19 @@
+/*
+**
+** Copyright 2007, 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 android.content.integrity;
+
+parcelable Rule;
diff --git a/core/java/android/content/integrity/Rule.java b/core/java/android/content/integrity/Rule.java
new file mode 100644
index 0000000..914f147
--- /dev/null
+++ b/core/java/android/content/integrity/Rule.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2019 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 android.content.integrity;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Represent rules to be used in the rule evaluation engine to match against app installs.
+ *
+ * <p>Instances of this class are immutable.
+ *
+ * @hide
+ */
+@SystemApi
+@VisibleForTesting
+public final class Rule implements Parcelable {
+
+ @IntDef(
+ value = {
+ DENY,
+ FORCE_ALLOW,
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface Effect {}
+
+ /** If this rule matches the install, the install should be denied. */
+ public static final int DENY = 0;
+
+ /**
+ * If this rule matches the install, the install will be allowed regardless of other matched
+ * rules.
+ */
+ public static final int FORCE_ALLOW = 1;
+
+ private final @NonNull Formula mFormula;
+ private final @Effect int mEffect;
+
+ public Rule(@NonNull Formula formula, @Effect int effect) {
+ checkArgument(isValidEffect(effect), String.format("Unknown effect: %d", effect));
+ this.mFormula = checkNotNull(formula);
+ this.mEffect = effect;
+ }
+
+ Rule(Parcel in) {
+ mFormula = Formula.readFromParcel(in);
+ mEffect = in.readInt();
+ }
+
+ @NonNull
+ public static final Creator<Rule> CREATOR =
+ new Creator<Rule>() {
+ @Override
+ public Rule createFromParcel(Parcel in) {
+ return new Rule(in);
+ }
+
+ @Override
+ public Rule[] newArray(int size) {
+ return new Rule[size];
+ }
+ };
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(@NonNull Parcel dest, int flags) {
+ Formula.writeToParcel(mFormula, dest, flags);
+ dest.writeInt(mEffect);
+ }
+
+ @NonNull
+ public Formula getFormula() {
+ return mFormula;
+ }
+
+ public @Effect int getEffect() {
+ return mEffect;
+ }
+
+ @Override
+ public String toString() {
+ return String.format("Rule: %s, %s", mFormula, effectToString(mEffect));
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+ Rule that = (Rule) o;
+ return mEffect == that.mEffect && Objects.equals(mFormula, that.mFormula);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mFormula, mEffect);
+ }
+
+ private static String effectToString(int effect) {
+ switch (effect) {
+ case DENY:
+ return "DENY";
+ case FORCE_ALLOW:
+ return "FORCE_ALLOW";
+ default:
+ throw new IllegalArgumentException("Unknown effect " + effect);
+ }
+ }
+
+ private static boolean isValidEffect(int effect) {
+ return effect == DENY
+ || effect == FORCE_ALLOW;
+ }
+}
diff --git a/core/java/android/content/integrity/RuleSet.java b/core/java/android/content/integrity/RuleSet.java
new file mode 100644
index 0000000..a78f8c9
--- /dev/null
+++ b/core/java/android/content/integrity/RuleSet.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 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 android.content.integrity;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Immutable data class encapsulating all parameters of a rule set.
+ *
+ * @hide
+ */
+@SystemApi
+public class RuleSet {
+ private final String mVersion;
+ private final List<Rule> mRules;
+
+ private RuleSet(String version, List<Rule> rules) {
+ mVersion = version;
+ mRules = Collections.unmodifiableList(rules);
+ }
+
+ /** @see Builder#setVersion(String). */
+ @NonNull
+ public String getVersion() {
+ return mVersion;
+ }
+
+ /** @see Builder#addRules(List). */
+ @NonNull
+ public List<Rule> getRules() {
+ return mRules;
+ }
+
+ /** Builder class for RuleSetUpdateRequest. */
+ public static class Builder {
+ private String mVersion;
+ private List<Rule> mRules;
+
+ public Builder() {
+ mRules = new ArrayList<>();
+ }
+
+ /**
+ * Set a version string to identify this rule set. This can be retrieved by {@link
+ * AppIntegrityManager#getCurrentRuleSetVersion()}.
+ */
+ @NonNull
+ public Builder setVersion(@NonNull String version) {
+ mVersion = version;
+ return this;
+ }
+
+ /** Add the rules to include. */
+ @NonNull
+ public Builder addRules(@NonNull List<Rule> rules) {
+ mRules.addAll(rules);
+ return this;
+ }
+
+ /**
+ * Builds a {@link RuleSet}.
+ *
+ * @throws IllegalArgumentException if version is null
+ */
+ @NonNull
+ public RuleSet build() {
+ checkNotNull(mVersion);
+ return new RuleSet(mVersion, mRules);
+ }
+ }
+}