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