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