diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index 20026ccafb..72981f4f50 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -110,6 +110,7 @@ import static android.net.NetworkCapabilities.REDACT_FOR_ACCESS_FINE_LOCATION; import static android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS; import static android.net.NetworkCapabilities.REDACT_FOR_NETWORK_SETTINGS; import static android.net.NetworkCapabilities.REDACT_FOR_THREAD_NETWORK_PRIVILEGED; +import static android.net.NetworkCapabilities.REDACT_NONE; import static android.net.NetworkCapabilities.RES_ID_MATCH_ALL_RESERVATIONS; import static android.net.NetworkCapabilities.RES_ID_UNSET; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; @@ -209,6 +210,7 @@ import android.net.ConnectivityManager; import android.net.ConnectivityManager.BlockedReason; import android.net.ConnectivityManager.NetworkCallback; import android.net.ConnectivityManager.RestrictBackgroundStatus; +import android.net.NetworkCapabilities.RedactionHelper; import android.net.ConnectivitySettingsManager; import android.net.DataStallReportParcelable; import android.net.DnsResolverServiceManager; @@ -428,6 +430,8 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.io.PrintWriter; import java.io.Writer; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; @@ -8880,11 +8884,44 @@ public class ConnectivityService extends IConnectivityManager.Stub } } + @Nullable + private NetworkSpecifier maybeRedactRequestedNetworkSpecifier( + @Nullable NetworkSpecifier specifier, int callerPid, int callerUid, + String callerPackageName, @Nullable String callerAttributionTag, + boolean includeLocationSensitiveInfo) { + if (specifier == null) { + return null; + } + + final long applicableRedactions = + RedactionHelper.getSpecifierApplicableRedactions(specifier); + if (applicableRedactions == REDACT_NONE) { + return specifier; + } + + final RedactionPermissionChecker redactionPermissionChecker = + new RedactionPermissionChecker( + callerPid, callerUid, callerPackageName, callerAttributionTag); + final long redactions = retrieveRequiredRedactions( + applicableRedactions, redactionPermissionChecker, + includeLocationSensitiveInfo); + return RedactionHelper.getSpecifierRedactResult(specifier, redactions); + } + // This checks that the passed capabilities either do not request a // specific SSID/SignalStrength, or the calling app has permission to do so. private void ensureSufficientPermissionsForRequest(NetworkCapabilities nc, int callerPid, int callerUid, String callerPackageName, @Nullable String callerAttributionTag, boolean includeLocationSensitiveInfo) { + final NetworkSpecifier specifier = nc.getNetworkSpecifier(); + final NetworkSpecifier redactedSpecifier = maybeRedactRequestedNetworkSpecifier( + specifier, callerPid, callerUid, callerPackageName, callerAttributionTag, + includeLocationSensitiveInfo); + if (!Objects.equals(redactedSpecifier, specifier)) { + throw new SecurityException( + "Insufficient permissions to request a specific NetworkSpecifier"); + } + if (null != nc.getSsid() && !hasSettingsPermission(callerPid, callerUid)) { throw new SecurityException("Insufficient permissions to request a specific SSID"); } diff --git a/tests/unit/java/com/android/server/connectivityservice/CSNetworkSpecifierTest.kt b/tests/unit/java/com/android/server/connectivityservice/CSNetworkSpecifierTest.kt new file mode 100644 index 0000000000..ba77fcc995 --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkSpecifierTest.kt @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2025 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 + +import android.Manifest.permission.LOCAL_MAC_ADDRESS +import android.Manifest.permission.THREAD_NETWORK_PRIVILEGED +import android.content.pm.PackageManager.PERMISSION_DENIED +import android.content.pm.PackageManager.PERMISSION_GRANTED +import android.net.MacAddress +import android.net.NetworkCapabilities +import android.net.NetworkCapabilities.REDACT_FOR_LOCAL_MAC_ADDRESS +import android.net.NetworkCapabilities.REDACT_FOR_THREAD_NETWORK_PRIVILEGED +import android.net.NetworkCapabilities.TRANSPORT_WIFI +import android.net.NetworkRequest +import android.net.NetworkSpecifier +import android.os.Build.VERSION_CODES +import android.os.Parcel +import android.os.Parcelable +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRule.IgnoreAfter +import com.android.testutils.DevSdkIgnoreRule.IgnoreUpTo +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.TestableNetworkCallback +import com.android.testutils.assertThrows +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** Unit tests for {@link NetworkSpecifier} functionalities in the ConnectivityService class. */ +@RunWith(DevSdkIgnoreRunner::class) +@IgnoreUpTo(VERSION_CODES.R) +@DevSdkIgnoreRunner.MonitorThreadLeak +class CSNetworkSpecifierTest : CSTest() { + @get:Rule(order = 0) val mIgnoreRule = DevSdkIgnoreRule() + + /** + * A NetworkSpecifier subclass which overrides {@link + * NetworkSpecifier#getApplicableRedactions()} and {@link NetworkSpecifier#redact(long)}. + */ + data class RedactableNetworkSpecifier( + val macAddress: MacAddress?, val threadPrivilegedKey: String?) : + NetworkSpecifier(), Parcelable { + constructor( + p: Parcel + ) : this(p.readParcelable(MacAddress::class.java.classLoader, MacAddress::class.java), + p.readString()) + + override fun canBeSatisfiedBy(other: NetworkSpecifier?) = equals(other) + + override fun redact(redactions: Long): NetworkSpecifier? = + RedactableNetworkSpecifier( + if ((redactions and REDACT_FOR_LOCAL_MAC_ADDRESS) != 0L) null + else macAddress, + if ((redactions and REDACT_FOR_THREAD_NETWORK_PRIVILEGED) != 0L) null + else threadPrivilegedKey, + ) + + override fun getApplicableRedactions() = + REDACT_FOR_LOCAL_MAC_ADDRESS or REDACT_FOR_THREAD_NETWORK_PRIVILEGED + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeParcelable(macAddress, 0 /* parcelableFlags */) + dest.writeString(threadPrivilegedKey) + } + + override fun describeContents() = 0 + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(source: Parcel) = RedactableNetworkSpecifier(source) + override fun newArray(size: Int) = arrayOfNulls(size) + } + } + + fun newWifiRequestBuilder(): NetworkRequest.Builder { + return NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI) + } + + @Test + @IgnoreUpTo(VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testRequestNetworkPermission_permissionDenied_throwsSecurityException() { + context.setPermission(LOCAL_MAC_ADDRESS, PERMISSION_DENIED) + context.setPermission(THREAD_NETWORK_PRIVILEGED, PERMISSION_DENIED) + val specifier = RedactableNetworkSpecifier(DEFAULT_MAC_ADDR, DEFAULT_THREAD_KEY) + val request = newWifiRequestBuilder().setNetworkSpecifier(specifier).build() + + assertThrows(SecurityException::class.java) { + cm.requestNetwork(request, TestableNetworkCallback()) + } + assertThrows(SecurityException::class.java) { + cm.registerNetworkCallback(request, TestableNetworkCallback()) + } + } + + @Test + @IgnoreUpTo(VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testRequestNetworkPermission_noNetworkSpecifier_noThrow() { + context.setPermission(LOCAL_MAC_ADDRESS, PERMISSION_DENIED) + context.setPermission(THREAD_NETWORK_PRIVILEGED, PERMISSION_DENIED) + val request = newWifiRequestBuilder().build() + + cm.requestNetwork(request, TestableNetworkCallback()) + val callback = TestableNetworkCallback() + cm.registerNetworkCallback(request, callback) + cm.unregisterNetworkCallback(callback) + } + + @Test + @IgnoreUpTo(VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testRequestNetworkPermission_permissionGranted_noThrow() { + context.setPermission(LOCAL_MAC_ADDRESS, PERMISSION_GRANTED) + context.setPermission(THREAD_NETWORK_PRIVILEGED, PERMISSION_GRANTED) + val specifier = RedactableNetworkSpecifier(DEFAULT_MAC_ADDR, DEFAULT_THREAD_KEY) + val request = newWifiRequestBuilder().setNetworkSpecifier(specifier).build() + + cm.requestNetwork(request, TestableNetworkCallback()) + val callback = TestableNetworkCallback() + cm.registerNetworkCallback(request, callback) + cm.unregisterNetworkCallback(callback) + } + + @Test + @IgnoreAfter(VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testThreadPrivilegedPermission_platformOlderThanV_throwsSecurityException() { + context.setPermission(LOCAL_MAC_ADDRESS, PERMISSION_GRANTED) + context.setPermission(THREAD_NETWORK_PRIVILEGED, PERMISSION_GRANTED) + val specifier = RedactableNetworkSpecifier(DEFAULT_MAC_ADDR, DEFAULT_THREAD_KEY) + val request = newWifiRequestBuilder().setNetworkSpecifier(specifier).build() + + assertThrows(SecurityException::class.java) { + cm.requestNetwork(request, TestableNetworkCallback()) + } + assertThrows(SecurityException::class.java) { + cm.registerNetworkCallback(request, TestableNetworkCallback()) + } + } + + companion object { + private val DEFAULT_MAC_ADDR = MacAddress.fromString("0:1:2:3:4:5") + private val DEFAULT_THREAD_KEY = "thread-key" + } +}