/* * 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 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
* 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
* 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