/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.rkpdapp.utils; import android.content.Context; import android.content.SharedPreferences; import android.os.SystemProperties; import android.util.Log; import com.android.rkpdapp.GeekResponse; import com.android.rkpdapp.database.InstantConverter; import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; import java.time.Instant; import java.util.Random; /** * Settings makes use of SharedPreferences in order to store key/value pairs related to * configuration settings that can be retrieved from the server. In the event that none * have yet been retrieved, or for some reason a reset has occurred, there are * reasonable default values. */ public class Settings { public static final int ID_UPPER_BOUND = 1000000; public static final int EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT = 6; // Check for expiring certs in the next 3 days public static final int EXPIRING_BY_MS_DEFAULT = 1000 * 60 * 60 * 24 * 3; // Limit data consumption from failures within a window of time to 1 MB. public static final int FAILURE_DATA_USAGE_MAX = 1024 * 1024; public static final Duration FAILURE_DATA_USAGE_WINDOW = Duration.ofDays(1); public static final int MAX_REQUEST_TIME_MS_DEFAULT = 20000; public static final int NO_TIME_PROVIDED = -1; private static final String KEY_EXPIRING_BY = "expiring_by"; private static final String KEY_EXTRA_KEYS = "extra_keys"; private static final String KEY_ID = "settings_id"; private static final String KEY_FAILURE_DATA_WINDOW_START_TIME = "failure_start_time"; private static final String KEY_FAILURE_COUNTER = "failure_counter"; private static final String KEY_FAILURE_BYTES = "failure_data"; private static final String KEY_URL = "url"; private static final String KEY_MAX_REQUEST_TIME = "max_request_time"; private static final String KEY_LAST_BAD_CERT_TIME_START = "bad_cert_time_start"; private static final String KEY_LAST_BAD_CERT_TIME_END = "bad_cert_time_end"; private static final String PREFERENCES_NAME = "com.android.rkpdapp.utils.preferences"; private static final String TAG = "RkpdSettings"; /** * Determines if there is enough data budget remaining to attempt provisioning. * If {@code FAILURE_DATA_USAGE_MAX} bytes have already been used up in previous calls that * resulted in errors, then false will be returned. *
* Additionally, the rolling window of data usage is managed within this call. The used data * budget will be reset if a time greater than {@code FAILURE_DATA_USAGE_WINDOW} has passed. * * @param context The application context * @param curTime An instant representing the current time to measure the window against. If * null, then the code will use {@code Instant.now()} instead. * @return if the data budget has been exceeded. */ public static boolean hasErrDataBudget(Context context, Instant curTime) { if (curTime == null) { curTime = Instant.now(); } SharedPreferences sharedPref = getSharedPreferences(context); Instant logged = Instant.ofEpochMilli(sharedPref.getLong(KEY_FAILURE_DATA_WINDOW_START_TIME, 0)); if (Duration.between(logged, curTime).compareTo(FAILURE_DATA_USAGE_WINDOW) > 0) { SharedPreferences.Editor editor = sharedPref.edit(); editor.putLong(KEY_FAILURE_DATA_WINDOW_START_TIME, curTime.toEpochMilli()); editor.putInt(KEY_FAILURE_BYTES, 0); editor.apply(); return true; } return sharedPref.getInt(KEY_FAILURE_BYTES, 0) < FAILURE_DATA_USAGE_MAX; } /** * Fetches the amount of data currently consumed by calls within the current accounting window * to the backend that resulted in errors and returns it. * * @param context the application context. * @return the amount of data consumed. */ public static int getErrDataBudgetConsumed(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); return sharedPref.getInt(KEY_FAILURE_BYTES, 0); } /** * Increments the counter of data currently used up in transactions with the backend server. * This call will not check the current state of the rolling window, leaving that up to * {@code hasDataBudget}. * * @param context the application context. * @param bytesTransacted the number of bytes sent or received over the network. Must be a value * greater than {@code 0}. */ public static void consumeErrDataBudget(Context context, int bytesTransacted) { if (bytesTransacted < 1) return; SharedPreferences sharedPref = getSharedPreferences(context); SharedPreferences.Editor editor = sharedPref.edit(); int budgetUsed; try { budgetUsed = Math.addExact(sharedPref.getInt(KEY_FAILURE_BYTES, 0), bytesTransacted); } catch (Exception e) { Log.e(TAG, "Overflow on number of bytes sent over the network."); budgetUsed = Integer.MAX_VALUE; } editor.putInt(KEY_FAILURE_BYTES, budgetUsed); editor.apply(); } /** * Generates a random ID for the use of gradual ramp up of remote provisioning. */ public static void generateAndSetId(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); if (sharedPref.contains(KEY_ID)) { // ID is already set, don't rotate it. return; } Log.i(TAG, "Setting ID"); Random rand = new Random(); SharedPreferences.Editor editor = sharedPref.edit(); editor.putInt(KEY_ID, rand.nextInt(ID_UPPER_BOUND)); editor.apply(); } /** * Fetches the generated ID. */ public static int getId(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); Random rand = new Random(); return sharedPref.getInt(KEY_ID, rand.nextInt(ID_UPPER_BOUND) /* defaultValue */); } public static void resetDefaultConfig(Context context) { setDeviceConfig( context, EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT, Duration.ofMillis(EXPIRING_BY_MS_DEFAULT), getDefaultUrl()); clearFailureCounter(context); setMaxRequestTime(context, MAX_REQUEST_TIME_MS_DEFAULT); } /** * Sets the remote provisioning configuration values based on what was fetched from the server. * The server is not guaranteed to have sent every available parameter in the config that * was returned to the device, so the parameters should be checked for null values. * * @param extraKeys How many server signed remote provisioning key pairs that should be kept * available in KeyStore. * @param expiringBy How far in the future the app should check for expiring keys. * @param url The base URL for the provisioning server. * @return {@code true} if any settings were updated. */ public static boolean setDeviceConfig(Context context, int extraKeys, Duration expiringBy, String url) { SharedPreferences sharedPref = getSharedPreferences(context); SharedPreferences.Editor editor = sharedPref.edit(); boolean wereUpdatesMade = false; if (extraKeys != GeekResponse.NO_EXTRA_KEY_UPDATE && sharedPref.getInt(KEY_EXTRA_KEYS, -5) != extraKeys) { editor.putInt(KEY_EXTRA_KEYS, extraKeys); wereUpdatesMade = true; } if (expiringBy != null && sharedPref.getLong(KEY_EXPIRING_BY, -1) != expiringBy.toMillis()) { editor.putLong(KEY_EXPIRING_BY, expiringBy.toMillis()); wereUpdatesMade = true; } if (url != null && !sharedPref.getString(KEY_URL, "").equals(url)) { editor.putString(KEY_URL, url); wereUpdatesMade = true; } if (wereUpdatesMade) { editor.apply(); } return wereUpdatesMade; } /** * Gets the setting for how many extra keys should be kept signed and available in KeyStore. */ public static int getExtraSignedKeysAvailable(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); return sharedPref.getInt(KEY_EXTRA_KEYS, EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT); } /** * Gets the setting for how far into the future the provisioner should check for expiring keys. */ public static Duration getExpiringBy(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); return Duration.ofMillis(sharedPref.getLong(KEY_EXPIRING_BY, EXPIRING_BY_MS_DEFAULT)); } /** * Returns an Instant which represents the point in time that the provisioner should check * keys for expiration. */ public static Instant getExpirationTime(Context context) { return Instant.now().plusMillis(getExpiringBy(context).toMillis()); } /** * Gets the setting for what base URL the provisioner should use to talk to provisioning * servers. */ public static String getUrl(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); return sharedPref.getString(KEY_URL, getDefaultUrl()); } /** * Gets the system default URL for the remote provisioning server. This value is set at * build time by the device maker. * @return the system default, which may be overwritten in settings (see getUrl()). */ public static String getDefaultUrl() { String hostname = SystemProperties.get("remote_provisioning.hostname"); if (hostname.isEmpty()) { return ""; } try { return new URL("https", hostname, "v1").toExternalForm(); } catch (MalformedURLException e) { Log.e(TAG, "Unable to construct URL for hostname '" + hostname + "'", e); return ""; } } /** * Increments the failure counter. This is intended to be used when reaching the server fails * for any reason so that the app logic can decide if the preferences should be reset to * defaults in the event that a bad push stored an incorrect URL string. * * @return the current failure counter after incrementing. */ public static int incrementFailureCounter(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); SharedPreferences.Editor editor = sharedPref.edit(); int failures = sharedPref.getInt(KEY_FAILURE_COUNTER, 0 /* defaultValue */); editor.putInt(KEY_FAILURE_COUNTER, ++failures); editor.apply(); return failures; } /** * Gets the current failure counter. */ public static int getFailureCounter(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); return sharedPref.getInt(KEY_FAILURE_COUNTER, 0 /* defaultValue */); } /** * Resets the failure counter to {@code 0}. */ public static void clearFailureCounter(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); if (sharedPref.getInt(KEY_FAILURE_COUNTER, 0) != 0) { SharedPreferences.Editor editor = sharedPref.edit(); editor.putInt(KEY_FAILURE_COUNTER, 0); editor.apply(); } } /** * Gets max request time in milliseconds. */ public static int getMaxRequestTime(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); return sharedPref.getInt(KEY_MAX_REQUEST_TIME, MAX_REQUEST_TIME_MS_DEFAULT); } /** * Sets the server timeout time. */ public static void setMaxRequestTime(Context context, int timeout) { SharedPreferences sharedPref = getSharedPreferences(context); if (sharedPref.getInt(KEY_MAX_REQUEST_TIME, MAX_REQUEST_TIME_MS_DEFAULT) != 0) { SharedPreferences.Editor editor = sharedPref.edit(); editor.putInt(KEY_MAX_REQUEST_TIME, timeout); editor.apply(); } } /** * Gets start time in milliseconds when the bad certificates were provided by server. */ public static Instant getLastBadCertTimeStart(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); long lastBadCertTimeStartMillis = sharedPref.getLong(KEY_LAST_BAD_CERT_TIME_START, NO_TIME_PROVIDED); if (lastBadCertTimeStartMillis != -1) { return InstantConverter.fromTimestamp(lastBadCertTimeStartMillis); } else { return null; } } /** * Gets end time in milliseconds when the bad certificates were provided by server. */ public static Instant getLastBadCertTimeEnd(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); long lastBadCertTimeEndMillis = sharedPref.getLong(KEY_LAST_BAD_CERT_TIME_END, NO_TIME_PROVIDED); if (lastBadCertTimeEndMillis != -1) { return InstantConverter.fromTimestamp(lastBadCertTimeEndMillis); } else { return null; } } /** * Sets the time range for the last bad certificates. */ public static void setLastBadCertTimeRange(Context context, Instant lastBadCertTimeStart, Instant lastBadCertTimeEnd) { long startMillis = lastBadCertTimeStart.toEpochMilli(); long endMillis = lastBadCertTimeEnd.toEpochMilli(); SharedPreferences sharedPref = getSharedPreferences(context); SharedPreferences.Editor editor = sharedPref.edit(); boolean isUpdated = false; if (sharedPref.getLong(KEY_LAST_BAD_CERT_TIME_START, NO_TIME_PROVIDED) != startMillis) { editor.putLong(KEY_LAST_BAD_CERT_TIME_START, startMillis); isUpdated = true; } if (sharedPref.getLong(KEY_LAST_BAD_CERT_TIME_END, NO_TIME_PROVIDED) != endMillis) { editor.putLong(KEY_LAST_BAD_CERT_TIME_END, endMillis); isUpdated = true; } if (isUpdated) { editor.apply(); } } /** * Clears all preferences, thus restoring the defaults. */ public static void clearPreferences(Context context) { SharedPreferences sharedPref = getSharedPreferences(context); SharedPreferences.Editor editor = sharedPref.edit(); editor.clear(); editor.apply(); } private static SharedPreferences getSharedPreferences(Context context) { if (!context.isDeviceProtectedStorage()) { context = context.createDeviceProtectedStorageContext(); } return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE); } }