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