Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit d7ab8fec authored by Automerger Merge Worker's avatar Automerger Merge Worker
Browse files

Merge changes I446a8595,I68d2293f am: 29044acc

Change-Id: I8422163249ca637ab71b71777feded76e3225c2e
parents a2cf5100 29044acc
Loading
Loading
Loading
Loading
+8 −0
Original line number Diff line number Diff line
@@ -120,6 +120,14 @@ interface IConnectivityManager

    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
+64 −7
Original line number Diff line number Diff line
@@ -20,8 +20,17 @@ import static com.android.internal.util.Preconditions.checkNotNull;

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 @@ public class VpnManager {
    @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 @@ public class VpnManager {
    /**
     * 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 @@ public class VpnManager {
     *     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();
        }
    }
}
+80 −1
Original line number Diff line number Diff line
@@ -4310,7 +4310,7 @@ public class ConnectivityService extends IConnectivityManager.Stub
            throwIfLockdownEnabled();
            Vpn vpn = mVpns.get(userId);
            if (vpn != null) {
                return vpn.prepare(oldPackage, newPackage);
                return vpn.prepare(oldPackage, newPackage, false);
            } else {
                return false;
            }
@@ -4358,6 +4358,78 @@ public class ConnectivityService extends IConnectivityManager.Stub
        }
    }

    /**
     * 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 @@ public class ConnectivityService extends IConnectivityManager.Stub
        }
    }

    /**
     * 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) {
+199 −11
Original line number Diff line number Diff line
@@ -24,6 +24,8 @@ import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING;
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 @@ public class Vpn {
    // 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 @@ public class Vpn {
     * 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 @@ public class Vpn {
     *
     * @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 @@ public class Vpn {
            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 @@ public class Vpn {
        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 @@ public class Vpn {
     * 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 @@ public class Vpn {
            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 class Vpn {
        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 @@ public class Vpn {
            }
        }
    }

    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);
    }
}
+47 −19
Original line number Diff line number Diff line
@@ -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 @@ import org.junit.runner.RunWith;
@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 @@ public class VpnManagerTest {
            new MockContext() {
                @Override
                public String getOpPackageName() {
                    return "fooPackage";
                    return PKG_NAME;
                }
            };

@@ -50,34 +63,49 @@ public class VpnManagerTest {
    }

    @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) {
        }
        verify(mMockCs).deleteVpnProfile(eq(PKG_NAME));
    }

    @Test
    public void testStartProvisionedVpnProfile() throws Exception {
        try {
        mVpnManager.startProvisionedVpnProfile();
        } catch (UnsupportedOperationException expected) {
        }
        verify(mMockCs).startVpnProfile(eq(PKG_NAME));
    }

    @Test
    public void testStopProvisionedVpnProfile() throws Exception {
        try {
        mVpnManager.stopProvisionedVpnProfile();
        } catch (UnsupportedOperationException expected) {
        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();
    }
}
Loading