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

Commit 50b147d8 authored by Benedict Wong's avatar Benedict Wong
Browse files

Add additional fields to VpnProfile for profile-based IKEv2/IPsec VPNs

This commit adds the fields required to support IKEv2/IPsec VPNs. Other
fields will be reused where possible.

Bug: 143221465
Test: Compiles, new tests written for parcel/unparcel, encode/decode
Change-Id: I4c0e8fb934e75548424a15bbfb35c2ea9a3a57bc
parent 48cbd0cf
Loading
Loading
Loading
Loading
+236 −57
Original line number Diff line number Diff line
@@ -16,6 +16,7 @@

package com.android.internal.net;

import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.net.ProxyInfo;
import android.os.Build;
@@ -23,21 +24,34 @@ import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils;

import com.android.internal.annotations.VisibleForTesting;

import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
 * Parcel-like entity class for VPN profiles. To keep things simple, all
 * fields are package private. Methods are provided for serialization, so
 * storage can be implemented easily. Two rules are set for this class.
 * First, all fields must be kept non-null. Second, always make a copy
 * using clone() before modifying.
 * Profile storage class for a platform VPN.
 *
 * <p>This class supports both the Legacy VPN, as well as application-configurable platform VPNs
 * (such as IKEv2/IPsec).
 *
 * <p>This class is serialized and deserialized via the {@link #encode()} and {@link #decode()}
 * functions for persistent storage in the Android Keystore. The encoding is entirely custom, but
 * must be kept for backward compatibility for devices upgrading between Android versions.
 *
 * @hide
 */
public class VpnProfile implements Cloneable, Parcelable {
public final class VpnProfile implements Cloneable, Parcelable {
    private static final String TAG = "VpnProfile";

    @VisibleForTesting static final String VALUE_DELIMITER = "\0";
    @VisibleForTesting static final String LIST_DELIMITER = ",";

    // Match these constants with R.array.vpn_types.
    public static final int TYPE_PPTP = 0;
    public static final int TYPE_L2TP_IPSEC_PSK = 1;
@@ -45,21 +59,30 @@ public class VpnProfile implements Cloneable, Parcelable {
    public static final int TYPE_IPSEC_XAUTH_PSK = 3;
    public static final int TYPE_IPSEC_XAUTH_RSA = 4;
    public static final int TYPE_IPSEC_HYBRID_RSA = 5;
    public static final int TYPE_MAX = 5;
    public static final int TYPE_IKEV2_IPSEC_USER_PASS = 6;
    public static final int TYPE_IKEV2_IPSEC_PSK = 7;
    public static final int TYPE_IKEV2_IPSEC_RSA = 8;
    public static final int TYPE_MAX = 8;

    // Match these constants with R.array.vpn_proxy_settings.
    public static final int PROXY_NONE = 0;
    public static final int PROXY_MANUAL = 1;

    private static final String ENCODED_NULL_PROXY_INFO = "\0\0\0\0";

    // Entity fields.
    @UnsupportedAppUsage
    public final String key;                                   // -1

    @UnsupportedAppUsage
    public String name = "";                                   // 0

    @UnsupportedAppUsage
    public int type = TYPE_PPTP;                               // 1

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
    public String server = "";                                 // 2

    @UnsupportedAppUsage
    public String username = "";                               // 3
    public String password = "";                               // 4
@@ -69,15 +92,52 @@ public class VpnProfile implements Cloneable, Parcelable {
    public boolean mppe = true;                                // 8
    public String l2tpSecret = "";                             // 9
    public String ipsecIdentifier = "";                        // 10

    /**
     * The RSA private key or pre-shared key used for authentication.
     *
     * <p>If areAuthParamsInline is {@code true}, this String will be either:
     *
     * <ul>
     *   <li>If this is an IKEv2 RSA profile: a PKCS#8 encoded {@link java.security.PrivateKey}
     *   <li>If this is an IKEv2 PSK profile: a string value representing the PSK.
     * </ul>
     */
    public String ipsecSecret = "";                            // 11

    /**
     * The RSA certificate to be used for digital signature authentication.
     *
     * <p>If areAuthParamsInline is {@code true}, this String will be a pem-encoded {@link
     * java.security.X509Certificate}
     */
    public String ipsecUserCert = "";                          // 12

    /**
     * The RSA certificate that should be used to verify the server's end/target certificate.
     *
     * <p>If areAuthParamsInline is {@code true}, this String will be a pem-encoded {@link
     * java.security.X509Certificate}
     */
    public String ipsecCaCert = "";                            // 13
    public String ipsecServerCert = "";                        // 14
    public ProxyInfo proxy = null;                             // 15~18

    /**
     * The list of allowable algorithms.
     *
     * <p>This list is validated in the setter to ensure that encoding characters (list, value
     * delimiters) are not present in the algorithm names. See {@link #validateAllowedAlgorithms()}
     */
    private List<String> mAllowedAlgorithms = new ArrayList<>(); // 19
    public boolean isBypassable = false;                       // 20
    public boolean isMetered = false;                          // 21
    public int maxMtu = 1400;                                  // 22
    public boolean areAuthParamsInline = false;                   // 23

    // Helper fields.
    @UnsupportedAppUsage
    public boolean saveLogin = false;
    public transient boolean saveLogin = false;

    public VpnProfile(String key) {
        this.key = key;
@@ -103,6 +163,34 @@ public class VpnProfile implements Cloneable, Parcelable {
        ipsecServerCert = in.readString();
        saveLogin = in.readInt() != 0;
        proxy = in.readParcelable(null);
        mAllowedAlgorithms = new ArrayList<>();
        in.readList(mAllowedAlgorithms, null);
        isBypassable = in.readBoolean();
        isMetered = in.readBoolean();
        maxMtu = in.readInt();
        areAuthParamsInline = in.readBoolean();
    }

    /**
     * Retrieves the list of allowed algorithms.
     *
     * <p>The contained elements are as listed in {@link IpSecAlgorithm}
     */
    public List<String> getAllowedAlgorithms() {
        return Collections.unmodifiableList(mAllowedAlgorithms);
    }

    /**
     * Validates and sets the list of algorithms that can be used for the IPsec transforms.
     *
     * @param allowedAlgorithms the list of allowable algorithms, as listed in {@link
     *     IpSecAlgorithm}.
     * @throws IllegalArgumentException if any delimiters are used in algorithm names. See {@link
     *     #VALUE_DELIMITER} and {@link LIST_DELIMITER}.
     */
    public void setAllowedAlgorithms(List<String> allowedAlgorithms) {
        validateAllowedAlgorithms(allowedAlgorithms);
        mAllowedAlgorithms = allowedAlgorithms;
    }

    @Override
@@ -125,8 +213,18 @@ public class VpnProfile implements Cloneable, Parcelable {
        out.writeString(ipsecServerCert);
        out.writeInt(saveLogin ? 1 : 0);
        out.writeParcelable(proxy, flags);
        out.writeList(mAllowedAlgorithms);
        out.writeBoolean(isBypassable);
        out.writeBoolean(isMetered);
        out.writeInt(maxMtu);
        out.writeBoolean(areAuthParamsInline);
    }

    /**
     * Decodes a VpnProfile instance from the encoded byte array.
     *
     * <p>See {@link #encode()}
     */
    @UnsupportedAppUsage
    public static VpnProfile decode(String key, byte[] value) {
        try {
@@ -134,9 +232,11 @@ public class VpnProfile implements Cloneable, Parcelable {
                return null;
            }

            String[] values = new String(value, StandardCharsets.UTF_8).split("\0", -1);
            // There can be 14 - 19 Bytes in values.length.
            if (values.length < 14 || values.length > 19) {
            String[] values = new String(value, StandardCharsets.UTF_8).split(VALUE_DELIMITER, -1);
            // Acceptable numbers of values are:
            // 14-19: Standard profile, with option for serverCert, proxy
            // 24: Standard profile with serverCert, proxy and platform-VPN parameters.
            if ((values.length < 14 || values.length > 19) && values.length != 24) {
                return null;
            }

@@ -164,13 +264,23 @@ public class VpnProfile implements Cloneable, Parcelable {
                String port = (values.length > 16) ? values[16] : "";
                String exclList = (values.length > 17) ? values[17] : "";
                String pacFileUrl = (values.length > 18) ? values[18] : "";
                if (pacFileUrl.isEmpty()) {
                if (!host.isEmpty() || !port.isEmpty() || !exclList.isEmpty()) {
                    profile.proxy = new ProxyInfo(host, port.isEmpty() ?
                            0 : Integer.parseInt(port), exclList);
                } else {
                } else if (!pacFileUrl.isEmpty()) {
                    profile.proxy = new ProxyInfo(pacFileUrl);
                }
            } // else profle.proxy = null
            } // else profile.proxy = null

            // Either all must be present, or none must be.
            if (values.length >= 24) {
                profile.mAllowedAlgorithms = Arrays.asList(values[19].split(LIST_DELIMITER));
                profile.isBypassable = Boolean.parseBoolean(values[20]);
                profile.isMetered = Boolean.parseBoolean(values[21]);
                profile.maxMtu = Integer.parseInt(values[22]);
                profile.areAuthParamsInline = Boolean.parseBoolean(values[23]);
            }

            profile.saveLogin = !profile.username.isEmpty() || !profile.password.isEmpty();
            return profile;
        } catch (Exception e) {
@@ -179,36 +289,52 @@ public class VpnProfile implements Cloneable, Parcelable {
        return null;
    }

    /**
     * Encodes a VpnProfile instance to a byte array for storage.
     *
     * <p>See {@link #decode(String, byte[])}
     */
    public byte[] encode() {
        StringBuilder builder = new StringBuilder(name);
        builder.append('\0').append(type);
        builder.append('\0').append(server);
        builder.append('\0').append(saveLogin ? username : "");
        builder.append('\0').append(saveLogin ? password : "");
        builder.append('\0').append(dnsServers);
        builder.append('\0').append(searchDomains);
        builder.append('\0').append(routes);
        builder.append('\0').append(mppe);
        builder.append('\0').append(l2tpSecret);
        builder.append('\0').append(ipsecIdentifier);
        builder.append('\0').append(ipsecSecret);
        builder.append('\0').append(ipsecUserCert);
        builder.append('\0').append(ipsecCaCert);
        builder.append('\0').append(ipsecServerCert);
        builder.append(VALUE_DELIMITER).append(type);
        builder.append(VALUE_DELIMITER).append(server);
        builder.append(VALUE_DELIMITER).append(saveLogin ? username : "");
        builder.append(VALUE_DELIMITER).append(saveLogin ? password : "");
        builder.append(VALUE_DELIMITER).append(dnsServers);
        builder.append(VALUE_DELIMITER).append(searchDomains);
        builder.append(VALUE_DELIMITER).append(routes);
        builder.append(VALUE_DELIMITER).append(mppe);
        builder.append(VALUE_DELIMITER).append(l2tpSecret);
        builder.append(VALUE_DELIMITER).append(ipsecIdentifier);
        builder.append(VALUE_DELIMITER).append(ipsecSecret);
        builder.append(VALUE_DELIMITER).append(ipsecUserCert);
        builder.append(VALUE_DELIMITER).append(ipsecCaCert);
        builder.append(VALUE_DELIMITER).append(ipsecServerCert);
        if (proxy != null) {
            builder.append('\0').append(proxy.getHost() != null ? proxy.getHost() : "");
            builder.append('\0').append(proxy.getPort());
            builder.append('\0').append(proxy.getExclusionListAsString() != null ?
                    proxy.getExclusionListAsString() : "");
            builder.append('\0').append(proxy.getPacFileUrl().toString());
            builder.append(VALUE_DELIMITER).append(proxy.getHost() != null ? proxy.getHost() : "");
            builder.append(VALUE_DELIMITER).append(proxy.getPort());
            builder.append(VALUE_DELIMITER)
                    .append(
                            proxy.getExclusionListAsString() != null
                                    ? proxy.getExclusionListAsString()
                                    : "");
            builder.append(VALUE_DELIMITER).append(proxy.getPacFileUrl().toString());
        } else {
            builder.append(ENCODED_NULL_PROXY_INFO);
        }

        builder.append(VALUE_DELIMITER).append(String.join(LIST_DELIMITER, mAllowedAlgorithms));
        builder.append(VALUE_DELIMITER).append(isBypassable);
        builder.append(VALUE_DELIMITER).append(isMetered);
        builder.append(VALUE_DELIMITER).append(maxMtu);
        builder.append(VALUE_DELIMITER).append(areAuthParamsInline);

        return builder.toString().getBytes(StandardCharsets.UTF_8);
    }

    /**
     * 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, which requires IPv4 address for both server and DNS.
     * Server hostnames would require using DNS before connection.
     */
    public boolean isValidLockdownProfile() {
        return isTypeValidForLockdown()
@@ -238,10 +364,7 @@ public class VpnProfile implements Cloneable, Parcelable {
        return !TextUtils.isEmpty(dnsServers);
    }

    /**
     * Returns {@code true} if all DNS servers have numeric addresses,
     * e.g. 8.8.8.8
     */
    /** Returns {@code true} if all DNS servers have numeric addresses, e.g. 8.8.8.8 */
    public boolean areDnsAddressesNumeric() {
        try {
            for (String dnsServer : dnsServers.split(" +")) {
@@ -253,6 +376,62 @@ public class VpnProfile implements Cloneable, Parcelable {
        return true;
    }

    /**
     * Validates that the provided list of algorithms does not contain illegal characters.
     *
     * @param allowedAlgorithms The list to be validated
     */
    public static void validateAllowedAlgorithms(List<String> allowedAlgorithms) {
        for (final String alg : allowedAlgorithms) {
            if (alg.contains(VALUE_DELIMITER) || alg.contains(LIST_DELIMITER)) {
                throw new IllegalArgumentException(
                        "Algorithm contained illegal ('\0' or ',') character");
            }
        }
    }

    /** Generates a hashcode over the VpnProfile. */
    @Override
    public int hashCode() {
        return Objects.hash(
            key, type, server, username, password, dnsServers, searchDomains, routes, mppe,
            l2tpSecret, ipsecIdentifier, ipsecSecret, ipsecUserCert, ipsecCaCert, ipsecServerCert,
            proxy, mAllowedAlgorithms, isBypassable, isMetered, maxMtu, areAuthParamsInline);
    }

    /** Checks VPN profiles for interior equality. */
    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof VpnProfile)) {
            return false;
        }

        final VpnProfile other = (VpnProfile) obj;
        return Objects.equals(key, other.key)
                && Objects.equals(name, other.name)
                && type == other.type
                && Objects.equals(server, other.server)
                && Objects.equals(username, other.username)
                && Objects.equals(password, other.password)
                && Objects.equals(dnsServers, other.dnsServers)
                && Objects.equals(searchDomains, other.searchDomains)
                && Objects.equals(routes, other.routes)
                && mppe == other.mppe
                && Objects.equals(l2tpSecret, other.l2tpSecret)
                && Objects.equals(ipsecIdentifier, other.ipsecIdentifier)
                && Objects.equals(ipsecSecret, other.ipsecSecret)
                && Objects.equals(ipsecUserCert, other.ipsecUserCert)
                && Objects.equals(ipsecCaCert, other.ipsecCaCert)
                && Objects.equals(ipsecServerCert, other.ipsecServerCert)
                && Objects.equals(proxy, other.proxy)
                && Objects.equals(mAllowedAlgorithms, other.mAllowedAlgorithms)
                && isBypassable == other.isBypassable
                && isMetered == other.isMetered
                && maxMtu == other.maxMtu
                && areAuthParamsInline == other.areAuthParamsInline;
    }

    @NonNull
    public static final Creator<VpnProfile> CREATOR = new Creator<VpnProfile>() {
        @Override
        public VpnProfile createFromParcel(Parcel in) {
+185 −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 com.android.internal.net;

import static com.android.testutils.ParcelUtilsKt.assertParcelSane;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import android.net.IpSecAlgorithm;

import androidx.test.filters.SmallTest;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;

import java.util.Arrays;

/** Unit tests for {@link VpnProfile}. */
@SmallTest
@RunWith(JUnit4.class)
public class VpnProfileTest {
    private static final String DUMMY_PROFILE_KEY = "Test";

    @Test
    public void testDefaults() throws Exception {
        final VpnProfile p = new VpnProfile(DUMMY_PROFILE_KEY);

        assertEquals(DUMMY_PROFILE_KEY, p.key);
        assertEquals("", p.name);
        assertEquals(VpnProfile.TYPE_PPTP, p.type);
        assertEquals("", p.server);
        assertEquals("", p.username);
        assertEquals("", p.password);
        assertEquals("", p.dnsServers);
        assertEquals("", p.searchDomains);
        assertEquals("", p.routes);
        assertTrue(p.mppe);
        assertEquals("", p.l2tpSecret);
        assertEquals("", p.ipsecIdentifier);
        assertEquals("", p.ipsecSecret);
        assertEquals("", p.ipsecUserCert);
        assertEquals("", p.ipsecCaCert);
        assertEquals("", p.ipsecServerCert);
        assertEquals(null, p.proxy);
        assertTrue(p.getAllowedAlgorithms() != null && p.getAllowedAlgorithms().isEmpty());
        assertFalse(p.isBypassable);
        assertFalse(p.isMetered);
        assertEquals(1400, p.maxMtu);
        assertFalse(p.areAuthParamsInline);
    }

    private VpnProfile getSampleIkev2Profile(String key) {
        final VpnProfile p = new VpnProfile(key);

        p.name = "foo";
        p.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
        p.server = "bar";
        p.username = "baz";
        p.password = "qux";
        p.dnsServers = "8.8.8.8";
        p.searchDomains = "";
        p.routes = "0.0.0.0/0";
        p.mppe = false;
        p.l2tpSecret = "";
        p.ipsecIdentifier = "quux";
        p.ipsecSecret = "quuz";
        p.ipsecUserCert = "corge";
        p.ipsecCaCert = "grault";
        p.ipsecServerCert = "garply";
        p.proxy = null;
        p.setAllowedAlgorithms(
                Arrays.asList(
                        IpSecAlgorithm.AUTH_CRYPT_AES_GCM,
                        IpSecAlgorithm.AUTH_HMAC_SHA512,
                        IpSecAlgorithm.CRYPT_AES_CBC));
        p.isBypassable = true;
        p.isMetered = true;
        p.maxMtu = 1350;
        p.areAuthParamsInline = true;

        // Not saved, but also not compared.
        p.saveLogin = true;

        return p;
    }

    @Test
    public void testEquals() {
        assertEquals(
                getSampleIkev2Profile(DUMMY_PROFILE_KEY), getSampleIkev2Profile(DUMMY_PROFILE_KEY));

        final VpnProfile modified = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
        modified.maxMtu--;
        assertNotEquals(getSampleIkev2Profile(DUMMY_PROFILE_KEY), modified);
    }

    @Test
    public void testParcelUnparcel() {
        assertParcelSane(getSampleIkev2Profile(DUMMY_PROFILE_KEY), 22);
    }

    @Test
    public void testSetInvalidAlgorithmValueDelimiter() {
        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);

        try {
            profile.setAllowedAlgorithms(
                    Arrays.asList("test" + VpnProfile.VALUE_DELIMITER + "test"));
            fail("Expected failure due to value separator in algorithm name");
        } catch (IllegalArgumentException expected) {
        }
    }

    @Test
    public void testSetInvalidAlgorithmListDelimiter() {
        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);

        try {
            profile.setAllowedAlgorithms(
                    Arrays.asList("test" + VpnProfile.LIST_DELIMITER + "test"));
            fail("Expected failure due to value separator in algorithm name");
        } catch (IllegalArgumentException expected) {
        }
    }

    @Test
    public void testEncodeDecode() {
        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
        assertEquals(profile, decoded);
    }

    @Test
    public void testEncodeDecodeTooManyValues() {
        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
        final byte[] tooManyValues =
                (new String(profile.encode()) + VpnProfile.VALUE_DELIMITER + "invalid").getBytes();

        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooManyValues));
    }

    @Test
    public void testEncodeDecodeInvalidNumberOfValues() {
        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
        final String encoded = new String(profile.encode());
        final byte[] tooFewValues =
                encoded.substring(0, encoded.lastIndexOf(VpnProfile.VALUE_DELIMITER)).getBytes();

        assertNull(VpnProfile.decode(DUMMY_PROFILE_KEY, tooFewValues));
    }

    @Test
    public void testEncodeDecodeLoginsNotSaved() {
        final VpnProfile profile = getSampleIkev2Profile(DUMMY_PROFILE_KEY);
        profile.saveLogin = false;

        final VpnProfile decoded = VpnProfile.decode(DUMMY_PROFILE_KEY, profile.encode());
        assertNotEquals(profile, decoded);

        // Add the username/password back, everything else must be equal.
        decoded.username = profile.username;
        decoded.password = profile.password;
        assertEquals(profile, decoded);
    }
}