Merge "Remove metered vs unmetered network checks"
diff --git a/Android.bp b/Android.bp
index 43abc24..f67340a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -26,6 +26,9 @@
"android.system.keystore2-V1-java",
"framework-annotations-lib",
],
+ optimize: {
+ proguard_flags_files: ["proguard.flags"]
+ },
static_libs: [
"android.hardware.security.keymint-V1-java",
"android.security.remoteprovisioning-java",
diff --git a/proguard.flags b/proguard.flags
new file mode 100644
index 0000000..0182963
--- /dev/null
+++ b/proguard.flags
@@ -0,0 +1 @@
+-keep class com.android.remoteprovisioner.SettingsManager { *; }
diff --git a/src/com/android/remoteprovisioner/PeriodicProvisioner.java b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
index d9fcc79..1291376 100644
--- a/src/com/android/remoteprovisioner/PeriodicProvisioner.java
+++ b/src/com/android/remoteprovisioner/PeriodicProvisioner.java
@@ -19,7 +19,6 @@
import static java.lang.Math.min;
import android.content.Context;
-import android.net.ConnectivityManager;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.security.remoteprovisioning.AttestationPoolStatus;
@@ -46,9 +45,6 @@
// How long to wait in between key pair generations to avoid flooding keystore with requests.
private static final Duration KEY_GENERATION_PAUSE = Duration.ofMillis(1000);
- // If the connection is metered when the job service is started, try to avoid provisioning.
- private static final long METERED_CONNECTION_EXPIRATION_CHECK = Duration.ofDays(1).toMillis();
-
private static final String SERVICE = "android.security.remoteprovisioning";
private static final String TAG = "RemoteProvisioningService";
private Context mContext;
@@ -71,21 +67,6 @@
Log.e(TAG, "Binder returned null pointer to RemoteProvisioning service.");
return Result.failure();
}
-
- ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
- Context.CONNECTIVITY_SERVICE);
- boolean isMetered = cm.isActiveNetworkMetered();
- Log.i(TAG, "Connection is metered: " + isMetered);
- long expiringBy;
- if (isMetered) {
- // Check a shortened duration to attempt to avoid metered connection
- // provisioning.
- expiringBy = System.currentTimeMillis() + METERED_CONNECTION_EXPIRATION_CHECK;
- } else {
- expiringBy = SettingsManager.getExpiringBy(mContext)
- .plusMillis(System.currentTimeMillis())
- .toMillis();
- }
ImplInfo[] implInfos = binder.getImplementationInfo();
if (implInfos == null) {
Log.e(TAG, "No instances of IRemotelyProvisionedComponent registered in "
@@ -93,35 +74,27 @@
return Result.failure();
}
int[] keysNeededForSecLevel = new int[implInfos.length];
- boolean provisioningNeeded =
- isProvisioningNeeded(binder, expiringBy, implInfos, keysNeededForSecLevel);
GeekResponse resp = null;
- if (!provisioningNeeded) {
- if (!isMetered) {
- // So long as the connection is unmetered, go ahead and grab an updated
- // device configuration file.
- resp = ServerInterface.fetchGeek(mContext);
- SettingsManager.setDeviceConfig(mContext,
- resp.numExtraAttestationKeys,
- resp.timeToRefresh,
- resp.provisioningUrl);
- if (resp.numExtraAttestationKeys == 0) {
- binder.deleteAllKeys();
- }
+ if (SettingsManager.getExtraSignedKeysAvailable(mContext) == 0) {
+ // Provisioning has been purposefully disabled in the past. Go ahead and grab
+ // an EEK just to see if provisioning should resume.
+ resp = fetchGeekAndUpdate(binder);
+ if (resp.numExtraAttestationKeys == 0) {
+ return Result.success();
}
+ }
+ boolean provisioningNeeded =
+ isProvisioningNeeded(binder,
+ SettingsManager.getExpirationTime(mContext).toEpochMilli(),
+ implInfos, keysNeededForSecLevel);
+ if (!provisioningNeeded) {
return Result.success();
}
- resp = ServerInterface.fetchGeek(mContext);
- SettingsManager.setDeviceConfig(mContext,
- resp.numExtraAttestationKeys,
- resp.timeToRefresh,
- resp.provisioningUrl);
-
+ // Resp may already be populated in the extremely rare case that this job is executing
+ // to resume provisioning for the first time after a server-induced RKP shutdown. Grab
+ // a fresh response anyways to refresh the challenge.
+ resp = fetchGeekAndUpdate(binder);
if (resp.numExtraAttestationKeys == 0) {
- // Provisioning is disabled. Check with the server if it's time to turn it back
- // on. If not, quit. Avoid checking if the connection is metered. Opt instead
- // to just continue using the fallback factory provisioned key.
- binder.deleteAllKeys();
return Result.success();
}
for (int i = 0; i < implInfos.length; i++) {
@@ -142,12 +115,32 @@
Log.e(TAG, "Encountered RemoteProvisioningException", e);
if (SettingsManager.getFailureCounter(mContext) > FAILURE_MAXIMUM) {
Log.e(TAG, "Too many failures, resetting defaults.");
- SettingsManager.clearPreferences(mContext);
+ SettingsManager.resetDefaultConfig(mContext);
}
return Result.failure();
}
}
+ /**
+ * Fetch a GEEK from the server and update SettingsManager appropriately with the return
+ * values. This will also delete all keys in the attestation key pool if the server has
+ * indicated that RKP should be turned off.
+ */
+ private GeekResponse fetchGeekAndUpdate(IRemoteProvisioning binder)
+ throws RemoteException, RemoteProvisioningException {
+ GeekResponse resp = ServerInterface.fetchGeek(mContext);
+ SettingsManager.setDeviceConfig(mContext,
+ resp.numExtraAttestationKeys,
+ resp.timeToRefresh,
+ resp.provisioningUrl);
+
+ if (resp.numExtraAttestationKeys == 0) {
+ // The server has indicated that provisioning is disabled.
+ binder.deleteAllKeys();
+ }
+ return resp;
+ }
+
public static void batchProvision(IRemoteProvisioning binder, Context context,
int keysToProvision, int secLevel,
byte[] geekChain, byte[] challenge)
diff --git a/src/com/android/remoteprovisioner/ServerInterface.java b/src/com/android/remoteprovisioner/ServerInterface.java
index 697d8dd..571b559 100644
--- a/src/com/android/remoteprovisioner/ServerInterface.java
+++ b/src/com/android/remoteprovisioner/ServerInterface.java
@@ -60,6 +60,8 @@
public static List<byte[]> requestSignedCertificates(Context context, byte[] csr,
byte[] challenge) throws
RemoteProvisioningException {
+ checkDataBudget(context);
+ int bytesTransacted = 0;
try {
URL url = new URL(SettingsManager.getUrl(context) + CERTIFICATE_SIGNING_URL
+ Base64.encodeToString(challenge, Base64.URL_SAFE));
@@ -73,12 +75,14 @@
// the output stream being automatically closed.
try (OutputStream os = con.getOutputStream()) {
os.write(csr, 0, csr.length);
+ bytesTransacted += csr.length;
}
if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
int failures = SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Server connection for signing failed, response code: "
+ con.getResponseCode() + "\nRepeated failure count: " + failures);
+ SettingsManager.consumeErrDataBudget(context, bytesTransacted);
throw RemoteProvisioningException.createFromHttpError(con.getResponseCode());
}
SettingsManager.clearFailureCounter(context);
@@ -88,17 +92,17 @@
int read = 0;
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
cborBytes.write(buffer, 0, read);
+ bytesTransacted += read;
}
return CborUtils.parseSignedCertificates(cborBytes.toByteArray());
} catch (SocketTimeoutException e) {
- SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Server timed out", e);
- throw makeNetworkError(context, "Server timed out");
} catch (IOException e) {
- SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Failed to request signed certificates from the server", e);
- throw makeNetworkError(context, e.getMessage());
}
+ SettingsManager.incrementFailureCounter(context);
+ SettingsManager.consumeErrDataBudget(context, bytesTransacted);
+ throw makeNetworkError(context, "Error getting CSR signed.");
}
/**
@@ -114,6 +118,8 @@
* @return A GeekResponse object which optionally contains configuration data.
*/
public static GeekResponse fetchGeek(Context context) throws RemoteProvisioningException {
+ checkDataBudget(context);
+ int bytesTransacted = 0;
try {
URL url = new URL(SettingsManager.getUrl(context) + GEEK_URL);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
@@ -125,12 +131,14 @@
byte[] config = CborUtils.buildProvisioningInfo(context);
try (OutputStream os = con.getOutputStream()) {
os.write(config, 0, config.length);
+ bytesTransacted += config.length;
}
if (con.getResponseCode() != HttpURLConnection.HTTP_OK) {
int failures = SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Server connection for GEEK failed, response code: "
+ con.getResponseCode() + "\nRepeated failure count: " + failures);
+ SettingsManager.consumeErrDataBudget(context, bytesTransacted);
throw RemoteProvisioningException.createFromHttpError(con.getResponseCode());
}
SettingsManager.clearFailureCounter(context);
@@ -141,20 +149,36 @@
int read = 0;
while ((read = inputStream.read(buffer, 0, buffer.length)) != -1) {
cborBytes.write(buffer, 0, read);
+ bytesTransacted += read;
}
inputStream.close();
- return CborUtils.parseGeekResponse(cborBytes.toByteArray());
+ GeekResponse resp = CborUtils.parseGeekResponse(cborBytes.toByteArray());
+ if (resp == null) {
+ throw new RemoteProvisioningException(
+ IGenerateRkpKeyService.Status.HTTP_SERVER_ERROR,
+ "Response failed to parse.");
+ }
+ return resp;
} catch (SocketTimeoutException e) {
- SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Server timed out", e);
} catch (IOException e) {
// This exception will trigger on a completely malformed URL.
- SettingsManager.incrementFailureCounter(context);
Log.e(TAG, "Failed to fetch GEEK from the servers.", e);
}
+ SettingsManager.incrementFailureCounter(context);
+ SettingsManager.consumeErrDataBudget(context, bytesTransacted);
throw makeNetworkError(context, "Error fetching GEEK");
}
+ private static void checkDataBudget(Context context) throws RemoteProvisioningException {
+ if (!SettingsManager.hasErrDataBudget(context, null /* curTime */)) {
+ int bytesConsumed = SettingsManager.getErrDataBudgetConsumed(context);
+ throw makeNetworkError(context,
+ "Out of data budget due to repeated errors. Consumed "
+ + bytesConsumed + " bytes.");
+ }
+ }
+
private static RemoteProvisioningException makeNetworkError(Context context, String message) {
ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
NetworkInfo networkInfo = cm.getActiveNetworkInfo();
diff --git a/src/com/android/remoteprovisioner/SettingsManager.java b/src/com/android/remoteprovisioner/SettingsManager.java
index fbf4261..ad6e6ac 100644
--- a/src/com/android/remoteprovisioner/SettingsManager.java
+++ b/src/com/android/remoteprovisioner/SettingsManager.java
@@ -21,6 +21,7 @@
import android.util.Log;
import java.time.Duration;
+import java.time.Instant;
import java.util.Random;
/**
@@ -36,16 +37,90 @@
public static final int EXPIRING_BY_MS_DEFAULT = 1000 * 60 * 60 * 24 * 3;
public static final String URL_DEFAULT = "https://remoteprovisioning.googleapis.com/v1";
public static final boolean IS_TEST_MODE = false;
+ // 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);
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 PREFERENCES_NAME = "com.android.remoteprovisioner.preferences";
private static final String TAG = "RemoteProvisionerSettings";
/**
+ * Determines whether or not 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 whether or not the data budget has been exceeded.
+ */
+ public static boolean hasErrDataBudget(Context context, Instant curTime) {
+ if (curTime == null) {
+ curTime = Instant.now();
+ }
+ SharedPreferences sharedPref =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ 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 =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ 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 =
+ context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
+ SharedPreferences.Editor editor = sharedPref.edit();
+ int budgetUsed = 0;
+ 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) {
@@ -72,6 +147,15 @@
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),
+ URL_DEFAULT);
+ clearFailureCounter(context);
+ }
+
/**
* 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
@@ -128,6 +212,14 @@
}
/**
+ * 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.
*/
diff --git a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
index 6d8f48a..f4475ca 100644
--- a/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
+++ b/src/com/android/remoteprovisioner/service/GenerateRkpKeyService.java
@@ -32,8 +32,8 @@
import com.android.remoteprovisioner.PeriodicProvisioner;
import com.android.remoteprovisioner.RemoteProvisioningException;
import com.android.remoteprovisioner.ServerInterface;
+import com.android.remoteprovisioner.SettingsManager;
-import java.time.Duration;
import java.util.concurrent.locks.ReentrantLock;
/**
@@ -41,7 +41,6 @@
*/
public class GenerateRkpKeyService extends Service {
private static final int KEY_GENERATION_PAUSE_MS = 1000;
- private static final Duration LOOKAHEAD_TIME = Duration.ofDays(1);
private static final String SERVICE = "android.security.remoteprovisioning";
private static final String TAG = "RemoteProvisioningService";
@@ -104,8 +103,10 @@
Context context = getApplicationContext();
int keysToProvision =
- PeriodicProvisioner.generateNumKeysNeeded(binder, context,
- LOOKAHEAD_TIME.toMillis(),
+ PeriodicProvisioner.generateNumKeysNeeded(
+ binder,
+ context,
+ SettingsManager.getExpirationTime(context).toEpochMilli(),
secLevel);
if (keysToProvision != 0) {
Log.i(TAG, "All signed keys are currently in use, provisioning more.");
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
index 42281f9..975370c 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/ServerToSystemTest.java
@@ -25,6 +25,7 @@
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
import android.Manifest;
import android.content.Context;
@@ -49,6 +50,7 @@
import com.android.bedstead.nene.permissions.PermissionContext;
import com.android.remoteprovisioner.GeekResponse;
import com.android.remoteprovisioner.Provisioner;
+import com.android.remoteprovisioner.RemoteProvisioningException;
import com.android.remoteprovisioner.ServerInterface;
import com.android.remoteprovisioner.SettingsManager;
@@ -187,11 +189,13 @@
sBinder.generateKeyPair(IS_TEST_MODE, TRUSTED_ENVIRONMENT);
assertPoolStatus(numTestKeys, 0, 0, 0, mDuration);
GeekResponse geek = ServerInterface.fetchGeek(sContext);
+ assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
assertNotNull(geek);
int numProvisioned =
Provisioner.provisionCerts(numTestKeys, TRUSTED_ENVIRONMENT,
geek.getGeekChain(sCurve), geek.getChallenge(), sBinder,
sContext);
+ assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
assertEquals(numTestKeys, numProvisioned);
assertPoolStatus(numTestKeys, numTestKeys, numTestKeys, 0, mDuration);
// Certificate duration sent back from the server may change, however ~6 months should be
@@ -245,6 +249,32 @@
}
@Test
+ public void testDataBudgetEmptyFetchGeek() throws Exception {
+ // Check the data budget in order to initialize a rolling window.
+ assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
+ SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX);
+ try {
+ ServerInterface.fetchGeek(sContext);
+ fail("Network transaction should not have proceeded.");
+ } catch (RemoteProvisioningException e) {
+ return;
+ }
+ }
+
+ @Test
+ public void testDataBudgetEmptySignCerts() throws Exception {
+ // Check the data budget in order to initialize a rolling window.
+ assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
+ SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX);
+ try {
+ ServerInterface.requestSignedCertificates(sContext, null, null);
+ fail("Network transaction should not have proceeded.");
+ } catch (RemoteProvisioningException e) {
+ return;
+ }
+ }
+
+ @Test
public void testRetryableRkpError() throws Exception {
try (ForceRkpOnlyContext c = new ForceRkpOnlyContext()) {
SettingsManager.setDeviceConfig(sContext, 1 /* extraKeys */, mDuration /* expiringBy */,
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java
index 7db20a9..988d557 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/SettingsManagerTest.java
@@ -17,6 +17,7 @@
package com.android.remoteprovisioner.unittest;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import android.content.Context;
@@ -33,6 +34,7 @@
import org.junit.runner.RunWith;
import java.time.Duration;
+import java.time.Instant;
@RunWith(AndroidJUnit4.class)
public class SettingsManagerTest {
@@ -80,6 +82,24 @@
}
@Test
+ public void testResetDefaults() throws Exception {
+ int extraKeys = 12;
+ Duration expiringBy = Duration.ofMillis(1000);
+ String url = "https://www.remoteprovisionalot";
+ assertTrue("Method did not return true on write.",
+ SettingsManager.setDeviceConfig(sContext, extraKeys, expiringBy, url));
+ SettingsManager.incrementFailureCounter(sContext);
+ SettingsManager.resetDefaultConfig(sContext);
+ assertEquals(SettingsManager.EXTRA_SIGNED_KEYS_AVAILABLE_DEFAULT,
+ SettingsManager.getExtraSignedKeysAvailable(sContext));
+ assertEquals(SettingsManager.EXPIRING_BY_MS_DEFAULT,
+ SettingsManager.getExpiringBy(sContext).toMillis());
+ assertEquals(SettingsManager.URL_DEFAULT,
+ SettingsManager.getUrl(sContext));
+ assertEquals(0, SettingsManager.getFailureCounter(sContext));
+ }
+
+ @Test
public void testSetDeviceConfig() {
int extraKeys = 12;
Duration expiringBy = Duration.ofMillis(1000);
@@ -92,6 +112,14 @@
}
@Test
+ public void testGetExpirationTime() {
+ long expiringBy = SettingsManager.getExpiringBy(sContext).toMillis();
+ long timeDif = SettingsManager.getExpirationTime(sContext).toEpochMilli()
+ - (expiringBy + System.currentTimeMillis());
+ assertTrue(Math.abs(timeDif) < 1000);
+ }
+
+ @Test
public void testFailureCounter() {
assertEquals(1, SettingsManager.incrementFailureCounter(sContext));
assertEquals(1, SettingsManager.getFailureCounter(sContext));
@@ -103,4 +131,62 @@
SettingsManager.incrementFailureCounter(sContext);
assertEquals(1, SettingsManager.getFailureCounter(sContext));
}
+
+ @Test
+ public void testDataBudgetUnused() {
+ assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
+ }
+
+ @Test
+ public void testDataBudgetIncrement() {
+ int[] bytesUsed = new int[]{1, 40, 100};
+ assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
+
+ SettingsManager.consumeErrDataBudget(sContext, bytesUsed[0]);
+ assertEquals(bytesUsed[0], SettingsManager.getErrDataBudgetConsumed(sContext));
+
+ SettingsManager.consumeErrDataBudget(sContext, bytesUsed[1]);
+ assertEquals(bytesUsed[0] + bytesUsed[1],
+ SettingsManager.getErrDataBudgetConsumed(sContext));
+
+ SettingsManager.consumeErrDataBudget(sContext, bytesUsed[2]);
+ assertEquals(bytesUsed[0] + bytesUsed[1] + bytesUsed[2],
+ SettingsManager.getErrDataBudgetConsumed(sContext));
+ }
+
+ @Test
+ public void testDataBudgetInvalidIncrement() {
+ assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
+ SettingsManager.consumeErrDataBudget(sContext, -20);
+ assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
+ SettingsManager.consumeErrDataBudget(sContext, 40);
+ SettingsManager.consumeErrDataBudget(sContext, -400);
+ SettingsManager.consumeErrDataBudget(sContext, 60);
+ assertEquals(100, SettingsManager.getErrDataBudgetConsumed(sContext));
+ }
+
+ @Test
+ public void testDataBudgetReset() {
+ // The first call to hasErrDataBudget will set the start of the bucket.
+ assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
+
+ SettingsManager.consumeErrDataBudget(sContext, 100);
+ assertTrue(SettingsManager.hasErrDataBudget(sContext, null));
+ assertEquals(100, SettingsManager.getErrDataBudgetConsumed(sContext));
+
+ assertTrue(SettingsManager.hasErrDataBudget(sContext,
+ Instant.now().plusMillis(SettingsManager.FAILURE_DATA_USAGE_WINDOW.toMillis()
+ + 20)));
+ assertEquals(0, SettingsManager.getErrDataBudgetConsumed(sContext));
+ }
+
+ @Test
+ public void testDataBudgetExceeded() {
+ // The first call to hasErrDataBudget will set the start of the bucket.
+ assertTrue(SettingsManager.hasErrDataBudget(sContext, null /* curTime */));
+ SettingsManager.consumeErrDataBudget(sContext, SettingsManager.FAILURE_DATA_USAGE_MAX - 1);
+ assertTrue(SettingsManager.hasErrDataBudget(sContext, null));
+ SettingsManager.consumeErrDataBudget(sContext, 1);
+ assertFalse(SettingsManager.hasErrDataBudget(sContext, null));
+ }
}
diff --git a/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java b/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
index ec35a0c..cb664c8 100644
--- a/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
+++ b/tests/unittests/src/com/android/remoteprovisioner/unittest/SystemInterfaceTest.java
@@ -69,6 +69,8 @@
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
+import java.time.Duration;
+import java.time.Instant;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
@@ -197,10 +199,11 @@
for (int j = 0; j < certChain[i].length; j++) {
os.write(certChain[i][j].getEncoded());
}
+ Instant expiringBy = Instant.now().plusMillis(Duration.ofDays(4).toMillis());
SystemInterface.provisionCertChain(X509Utils.getAndFormatRawPublicKey(certChain[i][0]),
certChain[i][0].getEncoded() /* leafCert */,
os.toByteArray() /* certChain */,
- System.currentTimeMillis() + 25000 /* validity */,
+ expiringBy.toEpochMilli() /* validity */,
SecurityLevel.TRUSTED_ENVIRONMENT,
mBinder);
}