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.encryptionkey; 18 19 import android.net.Uri; 20 21 import androidx.annotation.NonNull; 22 import androidx.annotation.Nullable; 23 24 import com.android.adservices.LoggerFactory; 25 import com.android.adservices.service.Flags; 26 import com.android.adservices.service.FlagsFactory; 27 import com.android.adservices.service.enrollment.EnrollmentData; 28 import com.android.adservices.service.stats.AdServicesEncryptionKeyFetchedStats; 29 import com.android.adservices.service.stats.AdServicesEncryptionKeyFetchedStats.FetchJobType; 30 import com.android.adservices.service.stats.AdServicesEncryptionKeyFetchedStats.FetchStatus; 31 import com.android.adservices.service.stats.AdServicesLogger; 32 import com.android.adservices.service.stats.AdServicesLoggerImpl; 33 import com.android.internal.annotations.VisibleForTesting; 34 35 import org.json.JSONArray; 36 import org.json.JSONException; 37 import org.json.JSONObject; 38 39 import java.io.BufferedReader; 40 import java.io.IOException; 41 import java.io.InputStreamReader; 42 import java.net.HttpURLConnection; 43 import java.net.MalformedURLException; 44 import java.net.URL; 45 import java.net.URLConnection; 46 import java.text.SimpleDateFormat; 47 import java.util.ArrayList; 48 import java.util.Date; 49 import java.util.List; 50 import java.util.Locale; 51 import java.util.Objects; 52 import java.util.Optional; 53 import java.util.TimeZone; 54 import java.util.UUID; 55 56 /** Fetch encryption keys. */ 57 public final class EncryptionKeyFetcher { 58 59 public static final String ENCRYPTION_KEY_ENDPOINT = "/.well-known/encryption-keys"; 60 public static final String IF_MODIFIED_SINCE_HEADER = "If-Modified-Since"; 61 private static final String DATE_FORMAT = "EEE, dd MMM yyyy HH:mm:ss 'GMT'"; 62 63 @NonNull private final AdServicesLogger mAdServicesLogger; 64 @NonNull private final FetchJobType mFetchJobType; 65 private boolean mIsFirstTimeFetch; 66 EncryptionKeyFetcher(@onNull FetchJobType fetchJobType)67 public EncryptionKeyFetcher(@NonNull FetchJobType fetchJobType) { 68 mAdServicesLogger = AdServicesLoggerImpl.getInstance(); 69 mFetchJobType = fetchJobType; 70 mIsFirstTimeFetch = false; 71 } 72 73 @VisibleForTesting EncryptionKeyFetcher(@onNull AdServicesLogger adServicesLogger)74 EncryptionKeyFetcher(@NonNull AdServicesLogger adServicesLogger) { 75 mFetchJobType = FetchJobType.ENCRYPTION_KEY_DAILY_FETCH_JOB; 76 mAdServicesLogger = adServicesLogger; 77 mIsFirstTimeFetch = false; 78 } 79 80 /** Define encryption key endpoint JSON response parameters. */ 81 @VisibleForTesting 82 interface JSONResponseContract { 83 String ENCRYPTION_KEY = "encryption_key"; 84 String SIGNING_KEY = "signing_key"; 85 String PROTOCOL_TYPE = "protocol_type"; 86 String KEYS = "keys"; 87 } 88 89 /** Define parameters for actual keys fields. */ 90 @VisibleForTesting 91 interface KeyResponseContract { 92 String ID = "id"; 93 String BODY = "body"; 94 String EXPIRY = "expiry"; 95 } 96 97 /** 98 * Send HTTP GET request to Adtech encryption key endpoint, parse JSON response, convert it to 99 * EncryptionKey objects. 100 * 101 * @param encryptionKey the existing encryption key in adservices_shared.db. 102 * @param enrollmentData the enrollment data that we needed to fetch keys. 103 * @param isFirstTimeFetch whether it is the first time key fetch for the enrollment data. 104 * @return a list of encryption keys or Optional.empty() when no key can be fetched. 105 */ fetchEncryptionKeys( @ullable EncryptionKey encryptionKey, @NonNull EnrollmentData enrollmentData, boolean isFirstTimeFetch)106 public Optional<List<EncryptionKey>> fetchEncryptionKeys( 107 @Nullable EncryptionKey encryptionKey, 108 @NonNull EnrollmentData enrollmentData, 109 boolean isFirstTimeFetch) { 110 mIsFirstTimeFetch = isFirstTimeFetch; 111 112 String encryptionKeyUrl = constructEncryptionKeyUrl(enrollmentData); 113 if (encryptionKeyUrl == null) { 114 logEncryptionKeyFetchedStats(FetchStatus.NULL_ENDPOINT, enrollmentData, null); 115 return Optional.empty(); 116 } 117 if (!isEncryptionKeyUrlValid(encryptionKeyUrl)) { 118 logEncryptionKeyFetchedStats( 119 FetchStatus.INVALID_ENDPOINT, enrollmentData, encryptionKeyUrl); 120 return Optional.empty(); 121 } 122 123 URL url; 124 try { 125 url = new URL(encryptionKeyUrl); 126 } catch (MalformedURLException e) { 127 LoggerFactory.getLogger().d(e, "Malformed encryption key URL."); 128 logEncryptionKeyFetchedStats( 129 FetchStatus.INVALID_ENDPOINT, enrollmentData, encryptionKeyUrl); 130 return Optional.empty(); 131 } 132 HttpURLConnection urlConnection; 133 try { 134 urlConnection = (HttpURLConnection) setUpURLConnection(url); 135 } catch (IOException e) { 136 LoggerFactory.getLogger().e(e, "Failed to open encryption key URL"); 137 logEncryptionKeyFetchedStats( 138 FetchStatus.IO_EXCEPTION, enrollmentData, encryptionKeyUrl); 139 return Optional.empty(); 140 } 141 try { 142 urlConnection.setRequestMethod("GET"); 143 if (!isFirstTimeFetch 144 && encryptionKey != null 145 && encryptionKey.getLastFetchTime() != 0L) { 146 // Re-fetch to update or revoke keys, use "If-Modified-Since" header with time diff 147 // set to `last_fetch_time` field saved in EncryptionKey object. 148 urlConnection.setRequestProperty( 149 IF_MODIFIED_SINCE_HEADER, 150 constructHttpDate(encryptionKey.getLastFetchTime())); 151 } 152 int responseCode = urlConnection.getResponseCode(); 153 if (!isFirstTimeFetch && responseCode == HttpURLConnection.HTTP_NOT_MODIFIED) { 154 LoggerFactory.getLogger() 155 .d( 156 "Re-fetch encryption key response code = " 157 + responseCode 158 + "no change made to previous keys."); 159 logEncryptionKeyFetchedStats( 160 FetchStatus.KEY_NOT_MODIFIED, enrollmentData, encryptionKeyUrl); 161 return Optional.empty(); 162 } 163 if (responseCode != HttpURLConnection.HTTP_OK) { 164 LoggerFactory.getLogger().v("Fetch encryption key response code = " + responseCode); 165 logEncryptionKeyFetchedStats( 166 FetchStatus.BAD_REQUEST_EXCEPTION, enrollmentData, encryptionKeyUrl); 167 return Optional.empty(); 168 } 169 JSONObject jsonResponse = 170 getJsonResponse(urlConnection, enrollmentData, encryptionKeyUrl); 171 if (jsonResponse == null) { 172 return Optional.empty(); 173 } 174 return parseEncryptionKeyJSONResponse(enrollmentData, encryptionKeyUrl, jsonResponse); 175 } catch (IOException e) { 176 LoggerFactory.getLogger().e(e, "Failed to get encryption key response."); 177 logEncryptionKeyFetchedStats( 178 FetchStatus.IO_EXCEPTION, enrollmentData, encryptionKeyUrl); 179 return Optional.empty(); 180 } finally { 181 urlConnection.disconnect(); 182 } 183 } 184 185 @Nullable constructEncryptionKeyUrl(EnrollmentData enrollmentData)186 private static String constructEncryptionKeyUrl(EnrollmentData enrollmentData) { 187 // We use encryption key url field in DB to store the Site, and append suffix to construct 188 // the actual encryption key url. 189 if (enrollmentData.getEncryptionKeyUrl() == null 190 || enrollmentData.getEncryptionKeyUrl().trim().isEmpty()) { 191 LoggerFactory.getLogger().d("No encryption key url in enrollment data."); 192 return null; 193 } else { 194 return enrollmentData.getEncryptionKeyUrl() + ENCRYPTION_KEY_ENDPOINT; 195 } 196 } 197 isEncryptionKeyUrlValid(String encryptionKeyUrl)198 private boolean isEncryptionKeyUrlValid(String encryptionKeyUrl) { 199 if (Uri.parse(encryptionKeyUrl).getScheme() == null 200 || !Uri.parse(encryptionKeyUrl).getScheme().equalsIgnoreCase("https")) { 201 LoggerFactory.getLogger().d("Encryption key url doesn't start with https."); 202 return false; 203 } 204 return true; 205 } 206 207 @Nullable getJsonResponse( HttpURLConnection urlConnection, EnrollmentData enrollmentData, String encryptionKeyUrl)208 private JSONObject getJsonResponse( 209 HttpURLConnection urlConnection, 210 EnrollmentData enrollmentData, 211 String encryptionKeyUrl) { 212 try { 213 BufferedReader bufferedReader = 214 new BufferedReader(new InputStreamReader(urlConnection.getInputStream())); 215 String inputLine; 216 StringBuilder response = new StringBuilder(); 217 while ((inputLine = bufferedReader.readLine()) != null) { 218 response.append(inputLine); 219 } 220 bufferedReader.close(); 221 return new JSONObject(response.toString()); 222 } catch (IOException | JSONException e) { 223 logEncryptionKeyFetchedStats( 224 FetchStatus.BAD_REQUEST_EXCEPTION, enrollmentData, encryptionKeyUrl); 225 return null; 226 } 227 } 228 229 /** 230 * Parse encryption key endpoint JSONResponse to an EncryptionKey object. 231 * 232 * @param enrollmentData the enrollment data contains the encryption key url. 233 * @param jsonObject JSONResponse received by calling encryption key endpoint. 234 * @return a list of encryption keys. 235 */ parseEncryptionKeyJSONResponse( @onNull EnrollmentData enrollmentData, String encryptionKeyUrl, @NonNull JSONObject jsonObject)236 private Optional<List<EncryptionKey>> parseEncryptionKeyJSONResponse( 237 @NonNull EnrollmentData enrollmentData, 238 String encryptionKeyUrl, 239 @NonNull JSONObject jsonObject) { 240 try { 241 List<EncryptionKey> encryptionKeys = new ArrayList<>(); 242 if (jsonObject.has(JSONResponseContract.ENCRYPTION_KEY)) { 243 JSONObject encryptionObject = 244 jsonObject.getJSONObject(JSONResponseContract.ENCRYPTION_KEY); 245 encryptionKeys.addAll( 246 buildEncryptionKeys( 247 enrollmentData, 248 encryptionObject, 249 EncryptionKey.KeyType.ENCRYPTION)); 250 } 251 if (jsonObject.has(JSONResponseContract.SIGNING_KEY)) { 252 JSONObject signingObject = 253 jsonObject.getJSONObject(JSONResponseContract.SIGNING_KEY); 254 encryptionKeys.addAll( 255 buildEncryptionKeys( 256 enrollmentData, signingObject, EncryptionKey.KeyType.SIGNING)); 257 } 258 logEncryptionKeyFetchedStats(FetchStatus.SUCCESS, enrollmentData, encryptionKeyUrl); 259 return Optional.of(encryptionKeys); 260 } catch (JSONException e) { 261 LoggerFactory.getLogger().e(e, "Parse json response to encryption key exception."); 262 logEncryptionKeyFetchedStats( 263 FetchStatus.BAD_REQUEST_EXCEPTION, enrollmentData, encryptionKeyUrl); 264 return Optional.empty(); 265 } 266 } 267 buildEncryptionKeys( EnrollmentData enrollmentData, JSONObject jsonObject, EncryptionKey.KeyType keyType)268 private List<EncryptionKey> buildEncryptionKeys( 269 EnrollmentData enrollmentData, JSONObject jsonObject, EncryptionKey.KeyType keyType) { 270 List<EncryptionKey> encryptionKeyList = new ArrayList<>(); 271 String encryptionKeyUrl = null; 272 Uri reportingOrigin = null; 273 if (enrollmentData.getEncryptionKeyUrl() != null) { 274 encryptionKeyUrl = enrollmentData.getEncryptionKeyUrl() + ENCRYPTION_KEY_ENDPOINT; 275 reportingOrigin = Uri.parse(enrollmentData.getEncryptionKeyUrl()); 276 } 277 278 EncryptionKey.ProtocolType protocolType; 279 try { 280 if (jsonObject.has(JSONResponseContract.PROTOCOL_TYPE)) { 281 protocolType = 282 EncryptionKey.ProtocolType.valueOf( 283 jsonObject.getString(JSONResponseContract.PROTOCOL_TYPE)); 284 } else { 285 // Set default protocol_type as HPKE for now since HPKE is the only encryption algo. 286 protocolType = EncryptionKey.ProtocolType.HPKE; 287 } 288 if (jsonObject.has(JSONResponseContract.KEYS)) { 289 JSONArray keyArray = jsonObject.getJSONArray(JSONResponseContract.KEYS); 290 for (int i = 0; i < keyArray.length(); i++) { 291 EncryptionKey.Builder builder = new EncryptionKey.Builder(); 292 builder.setId(UUID.randomUUID().toString()); 293 builder.setKeyType(keyType); 294 builder.setEnrollmentId(enrollmentData.getEnrollmentId()); 295 builder.setReportingOrigin(reportingOrigin); 296 builder.setEncryptionKeyUrl(encryptionKeyUrl); 297 builder.setProtocolType(protocolType); 298 299 JSONObject keyObject = keyArray.getJSONObject(i); 300 if (keyObject.has(KeyResponseContract.ID)) { 301 builder.setKeyCommitmentId(keyObject.getInt(KeyResponseContract.ID)); 302 } 303 if (keyObject.has(KeyResponseContract.BODY)) { 304 builder.setBody(keyObject.getString(KeyResponseContract.BODY)); 305 } 306 if (keyObject.has(KeyResponseContract.EXPIRY)) { 307 builder.setExpiration( 308 Long.parseLong(keyObject.getString(KeyResponseContract.EXPIRY))); 309 } 310 builder.setLastFetchTime(System.currentTimeMillis()); 311 encryptionKeyList.add(builder.build()); 312 } 313 } 314 } catch (JSONException e) { 315 LoggerFactory.getLogger() 316 .e( 317 e, 318 "Failed to build encryption key from json object exception." 319 + jsonObject); 320 logEncryptionKeyFetchedStats( 321 FetchStatus.BAD_REQUEST_EXCEPTION, enrollmentData, encryptionKeyUrl); 322 } 323 return encryptionKeyList; 324 } 325 326 /** Open a {@link URLConnection} and sets the network connection and read timeout. */ 327 @NonNull setUpURLConnection(@onNull URL url)328 public URLConnection setUpURLConnection(@NonNull URL url) throws IOException { 329 Objects.requireNonNull(url); 330 HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection(); 331 332 final Flags flags = FlagsFactory.getFlags(); 333 urlConnection.setConnectTimeout(flags.getEncryptionKeyNetworkConnectTimeoutMs()); 334 urlConnection.setReadTimeout(flags.getEncryptionKeyNetworkReadTimeoutMs()); 335 return urlConnection; 336 } 337 338 /** Construct a HttpDate string for time diff set to last_fetch_time. */ constructHttpDate(long lastFetchTime)339 private String constructHttpDate(long lastFetchTime) { 340 SimpleDateFormat dateFormat = new SimpleDateFormat(DATE_FORMAT, Locale.US); 341 dateFormat.setLenient(false); 342 dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); 343 return dateFormat.format(new Date(lastFetchTime)); 344 } 345 logEncryptionKeyFetchedStats( FetchStatus fetchStatus, EnrollmentData enrollmentData, @Nullable String encryptionKeyUrl)346 private void logEncryptionKeyFetchedStats( 347 FetchStatus fetchStatus, 348 EnrollmentData enrollmentData, 349 @Nullable String encryptionKeyUrl) { 350 AdServicesEncryptionKeyFetchedStats stats = 351 AdServicesEncryptionKeyFetchedStats.builder() 352 .setFetchJobType(mFetchJobType) 353 .setFetchStatus(fetchStatus) 354 .setIsFirstTimeFetch(mIsFirstTimeFetch) 355 .setAdtechEnrollmentId(enrollmentData.getEnrollmentId()) 356 .setEncryptionKeyUrl(encryptionKeyUrl) 357 .build(); 358 mAdServicesLogger.logEncryptionKeyFetchedStats(stats); 359 } 360 } 361