Add new methods to redact NetworkCapabilities & LinkProperties

Some system components like VPN need to know how to redact
NetworkCapabilities & LinkProperties that they received from
the system but need to send to third-party applications with
less privilege than themselves. To make sure the redaction is
consistent, expose system API methods to do it that are wired
to the same redaction code used by ConnectivityService.

Bug: 191413541
Test: atest CtsNetTestCases, which includes new CTS for these
Change-Id: Ia3ae4755b5192884c147d6828f96cedac000a25b
diff --git a/framework/api/module-lib-current.txt b/framework/api/module-lib-current.txt
index 64e159b..dec9d35 100644
--- a/framework/api/module-lib-current.txt
+++ b/framework/api/module-lib-current.txt
@@ -10,6 +10,8 @@
     method @NonNull @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public java.util.List<android.net.NetworkStateSnapshot> getAllNetworkStateSnapshots();
     method @Nullable public android.net.ProxyInfo getGlobalProxy();
     method @NonNull public static android.util.Range<java.lang.Integer> getIpSecNetIdRange();
+    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.LinkProperties redactLinkPropertiesForPackage(@NonNull android.net.LinkProperties, int, @NonNull String);
+    method @Nullable @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public android.net.NetworkCapabilities redactNetworkCapabilitiesForPackage(@NonNull android.net.NetworkCapabilities, int, @NonNull String);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerDefaultNetworkCallbackForUid(int, @NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
     method @RequiresPermission(anyOf={android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK, android.Manifest.permission.NETWORK_SETTINGS}) public void registerSystemDefaultNetworkCallback(@NonNull android.net.ConnectivityManager.NetworkCallback, @NonNull android.os.Handler);
     method @RequiresPermission(anyOf={android.Manifest.permission.NETWORK_SETTINGS, android.Manifest.permission.NETWORK_STACK, android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK}) public void replaceFirewallChain(int, @NonNull int[]);
diff --git a/framework/src/android/net/ConnectivityManager.java b/framework/src/android/net/ConnectivityManager.java
index 5246623..e8e1efa 100644
--- a/framework/src/android/net/ConnectivityManager.java
+++ b/framework/src/android/net/ConnectivityManager.java
@@ -1625,16 +1625,45 @@
     }
 
     /**
-     * Get the {@link NetworkCapabilities} for the given {@link Network}.  This
-     * will return {@code null} if the network is unknown or if the |network| argument is null.
+     * Redact {@link LinkProperties} for a given package
      *
-     * This will remove any location sensitive data in {@link TransportInfo} embedded in
-     * {@link NetworkCapabilities#getTransportInfo()}. Some transport info instances like
-     * {@link android.net.wifi.WifiInfo} contain location sensitive information. Retrieving
-     * this location sensitive information (subject to app's location permissions) will be
-     * noted by system. To include any location sensitive data in {@link TransportInfo},
-     * use a {@link NetworkCallback} with
-     * {@link NetworkCallback#FLAG_INCLUDE_LOCATION_INFO} flag.
+     * Returns an instance of the given {@link LinkProperties} appropriately redacted to send to the
+     * given package, considering its permissions.
+     *
+     * @param lp A {@link LinkProperties} which will be redacted.
+     * @param uid The target uid.
+     * @param packageName The name of the package, for appops logging.
+     * @return A redacted {@link LinkProperties} which is appropriate to send to the given uid,
+     *         or null if the uid lacks the ACCESS_NETWORK_STATE permission.
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.NETWORK_SETTINGS})
+    @SystemApi(client = MODULE_LIBRARIES)
+    @Nullable
+    public LinkProperties redactLinkPropertiesForPackage(@NonNull LinkProperties lp, int uid,
+            @NonNull String packageName) {
+        try {
+            return mService.redactLinkPropertiesForPackage(
+                    lp, uid, packageName, getAttributionTag());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the {@link NetworkCapabilities} for the given {@link Network}, or null.
+     *
+     * This will remove any location sensitive data in the returned {@link NetworkCapabilities}.
+     * Some {@link TransportInfo} instances like {@link android.net.wifi.WifiInfo} contain location
+     * sensitive information. To retrieve this location sensitive information (subject to
+     * the caller's location permissions), use a {@link NetworkCallback} with the
+     * {@link NetworkCallback#FLAG_INCLUDE_LOCATION_INFO} flag instead.
+     *
+     * This method returns {@code null} if the network is unknown or if the |network| argument
+     * is null.
      *
      * @param network The {@link Network} object identifying the network in question.
      * @return The {@link NetworkCapabilities} for the network, or {@code null}.
@@ -1651,6 +1680,38 @@
     }
 
     /**
+     * Redact {@link NetworkCapabilities} for a given package.
+     *
+     * Returns an instance of {@link NetworkCapabilities} that is appropriately redacted to send
+     * to the given package, considering its permissions. Calling this method will blame the UID for
+     * retrieving the device location if the passed capabilities contain location-sensitive
+     * information.
+     *
+     * @param nc A {@link NetworkCapabilities} instance which will be redacted.
+     * @param uid The target uid.
+     * @param packageName The name of the package, for appops logging.
+     * @return A redacted {@link NetworkCapabilities} which is appropriate to send to the given uid,
+     *         or null if the uid lacks the ACCESS_NETWORK_STATE permission.
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK,
+            android.Manifest.permission.NETWORK_SETTINGS})
+    @SystemApi(client = MODULE_LIBRARIES)
+    @Nullable
+    public NetworkCapabilities redactNetworkCapabilitiesForPackage(
+            @NonNull NetworkCapabilities nc,
+            int uid, @NonNull String packageName) {
+        try {
+            return mService.redactNetworkCapabilitiesForPackage(nc, uid, packageName,
+                    getAttributionTag());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
      * Gets a URL that can be used for resolving whether a captive portal is present.
      * 1. This URL should respond with a 204 response to a GET request to indicate no captive
      *    portal is present.
@@ -3547,7 +3608,20 @@
          * @hide
          */
         public static final int FLAG_NONE = 0;
