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

Commit 0bdf785b authored by Automerger Merge Worker's avatar Automerger Merge Worker
Browse files

Merge changes from topic "settings-vpn" am: 70f90282 am: ff39b8ba am: 752013f6

Change-Id: Idac6e83bab50b818e2776f28da380f08000e2a42
parents 24862dd1 752013f6
Loading
Loading
Loading
Loading
+73 −3
Original line number Diff line number Diff line
@@ -25,7 +25,10 @@ import static com.android.internal.util.Preconditions.checkStringNotEmpty;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Process;
import android.security.Credentials;
import android.security.KeyStore;
import android.security.keystore.AndroidKeyStoreProvider;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.net.VpnProfile;
@@ -59,6 +62,11 @@ import java.util.Objects;
 *     Exchange, Version 2 (IKEv2)</a>
 */
public final class Ikev2VpnProfile extends PlatformVpnProfile {
    /** Prefix for when a Private Key is an alias to look for in KeyStore @hide */
    public static final String PREFIX_KEYSTORE_ALIAS = "KEYSTORE_ALIAS:";
    /** Prefix for when a Private Key is stored directly in the profile @hide */
    public static final String PREFIX_INLINE = "INLINE:";

    private static final String MISSING_PARAM_MSG_TMPL = "Required parameter was not provided: %s";
    private static final String EMPTY_CERT = "";

@@ -339,7 +347,8 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile {
                break;
            case TYPE_IKEV2_IPSEC_RSA:
                profile.ipsecUserCert = certificateToPemString(mUserCert);
                profile.ipsecSecret = encodeForIpsecSecret(mRsaPrivateKey.getEncoded());
                profile.ipsecSecret =
                        PREFIX_INLINE + encodeForIpsecSecret(mRsaPrivateKey.getEncoded());
                profile.ipsecCaCert =
                        mServerRootCaCert == null ? "" : certificateToPemString(mServerRootCaCert);
                break;
@@ -360,6 +369,22 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile {
    @NonNull
    public static Ikev2VpnProfile fromVpnProfile(@NonNull VpnProfile profile)
            throws IOException, GeneralSecurityException {
        return fromVpnProfile(profile, null);
    }

    /**
     * Builds the Ikev2VpnProfile from the given profile.
     *
     * @param profile the source VpnProfile to build from
     * @param keyStore the Android Keystore instance to use to retrieve the private key, or null if
     *     the private key is PEM-encoded into the profile.
     * @return The IKEv2/IPsec VPN profile
     * @hide
     */
    @NonNull
    public static Ikev2VpnProfile fromVpnProfile(
            @NonNull VpnProfile profile, @Nullable KeyStore keyStore)
            throws IOException, GeneralSecurityException {
        final Builder builder = new Builder(profile.server, profile.ipsecIdentifier);
        builder.setProxy(profile.proxy);
        builder.setAllowedAlgorithms(profile.getAllowedAlgorithms());
@@ -378,8 +403,21 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile {
                builder.setAuthPsk(decodeFromIpsecSecret(profile.ipsecSecret));
                break;
            case TYPE_IKEV2_IPSEC_RSA:
                final PrivateKey key;
                if (profile.ipsecSecret.startsWith(PREFIX_KEYSTORE_ALIAS)) {
                    Objects.requireNonNull(keyStore, "Missing Keystore for aliased PrivateKey");

                    final String alias =
                            profile.ipsecSecret.substring(PREFIX_KEYSTORE_ALIAS.length());
                    key = AndroidKeyStoreProvider.loadAndroidKeyStorePrivateKeyFromKeystore(
                            keyStore, alias, Process.myUid());
                } else if (profile.ipsecSecret.startsWith(PREFIX_INLINE)) {
                    key = getPrivateKey(profile.ipsecSecret.substring(PREFIX_INLINE.length()));
                } else {
                    throw new IllegalArgumentException("Invalid RSA private key prefix");
                }

                final X509Certificate userCert = certificateFromPemString(profile.ipsecUserCert);
                final PrivateKey key = getPrivateKey(profile.ipsecSecret);
                final X509Certificate serverRootCa = certificateFromPemString(profile.ipsecCaCert);
                builder.setAuthDigitalSignature(userCert, key, serverRootCa);
                break;
@@ -390,6 +428,39 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile {
        return builder.build();
    }

    /**
     * Validates that the VpnProfile is acceptable for the purposes of an Ikev2VpnProfile.
     *
     * @hide
     */
    public static boolean isValidVpnProfile(@NonNull VpnProfile profile) {
        if (profile.server.isEmpty() || profile.ipsecIdentifier.isEmpty()) {
            return false;
        }

        switch (profile.type) {
            case TYPE_IKEV2_IPSEC_USER_PASS:
                if (profile.username.isEmpty() || profile.password.isEmpty()) {
                    return false;
                }
                break;
            case TYPE_IKEV2_IPSEC_PSK:
                if (profile.ipsecSecret.isEmpty()) {
                    return false;
                }
                break;
            case TYPE_IKEV2_IPSEC_RSA:
                if (profile.ipsecSecret.isEmpty() || profile.ipsecUserCert.isEmpty()) {
                    return false;
                }
                break;
            default:
                return false;
        }

        return true;
    }

    /**
     * Converts a X509 Certificate to a PEM-formatted string.
     *
@@ -432,7 +503,6 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile {

    /** @hide */
    @NonNull
    @VisibleForTesting(visibility = Visibility.PRIVATE)
    public static String encodeForIpsecSecret(@NonNull byte[] secret) {
        checkNotNull(secret, MISSING_PARAM_MSG_TMPL, "secret");

+29 −5
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.internal.net;

import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.Ikev2VpnProfile;
import android.net.ProxyInfo;
import android.os.Build;
import android.os.Parcel;
@@ -332,15 +333,38 @@ public final class VpnProfile implements Cloneable, Parcelable {
        return builder.toString().getBytes(StandardCharsets.UTF_8);
    }

    /** Checks if this profile specifies a LegacyVpn type. */
    public static boolean isLegacyType(int type) {
        switch (type) {
            case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: // fall through
            case VpnProfile.TYPE_IKEV2_IPSEC_RSA: // fall through
            case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
                return false;
            default:
                return true;
        }
    }

    private boolean isValidLockdownLegacyVpnProfile() {
        return isLegacyType(type) && isServerAddressNumeric() && hasDns()
                && areDnsAddressesNumeric();
    }

    private boolean isValidLockdownPlatformVpnProfile() {
        return Ikev2VpnProfile.isValidVpnProfile(this);
    }

    /**
     * Tests if profile is valid for lockdown, which requires IPv4 address for both server and DNS.
     * Server hostnames would require using DNS before connection.
     * Tests if profile is valid for lockdown.
     *
     * <p>For LegacyVpn profiles, this requires an IPv4 address for both the server and DNS.
     *
     * <p>For PlatformVpn profiles, this requires a server, an identifier and the relevant fields to
     * be non-null.
     */
    public boolean isValidLockdownProfile() {
        return isTypeValidForLockdown()
                && isServerAddressNumeric()
                && hasDns()
                && areDnsAddressesNumeric();
                && (isValidLockdownLegacyVpnProfile() || isValidLockdownPlatformVpnProfile());
    }

    /** Returns {@code true} if the VPN type is valid for lockdown. */
+48 −13
Original line number Diff line number Diff line
@@ -690,13 +690,14 @@ public class Vpn {
            // Prefer VPN profiles, if any exist.
            VpnProfile profile = getVpnProfilePrivileged(alwaysOnPackage, keyStore);
            if (profile != null) {
                startVpnProfilePrivileged(profile, alwaysOnPackage);
                startVpnProfilePrivileged(profile, alwaysOnPackage,
                        null /* keyStore for private key retrieval - unneeded */);

                // If the above startVpnProfilePrivileged() call returns, the Ikev2VpnProfile was
                // correctly parsed, and the VPN has started running in a different thread. The only
                // other possibility is that the above call threw an exception, which will be
                // caught below, and returns false (clearing the always-on VPN). Once started, the
                // Platform VPN cannot permanantly fail, and is resiliant to temporary failures. It
                // Platform VPN cannot permanently fail, and is resilient to temporary failures. It
                // will continue retrying until shut down by the user, or always-on is toggled off.
                return true;
            }
@@ -818,6 +819,7 @@ public class Vpn {
    }

    /** Prepare the VPN for the given package. Does not perform permission checks. */
    @GuardedBy("this")
    private void prepareInternal(String newPackage) {
        long token = Binder.clearCallingIdentity();
        try {
@@ -1943,6 +1945,27 @@ public class Vpn {
        // Prepare arguments for racoon.
        String[] racoon = null;
        switch (profile.type) {
            case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
                // Secret key is still just the alias (not the actual private key). The private key
                // is retrieved from the KeyStore during conversion of the VpnProfile to an
                // Ikev2VpnProfile.
                profile.ipsecSecret = Ikev2VpnProfile.PREFIX_KEYSTORE_ALIAS + privateKey;
                profile.ipsecUserCert = userCert;
                // Fallthrough
            case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
                profile.ipsecCaCert = caCert;

                // Start VPN profile
                startVpnProfilePrivileged(profile, VpnConfig.LEGACY_VPN, keyStore);
                return;
            case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
                // Ikev2VpnProfiles expect a base64-encoded preshared key.
                profile.ipsecSecret =
                        Ikev2VpnProfile.encodeForIpsecSecret(profile.ipsecSecret.getBytes());

                // Start VPN profile
                startVpnProfilePrivileged(profile, VpnConfig.LEGACY_VPN, keyStore);
                return;
            case VpnProfile.TYPE_L2TP_IPSEC_PSK:
                racoon = new String[] {
                    iface, profile.server, "udppsk", profile.ipsecIdentifier,
@@ -2949,24 +2972,35 @@ public class Vpn {
                        throw new IllegalArgumentException("No profile found for " + packageName);
                    }

                    startVpnProfilePrivileged(profile, packageName);
                    startVpnProfilePrivileged(profile, packageName,
                            null /* keyStore for private key retrieval - unneeded */);
                });
    }

    private void startVpnProfilePrivileged(
            @NonNull VpnProfile profile, @NonNull String packageName) {
        // Ensure that no other previous instance is running.
        if (mVpnRunner != null) {
            mVpnRunner.exit();
            mVpnRunner = null;
        }
    private synchronized void startVpnProfilePrivileged(
            @NonNull VpnProfile profile, @NonNull String packageName, @Nullable KeyStore keyStore) {
        // Make sure VPN is prepared. This method can be called by user apps via startVpnProfile(),
        // by the Setting app via startLegacyVpn(), or by ConnectivityService via
        // startAlwaysOnVpn(), so this is the common place to prepare the VPN. This also has the
        // nice property of ensuring there are no other VpnRunner instances running.
        prepareInternal(packageName);
        updateState(DetailedState.CONNECTING, "startPlatformVpn");

        try {
            // Build basic config
            mConfig = new VpnConfig();
            if (VpnConfig.LEGACY_VPN.equals(packageName)) {
                mConfig.legacy = true;
                mConfig.session = profile.name;
                mConfig.user = profile.key;

                // TODO: Add support for configuring meteredness via Settings. Until then, use a
                // safe default.
                mConfig.isMetered = true;
            } else {
                mConfig.user = packageName;
                mConfig.isMetered = profile.isMetered;
            }
            mConfig.startTime = SystemClock.elapsedRealtime();
            mConfig.proxyInfo = profile.proxy;

@@ -2974,7 +3008,8 @@ public class Vpn {
                case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
                case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
                case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
                    mVpnRunner = new IkeV2VpnRunner(Ikev2VpnProfile.fromVpnProfile(profile));
                    mVpnRunner =
                            new IkeV2VpnRunner(Ikev2VpnProfile.fromVpnProfile(profile, keyStore));
                    mVpnRunner.start();
                    break;
                default:
+3 −2
Original line number Diff line number Diff line
@@ -22,7 +22,6 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;

import android.test.mock.MockContext;

@@ -232,10 +231,12 @@ public class Ikev2VpnProfileTest {
        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
        final VpnProfile profile = builder.build().toVpnProfile();

        final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE
                + Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded());
        verifyVpnProfileCommon(profile);
        assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert);
        assertEquals(
                Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded()),
                expectedSecret,
                profile.ipsecSecret);
        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);

+49 −2
Original line number Diff line number Diff line
@@ -59,9 +59,15 @@ import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.Ikev2VpnProfile;
import android.net.InetAddresses;
import android.net.IpPrefix;
import android.net.IpSecManager;
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.VpnManager;
import android.net.VpnService;
@@ -84,6 +90,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.R;
import com.android.internal.net.VpnConfig;
import com.android.internal.net.VpnProfile;
import com.android.server.IpSecService;

import org.junit.Before;
import org.junit.Test;
@@ -93,6 +100,7 @@ import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import java.net.Inet4Address;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@@ -125,6 +133,9 @@ public class VpnTest {
    }

    static final String TEST_VPN_PKG = "com.dummy.vpn";
    private static final String TEST_VPN_SERVER = "1.2.3.4";
    private static final String TEST_VPN_IDENTITY = "identity";
    private static final byte[] TEST_VPN_PSK = "psk".getBytes();

    /**
     * Names and UIDs for some fake packages. Important points:
@@ -151,23 +162,39 @@ public class VpnTest {
    @Mock private Vpn.SystemServices mSystemServices;
    @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
    @Mock private ConnectivityManager mConnectivityManager;
    @Mock private IpSecService mIpSecService;
    @Mock private KeyStore mKeyStore;
    private final VpnProfile mVpnProfile = new VpnProfile("key");
    private final VpnProfile mVpnProfile;

    private IpSecManager mIpSecManager;

    public VpnTest() throws Exception {
        // Build an actual VPN profile that is capable of being converted to and from an
        // Ikev2VpnProfile
        final Ikev2VpnProfile.Builder builder =
                new Ikev2VpnProfile.Builder(TEST_VPN_SERVER, TEST_VPN_IDENTITY);
        builder.setAuthPsk(TEST_VPN_PSK);
        mVpnProfile = builder.build().toVpnProfile();
    }

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        mIpSecManager = new IpSecManager(mContext, mIpSecService);

        when(mContext.getPackageManager()).thenReturn(mPackageManager);
        setMockedPackages(mPackages);

        when(mContext.getPackageName()).thenReturn(Vpn.class.getPackage().getName());
        when(mContext.getPackageName()).thenReturn(TEST_VPN_PKG);
        when(mContext.getOpPackageName()).thenReturn(TEST_VPN_PKG);
        when(mContext.getSystemService(eq(Context.USER_SERVICE))).thenReturn(mUserManager);
        when(mContext.getSystemService(eq(Context.APP_OPS_SERVICE))).thenReturn(mAppOps);
        when(mContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
                .thenReturn(mNotificationManager);
        when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE)))
                .thenReturn(mConnectivityManager);
        when(mContext.getSystemService(eq(Context.IPSEC_SERVICE))).thenReturn(mIpSecManager);
        when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent))
                .thenReturn(Resources.getSystem().getString(
                        R.string.config_customVpnAlwaysOnDisconnectedDialogComponent));
@@ -962,6 +989,26 @@ public class VpnTest {
        // a subsequent CL.
    }

    @Test
    public void testStartLegacyVpn() throws Exception {
        final Vpn vpn = createVpn(primaryUser.id);
        setMockedUsers(primaryUser);

        // Dummy egress interface
        final String egressIface = "DUMMY0";
        final LinkProperties lp = new LinkProperties();
        lp.setInterfaceName(egressIface);

        final RouteInfo defaultRoute = new RouteInfo(new IpPrefix(Inet4Address.ANY, 0),
                        InetAddresses.parseNumericAddress("192.0.2.0"), egressIface);
        lp.addRoute(defaultRoute);

        vpn.startLegacyVpn(mVpnProfile, mKeyStore, lp);

        // TODO: Test the Ikev2VpnRunner started up properly. Relies on utility methods added in
        // a subsequent CL.
    }

    /**
     * Mock some methods of vpn object.
     */