Install CT log list on successful download
Add the CertificateTransparencyInstaller, that is in charge of
atomically update the log list file that is read by SCT verification in
Conscrypt.
Flag: com.android.net.ct.flags.certificate_transparency_service
Bug: 319829948
Test: atest NetworkSecurityUnitTests
Change-Id: I7c82e36820256bc0df11e44fd973b25a64d69f54
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
index 6e787d1..f35b163 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -25,6 +25,9 @@
import androidx.annotation.VisibleForTesting;
+import java.io.IOException;
+import java.io.InputStream;
+
/** Helper class to download certificate transparency log files. */
class CertificateTransparencyDownloader extends BroadcastReceiver {
@@ -33,17 +36,26 @@
private final Context mContext;
private final DataStore mDataStore;
private final DownloadHelper mDownloadHelper;
+ private final CertificateTransparencyInstaller mInstaller;
@VisibleForTesting
CertificateTransparencyDownloader(
- Context context, DataStore dataStore, DownloadHelper downloadHelper) {
+ Context context,
+ DataStore dataStore,
+ DownloadHelper downloadHelper,
+ CertificateTransparencyInstaller installer) {
mContext = context;
mDataStore = dataStore;
mDownloadHelper = downloadHelper;
+ mInstaller = installer;
}
CertificateTransparencyDownloader(Context context, DataStore dataStore) {
- this(context, dataStore, new DownloadHelper(context));
+ this(
+ context,
+ dataStore,
+ new DownloadHelper(context),
+ new CertificateTransparencyInstaller());
}
void registerReceiver() {
@@ -110,7 +122,7 @@
return;
}
- startContentDownload(mDataStore.getProperty(Config.CONTENT_URL));
+ startContentDownload(mDataStore.getProperty(Config.CONTENT_URL_PENDING));
}
private void handleContentDownloadCompleted(long downloadId) {
@@ -127,7 +139,26 @@
return;
}
- // TODO: 1. verify file signature, 2. validate file content, 3. install log file.
+ // TODO: 1. verify file signature, 2. validate file content.
+
+ String version = mDataStore.getProperty(Config.VERSION_PENDING);
+ String contentUrl = mDataStore.getProperty(Config.CONTENT_URL_PENDING);
+ String metadataUrl = mDataStore.getProperty(Config.METADATA_URL_PENDING);
+ boolean success = false;
+ try (InputStream inputStream = mContext.getContentResolver().openInputStream(contentUri)) {
+ success = mInstaller.install(inputStream, version);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not install new content", e);
+ return;
+ }
+
+ if (success) {
+ // Update information about the stored version on successful install.
+ mDataStore.setProperty(Config.VERSION, version);
+ mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
+ mDataStore.setProperty(Config.METADATA_URL, metadataUrl);
+ mDataStore.store();
+ }
}
private long download(String url) {
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
index ecf94d5..fdac434 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -85,9 +85,9 @@
return;
}
- mDataStore.setProperty(Config.VERSION, newVersion);
- mDataStore.setProperty(Config.CONTENT_URL, newContentUrl);
- mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
+ mDataStore.setProperty(Config.VERSION_PENDING, newVersion);
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, newContentUrl);
+ mDataStore.setProperty(Config.METADATA_URL_PENDING, newMetadataUrl);
mDataStore.store();
mCertificateTransparencyDownloader.startMetadataDownload(newMetadataUrl);
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
new file mode 100644
index 0000000..82dcadf
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyInstaller.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2024 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.server.net.ct;
+
+import android.annotation.SuppressLint;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+
+/** Installer of CT log lists. */
+public class CertificateTransparencyInstaller {
+
+ private static final String TAG = "CertificateTransparencyInstaller";
+ private static final String CT_DIR_NAME = "/data/misc/keychain/ct/";
+
+ static final String LOGS_DIR_PREFIX = "logs-";
+ static final String LOGS_LIST_FILE_NAME = "log_list.json";
+ static final String CURRENT_DIR_SYMLINK_NAME = "current";
+
+ private final File mCertificateTransparencyDir;
+ private final File mCurrentDirSymlink;
+
+ CertificateTransparencyInstaller(File certificateTransparencyDir) {
+ mCertificateTransparencyDir = certificateTransparencyDir;
+ mCurrentDirSymlink = new File(certificateTransparencyDir, CURRENT_DIR_SYMLINK_NAME);
+ }
+
+ CertificateTransparencyInstaller() {
+ this(new File(CT_DIR_NAME));
+ }
+
+ /**
+ * Install a new log list to use during SCT verification.
+ *
+ * @param newContent an input stream providing the log list
+ * @param version the version of the new log list
+ * @return true if the log list was installed successfully, false otherwise.
+ * @throws IOException if the list cannot be saved in the CT directory.
+ */
+ public boolean install(InputStream newContent, String version) throws IOException {
+ // To support atomically replacing the old configuration directory with the new there's a
+ // bunch of steps. We create a new directory with the logs and then do an atomic update of
+ // the current symlink to point to the new directory.
+ // 1. Ensure that the update dir exists and is readable.
+ makeDir(mCertificateTransparencyDir);
+
+ File newLogsDir = new File(mCertificateTransparencyDir, LOGS_DIR_PREFIX + version);
+ // 2. Handle the corner case where the new directory already exists.
+ if (newLogsDir.exists()) {
+ // If the symlink has already been updated then the update died between steps 6 and 7
+ // and so we cannot delete the directory since it is in use.
+ if (newLogsDir.getCanonicalPath().equals(mCurrentDirSymlink.getCanonicalPath())) {
+ deleteOldLogDirectories();
+ return false;
+ }
+ // If the symlink has not been updated then the previous installation failed and this is
+ // a re-attempt. Clean-up leftover files and try again.
+ deleteContentsAndDir(newLogsDir);
+ }
+ try {
+ // 3. Create /data/misc/keychain/ct/logs-<new_version>/ .
+ makeDir(newLogsDir);
+
+ // 4. Move the log list json file in logs-<new_version>/ .
+ File logListFile = new File(newLogsDir, LOGS_LIST_FILE_NAME);
+ if (Files.copy(newContent, logListFile.toPath()) == 0) {
+ throw new IOException("The log list appears empty");
+ }
+ setWorldReadable(logListFile);
+
+ // 5. Create temp symlink. We rename this to the target symlink to get an atomic update.
+ File tempSymlink = new File(mCertificateTransparencyDir, "new_symlink");
+ try {
+ Os.symlink(newLogsDir.getCanonicalPath(), tempSymlink.getCanonicalPath());
+ } catch (ErrnoException e) {
+ throw new IOException("Failed to create symlink", e);
+ }
+
+ // 6. Update the symlink target, this is the actual update step.
+ tempSymlink.renameTo(mCurrentDirSymlink.getAbsoluteFile());
+ } catch (IOException | RuntimeException e) {
+ deleteContentsAndDir(newLogsDir);
+ throw e;
+ }
+ Log.i(TAG, "CT log directory updated to " + newLogsDir.getAbsolutePath());
+ // 7. Cleanup
+ deleteOldLogDirectories();
+ return true;
+ }
+
+ private void makeDir(File dir) throws IOException {
+ dir.mkdir();
+ if (!dir.isDirectory()) {
+ throw new IOException("Unable to make directory " + dir.getCanonicalPath());
+ }
+ setWorldReadable(dir);
+ }
+
+ // CT files and directories are readable by all apps.
+ @SuppressLint("SetWorldReadable")
+ private void setWorldReadable(File file) throws IOException {
+ if (!file.setReadable(true, false)) {
+ throw new IOException("Failed to set " + file.getCanonicalPath() + " readable");
+ }
+ }
+
+ private void deleteOldLogDirectories() throws IOException {
+ if (!mCertificateTransparencyDir.exists()) {
+ return;
+ }
+ File currentTarget = mCurrentDirSymlink.getCanonicalFile();
+ for (File file : mCertificateTransparencyDir.listFiles()) {
+ if (!currentTarget.equals(file.getCanonicalFile())
+ && file.getName().startsWith(LOGS_DIR_PREFIX)) {
+ deleteContentsAndDir(file);
+ }
+ }
+ }
+
+ static boolean deleteContentsAndDir(File dir) {
+ if (deleteContents(dir)) {
+ return dir.delete();
+ } else {
+ return false;
+ }
+ }
+
+ private static boolean deleteContents(File dir) {
+ File[] files = dir.listFiles();
+ boolean success = true;
+ if (files != null) {
+ for (File file : files) {
+ if (file.isDirectory()) {
+ success &= deleteContents(file);
+ }
+ if (!file.delete()) {
+ Log.w(TAG, "Failed to delete " + file);
+ success = false;
+ }
+ }
+ }
+ return success;
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
index e184359..04b7dac 100644
--- a/networksecurity/service/src/com/android/server/net/ct/Config.java
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -34,9 +34,12 @@
static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
// flags and properties names
+ static final String VERSION_PENDING = "version_pending";
static final String VERSION = "version";
+ static final String CONTENT_URL_PENDING = "content_url_pending";
static final String CONTENT_URL = "content_url";
static final String CONTENT_URL_KEY = "content_url_key";
+ static final String METADATA_URL_PENDING = "metadata_url_pending";
static final String METADATA_URL = "metadata_url";
static final String METADATA_URL_KEY = "metadata_url_key";
}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
index acd0d36..5131a71 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -17,6 +17,8 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -25,6 +27,7 @@
import android.app.DownloadManager;
import android.content.Context;
import android.content.Intent;
+import android.net.Uri;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -44,6 +47,7 @@
public class CertificateTransparencyDownloaderTest {
@Mock private DownloadHelper mDownloadHelper;
+ @Mock private CertificateTransparencyInstaller mCertificateTransparencyInstaller;
private Context mContext;
private File mTempFile;
@@ -60,7 +64,8 @@
mDataStore.load();
mCertificateTransparencyDownloader =
- new CertificateTransparencyDownloader(mContext, mDataStore, mDownloadHelper);
+ new CertificateTransparencyDownloader(
+ mContext, mDataStore, mDownloadHelper, mCertificateTransparencyInstaller);
}
@After
@@ -98,7 +103,7 @@
long contentId = 666;
String contentUrl = "http://test-content.org";
- mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUrl);
when(mDownloadHelper.startDownload(contentUrl)).thenReturn(contentId);
mCertificateTransparencyDownloader.onReceive(
@@ -114,7 +119,7 @@
when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(false);
String contentUrl = "http://test-content.org";
- mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUrl);
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(metadataId));
@@ -123,19 +128,64 @@
}
@Test
- public void testDownloader_handleContentCompleteSuccessful() {
+ public void testDownloader_handleContentCompleteInstallSuccessful() throws IOException {
+ String version = "666";
+ mDataStore.setProperty(Config.VERSION_PENDING, version);
+
long metadataId = 123;
mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-metadata", "txt"));
+ mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
+ when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
long contentId = 666;
mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
+ Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
+ when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+
+ when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(true);
+
+ assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
+ assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
mCertificateTransparencyDownloader.onReceive(
mContext, makeDownloadCompleteIntent(contentId));
- verify(mDownloadHelper, times(1)).getUri(metadataId);
- verify(mDownloadHelper, times(1)).getUri(contentId);
+ verify(mCertificateTransparencyInstaller, times(1)).install(any(), eq(version));
+ assertThat(mDataStore.getProperty(Config.VERSION)).isEqualTo(version);
+ assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isEqualTo(contentUri.toString());
+ assertThat(mDataStore.getProperty(Config.METADATA_URL)).isEqualTo(metadataUri.toString());
+ }
+
+ @Test
+ public void testDownloader_handleContentCompleteInstallFails() throws IOException {
+ String version = "666";
+ mDataStore.setProperty(Config.VERSION_PENDING, version);
+
+ long metadataId = 123;
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ Uri metadataUri = Uri.fromFile(File.createTempFile("log_list-metadata", "txt"));
+ mDataStore.setProperty(Config.METADATA_URL_PENDING, metadataUri.toString());
+ when(mDownloadHelper.getUri(metadataId)).thenReturn(metadataUri);
+
+ long contentId = 666;
+ mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
+ when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
+ Uri contentUri = Uri.fromFile(File.createTempFile("log_list", "json"));
+ mDataStore.setProperty(Config.CONTENT_URL_PENDING, contentUri.toString());
+ when(mDownloadHelper.getUri(contentId)).thenReturn(contentUri);
+
+ when(mCertificateTransparencyInstaller.install(any(), eq(version))).thenReturn(false);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ assertThat(mDataStore.getProperty(Config.VERSION)).isNull();
+ assertThat(mDataStore.getProperty(Config.CONTENT_URL)).isNull();
+ assertThat(mDataStore.getProperty(Config.METADATA_URL)).isNull();
}
private Intent makeDownloadCompleteIntent(long downloadId) {
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
new file mode 100644
index 0000000..bfb8bdf
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyInstallerTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2024 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.server.net.ct;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.system.ErrnoException;
+import android.system.Os;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.ByteArrayInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/** Tests for the {@link CertificateTransparencyInstaller}. */
+@RunWith(JUnit4.class)
+public class CertificateTransparencyInstallerTest {
+
+ private File mTestDir =
+ new File(
+ InstrumentationRegistry.getInstrumentation().getContext().getFilesDir(),
+ "test-dir");
+ private File mTestSymlink =
+ new File(mTestDir, CertificateTransparencyInstaller.CURRENT_DIR_SYMLINK_NAME);
+ private CertificateTransparencyInstaller mCertificateTransparencyInstaller =
+ new CertificateTransparencyInstaller(mTestDir);
+
+ @Before
+ public void setUp() {
+ CertificateTransparencyInstaller.deleteContentsAndDir(mTestDir);
+ }
+
+ @Test
+ public void testCertificateTransparencyInstaller_installSuccessfully() throws IOException {
+ String content = "i_am_a_certificate_and_i_am_transparent";
+ String version = "666";
+ boolean success = false;
+
+ try (InputStream inputStream = asStream(content)) {
+ success = mCertificateTransparencyInstaller.install(inputStream, version);
+ }
+
+ assertThat(success).isTrue();
+ assertThat(mTestDir.exists()).isTrue();
+ assertThat(mTestDir.isDirectory()).isTrue();
+ assertThat(mTestSymlink.exists()).isTrue();
+ assertThat(mTestSymlink.isDirectory()).isTrue();
+
+ File logsDir =
+ new File(mTestDir, CertificateTransparencyInstaller.LOGS_DIR_PREFIX + version);
+ assertThat(logsDir.exists()).isTrue();
+ assertThat(logsDir.isDirectory()).isTrue();
+ assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(logsDir.getCanonicalPath());
+
+ File logsListFile = new File(logsDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+ assertThat(logsListFile.exists()).isTrue();
+ assertThat(readAsString(logsListFile)).isEqualTo(content);
+ }
+
+ @Test
+ public void testCertificateTransparencyInstaller_versionIsAlreadyInstalled()
+ throws IOException, ErrnoException {
+ String existingVersion = "666";
+ String existingContent = "i_was_already_installed_successfully";
+ File existingLogDir =
+ new File(
+ mTestDir,
+ CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion);
+ assertThat(mTestDir.mkdir()).isTrue();
+ assertThat(existingLogDir.mkdir()).isTrue();
+ Os.symlink(existingLogDir.getCanonicalPath(), mTestSymlink.getCanonicalPath());
+ File logsListFile =
+ new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+ logsListFile.createNewFile();
+ writeToFile(logsListFile, existingContent);
+ boolean success = false;
+
+ try (InputStream inputStream = asStream("i_will_be_ignored")) {
+ success = mCertificateTransparencyInstaller.install(inputStream, existingVersion);
+ }
+
+ assertThat(success).isFalse();
+ assertThat(readAsString(logsListFile)).isEqualTo(existingContent);
+ }
+
+ @Test
+ public void testCertificateTransparencyInstaller_versionInstalledFailed()
+ throws IOException, ErrnoException {
+ String existingVersion = "666";
+ String existingContent = "somebody_tried_to_install_me_but_failed_halfway_through";
+ String newContent = "i_am_the_real_certificate";
+ File existingLogDir =
+ new File(
+ mTestDir,
+ CertificateTransparencyInstaller.LOGS_DIR_PREFIX + existingVersion);
+ assertThat(mTestDir.mkdir()).isTrue();
+ assertThat(existingLogDir.mkdir()).isTrue();
+ File logsListFile =
+ new File(existingLogDir, CertificateTransparencyInstaller.LOGS_LIST_FILE_NAME);
+ logsListFile.createNewFile();
+ writeToFile(logsListFile, existingContent);
+ boolean success = false;
+
+ try (InputStream inputStream = asStream(newContent)) {
+ success = mCertificateTransparencyInstaller.install(inputStream, existingVersion);
+ }
+
+ assertThat(success).isTrue();
+ assertThat(mTestSymlink.getCanonicalPath()).isEqualTo(existingLogDir.getCanonicalPath());
+ assertThat(readAsString(logsListFile)).isEqualTo(newContent);
+ }
+
+ private static InputStream asStream(String string) throws IOException {
+ return new ByteArrayInputStream(string.getBytes());
+ }
+
+ private static String readAsString(File file) throws IOException {
+ return new String(new FileInputStream(file).readAllBytes());
+ }
+
+ private static void writeToFile(File file, String string) throws IOException {
+ try (OutputStream out = new FileOutputStream(file)) {
+ out.write(string.getBytes());
+ }
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
index d16f138..3e670d4 100644
--- a/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
@@ -37,7 +37,6 @@
public void setUp() throws IOException {
mTempFile = File.createTempFile("datastore-test", ".properties");
mDataStore = new DataStore(mTempFile);
- mDataStore.load();
}
@After