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

Commit feb69c13 authored by Benedict Wong's avatar Benedict Wong
Browse files

Add Ikev2VpnProfile as public API

This change adds configuration classes for Platform VPNs, with the
extensibility for further platform VPNs to be added in future.

Profile validity is checked upon construction, and upon changing
conversion from VpnProfile instances. Internal storage and method calls
all use VpnProfiles to allow for reuse of existing infrastructure.
However, when Platform VPN implementations are started, the internal
VpnProfile will be converted back into the respective
PlatformVpnProfile for validity checking.

Bug: 143325939
Test: Compiles, FrameworksNetTests passing.
Change-Id: I3c375fb08c132fc062e893c375f5c36460122162
parent 50b147d8
Loading
Loading
Loading
Loading
+37 −0
Original line number Diff line number Diff line
@@ -28866,6 +28866,35 @@ package android.net {
    field public final int code;
  }
  public final class Ikev2VpnProfile extends android.net.PlatformVpnProfile {
    method @NonNull public java.util.List<java.lang.String> getAllowedAlgorithms();
    method public int getMaxMtu();
    method @Nullable public String getPassword();
    method @Nullable public byte[] getPresharedKey();
    method @Nullable public android.net.ProxyInfo getProxyInfo();
    method @Nullable public java.security.PrivateKey getRsaPrivateKey();
    method @NonNull public String getServerAddr();
    method @Nullable public java.security.cert.X509Certificate getServerRootCaCert();
    method @Nullable public java.security.cert.X509Certificate getUserCert();
    method @NonNull public String getUserIdentity();
    method @Nullable public String getUsername();
    method public boolean isBypassable();
    method public boolean isMetered();
  }
  public static final class Ikev2VpnProfile.Builder {
    ctor public Ikev2VpnProfile.Builder(@NonNull String, @NonNull String);
    method @NonNull public android.net.Ikev2VpnProfile build();
    method @NonNull public android.net.Ikev2VpnProfile.Builder setAllowedAlgorithms(@NonNull java.util.List<java.lang.String>);
    method @NonNull public android.net.Ikev2VpnProfile.Builder setAuthDigitalSignature(@NonNull java.security.cert.X509Certificate, @NonNull java.security.PrivateKey, @Nullable java.security.cert.X509Certificate);
    method @NonNull public android.net.Ikev2VpnProfile.Builder setAuthPsk(@NonNull byte[]);
    method @NonNull public android.net.Ikev2VpnProfile.Builder setAuthUsernamePassword(@NonNull String, @NonNull String, @Nullable java.security.cert.X509Certificate);
    method @NonNull public android.net.Ikev2VpnProfile.Builder setBypassable(boolean);
    method @NonNull public android.net.Ikev2VpnProfile.Builder setMaxMtu(int);
    method @NonNull public android.net.Ikev2VpnProfile.Builder setMetered(boolean);
    method @NonNull public android.net.Ikev2VpnProfile.Builder setProxy(@Nullable android.net.ProxyInfo);
  }
  public class InetAddresses {
    method public static boolean isNumericAddress(@NonNull String);
    method @NonNull public static java.net.InetAddress parseNumericAddress(@NonNull String);
@@ -29212,6 +29241,14 @@ package android.net {
    field public String response;
  }
  public abstract class PlatformVpnProfile {
    method public final int getType();
    method @NonNull public final String getTypeString();
    field public static final int TYPE_IKEV2_IPSEC_PSK = 7; // 0x7
    field public static final int TYPE_IKEV2_IPSEC_RSA = 8; // 0x8
    field public static final int TYPE_IKEV2_IPSEC_USER_PASS = 6; // 0x6
  }
  public final class Proxy {
    ctor public Proxy();
    method @Deprecated public static String getDefaultHost();
+728 −0

File added.

Preview size limit exceeded, changes collapsed.

+2 −1
Original line number Diff line number Diff line
@@ -80,7 +80,8 @@ public final class LinkProperties implements Parcelable {
    private final transient boolean mParcelSensitiveFields;

    private static final int MIN_MTU    = 68;
    private static final int MIN_MTU_V6 = 1280;
    /* package-visibility - Used in other files (such as Ikev2VpnProfile) as minimum iface MTU. */
    static final int MIN_MTU_V6 = 1280;
    private static final int MAX_MTU    = 10000;

    private static final int INET6_ADDR_LENGTH = 16;
+107 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net;

import static android.net.PlatformVpnProfile.TYPE_IKEV2_IPSEC_PSK;
import static android.net.PlatformVpnProfile.TYPE_IKEV2_IPSEC_RSA;
import static android.net.PlatformVpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;

import android.annotation.IntDef;
import android.annotation.NonNull;

import com.android.internal.net.VpnProfile;

import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.security.GeneralSecurityException;

/**
 * PlatformVpnProfile represents a configuration for a platform-based VPN implementation.
 *
 * <p>Platform-based VPNs allow VPN applications to provide configuration and authentication options
 * to leverage the Android OS' implementations of well-defined control plane (authentication, key
 * negotiation) and data plane (per-packet encryption) protocols to simplify the creation of VPN
 * tunnels. In contrast, {@link VpnService} based VPNs must implement both the control and data
 * planes on a per-app basis.
 *
 * @see Ikev2VpnProfile
 */
public abstract class PlatformVpnProfile {
    /**
     * Alias to platform VPN related types from VpnProfile, for API use.
     *
     * @hide
     */
    @Retention(RetentionPolicy.SOURCE)
    @IntDef({
        TYPE_IKEV2_IPSEC_USER_PASS,
        TYPE_IKEV2_IPSEC_PSK,
        TYPE_IKEV2_IPSEC_RSA,
    })
    public static @interface PlatformVpnType {}

    public static final int TYPE_IKEV2_IPSEC_USER_PASS = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
    public static final int TYPE_IKEV2_IPSEC_PSK = VpnProfile.TYPE_IKEV2_IPSEC_PSK;
    public static final int TYPE_IKEV2_IPSEC_RSA = VpnProfile.TYPE_IKEV2_IPSEC_RSA;

    /** @hide */
    @PlatformVpnType protected final int mType;

    /** @hide */
    PlatformVpnProfile(@PlatformVpnType int type) {
        mType = type;
    }
    /** Returns the profile integer type. */
    @PlatformVpnType
    public final int getType() {
        return mType;
    }

    /** Returns a type string describing the VPN profile type */
    @NonNull
    public final String getTypeString() {
        switch (mType) {
            case TYPE_IKEV2_IPSEC_USER_PASS:
                return "IKEv2/IPsec Username/Password";
            case TYPE_IKEV2_IPSEC_PSK:
                return "IKEv2/IPsec Preshared key";
            case TYPE_IKEV2_IPSEC_RSA:
                return "IKEv2/IPsec RSA Digital Signature";
            default:
                return "Unknown VPN profile type";
        }
    }

    /** @hide */
    @NonNull
    public abstract VpnProfile toVpnProfile() throws IOException, GeneralSecurityException;

    /** @hide */
    @NonNull
    public static PlatformVpnProfile fromVpnProfile(@NonNull VpnProfile profile)
            throws IOException, GeneralSecurityException {
        switch (profile.type) {
            case TYPE_IKEV2_IPSEC_USER_PASS: // fallthrough
            case TYPE_IKEV2_IPSEC_PSK: // fallthrough
            case TYPE_IKEV2_IPSEC_RSA:
                return Ikev2VpnProfile.fromVpnProfile(profile);
            default:
                throw new IllegalArgumentException("Unknown VPN Profile type");
        }
    }
}
+360 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2019 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.net;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
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;

import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.internal.net.VpnProfile;
import com.android.org.bouncycastle.x509.X509V1CertificateGenerator;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.concurrent.TimeUnit;

import javax.security.auth.x500.X500Principal;

/** Unit tests for {@link Ikev2VpnProfile.Builder}. */
@SmallTest
@RunWith(AndroidJUnit4.class)
public class Ikev2VpnProfileTest {
    private static final String SERVER_ADDR_STRING = "1.2.3.4";
    private static final String IDENTITY_STRING = "Identity";
    private static final String USERNAME_STRING = "username";
    private static final String PASSWORD_STRING = "pa55w0rd";
    private static final String EXCL_LIST = "exclList";
    private static final byte[] PSK_BYTES = "preSharedKey".getBytes();
    private static final int TEST_MTU = 1300;

    private final MockContext mMockContext =
            new MockContext() {
                @Override
                public String getOpPackageName() {
                    return "fooPackage";
                }
            };
    private final ProxyInfo mProxy = new ProxyInfo(SERVER_ADDR_STRING, -1, EXCL_LIST);

    private X509Certificate mUserCert;
    private X509Certificate mServerRootCa;
    private PrivateKey mPrivateKey;

    @Before
    public void setUp() throws Exception {
        mServerRootCa = generateRandomCertAndKeyPair().cert;

        final CertificateAndKey userCertKey = generateRandomCertAndKeyPair();
        mUserCert = userCertKey.cert;
        mPrivateKey = userCertKey.key;
    }

    private Ikev2VpnProfile.Builder getBuilderWithDefaultOptions() {
        final Ikev2VpnProfile.Builder builder =
                new Ikev2VpnProfile.Builder(SERVER_ADDR_STRING, IDENTITY_STRING);

        builder.setBypassable(true);
        builder.setProxy(mProxy);
        builder.setMaxMtu(TEST_MTU);
        builder.setMetered(true);

        return builder;
    }

    @Test
    public void testBuildValidProfileWithOptions() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
        final Ikev2VpnProfile profile = builder.build();
        assertNotNull(profile);

        // Check non-auth parameters correctly stored
        assertEquals(SERVER_ADDR_STRING, profile.getServerAddr());
        assertEquals(IDENTITY_STRING, profile.getUserIdentity());
        assertEquals(mProxy, profile.getProxyInfo());
        assertTrue(profile.isBypassable());
        assertTrue(profile.isMetered());
        assertEquals(TEST_MTU, profile.getMaxMtu());
    }

    @Test
    public void testBuildUsernamePasswordProfile() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
        final Ikev2VpnProfile profile = builder.build();
        assertNotNull(profile);

        assertEquals(USERNAME_STRING, profile.getUsername());
        assertEquals(PASSWORD_STRING, profile.getPassword());
        assertEquals(mServerRootCa, profile.getServerRootCaCert());

        assertNull(profile.getPresharedKey());
        assertNull(profile.getRsaPrivateKey());
        assertNull(profile.getUserCert());
    }

    @Test
    public void testBuildDigitalSignatureProfile() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
        final Ikev2VpnProfile profile = builder.build();
        assertNotNull(profile);

        assertEquals(profile.getUserCert(), mUserCert);
        assertEquals(mPrivateKey, profile.getRsaPrivateKey());
        assertEquals(profile.getServerRootCaCert(), mServerRootCa);

        assertNull(profile.getPresharedKey());
        assertNull(profile.getUsername());
        assertNull(profile.getPassword());
    }

    @Test
    public void testBuildPresharedKeyProfile() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthPsk(PSK_BYTES);
        final Ikev2VpnProfile profile = builder.build();
        assertNotNull(profile);

        assertArrayEquals(PSK_BYTES, profile.getPresharedKey());

        assertNull(profile.getServerRootCaCert());
        assertNull(profile.getUsername());
        assertNull(profile.getPassword());
        assertNull(profile.getRsaPrivateKey());
        assertNull(profile.getUserCert());
    }

    @Test
    public void testBuildNoAuthMethodSet() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        try {
            builder.build();
            fail("Expected exception due to lack of auth method");
        } catch (IllegalArgumentException expected) {
        }
    }

    @Test
    public void testBuildInvalidMtu() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        try {
            builder.setMaxMtu(500);
            fail("Expected exception due to too-small MTU");
        } catch (IllegalArgumentException expected) {
        }
    }

    private void verifyVpnProfileCommon(VpnProfile profile) {
        assertEquals(SERVER_ADDR_STRING, profile.server);
        assertEquals(IDENTITY_STRING, profile.ipsecIdentifier);
        assertEquals(mProxy, profile.proxy);
        assertTrue(profile.isBypassable);
        assertTrue(profile.isMetered);
        assertEquals(TEST_MTU, profile.maxMtu);
    }

    @Test
    public void testPskConvertToVpnProfile() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthPsk(PSK_BYTES);
        final VpnProfile profile = builder.build().toVpnProfile();

        verifyVpnProfileCommon(profile);
        assertEquals(Ikev2VpnProfile.encodeForIpsecSecret(PSK_BYTES), profile.ipsecSecret);

        // Check nothing else is set
        assertEquals("", profile.username);
        assertEquals("", profile.password);
        assertEquals("", profile.ipsecUserCert);
        assertEquals("", profile.ipsecCaCert);
    }

    @Test
    public void testUsernamePasswordConvertToVpnProfile() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
        final VpnProfile profile = builder.build().toVpnProfile();

        verifyVpnProfileCommon(profile);
        assertEquals(USERNAME_STRING, profile.username);
        assertEquals(PASSWORD_STRING, profile.password);
        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);

        // Check nothing else is set
        assertEquals("", profile.ipsecUserCert);
        assertEquals("", profile.ipsecSecret);
    }

    @Test
    public void testRsaConvertToVpnProfile() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
        final VpnProfile profile = builder.build().toVpnProfile();

        verifyVpnProfileCommon(profile);
        assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert);
        assertEquals(
                Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded()),
                profile.ipsecSecret);
        assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);

        // Check nothing else is set
        assertEquals("", profile.username);
        assertEquals("", profile.password);
    }

    @Test
    public void testPskFromVpnProfileDiscardsIrrelevantValues() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthPsk(PSK_BYTES);
        final VpnProfile profile = builder.build().toVpnProfile();
        profile.username = USERNAME_STRING;
        profile.password = PASSWORD_STRING;
        profile.ipsecCaCert = Ikev2VpnProfile.certificateToPemString(mServerRootCa);
        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);

        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
        assertNull(result.getUsername());
        assertNull(result.getPassword());
        assertNull(result.getUserCert());
        assertNull(result.getRsaPrivateKey());
        assertNull(result.getServerRootCaCert());
    }

    @Test
    public void testUsernamePasswordFromVpnProfileDiscardsIrrelevantValues() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
        final VpnProfile profile = builder.build().toVpnProfile();
        profile.ipsecSecret = new String(PSK_BYTES);
        profile.ipsecUserCert = Ikev2VpnProfile.certificateToPemString(mUserCert);

        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
        assertNull(result.getPresharedKey());
        assertNull(result.getUserCert());
        assertNull(result.getRsaPrivateKey());
    }

    @Test
    public void testRsaFromVpnProfileDiscardsIrrelevantValues() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
        final VpnProfile profile = builder.build().toVpnProfile();
        profile.username = USERNAME_STRING;
        profile.password = PASSWORD_STRING;

        final Ikev2VpnProfile result = Ikev2VpnProfile.fromVpnProfile(profile);
        assertNull(result.getUsername());
        assertNull(result.getPassword());
        assertNull(result.getPresharedKey());
    }

    @Test
    public void testPskConversionIsLossless() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthPsk(PSK_BYTES);
        final Ikev2VpnProfile ikeProfile = builder.build();

        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
    }

    @Test
    public void testUsernamePasswordConversionIsLossless() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthUsernamePassword(USERNAME_STRING, PASSWORD_STRING, mServerRootCa);
        final Ikev2VpnProfile ikeProfile = builder.build();

        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
    }

    @Test
    public void testRsaConversionIsLossless() throws Exception {
        final Ikev2VpnProfile.Builder builder = getBuilderWithDefaultOptions();

        builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
        final Ikev2VpnProfile ikeProfile = builder.build();

        assertEquals(ikeProfile, Ikev2VpnProfile.fromVpnProfile(ikeProfile.toVpnProfile()));
    }

    private static class CertificateAndKey {
        public final X509Certificate cert;
        public final PrivateKey key;

        CertificateAndKey(X509Certificate cert, PrivateKey key) {
            this.cert = cert;
            this.key = key;
        }
    }

    private static CertificateAndKey generateRandomCertAndKeyPair() throws Exception {
        final Date validityBeginDate =
                new Date(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1L));
        final Date validityEndDate =
                new Date(System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1L));

        // Generate a keypair
        final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
        keyPairGenerator.initialize(512);
        final KeyPair keyPair = keyPairGenerator.generateKeyPair();

        final X500Principal dnName = new X500Principal("CN=test.android.com");
        final X509V1CertificateGenerator certGen = new X509V1CertificateGenerator();
        certGen.setSerialNumber(BigInteger.valueOf(System.currentTimeMillis()));
        certGen.setSubjectDN(dnName);
        certGen.setIssuerDN(dnName);
        certGen.setNotBefore(validityBeginDate);
        certGen.setNotAfter(validityEndDate);
        certGen.setPublicKey(keyPair.getPublic());
        certGen.setSignatureAlgorithm("SHA256WithRSAEncryption");

        final X509Certificate cert = certGen.generate(keyPair.getPrivate(), "AndroidOpenSSL");
        return new CertificateAndKey(cert, keyPair.getPrivate());
    }
}