diff --git a/core/java/android/net/Ikev2VpnProfile.java b/core/java/android/net/Ikev2VpnProfile.java index 42b4da14d8798..f19a3410d673d 100644 --- a/core/java/android/net/Ikev2VpnProfile.java +++ b/core/java/android/net/Ikev2VpnProfile.java @@ -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) */ 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"); diff --git a/core/java/com/android/internal/net/VpnProfile.java b/core/java/com/android/internal/net/VpnProfile.java index bbae0273ef4eb..23b1ab52cb219 100644 --- a/core/java/com/android/internal/net/VpnProfile.java +++ b/core/java/com/android/internal/net/VpnProfile.java @@ -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. + * + *
For LegacyVpn profiles, this requires an IPv4 address for both the server and DNS. + * + *
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. */ diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java index 3138639c4a2b2..e484ca0a24879 100644 --- a/services/core/java/com/android/server/connectivity/Vpn.java +++ b/services/core/java/com/android/server/connectivity/Vpn.java @@ -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 { @@ -1943,6 +1945,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, @@ -2949,24 +2972,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; @@ -2974,7 +3008,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: diff --git a/tests/net/java/android/net/Ikev2VpnProfileTest.java b/tests/net/java/android/net/Ikev2VpnProfileTest.java index d6a2176d7e813..2273bc61225c2 100644 --- a/tests/net/java/android/net/Ikev2VpnProfileTest.java +++ b/tests/net/java/android/net/Ikev2VpnProfileTest.java @@ -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); diff --git a/tests/net/java/com/android/server/connectivity/VpnTest.java b/tests/net/java/com/android/server/connectivity/VpnTest.java index 0e3b797613192..1994d1f2ed455 100644 --- a/tests/net/java/com/android/server/connectivity/VpnTest.java +++ b/tests/net/java/com/android/server/connectivity/VpnTest.java @@ -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. */