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