Merge changes from topic "settings-vpn" am: 70f90282ef am: ff39b8ba90

Change-Id: I2e4fdab485b9a94e8b2f121b21269f374b21dcb9
This commit is contained in:
Automerger Merge Worker
2020-02-19 08:07:41 +00:00
5 changed files with 202 additions and 25 deletions

View File

@@ -25,7 +25,10 @@ import static com.android.internal.util.Preconditions.checkStringNotEmpty;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.annotation.Nullable; import android.annotation.Nullable;
import android.os.Process;
import android.security.Credentials; import android.security.Credentials;
import android.security.KeyStore;
import android.security.keystore.AndroidKeyStoreProvider;
import com.android.internal.annotations.VisibleForTesting; import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.net.VpnProfile; import com.android.internal.net.VpnProfile;
@@ -59,6 +62,11 @@ import java.util.Objects;
* Exchange, Version 2 (IKEv2)</a> * Exchange, Version 2 (IKEv2)</a>
*/ */
public final class Ikev2VpnProfile extends PlatformVpnProfile { 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 MISSING_PARAM_MSG_TMPL = "Required parameter was not provided: %s";
private static final String EMPTY_CERT = ""; private static final String EMPTY_CERT = "";
@@ -339,7 +347,8 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile {
break; break;
case TYPE_IKEV2_IPSEC_RSA: case TYPE_IKEV2_IPSEC_RSA:
profile.ipsecUserCert = certificateToPemString(mUserCert); profile.ipsecUserCert = certificateToPemString(mUserCert);
profile.ipsecSecret = encodeForIpsecSecret(mRsaPrivateKey.getEncoded()); profile.ipsecSecret =
PREFIX_INLINE + encodeForIpsecSecret(mRsaPrivateKey.getEncoded());
profile.ipsecCaCert = profile.ipsecCaCert =
mServerRootCaCert == null ? "" : certificateToPemString(mServerRootCaCert); mServerRootCaCert == null ? "" : certificateToPemString(mServerRootCaCert);
break; break;
@@ -360,6 +369,22 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile {
@NonNull @NonNull
public static Ikev2VpnProfile fromVpnProfile(@NonNull VpnProfile profile) public static Ikev2VpnProfile fromVpnProfile(@NonNull VpnProfile profile)
throws IOException, GeneralSecurityException { 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); final Builder builder = new Builder(profile.server, profile.ipsecIdentifier);
builder.setProxy(profile.proxy); builder.setProxy(profile.proxy);
builder.setAllowedAlgorithms(profile.getAllowedAlgorithms()); builder.setAllowedAlgorithms(profile.getAllowedAlgorithms());
@@ -378,8 +403,21 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile {
builder.setAuthPsk(decodeFromIpsecSecret(profile.ipsecSecret)); builder.setAuthPsk(decodeFromIpsecSecret(profile.ipsecSecret));
break; break;
case TYPE_IKEV2_IPSEC_RSA: 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 X509Certificate userCert = certificateFromPemString(profile.ipsecUserCert);
final PrivateKey key = getPrivateKey(profile.ipsecSecret);
final X509Certificate serverRootCa = certificateFromPemString(profile.ipsecCaCert); final X509Certificate serverRootCa = certificateFromPemString(profile.ipsecCaCert);
builder.setAuthDigitalSignature(userCert, key, serverRootCa); builder.setAuthDigitalSignature(userCert, key, serverRootCa);
break; break;
@@ -390,6 +428,39 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile {
return builder.build(); 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. * Converts a X509 Certificate to a PEM-formatted string.
* *
@@ -432,7 +503,6 @@ public final class Ikev2VpnProfile extends PlatformVpnProfile {
/** @hide */ /** @hide */
@NonNull @NonNull
@VisibleForTesting(visibility = Visibility.PRIVATE)
public static String encodeForIpsecSecret(@NonNull byte[] secret) { public static String encodeForIpsecSecret(@NonNull byte[] secret) {
checkNotNull(secret, MISSING_PARAM_MSG_TMPL, "secret"); checkNotNull(secret, MISSING_PARAM_MSG_TMPL, "secret");

View File

@@ -18,6 +18,7 @@ package com.android.internal.net;
import android.annotation.NonNull; import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage; import android.compat.annotation.UnsupportedAppUsage;
import android.net.Ikev2VpnProfile;
import android.net.ProxyInfo; import android.net.ProxyInfo;
import android.os.Build; import android.os.Build;
import android.os.Parcel; import android.os.Parcel;
@@ -332,15 +333,38 @@ public final class VpnProfile implements Cloneable, Parcelable {
return builder.toString().getBytes(StandardCharsets.UTF_8); 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. * Tests if profile is valid for lockdown.
* Server hostnames would require using DNS before connection. *
* <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() { public boolean isValidLockdownProfile() {
return isTypeValidForLockdown() return isTypeValidForLockdown()
&& isServerAddressNumeric() && (isValidLockdownLegacyVpnProfile() || isValidLockdownPlatformVpnProfile());
&& hasDns()
&& areDnsAddressesNumeric();
} }
/** Returns {@code true} if the VPN type is valid for lockdown. */ /** Returns {@code true} if the VPN type is valid for lockdown. */

View File

@@ -690,13 +690,14 @@ public class Vpn {
// Prefer VPN profiles, if any exist. // Prefer VPN profiles, if any exist.
VpnProfile profile = getVpnProfilePrivileged(alwaysOnPackage, keyStore); VpnProfile profile = getVpnProfilePrivileged(alwaysOnPackage, keyStore);
if (profile != null) { 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 // If the above startVpnProfilePrivileged() call returns, the Ikev2VpnProfile was
// correctly parsed, and the VPN has started running in a different thread. The only // 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 // 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 // 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. // will continue retrying until shut down by the user, or always-on is toggled off.
return true; return true;
} }
@@ -818,6 +819,7 @@ public class Vpn {
} }
/** Prepare the VPN for the given package. Does not perform permission checks. */ /** Prepare the VPN for the given package. Does not perform permission checks. */
@GuardedBy("this")
private void prepareInternal(String newPackage) { private void prepareInternal(String newPackage) {
long token = Binder.clearCallingIdentity(); long token = Binder.clearCallingIdentity();
try { try {
@@ -1939,6 +1941,27 @@ public class Vpn {
// Prepare arguments for racoon. // Prepare arguments for racoon.
String[] racoon = null; String[] racoon = null;
switch (profile.type) { 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: case VpnProfile.TYPE_L2TP_IPSEC_PSK:
racoon = new String[] { racoon = new String[] {
iface, profile.server, "udppsk", profile.ipsecIdentifier, iface, profile.server, "udppsk", profile.ipsecIdentifier,
@@ -2945,24 +2968,35 @@ public class Vpn {
throw new IllegalArgumentException("No profile found for " + packageName); throw new IllegalArgumentException("No profile found for " + packageName);
} }
startVpnProfilePrivileged(profile, packageName); startVpnProfilePrivileged(profile, packageName,
null /* keyStore for private key retrieval - unneeded */);
}); });
} }
private void startVpnProfilePrivileged( private synchronized void startVpnProfilePrivileged(
@NonNull VpnProfile profile, @NonNull String packageName) { @NonNull VpnProfile profile, @NonNull String packageName, @Nullable KeyStore keyStore) {
// Ensure that no other previous instance is running. // Make sure VPN is prepared. This method can be called by user apps via startVpnProfile(),
if (mVpnRunner != null) { // by the Setting app via startLegacyVpn(), or by ConnectivityService via
mVpnRunner.exit(); // startAlwaysOnVpn(), so this is the common place to prepare the VPN. This also has the
mVpnRunner = null; // nice property of ensuring there are no other VpnRunner instances running.
} prepareInternal(packageName);
updateState(DetailedState.CONNECTING, "startPlatformVpn"); updateState(DetailedState.CONNECTING, "startPlatformVpn");
try { try {
// Build basic config // Build basic config
mConfig = new VpnConfig(); 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.user = packageName;
mConfig.isMetered = profile.isMetered; mConfig.isMetered = profile.isMetered;
}
mConfig.startTime = SystemClock.elapsedRealtime(); mConfig.startTime = SystemClock.elapsedRealtime();
mConfig.proxyInfo = profile.proxy; mConfig.proxyInfo = profile.proxy;
@@ -2970,7 +3004,8 @@ public class Vpn {
case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
case VpnProfile.TYPE_IKEV2_IPSEC_PSK: case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
case VpnProfile.TYPE_IKEV2_IPSEC_RSA: case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
mVpnRunner = new IkeV2VpnRunner(Ikev2VpnProfile.fromVpnProfile(profile)); mVpnRunner =
new IkeV2VpnRunner(Ikev2VpnProfile.fromVpnProfile(profile, keyStore));
mVpnRunner.start(); mVpnRunner.start();
break; break;
default: default:

View File

@@ -22,7 +22,6 @@ import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull; import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail; import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import android.test.mock.MockContext; import android.test.mock.MockContext;
@@ -232,10 +231,12 @@ public class Ikev2VpnProfileTest {
builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa); builder.setAuthDigitalSignature(mUserCert, mPrivateKey, mServerRootCa);
final VpnProfile profile = builder.build().toVpnProfile(); final VpnProfile profile = builder.build().toVpnProfile();
final String expectedSecret = Ikev2VpnProfile.PREFIX_INLINE
+ Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded());
verifyVpnProfileCommon(profile); verifyVpnProfileCommon(profile);
assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert); assertEquals(Ikev2VpnProfile.certificateToPemString(mUserCert), profile.ipsecUserCert);
assertEquals( assertEquals(
Ikev2VpnProfile.encodeForIpsecSecret(mPrivateKey.getEncoded()), expectedSecret,
profile.ipsecSecret); profile.ipsecSecret);
assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert); assertEquals(Ikev2VpnProfile.certificateToPemString(mServerRootCa), profile.ipsecCaCert);

View File

@@ -59,9 +59,15 @@ import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo; import android.content.pm.UserInfo;
import android.content.res.Resources; import android.content.res.Resources;
import android.net.ConnectivityManager; 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.Network;
import android.net.NetworkCapabilities; import android.net.NetworkCapabilities;
import android.net.NetworkInfo.DetailedState; import android.net.NetworkInfo.DetailedState;
import android.net.RouteInfo;
import android.net.UidRange; import android.net.UidRange;
import android.net.VpnManager; import android.net.VpnManager;
import android.net.VpnService; import android.net.VpnService;
@@ -84,6 +90,7 @@ import androidx.test.runner.AndroidJUnit4;
import com.android.internal.R; import com.android.internal.R;
import com.android.internal.net.VpnConfig; import com.android.internal.net.VpnConfig;
import com.android.internal.net.VpnProfile; import com.android.internal.net.VpnProfile;
import com.android.server.IpSecService;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@@ -93,6 +100,7 @@ import org.mockito.InOrder;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import java.net.Inet4Address;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
@@ -125,6 +133,9 @@ public class VpnTest {
} }
static final String TEST_VPN_PKG = "com.dummy.vpn"; 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: * Names and UIDs for some fake packages. Important points:
@@ -151,23 +162,39 @@ public class VpnTest {
@Mock private Vpn.SystemServices mSystemServices; @Mock private Vpn.SystemServices mSystemServices;
@Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator; @Mock private Vpn.Ikev2SessionCreator mIkev2SessionCreator;
@Mock private ConnectivityManager mConnectivityManager; @Mock private ConnectivityManager mConnectivityManager;
@Mock private IpSecService mIpSecService;
@Mock private KeyStore mKeyStore; @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 @Before
public void setUp() throws Exception { public void setUp() throws Exception {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mIpSecManager = new IpSecManager(mContext, mIpSecService);
when(mContext.getPackageManager()).thenReturn(mPackageManager); when(mContext.getPackageManager()).thenReturn(mPackageManager);
setMockedPackages(mPackages); 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.USER_SERVICE))).thenReturn(mUserManager);
when(mContext.getSystemService(eq(Context.APP_OPS_SERVICE))).thenReturn(mAppOps); when(mContext.getSystemService(eq(Context.APP_OPS_SERVICE))).thenReturn(mAppOps);
when(mContext.getSystemService(eq(Context.NOTIFICATION_SERVICE))) when(mContext.getSystemService(eq(Context.NOTIFICATION_SERVICE)))
.thenReturn(mNotificationManager); .thenReturn(mNotificationManager);
when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE))) when(mContext.getSystemService(eq(Context.CONNECTIVITY_SERVICE)))
.thenReturn(mConnectivityManager); .thenReturn(mConnectivityManager);
when(mContext.getSystemService(eq(Context.IPSEC_SERVICE))).thenReturn(mIpSecManager);
when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)) when(mContext.getString(R.string.config_customVpnAlwaysOnDisconnectedDialogComponent))
.thenReturn(Resources.getSystem().getString( .thenReturn(Resources.getSystem().getString(
R.string.config_customVpnAlwaysOnDisconnectedDialogComponent)); R.string.config_customVpnAlwaysOnDisconnectedDialogComponent));
@@ -962,6 +989,26 @@ public class VpnTest {
// a subsequent CL. // 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. * Mock some methods of vpn object.
*/ */