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