1 /* 2 * Copyright (C) 2022 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.rkpdapp.utils; 18 19 import android.content.Context; 20 import android.content.SharedPreferences; 21 import android.os.SystemProperties; 22 import android.util.Log; 23 24 import com.android.rkpdapp.GeekResponse; 25 import com.android.rkpdapp.database.InstantConverter; 26 27 import java.net.MalformedURLException; 28 import java.net.URL; 29 import java.time.Duration; 30 import java.time.Instant; 31 import java.util.Random; 32 33 /** 34 * Settings makes use of SharedPreferences in order to store key/value pairs related to 35 * configuration settings that can be retrieved from the server. In the event that none 36 * have yet been retrieved, or for some reason a reset has occurred, there are 37 * reasonable default values. 38 */ 39 public class Settings { 40 41 public static final int ID_UPPER_BOUND = 1000000; 42 public static final int EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT = 6; 43 // Check for expiring certs in the next 3 days 44 public static final int EXPIRING_BY_MS_DEFAULT = 1000 * 60 * 60 * 24 * 3; 45 // Limit data consumption from failures within a window of time to 1 MB. 46 public static final int FAILURE_DATA_USAGE_MAX = 1024 * 1024; 47 public static final Duration FAILURE_DATA_USAGE_WINDOW = Duration.ofDays(1); 48 public static final int MAX_REQUEST_TIME_MS_DEFAULT = 20000; 49 public static final int NO_TIME_PROVIDED = -1; 50 51 private static final String KEY_EXPIRING_BY = "expiring_by"; 52 private static final String KEY_EXTRA_KEYS = "extra_keys"; 53 private static final String KEY_ID = "settings_id"; 54 private static final String KEY_FAILURE_DATA_WINDOW_START_TIME = "failure_start_time"; 55 private static final String KEY_FAILURE_COUNTER = "failure_counter"; 56 private static final String KEY_FAILURE_BYTES = "failure_data"; 57 private static final String KEY_URL = "url"; 58 private static final String KEY_MAX_REQUEST_TIME = "max_request_time"; 59 private static final String KEY_LAST_BAD_CERT_TIME_START = "bad_cert_time_start"; 60 private static final String KEY_LAST_BAD_CERT_TIME_END = "bad_cert_time_end"; 61 private static final String PREFERENCES_NAME = "com.android.rkpdapp.utils.preferences"; 62 private static final String TAG = "RkpdSettings"; 63 64 /** 65 * Determines if there is enough data budget remaining to attempt provisioning. 66 * If {@code FAILURE_DATA_USAGE_MAX} bytes have already been used up in previous calls that 67 * resulted in errors, then false will be returned. 68 * <p> 69 * Additionally, the rolling window of data usage is managed within this call. The used data 70 * budget will be reset if a time greater than {@code FAILURE_DATA_USAGE_WINDOW} has passed. 71 * 72 * @param context The application context 73 * @param curTime An instant representing the current time to measure the window against. If 74 * null, then the code will use {@code Instant.now()} instead. 75 * @return if the data budget has been exceeded. 76 */ hasErrDataBudget(Context context, Instant curTime)77 public static boolean hasErrDataBudget(Context context, Instant curTime) { 78 if (curTime == null) { 79 curTime = Instant.now(); 80 } 81 SharedPreferences sharedPref = getSharedPreferences(context); 82 Instant logged = 83 Instant.ofEpochMilli(sharedPref.getLong(KEY_FAILURE_DATA_WINDOW_START_TIME, 0)); 84 if (Duration.between(logged, curTime).compareTo(FAILURE_DATA_USAGE_WINDOW) > 0) { 85 SharedPreferences.Editor editor = sharedPref.edit(); 86 editor.putLong(KEY_FAILURE_DATA_WINDOW_START_TIME, curTime.toEpochMilli()); 87 editor.putInt(KEY_FAILURE_BYTES, 0); 88 editor.apply(); 89 return true; 90 } 91 return sharedPref.getInt(KEY_FAILURE_BYTES, 0) < FAILURE_DATA_USAGE_MAX; 92 } 93 94 /** 95 * Fetches the amount of data currently consumed by calls within the current accounting window 96 * to the backend that resulted in errors and returns it. 97 * 98 * @param context the application context. 99 * @return the amount of data consumed. 100 */ getErrDataBudgetConsumed(Context context)101 public static int getErrDataBudgetConsumed(Context context) { 102 SharedPreferences sharedPref = getSharedPreferences(context); 103 return sharedPref.getInt(KEY_FAILURE_BYTES, 0); 104 } 105 106 /** 107 * Increments the counter of data currently used up in transactions with the backend server. 108 * This call will not check the current state of the rolling window, leaving that up to 109 * {@code hasDataBudget}. 110 * 111 * @param context the application context. 112 * @param bytesTransacted the number of bytes sent or received over the network. Must be a value 113 * greater than {@code 0}. 114 */ consumeErrDataBudget(Context context, int bytesTransacted)115 public static void consumeErrDataBudget(Context context, int bytesTransacted) { 116 if (bytesTransacted < 1) return; 117 SharedPreferences sharedPref = getSharedPreferences(context); 118 SharedPreferences.Editor editor = sharedPref.edit(); 119 int budgetUsed; 120 try { 121 budgetUsed = Math.addExact(sharedPref.getInt(KEY_FAILURE_BYTES, 0), bytesTransacted); 122 } catch (Exception e) { 123 Log.e(TAG, "Overflow on number of bytes sent over the network."); 124 budgetUsed = Integer.MAX_VALUE; 125 } 126 editor.putInt(KEY_FAILURE_BYTES, budgetUsed); 127 editor.apply(); 128 } 129 130 /** 131 * Generates a random ID for the use of gradual ramp up of remote provisioning. 132 */ generateAndSetId(Context context)133 public static void generateAndSetId(Context context) { 134 SharedPreferences sharedPref = getSharedPreferences(context); 135 if (sharedPref.contains(KEY_ID)) { 136 // ID is already set, don't rotate it. 137 return; 138 } 139 Log.i(TAG, "Setting ID"); 140 Random rand = new Random(); 141 SharedPreferences.Editor editor = sharedPref.edit(); 142 editor.putInt(KEY_ID, rand.nextInt(ID_UPPER_BOUND)); 143 editor.apply(); 144 } 145 146 /** 147 * Fetches the generated ID. 148 */ getId(Context context)149 public static int getId(Context context) { 150 SharedPreferences sharedPref = getSharedPreferences(context); 151 Random rand = new Random(); 152 return sharedPref.getInt(KEY_ID, rand.nextInt(ID_UPPER_BOUND) /* defaultValue */); 153 } 154 resetDefaultConfig(Context context)155 public static void resetDefaultConfig(Context context) { 156 setDeviceConfig( 157 context, 158 EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT, 159 Duration.ofMillis(EXPIRING_BY_MS_DEFAULT), 160 getDefaultUrl()); 161 clearFailureCounter(context); 162 setMaxRequestTime(context, MAX_REQUEST_TIME_MS_DEFAULT); 163 } 164 165 /** 166 * Sets the remote provisioning configuration values based on what was fetched from the server. 167 * The server is not guaranteed to have sent every available parameter in the config that 168 * was returned to the device, so the parameters should be checked for null values. 169 * 170 * @param extraKeys How many server signed remote provisioning key pairs that should be kept 171 * available in KeyStore. 172 * @param expiringBy How far in the future the app should check for expiring keys. 173 * @param url The base URL for the provisioning server. 174 * @return {@code true} if any settings were updated. 175 */ setDeviceConfig(Context context, int extraKeys, Duration expiringBy, String url)176 public static boolean setDeviceConfig(Context context, int extraKeys, 177 Duration expiringBy, String url) { 178 SharedPreferences sharedPref = getSharedPreferences(context); 179 SharedPreferences.Editor editor = sharedPref.edit(); 180 boolean wereUpdatesMade = false; 181 if (extraKeys != GeekResponse.NO_EXTRA_KEY_UPDATE 182 && sharedPref.getInt(KEY_EXTRA_KEYS, -5) != extraKeys) { 183 editor.putInt(KEY_EXTRA_KEYS, extraKeys); 184 wereUpdatesMade = true; 185 } 186 if (expiringBy != null 187 && sharedPref.getLong(KEY_EXPIRING_BY, -1) != expiringBy.toMillis()) { 188 editor.putLong(KEY_EXPIRING_BY, expiringBy.toMillis()); 189 wereUpdatesMade = true; 190 } 191 if (url != null && !sharedPref.getString(KEY_URL, "").equals(url)) { 192 editor.putString(KEY_URL, url); 193 wereUpdatesMade = true; 194 } 195 if (wereUpdatesMade) { 196 editor.apply(); 197 } 198 return wereUpdatesMade; 199 } 200 201 /** 202 * Gets the setting for how many extra keys should be kept signed and available in KeyStore. 203 */ getExtraSignedKeysAvailable(Context context)204 public static int getExtraSignedKeysAvailable(Context context) { 205 SharedPreferences sharedPref = getSharedPreferences(context); 206 return sharedPref.getInt(KEY_EXTRA_KEYS, EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT); 207 } 208 209 /** 210 * Gets the setting for how far into the future the provisioner should check for expiring keys. 211 */ getExpiringBy(Context context)212 public static Duration getExpiringBy(Context context) { 213 SharedPreferences sharedPref = getSharedPreferences(context); 214 return Duration.ofMillis(sharedPref.getLong(KEY_EXPIRING_BY, EXPIRING_BY_MS_DEFAULT)); 215 } 216 217 /** 218 * Returns an Instant which represents the point in time that the provisioner should check 219 * keys for expiration. 220 */ getExpirationTime(Context context)221 public static Instant getExpirationTime(Context context) { 222 return Instant.now().plusMillis(getExpiringBy(context).toMillis()); 223 } 224 225 /** 226 * Gets the setting for what base URL the provisioner should use to talk to provisioning 227 * servers. 228 */ getUrl(Context context)229 public static String getUrl(Context context) { 230 SharedPreferences sharedPref = getSharedPreferences(context); 231 return sharedPref.getString(KEY_URL, getDefaultUrl()); 232 } 233 234 /** 235 * Gets the system default URL for the remote provisioning server. This value is set at 236 * build time by the device maker. 237 * @return the system default, which may be overwritten in settings (see getUrl()). 238 */ getDefaultUrl()239 public static String getDefaultUrl() { 240 String hostname = SystemProperties.get("remote_provisioning.hostname"); 241 if (hostname.isEmpty()) { 242 return ""; 243 } 244 245 try { 246 return new URL("https", hostname, "v1").toExternalForm(); 247 } catch (MalformedURLException e) { 248 Log.e(TAG, "Unable to construct URL for hostname '" + hostname + "'", e); 249 return ""; 250 } 251 } 252 253 /** 254 * Increments the failure counter. This is intended to be used when reaching the server fails 255 * for any reason so that the app logic can decide if the preferences should be reset to 256 * defaults in the event that a bad push stored an incorrect URL string. 257 * 258 * @return the current failure counter after incrementing. 259 */ incrementFailureCounter(Context context)260 public static int incrementFailureCounter(Context context) { 261 SharedPreferences sharedPref = getSharedPreferences(context); 262 SharedPreferences.Editor editor = sharedPref.edit(); 263 int failures = sharedPref.getInt(KEY_FAILURE_COUNTER, 0 /* defaultValue */); 264 editor.putInt(KEY_FAILURE_COUNTER, ++failures); 265 editor.apply(); 266 return failures; 267 } 268 269 /** 270 * Gets the current failure counter. 271 */ getFailureCounter(Context context)272 public static int getFailureCounter(Context context) { 273 SharedPreferences sharedPref = getSharedPreferences(context); 274 return sharedPref.getInt(KEY_FAILURE_COUNTER, 0 /* defaultValue */); 275 } 276 277 /** 278 * Resets the failure counter to {@code 0}. 279 */ clearFailureCounter(Context context)280 public static void clearFailureCounter(Context context) { 281 SharedPreferences sharedPref = getSharedPreferences(context); 282 if (sharedPref.getInt(KEY_FAILURE_COUNTER, 0) != 0) { 283 SharedPreferences.Editor editor = sharedPref.edit(); 284 editor.putInt(KEY_FAILURE_COUNTER, 0); 285 editor.apply(); 286 } 287 } 288 289 /** 290 * Gets max request time in milliseconds. 291 */ getMaxRequestTime(Context context)292 public static int getMaxRequestTime(Context context) { 293 SharedPreferences sharedPref = getSharedPreferences(context); 294 return sharedPref.getInt(KEY_MAX_REQUEST_TIME, MAX_REQUEST_TIME_MS_DEFAULT); 295 } 296 297 /** 298 * Sets the server timeout time. 299 */ setMaxRequestTime(Context context, int timeout)300 public static void setMaxRequestTime(Context context, int timeout) { 301 SharedPreferences sharedPref = getSharedPreferences(context); 302 if (sharedPref.getInt(KEY_MAX_REQUEST_TIME, MAX_REQUEST_TIME_MS_DEFAULT) != 0) { 303 SharedPreferences.Editor editor = sharedPref.edit(); 304 editor.putInt(KEY_MAX_REQUEST_TIME, timeout); 305 editor.apply(); 306 } 307 } 308 309 /** 310 * Gets start time in milliseconds when the bad certificates were provided by server. 311 */ getLastBadCertTimeStart(Context context)312 public static Instant getLastBadCertTimeStart(Context context) { 313 SharedPreferences sharedPref = getSharedPreferences(context); 314 long lastBadCertTimeStartMillis = 315 sharedPref.getLong(KEY_LAST_BAD_CERT_TIME_START, NO_TIME_PROVIDED); 316 if (lastBadCertTimeStartMillis != -1) { 317 return InstantConverter.fromTimestamp(lastBadCertTimeStartMillis); 318 } else { 319 return null; 320 } 321 } 322 323 /** 324 * Gets end time in milliseconds when the bad certificates were provided by server. 325 */ getLastBadCertTimeEnd(Context context)326 public static Instant getLastBadCertTimeEnd(Context context) { 327 SharedPreferences sharedPref = getSharedPreferences(context); 328 long lastBadCertTimeEndMillis = 329 sharedPref.getLong(KEY_LAST_BAD_CERT_TIME_END, NO_TIME_PROVIDED); 330 if (lastBadCertTimeEndMillis != -1) { 331 return InstantConverter.fromTimestamp(lastBadCertTimeEndMillis); 332 } else { 333 return null; 334 } 335 } 336 337 /** 338 * Sets the time range for the last bad certificates. 339 */ setLastBadCertTimeRange(Context context, Instant lastBadCertTimeStart, Instant lastBadCertTimeEnd)340 public static void setLastBadCertTimeRange(Context context, Instant lastBadCertTimeStart, 341 Instant lastBadCertTimeEnd) { 342 long startMillis = lastBadCertTimeStart.toEpochMilli(); 343 long endMillis = lastBadCertTimeEnd.toEpochMilli(); 344 SharedPreferences sharedPref = getSharedPreferences(context); 345 SharedPreferences.Editor editor = sharedPref.edit(); 346 347 boolean isUpdated = false; 348 if (sharedPref.getLong(KEY_LAST_BAD_CERT_TIME_START, NO_TIME_PROVIDED) != startMillis) { 349 editor.putLong(KEY_LAST_BAD_CERT_TIME_START, startMillis); 350 isUpdated = true; 351 } 352 if (sharedPref.getLong(KEY_LAST_BAD_CERT_TIME_END, NO_TIME_PROVIDED) != endMillis) { 353 editor.putLong(KEY_LAST_BAD_CERT_TIME_END, endMillis); 354 isUpdated = true; 355 } 356 if (isUpdated) { 357 editor.apply(); 358 } 359 } 360 361 /** 362 * Clears all preferences, thus restoring the defaults. 363 */ clearPreferences(Context context)364 public static void clearPreferences(Context context) { 365 SharedPreferences sharedPref = getSharedPreferences(context); 366 SharedPreferences.Editor editor = sharedPref.edit(); 367 editor.clear(); 368 editor.apply(); 369 } 370 getSharedPreferences(Context context)371 private static SharedPreferences getSharedPreferences(Context context) { 372 if (!context.isDeviceProtectedStorage()) { 373 context = context.createDeviceProtectedStorageContext(); 374 } 375 return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); 376 } 377 } 378