1 /*
2  * Copyright (C) 2023 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.app.appsearch.aidl;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.RequiresApi;
22 import android.app.appsearch.safeparcel.AbstractSafeParcelable;
23 import android.app.appsearch.safeparcel.SafeParcelable;
24 import android.content.AttributionSource;
25 import android.content.Context;
26 import android.os.Binder;
27 import android.os.Build;
28 import android.os.Build.VERSION;
29 import android.os.Build.VERSION_CODES;
30 import android.os.Parcel;
31 import android.os.Parcelable;
32 import android.os.Process;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 
36 import java.util.Objects;
37 
38 /**
39  * Compatibility version of AttributionSource.
40  *
41  * <p>Refactor AttributionSource to work on older API levels. For Android S+, this class maintains
42  * the original implementation of AttributionSource methods. However, for Android R-, this class
43  * creates a new implementation. Replace calls to AttributionSource with AppSearchAttributionSource.
44  * For a given Context, replace calls to getAttributionSource with createAttributionSource.
45  *
46  * @hide
47  */
48 @SafeParcelable.Class(creator = "AppSearchAttributionSourceCreator")
49 public final class AppSearchAttributionSource extends AbstractSafeParcelable {
50     @NonNull
51     public static final Parcelable.Creator<AppSearchAttributionSource> CREATOR =
52             new AppSearchAttributionSourceCreator();
53 
54     @NonNull private final Compat mCompat;
55 
56     @Nullable
57     @Field(id = 1, getter = "getAttributionSource")
58     private final AttributionSource mAttributionSource;
59 
60     @NonNull
61     @Field(id = 2, getter = "getPackageName")
62     private final String mCallingPackageName;
63 
64     @Field(id = 3, getter = "getUid")
65     private final int mCallingUid;
66 
67     @Field(id = 4, getter = "getPid")
68     private int mCallingPid;
69 
70     private static final int INVALID_PID = -1;
71 
72     /**
73      * Constructs an instance of AppSearchAttributionSource for AbstractSafeParcelable.
74      *
75      * @param attributionSource The attribution source that is accessing permission protected data.
76      * @param callingPackageName The package that is accessing the permission protected data.
77      * @param callingUid The UID that is accessing the permission protected data.
78      */
79     @Constructor
AppSearchAttributionSource( @aramid = 1) @ullable AttributionSource attributionSource, @Param(id = 2) @NonNull String callingPackageName, @Param(id = 3) int callingUid, @Param(id = 4) int callingPid)80     AppSearchAttributionSource(
81             @Param(id = 1) @Nullable AttributionSource attributionSource,
82             @Param(id = 2) @NonNull String callingPackageName,
83             @Param(id = 3) int callingUid,
84             @Param(id = 4) int callingPid) {
85         mAttributionSource = attributionSource;
86         mCallingPackageName = Objects.requireNonNull(callingPackageName);
87         mCallingUid = callingUid;
88         mCallingPid = callingPid;
89         if (VERSION.SDK_INT >= Build.VERSION_CODES.S && mAttributionSource != null) {
90             mCompat = new Api31Impl(mAttributionSource, mCallingPid);
91         } else {
92             // If this object is being constructed as part of a oneway Binder call, getCallingPid
93             // will return 0 instead of the true PID. In that case, invalidate the PID by setting it
94             // to INVALID_PID (-1).
95             final int callingPidFromBinder = Binder.getCallingPid();
96             if (callingPidFromBinder == 0) {
97                 mCallingPid = INVALID_PID;
98             }
99             Api19Impl impl = new Api19Impl(mCallingPackageName, mCallingUid, mCallingPid);
100             impl.enforceCallingUid();
101             impl.enforceCallingPid();
102             mCompat = impl;
103         }
104     }
105 
106     /**
107      * Constructs an instance of AppSearchAttributionSource.
108      *
109      * @param compat The compat version that provides AttributionSource implementation on lower API
110      *     levels.
111      */
AppSearchAttributionSource(@onNull Compat compat)112     private AppSearchAttributionSource(@NonNull Compat compat) {
113         mCompat = Objects.requireNonNull(compat);
114         mAttributionSource = mCompat.getAttributionSource();
115         mCallingPackageName = mCompat.getPackageName();
116         mCallingUid = mCompat.getUid();
117         mCallingPid = mCompat.getPid();
118     }
119 
120     /**
121      * Constructs an instance of AppSearchAttributionSource for testing.
122      *
123      * @param callingPackageName The package that is accessing the permission protected data.
124      * @param callingUid The UID that is accessing the permission protected data.
125      */
126     @VisibleForTesting
AppSearchAttributionSource( @onNull String callingPackageName, int callingUid, int callingPid)127     public AppSearchAttributionSource(
128             @NonNull String callingPackageName, int callingUid, int callingPid) {
129         mCallingPackageName = Objects.requireNonNull(callingPackageName);
130         mCallingUid = callingUid;
131         mCallingPid = callingPid;
132 
133         if (VERSION.SDK_INT >= Build.VERSION_CODES.S) {
134             // This constructor is only used in unit test, AttributionSource#setPid is only
135             // available on 34+.
136             AttributionSource.Builder attributionSourceBuilder =
137                     new AttributionSource.Builder(mCallingUid).setPackageName(mCallingPackageName);
138             if (VERSION.SDK_INT >= VERSION_CODES.UPSIDE_DOWN_CAKE) {
139                 attributionSourceBuilder.setPid(callingPid);
140             }
141             mAttributionSource = attributionSourceBuilder.build();
142             mCompat = new Api31Impl(mAttributionSource, mCallingPid);
143         } else {
144             mAttributionSource = null;
145             mCompat = new Api19Impl(mCallingPackageName, mCallingUid, mCallingPid);
146         }
147     }
148 
149     /**
150      * Provides a backward-compatible wrapper for AttributionSource.
151      *
152      * <p>This method is not supported on devices running SDK <= 30(R) since the AttributionSource
153      * class will not be available.
154      *
155      * @param attributionSource AttributionSource class to wrap, must not be null
156      * @return wrapped class
157      */
158     @RequiresApi(Build.VERSION_CODES.S)
159     @NonNull
toAppSearchAttributionSource( @onNull AttributionSource attributionSource, int pid)160     private static AppSearchAttributionSource toAppSearchAttributionSource(
161             @NonNull AttributionSource attributionSource, int pid) {
162         return new AppSearchAttributionSource(new Api31Impl(attributionSource, pid));
163     }
164 
165     /**
166      * Provides a backward-compatible wrapper for AttributionSource.
167      *
168      * <p>This method is not supported on devices running SDK <= 19(H) since the AttributionSource
169      * class will not be available.
170      *
171      * @param packageName The package name to wrap, must not be null
172      * @param uid The uid to wrap
173      * @return wrapped class
174      */
toAppSearchAttributionSource( @onNull String packageName, int uid, int pid)175     private static AppSearchAttributionSource toAppSearchAttributionSource(
176             @NonNull String packageName, int uid, int pid) {
177         return new AppSearchAttributionSource(new Api19Impl(packageName, uid, pid));
178     }
179 
180     /**
181      * Create an instance of AppSearchAttributionSource.
182      *
183      * @param context Context the application is running on.
184      */
createAttributionSource( @onNull Context context, int callingPid)185     public static AppSearchAttributionSource createAttributionSource(
186             @NonNull Context context, int callingPid) {
187         if (VERSION.SDK_INT >= Build.VERSION_CODES.S) {
188             return toAppSearchAttributionSource(context.getAttributionSource(), callingPid);
189         }
190 
191         return toAppSearchAttributionSource(context.getPackageName(), Process.myUid(), callingPid);
192     }
193 
194     /** Return AttributionSource on Android S+ and return null on Android R-. */
195     @Nullable
getAttributionSource()196     public AttributionSource getAttributionSource() {
197         return mCompat.getAttributionSource();
198     }
199 
200     @NonNull
getPackageName()201     public String getPackageName() {
202         return mCompat.getPackageName();
203     }
204 
getUid()205     public int getUid() {
206         return mCompat.getUid();
207     }
208 
getPid()209     public int getPid() {
210         return mCompat.getPid();
211     }
212 
213     @Override
hashCode()214     public int hashCode() {
215         if (VERSION.SDK_INT >= Build.VERSION_CODES.S) {
216             AttributionSource attributionSource =
217                     Objects.requireNonNull(mCompat.getAttributionSource());
218             return attributionSource.hashCode();
219         }
220 
221         return Objects.hash(mCompat.getUid(), mCompat.getPackageName());
222     }
223 
224     @Override
equals(@ullable Object o)225     public boolean equals(@Nullable Object o) {
226         if (o == null || !(o instanceof AppSearchAttributionSource)) {
227             return false;
228         }
229 
230         AppSearchAttributionSource that = (AppSearchAttributionSource) o;
231         if (VERSION.SDK_INT >= Build.VERSION_CODES.S) {
232             AttributionSource thisAttributionSource =
233                     Objects.requireNonNull(mCompat.getAttributionSource());
234             AttributionSource thatAttributionSource =
235                     Objects.requireNonNull(that.getAttributionSource());
236             return thisAttributionSource.equals(thatAttributionSource)
237                     && (that.getPid() == mCompat.getPid());
238         }
239 
240         return (Objects.equals(mCompat.getPackageName(), that.getPackageName())
241                 && (mCompat.getUid() == that.getUid())
242                 && mCompat.getPid() == that.getPid());
243     }
244 
245     @Override
writeToParcel(@onNull Parcel dest, int flags)246     public void writeToParcel(@NonNull Parcel dest, int flags) {
247         AppSearchAttributionSourceCreator.writeToParcel(this, dest, flags);
248     }
249 
250     /** Compat class for AttributionSource to provide implementation for lower API levels. */
251     private interface Compat {
252         /** The package that is accessing the permission protected data. */
253         @NonNull
getPackageName()254         String getPackageName();
255 
256         /** The attribution source of the app accessing the permission protected data. */
257         @Nullable
getAttributionSource()258         AttributionSource getAttributionSource();
259 
260         /** The UID that is accessing the permission protected data. */
getUid()261         int getUid();
262 
263         /** The PID that is accessing the permission protected data. */
getPid()264         int getPid();
265     }
266 
267     @RequiresApi(VERSION_CODES.S)
268     private static final class Api31Impl implements Compat {
269 
270         private final AttributionSource mAttributionSource;
271         private final int mPid;
272 
273         /**
274          * Creates a new implementation for AppSearchAttributionSource's Compat for API levels 31+.
275          *
276          * @param attributionSource The attribution source that is accessing permission protected
277          *     data.
278          */
Api31Impl(@onNull AttributionSource attributionSource, int pid)279         Api31Impl(@NonNull AttributionSource attributionSource, int pid) {
280             mAttributionSource = attributionSource;
281             mPid = pid;
282         }
283 
284         @Override
285         @NonNull
getPackageName()286         public String getPackageName() {
287             // The {@link AttributionSource} in the constructor is set using
288             // {@link Context#getAttributionSource} and not using the Builder. The
289             // packageName returned from {@link AttributionSource#getPackageName} can be null as
290             // AttributionSource can use either uid and package name to determine who has access
291             // to the data, so either one of them can be null but not both. It is a common practice
292             // to use {@link AttributionSource#getPackageName} without any known issues/bugs. If
293             // we ever receive a null here we will throw a NullPointerException.
294             return Objects.requireNonNull(mAttributionSource.getPackageName());
295         }
296 
297         @Nullable
298         @Override
getAttributionSource()299         public AttributionSource getAttributionSource() {
300             return mAttributionSource;
301         }
302 
303         @Override
getUid()304         public int getUid() {
305             return mAttributionSource.getUid();
306         }
307 
308         @Override
getPid()309         public int getPid() {
310             return mPid;
311         }
312     }
313 
314     private static class Api19Impl implements Compat {
315 
316         @NonNull private final String mPackageName;
317         private final int mUid;
318         private final int mPid;
319 
320         /**
321          * Creates a new implementation for AppSearchAttributionSource's Compat for API levels 19+.
322          *
323          * @param packageName The package name that is accessing permission protected data.
324          * @param uid The uid that is accessing permission protected data.
325          */
Api19Impl(@onNull String packageName, int uid, int pid)326         Api19Impl(@NonNull String packageName, int uid, int pid) {
327             mPackageName = Objects.requireNonNull(packageName);
328             mUid = uid;
329             mPid = pid;
330         }
331 
332         @Override
333         @NonNull
getPackageName()334         public String getPackageName() {
335             return mPackageName;
336         }
337 
338         @Nullable
339         @Override
getAttributionSource()340         public AttributionSource getAttributionSource() {
341             // AttributionSource class was added in Api level 31 and hence it is unavailable on API
342             // levels lower than 31. This class is used in AppSearch to get package name, uid etc,
343             // this implementation has util methods for getPackageName, getUid etc which could
344             // be used instead.
345             return null;
346         }
347 
348         @Override
getUid()349         public int getUid() {
350             return mUid;
351         }
352 
353         @Override
getPid()354         public int getPid() {
355             return mPid;
356         }
357 
358         /**
359          * If you are handling an IPC and you don't trust the caller you need to validate whether
360          * the attribution source is one for the calling app to prevent the caller to pass you a
361          * source from another app without including themselves in the attribution chain.
362          *
363          * @throws SecurityException if the attribution source cannot be trusted to be from the
364          *     caller.
365          */
enforceCallingUid()366         private void enforceCallingUid() {
367             if (!checkCallingUid()) {
368                 int callingUid = Binder.getCallingUid();
369                 throw new SecurityException(
370                         "Calling uid: " + callingUid + " doesn't match source uid: " + mUid);
371             }
372             // The verification for calling package happens in the service during API call.
373         }
374 
375         /**
376          * If you are handling an IPC and you don't trust the caller you need to validate whether
377          * the attribution source is one for the calling app to prevent the caller to pass you a
378          * source from another app without including themselves in the attribution chain.
379          *
380          * @return if the attribution source cannot be trusted to be from the caller.
381          */
checkCallingUid()382         private boolean checkCallingUid() {
383             final int callingUid = Binder.getCallingUid();
384             if (callingUid != mUid) {
385                 return false;
386             }
387             // The verification for calling package happens in the service during API call.
388             return true;
389         }
390 
391         /**
392          * Validate that the pid being claimed for the calling app is not spoofed.
393          *
394          * <p>Note that the PID may be unavailable, for example if we're in a oneway Binder call. In
395          * this case, calling enforceCallingPid is guaranteed to fail. The caller should anticipate
396          * this.
397          *
398          * @throws SecurityException if the attribution source cannot be trusted to be from the
399          *     caller.
400          */
enforceCallingPid()401         private void enforceCallingPid() {
402             if (!checkCallingPid()) {
403                 if (Binder.getCallingPid() == 0) {
404                     throw new SecurityException(
405                             "Calling pid unavailable due to oneway Binder " + "call.");
406                 } else {
407                     throw new SecurityException(
408                             "Calling pid: "
409                                     + Binder.getCallingPid()
410                                     + " doesn't match source pid: "
411                                     + mPid);
412                 }
413             }
414         }
415 
416         /**
417          * Validate that the pid being claimed for the calling app is not spoofed
418          *
419          * @return if the attribution source cannot be trusted to be from the caller.
420          */
checkCallingPid()421         private boolean checkCallingPid() {
422             final int callingPid = Binder.getCallingPid();
423             if (mPid != INVALID_PID && mPid != callingPid) {
424                 // Only call this on the binder thread. If a new thread is created to handle the
425                 // client request, Binder.getCallingPid() will return the thread's own pid.
426                 return false;
427             }
428             return true;
429         }
430     }
431 }
432