• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 com.android.server.companion;
18 
19 import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
20 import static android.app.PendingIntent.FLAG_IMMUTABLE;
21 import static android.app.PendingIntent.FLAG_ONE_SHOT;
22 import static android.companion.CompanionDeviceManager.COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME;
23 import static android.content.ComponentName.createRelative;
24 
25 import static com.android.server.companion.CompanionDeviceManagerService.DEBUG;
26 import static com.android.server.companion.PackageUtils.enforceUsesCompanionDeviceFeature;
27 import static com.android.server.companion.PermissionsUtils.enforcePermissionsForAssociation;
28 import static com.android.server.companion.RolesUtils.isRoleHolder;
29 
30 import static java.util.Objects.requireNonNull;
31 
32 import android.annotation.NonNull;
33 import android.annotation.Nullable;
34 import android.annotation.SuppressLint;
35 import android.annotation.UserIdInt;
36 import android.app.PendingIntent;
37 import android.companion.AssociationInfo;
38 import android.companion.AssociationRequest;
39 import android.companion.IAssociationRequestCallback;
40 import android.content.ComponentName;
41 import android.content.Context;
42 import android.content.Intent;
43 import android.content.IntentSender;
44 import android.content.pm.PackageManagerInternal;
45 import android.content.pm.Signature;
46 import android.net.MacAddress;
47 import android.os.Binder;
48 import android.os.Bundle;
49 import android.os.Handler;
50 import android.os.Parcel;
51 import android.os.RemoteException;
52 import android.os.ResultReceiver;
53 import android.os.UserHandle;
54 import android.util.Log;
55 import android.util.PackageUtils;
56 import android.util.Slog;
57 
58 import com.android.internal.util.ArrayUtils;
59 
60 import java.util.Arrays;
61 import java.util.HashSet;
62 import java.util.List;
63 import java.util.Set;
64 
65 /**
66  * Class responsible for handling incoming {@link AssociationRequest}s.
67  * The main responsibilities of an {@link AssociationRequestsProcessor} are:
68  * <ul>
69  * <li> Requests validation and checking if the package that would own the association holds all
70  * necessary permissions.
71  * <li> Communication with the requester via a provided
72  * {@link android.companion.CompanionDeviceManager.Callback}.
73  * <li> Constructing an {@link Intent} for collecting user's approval (if needed), and handling the
74  * approval.
75  * <li> Calling to {@link CompanionDeviceManagerService} to create an association when/if the
76  * request was found valid and was approved by user.
77  * </ul>
78  *
79  * The class supports two variants of the "Association Flow": the full variant, and the shortened
80  * (a.k.a. No-UI) variant.
81  * Both flows start similarly: in
82  * {@link #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)}
83  * invoked from
84  * {@link CompanionDeviceManagerService.CompanionDeviceManagerImpl#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
85  * method call.
86  * Then an {@link AssociationRequestsProcessor} makes a decision whether user's confirmation is
87  * required.
88  *
89  * If the user's approval is NOT required: an {@link AssociationRequestsProcessor} invokes
90  * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}
91  * which after calling to  {@link CompanionDeviceManagerService} to create an association, notifies
92  * the requester via
93  * {@link android.companion.CompanionDeviceManager.Callback#onAssociationCreated(AssociationInfo)}.
94  *
95  * If the user's approval is required: an {@link AssociationRequestsProcessor} constructs a
96  * {@link PendingIntent} for the approval UI and sends it back to the requester via
97  * {@link android.companion.CompanionDeviceManager.Callback#onAssociationPending(IntentSender)}.
98  * When/if user approves the request,  {@link AssociationRequestsProcessor} receives a "callback"
99  * from the Approval UI in via {@link #mOnRequestConfirmationReceiver} and invokes
100  * {@link #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback, ResultReceiver, MacAddress)}
101  * which one more time checks that the packages holds all necessary permissions before proceeding to
102  * {@link #createAssociationAndNotifyApplication(AssociationRequest, String, int, MacAddress, IAssociationRequestCallback)}.
103  *
104  * @see #processNewAssociationRequest(AssociationRequest, String, int, IAssociationRequestCallback)
105  * @see #processAssociationRequestApproval(AssociationRequest, IAssociationRequestCallback,
106  * ResultReceiver, MacAddress)
107  */
108 @SuppressLint("LongLogTag")
109 class AssociationRequestsProcessor {
110     private static final String TAG = "CompanionDevice_AssociationRequestsProcessor";
111 
112     private static final ComponentName ASSOCIATION_REQUEST_APPROVAL_ACTIVITY =
113             createRelative(COMPANION_DEVICE_DISCOVERY_PACKAGE_NAME, ".CompanionDeviceActivity");
114 
115     // AssociationRequestsProcessor <-> UI
116     private static final String EXTRA_APPLICATION_CALLBACK = "application_callback";
117     private static final String EXTRA_ASSOCIATION_REQUEST = "association_request";
118     private static final String EXTRA_RESULT_RECEIVER = "result_receiver";
119 
120     // AssociationRequestsProcessor -> UI
121     private static final int RESULT_CODE_ASSOCIATION_CREATED = 0;
122     private static final String EXTRA_ASSOCIATION = "association";
123 
124     // UI -> AssociationRequestsProcessor
125     private static final int RESULT_CODE_ASSOCIATION_APPROVED = 0;
126     private static final String EXTRA_MAC_ADDRESS = "mac_address";
127 
128     private static final int ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW = 5;
129     private static final long ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS = 60 * 60 * 1000; // 60 min;
130 
131     private final @NonNull Context mContext;
132     private final @NonNull CompanionDeviceManagerService mService;
133     private final @NonNull PackageManagerInternal mPackageManager;
134     private final @NonNull AssociationStore mAssociationStore;
135 
AssociationRequestsProcessor(@onNull CompanionDeviceManagerService service, @NonNull AssociationStore associationStore)136     AssociationRequestsProcessor(@NonNull CompanionDeviceManagerService service,
137             @NonNull AssociationStore associationStore) {
138         mContext = service.getContext();
139         mService = service;
140         mPackageManager = service.mPackageManagerInternal;
141         mAssociationStore = associationStore;
142     }
143 
144     /**
145      * Handle incoming {@link AssociationRequest}s, sent via
146      * {@link android.companion.ICompanionDeviceManager#associate(AssociationRequest, IAssociationRequestCallback, String, int)}
147      */
processNewAssociationRequest(@onNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @NonNull IAssociationRequestCallback callback)148     void processNewAssociationRequest(@NonNull AssociationRequest request,
149             @NonNull String packageName, @UserIdInt int userId,
150             @NonNull IAssociationRequestCallback callback) {
151         requireNonNull(request, "Request MUST NOT be null");
152         if (request.isSelfManaged()) {
153             requireNonNull(request.getDisplayName(), "AssociationRequest.displayName "
154                     + "MUST NOT be null.");
155         }
156         requireNonNull(packageName, "Package name MUST NOT be null");
157         requireNonNull(callback, "Callback MUST NOT be null");
158 
159         final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
160         if (DEBUG) {
161             Slog.d(TAG, "processNewAssociationRequest() "
162                     + "request=" + request + ", "
163                     + "package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")");
164         }
165 
166         // 1. Enforce permissions and other requirements.
167         enforcePermissionsForAssociation(mContext, request, packageUid);
168         enforceUsesCompanionDeviceFeature(mContext, userId, packageName);
169 
170         // 2. Check if association can be created without launching UI (i.e. CDM needs NEITHER
171         // to perform discovery NOR to collect user consent).
172         if (request.isSelfManaged() && !request.isForceConfirmation()
173                 && !willAddRoleHolder(request, packageName, userId)) {
174             // 2a. Create association right away.
175             createAssociationAndNotifyApplication(request, packageName, userId,
176                     /*macAddress*/ null, callback);
177             return;
178         }
179 
180         // 2b. Build a PendingIntent for launching the confirmation UI, and send it back to the app:
181 
182         // 2b.1. Populate the request with required info.
183         request.setPackageName(packageName);
184         request.setUserId(userId);
185         request.setSkipPrompt(mayAssociateWithoutPrompt(packageName, userId));
186 
187         // 2b.2. Prepare extras and create an Intent.
188         final Bundle extras = new Bundle();
189         extras.putParcelable(EXTRA_ASSOCIATION_REQUEST, request);
190         extras.putBinder(EXTRA_APPLICATION_CALLBACK, callback.asBinder());
191         extras.putParcelable(EXTRA_RESULT_RECEIVER, prepareForIpc(mOnRequestConfirmationReceiver));
192 
193         final Intent intent = new Intent();
194         intent.setComponent(ASSOCIATION_REQUEST_APPROVAL_ACTIVITY);
195         intent.putExtras(extras);
196 
197         // 2b.3. Create a PendingIntent.
198         final PendingIntent pendingIntent;
199         final long token = Binder.clearCallingIdentity();
200         try {
201             // Using uid of the application that will own the association (usually the same
202             // application that sent the request) allows us to have multiple "pending" association
203             // requests at the same time.
204             // If the application already has a pending association request, that PendingIntent
205             // will be cancelled.
206             pendingIntent = PendingIntent.getActivityAsUser(
207                     mContext, /*requestCode */ packageUid, intent,
208                     FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE,
209                     /* options= */ null, UserHandle.CURRENT);
210         } finally {
211             Binder.restoreCallingIdentity(token);
212         }
213 
214         // 2b.4. Send the PendingIntent back to the app.
215         try {
216             callback.onAssociationPending(pendingIntent);
217         } catch (RemoteException ignore) { }
218     }
219 
processAssociationRequestApproval(@onNull AssociationRequest request, @NonNull IAssociationRequestCallback callback, @NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress)220     private void processAssociationRequestApproval(@NonNull AssociationRequest request,
221             @NonNull IAssociationRequestCallback callback,
222             @NonNull ResultReceiver resultReceiver, @Nullable MacAddress macAddress) {
223         final String packageName = request.getPackageName();
224         final int userId = request.getUserId();
225         final int packageUid = mPackageManager.getPackageUid(packageName, 0, userId);
226 
227         if (DEBUG) {
228             Slog.d(TAG, "processAssociationRequestApproval()\n"
229                     + "   package=u" + userId + "/" + packageName + " (uid=" + packageUid + ")\n"
230                     + "   request=" + request + "\n"
231                     + "   macAddress=" + macAddress + "\n");
232         }
233 
234         // 1. Need to check permissions again in case something changed, since we first received
235         // this request.
236         try {
237             enforcePermissionsForAssociation(mContext, request, packageUid);
238         } catch (SecurityException e) {
239             // Since, at this point the caller is our own UI, we need to catch the exception on
240             // forward it back to the application via the callback.
241             try {
242                 callback.onFailure(e.getMessage());
243             } catch (RemoteException ignore) { }
244             return;
245         }
246 
247         // 2. Create association and notify the application.
248         final AssociationInfo association = createAssociationAndNotifyApplication(
249                 request, packageName, userId, macAddress, callback);
250 
251         // 3. Send the association back the Approval Activity, so that it can report back to the app
252         // via Activity.setResult().
253         final Bundle data = new Bundle();
254         data.putParcelable(EXTRA_ASSOCIATION, association);
255         resultReceiver.send(RESULT_CODE_ASSOCIATION_CREATED, data);
256     }
257 
createAssociationAndNotifyApplication( @onNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId, @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback)258     private AssociationInfo createAssociationAndNotifyApplication(
259             @NonNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId,
260             @Nullable MacAddress macAddress, @NonNull IAssociationRequestCallback callback) {
261         final AssociationInfo association;
262         final long callingIdentity = Binder.clearCallingIdentity();
263         try {
264             association = mService.createAssociation(userId, packageName, macAddress,
265                     request.getDisplayName(), request.getDeviceProfile(), request.isSelfManaged());
266         } finally {
267             Binder.restoreCallingIdentity(callingIdentity);
268         }
269 
270         try {
271             callback.onAssociationCreated(association);
272         } catch (RemoteException ignore) { }
273 
274         return association;
275     }
276 
willAddRoleHolder(@onNull AssociationRequest request, @NonNull String packageName, @UserIdInt int userId)277     private boolean willAddRoleHolder(@NonNull AssociationRequest request,
278             @NonNull String packageName, @UserIdInt int userId) {
279         final String deviceProfile = request.getDeviceProfile();
280         if (deviceProfile == null) return false;
281 
282         final boolean isRoleHolder = Binder.withCleanCallingIdentity(
283                 () -> isRoleHolder(mContext, userId, packageName, deviceProfile));
284 
285         // Don't need to "grant" the role, if the package already holds the role.
286         return !isRoleHolder;
287     }
288 
289     private final ResultReceiver mOnRequestConfirmationReceiver =
290             new ResultReceiver(Handler.getMain()) {
291         @Override
292         protected void onReceiveResult(int resultCode, Bundle data) {
293             if (DEBUG) {
294                 Slog.d(TAG, "mOnRequestConfirmationReceiver.onReceiveResult() "
295                         + "code=" + resultCode + ", " + "data=" + data);
296             }
297 
298             if (resultCode != RESULT_CODE_ASSOCIATION_APPROVED) {
299                 Slog.w(TAG, "Unknown result code:" + resultCode);
300                 return;
301             }
302 
303             final AssociationRequest request = data.getParcelable(EXTRA_ASSOCIATION_REQUEST);
304             final IAssociationRequestCallback callback = IAssociationRequestCallback.Stub
305                     .asInterface(data.getBinder(EXTRA_APPLICATION_CALLBACK));
306             final ResultReceiver resultReceiver = data.getParcelable(EXTRA_RESULT_RECEIVER);
307 
308             requireNonNull(request);
309             requireNonNull(callback);
310             requireNonNull(resultReceiver);
311 
312             final MacAddress macAddress;
313             if (request.isSelfManaged()) {
314                 macAddress = null;
315             } else {
316                 macAddress = data.getParcelable(EXTRA_MAC_ADDRESS);
317                 requireNonNull(macAddress);
318             }
319 
320             processAssociationRequestApproval(request, callback, resultReceiver, macAddress);
321         }
322     };
323 
mayAssociateWithoutPrompt(@onNull String packageName, @UserIdInt int userId)324     private boolean mayAssociateWithoutPrompt(@NonNull String packageName, @UserIdInt int userId) {
325         // Below we check if the requesting package is allowlisted (usually by the OEM) for creating
326         // CDM associations without user confirmation (prompt).
327         // For this we'll check to config arrays:
328         // - com.android.internal.R.array.config_companionDevicePackages
329         // and
330         // - com.android.internal.R.array.config_companionDeviceCerts.
331         // Both arrays are expected to contain similar number of entries.
332         // config_companionDevicePackages contains package names of the allowlisted packages.
333         // config_companionDeviceCerts contains SHA256 digests of the signatures of the
334         // corresponding packages.
335         // If a package may be signed with one of several certificates, its package name would
336         // appear multiple times in the config_companionDevicePackages, with different entries
337         // (one for each of the valid signing certificates) at the corresponding positions in
338         // config_companionDeviceCerts.
339         final String[] allowlistedPackages = mContext.getResources()
340                 .getStringArray(com.android.internal.R.array.config_companionDevicePackages);
341         if (!ArrayUtils.contains(allowlistedPackages, packageName)) {
342             if (DEBUG) {
343                 Log.d(TAG, packageName + " is not allowlisted for creating associations "
344                         + "without user confirmation (prompt)");
345                 Log.v(TAG, "Allowlisted packages=" + Arrays.toString(allowlistedPackages));
346             }
347             return false;
348         }
349 
350         // Throttle frequent associations
351         final long now = System.currentTimeMillis();
352         final List<AssociationInfo> associationForPackage =
353                 mAssociationStore.getAssociationsForPackage(userId, packageName);
354         // Number of "recent" associations.
355         int recent = 0;
356         for (AssociationInfo association : associationForPackage) {
357             final boolean isRecent =
358                     now - association.getTimeApprovedMs() < ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS;
359             if (isRecent) {
360                 if (++recent >= ASSOCIATE_WITHOUT_PROMPT_MAX_PER_TIME_WINDOW) {
361                     Slog.w(TAG, "Too many associations: " + packageName + " already "
362                             + "associated " + recent + " devices within the last "
363                             + ASSOCIATE_WITHOUT_PROMPT_WINDOW_MS + "ms");
364                     return false;
365                 }
366             }
367         }
368 
369         final String[] allowlistedPackagesSignatureDigests = mContext.getResources()
370                 .getStringArray(com.android.internal.R.array.config_companionDeviceCerts);
371         final Set<String> allowlistedSignatureDigestsForRequestingPackage = new HashSet<>();
372         for (int i = 0; i < allowlistedPackages.length; i++) {
373             if (allowlistedPackages[i].equals(packageName)) {
374                 final String digest = allowlistedPackagesSignatureDigests[i].replaceAll(":", "");
375                 allowlistedSignatureDigestsForRequestingPackage.add(digest);
376             }
377         }
378 
379         final Signature[] requestingPackageSignatures = mPackageManager.getPackage(packageName)
380                 .getSigningDetails().getSignatures();
381         final String[] requestingPackageSignatureDigests =
382                 PackageUtils.computeSignaturesSha256Digests(requestingPackageSignatures);
383 
384         boolean requestingPackageSignatureAllowlisted = false;
385         for (String signatureDigest : requestingPackageSignatureDigests) {
386             if (allowlistedSignatureDigestsForRequestingPackage.contains(signatureDigest)) {
387                 requestingPackageSignatureAllowlisted = true;
388                 break;
389             }
390         }
391 
392         if (!requestingPackageSignatureAllowlisted) {
393             Slog.w(TAG, "Certificate mismatch for allowlisted package " + packageName);
394             if (DEBUG) {
395                 Log.d(TAG, "  > allowlisted signatures for " + packageName + ": ["
396                         + String.join(", ", allowlistedSignatureDigestsForRequestingPackage)
397                         + "]");
398                 Log.d(TAG, "  > actual signatures for " + packageName + ": "
399                         + Arrays.toString(requestingPackageSignatureDigests));
400             }
401         }
402 
403         return requestingPackageSignatureAllowlisted;
404     }
405 
406     /**
407      * Convert an instance of a "locally-defined" ResultReceiver to an instance of
408      * {@link android.os.ResultReceiver} itself, which the receiving process will be able to
409      * unmarshall.
410      */
prepareForIpc(T resultReceiver)411     private static <T extends ResultReceiver> ResultReceiver prepareForIpc(T resultReceiver) {
412         final Parcel parcel = Parcel.obtain();
413         resultReceiver.writeToParcel(parcel, 0);
414         parcel.setDataPosition(0);
415 
416         final ResultReceiver ipcFriendly = ResultReceiver.CREATOR.createFromParcel(parcel);
417         parcel.recycle();
418 
419         return ipcFriendly;
420     }
421 }
422