diff --git a/framework/src/android/net/NetworkCapabilities.java b/framework/src/android/net/NetworkCapabilities.java index e8b4642429..a4aac2f082 100644 --- a/framework/src/android/net/NetworkCapabilities.java +++ b/framework/src/android/net/NetworkCapabilities.java @@ -19,6 +19,8 @@ package android.net; import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE; import static com.android.net.module.util.BitUtils.appendStringRepresentationOfBitMaskToStringBuilder; import static com.android.net.module.util.BitUtils.describeDifferences; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED; +import static com.android.net.module.util.FrameworkConnectivityStatsLog.CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_SPECIFIER_REDACTION_REFLECTION_ERROR; import android.annotation.FlaggedApi; import android.annotation.IntDef; @@ -37,6 +39,7 @@ import android.os.Process; import android.text.TextUtils; import android.util.ArraySet; import android.util.Log; +import android.util.LruCache; import android.util.Range; import com.android.internal.annotations.VisibleForTesting; @@ -44,15 +47,19 @@ import com.android.modules.utils.build.SdkLevel; import com.android.net.flags.Flags; import com.android.net.module.util.BitUtils; import com.android.net.module.util.CollectionUtils; +import com.android.net.module.util.FrameworkConnectivityStatsLog; import com.android.net.module.util.NetworkCapabilitiesUtils; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.StringJoiner; @@ -169,6 +176,15 @@ public final class NetworkCapabilities implements Parcelable { @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public static final long REDACT_FOR_NETWORK_SETTINGS = 1 << 2; + /** + * Redact any fields that need {@link android.Manifest.permission#THREAD_NETWORK_PRIVILEGED} + * permission since the receiving app does not hold this permission. + * + * @see android.Manifest.permission#THREAD_NETWORK_PRIVILEGED + * @hide for only {@code ThreadNetworkSpecifier} which is in the same module + */ + public static final long REDACT_FOR_THREAD_NETWORK_PRIVILEGED = 1 << 3; + /** * Redact all fields in this object that require any relevant permission. * @hide @@ -182,6 +198,7 @@ public final class NetworkCapabilities implements Parcelable { REDACT_FOR_ACCESS_FINE_LOCATION, REDACT_FOR_LOCAL_MAC_ADDRESS, REDACT_FOR_NETWORK_SETTINGS, + REDACT_FOR_THREAD_NETWORK_PRIVILEGED, REDACT_ALL }) @Retention(RetentionPolicy.SOURCE) @@ -335,6 +352,11 @@ public final class NetworkCapabilities implements Parcelable { if (mTransportInfo != null) { mTransportInfo = nc.mTransportInfo.makeCopy(redactions); } + if (mNetworkSpecifier != null) { + mNetworkSpecifier = + (NetworkSpecifier) RedactionHelper.getSpecifierRedactResult( + mNetworkSpecifier, redactions); + } } /** @@ -3005,13 +3027,14 @@ public final class NetworkCapabilities implements Parcelable { * @hide */ public @RedactionType long getApplicableRedactions() { - // Currently, there are no fields redacted in NetworkCapabilities itself, so we just - // passthrough the redactions required by the embedded TransportInfo. If this changes - // in the future, modify this method. - if (mTransportInfo == null) { - return NetworkCapabilities.REDACT_NONE; + long redactions = REDACT_NONE; + if (mTransportInfo != null) { + redactions |= mTransportInfo.getApplicableRedactions(); } - return mTransportInfo.getApplicableRedactions(); + if (mNetworkSpecifier != null) { + redactions |= RedactionHelper.getSpecifierApplicableRedactions(mNetworkSpecifier); + } + return redactions; } private NetworkCapabilities removeDefaultCapabilites() { @@ -3509,4 +3532,148 @@ public final class NetworkCapabilities implements Parcelable { return new NetworkCapabilities(mCaps); } } + + /** + * Helper class for providing access to {@link NetworkSpecifier} redaction methods via + * reflection. + * + * Reflection is necessary because NetworkSpecifier is platform code. So it's possible for a + * mainline update to define a getApplicableRedactions() or redact(long) method in a subclass + * of NetworkSpecifier (e.g., EthernetNetworkSpecifier), and then to run that mainline module + * on an Android release where the NetworkSpecifier base class does not have those methods. + * On such a device, ConnectivityService must still call those methods if they exist, otherwise + * it would fail to redact information that needs to be redacted. Another way to do this would + * be for ConnectivityService to just call the method and catch NoSuchMethodError, but this + * code runs fairly often and constructing exceptions is expensive. + * + * @hide + */ + public static final class RedactionHelper { + // LruCache is thread-safe so that no locks are used to project it this class + private static final LruCache, Optional> + sGetApplicableRedactionsMethodCache = new LruCache<>(100); + private static final LruCache, Optional> + sRedactLongMethodCache = new LruCache<>(100); + + private RedactionHelper() {} + + /** + * Returns the {@link Method} of the {@link NetworkSpecifier} class for the given method + * name, or {@code null} if the method doesn't exist. + *

+ * This is typically used for {@link NetworkSpecifier#getApplicableRedactions()} and {@link + * NetworkSpecifier#redact(long)} to work with new NetworkSpecifier subclasses added in new + * mainline version but deployed to older platforms which don't have those two + * NetworkSpecifier methods. + *

+ * See {@link #createWithSensitiveInfoSanitizedIfNecessaryWhenParceled} and {@link + * #ensureSufficientPermissionsForRequest} for how this method should be used. + */ + @Nullable + private static Method getNetworkSpecifierMethod( + Class clazz, String name, Class returnType, + Class... parameterTypes) { + // Use {@code getMethods()} rather than {@code getDeclaredMethods()} so that it can + // return the inherited NetworkSpecifier methods for those existing subclasses which + // don't override {@link NetworkSpecifier#getApplicableRedactions()} and {@link + // NetworkSpecifier#redact(long)}. + // Note also {@code getMethods()} returns only public methods, which is necessary for + // code to call the method later. + for (Method method : clazz.getMethods()) { + if (name.equals(method.getName()) + && returnType.equals(method.getReturnType()) + && Arrays.equals(parameterTypes, method.getParameterTypes())) { + return method; + } + } + return null; + } + + /** + * Returns the {@link NetworkSpecifier#getApplicableRedactions()} method or {@code null} if + * it doesn't exist for the given {@link NetworkSpecifier} subclass. + */ + @Nullable + private static Method getApplicableRedactionsMethod( + Class clazz) { + // If this method is called concurrently from two threads, both may see cache missed + // and the reflection branch in method will be executed for twice. But this is okay + // because getApplicableRedactions() itself has no side-effect (and this is why we can + // create caches for it). + Optional cache = sGetApplicableRedactionsMethodCache.get(clazz); + if (cache != null) { + return cache.orElse(null); + } + Method method = getNetworkSpecifierMethod( + clazz, "getApplicableRedactions", long.class); + sGetApplicableRedactionsMethodCache.put(clazz, Optional.ofNullable(method)); + return method; + } + + /** + * Returns the value of {@link NetworkSpecifier#getApplicableRedactions()} or + * {@link REDACT_NONE} if the platform doesn't support + * {@link NetworkSpecifier#getApplicableRedactions()}. + */ + public static long getSpecifierApplicableRedactions(NetworkSpecifier specifier) { + final Method getApplicableRedactions = + getApplicableRedactionsMethod(specifier.getClass()); + if (getApplicableRedactions == null) { + return REDACT_NONE; + } + + try { + return (long) getApplicableRedactions.invoke(specifier); + } catch (IllegalAccessException | InvocationTargetException + | IllegalArgumentException e) { + FrameworkConnectivityStatsLog.write(CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED, + CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_SPECIFIER_REDACTION_REFLECTION_ERROR); + Log.wtf(TAG, "Un expected reflection error on specifier " + specifier, e); + return REDACT_ALL; + } + } + + /** + * Returns the {@link NetworkSpecifier#redact(long)} method or {@code null} if it doesn't + * exist for the given {@link NetworkSpecifier} subclass. + */ + @Nullable + private static Method getRedactLongMethod(Class clazz) { + // If this method is called concurrently from two threads, both may see cache missed + // and the reflection branch in method will be executed for twice. But this is okay + // because redact(long) itself has no side-effect (and this is why we can create caches + // for it). + Optional cache = sRedactLongMethodCache.get(clazz); + if (cache != null) { + return cache.orElse(null); + } + Method method = getNetworkSpecifierMethod( + clazz, "redact", NetworkSpecifier.class, long.class); + sRedactLongMethodCache.put(clazz, Optional.ofNullable(method)); + return method; + } + + /** + * Returns the result of {@link NetworkSpecifier#redact(long)} or {@code specifier} itself + * if the platform doesn't support {@link NetworkSpecifier#redact(long)}. + */ + @Nullable + public static NetworkSpecifier getSpecifierRedactResult( + NetworkSpecifier specifier, long redactions) { + final Method redactLong = getRedactLongMethod(specifier.getClass()); + if (redactLong == null) { + return specifier; + } + + try { + return (NetworkSpecifier) redactLong.invoke(specifier, redactions); + } catch (IllegalAccessException | InvocationTargetException + | IllegalArgumentException e) { + FrameworkConnectivityStatsLog.write(CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED, + CORE_NETWORKING_TERRIBLE_ERROR_OCCURRED__ERROR_TYPE__TYPE_SPECIFIER_REDACTION_REFLECTION_ERROR); + Log.wtf(TAG, "Un expected reflection error on specifier " + specifier, e); + return null; + } + } + } } diff --git a/framework/src/android/net/TransportInfo.java b/framework/src/android/net/TransportInfo.java index fa889eabb8..9a49ecef15 100644 --- a/framework/src/android/net/TransportInfo.java +++ b/framework/src/android/net/TransportInfo.java @@ -50,6 +50,9 @@ public interface TransportInfo { * *

* + *

Subclasses should silently ignore redactions which are not included in {@link + * #getApplicableRedactions()}. + * * @param redactions bitmask of redactions that needs to be performed on this instance. * @return Copy of this instance with the necessary redactions. * @hide diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java index 1fe49d8020..aef22fbf74 100644 --- a/service/src/com/android/server/ConnectivityService.java +++ b/service/src/com/android/server/ConnectivityService.java @@ -110,6 +110,8 @@ import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5; 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; @@ -210,6 +212,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; @@ -430,6 +433,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; @@ -2904,7 +2909,7 @@ public class ConnectivityService extends IConnectivityManager.Stub && !result.containsKey(nai.network)) { result.put( nai.network, - createWithLocationInfoSanitizedIfNecessaryWhenParceled( + createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( nc, false /* includeLocationSensitiveInfo */, getCallingPid(), mDeps.getCallingUid(), callingPackageName, callingAttributionTag)); @@ -2919,7 +2924,7 @@ public class ConnectivityService extends IConnectivityManager.Stub if (null != nc) { result.put( network, - createWithLocationInfoSanitizedIfNecessaryWhenParceled( + createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( nc, false /* includeLocationSensitiveInfo */, getCallingPid(), mDeps.getCallingUid(), callingPackageName, @@ -3118,8 +3123,7 @@ public class ConnectivityService extends IConnectivityManager.Stub private NetworkCapabilities getNetworkCapabilitiesInternal(NetworkAgentInfo nai) { if (nai == null) return null; synchronized (nai) { - return networkCapabilitiesRestrictedForCallerPermissions( - nai.networkCapabilities, Binder.getCallingPid(), mDeps.getCallingUid()); + return new NetworkCapabilities(nai.networkCapabilities); } } @@ -3128,7 +3132,7 @@ public class ConnectivityService extends IConnectivityManager.Stub @Nullable String callingAttributionTag) { mAppOpsManager.checkPackage(mDeps.getCallingUid(), callingPackageName); enforceAccessPermission(); - return createWithLocationInfoSanitizedIfNecessaryWhenParceled( + return createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( getNetworkCapabilitiesInternal(network), false /* includeLocationSensitiveInfo */, getCallingPid(), mDeps.getCallingUid(), callingPackageName, callingAttributionTag); @@ -3144,9 +3148,8 @@ public class ConnectivityService extends IConnectivityManager.Stub if (!hasAccessPermission(-1 /* pid */, uid)) { return null; } - return createWithLocationInfoSanitizedIfNecessaryWhenParceled( - networkCapabilitiesRestrictedForCallerPermissions(nc, -1 /* callerPid */, uid), - true /* includeLocationSensitiveInfo */, -1 /* callingPid */, uid, packageName, + return createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( + nc, true /* includeLocationSensitiveInfo */, -1 /* callingPid */, uid, packageName, callingAttributionTag); } @@ -3164,35 +3167,6 @@ public class ConnectivityService extends IConnectivityManager.Stub android.Manifest.permission.NETWORK_FACTORY); } - @VisibleForTesting - NetworkCapabilities networkCapabilitiesRestrictedForCallerPermissions( - NetworkCapabilities nc, int callerPid, int callerUid) { - // NoteĀ : here it would be nice to check ACCESS_NETWORK_STATE and return null, but - // this would be expensive (one more permission check every time any NC callback is - // sent) and possibly dangerous : apps normally can't lose ACCESS_NETWORK_STATE, if - // it happens for some reason (e.g. the package is uninstalled while CS is trying to - // send the callback) it would crash the system server with NPE. - final NetworkCapabilities newNc = new NetworkCapabilities(nc); - if (!hasSettingsPermission(callerPid, callerUid)) { - newNc.setUids(null); - newNc.setSSID(null); - } - if (newNc.getNetworkSpecifier() != null) { - newNc.setNetworkSpecifier(newNc.getNetworkSpecifier().redact()); - } - if (!hasAnyPermissionOf(mContext, callerPid, callerUid, - android.Manifest.permission.NETWORK_STACK, - NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)) { - newNc.setAdministratorUids(new int[0]); - } - if (!canSeeAllowedUids(callerPid, callerUid, newNc.getOwnerUid())) { - newNc.setAllowedUids(new ArraySet<>()); - } - redactUnderlyingNetworksForCapabilities(newNc, callerPid, callerUid); - - return newNc; - } - /** * Wrapper used to cache the permission check results performed for the corresponding * app. This avoids performing multiple permission checks for different fields in @@ -3211,6 +3185,7 @@ public class ConnectivityService extends IConnectivityManager.Stub private Boolean mHasLocationPermission = null; private Boolean mHasLocalMacAddressPermission = null; private Boolean mHasSettingsPermission = null; + private Boolean mHasThreadNetworkPrivilegedPermission = null; RedactionPermissionChecker(int callingPid, int callingUid, @NonNull String callingPackageName, @Nullable String callingAttributionTag) { @@ -3270,6 +3245,20 @@ public class ConnectivityService extends IConnectivityManager.Stub } return mHasSettingsPermission; } + + /** + * Returns whether the app holds THREAD_NETWORK_PRIVILEGED permission or not (might return + * cached result if the permission was already checked before). + */ + @CheckResult + private boolean hasThreadNetworkPrivilegedPermission() { + if (mHasThreadNetworkPrivilegedPermission == null) { + // If there is no cached result, perform the check now. + mHasThreadNetworkPrivilegedPermission = ConnectivityService.this + .hasThreadNetworkPrivilegedPermission(mCallingPid, mCallingUid); + } + return mHasThreadNetworkPrivilegedPermission; + } } private static boolean shouldRedact(@NetworkCapabilities.RedactionType long redactions, @@ -3304,12 +3293,17 @@ public class ConnectivityService extends IConnectivityManager.Stub redactions &= ~REDACT_FOR_NETWORK_SETTINGS; } } + if (shouldRedact(redactions, REDACT_FOR_THREAD_NETWORK_PRIVILEGED)) { + if (redactionPermissionChecker.hasThreadNetworkPrivilegedPermission()) { + redactions &= ~REDACT_FOR_THREAD_NETWORK_PRIVILEGED; + } + } return redactions; } @VisibleForTesting @Nullable - NetworkCapabilities createWithLocationInfoSanitizedIfNecessaryWhenParceled( + NetworkCapabilities createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( @Nullable NetworkCapabilities nc, boolean includeLocationSensitiveInfo, int callingPid, int callingUid, @NonNull String callingPkgName, @Nullable String callingAttributionTag) { @@ -3325,6 +3319,42 @@ public class ConnectivityService extends IConnectivityManager.Stub nc.getApplicableRedactions(), redactionPermissionChecker, includeLocationSensitiveInfo); final NetworkCapabilities newNc = new NetworkCapabilities(nc, redactions); + + // NoteĀ : here it would be nice to check ACCESS_NETWORK_STATE and return null, but + // this would be expensive (one more permission check every time any NC callback is + // sent) and possibly dangerous : apps normally can't lose ACCESS_NETWORK_STATE, if + // it happens for some reason (e.g. the package is uninstalled while CS is trying to + // send the callback) it would crash the system server with NPE. + if (!hasSettingsPermission(callingPid, callingUid)) { + newNc.setUids(null); + newNc.setSSID(null); + } + // This code does not need to call redact(long) because that was already done above during + // the construction of newNc. + // If {@code getApplicableRedactions()} of a NetworkSpecifier subclass returns + // {@code REDACT_NONE}, it's because either 1) it doesn't override it or 2) it does + // override it, and the implementation returned REDACT_NONE. For the first case, call + // {@code redact()} for backward compatibility with existing NetworkSpecifier such as + // WifiNetworkSpecifier. For the second case, it's still okay to call {@code redact()} as + // it's suggested that all new NetworkSpecifier subclasses should not override the default + // {@code redact()} and it will do nothing (this is consistent with what REDACT_NONE + // indicates). + final NetworkSpecifier specifier = newNc.getNetworkSpecifier(); + if (specifier != null + && RedactionHelper.getSpecifierApplicableRedactions(specifier) == REDACT_NONE) { + newNc.setNetworkSpecifier(specifier.redact()); + } + + if (!hasAnyPermissionOf(mContext, callingPid, callingUid, + android.Manifest.permission.NETWORK_STACK, + NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)) { + newNc.setAdministratorUids(new int[0]); + } + if (!canSeeAllowedUids(callingPid, callingUid, newNc.getOwnerUid())) { + newNc.setAllowedUids(new ArraySet<>()); + } + redactUnderlyingNetworksForCapabilities(newNc, callingPid, callingUid); + // Reset owner uid if not destined for the owner app. // TODO : calling UID is redacted because apps should generally not know what UID is // bringing up the VPN, but this should not apply to some very privileged apps like settings @@ -4272,6 +4302,16 @@ public class ConnectivityService extends IConnectivityManager.Stub Manifest.permission.LOCAL_MAC_ADDRESS, pid, uid); } + @CheckResult + private boolean hasThreadNetworkPrivilegedPermission(int pid, int uid) { + // The THREAD_NETWORK_PRIVILEGED permission was added in Android V + if (!SdkLevel.isAtLeastV()) { + return false; + } + return PERMISSION_GRANTED == mContext.checkPermission( + Manifest.permission.THREAD_NETWORK_PRIVILEGED, pid, uid); + } + private void sendConnectedBroadcast(NetworkInfo info) { sendGeneralBroadcast(info, CONNECTIVITY_ACTION); } @@ -8910,11 +8950,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"); } @@ -11837,22 +11910,18 @@ public class ConnectivityService extends IConnectivityManager.Stub switch (notificationType) { case CALLBACK_RESERVED: { final NetworkCapabilities nc = - createWithLocationInfoSanitizedIfNecessaryWhenParceled( - networkCapabilitiesRestrictedForCallerPermissions( - nri.getReservedCapabilities(), nri.mPid, nri.mUid), - includeLocationSensitiveInfo, nri.mPid, nri.mUid, - nrForCallback.getRequestorPackageName(), + createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( + nri.getReservedCapabilities(), includeLocationSensitiveInfo, + nri.mPid, nri.mUid, nrForCallback.getRequestorPackageName(), nri.mCallingAttributionTag); putParcelable(bundle, nc); break; } case CALLBACK_AVAILABLE: { final NetworkCapabilities nc = - createWithLocationInfoSanitizedIfNecessaryWhenParceled( - networkCapabilitiesRestrictedForCallerPermissions( - networkAgent.networkCapabilities, nri.mPid, nri.mUid), - includeLocationSensitiveInfo, nri.mPid, nri.mUid, - nrForCallback.getRequestorPackageName(), + createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( + networkAgent.networkCapabilities, includeLocationSensitiveInfo, + nri.mPid, nri.mUid, nrForCallback.getRequestorPackageName(), nri.mCallingAttributionTag); putParcelable(bundle, nc); putParcelable(bundle, linkPropertiesRestrictedForCallerPermissions( @@ -11865,14 +11934,11 @@ public class ConnectivityService extends IConnectivityManager.Stub } case CALLBACK_CAP_CHANGED: { // networkAgent can't be null as it has been accessed a few lines above. - final NetworkCapabilities netCap = - networkCapabilitiesRestrictedForCallerPermissions( - networkAgent.networkCapabilities, nri.mPid, nri.mUid); putParcelable( bundle, - createWithLocationInfoSanitizedIfNecessaryWhenParceled( - netCap, includeLocationSensitiveInfo, nri.mPid, nri.mUid, - nrForCallback.getRequestorPackageName(), + createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( + networkAgent.networkCapabilities, includeLocationSensitiveInfo, + nri.mPid, nri.mUid, nrForCallback.getRequestorPackageName(), nri.mCallingAttributionTag)); break; } diff --git a/tests/common/java/android/net/NetworkCapabilitiesTest.java b/tests/common/java/android/net/NetworkCapabilitiesTest.java index 499b52d3be..373b1f36da 100644 --- a/tests/common/java/android/net/NetworkCapabilitiesTest.java +++ b/tests/common/java/android/net/NetworkCapabilitiesTest.java @@ -51,6 +51,8 @@ import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_5; 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.SIGNAL_STRENGTH_UNSPECIFIED; import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR; import static android.net.NetworkCapabilities.TRANSPORT_ETHERNET; @@ -63,9 +65,11 @@ import static android.net.NetworkCapabilities.TRANSPORT_WIFI; import static android.net.NetworkCapabilities.TRANSPORT_WIFI_AWARE; import static android.os.Process.INVALID_UID; +import static com.android.modules.utils.build.SdkLevel.isAtLeastB; import static com.android.modules.utils.build.SdkLevel.isAtLeastS; import static com.android.modules.utils.build.SdkLevel.isAtLeastT; import static com.android.modules.utils.build.SdkLevel.isAtLeastV; +import static com.android.net.thread.platform.flags.Flags.FLAG_THREAD_MOBILE_ENABLED; import static com.android.testutils.MiscAsserts.assertEmpty; import static com.android.testutils.MiscAsserts.assertThrows; import static com.android.testutils.ParcelUtils.assertParcelingIsLossless; @@ -74,11 +78,15 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import static org.junit.Assume.assumeFalse; import static org.junit.Assume.assumeTrue; +import android.annotation.Nullable; +import android.net.NetworkCapabilities.RedactionHelper; import android.net.wifi.aware.DiscoverySession; import android.net.wifi.aware.PeerHandle; import android.net.wifi.aware.WifiAwareNetworkSpecifier; @@ -99,6 +107,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mockito; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -409,13 +418,15 @@ public class NetworkCapabilitiesTest { assertParcelingIsLossless(cap); } - private static NetworkCapabilities createNetworkCapabilitiesWithTransportInfo() { + private static NetworkCapabilities createNetworkCapabilitiesWithTransportInfoAndSpecifier() { return new NetworkCapabilities() .addCapability(NET_CAPABILITY_INTERNET) .addCapability(NET_CAPABILITY_EIMS) .addCapability(NET_CAPABILITY_NOT_METERED) + .addTransportType(TRANSPORT_THREAD) .setSSID(TEST_SSID) .setTransportInfo(new TestTransportInfo()) + .setNetworkSpecifier(new RedactableNetworkSpecifier()) .setRequestorPackageName("com.android.test") .setRequestorUid(9304); } @@ -424,7 +435,8 @@ public class NetworkCapabilitiesTest { public void testNetworkCapabilitiesCopyWithNoRedactions() { assumeTrue(isAtLeastS()); - final NetworkCapabilities netCap = createNetworkCapabilitiesWithTransportInfo(); + final NetworkCapabilities netCap = + createNetworkCapabilitiesWithTransportInfoAndSpecifier(); final NetworkCapabilities netCapWithNoRedactions = new NetworkCapabilities(netCap, NetworkCapabilities.REDACT_NONE); TestTransportInfo testTransportInfo = @@ -432,13 +444,18 @@ public class NetworkCapabilitiesTest { assertFalse(testTransportInfo.locationRedacted); assertFalse(testTransportInfo.localMacAddressRedacted); assertFalse(testTransportInfo.settingsRedacted); + RedactableNetworkSpecifier testNetworkSpecifier = + (RedactableNetworkSpecifier) netCapWithNoRedactions.getNetworkSpecifier(); + assertFalse(testNetworkSpecifier.locationRedacted); + assertFalse(testNetworkSpecifier.threadNetworkRedacted); } @Test public void testNetworkCapabilitiesCopyWithoutLocationSensitiveFields() { assumeTrue(isAtLeastS()); - final NetworkCapabilities netCap = createNetworkCapabilitiesWithTransportInfo(); + final NetworkCapabilities netCap = + createNetworkCapabilitiesWithTransportInfoAndSpecifier(); final NetworkCapabilities netCapWithNoRedactions = new NetworkCapabilities(netCap, REDACT_FOR_ACCESS_FINE_LOCATION); TestTransportInfo testTransportInfo = @@ -446,6 +463,24 @@ public class NetworkCapabilitiesTest { assertTrue(testTransportInfo.locationRedacted); assertFalse(testTransportInfo.localMacAddressRedacted); assertFalse(testTransportInfo.settingsRedacted); + RedactableNetworkSpecifier testNetworkSpecifier = + (RedactableNetworkSpecifier) netCapWithNoRedactions.getNetworkSpecifier(); + assertTrue(testNetworkSpecifier.locationRedacted); + assertFalse(testNetworkSpecifier.threadNetworkRedacted); + } + + @Test + public void testGetApplicableRedactions() { + assumeTrue(isAtLeastS()); + + final NetworkCapabilities netCap = + createNetworkCapabilitiesWithTransportInfoAndSpecifier(); + final long redactions = netCap.getApplicableRedactions(); + + assertEquals( + REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_LOCAL_MAC_ADDRESS + | REDACT_FOR_NETWORK_SETTINGS | REDACT_FOR_THREAD_NETWORK_PRIVILEGED, + redactions); } @Test @@ -1235,6 +1270,44 @@ public class NetworkCapabilitiesTest { } } + /** + * Redactable NetworkSpecifier subclass for verifying redaction mechanism. + */ + private static final class RedactableNetworkSpecifier extends NetworkSpecifier { + final boolean locationRedacted; + final boolean threadNetworkRedacted; + + RedactableNetworkSpecifier() { + locationRedacted = false; + threadNetworkRedacted = false; + } + + RedactableNetworkSpecifier(boolean locationRedacted, boolean threadNetworkRedacted) { + this.locationRedacted = locationRedacted; + this.threadNetworkRedacted = threadNetworkRedacted; + } + + @Override + public long getApplicableRedactions() { + return REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_THREAD_NETWORK_PRIVILEGED; + } + + @Override + @Nullable + public NetworkSpecifier redact(long redactions) { + return new RedactableNetworkSpecifier( + (redactions & REDACT_FOR_ACCESS_FINE_LOCATION) != 0, + (redactions & REDACT_FOR_THREAD_NETWORK_PRIVILEGED) != 0); + } + } + + /** + * Unredactable NetworkSpecifier subclass for verifying redaction mechanism. + */ + private static final class UnredactableNetworkSpecifier extends NetworkSpecifier { + // not overriding the getApplicableRedactions() and redact(long) methods + } + @Test public void testBuilder() { final int ownerUid = 1001; @@ -1621,4 +1694,35 @@ public class NetworkCapabilitiesTest { assertFalse(wifiRequestNc.satisfiedByNetworkCapabilities(localWifiNc)); } } + + @Test + public void testRedactionHelper() throws Exception { + final UnredactableNetworkSpecifier unredactableSpecifier = + new UnredactableNetworkSpecifier(); + final RedactableNetworkSpecifier redactableSpecifier = new RedactableNetworkSpecifier(); + + assertEquals(REDACT_NONE, + RedactionHelper.getSpecifierApplicableRedactions(unredactableSpecifier)); + assertEquals(unredactableSpecifier, + RedactionHelper.getSpecifierRedactResult( + unredactableSpecifier, REDACT_FOR_ACCESS_FINE_LOCATION)); + assertEquals(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_THREAD_NETWORK_PRIVILEGED, + RedactionHelper.getSpecifierApplicableRedactions(redactableSpecifier)); + // Call getSpecifierApplicableRedactions() twice to make sure the cache works + assertEquals(REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_THREAD_NETWORK_PRIVILEGED, + RedactionHelper.getSpecifierApplicableRedactions(redactableSpecifier)); + final RedactableNetworkSpecifier redactedSpecifier = + (RedactableNetworkSpecifier) RedactionHelper.getSpecifierRedactResult( + redactableSpecifier, + REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_THREAD_NETWORK_PRIVILEGED); + assertTrue(redactedSpecifier.locationRedacted); + assertTrue(redactedSpecifier.threadNetworkRedacted); + // Call getSpecifierRedactResult twice to make sure the cache works + final RedactableNetworkSpecifier redactedSpecifier2 = + (RedactableNetworkSpecifier) RedactionHelper.getSpecifierRedactResult( + redactableSpecifier, + REDACT_FOR_ACCESS_FINE_LOCATION | REDACT_FOR_THREAD_NETWORK_PRIVILEGED); + assertTrue(redactedSpecifier2.locationRedacted); + assertTrue(redactedSpecifier2.threadNetworkRedacted); + } } diff --git a/tests/unit/java/com/android/server/ConnectivityServiceTest.java b/tests/unit/java/com/android/server/ConnectivityServiceTest.java index 2dd42081d5..bb13c304ec 100644 --- a/tests/unit/java/com/android/server/ConnectivityServiceTest.java +++ b/tests/unit/java/com/android/server/ConnectivityServiceTest.java @@ -12645,7 +12645,7 @@ public class ConnectivityServiceTest { boolean includeLocationSensitiveInfo) { final NetworkCapabilities netCap = new NetworkCapabilities().setOwnerUid(ownerUid); - return mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + return mService.createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( netCap, includeLocationSensitiveInfo, Process.myUid(), callerUid, mContext.getPackageName(), getAttributionTag()) .getOwnerUid(); @@ -12659,7 +12659,7 @@ public class ConnectivityServiceTest { final NetworkCapabilities netCap = new NetworkCapabilities().setTransportInfo(transportInfo); - mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + mService.createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( netCap, includeLocationSensitiveInfo, Process.myPid(), callerUid, mContext.getPackageName(), getAttributionTag()); if (shouldMakeCopyWithLocationSensitiveFieldsParcelable) { @@ -12817,7 +12817,7 @@ public class ConnectivityServiceTest { final NetworkCapabilities netCap = new NetworkCapabilities().setTransportInfo(transportInfo); - mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + mService.createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( netCap, false /* includeLocationSensitiveInfoInTransportInfo */, Process.myPid(), Process.myUid(), mContext.getPackageName(), getAttributionTag()); @@ -12836,7 +12836,7 @@ public class ConnectivityServiceTest { final NetworkCapabilities netCap = new NetworkCapabilities().setTransportInfo(transportInfo); - mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + mService.createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( netCap, false /* includeLocationSensitiveInfoInTransportInfo */, Process.myPid(), Process.myUid(), mContext.getPackageName(), getAttributionTag()); @@ -12856,7 +12856,7 @@ public class ConnectivityServiceTest { final NetworkCapabilities netCap = new NetworkCapabilities().setTransportInfo(transportInfo); - mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + mService.createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( netCap, false /* includeLocationSensitiveInfoInTransportInfo */, Process.myPid(), Process.myUid(), mContext.getPackageName(), getAttributionTag()); @@ -12875,7 +12875,7 @@ public class ConnectivityServiceTest { final NetworkCapabilities netCap = new NetworkCapabilities().setTransportInfo(transportInfo); - mService.createWithLocationInfoSanitizedIfNecessaryWhenParceled( + mService.createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( netCap, false /* includeLocationSensitiveInfoInTransportInfo */, Process.myPid(), Process.myUid(), mContext.getPackageName(), getAttributionTag()); @@ -17698,8 +17698,9 @@ public class ConnectivityServiceTest { nc.setSubscriptionIds(subIds); final NetworkCapabilities result = - mService.networkCapabilitiesRestrictedForCallerPermissions( - nc, Process.myPid(), Process.myUid()); + mService.createWithSensitiveInfoSanitizedIfNecessaryWhenParceled( + nc, false /* includeLocationSensitiveInfo */, Process.myPid(), + Process.myUid(), mContext.getPackageName(), getAttributionTag()); assertEquals(subIds, result.getSubscriptionIds()); } 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..780d24079e --- /dev/null +++ b/tests/unit/java/com/android/server/connectivityservice/CSNetworkSpecifierTest.kt @@ -0,0 +1,240 @@ +/* + * 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.TestableNetworkCallback.Event.CapabilitiesChanged +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) + } + } + + /** Legacy NetworkSpecifier subclass which overrides only the {@code redact()} method. */ + data class LegacyNetworkSpecifier(val macAddress: MacAddress?) : + NetworkSpecifier(), Parcelable { + constructor( + p: Parcel + ) : this(p.readParcelable(MacAddress::class.java.classLoader, MacAddress::class.java)) + + override fun canBeSatisfiedBy(other: NetworkSpecifier?) = equals(other) + + override fun redact(): NetworkSpecifier? = + LegacyNetworkSpecifier(null) + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeParcelable(macAddress, 0 /* parcelableFlags */) + } + + override fun describeContents() = 0 + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(source: Parcel) = LegacyNetworkSpecifier(source) + override fun newArray(size: Int) = arrayOfNulls(size) + } + } + + fun newWifiRequestBuilder(): NetworkRequest.Builder { + return NetworkRequest.Builder().addTransportType(TRANSPORT_WIFI) + } + + fun newWifiCapabilities(specifier: NetworkSpecifier): NetworkCapabilities { + return defaultNc().apply { + addTransportType(TRANSPORT_WIFI) + setNetworkSpecifier(specifier) + } + } + + @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()) + } + } + + @Test + @IgnoreUpTo(VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testNetworkCallback_permissionGranted_notRedacted() { + 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() + + val callback = TestableNetworkCallback() + cm.registerNetworkCallback(request, callback) + Agent(TRANSPORT_WIFI, baseNc = newWifiCapabilities(specifier)).connect() + + callback.eventuallyExpect { + it.caps.networkSpecifier == specifier + } + cm.unregisterNetworkCallback(callback) + } + + @Test + @IgnoreUpTo(VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testNetworkCallback_permissionDenied_redacted() { + 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().build() + + val callback = TestableNetworkCallback() + cm.registerNetworkCallback(request, callback) + Agent(TRANSPORT_WIFI, baseNc = newWifiCapabilities(specifier)).connect() + + callback.eventuallyExpect { + (it.caps.networkSpecifier as RedactableNetworkSpecifier).macAddress == null + (it.caps.networkSpecifier as RedactableNetworkSpecifier).threadPrivilegedKey == null + } + cm.unregisterNetworkCallback(callback) + } + + @Test + @IgnoreUpTo(VERSION_CODES.UPSIDE_DOWN_CAKE) + fun testNetworkCallback_permissionGrantedForLegacyNetworkSpecifier_redacted() { + context.setPermission(LOCAL_MAC_ADDRESS, PERMISSION_GRANTED) + context.setPermission(THREAD_NETWORK_PRIVILEGED, PERMISSION_GRANTED) + val specifier = LegacyNetworkSpecifier(DEFAULT_MAC_ADDR) + val request = newWifiRequestBuilder().build() + + val callback = TestableNetworkCallback() + cm.registerNetworkCallback(request, callback) + Agent(TRANSPORT_WIFI, baseNc = newWifiCapabilities(specifier)).connect() + + callback.eventuallyExpect { + (it.caps.networkSpecifier as LegacyNetworkSpecifier).macAddress == null + } + cm.unregisterNetworkCallback(callback) + } + + companion object { + private val DEFAULT_MAC_ADDR = MacAddress.fromString("0:1:2:3:4:5") + private val DEFAULT_THREAD_KEY = "thread-key" + } +}