+
         /**
+         * Inclusion of this flag means location-sensitive redaction requests keeping location info.
+         *
+         * Some objects like {@link NetworkCapabilities} may contain location-sensitive information.
+         * Prior to Android 12, this information is always returned to apps holding the appropriate
+         * permission, possibly noting that the app has used location.
+         * <p>In Android 12 and above, by default the sent objects do not contain any location
+         * information, even if the app holds the necessary permissions, and the system does not
+         * take note of location usage by the app. Apps can request that location information is
+         * included, in which case the system will check location permission and the location
+         * toggle state, and take note of location usage by the app if any such information is
+         * returned.
+         *
          * Use this flag to include any location sensitive data in {@link NetworkCapabilities} sent
          * via {@link #onCapabilitiesChanged(Network, NetworkCapabilities)}.
          * <p>
@@ -3564,8 +3638,7 @@
          * <li> Retrieving this location sensitive information (subject to app's location
          * permissions) will be noted by system. </li>
          * <li> Without this flag any {@link NetworkCapabilities} provided via the callback does
-         * not include location sensitive info.
-         * </p>
+         * not include location sensitive information.
          */
         // Note: Some existing fields which are location sensitive may still be included without
         // this flag if the app targets SDK < S (to maintain backwards compatibility).
diff --git a/framework/src/android/net/IConnectivityManager.aidl b/framework/src/android/net/IConnectivityManager.aidl
index df4663f..23a3850 100644
--- a/framework/src/android/net/IConnectivityManager.aidl
+++ b/framework/src/android/net/IConnectivityManager.aidl
@@ -76,10 +76,15 @@
     LinkProperties getActiveLinkProperties();
     LinkProperties getLinkPropertiesForType(int networkType);
     LinkProperties getLinkProperties(in Network network);
+    LinkProperties redactLinkPropertiesForPackage(in LinkProperties lp, int uid, String packageName,
+            String callingAttributionTag);
 
     NetworkCapabilities getNetworkCapabilities(in Network network, String callingPackageName,
             String callingAttributionTag);
 
