1 /* 2 * Copyright (C) 2022 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.adservices.download; 18 19 import static com.android.adservices.service.enrollment.EnrollmentUtil.BUILD_ID; 20 import static com.android.adservices.service.enrollment.EnrollmentUtil.ENROLLMENT_SHARED_PREF; 21 import static com.android.adservices.service.enrollment.EnrollmentUtil.FILE_GROUP_STATUS; 22 import static com.android.adservices.service.stats.AdServicesEncryptionKeyFetchedStats.FetchJobType.MDD_DOWNLOAD_JOB; 23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENROLLMENT_FAILED_PARSING; 24 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__LOAD_MDD_FILE_GROUP_FAILURE; 25 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_UPDATE_FAILURE; 26 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT; 27 28 import android.content.Context; 29 import android.content.SharedPreferences; 30 import android.net.Uri; 31 import android.os.Build; 32 import android.util.Pair; 33 34 import androidx.annotation.RequiresApi; 35 36 import com.android.adservices.LogUtil; 37 import com.android.adservices.data.encryptionkey.EncryptionKeyDao; 38 import com.android.adservices.data.enrollment.EnrollmentDao; 39 import com.android.adservices.errorlogging.ErrorLogUtil; 40 import com.android.adservices.service.Flags; 41 import com.android.adservices.service.FlagsFactory; 42 import com.android.adservices.service.encryptionkey.EncryptionKey; 43 import com.android.adservices.service.encryptionkey.EncryptionKeyFetcher; 44 import com.android.adservices.service.enrollment.EnrollmentData; 45 import com.android.adservices.service.enrollment.EnrollmentUtil; 46 import com.android.adservices.service.proto.PrivacySandboxApi; 47 import com.android.adservices.service.proto.RbEnrollment; 48 import com.android.adservices.service.proto.RbEnrollmentList; 49 import com.android.adservices.service.stats.AdServicesLogger; 50 import com.android.adservices.service.stats.AdServicesLoggerImpl; 51 import com.android.adservices.shared.common.ApplicationContextSingleton; 52 import com.android.internal.annotations.VisibleForTesting; 53 54 import com.google.android.libraries.mobiledatadownload.GetFileGroupRequest; 55 import com.google.android.libraries.mobiledatadownload.MobileDataDownload; 56 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 57 import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener; 58 import com.google.common.util.concurrent.Futures; 59 import com.google.common.util.concurrent.ListenableFuture; 60 import com.google.mobiledatadownload.ClientConfigProto.ClientFile; 61 import com.google.mobiledatadownload.ClientConfigProto.ClientFileGroup; 62 63 import java.io.BufferedReader; 64 import java.io.IOException; 65 import java.io.InputStream; 66 import java.io.InputStreamReader; 67 import java.util.ArrayList; 68 import java.util.Arrays; 69 import java.util.List; 70 import java.util.Optional; 71 import java.util.concurrent.ExecutionException; 72 73 /** Handles EnrollmentData download from MDD server to device. */ 74 // TODO(b/269798827): Enable for R. 75 @RequiresApi(Build.VERSION_CODES.S) 76 public class EnrollmentDataDownloadManager { 77 private final Context mContext; 78 private static volatile EnrollmentDataDownloadManager sEnrollmentDataDownloadManager; 79 private final MobileDataDownload mMobileDataDownload; 80 private final SynchronousFileStorage mFileStorage; 81 private final Flags mFlags; 82 private final AdServicesLogger mLogger; 83 private final EnrollmentUtil mEnrollmentUtil; 84 private final EncryptionKeyFetcher mEncryptionKeyFetcher; 85 86 private static final String GROUP_NAME = "adtech_enrollment_data"; 87 private static final String PROTO_GROUP_NAME = "adtech_enrollment_proto_data"; 88 private static final String DOWNLOADED_ENROLLMENT_DATA_FILE_ID = "adtech_enrollment_data.csv"; 89 private static final String DOWNLOADED_ENROLLMENT_DATA_PROTO_FILE_ID = 90 "rb_prod_enrollment.binarypb"; 91 private static final String ENROLLMENT_FILE_READ_STATUS_SHARED_PREFERENCES = 92 "enrollment_data_read_status"; 93 94 @VisibleForTesting EnrollmentDataDownloadManager(Context context, Flags flags)95 EnrollmentDataDownloadManager(Context context, Flags flags) { 96 this( 97 context, 98 flags, 99 AdServicesLoggerImpl.getInstance(), 100 EnrollmentUtil.getInstance(), 101 new EncryptionKeyFetcher(MDD_DOWNLOAD_JOB)); 102 } 103 104 @VisibleForTesting EnrollmentDataDownloadManager( Context context, Flags flags, AdServicesLogger logger, EnrollmentUtil enrollmentUtil, EncryptionKeyFetcher encryptionKeyFetcher)105 EnrollmentDataDownloadManager( 106 Context context, 107 Flags flags, 108 AdServicesLogger logger, 109 EnrollmentUtil enrollmentUtil, 110 EncryptionKeyFetcher encryptionKeyFetcher) { 111 mContext = context.getApplicationContext(); 112 mMobileDataDownload = MobileDataDownloadFactory.getMdd(flags); 113 mFileStorage = MobileDataDownloadFactory.getFileStorage(); 114 mFlags = flags; 115 mLogger = logger; 116 mEnrollmentUtil = enrollmentUtil; 117 mEncryptionKeyFetcher = encryptionKeyFetcher; 118 } 119 120 /** Gets an instance of EnrollmentDataDownloadManager to be used. */ getInstance()121 public static EnrollmentDataDownloadManager getInstance() { 122 if (sEnrollmentDataDownloadManager == null) { 123 synchronized (EnrollmentDataDownloadManager.class) { 124 if (sEnrollmentDataDownloadManager == null) { 125 sEnrollmentDataDownloadManager = 126 new EnrollmentDataDownloadManager( 127 ApplicationContextSingleton.get(), 128 FlagsFactory.getFlags(), 129 AdServicesLoggerImpl.getInstance(), 130 EnrollmentUtil.getInstance(), 131 new EncryptionKeyFetcher(MDD_DOWNLOAD_JOB)); 132 } 133 } 134 } 135 return sEnrollmentDataDownloadManager; 136 } 137 138 /** 139 * Find, open and read the enrollment data file from MDD and only insert new data into the 140 * enrollment database. 141 */ readAndInsertEnrollmentDataFromMdd()142 public ListenableFuture<DownloadStatus> readAndInsertEnrollmentDataFromMdd() { 143 LogUtil.d("Reading MDD data from file."); 144 boolean protoFileFound = false; 145 Pair<ClientFile, String> FileGroupAndBuildIdPair = null; 146 if (mFlags.getEnrollmentProtoFileEnabled()) { 147 Pair<ClientFile, String> FileProtoGroupAndBuildIdPair = 148 getEnrollmentDataFile(/* getProto= */ true); 149 if (FileProtoGroupAndBuildIdPair == null 150 || FileProtoGroupAndBuildIdPair.first == null) { 151 // TODO (b/280579966): Add CEL Logging 152 LogUtil.d("Proto flag enabled, but no proto file found."); 153 } else { 154 protoFileFound = true; 155 FileGroupAndBuildIdPair = FileProtoGroupAndBuildIdPair; 156 } 157 } 158 159 if (!protoFileFound) { 160 FileGroupAndBuildIdPair = getEnrollmentDataFile(/* getProto= */ false); 161 if (FileGroupAndBuildIdPair == null || FileGroupAndBuildIdPair.first == null) { 162 return Futures.immediateFuture(DownloadStatus.NO_FILE_AVAILABLE); 163 } 164 } 165 166 ClientFile enrollmentDataFile = FileGroupAndBuildIdPair.first; 167 String fileGroupBuildId = FileGroupAndBuildIdPair.second; 168 SharedPreferences sharedPrefs = 169 mContext.getSharedPreferences( 170 ENROLLMENT_FILE_READ_STATUS_SHARED_PREFERENCES, Context.MODE_PRIVATE); 171 if (sharedPrefs.getBoolean(fileGroupBuildId, false)) { 172 LogUtil.d( 173 "Enrollment data build id = %s has been saved into DB. Skip adding same data.", 174 fileGroupBuildId); 175 return Futures.immediateFuture(DownloadStatus.SKIP); 176 } 177 boolean shouldTrimEnrollmentData = mFlags.getEnrollmentMddRecordDeletionEnabled(); 178 Optional<List<EnrollmentData>> enrollmentDataList; 179 if (protoFileFound) { 180 enrollmentDataList = 181 processDownloadedProtoFile(enrollmentDataFile, shouldTrimEnrollmentData); 182 } else { 183 enrollmentDataList = 184 processDownloadedFile(enrollmentDataFile, shouldTrimEnrollmentData); 185 } 186 187 if (enrollmentDataList.isPresent()) { 188 SharedPreferences.Editor editor = sharedPrefs.edit(); 189 editor.clear().putBoolean(fileGroupBuildId, true); 190 if (!editor.commit()) { 191 LogUtil.e("Saving to the enrollment file read status sharedpreference failed"); 192 ErrorLogUtil.e( 193 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_UPDATE_FAILURE, 194 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); 195 } 196 LogUtil.d( 197 "Inserted new enrollment data build id = %s into DB. " 198 + "Enrollment Mdd Record Deletion Feature Enabled: %b", 199 fileGroupBuildId, shouldTrimEnrollmentData); 200 mEnrollmentUtil.logEnrollmentFileDownloadStats(mLogger, true, fileGroupBuildId); 201 202 if (!mFlags.getEncryptionKeyNewEnrollmentFetchKillSwitch()) { 203 // For new enrollment, fetch and save encryption/signing keys into DB. 204 LogUtil.i("Fetch and save encryption/signing keys for new enrollment."); 205 fetchEncryptionKeysForNewEnrollment(enrollmentDataList.get()); 206 } 207 return Futures.immediateFuture(DownloadStatus.SUCCESS); 208 } else { 209 mEnrollmentUtil.logEnrollmentFileDownloadStats(mLogger, false, fileGroupBuildId); 210 return Futures.immediateFuture(DownloadStatus.PARSING_FAILED); 211 } 212 } 213 fetchEncryptionKeysForNewEnrollment(List<EnrollmentData> enrollmentDataList)214 private void fetchEncryptionKeysForNewEnrollment(List<EnrollmentData> enrollmentDataList) { 215 EncryptionKeyDao encryptionKeyDao = EncryptionKeyDao.getInstance(); 216 for (EnrollmentData enrollmentData : enrollmentDataList) { 217 List<EncryptionKey> existingKeys = 218 encryptionKeyDao.getEncryptionKeyFromEnrollmentId( 219 enrollmentData.getEnrollmentId()); 220 // New enrollment which doesn't have any keys before, fetch keys for the first time. 221 if (existingKeys == null || existingKeys.size() == 0) { 222 Optional<List<EncryptionKey>> currentEncryptionKeys = 223 mEncryptionKeyFetcher.fetchEncryptionKeys(null, enrollmentData, true); 224 if (currentEncryptionKeys.isEmpty()) { 225 LogUtil.d("No encryption key is provided by this enrollment data."); 226 } else { 227 for (EncryptionKey encryptionKey : currentEncryptionKeys.get()) { 228 encryptionKeyDao.insert(encryptionKey); 229 } 230 } 231 } 232 } 233 } 234 processDownloadedFile( ClientFile enrollmentDataFile, boolean trimTable)235 private Optional<List<EnrollmentData>> processDownloadedFile( 236 ClientFile enrollmentDataFile, boolean trimTable) { 237 LogUtil.d("Inserting MDD data into DB."); 238 try { 239 InputStream inputStream = 240 mFileStorage.open( 241 Uri.parse(enrollmentDataFile.getFileUri()), ReadStreamOpener.create()); 242 BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); 243 bufferedReader.readLine(); 244 String line = null; 245 // While loop runs from the second line. 246 EnrollmentDao enrollmentDao = EnrollmentDao.getInstance(); 247 List<EnrollmentData> newEnrollments = new ArrayList<>(); 248 249 while ((line = bufferedReader.readLine()) != null) { 250 // Parses CSV into EnrollmentData list. 251 String[] data = line.split(","); 252 if (data.length == 8) { 253 String enrollmentId = data[0]; 254 LogUtil.d("Adding enrollmentId - %s", enrollmentId); 255 EnrollmentData enrollmentData = 256 new EnrollmentData.Builder() 257 .setEnrollmentId(enrollmentId) 258 .setEnrolledAPIs(data[1]) 259 .setSdkNames(data[2]) 260 .setAttributionSourceRegistrationUrl( 261 data[3].contains(" ") 262 ? Arrays.asList(data[3].split(" ")) 263 : List.of(data[3])) 264 .setAttributionTriggerRegistrationUrl( 265 data[4].contains(" ") 266 ? Arrays.asList(data[4].split(" ")) 267 : List.of(data[4])) 268 .setAttributionReportingUrl( 269 data[5].contains(" ") 270 ? Arrays.asList(data[5].split(" ")) 271 : List.of(data[5])) 272 .setRemarketingResponseBasedRegistrationUrl( 273 data[6].contains(" ") 274 ? Arrays.asList(data[6].split(" ")) 275 : List.of(data[6])) 276 .setEncryptionKeyUrl(data[7]) 277 .setEnrolledSite(data[7]) 278 .build(); 279 newEnrollments.add(enrollmentData); 280 } else { 281 LogUtil.e("Incorrect number of elements in row."); 282 ErrorLogUtil.e( 283 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENROLLMENT_FAILED_PARSING, 284 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); 285 } 286 } 287 if (trimTable) { 288 enrollmentDao.overwriteData(newEnrollments); 289 return Optional.of(newEnrollments); 290 } 291 for (EnrollmentData enrollmentData : newEnrollments) { 292 enrollmentDao.insert(enrollmentData); 293 } 294 return Optional.of(newEnrollments); 295 } catch (IOException e) { 296 ErrorLogUtil.e( 297 e, 298 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENROLLMENT_FAILED_PARSING, 299 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); 300 return Optional.empty(); 301 } 302 } 303 processDownloadedProtoFile( ClientFile enrollmentDataFile, boolean trimTable)304 private Optional<List<EnrollmentData>> processDownloadedProtoFile( 305 ClientFile enrollmentDataFile, boolean trimTable) { 306 LogUtil.d("Inserting MDD data from proto file into DB."); 307 try { 308 InputStream inputStream = 309 mFileStorage.open( 310 Uri.parse(enrollmentDataFile.getFileUri()), ReadStreamOpener.create()); 311 312 EnrollmentDao enrollmentDao = EnrollmentDao.getInstance(); 313 List<EnrollmentData> newEnrollments = new ArrayList<>(); 314 315 RbEnrollmentList rbProdEnrollmentList = 316 RbEnrollmentList.newBuilder().build().parseFrom(inputStream); 317 // Parses proto file into EnrollmentData list. 318 for (RbEnrollment rbEnrollment : rbProdEnrollmentList.getEntryList()) { 319 String enrollmentId = rbEnrollment.getEnrollmentId(); 320 LogUtil.d("Adding enrollmentId - %s", enrollmentId); 321 EnrollmentData enrollmentData = 322 new EnrollmentData.Builder() 323 .setEnrollmentId(enrollmentId) 324 .setEnrolledAPIs( 325 enrolledApiEnumListToString( 326 rbEnrollment.getEnrolledApisList())) 327 .setEnrolledSite(rbEnrollment.getEnrolledSite()) 328 .setSdkNames(rbEnrollment.getSdkNamesList()) 329 .build(); 330 newEnrollments.add(enrollmentData); 331 } 332 if (newEnrollments.isEmpty()) { 333 LogUtil.e("No new enrollments found."); 334 return Optional.empty(); 335 } 336 if (trimTable) { 337 enrollmentDao.overwriteData(newEnrollments); 338 return Optional.of(newEnrollments); 339 } 340 for (EnrollmentData enrollmentData : newEnrollments) { 341 enrollmentDao.insert(enrollmentData); 342 } 343 return Optional.of(newEnrollments); 344 } catch (IOException e) { 345 LogUtil.e("Failed to parse proto file"); 346 ErrorLogUtil.e( 347 e, 348 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENROLLMENT_FAILED_PARSING, 349 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); 350 return Optional.empty(); 351 } 352 } 353 354 public enum DownloadStatus { 355 SUCCESS, 356 NO_FILE_AVAILABLE, 357 PARSING_FAILED, 358 // Skip reading and inserting same enrollment data to DB if the data has been saved 359 // previously. 360 SKIP; 361 } 362 getEnrollmentDataFile(boolean getProto)363 private Pair<ClientFile, String> getEnrollmentDataFile(boolean getProto) { 364 String groupName = getProto ? PROTO_GROUP_NAME : GROUP_NAME; 365 366 GetFileGroupRequest getFileGroupRequest = 367 GetFileGroupRequest.newBuilder().setGroupName(groupName).build(); 368 try { 369 ListenableFuture<ClientFileGroup> fileGroupFuture = 370 mMobileDataDownload.getFileGroup(getFileGroupRequest); 371 ClientFileGroup fileGroup = fileGroupFuture.get(); 372 if (fileGroup == null) { 373 LogUtil.d("MDD has not downloaded the Enrollment Data Files yet."); 374 return null; 375 } 376 377 // store file group status and build id in shared preference for logging purposes 378 commitFileGroupDataToSharedPref(fileGroup); 379 String fileGroupBuildId = String.valueOf(fileGroup.getBuildId()); 380 ClientFile enrollmentDataFile = null; 381 String targetFileId = 382 getProto 383 ? DOWNLOADED_ENROLLMENT_DATA_PROTO_FILE_ID 384 : DOWNLOADED_ENROLLMENT_DATA_FILE_ID; 385 for (ClientFile file : fileGroup.getFileList()) { 386 if (file.getFileId().equals(targetFileId)) { 387 enrollmentDataFile = file; 388 break; 389 } 390 } 391 return Pair.create(enrollmentDataFile, fileGroupBuildId); 392 393 } catch (ExecutionException | InterruptedException e) { 394 LogUtil.e(e, "Unable to load MDD file group."); 395 ErrorLogUtil.e( 396 e, 397 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__LOAD_MDD_FILE_GROUP_FAILURE, 398 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); 399 return null; 400 } 401 } 402 commitFileGroupDataToSharedPref(ClientFileGroup fileGroup)403 private void commitFileGroupDataToSharedPref(ClientFileGroup fileGroup) { 404 Long buildId = fileGroup.getBuildId(); 405 ClientFileGroup.Status fileGroupStatus = fileGroup.getStatus(); 406 SharedPreferences prefs = 407 mContext.getSharedPreferences(ENROLLMENT_SHARED_PREF, Context.MODE_PRIVATE); 408 SharedPreferences.Editor edit = prefs.edit(); 409 edit.putInt(BUILD_ID, buildId.intValue()); 410 edit.putInt(FILE_GROUP_STATUS, fileGroupStatus.getNumber()); 411 if (!edit.commit()) { 412 LogUtil.e( 413 "Saving shared preferences - %s , %s and %s failed", 414 ENROLLMENT_SHARED_PREF, BUILD_ID, FILE_GROUP_STATUS); 415 ErrorLogUtil.e( 416 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__SHARED_PREF_UPDATE_FAILURE, 417 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__MEASUREMENT); 418 } 419 } 420 enrolledApiEnumListToString(List<PrivacySandboxApi> enrolledAPIList)421 private static String enrolledApiEnumListToString(List<PrivacySandboxApi> enrolledAPIList) { 422 StringBuilder enrolledAPIs = new StringBuilder(); 423 for (PrivacySandboxApi enrolledAPI : enrolledAPIList) { 424 enrolledAPIs 425 .append( 426 EnrollmentData.ENROLLMENT_API_ENUM_STRING_MAP.getOrDefault( 427 enrolledAPI, /* defaultValue= */ "PRIVACY_SANDBOX_API_UNKNOWN")) 428 .append(" "); 429 } 430 return enrolledAPIs.toString().stripTrailing(); 431 } 432 } 433