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.adselection.encryption;
18 
19 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.BACKGROUND_KEY_FETCH_STATUS_NO_OP;
20 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.BACKGROUND_KEY_FETCH_STATUS_REFRESH_KEYS_INITIATED;
21 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.SERVER_AUCTION_COORDINATOR_SOURCE_API;
22 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.SERVER_AUCTION_COORDINATOR_SOURCE_DEFAULT;
23 import static com.android.adservices.service.stats.AdsRelevanceStatusUtils.SERVER_AUCTION_KEY_FETCH_SOURCE_BACKGROUND_FETCH;
24 
25 import android.annotation.NonNull;
26 import android.content.Context;
27 import android.net.Uri;
28 
29 import com.android.adservices.LoggerFactory;
30 import com.android.adservices.concurrency.AdServicesExecutors;
31 import com.android.adservices.data.adselection.DBEncryptionKey;
32 import com.android.adservices.service.Flags;
33 import com.android.adservices.service.FlagsFactory;
34 import com.android.adservices.service.adselection.MultiCloudSupportStrategyFactory;
35 import com.android.adservices.service.common.AllowLists;
36 import com.android.adservices.service.common.SingletonRunner;
37 import com.android.adservices.service.common.httpclient.AdServicesHttpsClient;
38 import com.android.adservices.service.devapi.DevContext;
39 import com.android.adservices.service.stats.AdServicesLogger;
40 import com.android.adservices.service.stats.AdServicesLoggerImpl;
41 import com.android.adservices.service.stats.AdsRelevanceStatusUtils;
42 import com.android.adservices.service.stats.FetchProcessLogger;
43 import com.android.adservices.service.stats.ServerAuctionBackgroundKeyFetchScheduledStats;
44 import com.android.adservices.service.stats.ServerAuctionKeyFetchExecutionLoggerFactory;
45 import com.android.internal.annotations.VisibleForTesting;
46 
47 import com.google.common.base.Strings;
48 import com.google.common.util.concurrent.ExecutionSequencer;
49 import com.google.common.util.concurrent.FluentFuture;
50 import com.google.common.util.concurrent.Futures;
51 import com.google.common.util.concurrent.ListenableFuture;
52 
53 import java.time.Clock;
54 import java.time.Instant;
55 import java.util.ArrayList;
56 import java.util.List;
57 import java.util.Objects;
58 import java.util.Set;
59 import java.util.concurrent.TimeUnit;
60 import java.util.function.Supplier;
61 import java.util.stream.Collectors;
62 import java.util.stream.Stream;
63 
64 /** Worker instance for fetching encryption keys and persisting to DB. */
65 public class BackgroundKeyFetchWorker {
66     private static final LoggerFactory.Logger sLogger = LoggerFactory.getFledgeLogger();
67     public static final String JOB_DESCRIPTION = "Ad selection data encryption key fetch job";
68     private static final Object SINGLETON_LOCK = new Object();
69     private static volatile BackgroundKeyFetchWorker sBackgroundKeyFetchWorker;
70     private final ProtectedServersEncryptionConfigManagerBase mKeyConfigManager;
71     private final DevContext mDevContext;
72     private final Flags mFlags;
73     private final Clock mClock;
74     private final AdServicesLogger mAdServicesLogger;
75     private final SingletonRunner<Void> mSingletonRunner =
76             new SingletonRunner<>(JOB_DESCRIPTION, this::doRun);
77 
78     @VisibleForTesting
BackgroundKeyFetchWorker( @onNull ProtectedServersEncryptionConfigManagerBase keyConfigManager, @NonNull DevContext devContext, @NonNull Flags flags, @NonNull Clock clock, @NonNull AdServicesLogger adServicesLogger)79     protected BackgroundKeyFetchWorker(
80             @NonNull ProtectedServersEncryptionConfigManagerBase keyConfigManager,
81             @NonNull DevContext devContext,
82             @NonNull Flags flags,
83             @NonNull Clock clock,
84             @NonNull AdServicesLogger adServicesLogger) {
85         Objects.requireNonNull(keyConfigManager);
86         Objects.requireNonNull(devContext);
87         Objects.requireNonNull(flags);
88         Objects.requireNonNull(clock);
89         Objects.requireNonNull(adServicesLogger);
90         mKeyConfigManager = keyConfigManager;
91         mDevContext = devContext;
92         mClock = clock;
93         mFlags = flags;
94         mAdServicesLogger = adServicesLogger;
95     }
96 
97     /**
98      * Gets an instance of a {@link BackgroundKeyFetchWorker}. If an instance hasn't been
99      * initialized, a new singleton will be created and returned.
100      */
101     @NonNull
getInstance(@onNull Context context)102     public static BackgroundKeyFetchWorker getInstance(@NonNull Context context) {
103         Objects.requireNonNull(context);
104 
105         if (sBackgroundKeyFetchWorker == null) {
106             synchronized (SINGLETON_LOCK) {
107                 if (sBackgroundKeyFetchWorker == null) {
108                     Flags flags = FlagsFactory.getFlags();
109                     AdServicesHttpsClient adServicesHttpsClient =
110                             new AdServicesHttpsClient(
111                                     AdServicesExecutors.getBlockingExecutor(),
112                                     flags
113                                             .getFledgeAuctionServerBackgroundKeyFetchNetworkConnectTimeoutMs(),
114                                     flags
115                                             .getFledgeAuctionServerBackgroundKeyFetchNetworkReadTimeoutMs(),
116                                     flags.getFledgeAuctionServerBackgroundKeyFetchMaxResponseSizeB());
117                     ProtectedServersEncryptionConfigManagerBase configManager =
118                             MultiCloudSupportStrategyFactory.getStrategy(
119                                             flags.getFledgeAuctionServerMultiCloudEnabled(),
120                                             flags.getFledgeAuctionServerCoordinatorUrlAllowlist())
121                                     .getEncryptionConfigManager(
122                                             context, flags, adServicesHttpsClient);
123                     // TODO (b/344636522): Derive DevContext from calling environment.
124                     sBackgroundKeyFetchWorker =
125                             new BackgroundKeyFetchWorker(
126                                     configManager,
127                                     DevContext.createForDevOptionsDisabled(),
128                                     flags,
129                                     Clock.systemUTC(),
130                                     AdServicesLoggerImpl.getInstance());
131                 }
132             }
133         }
134         return sBackgroundKeyFetchWorker;
135     }
136 
getFlags()137     public Flags getFlags() {
138         return mFlags;
139     }
140 
concatAbsentAndExpiredKeyTypes(Instant keyExpiryInstant)141     private Set<Integer> concatAbsentAndExpiredKeyTypes(Instant keyExpiryInstant) {
142         return Stream.concat(
143                         mKeyConfigManager
144                                 .getExpiredAdSelectionEncryptionKeyTypes(keyExpiryInstant)
145                                 .stream(),
146                         mKeyConfigManager.getAbsentAdSelectionEncryptionKeyTypes().stream())
147                 .collect(Collectors.toSet());
148     }
149 
getAbsentAndExpiredKeyTypes(Instant keyExpiryInstant)150     private FluentFuture<Set<Integer>> getAbsentAndExpiredKeyTypes(Instant keyExpiryInstant) {
151         return FluentFuture.from(
152                 AdServicesExecutors.getBackgroundExecutor()
153                         .submit(() -> concatAbsentAndExpiredKeyTypes(keyExpiryInstant)));
154     }
155 
getExpiredKeyTypes(Instant keyExpiryInstant)156     private FluentFuture<Set<Integer>> getExpiredKeyTypes(Instant keyExpiryInstant) {
157         return FluentFuture.from(
158                 AdServicesExecutors.getBackgroundExecutor()
159                         .submit(
160                                 () ->
161                                         mKeyConfigManager.getExpiredAdSelectionEncryptionKeyTypes(
162                                                 keyExpiryInstant)));
163     }
164 
fetchNewKeys( Set<Integer> expiredKeyTypes, Instant keyExpiryInstant, Supplier<Boolean> shouldStop)165     private FluentFuture<Void> fetchNewKeys(
166             Set<Integer> expiredKeyTypes, Instant keyExpiryInstant, Supplier<Boolean> shouldStop) {
167         if (expiredKeyTypes.isEmpty()) {
168             if (mFlags.getFledgeAuctionServerKeyFetchMetricsEnabled()) {
169                 mAdServicesLogger.logServerAuctionBackgroundKeyFetchScheduledStats(
170                         ServerAuctionBackgroundKeyFetchScheduledStats.builder()
171                                 .setStatus(BACKGROUND_KEY_FETCH_STATUS_NO_OP)
172                                 .setCountAuctionUrls(0)
173                                 .setCountJoinUrls(0)
174                                 .build());
175             }
176 
177             return FluentFuture.from(Futures.immediateVoidFuture())
178                     .transform(ignored -> null, AdServicesExecutors.getLightWeightExecutor());
179         }
180 
181         List<ListenableFuture<List<DBEncryptionKey>>> keyFetchFutures = new ArrayList<>();
182         int countAuctionUrls = 0;
183         int countJoinUrls = 0;
184 
185         // Keys are fetched and persisted in sequence to prevent making multiple network
186         // calls in parallel.
187         ExecutionSequencer sequencer = ExecutionSequencer.create();
188         ServerAuctionKeyFetchExecutionLoggerFactory serverAuctionKeyFetchExecutionLoggerFactory =
189                 new ServerAuctionKeyFetchExecutionLoggerFactory(
190                         com.android.adservices.shared.util.Clock.getInstance(),
191                         mAdServicesLogger,
192                         mFlags);
193         FetchProcessLogger keyFetchLogger =
194                 serverAuctionKeyFetchExecutionLoggerFactory.getAdsRelevanceExecutionLogger();
195         keyFetchLogger.setSource(SERVER_AUCTION_KEY_FETCH_SOURCE_BACKGROUND_FETCH);
196 
197         if (mFlags.getFledgeAuctionServerBackgroundAuctionKeyFetchEnabled()
198                 && expiredKeyTypes.contains(
199                         AdSelectionEncryptionKey.AdSelectionEncryptionKeyType.AUCTION)
200                 && !shouldStop.get()) {
201 
202             boolean multicloudEnabled = mFlags.getFledgeAuctionServerMultiCloudEnabled();
203             String allowlist = mFlags.getFledgeAuctionServerCoordinatorUrlAllowlist();
204 
205             if (multicloudEnabled && !Strings.isNullOrEmpty(allowlist)) {
206                 List<String> allowedUrls = AllowLists.splitAllowList(allowlist);
207                 countAuctionUrls = allowedUrls.size();
208                 keyFetchLogger.setCoordinatorSource(SERVER_AUCTION_COORDINATOR_SOURCE_API);
209                 for (String coordinator : allowedUrls) {
210                     keyFetchFutures.add(
211                             fetchAndPersistAuctionKeys(
212                                     keyExpiryInstant,
213                                     sequencer,
214                                     Uri.parse(coordinator),
215                                     keyFetchLogger));
216                 }
217             } else {
218                 String defaultUrl = mFlags.getFledgeAuctionServerAuctionKeyFetchUri();
219                 if (defaultUrl != null) {
220                     countAuctionUrls = 1;
221                     keyFetchLogger.setCoordinatorSource(SERVER_AUCTION_COORDINATOR_SOURCE_DEFAULT);
222                     keyFetchFutures.add(
223                             fetchAndPersistAuctionKeys(
224                                     keyExpiryInstant,
225                                     sequencer,
226                                     Uri.parse(defaultUrl),
227                                     keyFetchLogger));
228                 }
229             }
230         }
231 
232         if (mFlags.getFledgeAuctionServerBackgroundJoinKeyFetchEnabled()
233                 && expiredKeyTypes.contains(
234                         AdSelectionEncryptionKey.AdSelectionEncryptionKeyType.JOIN)
235                 && !shouldStop.get()) {
236             countJoinUrls = 1;
237             keyFetchLogger.setCoordinatorSource(SERVER_AUCTION_COORDINATOR_SOURCE_DEFAULT);
238             keyFetchFutures.add(
239                     fetchAndPersistJoinKey(keyExpiryInstant, sequencer, keyFetchLogger));
240         }
241 
242         if (mFlags.getFledgeAuctionServerKeyFetchMetricsEnabled()) {
243             @AdsRelevanceStatusUtils.BackgroundKeyFetchStatus
244             int status =
245                     countAuctionUrls + countJoinUrls > 0
246                             ? BACKGROUND_KEY_FETCH_STATUS_REFRESH_KEYS_INITIATED
247                             : BACKGROUND_KEY_FETCH_STATUS_NO_OP;
248 
249             mAdServicesLogger.logServerAuctionBackgroundKeyFetchScheduledStats(
250                     ServerAuctionBackgroundKeyFetchScheduledStats.builder()
251                             .setStatus(status)
252                             .setCountAuctionUrls(countAuctionUrls)
253                             .setCountJoinUrls(countJoinUrls)
254                             .build());
255         }
256 
257         return FluentFuture.from(Futures.allAsList(keyFetchFutures))
258                 .withTimeout(
259                         mFlags.getFledgeAuctionServerBackgroundKeyFetchJobMaxRuntimeMs(),
260                         TimeUnit.MILLISECONDS,
261                         AdServicesExecutors.getScheduler())
262                 .transform(ignored -> null, AdServicesExecutors.getLightWeightExecutor());
263     }
264 
doRun(@onNull Supplier<Boolean> shouldStop)265     private FluentFuture<Void> doRun(@NonNull Supplier<Boolean> shouldStop) {
266         if (shouldStop.get()) {
267             sLogger.d("Stopping " + JOB_DESCRIPTION);
268             return FluentFuture.from(Futures.immediateVoidFuture())
269                     .transform(ignored -> null, AdServicesExecutors.getLightWeightExecutor());
270         }
271 
272         Instant currentInstant = mClock.instant();
273         if (mFlags.getFledgeAuctionServerBackgroundKeyFetchOnEmptyDbAndInAdvanceEnabled()) {
274             long inAdvanceIntervalMs =
275                     mFlags.getFledgeAuctionServerBackgroundKeyFetchInAdvanceIntervalMs();
276             return getAbsentAndExpiredKeyTypes(currentInstant.plusMillis(inAdvanceIntervalMs))
277                     .transformAsync(
278                             keyTypesToFetch ->
279                                     fetchNewKeys(keyTypesToFetch, currentInstant, shouldStop),
280                             AdServicesExecutors.getBackgroundExecutor());
281         }
282 
283         return getExpiredKeyTypes(currentInstant)
284                 .transformAsync(
285                         expiredKeyTypes ->
286                                 fetchNewKeys(expiredKeyTypes, currentInstant, shouldStop),
287                         AdServicesExecutors.getBackgroundExecutor());
288     }
289 
290     /**
291      * Runs the background key fetch job for Ad Selection Data, including persisting fetched key and
292      * removing expired keys.
293      *
294      * @return A future to be used to check when the task has completed.
295      */
runBackgroundKeyFetch()296     public FluentFuture<Void> runBackgroundKeyFetch() {
297         sLogger.d("Starting %s", JOB_DESCRIPTION);
298         return mSingletonRunner.runSingleInstance();
299     }
300 
301     /** Requests that any ongoing work be stopped gracefully and waits for work to be stopped. */
stopWork()302     public void stopWork() {
303         mSingletonRunner.stopWork();
304     }
305 
fetchAndPersistAuctionKeys( Instant keyExpiryInstant, ExecutionSequencer sequencer, Uri coordinatorUri, FetchProcessLogger keyFetchLogger)306     private ListenableFuture<List<DBEncryptionKey>> fetchAndPersistAuctionKeys(
307             Instant keyExpiryInstant,
308             ExecutionSequencer sequencer,
309             Uri coordinatorUri,
310             FetchProcessLogger keyFetchLogger) {
311 
312         return sequencer.submitAsync(
313                 () ->
314                         mKeyConfigManager.fetchAndPersistActiveKeysOfType(
315                                 AdSelectionEncryptionKey.AdSelectionEncryptionKeyType.AUCTION,
316                                 keyExpiryInstant,
317                                 mFlags.getFledgeAuctionServerBackgroundKeyFetchJobMaxRuntimeMs(),
318                                 coordinatorUri,
319                                 mDevContext,
320                                 keyFetchLogger),
321                 AdServicesExecutors.getBackgroundExecutor());
322     }
323 
fetchAndPersistJoinKey( Instant keyExpiryInstant, ExecutionSequencer sequencer, FetchProcessLogger keyFetchLogger)324     private ListenableFuture<List<DBEncryptionKey>> fetchAndPersistJoinKey(
325             Instant keyExpiryInstant,
326             ExecutionSequencer sequencer,
327             FetchProcessLogger keyFetchLogger) {
328         return sequencer.submitAsync(
329                 () ->
330                         mKeyConfigManager.fetchAndPersistActiveKeysOfType(
331                                 AdSelectionEncryptionKey.AdSelectionEncryptionKeyType.JOIN,
332                                 keyExpiryInstant,
333                                 mFlags.getFledgeAuctionServerBackgroundKeyFetchJobMaxRuntimeMs(),
334                                 null,
335                                 mDevContext,
336                                 keyFetchLogger),
337                 AdServicesExecutors.getBackgroundExecutor());
338     }
339 }
340