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