1 /* 2 * Copyright (C) 2023 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.service.topics; 18 19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_ENCRYPTION_INVALID_RESPONSE_LENGTH; 20 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_ENCRYPTION_KEY_DECODE_FAILURE; 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_ENCRYPTION_KEY_MISSING; 22 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_ENCRYPTION_NULL_REQUEST; 23 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_ENCRYPTION_NULL_RESPONSE; 24 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS; 25 26 import android.annotation.NonNull; 27 28 import com.android.adservices.LoggerFactory; 29 import com.android.adservices.data.encryptionkey.EncryptionKeyDao; 30 import com.android.adservices.data.enrollment.EnrollmentDao; 31 import com.android.adservices.data.topics.EncryptedTopic; 32 import com.android.adservices.data.topics.Topic; 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.service.enrollment.EnrollmentData; 38 39 import org.json.JSONObject; 40 41 import java.nio.charset.StandardCharsets; 42 import java.util.Base64; 43 import java.util.Comparator; 44 import java.util.List; 45 import java.util.Objects; 46 import java.util.Optional; 47 48 /** 49 * Class to handle encryption for {@link Topic} objects. 50 * 51 * <p>Identify the algorithm supported for Encryption. 52 * 53 * <p>Fetch public key corresponding to sdk(adtech) caller. 54 * 55 * <p>Generate {@link EncryptedTopic} object from the encrypted cipher text. 56 */ 57 public class EncryptionManager { 58 private static final LoggerFactory.Logger sLogger = LoggerFactory.getTopicsLogger(); 59 private static final byte[] EMPTY_BYTE_ARRAY = new byte[] {}; 60 private static final int ENCAPSULATED_KEY_LENGTH = 32; 61 62 private static EncryptionManager sSingleton; 63 64 private Encrypter mEncrypter; 65 private EnrollmentDao mEnrollmentDao; 66 private EncryptionKeyDao mEncryptionKeyDao; 67 private Flags mFlags; 68 EncryptionManager( Encrypter encrypter, EnrollmentDao enrollmentDao, EncryptionKeyDao encryptionKeyDao, Flags flags)69 EncryptionManager( 70 Encrypter encrypter, 71 EnrollmentDao enrollmentDao, 72 EncryptionKeyDao encryptionKeyDao, 73 Flags flags) { 74 mEncrypter = encrypter; 75 mEnrollmentDao = enrollmentDao; 76 mEncryptionKeyDao = encryptionKeyDao; 77 mFlags = flags; 78 } 79 80 /** Returns the singleton instance of the {@link EncryptionManager} given a context. */ 81 @NonNull getInstance()82 public static EncryptionManager getInstance() { 83 synchronized (EncryptionManager.class) { 84 if (sSingleton == null) { 85 sSingleton = 86 new EncryptionManager( 87 new HpkeEncrypter(), 88 EnrollmentDao.getInstance(), 89 EncryptionKeyDao.getInstance(), 90 FlagsFactory.getFlags()); 91 } 92 } 93 return sSingleton; 94 } 95 96 /** 97 * Converts plain text {@link Topic} object to {@link EncryptedTopic}. 98 * 99 * <p>Returns {@link Optional#empty()} if encryption fails. 100 * 101 * @param topic object to be encrypted 102 * @return corresponding encrypted object 103 */ encryptTopic(Topic topic, String sdkName)104 public Optional<EncryptedTopic> encryptTopic(Topic topic, String sdkName) { 105 Optional<EncryptedTopic> encryptedTopicOptional = 106 encryptTopicWithKey(topic, fetchPublicKeyFor(sdkName)); 107 sLogger.v("Encrypted topic for %s is %s", topic, encryptedTopicOptional); 108 return encryptedTopicOptional; 109 } 110 111 /** 112 * Returns public key from the enrolled {@code sdkName}. Returns {@link Optional#empty()} if the 113 * public key is missing. 114 */ fetchPublicKeyFor(String sdkName)115 private Optional<String> fetchPublicKeyFor(String sdkName) { 116 if (!mFlags.getTopicsTestEncryptionPublicKey().isEmpty()) { 117 // Use testing key for encryption if the test key is non-empty. 118 return Optional.of(mFlags.getTopicsTestEncryptionPublicKey()); 119 } 120 121 sLogger.v("Fetching EnrollmentData for %s", sdkName); 122 EnrollmentData enrollmentData = mEnrollmentDao.getEnrollmentDataFromSdkName(sdkName); 123 if (enrollmentData != null && enrollmentData.getEnrollmentId() != null) { 124 sLogger.v("Fetching EncryptionKeys for %s", enrollmentData.getEnrollmentId()); 125 List<EncryptionKey> encryptionKeys = 126 mEncryptionKeyDao.getEncryptionKeyFromEnrollmentIdAndKeyType( 127 enrollmentData.getEnrollmentId(), EncryptionKey.KeyType.ENCRYPTION); 128 Optional<EncryptionKey> latestKey = 129 encryptionKeys.stream() 130 .max(Comparator.comparingLong(EncryptionKey::getExpiration)); 131 132 if (latestKey.isPresent() && latestKey.get().getBody() != null) { 133 return Optional.of(latestKey.get().getBody()); 134 } 135 } 136 sLogger.e("Failed to fetch encryption key for %s", sdkName); 137 ErrorLogUtil.e( 138 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_ENCRYPTION_KEY_MISSING, 139 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS); 140 return Optional.empty(); 141 } 142 143 /** 144 * Serialise {@link Topic} to JSON string with UTF-8 encoding. Encrypt serialised Topic with the 145 * given public key. 146 * 147 * <p>Returns {@link Optional#empty()} if the public key is missing or if the topic 148 * serialisation fails. 149 */ encryptTopicWithKey(Topic topic, Optional<String> publicKey)150 private Optional<EncryptedTopic> encryptTopicWithKey(Topic topic, Optional<String> publicKey) { 151 Objects.requireNonNull(topic); 152 153 Optional<JSONObject> optionalTopicJSON = TopicsJsonMapper.toJson(topic); 154 if (publicKey.isPresent() && optionalTopicJSON.isPresent()) { 155 try { 156 // UTF-8 is the default encoding for JSON data. 157 byte[] unencryptedSerializedTopic = 158 optionalTopicJSON.get().toString().getBytes(StandardCharsets.UTF_8); 159 byte[] base64DecodedPublicKey = Base64.getDecoder().decode(publicKey.get()); 160 byte[] response = 161 mEncrypter.encrypt( 162 /* publicKey */ base64DecodedPublicKey, 163 /* plainText */ unencryptedSerializedTopic, 164 /* contextInfo */ EMPTY_BYTE_ARRAY); 165 166 return buildEncryptedTopic(response, publicKey.get()); 167 } catch (IllegalArgumentException illegalArgumentException) { 168 ErrorLogUtil.e( 169 illegalArgumentException, 170 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_ENCRYPTION_KEY_DECODE_FAILURE, 171 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS); 172 sLogger.e( 173 illegalArgumentException, 174 "Failed to decode with Base64 decoder for public key = %s", 175 publicKey); 176 } catch (NullPointerException nullPointerException) { 177 ErrorLogUtil.e( 178 nullPointerException, 179 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_ENCRYPTION_NULL_REQUEST, 180 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS); 181 sLogger.e( 182 nullPointerException, "Null params while trying to encrypt Topics object."); 183 } 184 } 185 return Optional.empty(); 186 } 187 buildEncryptedTopic(byte[] response, String publicKey)188 private static Optional<EncryptedTopic> buildEncryptedTopic(byte[] response, String publicKey) { 189 if (response == null) { 190 ErrorLogUtil.e( 191 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_ENCRYPTION_NULL_RESPONSE, 192 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS); 193 sLogger.e("Null encryption response received for public key = %s", publicKey); 194 return Optional.empty(); 195 } 196 if (response.length < ENCAPSULATED_KEY_LENGTH) { 197 ErrorLogUtil.e( 198 AD_SERVICES_ERROR_REPORTED__ERROR_CODE__TOPICS_ENCRYPTION_INVALID_RESPONSE_LENGTH, 199 AD_SERVICES_ERROR_REPORTED__PPAPI_NAME__TOPICS); 200 sLogger.e( 201 "Encrypted response size is smaller than minimum expected size of " 202 + ENCAPSULATED_KEY_LENGTH); 203 return Optional.empty(); 204 } 205 206 // First 32 bytes are the encapsulated key and the remaining array in the cipher text. 207 int cipherTextLength = response.length - ENCAPSULATED_KEY_LENGTH; 208 byte[] encapsulatedKey = new byte[ENCAPSULATED_KEY_LENGTH]; 209 byte[] cipherText = new byte[cipherTextLength]; 210 System.arraycopy( 211 response, 212 /* srcPos */ 0, 213 encapsulatedKey, 214 /* destPos */ 0, 215 /* length */ ENCAPSULATED_KEY_LENGTH); 216 System.arraycopy( 217 response, 218 /* srcPos */ ENCAPSULATED_KEY_LENGTH, 219 cipherText, 220 /* destPos */ 0, 221 /* length */ cipherTextLength); 222 223 return Optional.of(EncryptedTopic.create(cipherText, publicKey, encapsulatedKey)); 224 } 225 } 226