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