Merge changes I446a8595,I68d2293f am: 29044acc20 am: d7ab8fec8c

Change-Id: I27a4344fa2e3b91fe31e02bf41f20a2bc11c2ff6
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 3e9e7fa..b050e47 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -120,6 +120,14 @@
 
     ParcelFileDescriptor establishVpn(in VpnConfig config);
 
+    boolean provisionVpnProfile(in VpnProfile profile, String packageName);
+
+    void deleteVpnProfile(String packageName);
+
+    void startVpnProfile(String packageName);
+
+    void stopVpnProfile(String packageName);
+
     VpnConfig getVpnConfig(int userId);
 
     @UnsupportedAppUsage
diff --git a/core/java/android/net/VpnManager.java b/core/java/android/net/VpnManager.java
index f95807a..e60cc81 100644
--- a/core/java/android/net/VpnManager.java
+++ b/core/java/android/net/VpnManager.java
@@ -20,8 +20,17 @@
 
 import android.annotation.NonNull;
 import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
+import android.content.res.Resources;
+import android.os.RemoteException;
+
+import com.android.internal.net.VpnProfile;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
 
 /**
  * This class provides an interface for apps to manage platform VPN profiles
@@ -41,6 +50,15 @@
     @NonNull private final Context mContext;
     @NonNull private final IConnectivityManager mService;
 
+    private static Intent getIntentForConfirmation() {
+        final Intent intent = new Intent();
+        final ComponentName componentName = ComponentName.unflattenFromString(
+                Resources.getSystem().getString(
+                        com.android.internal.R.string.config_customVpnConfirmDialogComponent));
+        intent.setComponent(componentName);
+        return intent;
+    }
+
     /**
      * Create an instance of the VpnManger with the given context.
      *
@@ -57,18 +75,49 @@
     /**
      * Install a VpnProfile configuration keyed on the calling app's package name.
      *
-     * @param profile the PlatformVpnProfile provided by this package. Will override any previous
-     *     PlatformVpnProfile stored for this package.
-     * @return an intent to request user consent if needed (null otherwise).
+     * <p>This method returns {@code null} if user consent has already been granted, or an {@link
+     * Intent} to a system activity. If an intent is returned, the application should launch the
+     * activity using {@link Activity#startActivityForResult} to request user consent. The activity
+     * may pop up a dialog to require user action, and the result will come back via its {@link
+     * Activity#onActivityResult}. If the result is {@link Activity#RESULT_OK}, the user has
+     * consented, and the VPN profile can be started.
+     *
+     * @param profile the VpnProfile provided by this package. Will override any previous VpnProfile
+     *     stored for this package.
+     * @return an Intent requesting user consent to start the VPN, or null if consent is not
+     *     required based on privileges or previous user consent.
      */
     @Nullable
     public Intent provisionVpnProfile(@NonNull PlatformVpnProfile profile) {
-        throw new UnsupportedOperationException("Not yet implemented");
+        final VpnProfile internalProfile;
+
+        try {
+            internalProfile = profile.toVpnProfile();
+        } catch (GeneralSecurityException | IOException e) {
+            // Conversion to VpnProfile failed; this is an invalid profile. Both of these exceptions
+            // indicate a failure to convert a PrivateKey or X509Certificate to a Base64 encoded
+            // string as required by the VpnProfile.
+            throw new IllegalArgumentException("Failed to serialize PlatformVpnProfile", e);
+        }
+
+        try {
+            // Profile can never be null; it either gets set, or an exception is thrown.
+            if (mService.provisionVpnProfile(internalProfile, mContext.getOpPackageName())) {
+                return null;
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        return getIntentForConfirmation();
     }
 
     /** Delete the VPN profile configuration that was provisioned by the calling app */
     public void deleteProvisionedVpnProfile() {
-        throw new UnsupportedOperationException("Not yet implemented");
+        try {
+            mService.deleteVpnProfile(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /**
@@ -78,11 +127,19 @@
      *     setup, or if user consent has not been granted
      */
     public void startProvisionedVpnProfile() {
-        throw new UnsupportedOperationException("Not yet implemented");
+        try {
+            mService.startVpnProfile(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 
     /** Tear down the VPN provided by the calling app (if any) */
     public void stopProvisionedVpnProfile() {
-        throw new UnsupportedOperationException("Not yet implemented");
+        try {
+            mService.stopVpnProfile(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
     }
 }
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 51f6ab0..4b9925f 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -4310,7 +4310,7 @@
             throwIfLockdownEnabled();
             Vpn vpn = mVpns.get(userId);
             if (vpn != null) {
-                return vpn.prepare(oldPackage, newPackage);
+                return vpn.prepare(oldPackage, newPackage, false);
             } else {
                 return false;
             }
@@ -4359,6 +4359,78 @@
     }
 
     /**
+     * Stores the given VPN profile based on the provisioning package name.
+     *
+     * <p>If there is already a VPN profile stored for the provisioning package, this call will
+     * overwrite the profile.
+     *
+     * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed
+     * exclusively by the Settings app, and passed into the platform at startup time.
+     *
+     * @return {@code true} if user consent has already been granted, {@code false} otherwise.
+     * @hide
+     */
+    @Override
+    public boolean provisionVpnProfile(@NonNull VpnProfile profile, @NonNull String packageName) {
+        final int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized (mVpns) {
+            return mVpns.get(user).provisionVpnProfile(packageName, profile, mKeyStore);
+        }
+    }
+
+    /**
+     * Deletes the stored VPN profile for the provisioning package
+     *
+     * <p>If there are no profiles for the given package, this method will silently succeed.
+     *
+     * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed
+     * exclusively by the Settings app, and passed into the platform at startup time.
+     *
+     * @hide
+     */
+    @Override
+    public void deleteVpnProfile(@NonNull String packageName) {
+        final int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized (mVpns) {
+            mVpns.get(user).deleteVpnProfile(packageName, mKeyStore);
+        }
+    }
+
+    /**
+     * Starts the VPN based on the stored profile for the given package
+     *
+     * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed
+     * exclusively by the Settings app, and passed into the platform at startup time.
+     *
+     * @throws IllegalArgumentException if no profile was found for the given package name.
+     * @hide
+     */
+    @Override
+    public void startVpnProfile(@NonNull String packageName) {
+        final int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized (mVpns) {
+            throwIfLockdownEnabled();
+            mVpns.get(user).startVpnProfile(packageName, mKeyStore);
+        }
+    }
+
+    /**
+     * Stops the Platform VPN if the provided package is running one.
+     *
+     * <p>This is designed to serve the VpnManager only; settings-based VPN profiles are managed
+     * exclusively by the Settings app, and passed into the platform at startup time.
+     *
+     * @hide
+     */
+    @Override
+    public void stopVpnProfile(@NonNull String packageName) {
+        final int user = UserHandle.getUserId(Binder.getCallingUid());
+        synchronized (mVpns) {
+            mVpns.get(user).stopVpnProfile(packageName);
+        }
+    }
+
+    /**
      * Start legacy VPN, controlling native daemons as needed. Creates a
      * secondary thread to perform connection work, returning quickly.
      */
@@ -4561,6 +4633,13 @@
         }
     }
 
+    /**
+     * Throws if there is any currently running, always-on Legacy VPN.
+     *
+     * <p>The LockdownVpnTracker and mLockdownEnabled both track whether an always-on Legacy VPN is
+     * running across the entire system. Tracking for app-based VPNs is done on a per-user,
+     * per-package basis in Vpn.java
+     */
     @GuardedBy("mVpns")
     private void throwIfLockdownEnabled() {
         if (mLockdownEnabled) {
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 2933fab..eabc083 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -24,6 +24,8 @@
 import static android.net.RouteInfo.RTN_THROW;
 import static android.net.RouteInfo.RTN_UNREACHABLE;
 
+import static com.android.internal.util.Preconditions.checkNotNull;
+
 import android.Manifest;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
@@ -157,6 +159,16 @@
     // is actually O(n²)+O(n²).
     private static final int MAX_ROUTES_TO_EVALUATE = 150;
 
+    /**
+     * Largest profile size allowable for Platform VPNs.
+     *
+     * <p>The largest platform VPN profiles use IKEv2 RSA Certificate Authentication and have two
+     * X509Certificates, and one RSAPrivateKey. This should lead to a max size of 2x 12kB for the
+     * certificates, plus a reasonable upper bound on the private key of 32kB. The rest of the
+     * profile is expected to be negligible in size.
+     */
+    @VisibleForTesting static final int MAX_VPN_PROFILE_SIZE_BYTES = 1 << 17; // 128kB
+
     // TODO: create separate trackers for each unique VPN to support
     // automated reconnection
 
@@ -656,6 +668,11 @@
      * It uses {@link VpnConfig#LEGACY_VPN} as its package name, and
      * it can be revoked by itself.
      *
+     * The permission checks to verify that the VPN has already been granted
+     * user consent are dependent on the type of the VPN being prepared. See
+     * {@link AppOpsManager#OP_ACTIVATE_VPN} and {@link
+     * AppOpsManager#OP_ACTIVATE_PLATFORM_VPN} for more information.
+     *
      * Note: when we added VPN pre-consent in
      * https://android.googlesource.com/platform/frameworks/base/+/0554260
      * the names oldPackage and newPackage became misleading, because when
@@ -674,10 +691,13 @@
      *
      * @param oldPackage The package name of the old VPN application
      * @param newPackage The package name of the new VPN application
-     *
+     * @param isPlatformVpn Whether the package being prepared is using a platform VPN profile.
+     *     Preparing a platform VPN profile requires only the lesser ACTIVATE_PLATFORM_VPN appop.
      * @return true if the operation succeeded.
      */
-    public synchronized boolean prepare(String oldPackage, String newPackage) {
+    // TODO: Use an Intdef'd type to represent what kind of VPN the caller is preparing.
+    public synchronized boolean prepare(
+            String oldPackage, String newPackage, boolean isPlatformVpn) {
         if (oldPackage != null) {
             // Stop an existing always-on VPN from being dethroned by other apps.
             if (mAlwaysOn && !isCurrentPreparedPackage(oldPackage)) {
@@ -688,13 +708,14 @@
             if (!isCurrentPreparedPackage(oldPackage)) {
                 // The package doesn't match. We return false (to obtain user consent) unless the
                 // user has already consented to that VPN package.
-                if (!oldPackage.equals(VpnConfig.LEGACY_VPN) && isVpnUserPreConsented(oldPackage)) {
+                if (!oldPackage.equals(VpnConfig.LEGACY_VPN)
+                        && isVpnPreConsented(mContext, oldPackage, isPlatformVpn)) {
                     prepareInternal(oldPackage);
                     return true;
                 }
                 return false;
             } else if (!oldPackage.equals(VpnConfig.LEGACY_VPN)
-                    && !isVpnUserPreConsented(oldPackage)) {
+                    && !isVpnPreConsented(mContext, oldPackage, isPlatformVpn)) {
                 // Currently prepared VPN is revoked, so unprepare it and return false.
                 prepareInternal(VpnConfig.LEGACY_VPN);
                 return false;
@@ -805,13 +826,29 @@
         return false;
     }
 
-    private boolean isVpnUserPreConsented(String packageName) {
-        AppOpsManager appOps =
-                (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
+    private static boolean isVpnPreConsented(
+            Context context, String packageName, boolean isPlatformVpn) {
+        return isPlatformVpn
+                ? isVpnProfilePreConsented(context, packageName)
+                : isVpnServicePreConsented(context, packageName);
+    }
 
-        // Verify that the caller matches the given package and has permission to activate VPNs.
-        return appOps.noteOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, Binder.getCallingUid(),
-                packageName) == AppOpsManager.MODE_ALLOWED;
+    private static boolean doesPackageHaveAppop(Context context, String packageName, int appop) {
+        final AppOpsManager appOps =
+                (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
+
+        // Verify that the caller matches the given package and has the required permission.
+        return appOps.noteOpNoThrow(appop, Binder.getCallingUid(), packageName)
+                == AppOpsManager.MODE_ALLOWED;
+    }
+
+    private static boolean isVpnServicePreConsented(Context context, String packageName) {
+        return doesPackageHaveAppop(context, packageName, AppOpsManager.OP_ACTIVATE_VPN);
+    }
+
+    private static boolean isVpnProfilePreConsented(Context context, String packageName) {
+        return doesPackageHaveAppop(context, packageName, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN)
+                || isVpnServicePreConsented(context, packageName);
     }
 
     private int getAppUid(String app, int userHandle) {
@@ -1001,6 +1038,9 @@
      * Establish a VPN network and return the file descriptor of the VPN interface. This methods
      * returns {@code null} if the application is revoked or not prepared.
      *
+     * <p>This method supports ONLY VpnService-based VPNs. For Platform VPNs, see {@link
+     * provisionVpnProfile} and {@link startVpnProfile}
+     *
      * @param config The parameters to configure the network.
      * @return The file descriptor of the VPN interface.
      */
@@ -1011,7 +1051,7 @@
             return null;
         }
         // Check to ensure consent hasn't been revoked since we were prepared.
-        if (!isVpnUserPreConsented(mPackage)) {
+        if (!isVpnServicePreConsented(mContext, mPackage)) {
             return null;
         }
         // Check if the service is properly declared.
@@ -1676,6 +1716,10 @@
         public int settingsSecureGetIntForUser(String key, int def, int userId) {
             return Settings.Secure.getIntForUser(mContext.getContentResolver(), key, def, userId);
         }
+
+        public boolean isCallerSystem() {
+            return Binder.getCallingUid() == Process.SYSTEM_UID;
+        }
     }
 
     private native int jniCreate(int mtu);
@@ -2224,4 +2268,148 @@
             }
         }
     }
+
+    private void verifyCallingUidAndPackage(String packageName) {
+        if (getAppUid(packageName, mUserHandle) != Binder.getCallingUid()) {
+            throw new SecurityException("Mismatched package and UID");
+        }
+    }
+
+    @VisibleForTesting
+    String getProfileNameForPackage(String packageName) {
+        return Credentials.PLATFORM_VPN + mUserHandle + "_" + packageName;
+    }
+
+    /**
+     * Stores an app-provisioned VPN profile and returns whether the app is already prepared.
+     *
+     * @param packageName the package name of the app provisioning this profile
+     * @param profile the profile to be stored and provisioned
+     * @param keyStore the System keystore instance to save VPN profiles
+     * @returns whether or not the app has already been granted user consent
+     */
+    public synchronized boolean provisionVpnProfile(
+            @NonNull String packageName, @NonNull VpnProfile profile, @NonNull KeyStore keyStore) {
+        checkNotNull(packageName, "No package name provided");
+        checkNotNull(profile, "No profile provided");
+        checkNotNull(keyStore, "KeyStore missing");
+
+        verifyCallingUidAndPackage(packageName);
+
+        final byte[] encodedProfile = profile.encode();
+        if (encodedProfile.length > MAX_VPN_PROFILE_SIZE_BYTES) {
+            throw new IllegalArgumentException("Profile too big");
+        }
+
+        // Permissions checked during startVpnProfile()
+        Binder.withCleanCallingIdentity(
+                () -> {
+                    keyStore.put(
+                            getProfileNameForPackage(packageName),
+                            encodedProfile,
+                            Process.SYSTEM_UID,
+                            0 /* flags */);
+                });
+
+        // TODO: if package has CONTROL_VPN, grant the ACTIVATE_PLATFORM_VPN appop.
+        // This mirrors the prepareAndAuthorize that is used by VpnService.
+
+        // Return whether the app is already pre-consented
+        return isVpnProfilePreConsented(mContext, packageName);
+    }
+
+    /**
+     * Deletes an app-provisioned VPN profile.
+     *
+     * @param packageName the package name of the app provisioning this profile
+     * @param keyStore the System keystore instance to save VPN profiles
+     */
+    public synchronized void deleteVpnProfile(
+            @NonNull String packageName, @NonNull KeyStore keyStore) {
+        checkNotNull(packageName, "No package name provided");
+        checkNotNull(keyStore, "KeyStore missing");
+
+        verifyCallingUidAndPackage(packageName);
+
+        Binder.withCleanCallingIdentity(
+                () -> {
+                    keyStore.delete(getProfileNameForPackage(packageName), Process.SYSTEM_UID);
+                });
+    }
+
+    /**
+     * Retrieves the VpnProfile.
+     *
+     * <p>Must be used only as SYSTEM_UID, otherwise the key/UID pair will not match anything in the
+     * keystore.
+     */
+    @VisibleForTesting
+    @Nullable
+    VpnProfile getVpnProfilePrivileged(@NonNull String packageName, @NonNull KeyStore keyStore) {
+        if (!mSystemServices.isCallerSystem()) {
+            Log.wtf(TAG, "getVpnProfilePrivileged called as non-System UID ");
+            return null;
+        }
+
+        final byte[] encoded = keyStore.get(getProfileNameForPackage(packageName));
+        if (encoded == null) return null;
+
+        return VpnProfile.decode("" /* Key unused */, encoded);
+    }
+
+    /**
+     * Starts an already provisioned VPN Profile, keyed by package name.
+     *
+     * <p>This method is meant to be called by apps (via VpnManager and ConnectivityService).
+     * Privileged (system) callers should use startVpnProfilePrivileged instead. Otherwise the UIDs
+     * will not match during appop checks.
+     *
+     * @param packageName the package name of the app provisioning this profile
+     * @param keyStore the System keystore instance to retrieve VPN profiles
+     */
+    public synchronized void startVpnProfile(
+            @NonNull String packageName, @NonNull KeyStore keyStore) {
+        checkNotNull(packageName, "No package name provided");
+        checkNotNull(keyStore, "KeyStore missing");
+
+        // Prepare VPN for startup
+        if (!prepare(packageName, null /* newPackage */, true /* isPlatformVpn */)) {
+            throw new SecurityException("User consent not granted for package " + packageName);
+        }
+
+        Binder.withCleanCallingIdentity(
+                () -> {
+                    final VpnProfile profile = getVpnProfilePrivileged(packageName, keyStore);
+                    if (profile == null) {
+                        throw new IllegalArgumentException("No profile found for " + packageName);
+                    }
+
+                    startVpnProfilePrivileged(profile);
+                });
+    }
+
+    private void startVpnProfilePrivileged(@NonNull VpnProfile profile) {
+        // TODO: Start PlatformVpnRunner
+    }
+
+    /**
+     * Stops an already running VPN Profile for the given package.
+     *
+     * <p>This method is meant to be called by apps (via VpnManager and ConnectivityService).
+     * Privileged (system) callers should (re-)prepare the LEGACY_VPN instead.
+     *
+     * @param packageName the package name of the app provisioning this profile
+     */
+    public synchronized void stopVpnProfile(@NonNull String packageName) {
+        checkNotNull(packageName, "No package name provided");
+
+        // To stop the VPN profile, the caller must be the current prepared package. Otherwise,
+        // the app is not prepared, and we can just return.
+        if (!isCurrentPreparedPackage(packageName)) {
+            // TODO: Also check to make sure that the running VPN is a VPN profile.
+            return;
+        }
+
+        prepareInternal(VpnConfig.LEGACY_VPN);
+    }
 }
diff --git a/tests/net/java/android/net/VpnManagerTest.java b/tests/net/java/android/net/VpnManagerTest.java
index 655c4d1..97551c9 100644
--- a/tests/net/java/android/net/VpnManagerTest.java
+++ b/tests/net/java/android/net/VpnManagerTest.java
@@ -16,13 +16,21 @@
 
 package android.net;
 
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 
 import android.test.mock.MockContext;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import com.android.internal.net.VpnProfile;
+
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -31,7 +39,12 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class VpnManagerTest {
-    private static final String VPN_PROFILE_KEY = "KEY";
+    private static final String PKG_NAME = "fooPackage";
+
+    private static final String SESSION_NAME_STRING = "testSession";
+    private static final String SERVER_ADDR_STRING = "1.2.3.4";
+    private static final String IDENTITY_STRING = "Identity";
+    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
 
     private IConnectivityManager mMockCs;
     private VpnManager mVpnManager;
@@ -39,7 +52,7 @@
             new MockContext() {
                 @Override
                 public String getOpPackageName() {
-                    return "fooPackage";
+                    return PKG_NAME;
                 }
             };
 
@@ -50,34 +63,49 @@
     }
 
     @Test
-    public void testProvisionVpnProfile() throws Exception {
-        try {
-            mVpnManager.provisionVpnProfile(mock(PlatformVpnProfile.class));
-        } catch (UnsupportedOperationException expected) {
-        }
+    public void testProvisionVpnProfilePreconsented() throws Exception {
+        final PlatformVpnProfile profile = getPlatformVpnProfile();
+        when(mMockCs.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))).thenReturn(true);
+
+        // Expect there to be no intent returned, as consent has already been granted.
+        assertNull(mVpnManager.provisionVpnProfile(profile));
+        verify(mMockCs).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
+    }
+
+    @Test
+    public void testProvisionVpnProfileNeedsConsent() throws Exception {
+        final PlatformVpnProfile profile = getPlatformVpnProfile();
+        when(mMockCs.provisionVpnProfile(any(VpnProfile.class), eq(PKG_NAME))).thenReturn(false);
+
+        // Expect intent to be returned, as consent has not already been granted.
+        assertNotNull(mVpnManager.provisionVpnProfile(profile));
+        verify(mMockCs).provisionVpnProfile(eq(profile.toVpnProfile()), eq(PKG_NAME));
     }
 
     @Test
     public void testDeleteProvisionedVpnProfile() throws Exception {
-        try {
-            mVpnManager.deleteProvisionedVpnProfile();
-        } catch (UnsupportedOperationException expected) {
-        }
+        mVpnManager.deleteProvisionedVpnProfile();
+        verify(mMockCs).deleteVpnProfile(eq(PKG_NAME));
     }
 
     @Test
     public void testStartProvisionedVpnProfile() throws Exception {
-        try {
-            mVpnManager.startProvisionedVpnProfile();
-        } catch (UnsupportedOperationException expected) {
-        }
+        mVpnManager.startProvisionedVpnProfile();
+        verify(mMockCs).startVpnProfile(eq(PKG_NAME));
     }
 
     @Test
     public void testStopProvisionedVpnProfile() throws Exception {
-        try {
-            mVpnManager.stopProvisionedVpnProfile();
-        } catch (UnsupportedOperationException expected) {
-        }
+        mVpnManager.stopProvisionedVpnProfile();
+        verify(mMockCs).stopVpnProfile(eq(PKG_NAME));
+    }
+
+    private Ikev2VpnProfile getPlatformVpnProfile() throws Exception {
+        return new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING)
+                .setBypassable(true)
+                .setMaxMtu(1300)
+                .setMetered(true)
+                .setAuthPsk(PSK_BYTES)
+                .build();
     }
 }
diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java
index ce50bef..084ec73 100644
--- a/tests/net/java/com/android/server/connectivity/VpnTest.java
+++ b/tests/net/java/com/android/server/connectivity/VpnTest.java
@@ -28,11 +28,11 @@
 import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
 import static android.net.NetworkCapabilities.TRANSPORT_VPN;
 import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-import static android.net.RouteInfo.RTN_UNREACHABLE;
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
 import static org.mockito.AdditionalMatchers.aryEq;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.ArgumentMatchers.anyBoolean;
@@ -43,6 +43,7 @@
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.doNothing;
 import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
@@ -58,21 +59,20 @@
 import android.content.pm.UserInfo;
 import android.content.res.Resources;
 import android.net.ConnectivityManager;
-import android.net.IpPrefix;
-import android.net.LinkProperties;
 import android.net.Network;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo.DetailedState;
-import android.net.RouteInfo;
 import android.net.UidRange;
 import android.net.VpnService;
 import android.os.Build.VERSION_CODES;
 import android.os.Bundle;
 import android.os.INetworkManagementService;
 import android.os.Looper;
-import android.os.SystemClock;
+import android.os.Process;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.security.Credentials;
+import android.security.KeyStore;
 import android.util.ArrayMap;
 import android.util.ArraySet;
 
@@ -81,6 +81,7 @@
 
 import com.android.internal.R;
 import com.android.internal.net.VpnConfig;
+import com.android.internal.net.VpnProfile;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -90,9 +91,6 @@
 import org.mockito.Mock;
 import org.mockito.MockitoAnnotations;
 
-import java.net.Inet4Address;
-import java.net.Inet6Address;
-import java.net.UnknownHostException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -124,6 +122,8 @@
         managedProfileA.profileGroupId = primaryUser.id;
     }
 
+    static final String TEST_VPN_PKG = "com.dummy.vpn";
+
     /**
      * Names and UIDs for some fake packages. Important points:
      *  - UID is ordered increasing.
@@ -148,6 +148,8 @@
     @Mock private NotificationManager mNotificationManager;
     @Mock private Vpn.SystemServices mSystemServices;
     @Mock private ConnectivityManager mConnectivityManager;
+    @Mock private KeyStore mKeyStore;
+    private final VpnProfile mVpnProfile = new VpnProfile("key");
 
     @Before
     public void setUp() throws Exception {
@@ -166,6 +168,7 @@
         when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent))
                 .thenReturn(Resources.getSystem().getString(
                         R.string.config_customVpnAlwaysOnDisconnectedDialogComponent));
+        when(mSystemServices.isCallerSystem()).thenReturn(true);
 
         // Used by {@link Notification.Builder}
         ApplicationInfo applicationInfo = new ApplicationInfo();
@@ -175,6 +178,10 @@
                 .thenReturn(applicationInfo);
 
         doNothing().when(mNetService).registerObserver(any());
+
+        // Deny all appops by default.
+        when(mAppOps.noteOpNoThrow(anyInt(), anyInt(), anyString()))
+                .thenReturn(AppOpsManager.MODE_IGNORED);
     }
 
     @Test
@@ -464,12 +471,12 @@
         order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser));
 
         // When a new VPN package is set the rules should change to cover that package.
-        vpn.prepare(null, PKGS[0]);
+        vpn.prepare(null, PKGS[0], false /* isPlatformVpn */);
         order.verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(entireUser));
         order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(exceptPkg0));
 
         // When that VPN package is unset, everything should be undone again in reverse.
-        vpn.prepare(null, VpnConfig.LEGACY_VPN);
+        vpn.prepare(null, VpnConfig.LEGACY_VPN, false /* isPlatformVpn */);
         order.verify(mNetService).setAllowOnlyVpnForUids(eq(false), aryEq(exceptPkg0));
         order.verify(mNetService).setAllowOnlyVpnForUids(eq(true), aryEq(entireUser));
     }
@@ -632,6 +639,185 @@
     }
 
     /**
+     * The profile name should NOT change between releases for backwards compatibility
+     *
+     * <p>If this is changed between releases, the {@link Vpn#getVpnProfilePrivileged()} method MUST
+     * be updated to ensure backward compatibility.
+     */
+    @Test
+    public void testGetProfileNameForPackage() throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+        setMockedUsers(primaryUser);
+
+        final String expected = Credentials.PLATFORM_VPN + primaryUser.id + "_" + TEST_VPN_PKG;
+        assertEquals(expected, vpn.getProfileNameForPackage(TEST_VPN_PKG));
+    }
+
+    private Vpn createVpnAndSetupUidChecks(int... grantedOps) throws Exception {
+        final Vpn vpn = createVpn(primaryUser.id);
+        setMockedUsers(primaryUser);
+
+        when(mPackageManager.getPackageUidAsUser(eq(TEST_VPN_PKG), anyInt()))
+                .thenReturn(Process.myUid());
+
+        for (final int op : grantedOps) {
+            when(mAppOps.noteOpNoThrow(op, Process.myUid(), TEST_VPN_PKG))
+                    .thenReturn(AppOpsManager.MODE_ALLOWED);
+        }
+
+        return vpn;
+    }
+
+    private void checkProvisionVpnProfile(Vpn vpn, boolean expectedResult, int... checkedOps) {
+        assertEquals(expectedResult, vpn.provisionVpnProfile(TEST_VPN_PKG, mVpnProfile, mKeyStore));
+
+        // The profile should always be stored, whether or not consent has been previously granted.
+        verify(mKeyStore)
+                .put(
+                        eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)),
+                        eq(mVpnProfile.encode()),
+                        eq(Process.SYSTEM_UID),
+                        eq(0));
+
+        for (final int checkedOp : checkedOps) {
+            verify(mAppOps).noteOpNoThrow(checkedOp, Process.myUid(), TEST_VPN_PKG);
+        }
+    }
+
+    @Test
+    public void testProvisionVpnProfilePreconsented() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+        checkProvisionVpnProfile(
+                vpn, true /* expectedResult */, AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+    }
+
+    @Test
+    public void testProvisionVpnProfileNotPreconsented() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        // Expect that both the ACTIVATE_VPN and ACTIVATE_PLATFORM_VPN were tried, but the caller
+        // had neither.
+        checkProvisionVpnProfile(vpn, false /* expectedResult */,
+                AppOpsManager.OP_ACTIVATE_PLATFORM_VPN, AppOpsManager.OP_ACTIVATE_VPN);
+    }
+
+    @Test
+    public void testProvisionVpnProfileVpnServicePreconsented() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_VPN);
+
+        checkProvisionVpnProfile(vpn, true /* expectedResult */, AppOpsManager.OP_ACTIVATE_VPN);
+    }
+
+    @Test
+    public void testProvisionVpnProfileTooLarge() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+        final VpnProfile bigProfile = new VpnProfile("");
+        bigProfile.name = new String(new byte[Vpn.MAX_VPN_PROFILE_SIZE_BYTES + 1]);
+
+        try {
+            vpn.provisionVpnProfile(TEST_VPN_PKG, bigProfile, mKeyStore);
+            fail("Expected IAE due to profile size");
+        } catch (IllegalArgumentException expected) {
+        }
+    }
+
+    @Test
+    public void testDeleteVpnProfile() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        vpn.deleteVpnProfile(TEST_VPN_PKG, mKeyStore);
+
+        verify(mKeyStore)
+                .delete(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)), eq(Process.SYSTEM_UID));
+    }
+
+    @Test
+    public void testGetVpnProfilePrivileged() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(new VpnProfile("").encode());
+
+        vpn.getVpnProfilePrivileged(TEST_VPN_PKG, mKeyStore);
+
+        verify(mKeyStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
+    }
+
+    @Test
+    public void testStartVpnProfile() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+        when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore);
+
+        verify(mKeyStore).get(eq(vpn.getProfileNameForPackage(TEST_VPN_PKG)));
+        verify(mAppOps)
+                .noteOpNoThrow(
+                        eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN),
+                        eq(Process.myUid()),
+                        eq(TEST_VPN_PKG));
+    }
+
+    @Test
+    public void testStartVpnProfileVpnServicePreconsented() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_VPN);
+
+        when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG)))
+                .thenReturn(mVpnProfile.encode());
+
+        vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore);
+
+        // Verify that the the ACTIVATE_VPN appop was checked, but no error was thrown.
+        verify(mAppOps).noteOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, Process.myUid(), TEST_VPN_PKG);
+    }
+
+    @Test
+    public void testStartVpnProfileNotConsented() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks();
+
+        try {
+            vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore);
+            fail("Expected failure due to no user consent");
+        } catch (SecurityException expected) {
+        }
+
+        // Verify both appops were checked.
+        verify(mAppOps)
+                .noteOpNoThrow(
+                        eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN),
+                        eq(Process.myUid()),
+                        eq(TEST_VPN_PKG));
+        verify(mAppOps).noteOpNoThrow(AppOpsManager.OP_ACTIVATE_VPN, Process.myUid(), TEST_VPN_PKG);
+
+        // Keystore should never have been accessed.
+        verify(mKeyStore, never()).get(any());
+    }
+
+    @Test
+    public void testStartVpnProfileMissingProfile() throws Exception {
+        final Vpn vpn = createVpnAndSetupUidChecks(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN);
+
+        when(mKeyStore.get(vpn.getProfileNameForPackage(TEST_VPN_PKG))).thenReturn(null);
+
+        try {
+            vpn.startVpnProfile(TEST_VPN_PKG, mKeyStore);
+            fail("Expected failure due to missing profile");
+        } catch (IllegalArgumentException expected) {
+        }
+
+        verify(mKeyStore).get(vpn.getProfileNameForPackage(TEST_VPN_PKG));
+        verify(mAppOps)
+                .noteOpNoThrow(
+                        eq(AppOpsManager.OP_ACTIVATE_PLATFORM_VPN),
+                        eq(Process.myUid()),
+                        eq(TEST_VPN_PKG));
+    }
+
+    /**
      * Mock some methods of vpn object.
      */
     private Vpn createVpn(@UserIdInt int userId) {