Schedule log list download on flag update
Add the CertificateTransparencyDownloader to download the data and
metadata needed by Certificate Transparency.
The download is triggered when either the content URL, the metadata URL,
or the log list flags change.
The version numbers and URLs are stored in an on-disk DataStore to make
sure the same files are not downloaded on repeated flag updates.
Flag: com.android.ct.flags.certificate_transparency_service
Bug: 319829948
Test: atest NetworkSecurityUnitTests
Change-Id: Ic30e2973fdce99ea861d13f7777128161dd4003e
diff --git a/networksecurity/TEST_MAPPING b/networksecurity/TEST_MAPPING
new file mode 100644
index 0000000..20ecbce
--- /dev/null
+++ b/networksecurity/TEST_MAPPING
@@ -0,0 +1,7 @@
+{
+ "postsubmit": [
+ {
+ "name": "NetworkSecurityUnitTests"
+ }
+ ]
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
new file mode 100644
index 0000000..6e787d1
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyDownloader.java
@@ -0,0 +1,159 @@
+/*
+ * 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.app.DownloadManager;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.Uri;
+import android.util.Log;
+
+import androidx.annotation.VisibleForTesting;
+
+/** Helper class to download certificate transparency log files. */
+class CertificateTransparencyDownloader extends BroadcastReceiver {
+
+ private static final String TAG = "CertificateTransparencyDownloader";
+
+ private final Context mContext;
+ private final DataStore mDataStore;
+ private final DownloadHelper mDownloadHelper;
+
+ @VisibleForTesting
+ CertificateTransparencyDownloader(
+ Context context, DataStore dataStore, DownloadHelper downloadHelper) {
+ mContext = context;
+ mDataStore = dataStore;
+ mDownloadHelper = downloadHelper;
+ }
+
+ CertificateTransparencyDownloader(Context context, DataStore dataStore) {
+ this(context, dataStore, new DownloadHelper(context));
+ }
+
+ void registerReceiver() {
+ IntentFilter intentFilter = new IntentFilter();
+ intentFilter.addAction(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
+ mContext.registerReceiver(this, intentFilter);
+
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyDownloader initialized successfully");
+ }
+ }
+
+ void startMetadataDownload(String metadataUrl) {
+ long downloadId = download(metadataUrl);
+ if (downloadId == -1) {
+ Log.e(TAG, "Metadata download request failed for " + metadataUrl);
+ return;
+ }
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, downloadId);
+ mDataStore.store();
+ }
+
+ void startContentDownload(String contentUrl) {
+ long downloadId = download(contentUrl);
+ if (downloadId == -1) {
+ Log.e(TAG, "Content download request failed for " + contentUrl);
+ return;
+ }
+ mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, downloadId);
+ mDataStore.store();
+ }
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (!DownloadManager.ACTION_DOWNLOAD_COMPLETE.equals(action)) {
+ Log.w(TAG, "Received unexpected broadcast with action " + action);
+ return;
+ }
+
+ long completedId = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1);
+ if (completedId == -1) {
+ Log.e(TAG, "Invalid completed download Id");
+ return;
+ }
+
+ if (isMetadataDownloadId(completedId)) {
+ handleMetadataDownloadCompleted(completedId);
+ return;
+ }
+
+ if (isContentDownloadId(completedId)) {
+ handleContentDownloadCompleted(completedId);
+ return;
+ }
+
+ Log.e(TAG, "Download id " + completedId + " is neither metadata nor content.");
+ }
+
+ private void handleMetadataDownloadCompleted(long downloadId) {
+ if (!mDownloadHelper.isSuccessful(downloadId)) {
+ Log.w(TAG, "Metadata download failed.");
+ // TODO: re-attempt download
+ return;
+ }
+
+ startContentDownload(mDataStore.getProperty(Config.CONTENT_URL));
+ }
+
+ private void handleContentDownloadCompleted(long downloadId) {
+ if (!mDownloadHelper.isSuccessful(downloadId)) {
+ Log.w(TAG, "Content download failed.");
+ // TODO: re-attempt download
+ return;
+ }
+
+ Uri contentUri = getContentDownloadUri();
+ Uri metadataUri = getMetadataDownloadUri();
+ if (contentUri == null || metadataUri == null) {
+ Log.e(TAG, "Invalid URIs");
+ return;
+ }
+
+ // TODO: 1. verify file signature, 2. validate file content, 3. install log file.
+ }
+
+ private long download(String url) {
+ try {
+ return mDownloadHelper.startDownload(url);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "Download request failed", e);
+ return -1;
+ }
+ }
+
+ @VisibleForTesting
+ boolean isMetadataDownloadId(long downloadId) {
+ return mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1) == downloadId;
+ }
+
+ @VisibleForTesting
+ boolean isContentDownloadId(long downloadId) {
+ return mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1) == downloadId;
+ }
+
+ private Uri getMetadataDownloadUri() {
+ return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.METADATA_URL_KEY, -1));
+ }
+
+ private Uri getContentDownloadUri() {
+ return mDownloadHelper.getUri(mDataStore.getPropertyLong(Config.CONTENT_URL_KEY, -1));
+ }
+}
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 8dd5951..ecf94d5 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyFlagsListener.java
@@ -30,17 +30,25 @@
/** Listener class for the Certificate Transparency Phenotype flags. */
class CertificateTransparencyFlagsListener implements DeviceConfig.OnPropertiesChangedListener {
- private static final String TAG = "CertificateTransparency";
+ private static final String TAG = "CertificateTransparencyFlagsListener";
- private static final String VERSION = "version";
- private static final String CONTENT_URL = "content_url";
- private static final String METADATA_URL = "metadata_url";
+ private final DataStore mDataStore;
+ private final CertificateTransparencyDownloader mCertificateTransparencyDownloader;
- CertificateTransparencyFlagsListener(Context context) {}
+ CertificateTransparencyFlagsListener(Context context) {
+ mDataStore = new DataStore(Config.PREFERENCES_FILE);
+ mCertificateTransparencyDownloader =
+ new CertificateTransparencyDownloader(context, mDataStore);
+ }
void initialize() {
+ mDataStore.load();
+ mCertificateTransparencyDownloader.registerReceiver();
DeviceConfig.addOnPropertiesChangedListener(
NAMESPACE_TETHERING, Executors.newSingleThreadExecutor(), this);
+ if (Config.DEBUG) {
+ Log.d(TAG, "CertificateTransparencyFlagsListener initialized successfully");
+ }
// TODO: handle property changes triggering on boot before registering this listener.
}
@@ -50,18 +58,38 @@
return;
}
- String newVersion = DeviceConfig.getString(NAMESPACE_TETHERING, VERSION, "");
- String newContentUrl = DeviceConfig.getString(NAMESPACE_TETHERING, CONTENT_URL, "");
- String newMetadataUrl = DeviceConfig.getString(NAMESPACE_TETHERING, METADATA_URL, "");
+ String newVersion = DeviceConfig.getString(NAMESPACE_TETHERING, Config.VERSION, "");
+ String newContentUrl = DeviceConfig.getString(NAMESPACE_TETHERING, Config.CONTENT_URL, "");
+ String newMetadataUrl =
+ DeviceConfig.getString(NAMESPACE_TETHERING, Config.METADATA_URL, "");
if (TextUtils.isEmpty(newVersion)
|| TextUtils.isEmpty(newContentUrl)
|| TextUtils.isEmpty(newMetadataUrl)) {
return;
}
- Log.d(TAG, "newVersion=" + newVersion);
- Log.d(TAG, "newContentUrl=" + newContentUrl);
- Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
- // TODO: start download of URLs.
+ if (Config.DEBUG) {
+ Log.d(TAG, "newVersion=" + newVersion);
+ Log.d(TAG, "newContentUrl=" + newContentUrl);
+ Log.d(TAG, "newMetadataUrl=" + newMetadataUrl);
+ }
+
+ String oldVersion = mDataStore.getProperty(Config.VERSION);
+ String oldContentUrl = mDataStore.getProperty(Config.CONTENT_URL);
+ String oldMetadataUrl = mDataStore.getProperty(Config.METADATA_URL);
+
+ if (TextUtils.equals(newVersion, oldVersion)
+ && TextUtils.equals(newContentUrl, oldContentUrl)
+ && TextUtils.equals(newMetadataUrl, oldMetadataUrl)) {
+ Log.i(TAG, "No flag changed, ignoring update");
+ return;
+ }
+
+ mDataStore.setProperty(Config.VERSION, newVersion);
+ mDataStore.setProperty(Config.CONTENT_URL, newContentUrl);
+ mDataStore.setProperty(Config.METADATA_URL, newMetadataUrl);
+ mDataStore.store();
+
+ mCertificateTransparencyDownloader.startMetadataDownload(newMetadataUrl);
}
}
diff --git a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
index 406a57f..52478c0 100644
--- a/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
+++ b/networksecurity/service/src/com/android/server/net/ct/CertificateTransparencyService.java
@@ -19,7 +19,6 @@
import android.content.Context;
import android.net.ct.ICertificateTransparencyManager;
import android.os.Build;
-import android.util.Log;
import com.android.net.ct.flags.Flags;
import com.android.net.module.util.DeviceConfigUtils;
@@ -29,7 +28,6 @@
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
public class CertificateTransparencyService extends ICertificateTransparencyManager.Stub {
- private static final String TAG = "CertificateTransparency";
private static final String CERTIFICATE_TRANSPARENCY_ENABLED =
"certificate_transparency_service_enabled";
@@ -59,7 +57,6 @@
switch (phase) {
case SystemService.PHASE_BOOT_COMPLETED:
- Log.d(TAG, "setting up flags listeners");
mFlagsListener.initialize();
break;
default:
diff --git a/networksecurity/service/src/com/android/server/net/ct/Config.java b/networksecurity/service/src/com/android/server/net/ct/Config.java
new file mode 100644
index 0000000..e184359
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/Config.java
@@ -0,0 +1,42 @@
+/*
+ * 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.content.ApexEnvironment;
+
+import com.android.net.module.util.DeviceConfigUtils;
+
+import java.io.File;
+
+/** Class holding the constants used by the CT feature. */
+final class Config {
+
+ static final boolean DEBUG = false;
+
+ // preferences file
+ private static final File DEVICE_PROTECTED_DATA_DIR =
+ ApexEnvironment.getApexEnvironment(DeviceConfigUtils.TETHERING_MODULE_NAME)
+ .getDeviceProtectedDataDir();
+ private static final String PREFERENCES_FILE_NAME = "ct.preferences";
+ static final File PREFERENCES_FILE = new File(DEVICE_PROTECTED_DATA_DIR, PREFERENCES_FILE_NAME);
+
+ // flags and properties names
+ static final String VERSION = "version";
+ static final String CONTENT_URL = "content_url";
+ static final String CONTENT_URL_KEY = "content_url_key";
+ static final String METADATA_URL = "metadata_url";
+ static final String METADATA_URL_KEY = "metadata_url_key";
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DataStore.java b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
new file mode 100644
index 0000000..cd6aebf
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DataStore.java
@@ -0,0 +1,67 @@
+/*
+ * 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.util.Log;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.Optional;
+import java.util.Properties;
+
+/** Class to persist data needed by CT. */
+class DataStore extends Properties {
+
+ private static final String TAG = "CertificateTransparency";
+
+ private final File mPropertyFile;
+
+ DataStore(File file) {
+ super();
+ mPropertyFile = file;
+ }
+
+ void load() {
+ if (!mPropertyFile.exists()) {
+ return;
+ }
+ try (InputStream in = new FileInputStream(mPropertyFile)) {
+ load(in);
+ } catch (IOException e) {
+ Log.e(TAG, "Error loading property store", e);
+ }
+ }
+
+ void store() {
+ try (OutputStream out = new FileOutputStream(mPropertyFile)) {
+ store(out, "");
+ } catch (IOException e) {
+ Log.e(TAG, "Error storing property store", e);
+ }
+ }
+
+ long getPropertyLong(String key, long defaultValue) {
+ return Optional.ofNullable(getProperty(key)).map(Long::parseLong).orElse(defaultValue);
+ }
+
+ Object setPropertyLong(String key, long value) {
+ return setProperty(key, Long.toString(value));
+ }
+}
diff --git a/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
new file mode 100644
index 0000000..cc8c4c0
--- /dev/null
+++ b/networksecurity/service/src/com/android/server/net/ct/DownloadHelper.java
@@ -0,0 +1,90 @@
+/*
+ * 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.app.DownloadManager;
+import android.app.DownloadManager.Query;
+import android.app.DownloadManager.Request;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+
+import androidx.annotation.VisibleForTesting;
+
+/** Class to handle downloads for Certificate Transparency. */
+public class DownloadHelper {
+
+ private final DownloadManager mDownloadManager;
+
+ @VisibleForTesting
+ DownloadHelper(DownloadManager downloadManager) {
+ mDownloadManager = downloadManager;
+ }
+
+ DownloadHelper(Context context) {
+ this(context.getSystemService(DownloadManager.class));
+ }
+
+ /**
+ * Sends a request to start the download of a provided url.
+ *
+ * @param url the url to download
+ * @return a downloadId if the request was created successfully, -1 otherwise.
+ */
+ public long startDownload(String url) {
+ return mDownloadManager.enqueue(
+ new Request(Uri.parse(url))
+ .setAllowedOverRoaming(false)
+ .setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
+ .setRequiresCharging(true));
+ }
+
+ /**
+ * Returns true if the specified download completed successfully.
+ *
+ * @param downloadId the download.
+ * @return true if the download completed successfully.
+ */
+ public boolean isSuccessful(long downloadId) {
+ try (Cursor cursor = mDownloadManager.query(new Query().setFilterById(downloadId))) {
+ if (cursor == null) {
+ return false;
+ }
+ if (cursor.moveToFirst()) {
+ int status =
+ cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS));
+ if (DownloadManager.STATUS_SUCCESSFUL == status) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Returns the URI of the specified download, or null if the download did not complete
+ * successfully.
+ *
+ * @param downloadId the download.
+ * @return the {@link Uri} if the download completed successfully, null otherwise.
+ */
+ public Uri getUri(long downloadId) {
+ if (downloadId == -1) {
+ return null;
+ }
+ return mDownloadManager.getUriForDownloadedFile(downloadId);
+ }
+}
diff --git a/networksecurity/tests/unit/Android.bp b/networksecurity/tests/unit/Android.bp
new file mode 100644
index 0000000..639f644
--- /dev/null
+++ b/networksecurity/tests/unit/Android.bp
@@ -0,0 +1,44 @@
+// 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 {
+ default_team: "trendy_team_platform_security",
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "NetworkSecurityUnitTests",
+ defaults: ["mts-target-sdk-version-current"],
+ test_suites: [
+ "general-tests",
+ "mts-tethering",
+ ],
+
+ srcs: ["src/**/*.java"],
+
+ libs: [
+ "android.test.base",
+ "android.test.mock",
+ "android.test.runner",
+ ],
+ static_libs: [
+ "androidx.test.ext.junit",
+ "androidx.test.rules",
+ "junit",
+ "mockito-target-minus-junit4",
+ "service-networksecurity-pre-jarjar",
+ "truth",
+ ],
+
+ sdk_version: "test_current",
+}
diff --git a/networksecurity/tests/unit/AndroidManifest.xml b/networksecurity/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..7a3f4b7
--- /dev/null
+++ b/networksecurity/tests/unit/AndroidManifest.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.net.ct">
+
+ <application>
+ <uses-library android:name="android.test.runner" />
+ </application>
+
+ <instrumentation
+ android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.net.ct"
+ android:label="NetworkSecurity Mainline Module Tests" />
+</manifest>
diff --git a/networksecurity/tests/unit/AndroidTest.xml b/networksecurity/tests/unit/AndroidTest.xml
new file mode 100644
index 0000000..3c94df7
--- /dev/null
+++ b/networksecurity/tests/unit/AndroidTest.xml
@@ -0,0 +1,36 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2021 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.
+ -->
+<configuration description="Runs NetworkSecurity Mainline unit Tests.">
+ <option name="test-tag" value="NetworkSecurityUnitTests" />
+
+ <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+ <option name="test-file-name" value="NetworkSecurityUnitTests.apk" />
+ </target_preparer>
+
+ <option name="config-descriptor:metadata" key="mainline-param"
+ value="com.google.android.tethering.next.apex" />
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.net.ct" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ </test>
+
+ <!-- Only run in MTS if the Tethering Mainline module is installed. -->
+ <object type="module_controller"
+ class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController">
+ <option name="mainline-module-package-name" value="com.google.android.tethering" />
+ </object>
+</configuration>
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
new file mode 100644
index 0000000..acd0d36
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/CertificateTransparencyDownloaderTest.java
@@ -0,0 +1,145 @@
+/*
+ * 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 static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.DownloadManager;
+import android.content.Context;
+import android.content.Intent;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Tests for the {@link CertificateTransparencyDownloader}. */
+@RunWith(JUnit4.class)
+public class CertificateTransparencyDownloaderTest {
+
+ @Mock private DownloadHelper mDownloadHelper;
+
+ private Context mContext;
+ private File mTempFile;
+ private DataStore mDataStore;
+ private CertificateTransparencyDownloader mCertificateTransparencyDownloader;
+
+ @Before
+ public void setUp() throws IOException {
+ MockitoAnnotations.initMocks(this);
+
+ mContext = InstrumentationRegistry.getInstrumentation().getContext();
+ mTempFile = File.createTempFile("datastore-test", ".properties");
+ mDataStore = new DataStore(mTempFile);
+ mDataStore.load();
+
+ mCertificateTransparencyDownloader =
+ new CertificateTransparencyDownloader(mContext, mDataStore, mDownloadHelper);
+ }
+
+ @After
+ public void tearDown() {
+ mTempFile.delete();
+ }
+
+ @Test
+ public void testDownloader_startMetadataDownload() {
+ String metadataUrl = "http://test-metadata.org";
+ long downloadId = 666;
+ when(mDownloadHelper.startDownload(metadataUrl)).thenReturn(downloadId);
+
+ assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isFalse();
+ mCertificateTransparencyDownloader.startMetadataDownload(metadataUrl);
+ assertThat(mCertificateTransparencyDownloader.isMetadataDownloadId(downloadId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_startContentDownload() {
+ String contentUrl = "http://test-content.org";
+ long downloadId = 666;
+ when(mDownloadHelper.startDownload(contentUrl)).thenReturn(downloadId);
+
+ assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isFalse();
+ mCertificateTransparencyDownloader.startContentDownload(contentUrl);
+ assertThat(mCertificateTransparencyDownloader.isContentDownloadId(downloadId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_handleMetadataCompleteSuccessful() {
+ long metadataId = 123;
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(true);
+
+ long contentId = 666;
+ String contentUrl = "http://test-content.org";
+ mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
+ when(mDownloadHelper.startDownload(contentUrl)).thenReturn(contentId);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(metadataId));
+
+ assertThat(mCertificateTransparencyDownloader.isContentDownloadId(contentId)).isTrue();
+ }
+
+ @Test
+ public void testDownloader_handleMetadataCompleteFailed() {
+ long metadataId = 123;
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+ when(mDownloadHelper.isSuccessful(metadataId)).thenReturn(false);
+
+ String contentUrl = "http://test-content.org";
+ mDataStore.setProperty(Config.CONTENT_URL, contentUrl);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(metadataId));
+
+ verify(mDownloadHelper, never()).startDownload(contentUrl);
+ }
+
+ @Test
+ public void testDownloader_handleContentCompleteSuccessful() {
+ long metadataId = 123;
+ mDataStore.setPropertyLong(Config.METADATA_URL_KEY, metadataId);
+
+ long contentId = 666;
+ mDataStore.setPropertyLong(Config.CONTENT_URL_KEY, contentId);
+ when(mDownloadHelper.isSuccessful(contentId)).thenReturn(true);
+
+ mCertificateTransparencyDownloader.onReceive(
+ mContext, makeDownloadCompleteIntent(contentId));
+
+ verify(mDownloadHelper, times(1)).getUri(metadataId);
+ verify(mDownloadHelper, times(1)).getUri(contentId);
+ }
+
+ private Intent makeDownloadCompleteIntent(long downloadId) {
+ return new Intent(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
+ .putExtra(DownloadManager.EXTRA_DOWNLOAD_ID, downloadId);
+ }
+}
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
new file mode 100644
index 0000000..d16f138
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DataStoreTest.java
@@ -0,0 +1,90 @@
+/*
+ * 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 org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import java.io.File;
+import java.io.IOException;
+
+/** Tests for the {@link DataStore}. */
+@RunWith(JUnit4.class)
+public class DataStoreTest {
+
+ private File mTempFile;
+ private DataStore mDataStore;
+
+ @Before
+ public void setUp() throws IOException {
+ mTempFile = File.createTempFile("datastore-test", ".properties");
+ mDataStore = new DataStore(mTempFile);
+ mDataStore.load();
+ }
+
+ @After
+ public void tearDown() {
+ mTempFile.delete();
+ }
+
+ @Test
+ public void testDataStore_propertyFileCreatedSuccessfully() {
+ assertThat(mTempFile.exists()).isTrue();
+ assertThat(mDataStore.isEmpty()).isTrue();
+ }
+
+ @Test
+ public void testDataStore_propertySet() {
+ String stringProperty = "prop1";
+ String stringValue = "i_am_a_string";
+ String longProperty = "prop3";
+ long longValue = 9000;
+
+ assertThat(mDataStore.getProperty(stringProperty)).isNull();
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(-1);
+
+ mDataStore.setProperty(stringProperty, stringValue);
+ mDataStore.setPropertyLong(longProperty, longValue);
+
+ assertThat(mDataStore.getProperty(stringProperty)).isEqualTo(stringValue);
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(longValue);
+ }
+
+ @Test
+ public void testDataStore_propertyStore() {
+ String stringProperty = "prop1";
+ String stringValue = "i_am_a_string";
+ String longProperty = "prop3";
+ long longValue = 9000;
+
+ mDataStore.setProperty(stringProperty, stringValue);
+ mDataStore.setPropertyLong(longProperty, longValue);
+ mDataStore.store();
+
+ mDataStore.clear();
+ assertThat(mDataStore.getProperty(stringProperty)).isNull();
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(-1);
+
+ mDataStore.load();
+ assertThat(mDataStore.getProperty(stringProperty)).isEqualTo(stringValue);
+ assertThat(mDataStore.getPropertyLong(longProperty, -1)).isEqualTo(longValue);
+ }
+}
diff --git a/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java b/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java
new file mode 100644
index 0000000..0b65e3c
--- /dev/null
+++ b/networksecurity/tests/unit/src/com/android/server/net/ct/DownloadHelperTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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 static org.junit.Assert.assertThrows;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.when;
+
+import android.app.DownloadManager;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/** Tests for the {@link DownloadHelper}. */
+@RunWith(JUnit4.class)
+public class DownloadHelperTest {
+
+ @Mock private DownloadManager mDownloadManager;
+
+ private DownloadHelper mDownloadHelper;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+
+ mDownloadHelper = new DownloadHelper(mDownloadManager);
+ }
+
+ @Test
+ public void testDownloadHelper_scheduleDownload() {
+ long downloadId = 666;
+ when(mDownloadManager.enqueue(any())).thenReturn(downloadId);
+
+ assertThat(mDownloadHelper.startDownload("http://test.org")).isEqualTo(downloadId);
+ }
+
+ @Test
+ public void testDownloadHelper_wrongUri() {
+ when(mDownloadManager.enqueue(any())).thenReturn(666L);
+
+ assertThrows(
+ IllegalArgumentException.class, () -> mDownloadHelper.startDownload("not_a_uri"));
+ }
+}