• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.odp.module.common.encryption;
18 
19 import android.annotation.Nullable;
20 import android.content.Context;
21 
22 import com.android.federatedcompute.internal.util.LogUtil;
23 import com.android.odp.module.common.Clock;
24 import com.android.odp.module.common.EventLogger;
25 import com.android.odp.module.common.MonotonicClock;
26 import com.android.odp.module.common.data.OdpEncryptionKeyDao;
27 import com.android.odp.module.common.data.OdpSQLiteOpenHelper;
28 import com.android.odp.module.common.http.HttpClient;
29 import com.android.odp.module.common.http.HttpClientUtils;
30 import com.android.odp.module.common.http.OdpHttpRequest;
31 import com.android.odp.module.common.http.OdpHttpResponse;
32 
33 import com.google.common.annotations.VisibleForTesting;
34 import com.google.common.collect.ImmutableList;
35 import com.google.common.util.concurrent.FluentFuture;
36 import com.google.common.util.concurrent.Futures;
37 import com.google.common.util.concurrent.ListeningExecutorService;
38 
39 import org.json.JSONArray;
40 import org.json.JSONException;
41 import org.json.JSONObject;
42 
43 import java.util.HashMap;
44 import java.util.List;
45 import java.util.Locale;
46 import java.util.Map;
47 import java.util.Objects;
48 import java.util.Optional;
49 import java.util.Random;
50 import java.util.concurrent.TimeUnit;
51 import java.util.concurrent.TimeoutException;
52 
53 /** Class to manage key fetch. */
54 public class OdpEncryptionKeyManager {
55     private static final String TAG = OdpEncryptionKeyManager.class.getSimpleName();
56 
57     // Helper class to allow injection of flags from either ODP or FCP code.
58     public interface KeyManagerConfig {
59 
60         /** Url from which to get encryption keys. */
getEncryptionKeyFetchUrl()61         String getEncryptionKeyFetchUrl();
62 
63         /** Retry limit for encryption key http requests. */
getHttpRequestRetryLimit()64         int getHttpRequestRetryLimit();
65 
66         /** Max age in seconds for federated compute encryption keys. */
getEncryptionKeyMaxAgeSeconds()67         long getEncryptionKeyMaxAgeSeconds();
68 
69         /** The {@link OdpSQLiteOpenHelper} instance for use by the encryption DAO. */
getSQLiteOpenHelper()70         OdpSQLiteOpenHelper getSQLiteOpenHelper();
71 
72         /** Background executor for use in key fetch and DB updates etc. */
getBackgroundExecutor()73         ListeningExecutorService getBackgroundExecutor();
74 
75         /** Blocking executor for use in http connection. */
getBlockingExecutor()76         ListeningExecutorService getBlockingExecutor();
77 
78     }
79 
80     private interface EncryptionKeyResponseContract {
81         String RESPONSE_HEADER_CACHE_CONTROL_LABEL = "cache-control";
82         String RESPONSE_HEADER_AGE_LABEL = "age";
83 
84         String RESPONSE_HEADER_CACHE_CONTROL_MAX_AGE_LABEL = "max-age=";
85 
86         String RESPONSE_KEYS_LABEL = "keys";
87 
88         String RESPONSE_KEY_ID_LABEL = "id";
89 
90         String RESPONSE_PUBLIC_KEY = "key";
91     }
92 
93     private final OdpEncryptionKeyDao mEncryptionKeyDao;
94 
95     private static volatile OdpEncryptionKeyManager sBackgroundKeyManager;
96 
97     private final Clock mClock;
98 
99     private final KeyManagerConfig mKeyManagerConfig;
100 
101     private final HttpClient mHttpClient;
102 
103     private final ListeningExecutorService mBackgroundExecutor;
104 
OdpEncryptionKeyManager( Clock clock, OdpEncryptionKeyDao encryptionKeyDao, KeyManagerConfig keyManagerConfig, HttpClient httpClient, ListeningExecutorService backgroundExecutor)105     private OdpEncryptionKeyManager(
106             Clock clock,
107             OdpEncryptionKeyDao encryptionKeyDao,
108             KeyManagerConfig keyManagerConfig,
109             HttpClient httpClient,
110             ListeningExecutorService backgroundExecutor) {
111         mClock = clock;
112         mEncryptionKeyDao = encryptionKeyDao;
113         mKeyManagerConfig = keyManagerConfig;
114         mHttpClient = httpClient;
115         mBackgroundExecutor = backgroundExecutor;
116     }
117 
118     @VisibleForTesting
resetForTesting()119     public static synchronized void resetForTesting() {
120         sBackgroundKeyManager = null;
121     }
122 
123     /**
124      * Test only getter that allows injection of test/mock versions of clock, DAO etc.
125      *
126      * <p>Should be used in conjunction with {@link #resetForTesting()}
127      */
128     @VisibleForTesting
getInstanceForTesting( Clock clock, OdpEncryptionKeyDao encryptionKeyDao, KeyManagerConfig keyManagerConfig, HttpClient httpClient, ListeningExecutorService backgroundExecutor)129     public static OdpEncryptionKeyManager getInstanceForTesting(
130             Clock clock,
131             OdpEncryptionKeyDao encryptionKeyDao,
132             KeyManagerConfig keyManagerConfig,
133             HttpClient httpClient,
134             ListeningExecutorService backgroundExecutor) {
135         if (sBackgroundKeyManager == null) {
136             synchronized (OdpEncryptionKeyManager.class) {
137                 if (sBackgroundKeyManager == null) {
138                     sBackgroundKeyManager =
139                             new OdpEncryptionKeyManager(
140                                     clock,
141                                     encryptionKeyDao,
142                                     keyManagerConfig,
143                                     httpClient,
144                                     backgroundExecutor);
145                 }
146             }
147         }
148         return sBackgroundKeyManager;
149     }
150 
151     /** Returns a singleton instance for the {@link OdpEncryptionKeyManager}. */
getInstance( Context context, KeyManagerConfig keyManagerConfig)152     public static OdpEncryptionKeyManager getInstance(
153             Context context, KeyManagerConfig keyManagerConfig) {
154         if (sBackgroundKeyManager == null) {
155             synchronized (OdpEncryptionKeyManager.class) {
156                 if (sBackgroundKeyManager == null) {
157                     OdpEncryptionKeyDao encryptionKeyDao =
158                             OdpEncryptionKeyDao.getInstance(
159                                     context, keyManagerConfig.getSQLiteOpenHelper());
160                     sBackgroundKeyManager =
161                             new OdpEncryptionKeyManager(
162                                     MonotonicClock.getInstance(),
163                                     encryptionKeyDao,
164                                     keyManagerConfig,
165                                     new HttpClient(
166                                             keyManagerConfig.getHttpRequestRetryLimit(),
167                                             keyManagerConfig.getBlockingExecutor()),
168                                     keyManagerConfig.getBackgroundExecutor());
169                 }
170             }
171         }
172         return sBackgroundKeyManager;
173     }
174 
175     /**
176      * Fetch the active key from the server, persists the fetched key to encryption_key table, and
177      * deletes expired keys
178      */
fetchAndPersistActiveKeys( @dpEncryptionKey.KeyType int keyType, boolean isScheduledJob, Optional<EventLogger> loggerOptional)179     public FluentFuture<List<OdpEncryptionKey>> fetchAndPersistActiveKeys(
180             @OdpEncryptionKey.KeyType int keyType, boolean isScheduledJob,
181             Optional<EventLogger> loggerOptional) {
182         String fetchUri = mKeyManagerConfig.getEncryptionKeyFetchUrl();
183         if (fetchUri == null) {
184             loggerOptional.ifPresent(EventLogger::logEncryptionKeyFetchEmptyUriEventKind);
185             return FluentFuture.from(
186                     Futures.immediateFailedFuture(
187                             new IllegalArgumentException(
188                                     "Url to fetch active encryption keys is null")));
189         }
190 
191         OdpHttpRequest request;
192         try {
193             request =
194                     OdpHttpRequest.create(
195                             fetchUri,
196                             HttpClientUtils.HttpMethod.GET,
197                             new HashMap<>(),
198                             HttpClientUtils.EMPTY_BODY);
199         } catch (Exception e) {
200             loggerOptional.ifPresent(EventLogger::logEncryptionKeyFetchRequestFailEventKind);
201             return FluentFuture.from(Futures.immediateFailedFuture(e));
202         }
203 
204         return FluentFuture.from(mHttpClient.performRequestAsyncWithRetry(request))
205                 .transform(
206                         response ->
207                                 parseFetchEncryptionKeyPayload(
208                                         response,
209                                         keyType,
210                                         mClock.currentTimeMillis(),
211                                         loggerOptional),
212                         mBackgroundExecutor)
213                 .transform(
214                         result -> {
215                             result.forEach(mEncryptionKeyDao::insertEncryptionKey);
216                             if (isScheduledJob) {
217                                 // When the job is a background scheduled job, delete the
218                                 // expired keys, otherwise, only fetch from the key server.
219                                 mEncryptionKeyDao.deleteExpiredKeys();
220                             }
221                             return result;
222                         },
223                         mBackgroundExecutor); // TODO: Add timeout controlled by Ph flags
224     }
225 
parseFetchEncryptionKeyPayload( OdpHttpResponse keyFetchResponse, @OdpEncryptionKey.KeyType int keyType, Long fetchTime, Optional<EventLogger> loggerOptional)226     private ImmutableList<OdpEncryptionKey> parseFetchEncryptionKeyPayload(
227             OdpHttpResponse keyFetchResponse,
228             @OdpEncryptionKey.KeyType int keyType,
229             Long fetchTime,
230             Optional<EventLogger> loggerOptional) {
231         String payload = new String(Objects.requireNonNull(keyFetchResponse.getPayload()));
232         Map<String, List<String>> headers = keyFetchResponse.getHeaders();
233         long ttlInSeconds = getTTL(headers);
234         if (ttlInSeconds <= 0) {
235             ttlInSeconds = mKeyManagerConfig.getEncryptionKeyMaxAgeSeconds();
236         }
237 
238         try {
239             JSONObject responseObj = new JSONObject(payload);
240             JSONArray keysArr =
241                     responseObj.getJSONArray(EncryptionKeyResponseContract.RESPONSE_KEYS_LABEL);
242             ImmutableList.Builder<OdpEncryptionKey> encryptionKeys = ImmutableList.builder();
243 
244             for (int i = 0; i < keysArr.length(); i++) {
245                 JSONObject keyObj = keysArr.getJSONObject(i);
246                 OdpEncryptionKey key =
247                         new OdpEncryptionKey.Builder()
248                                 .setKeyIdentifier(
249                                         keyObj.getString(
250                                                 EncryptionKeyResponseContract
251                                                         .RESPONSE_KEY_ID_LABEL))
252                                 .setPublicKey(
253                                         keyObj.getString(
254                                                 EncryptionKeyResponseContract.RESPONSE_PUBLIC_KEY))
255                                 .setKeyType(keyType)
256                                 .setCreationTime(fetchTime)
257                                 .setExpiryTime(
258                                         fetchTime + ttlInSeconds * 1000) // convert to milliseconds
259                                 .build();
260                 encryptionKeys.add(key);
261             }
262             return encryptionKeys.build();
263         } catch (JSONException e) {
264             loggerOptional.ifPresent(EventLogger::logEncryptionKeyFetchInvalidPayloadEventKind);
265             LogUtil.e(TAG, "Invalid Json response: " + e.getMessage());
266             return ImmutableList.of();
267         }
268     }
269 
270     /**
271      * Parse the "age" and "cache-control" of response headers. Calculate the ttl of the current key
272      * max-age (in cache-control) - age.
273      *
274      * @return the ttl in seconds of the keys.
275      */
276     @VisibleForTesting
getTTL(Map<String, List<String>> headers)277     static long getTTL(Map<String, List<String>> headers) {
278         String cacheControl = null;
279         int cachedAge = 0;
280         int remainingHeaders = 2;
281         for (String key : headers.keySet()) {
282             if (key != null) {
283                 if (key.equalsIgnoreCase(
284                         EncryptionKeyResponseContract.RESPONSE_HEADER_CACHE_CONTROL_LABEL)) {
285                     List<String> field = headers.get(key);
286                     if (field != null && field.size() > 0) {
287                         cacheControl = field.get(0).toLowerCase(Locale.ENGLISH);
288                         remainingHeaders -= 1;
289                     }
290                 } else if (key.equalsIgnoreCase(
291                         EncryptionKeyResponseContract.RESPONSE_HEADER_AGE_LABEL)) {
292                     List<String> field = headers.get(key);
293                     if (field != null && field.size() > 0) {
294                         try {
295                             cachedAge = Integer.parseInt(field.get(0));
296                         } catch (NumberFormatException e) {
297                             LogUtil.e(TAG, "Error parsing age header");
298                         }
299                         remainingHeaders -= 1;
300                     }
301                 }
302             }
303             if (remainingHeaders == 0) {
304                 break;
305             }
306         }
307         if (cacheControl == null) {
308             LogUtil.d(TAG, "Cache-Control header or value is missing");
309             return 0;
310         }
311 
312         String[] tokens = cacheControl.split(",", /* limit= */ 0);
313         long maxAge = 0;
314         for (String s : tokens) {
315             String token = s.trim();
316             if (token.startsWith(
317                     EncryptionKeyResponseContract.RESPONSE_HEADER_CACHE_CONTROL_MAX_AGE_LABEL)) {
318                 try {
319                     maxAge =
320                             Long.parseLong(
321                                     token.substring(
322                                             /* beginIndex= */ EncryptionKeyResponseContract
323                                                     .RESPONSE_HEADER_CACHE_CONTROL_MAX_AGE_LABEL
324                                                     .length())); // in the format of
325                     // "max-age=<number>"
326                 } catch (NumberFormatException e) {
327                     LogUtil.d(TAG, "Failed to parse max-age value");
328                     return 0;
329                 }
330             }
331         }
332         if (maxAge == 0) {
333             LogUtil.d(TAG, "max-age directive is missing");
334             return 0;
335         }
336         return maxAge - cachedAge;
337     }
338 
339     /**
340      * Helper method that returns one key at random from provided list of active {@link
341      * OdpEncryptionKey}s.
342      */
343     @Nullable
getRandomKey(List<OdpEncryptionKey> activeKeys)344     public static OdpEncryptionKey getRandomKey(List<OdpEncryptionKey> activeKeys) {
345         return activeKeys.isEmpty()
346                 ? null
347                 : activeKeys.get(new Random().nextInt(activeKeys.size()));
348     }
349 
350     /**
351      * Get active keys, if there is no active key, then force a fetch from the key service. In the
352      * case of key fetching from the key service, the http call is executed on a {@code
353      * BlockingExecutor}.
354      *
355      * @return The list of active keys.
356      */
getOrFetchActiveKeys(int keyType, int keyCount, Optional<EventLogger> loggerOptional)357     public List<OdpEncryptionKey> getOrFetchActiveKeys(int keyType, int keyCount,
358             Optional<EventLogger> loggerOptional) {
359         List<OdpEncryptionKey> activeKeys = mEncryptionKeyDao.getLatestExpiryNKeys(keyCount);
360         if (activeKeys.size() > 0) {
361             LogUtil.d(TAG, "Existing active keys present, number of keys : " + activeKeys.size());
362             return activeKeys;
363         }
364 
365         loggerOptional.ifPresent(EventLogger::logEncryptionKeyFetchStartEventKind);
366         LogUtil.d(TAG, "No existing active keys present, fetching new encryption keys.");
367         try {
368             var fetchedKeysUnused =
369                     fetchAndPersistActiveKeys(keyType, /* isScheduledJob= */ false, loggerOptional)
370                             .get(/* timeout= */ 5, TimeUnit.SECONDS);
371             activeKeys = mEncryptionKeyDao.getLatestExpiryNKeys(keyCount);
372             if (activeKeys.size() > 0) {
373                 return activeKeys;
374             }
375         } catch (TimeoutException e) {
376             LogUtil.d(TAG, "Time out when forcing encryption key fetch: " + e.getMessage());
377             loggerOptional.ifPresent(EventLogger::logEncryptionKeyFetchTimeoutEventKind);
378         } catch (Exception e) {
379             LogUtil.d(
380                     TAG,
381                     "Exception encountered when forcing encryption key fetch: " + e.getMessage());
382             loggerOptional.ifPresent(EventLogger::logEncryptionKeyFetchFailEventKind);
383         }
384         return activeKeys;
385     }
386 
387     /** Helper method to allow testing of injected {@link KeyManagerConfig}. */
388     @VisibleForTesting
getKeyManagerConfigForTesting()389     public KeyManagerConfig getKeyManagerConfigForTesting() {
390         return mKeyManagerConfig;
391     }
392 }
393