Merge changes from topic "settings-vpn" am: 70f90282ef am: ff39b8ba90
Change-Id: I2e4fdab485b9a94e8b2f121b21269f374b21dcb9
This commit is contained in:
@@ -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");
|
||||
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user