/* * Copyright (C) 2021 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; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.ActivityThread; import android.app.AppGlobals; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; import android.os.Process; import android.os.UserHandle; import android.permission.PermissionManager; import android.permission.flags.Flags; import android.util.ArraySet; import com.android.internal.annotations.Immutable; import java.util.Arrays; import java.util.Collections; import java.util.Objects; import java.util.Set; /** * This class represents a source to which access to permission protected data should be * attributed. Attribution sources can be chained to represent cases where the protected * data would flow through several applications. For example, app A may ask app B for * contacts and in turn app B may ask app C for contacts. In this case, the attribution * chain would be A -> B -> C and the data flow would be C -> B -> A. There are two * main benefits of using the attribution source mechanism: avoid doing explicit permission * checks on behalf of the calling app if you are accessing private data on their behalf * to send back; avoid double data access blaming which happens as you check the calling * app's permissions and when you access the data behind these permissions (for runtime * permissions). Also if not explicitly blaming the caller the data access would be * counted towards your app vs to the previous app where yours was just a proxy. *

* Every {@link Context} has an attribution source and you can get it via {@link * Context#getAttributionSource()} representing itself, which is a chain of one. You * can attribute work to another app, or more precisely to a chain of apps, through * which the data you would be accessing would flow, via {@link Context#createContext( * ContextParams)} plus specifying an attribution source for the next app to receive * the protected data you are accessing via {@link AttributionSource.Builder#setNext( * AttributionSource)}. Creating this attribution chain ensures that the datasource would * check whether every app in the attribution chain has permission to access the data * before releasing it. The datasource will also record appropriately that this data was * accessed by the apps in the sequence if the data is behind a sensitive permission * (e.g. dangerous). Again, this is useful if you are accessing the data on behalf of another * app, for example a speech recognizer using the mic so it can provide recognition to * a calling app. *

* You can create an attribution chain of you and any other app without any verification * as this is something already available via the {@link android.app.AppOpsManager} APIs. * This is supported to handle cases where you don't have access to the caller's attribution * source and you can directly use the {@link AttributionSource.Builder} APIs. However, * if the data flows through more than two apps (more than you access the data for the * caller) you need to have a handle to the {@link AttributionSource} for the calling app's * context in order to create an attribution context. This means you either need to have an * API for the other app to send you its attribution source or use a platform API that pipes * the callers attribution source. *

* You cannot forge an attribution chain without the participation of every app in the * attribution chain (aside of the special case mentioned above). To create an attribution * source that is trusted you need to create an attribution context that points to an * attribution source that was explicitly created by the app that it refers to, recursively. *

* Since creating an attribution context leads to all permissions for apps in the attribution * chain being checked, you need to expect getting a security exception when accessing * permission protected APIs since some app in the chain may not have the permission. */ @Immutable public final class AttributionSource implements Parcelable { private static final String DESCRIPTOR = "android.content.AttributionSource"; private static final Binder sDefaultToken = new Binder(DESCRIPTOR); private final @NonNull AttributionSourceState mAttributionSourceState; private @Nullable AttributionSource mNextCached; private @Nullable Set mRenouncedPermissionsCached; /** @hide */ @TestApi public AttributionSource(int uid, @Nullable String packageName, @Nullable String attributionTag) { this(uid, Process.INVALID_PID, packageName, attributionTag, sDefaultToken); } /** @hide */ public AttributionSource(int uid, @Nullable String packageName, @Nullable String attributionTag, int virtualDeviceId) { this(uid, Process.INVALID_PID, packageName, attributionTag, sDefaultToken, null, virtualDeviceId, null); } /** @hide */ public AttributionSource(int uid, int pid, @Nullable String packageName, @Nullable String attributionTag) { this(uid, pid, packageName, attributionTag, sDefaultToken); } /** @hide */ @TestApi public AttributionSource(int uid, @Nullable String packageName, @Nullable String attributionTag, @NonNull IBinder token) { this(uid, Process.INVALID_PID, packageName, attributionTag, token, /*renouncedPermissions*/ null, Context.DEVICE_ID_DEFAULT, /*next*/ null); } /** @hide */ public AttributionSource(int uid, int pid, @Nullable String packageName, @Nullable String attributionTag, @NonNull IBinder token) { this(uid, pid, packageName, attributionTag, token, /*renouncedPermissions*/ null, Context.DEVICE_ID_DEFAULT, /*next*/ null); } /** @hide */ @TestApi public AttributionSource(int uid, @Nullable String packageName, @Nullable String attributionTag, @Nullable Set renouncedPermissions, @Nullable AttributionSource next) { this(uid, Process.INVALID_PID, packageName, attributionTag, sDefaultToken, (renouncedPermissions != null) ? renouncedPermissions.toArray(new String[0]) : null, Context.DEVICE_ID_DEFAULT, /*next*/ next); } /** @hide */ public AttributionSource(@NonNull AttributionSource current, @Nullable AttributionSource next) { this(current.getUid(), current.getPid(), current.getPackageName(), current.getAttributionTag(), current.getToken(), current.mAttributionSourceState.renouncedPermissions, current.getDeviceId(), next); } /** @hide */ public AttributionSource(int uid, int pid, @Nullable String packageName, @Nullable String attributionTag, @Nullable String[] renouncedPermissions, int deviceId, @Nullable AttributionSource next) { this(uid, pid, packageName, attributionTag, sDefaultToken, renouncedPermissions, deviceId, next); } /** @hide */ @TestApi @FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED) public AttributionSource(int uid, int pid, @Nullable String packageName, @Nullable String attributionTag, @NonNull IBinder token, @Nullable String[] renouncedPermissions, int deviceId, @Nullable AttributionSource next) { mAttributionSourceState = new AttributionSourceState(); mAttributionSourceState.uid = uid; mAttributionSourceState.pid = pid; mAttributionSourceState.token = token; mAttributionSourceState.packageName = packageName; mAttributionSourceState.attributionTag = attributionTag; mAttributionSourceState.renouncedPermissions = renouncedPermissions; mAttributionSourceState.deviceId = deviceId; mAttributionSourceState.next = (next != null) ? new AttributionSourceState[] {next.mAttributionSourceState} : new AttributionSourceState[0]; } AttributionSource(@NonNull Parcel in) { this(AttributionSourceState.CREATOR.createFromParcel(in)); if (!Binder.isDirectlyHandlingTransaction()) { throw new SecurityException("AttributionSource should be unparceled during a binder " + "transaction for proper verification."); } // Since we just unpacked this object as part of it transiting a Binder // call, this is the perfect time to enforce that its UID and PID can be trusted enforceCallingUid(); // If this object is being constructed as part of a oneway Binder call, getCallingPid will // return 0 instead of the true PID. In that case, invalidate the PID by setting it to // INVALID_PID (-1). final int callingPid = Binder.getCallingPid(); if (callingPid == 0) { mAttributionSourceState.pid = Process.INVALID_PID; } enforceCallingPid(); } /** @hide */ public AttributionSource(@NonNull AttributionSourceState attributionSourceState) { mAttributionSourceState = attributionSourceState; } /** @hide */ public AttributionSource withNextAttributionSource(@Nullable AttributionSource next) { return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(), getToken(), mAttributionSourceState.renouncedPermissions, getDeviceId(), next); } /** @hide */ public AttributionSource withPackageName(@Nullable String packageName) { return new AttributionSource(getUid(), getPid(), packageName, getAttributionTag(), getToken(), mAttributionSourceState.renouncedPermissions, getDeviceId(), getNext()); } /** @hide */ public AttributionSource withToken(@NonNull IBinder token) { return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(), token, mAttributionSourceState.renouncedPermissions, getDeviceId(), getNext()); } /** @hide */ public AttributionSource withDefaultToken() { return withToken(sDefaultToken); } /** @hide */ public AttributionSource withPid(int pid) { return new AttributionSource(getUid(), pid, getPackageName(), getAttributionTag(), getToken(), mAttributionSourceState.renouncedPermissions, getDeviceId(), getNext()); } /** @hide */ public AttributionSource withDeviceId(int deviceId) { return new AttributionSource(getUid(), getPid(), getPackageName(), getAttributionTag(), getToken(), mAttributionSourceState.renouncedPermissions, deviceId, getNext()); } /** @hide */ public @NonNull AttributionSourceState asState() { return mAttributionSourceState; } /** @hide */ public @NonNull ScopedParcelState asScopedParcelState() { return new ScopedParcelState(this); } /** * Returns a generic {@link AttributionSource} that represents the entire * calling process. * *

Callers are strongly encouraged to use a more specific * attribution source whenever possible, such as from * {@link Context#getAttributionSource()}, since that enables developers to * have more detailed and scoped control over attribution within * sub-components of their app. * * @see Context#createAttributionContext(String) * @see Context#getAttributionTag() * @return a generic {@link AttributionSource} representing the entire * calling process * @throws IllegalStateException when no accurate {@link AttributionSource} * can be determined */ public static @NonNull AttributionSource myAttributionSource() { final AttributionSource globalSource = ActivityThread.currentAttributionSource(); if (globalSource != null) { return globalSource; } int uid = Process.myUid(); if (uid == Process.ROOT_UID) { uid = Process.SYSTEM_UID; } try { return new AttributionSource.Builder(uid) .setPid(Process.myPid()) .setDeviceId(Context.DEVICE_ID_DEFAULT) .setPackageName(AppGlobals.getPackageManager().getPackagesForUid(uid)[0]) .build(); } catch (Exception ignored) { } throw new IllegalStateException("Failed to resolve AttributionSource"); } /** * This is a scoped object that exposes the content of an attribution source * as a parcel. This is useful when passing one to native and avoid custom * conversion logic from Java to native state that needs to be kept in sync * as attribution source evolves. This way we use the same logic for passing * to native as the ones for passing in an IPC - in both cases this is the * same auto generated code. * * @hide */ public static class ScopedParcelState implements AutoCloseable { private final Parcel mParcel; public @NonNull Parcel getParcel() { return mParcel; } public ScopedParcelState(AttributionSource attributionSource) { mParcel = Parcel.obtain(); attributionSource.writeToParcel(mParcel, 0); mParcel.setDataPosition(0); } public void close() { mParcel.recycle(); } } /** * If you are handling an IPC and you don't trust the caller you need to validate * whether the attribution source is one for the calling app to prevent the caller * to pass you a source from another app without including themselves in the * attribution chain. * * @throws SecurityException if the attribution source cannot be trusted to be from the caller. */ public void enforceCallingUid() { if (!checkCallingUid()) { throw new SecurityException("Calling uid: " + Binder.getCallingUid() + " doesn't match source uid: " + mAttributionSourceState.uid); } // No need to check package as app ops manager does it already. } /** * If you are handling an IPC and you don't trust the caller you need to validate * whether the attribution source is one for the calling app to prevent the caller * to pass you a source from another app without including themselves in the * attribution chain. * * @return if the attribution source cannot be trusted to be from the caller. */ public boolean checkCallingUid() { final int callingUid = Binder.getCallingUid(); if (callingUid != Process.ROOT_UID && UserHandle.getAppId(callingUid) != Process.SYSTEM_UID && callingUid != mAttributionSourceState.uid) { return false; } // No need to check package as app ops manager does it already. return true; } /** * Validate that the pid being claimed for the calling app is not spoofed. * * Note that the PID may be unavailable, for example if we're in a oneway Binder call. In this * case, calling enforceCallingPid is guaranteed to fail. The caller should anticipate this. * * @throws SecurityException if the attribution source cannot be trusted to be from the caller. * @hide */ @TestApi public void enforceCallingPid() { if (!checkCallingPid()) { if (Binder.getCallingPid() == 0) { throw new SecurityException("Calling pid unavailable due to oneway Binder call."); } else { throw new SecurityException("Calling pid: " + Binder.getCallingPid() + " doesn't match source pid: " + mAttributionSourceState.pid); } } } /** * Validate that the pid being claimed for the calling app is not spoofed * * @return if the attribution source cannot be trusted to be from the caller. */ private boolean checkCallingPid() { final int callingPid = Binder.getCallingPid(); if (mAttributionSourceState.pid != Process.INVALID_PID && callingPid != mAttributionSourceState.pid) { return false; } return true; } @Override public String toString() { if (Build.IS_DEBUGGABLE) { return "AttributionSource { " + "uid = " + mAttributionSourceState.uid + ", " + "packageName = " + mAttributionSourceState.packageName + ", " + "attributionTag = " + mAttributionSourceState.attributionTag + ", " + "token = " + mAttributionSourceState.token + ", " + "deviceId = " + mAttributionSourceState.deviceId + ", " + "next = " + (mAttributionSourceState.next != null && mAttributionSourceState.next.length > 0 ? new AttributionSource(mAttributionSourceState.next[0]).toString() : null) + " }"; } return super.toString(); } /** * @return The next UID that would receive the permission protected data. * * @hide */ public int getNextUid() { if (mAttributionSourceState.next != null && mAttributionSourceState.next.length > 0) { return mAttributionSourceState.next[0].uid; } return Process.INVALID_UID; } /** * @return The next package that would receive the permission protected data. * * @hide */ public @Nullable String getNextPackageName() { if (mAttributionSourceState.next != null && mAttributionSourceState.next.length > 0) { return mAttributionSourceState.next[0].packageName; } return null; } /** * @return The next package's attribution tag that would receive * the permission protected data. * * @hide */ public @Nullable String getNextAttributionTag() { if (mAttributionSourceState.next != null && mAttributionSourceState.next.length > 0) { return mAttributionSourceState.next[0].attributionTag; } return null; } /** * @return The next package's token that would receive * the permission protected data. * * @hide */ public @Nullable IBinder getNextToken() { if (mAttributionSourceState.next != null && mAttributionSourceState.next.length > 0) { return mAttributionSourceState.next[0].token; } return null; } /** * @return The next package's device Id from its context. * This device ID is used for permissions checking during attribution source validation. * * @hide */ public int getNextDeviceId() { if (mAttributionSourceState.next != null && mAttributionSourceState.next.length > 0) { return mAttributionSourceState.next[0].deviceId; } return Context.DEVICE_ID_DEFAULT; } /** * Checks whether this attribution source can be trusted. That is whether * the app it refers to created it and provided to the attribution chain. * * @param context Context handle. * @return Whether this is a trusted source. */ public boolean isTrusted(@NonNull Context context) { return mAttributionSourceState.token != null && context.getSystemService(PermissionManager.class) .isRegisteredAttributionSource(this); } /** * Permissions that should be considered revoked regardless if granted. * * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.RENOUNCE_PERMISSIONS) @NonNull public Set getRenouncedPermissions() { if (mRenouncedPermissionsCached == null) { if (mAttributionSourceState.renouncedPermissions != null) { mRenouncedPermissionsCached = new ArraySet<>( mAttributionSourceState.renouncedPermissions); } else { mRenouncedPermissionsCached = Collections.emptySet(); } } return mRenouncedPermissionsCached; } /** * The UID that is accessing the permission protected data. */ public int getUid() { return mAttributionSourceState.uid; } /** * The PID that is accessing the permission protected data. */ public int getPid() { return mAttributionSourceState.pid; } /** * The package that is accessing the permission protected data. */ public @Nullable String getPackageName() { return mAttributionSourceState.packageName; } /** * The attribution tag of the app accessing the permission protected data. */ public @Nullable String getAttributionTag() { return mAttributionSourceState.attributionTag; } /** * Gets the device ID for this attribution source. Attribution source can set the device ID * using {@link Builder#setDeviceId(int)}, the default device ID is * {@link Context#DEVICE_ID_DEFAULT}. *

* This device ID is used for permissions checking during attribution source validation. */ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED) public int getDeviceId() { return mAttributionSourceState.deviceId; } /** * Unique token for that source. * * @hide */ public @NonNull IBinder getToken() { return mAttributionSourceState.token; } /** * The next app to receive the permission protected data. */ public @Nullable AttributionSource getNext() { if (mNextCached == null && mAttributionSourceState.next != null && mAttributionSourceState.next.length > 0) { mNextCached = new AttributionSource(mAttributionSourceState.next[0]); } return mNextCached; } @Override public boolean equals(@Nullable Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; AttributionSource that = (AttributionSource) o; return equalsExceptToken(that) && Objects.equals( mAttributionSourceState.token, that.mAttributionSourceState.token); } /** * We store trusted attribution sources without their token (the token is the key to the map) * to avoid having a strong reference to the token. This means, when checking the equality of a * supplied AttributionSource in PermissionManagerService.isTrustedAttributionSource, we want to * compare everything except the token. * * @hide */ public boolean equalsExceptToken(@Nullable AttributionSource o) { if (o == null) return false; return mAttributionSourceState.uid == o.mAttributionSourceState.uid && Objects.equals(mAttributionSourceState.packageName, o.mAttributionSourceState.packageName) && Objects.equals(mAttributionSourceState.attributionTag, o.mAttributionSourceState.attributionTag) && Arrays.equals(mAttributionSourceState.renouncedPermissions, o.mAttributionSourceState.renouncedPermissions) && Objects.equals(getNext(), o.getNext()); } @Override public int hashCode() { return Objects.hash(mAttributionSourceState.uid, mAttributionSourceState.packageName, mAttributionSourceState.attributionTag, mAttributionSourceState.token, Arrays.hashCode(mAttributionSourceState.renouncedPermissions), getNext()); } @Override public void writeToParcel(@NonNull Parcel dest, int flags) { mAttributionSourceState.writeToParcel(dest, flags); } @Override public int describeContents() { return 0; } public static final @NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public AttributionSource[] newArray(int size) { return new AttributionSource[size]; } @Override public AttributionSource createFromParcel(@NonNull Parcel in) { return new AttributionSource(in); } }; /** * A builder for {@link AttributionSource} */ public static final class Builder { private @NonNull final AttributionSourceState mAttributionSourceState = new AttributionSourceState(); private long mBuilderFieldsSet = 0L; /** * Creates a new Builder. * * @param uid * The UID that is accessing the permission protected data. */ public Builder(int uid) { mAttributionSourceState.uid = uid; } public Builder(@NonNull AttributionSource current) { if (current == null) { throw new IllegalArgumentException("current AttributionSource can not be null"); } mAttributionSourceState.uid = current.getUid(); mAttributionSourceState.pid = current.getPid(); mAttributionSourceState.packageName = current.getPackageName(); mAttributionSourceState.attributionTag = current.getAttributionTag(); mAttributionSourceState.token = current.getToken(); mAttributionSourceState.renouncedPermissions = current.mAttributionSourceState.renouncedPermissions; } /** * The PID of the process that is accessing the permission protected data. * * If not called, pid will default to {@link Process@INVALID_PID} (-1). This indicates that * the PID data is missing. Supplying a PID is not required, but recommended when * accessible. */ public @NonNull Builder setPid(int value) { checkNotUsed(); mBuilderFieldsSet |= 0x2; mAttributionSourceState.pid = value; return this; } /** * The package that is accessing the permission protected data. */ public @NonNull Builder setPackageName(@Nullable String value) { checkNotUsed(); mBuilderFieldsSet |= 0x4; mAttributionSourceState.packageName = value; return this; } /** * The attribution tag of the app accessing the permission protected data. */ public @NonNull Builder setAttributionTag(@Nullable String value) { checkNotUsed(); mBuilderFieldsSet |= 0x8; mAttributionSourceState.attributionTag = value; return this; } /** * Sets permissions which have been voluntarily "renounced" by the * calling app. *

* Interactions performed through services obtained from the created * Context will ideally be treated as if these "renounced" permissions * have not actually been granted to the app, regardless of their actual * grant status. *

* This is designed for use by separate logical components within an app * which have no intention of interacting with data or services that are * protected by the renounced permissions. *

* Note that only {@link PermissionInfo#PROTECTION_DANGEROUS} * permissions are supported by this mechanism. Additionally, this * mechanism only applies to calls made through services obtained via * {@link Context#getSystemService}; it has no effect on static or raw * Binder calls. * * @param renouncedPermissions The set of permissions to treat as * renounced, which is as if not granted. * @return This builder. * @hide */ @SystemApi @RequiresPermission(android.Manifest.permission.RENOUNCE_PERMISSIONS) public @NonNull Builder setRenouncedPermissions(@Nullable Set value) { checkNotUsed(); mBuilderFieldsSet |= 0x10; mAttributionSourceState.renouncedPermissions = (value != null) ? value.toArray(new String[0]) : null; return this; } /** * Set the device ID for this attribution source, permission check would happen * against this device ID. * * @return the builder */ @FlaggedApi(Flags.FLAG_DEVICE_AWARE_PERMISSION_APIS_ENABLED) public @NonNull Builder setDeviceId(int deviceId) { checkNotUsed(); mBuilderFieldsSet |= 0x12; mAttributionSourceState.deviceId = deviceId; return this; } /** * The next app to receive the permission protected data. */ public @NonNull Builder setNext(@Nullable AttributionSource value) { checkNotUsed(); mBuilderFieldsSet |= 0x20; mAttributionSourceState.next = (value != null) ? new AttributionSourceState[] {value.mAttributionSourceState} : mAttributionSourceState.next; return this; } /** * The next app to receive the permission protected data. */ @FlaggedApi(Flags.FLAG_SET_NEXT_ATTRIBUTION_SOURCE) public @NonNull Builder setNextAttributionSource(@NonNull AttributionSource value) { checkNotUsed(); if (value == null) { throw new IllegalArgumentException("Null AttributionSource not permitted."); } mBuilderFieldsSet |= 0x20; mAttributionSourceState.next = new AttributionSourceState[]{value.mAttributionSourceState}; return this; } /** Builds the instance. This builder should not be touched after calling this! */ public @NonNull AttributionSource build() { checkNotUsed(); mBuilderFieldsSet |= 0x40; // Mark builder used if ((mBuilderFieldsSet & 0x2) == 0) { mAttributionSourceState.pid = Process.INVALID_PID; } if ((mBuilderFieldsSet & 0x4) == 0) { mAttributionSourceState.packageName = null; } if ((mBuilderFieldsSet & 0x8) == 0) { mAttributionSourceState.attributionTag = null; } if ((mBuilderFieldsSet & 0x10) == 0) { mAttributionSourceState.renouncedPermissions = null; } if ((mBuilderFieldsSet & 0x12) == 0) { mAttributionSourceState.deviceId = Context.DEVICE_ID_DEFAULT; } if ((mBuilderFieldsSet & 0x20) == 0) { mAttributionSourceState.next = null; } mAttributionSourceState.token = sDefaultToken; if (mAttributionSourceState.next == null) { // The NDK aidl backend doesn't support null parcelable arrays. mAttributionSourceState.next = new AttributionSourceState[0]; } return new AttributionSource(mAttributionSourceState); } private void checkNotUsed() { if ((mBuilderFieldsSet & 0x40) != 0) { throw new IllegalStateException( "This Builder should not be reused. Use a new Builder instance instead"); } } } }