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