1 /*
2  * Copyright (C) 2024 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.download.EncryptionKeyConverterUtil.createEncryptionKeyFromJson;
20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENCRYPTION_KEYS_FAILED_DELETE_EXPIRED_KEY;
21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENCRYPTION_KEYS_FAILED_MDD_FILEGROUP;
22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENCRYPTION_KEYS_JSON_PARSING_ERROR;
23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENCRYPTION_KEYS_MDD_NO_FILE_AVAILABLE;
24 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON;
25 
26 import android.net.Uri;
27 import android.os.Build;
28 
29 import androidx.annotation.RequiresApi;
30 
31 import com.android.adservices.LoggerFactory;
32 import com.android.adservices.data.encryptionkey.EncryptionKeyDao;
33 import com.android.adservices.errorlogging.ErrorLogUtil;
34 import com.android.adservices.service.Flags;
35 import com.android.adservices.service.FlagsFactory;
36 import com.android.adservices.service.encryptionkey.EncryptionKey;
37 import com.android.adservices.shared.util.Clock;
38 import com.android.internal.annotations.VisibleForTesting;
39 
40 import com.google.android.libraries.mobiledatadownload.GetFileGroupRequest;
41 import com.google.android.libraries.mobiledatadownload.MobileDataDownload;
42 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
43 import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener;
44 import com.google.common.util.concurrent.Futures;
45 import com.google.common.util.concurrent.ListenableFuture;
46 import com.google.mobiledatadownload.ClientConfigProto;
47 
48 import org.json.JSONArray;
49 import org.json.JSONException;
50 import org.json.JSONObject;
51 
52 import java.io.BufferedReader;
53 import java.io.IOException;
54 import java.io.InputStream;
55 import java.io.InputStreamReader;
56 import java.util.ArrayList;
57 import java.util.List;
58 import java.util.Optional;
59 import java.util.concurrent.ExecutionException;
60 import java.util.stream.Collectors;
61 
62 import javax.annotation.Nullable;
63 
64 /** Handles EncryptionData download from MDD server to device. */
65 @RequiresApi(Build.VERSION_CODES.S)
66 public final class EncryptionDataDownloadManager {
67     private static volatile EncryptionDataDownloadManager sEncryptionDataDownloadManager;
68 
69     private final MobileDataDownload mMobileDataDownload;
70     private final SynchronousFileStorage mFileStorage;
71     private final EncryptionKeyDao mEncryptionKeyDao;
72     private final Clock mClock;
73     private static final LoggerFactory.Logger LOGGER = LoggerFactory.getLogger();
74 
75     private static final String GROUP_NAME = "encryption-keys";
76     private static final String DOWNLOADED_ENCRYPTION_DATA_FILE_TYPE = ".json";
77 
78     @VisibleForTesting
EncryptionDataDownloadManager(Flags flags, EncryptionKeyDao encryptionKeyDao, Clock clock)79     EncryptionDataDownloadManager(Flags flags, EncryptionKeyDao encryptionKeyDao, Clock clock) {
80         mMobileDataDownload = MobileDataDownloadFactory.getMdd(flags);
81         mFileStorage = MobileDataDownloadFactory.getFileStorage();
82         mEncryptionKeyDao = encryptionKeyDao;
83         mClock = clock;
84     }
85 
86     /** Gets an instance of EncryptionDataDownloadManager to be used. */
getInstance()87     public static EncryptionDataDownloadManager getInstance() {
88         // TODO(b/331428431): Fix singleton creation behaviour.
89         if (sEncryptionDataDownloadManager == null) {
90             synchronized (EncryptionDataDownloadManager.class) {
91                 if (sEncryptionDataDownloadManager == null) {
92                     sEncryptionDataDownloadManager =
93                             new EncryptionDataDownloadManager(
94                                     FlagsFactory.getFlags(),
95                                     EncryptionKeyDao.getInstance(),
96                                     Clock.getInstance());
97                 }
98             }
99         }
100         return sEncryptionDataDownloadManager;
101     }
102 
103     public enum DownloadStatus {
104         SUCCESS,
105         FAILURE,
106         NO_FILE_AVAILABLE,
107     }
108 
109     /**
110      * Find, open and read the encryption keys data file from MDD. Insert all keys into database.
111      */
readAndInsertEncryptionDataFromMdd()112     public ListenableFuture<DownloadStatus> readAndInsertEncryptionDataFromMdd() {
113         LOGGER.v("Reading encryption MDD data for group name: %s", GROUP_NAME);
114         List<ClientConfigProto.ClientFile> jsonKeyFiles = getEncryptionDataFiles();
115         if (jsonKeyFiles == null || jsonKeyFiles.isEmpty()) {
116             LOGGER.d("No files available for: %s", GROUP_NAME);
117             ErrorLogUtil.e(
118                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENCRYPTION_KEYS_MDD_NO_FILE_AVAILABLE,
119                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON);
120             return Futures.immediateFuture(DownloadStatus.NO_FILE_AVAILABLE);
121         }
122 
123         try {
124             for (ClientConfigProto.ClientFile clientFile : jsonKeyFiles) {
125                 Optional<List<EncryptionKey>> encryptionKeys = processDownloadedFile(clientFile);
126                 if (!encryptionKeys.isPresent()) {
127                     LOGGER.d("Parsing keys failed for %s ", clientFile.getFileId());
128                 }
129             }
130         } catch (IOException e) {
131             LOGGER.e(e, "Failed to open MDD files for encryption keys");
132             ErrorLogUtil.e(
133                     e,
134                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENCRYPTION_KEYS_FAILED_MDD_FILEGROUP,
135                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON);
136             return Futures.immediateFuture(DownloadStatus.FAILURE);
137         }
138         return Futures.immediateFuture(DownloadStatus.SUCCESS);
139     }
140 
141     @Nullable
getEncryptionDataFiles()142     private List<ClientConfigProto.ClientFile> getEncryptionDataFiles() {
143         GetFileGroupRequest getFileGroupRequest =
144                 GetFileGroupRequest.newBuilder().setGroupName(GROUP_NAME).build();
145         try {
146             ListenableFuture<ClientConfigProto.ClientFileGroup> fileGroupFuture =
147                     mMobileDataDownload.getFileGroup(getFileGroupRequest);
148             ClientConfigProto.ClientFileGroup fileGroup = fileGroupFuture.get();
149             if (fileGroup == null) {
150                 LOGGER.d("MDD has not downloaded the Encryption Data Files yet.");
151                 return null;
152             }
153 
154             List<ClientConfigProto.ClientFile> jsonKeyFiles = new ArrayList<>();
155             for (ClientConfigProto.ClientFile file : fileGroup.getFileList()) {
156                 if (file.getFileId().endsWith(DOWNLOADED_ENCRYPTION_DATA_FILE_TYPE)) {
157                     jsonKeyFiles.add(file);
158                 }
159             }
160             return jsonKeyFiles;
161 
162         } catch (ExecutionException | InterruptedException e) {
163             LOGGER.e(e, "Unable to load MDD file group for encryption.");
164             return null;
165         }
166     }
167 
processDownloadedFile( ClientConfigProto.ClientFile encryptionDataFile)168     private Optional<List<EncryptionKey>> processDownloadedFile(
169             ClientConfigProto.ClientFile encryptionDataFile) throws IOException {
170         LOGGER.d("Inserting Encryption MDD data into DB.");
171         try (InputStream inputStream =
172                         mFileStorage.open(
173                                 Uri.parse(encryptionDataFile.getFileUri()),
174                                 ReadStreamOpener.create());
175                 BufferedReader bufferedReader =
176                         new BufferedReader(new InputStreamReader(inputStream))) {
177 
178             String jsonString = bufferedReader.lines().collect(Collectors.joining("\n"));
179             JSONArray jsonArray = new JSONArray(jsonString);
180 
181             List<EncryptionKey> encryptionKeys = new ArrayList<>();
182             for (int index = 0; index < jsonArray.length(); index++) {
183                 JSONObject jsonKeyObject = jsonArray.getJSONObject(index);
184                 Optional<EncryptionKey> keyOptional = createEncryptionKeyFromJson(jsonKeyObject);
185                 keyOptional.ifPresent(encryptionKeys::add);
186             }
187 
188             LOGGER.v("Adding %d encryption keys to the database.", encryptionKeys.size());
189             mEncryptionKeyDao.insert(encryptionKeys);
190 
191             deleteExpiredKeys();
192 
193             return Optional.of(encryptionKeys);
194         } catch (JSONException e) {
195             LOGGER.e(
196                     e,
197                     "Parsing of encryption keys failed for %s.",
198                     encryptionDataFile.getFileUri());
199             ErrorLogUtil.e(
200                     e,
201                     AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENCRYPTION_KEYS_JSON_PARSING_ERROR,
202                     AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON);
203             return Optional.empty();
204         }
205     }
206 
deleteExpiredKeys()207     private void deleteExpiredKeys() {
208         List<EncryptionKey> allEncryptionKeys = mEncryptionKeyDao.getAllEncryptionKeys();
209         LOGGER.v(
210                 "Total number of encryption keys before deleting expired keys %d",
211                 allEncryptionKeys.size());
212 
213         long currentTimeMillis = mClock.currentTimeMillis();
214         List<EncryptionKey> expiredKeys =
215                 allEncryptionKeys.stream()
216                         .filter(encryptionKey -> encryptionKey.getExpiration() < currentTimeMillis)
217                         .collect(Collectors.toList());
218         LOGGER.v("Total number of expired encryption keys to be deleted %d", expiredKeys.size());
219 
220         for (EncryptionKey expiredKey : expiredKeys) {
221             LOGGER.v(
222                     "Deleting key = %s for enrollment id %s with expiration time %d. Current time"
223                             + " = %d",
224                     expiredKey.getBody(),
225                     expiredKey.getEnrollmentId(),
226                     expiredKey.getExpiration(),
227                     currentTimeMillis);
228             if (!mEncryptionKeyDao.delete(expiredKey.getId())) {
229                 ErrorLogUtil.e(
230                         AD_SERVICES_ERROR_REPORTED__ERROR_CODE__ENCRYPTION_KEYS_FAILED_DELETE_EXPIRED_KEY,
231                         AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__COMMON);
232                 LOGGER.e(
233                         "Failed to delete expired key = %s for enrollment = %s",
234                         expiredKey.getBody(), expiredKey.getEnrollmentId());
235             }
236         }
237     }
238 }
239