• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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