+    NetworkCapabilities redactNetworkCapabilitiesForPackage(in NetworkCapabilities nc, int uid,
+            String callingPackageName, String callingAttributionTag);
+
     @UnsupportedAppUsage(maxTargetSdk = 30, trackingBug = 170729553)
     NetworkState[] getAllNetworkState();
 
diff --git a/service/src/com/android/server/ConnectivityService.java b/service/src/com/android/server/ConnectivityService.java
index 2b70934..6944ca9 100644
--- a/service/src/com/android/server/ConnectivityService.java
+++ b/service/src/com/android/server/ConnectivityService.java
@@ -2081,6 +2081,19 @@
         }
     }
 
+    @Override
+    @Nullable
+    public LinkProperties redactLinkPropertiesForPackage(@NonNull LinkProperties lp, int uid,
+            @NonNull String packageName, @Nullable String callingAttributionTag) {
+        Objects.requireNonNull(packageName);
+        Objects.requireNonNull(lp);
+        enforceNetworkStackOrSettingsPermission();
+        if (!checkAccessPermission(-1 /* pid */, uid)) {
+            return null;
+        }
+        return linkPropertiesRestrictedForCallerPermissions(lp, -1 /* callerPid */, uid);
+    }
+
     private NetworkCapabilities getNetworkCapabilitiesInternal(Network network) {
         return getNetworkCapabilitiesInternal(getNetworkAgentInfoForNetwork(network));
     }
@@ -2104,13 +2117,34 @@
                 getCallingPid(), mDeps.getCallingUid(), callingPackageName, callingAttributionTag);
     }
 
