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.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");

View File

@@ -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. */

View File

@@ -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 {
@@ -1939,6 +1941,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,
@@ -2945,24 +2968,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();
mConfig.user = packageName;
mConfig.isMetered = profile.isMetered;
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;
@@ -2970,7 +3004,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:

View File

@@ -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);

View File

@@ -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.
*/