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