+    @Override
+    public NetworkCapabilities redactNetworkCapabilitiesForPackage(@NonNull NetworkCapabilities nc,
+            int uid, @NonNull String packageName, @Nullable String callingAttributionTag) {
+        Objects.requireNonNull(nc);
+        Objects.requireNonNull(packageName);
+        enforceNetworkStackOrSettingsPermission();
+        if (!checkAccessPermission(-1 /* pid */, uid)) {
+            return null;
+        }
+        return createWithLocationInfoSanitizedIfNecessaryWhenParceled(
+                networkCapabilitiesRestrictedForCallerPermissions(nc, -1 /* callerPid */, uid),
+                true /* includeLocationSensitiveInfo */, -1 /* callingPid */, uid, packageName,
+                callingAttributionTag);
+    }
+
     @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 (!checkSettingsPermission(callerPid, callerUid)) {
             newNc.setUids(null);
             newNc.setSSID(null);
+            // TODO: Processes holding NETWORK_FACTORY should be able to see the underlying networks
             newNc.setUnderlyingNetworks(null);
         }
         if (newNc.getNetworkSpecifier() != null) {
@@ -2128,7 +2162,7 @@
 
     /**
      * Wrapper used to cache the permission check results performed for the corresponding
-     * app. This avoid performing multiple permission checks for different fields in
+     * app. This avoids performing multiple permission checks for different fields in
      * NetworkCapabilities.
      * Note: This wrapper does not support any sort of invalidation and thus must not be
      * persistent or long-lived. It may only be used for the time necessary to
@@ -2256,6 +2290,8 @@
                 includeLocationSensitiveInfo);
         final NetworkCapabilities newNc = new NetworkCapabilities(nc, redactions);
         // 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
         if (callingUid != nc.getOwnerUid()) {
             newNc.setOwnerUid(INVALID_UID);
             return newNc;
@@ -2281,9 +2317,15 @@
         return newNc;
     }
 
+    @NonNull
     private LinkProperties linkPropertiesRestrictedForCallerPermissions(
             LinkProperties lp, int callerPid, int callerUid) {
         if (lp == null) return new LinkProperties();
+        // 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 LP 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.
 
         // Only do a permission check if sanitization is needed, to avoid unnecessary binder calls.
         final boolean needsSanitization =
@@ -2654,6 +2696,11 @@
                 "ConnectivityService");
     }
 
+    private boolean checkAccessPermission(int pid, int uid) {
+        return mContext.checkPermission(android.Manifest.permission.ACCESS_NETWORK_STATE, pid, uid)
+                == PERMISSION_GRANTED;
+    }
+
     /**
      * Performs a strict and comprehensive check of whether a calling package is allowed to
      * change the state of network, as the condition differs for pre-M, M+, and
diff --git a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
index 53e4ab7..ea64252 100644
--- a/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
+++ b/tests/cts/net/src/android/net/cts/ConnectivityManagerTest.java
@@ -16,9 +16,15 @@
 
 package android.net.cts;
 
+import static android.Manifest.permission.ACCESS_COARSE_LOCATION;
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.ACCESS_NETWORK_STATE;
 import static android.Manifest.permission.CONNECTIVITY_INTERNAL;
 import static android.Manifest.permission.CONNECTIVITY_USE_RESTRICTED_NETWORKS;
+import static android.Manifest.permission.NETWORK_FACTORY;
 import static android.Manifest.permission.NETWORK_SETTINGS;
+import static android.Manifest.permission.NETWORK_SETUP_WIZARD;
+import static android.Manifest.permission.NETWORK_STACK;
 import static android.Manifest.permission.READ_DEVICE_CONFIG;
 import static android.content.pm.PackageManager.FEATURE_BLUETOOTH;
 import static android.content.pm.PackageManager.FEATURE_ETHERNET;
@@ -54,7 +60,9 @@
 import static android.net.NetworkCapabilities.NET_CAPABILITY_PARTIAL_CONNECTIVITY;
 import static android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED;
 import static android.net.NetworkCapabilities.TRANSPORT_TEST;
+import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
 import static android.net.cts.util.CtsNetUtils.ConnectivityActionReceiver;
 import static android.net.cts.util.CtsNetUtils.HTTP_PORT;
 import static android.net.cts.util.CtsNetUtils.NETWORK_CALLBACK_ACTION;
@@ -64,6 +72,7 @@
 import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL;
 import static android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL;
 import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.os.Process.INVALID_UID;
 import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
 import static android.system.OsConstants.AF_INET;
 import static android.system.OsConstants.AF_INET6;
@@ -76,6 +85,7 @@
 import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_LOCKDOWN_VPN;
 import static com.android.networkstack.apishim.ConstantsShim.BLOCKED_REASON_NONE;
 import static com.android.testutils.Cleanup.testAndCleanup;
+import static com.android.testutils.DevSdkIgnoreRuleKt.SC_V2;
 import static com.android.testutils.MiscAsserts.assertThrows;
 import static com.android.testutils.TestNetworkTrackerKt.initTestNetwork;
 import static com.android.testutils.TestPermissionUtil.runAsShell;
@@ -103,6 +113,7 @@
 import android.content.pm.PackageInfo;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
+import android.net.CaptivePortalData;
 import android.net.ConnectivityManager;
 import android.net.ConnectivityManager.NetworkCallback;
 import android.net.ConnectivitySettingsManager;
@@ -132,6 +143,7 @@
 import android.net.cts.util.CtsNetUtils;
 import android.net.cts.util.CtsTetheringUtils;
 import android.net.util.KeepaliveUtils;
+import android.net.wifi.WifiInfo;
 import android.net.wifi.WifiManager;
 import android.os.Binder;
 import android.os.Build;
@@ -160,6 +172,7 @@
 
 import com.android.internal.util.ArrayUtils;
 import com.android.modules.utils.build.SdkLevel;
+import com.android.net.module.util.CollectionUtils;
 import com.android.networkstack.apishim.ConnectivityManagerShimImpl;
 import com.android.networkstack.apishim.ConstantsShim;
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
@@ -554,6 +567,223 @@
         }
     }
 
+    private boolean checkPermission(String perm, int uid) {
+        return mContext.checkPermission(perm, -1 /* pid */, uid) == PERMISSION_GRANTED;
+    }
+
+    private String findPackageByPermissions(@NonNull List<String> requiredPermissions,
+                @NonNull List<String> forbiddenPermissions) throws Exception {
+        final List<PackageInfo> packageInfos =
+                mPackageManager.getInstalledPackages(GET_PERMISSIONS);
+        for (PackageInfo packageInfo : packageInfos) {
+            final int uid = mPackageManager.getPackageUid(packageInfo.packageName, 0 /* flags */);
+            if (!CollectionUtils.all(requiredPermissions, perm -> checkPermission(perm, uid))) {
+                continue;
+            }
+            if (CollectionUtils.any(forbiddenPermissions, perm -> checkPermission(perm, uid))) {
+                continue;
+            }
+
+            return packageInfo.packageName;
+        }
+        return null;
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @Test
+    public void testRedactLinkPropertiesForPackage() throws Exception {
+        final String groundedPkg = findPackageByPermissions(
+                List.of(), /* requiredPermissions */
+                List.of(ACCESS_NETWORK_STATE) /* forbiddenPermissions */);
+        assertNotNull("Couldn't find any package without ACCESS_NETWORK_STATE", groundedPkg);
+        final int groundedUid = mPackageManager.getPackageUid(groundedPkg, 0 /* flags */);
+
+        final String normalPkg = findPackageByPermissions(
+                List.of(ACCESS_NETWORK_STATE) /* requiredPermissions */,
+                List.of(NETWORK_SETTINGS, NETWORK_STACK,
+                        PERMISSION_MAINLINE_NETWORK_STACK) /* forbiddenPermissions */);
+        assertNotNull("Couldn't find any package with ACCESS_NETWORK_STATE but"
+                + " without NETWORK_SETTINGS", normalPkg);
+        final int normalUid = mPackageManager.getPackageUid(normalPkg, 0 /* flags */);
+
+        // There are some privileged packages on the system, like the phone process, the network
+        // stack and the system server.
+        final String privilegedPkg = findPackageByPermissions(
+                List.of(ACCESS_NETWORK_STATE, NETWORK_SETTINGS), /* requiredPermissions */
+                List.of() /* forbiddenPermissions */);
+        assertNotNull("Couldn't find a package with sufficient permissions", privilegedPkg);
+        final int privilegedUid = mPackageManager.getPackageUid(privilegedPkg, 0);
+
+        // Set parcelSensitiveFields to true to preserve CaptivePortalApiUrl & CaptivePortalData
+        // when parceling.
+        final LinkProperties lp = new LinkProperties(new LinkProperties(),
+                true /* parcelSensitiveFields */);
+        final Uri capportUrl = Uri.parse("https://capport.example.com/api");
+        final CaptivePortalData capportData = new CaptivePortalData.Builder().build();
+        final int mtu = 12345;
+        lp.setMtu(mtu);
+        lp.setCaptivePortalApiUrl(capportUrl);
+        lp.setCaptivePortalData(capportData);
+
+        // No matter what the given uid is, a SecurityException will be thrown if the caller
+        // doesn't hold the NETWORK_SETTINGS permission.
+        assertThrows(SecurityException.class,
+                () -> mCm.redactLinkPropertiesForPackage(lp, groundedUid, groundedPkg));
+        assertThrows(SecurityException.class,
+                () -> mCm.redactLinkPropertiesForPackage(lp, normalUid, normalPkg));
+        assertThrows(SecurityException.class,
+                () -> mCm.redactLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg));
+
+        runAsShell(NETWORK_SETTINGS, () -> {
+            // No matter what the given uid is, if the given LinkProperties is null, then
+            // NullPointerException will be thrown.
+            assertThrows(NullPointerException.class,
+                    () -> mCm.redactLinkPropertiesForPackage(null, groundedUid, groundedPkg));
+            assertThrows(NullPointerException.class,
+                    () -> mCm.redactLinkPropertiesForPackage(null, normalUid, normalPkg));
+            assertThrows(NullPointerException.class,
+                    () -> mCm.redactLinkPropertiesForPackage(null, privilegedUid, privilegedPkg));
+
+            // Make sure null is returned for a UID without ACCESS_NETWORK_STATE.
+            assertNull(mCm.redactLinkPropertiesForPackage(lp, groundedUid, groundedPkg));
+
+            // CaptivePortalApiUrl & CaptivePortalData will be set to null if given uid doesn't hold
+            // the NETWORK_SETTINGS permission.
+            assertNull(mCm.redactLinkPropertiesForPackage(lp, normalUid, normalPkg)
+                    .getCaptivePortalApiUrl());
+            assertNull(mCm.redactLinkPropertiesForPackage(lp, normalUid, normalPkg)
+                    .getCaptivePortalData());
+            // MTU is not sensitive and is not redacted.
+            assertEquals(mtu, mCm.redactLinkPropertiesForPackage(lp, normalUid, normalPkg)
+                    .getMtu());
+
+            // CaptivePortalApiUrl & CaptivePortalData will be preserved if the given uid holds the
+            // NETWORK_SETTINGS permission.
+            assertEquals(capportUrl,
+                    mCm.redactLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
+                            .getCaptivePortalApiUrl());
+            assertEquals(capportData,
+                    mCm.redactLinkPropertiesForPackage(lp, privilegedUid, privilegedPkg)
+                            .getCaptivePortalData());
+        });
+    }
+
+    private NetworkCapabilities redactNc(@NonNull final NetworkCapabilities nc, int uid,
+            @NonNull String packageName) {
+        return mCm.redactNetworkCapabilitiesForPackage(nc, uid, packageName);
+    }
+
+    @DevSdkIgnoreRule.IgnoreUpTo(SC_V2)
+    @Test
+    public void testRedactNetworkCapabilitiesForPackage() throws Exception {
+        final String groundedPkg = findPackageByPermissions(
+                List.of(), /* requiredPermissions */
+                List.of(ACCESS_NETWORK_STATE) /* forbiddenPermissions */);
+        assertNotNull("Couldn't find any package without ACCESS_NETWORK_STATE", groundedPkg);
+        final int groundedUid = mPackageManager.getPackageUid(groundedPkg, 0 /* flags */);
+
+        // A package which doesn't have any of the permissions below, but has NETWORK_STATE.
+        // There should be a number of packages like this on the device; AOSP has many,
+        // including contacts, webview, the keyboard, pacprocessor, messaging.
+        final String normalPkg = findPackageByPermissions(
+                List.of(ACCESS_NETWORK_STATE) /* requiredPermissions */,
+                List.of(NETWORK_SETTINGS, NETWORK_FACTORY, NETWORK_SETUP_WIZARD,
+                        NETWORK_STACK, PERMISSION_MAINLINE_NETWORK_STACK,
+                        ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION) /* forbiddenPermissions */);
+        assertNotNull("Can't find a package with ACCESS_NETWORK_STATE but without any of"
+                + " the forbidden permissions", normalPkg);
+        final int normalUid = mPackageManager.getPackageUid(normalPkg, 0 /* flags */);
+
+        // There are some privileged packages on the system, like the phone process, the network
+        // stack and the system server.
+        final String privilegedPkg = findPackageByPermissions(
+                List.of(ACCESS_NETWORK_STATE, NETWORK_SETTINGS, NETWORK_FACTORY,
+                        ACCESS_FINE_LOCATION), /* requiredPermissions */
+                List.of() /* forbiddenPermissions */);
+        assertNotNull("Couldn't find a package with sufficient permissions", privilegedPkg);
+        final int privilegedUid = mPackageManager.getPackageUid(privilegedPkg, 0);
+
+        final Set<Range<Integer>> uids = new ArraySet<>();
+        uids.add(new Range<>(10000, 10100));
+        uids.add(new Range<>(10200, 10300));
+        final String ssid = "My-WiFi";
+        // This test will set underlying networks in the capabilities to redact to see if they
+        // are appropriately redacted, so fetch the default network to put in there as an example.
+        final Network defaultNetwork = mCm.getActiveNetwork();
+        assertNotNull("CTS requires a working Internet connection", defaultNetwork);
+        final int subId1 = 1;
+        final int subId2 = 2;
+        final int[] administratorUids = {normalUid};
+        final String bssid = "location sensitive";
+        final int rssi = 43; // not location sensitive
+        final WifiInfo wifiInfo = new WifiInfo.Builder()
+                .setBssid(bssid)
+                .setRssi(rssi)
+                .build();
+        final NetworkCapabilities nc = new NetworkCapabilities.Builder()
+                .setUids(uids)
+                .setSsid(ssid)
+                .setUnderlyingNetworks(List.of(defaultNetwork))
+                .setSubscriptionIds(Set.of(subId1, subId2))
+                .setAdministratorUids(administratorUids)
+                .setOwnerUid(normalUid)
+                .setTransportInfo(wifiInfo)
+                .build();
+
+        // No matter what the given uid is, a SecurityException will be thrown if the caller
+        // doesn't hold the NETWORK_SETTINGS permission.
+        assertThrows(SecurityException.class, () -> redactNc(nc, groundedUid, groundedPkg));
+        assertThrows(SecurityException.class, () -> redactNc(nc, normalUid, normalPkg));
+        assertThrows(SecurityException.class, () -> redactNc(nc, privilegedUid, privilegedPkg));
+
+        runAsShell(NETWORK_SETTINGS, () -> {
+            // Make sure that the NC is null if the package doesn't hold ACCESS_NETWORK_STATE.
+            assertNull(redactNc(nc, groundedUid, groundedPkg));
+
+            // Uids, ssid, underlying networks & subscriptionIds will be redacted if the given uid
+            // doesn't hold the associated permissions. The wifi transport info is also suitably
+            // redacted.
+            final NetworkCapabilities redactedNormal = redactNc(nc, normalUid, normalPkg);
+            assertNull(redactedNormal.getUids());
+            assertNull(redactedNormal.getSsid());
+            assertNull(redactedNormal.getUnderlyingNetworks());
+            assertEquals(0, redactedNormal.getSubscriptionIds().size());
+            assertEquals(WifiInfo.DEFAULT_MAC_ADDRESS,
+                    ((WifiInfo) redactedNormal.getTransportInfo()).getBSSID());
+            assertEquals(rssi, ((WifiInfo) redactedNormal.getTransportInfo()).getRssi());
+
+            // Uids, ssid, underlying networks & subscriptionIds will be preserved if the given uid
+            // holds the associated permissions.
+            final NetworkCapabilities redactedPrivileged =
+                    redactNc(nc, privilegedUid, privilegedPkg);
+            assertEquals(uids, redactedPrivileged.getUids());
+            assertEquals(ssid, redactedPrivileged.getSsid());
+            assertEquals(List.of(defaultNetwork), redactedPrivileged.getUnderlyingNetworks());
+            assertEquals(Set.of(subId1, subId2), redactedPrivileged.getSubscriptionIds());
+            assertEquals(bssid, ((WifiInfo) redactedPrivileged.getTransportInfo()).getBSSID());
+            assertEquals(rssi, ((WifiInfo) redactedPrivileged.getTransportInfo()).getRssi());
+
+            // The owner uid is only preserved when the network is a VPN and the uid is the
+            // same as the owner uid.
+            nc.addTransportType(TRANSPORT_VPN);
+            assertEquals(normalUid, redactNc(nc, normalUid, normalPkg).getOwnerUid());
+            assertEquals(INVALID_UID, redactNc(nc, privilegedUid, privilegedPkg).getOwnerUid());
+            nc.removeTransportType(TRANSPORT_VPN);
+
+            // If the given uid doesn't hold location permissions, the owner uid will be set to
+            // INVALID_UID even when sent to that UID (this avoids a wifi suggestor knowing where
+            // the device is by virtue of the device connecting to its own network).
+            assertEquals(INVALID_UID, redactNc(nc, normalUid, normalPkg).getOwnerUid());
+
+            // If the given uid holds location permissions, the owner uid is preserved. This works
+            // because the shell holds ACCESS_FINE_LOCATION.
+            final int[] administratorUids2 = { privilegedUid };
+            nc.setAdministratorUids(administratorUids2);
+            nc.setOwnerUid(privilegedUid);
+            assertEquals(privilegedUid, redactNc(nc, privilegedUid, privilegedPkg).getOwnerUid());
+        });
+    }
+
     /**
      * Tests that connections can be opened on WiFi and cellphone networks,
      * and that they are made from different IP addresses.