/* * 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 android.net; import static android.net.IpSecAlgorithm.AUTH_AES_CMAC; import static android.net.IpSecAlgorithm.AUTH_AES_XCBC; import static android.net.IpSecAlgorithm.AUTH_CRYPT_AES_GCM; import static android.net.IpSecAlgorithm.AUTH_CRYPT_CHACHA20_POLY1305; import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA256; import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA384; import static android.net.IpSecAlgorithm.AUTH_HMAC_SHA512; import static android.net.IpSecAlgorithm.CRYPT_AES_CBC; import static android.net.IpSecAlgorithm.CRYPT_AES_CTR; import static com.android.internal.annotations.VisibleForTesting.Visibility; import static com.android.internal.util.Preconditions.checkStringNotEmpty; import static com.android.net.module.util.NetworkStackConstants.IPV6_MIN_MTU; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresFeature; import android.content.pm.PackageManager; import android.net.ipsec.ike.IkeDerAsn1DnIdentification; import android.net.ipsec.ike.IkeFqdnIdentification; import android.net.ipsec.ike.IkeIdentification; import android.net.ipsec.ike.IkeIpv4AddrIdentification; import android.net.ipsec.ike.IkeIpv6AddrIdentification; import android.net.ipsec.ike.IkeKeyIdIdentification; import android.net.ipsec.ike.IkeRfc822AddrIdentification; import android.net.ipsec.ike.IkeSessionParams; import android.net.ipsec.ike.IkeTunnelConnectionParams; import android.security.Credentials; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.net.VpnProfile; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.Key; import java.security.KeyFactory; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; import java.security.spec.InvalidKeySpecException; import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; import java.util.Collections; import java.util.List; import java.util.Objects; /** * The Ikev2VpnProfile is a configuration for the platform setup of IKEv2/IPsec VPNs. * *

Together with VpnManager, this allows apps to provision IKEv2/IPsec VPNs that do not require * the VPN app to constantly run in the background. * * @see VpnManager * @see RFC 7296 - Internet Key * Exchange, Version 2 (IKEv2) */ public final class Ikev2VpnProfile extends PlatformVpnProfile { private static final String TAG = Ikev2VpnProfile.class.getSimpleName(); /** 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 ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore"; private static final String MISSING_PARAM_MSG_TMPL = "Required parameter was not provided: %s"; private static final String EMPTY_CERT = ""; /** @hide */ public static final List DEFAULT_ALGORITHMS; private static void addAlgorithmIfSupported(List algorithms, String ipSecAlgoName) { if (IpSecAlgorithm.getSupportedAlgorithms().contains(ipSecAlgoName)) { algorithms.add(ipSecAlgoName); } } static { final List algorithms = new ArrayList<>(); addAlgorithmIfSupported(algorithms, CRYPT_AES_CBC); addAlgorithmIfSupported(algorithms, CRYPT_AES_CTR); addAlgorithmIfSupported(algorithms, AUTH_HMAC_SHA256); addAlgorithmIfSupported(algorithms, AUTH_HMAC_SHA384); addAlgorithmIfSupported(algorithms, AUTH_HMAC_SHA512); addAlgorithmIfSupported(algorithms, AUTH_AES_XCBC); addAlgorithmIfSupported(algorithms, AUTH_AES_CMAC); addAlgorithmIfSupported(algorithms, AUTH_CRYPT_AES_GCM); addAlgorithmIfSupported(algorithms, AUTH_CRYPT_CHACHA20_POLY1305); DEFAULT_ALGORITHMS = Collections.unmodifiableList(algorithms); } @Nullable private final String mServerAddr; @Nullable private final String mUserIdentity; // PSK authentication @Nullable private final byte[] mPresharedKey; // Username/Password, RSA authentication @Nullable private final X509Certificate mServerRootCaCert; // Username/Password authentication @Nullable private final String mUsername; @Nullable private final String mPassword; // RSA Certificate authentication @Nullable private final PrivateKey mRsaPrivateKey; @Nullable private final X509Certificate mUserCert; @Nullable private final ProxyInfo mProxyInfo; @NonNull private final List mAllowedAlgorithms; private final boolean mIsBypassable; // Defaults in builder private final boolean mIsMetered; // Defaults in builder private final int mMaxMtu; // Defaults in builder private final boolean mIsRestrictedToTestNetworks; @Nullable private final IkeTunnelConnectionParams mIkeTunConnParams; private final boolean mAutomaticNattKeepaliveTimerEnabled; private final boolean mAutomaticIpVersionSelectionEnabled; private Ikev2VpnProfile( int type, @Nullable String serverAddr, @Nullable String userIdentity, @Nullable byte[] presharedKey, @Nullable X509Certificate serverRootCaCert, @Nullable String username, @Nullable String password, @Nullable PrivateKey rsaPrivateKey, @Nullable X509Certificate userCert, @Nullable ProxyInfo proxyInfo, @NonNull List allowedAlgorithms, boolean isBypassable, boolean isMetered, int maxMtu, boolean restrictToTestNetworks, boolean excludeLocalRoutes, boolean requiresInternetValidation, @Nullable IkeTunnelConnectionParams ikeTunConnParams, boolean automaticNattKeepaliveTimerEnabled, boolean automaticIpVersionSelectionEnabled) { super(type, excludeLocalRoutes, requiresInternetValidation); checkNotNull(allowedAlgorithms, MISSING_PARAM_MSG_TMPL, "Allowed Algorithms"); mServerAddr = serverAddr; mUserIdentity = userIdentity; mPresharedKey = presharedKey == null ? null : Arrays.copyOf(presharedKey, presharedKey.length); mServerRootCaCert = serverRootCaCert; mUsername = username; mPassword = password; mRsaPrivateKey = rsaPrivateKey; mUserCert = userCert; mProxyInfo = (proxyInfo == null) ? null : new ProxyInfo(proxyInfo); // UnmodifiableList doesn't make a defensive copy by default. mAllowedAlgorithms = Collections.unmodifiableList(new ArrayList<>(allowedAlgorithms)); if (excludeLocalRoutes && !isBypassable) { throw new IllegalArgumentException( "Vpn must be bypassable if excludeLocalRoutes is set"); } mIsBypassable = isBypassable; mIsMetered = isMetered; mMaxMtu = maxMtu; mIsRestrictedToTestNetworks = restrictToTestNetworks; mIkeTunConnParams = ikeTunConnParams; mAutomaticNattKeepaliveTimerEnabled = automaticNattKeepaliveTimerEnabled; mAutomaticIpVersionSelectionEnabled = automaticIpVersionSelectionEnabled; validate(); } private void validate() { // IPv6 MTU is greater; since profiles may be started by the system on IPv4 and IPv6 // networks, the VPN must provide a link fulfilling the stricter of the two conditions // (at least that of the IPv6 MTU). if (mMaxMtu < IPV6_MIN_MTU) { throw new IllegalArgumentException("Max MTU must be at least" + IPV6_MIN_MTU); } // Skip validating the other fields if mIkeTunConnParams is set because the required // information should all come from the mIkeTunConnParams. if (mIkeTunConnParams != null) return; // Server Address not validated except to check an address was provided. This allows for // dual-stack servers and hostname based addresses. checkStringNotEmpty(mServerAddr, MISSING_PARAM_MSG_TMPL, "Server Address"); checkStringNotEmpty(mUserIdentity, MISSING_PARAM_MSG_TMPL, "User Identity"); switch (mType) { case TYPE_IKEV2_IPSEC_USER_PASS: checkNotNull(mUsername, MISSING_PARAM_MSG_TMPL, "Username"); checkNotNull(mPassword, MISSING_PARAM_MSG_TMPL, "Password"); if (mServerRootCaCert != null) checkCert(mServerRootCaCert); break; case TYPE_IKEV2_IPSEC_PSK: checkNotNull(mPresharedKey, MISSING_PARAM_MSG_TMPL, "Preshared Key"); break; case TYPE_IKEV2_IPSEC_RSA: checkNotNull(mUserCert, MISSING_PARAM_MSG_TMPL, "User cert"); checkNotNull(mRsaPrivateKey, MISSING_PARAM_MSG_TMPL, "RSA Private key"); checkCert(mUserCert); if (mServerRootCaCert != null) checkCert(mServerRootCaCert); break; default: throw new IllegalArgumentException("Invalid auth method set"); } validateAllowedAlgorithms(mAllowedAlgorithms); } /** * Validates that the allowed algorithms are a valid set for IPsec purposes * *

In order for the algorithm list to be a valid set, it must contain at least one algorithm * that provides Authentication, and one that provides Encryption. Authenticated Encryption with * Associated Data (AEAD) algorithms are counted as providing Authentication and Encryption. * * @param algorithmNames The list to be validated */ private static void validateAllowedAlgorithms(@NonNull List algorithmNames) { // First, make sure no insecure algorithms were proposed. if (algorithmNames.contains(IpSecAlgorithm.AUTH_HMAC_MD5) || algorithmNames.contains(IpSecAlgorithm.AUTH_HMAC_SHA1)) { throw new IllegalArgumentException("Algorithm not supported for IKEv2 VPN profiles"); } // Validate that some valid combination (AEAD or AUTH + CRYPT) is present if (hasAeadAlgorithms(algorithmNames) || hasNormalModeAlgorithms(algorithmNames)) { return; } throw new IllegalArgumentException("Algorithm set missing support for Auth, Crypt or both"); } /** * Checks if the provided list has AEAD algorithms * * @hide */ public static boolean hasAeadAlgorithms(@NonNull List algorithmNames) { return algorithmNames.contains(IpSecAlgorithm.AUTH_CRYPT_AES_GCM); } /** * Checks the provided list has acceptable (non-AEAD) authentication and encryption algorithms * * @hide */ public static boolean hasNormalModeAlgorithms(@NonNull List algorithmNames) { final boolean hasCrypt = algorithmNames.contains(IpSecAlgorithm.CRYPT_AES_CBC); final boolean hasAuth = algorithmNames.contains(IpSecAlgorithm.AUTH_HMAC_SHA256) || algorithmNames.contains(IpSecAlgorithm.AUTH_HMAC_SHA384) || algorithmNames.contains(IpSecAlgorithm.AUTH_HMAC_SHA512); return hasCrypt && hasAuth; } /** Retrieves the server address string. */ @NonNull public String getServerAddr() { if (mIkeTunConnParams == null) return mServerAddr; final IkeSessionParams ikeSessionParams = mIkeTunConnParams.getIkeSessionParams(); return ikeSessionParams.getServerHostname(); } /** Retrieves the user identity. */ @NonNull public String getUserIdentity() { if (mIkeTunConnParams == null) return mUserIdentity; final IkeSessionParams ikeSessionParams = mIkeTunConnParams.getIkeSessionParams(); return getUserIdentityFromIkeSession(ikeSessionParams); } /** * Retrieves the pre-shared key. * *

May be null if the profile is not using Pre-shared key authentication, or the profile is * built from an {@link IkeTunnelConnectionParams}. */ @Nullable public byte[] getPresharedKey() { if (mIkeTunConnParams != null) return null; return mPresharedKey == null ? null : Arrays.copyOf(mPresharedKey, mPresharedKey.length); } /** * Retrieves the certificate for the server's root CA. * *

May be null if the profile is not using RSA Digital Signature Authentication or * Username/Password authentication, or the profile is built from an * {@link IkeTunnelConnectionParams}. */ @Nullable public X509Certificate getServerRootCaCert() { if (mIkeTunConnParams != null) return null; return mServerRootCaCert; } /** * Retrieves the username. * *

May be null if the profile is not using Username/Password authentication, or the profile * is built from an {@link IkeTunnelConnectionParams}. */ @Nullable public String getUsername() { if (mIkeTunConnParams != null) return null; return mUsername; } /** * Retrieves the password. * *

May be null if the profile is not using Username/Password authentication, or the profile * is built from an {@link IkeTunnelConnectionParams}. */ @Nullable public String getPassword() { if (mIkeTunConnParams != null) return null; return mPassword; } /** * Retrieves the RSA private key. * *

May be null if the profile is not using RSA Digital Signature authentication, or the * profile is built from an {@link IkeTunnelConnectionParams}. */ @Nullable public PrivateKey getRsaPrivateKey() { if (mIkeTunConnParams != null) return null; return mRsaPrivateKey; } /** Retrieves the user certificate, if any was set. * *

May be null if the profile is built from an {@link IkeTunnelConnectionParams}. */ @Nullable public X509Certificate getUserCert() { if (mIkeTunConnParams != null) return null; return mUserCert; } /** Retrieves the proxy information if any was set */ @Nullable public ProxyInfo getProxyInfo() { return mProxyInfo; } /** Returns all the algorithms allowed by this VPN profile. * *

May be an empty list if the profile is built from an {@link IkeTunnelConnectionParams}. */ @NonNull public List getAllowedAlgorithms() { if (mIkeTunConnParams != null) return new ArrayList<>(); return mAllowedAlgorithms; } /** Returns whether or not the VPN profile should be bypassable. */ public boolean isBypassable() { return mIsBypassable; } /** Returns whether or not the VPN profile should be always considered metered. */ public boolean isMetered() { return mIsMetered; } /** Retrieves the maximum MTU set for this VPN profile. */ public int getMaxMtu() { return mMaxMtu; } /** Retrieves the ikeTunnelConnectionParams contains IKEv2 configurations, if any was set. */ @Nullable public IkeTunnelConnectionParams getIkeTunnelConnectionParams() { return mIkeTunConnParams; } /** * Returns whether or not this VPN profile is restricted to test networks. * * @hide */ public boolean isRestrictedToTestNetworks() { return mIsRestrictedToTestNetworks; } /** Returns whether automatic NAT-T keepalive timers are enabled. */ public boolean isAutomaticNattKeepaliveTimerEnabled() { return mAutomaticNattKeepaliveTimerEnabled; } /** Returns whether automatic IP version selection is enabled. */ public boolean isAutomaticIpVersionSelectionEnabled() { return mAutomaticIpVersionSelectionEnabled; } @Override public int hashCode() { return Objects.hash( mType, mServerAddr, mUserIdentity, Arrays.hashCode(mPresharedKey), mServerRootCaCert, mUsername, mPassword, mRsaPrivateKey, mUserCert, mProxyInfo, mAllowedAlgorithms, mIsBypassable, mIsMetered, mMaxMtu, mIsRestrictedToTestNetworks, mExcludeLocalRoutes, mRequiresInternetValidation, mIkeTunConnParams, mAutomaticNattKeepaliveTimerEnabled, mAutomaticIpVersionSelectionEnabled); } @Override public boolean equals(@Nullable Object obj) { if (!(obj instanceof Ikev2VpnProfile)) { return false; } final Ikev2VpnProfile other = (Ikev2VpnProfile) obj; return mType == other.mType && Objects.equals(mServerAddr, other.mServerAddr) && Objects.equals(mUserIdentity, other.mUserIdentity) && Arrays.equals(mPresharedKey, other.mPresharedKey) && Objects.equals(mServerRootCaCert, other.mServerRootCaCert) && Objects.equals(mUsername, other.mUsername) && Objects.equals(mPassword, other.mPassword) && Objects.equals(mRsaPrivateKey, other.mRsaPrivateKey) && Objects.equals(mUserCert, other.mUserCert) && Objects.equals(mProxyInfo, other.mProxyInfo) && Objects.equals(mAllowedAlgorithms, other.mAllowedAlgorithms) && mIsBypassable == other.mIsBypassable && mIsMetered == other.mIsMetered && mMaxMtu == other.mMaxMtu && mIsRestrictedToTestNetworks == other.mIsRestrictedToTestNetworks && mExcludeLocalRoutes == other.mExcludeLocalRoutes && mRequiresInternetValidation == other.mRequiresInternetValidation && Objects.equals(mIkeTunConnParams, other.mIkeTunConnParams) && mAutomaticNattKeepaliveTimerEnabled == other.mAutomaticNattKeepaliveTimerEnabled && mAutomaticIpVersionSelectionEnabled == other.mAutomaticIpVersionSelectionEnabled; } /** * Builds a VpnProfile instance for internal use, based on the stored IKEv2/IPsec parameters. * *

Redundant authentication information (from previous calls to other setAuth* methods) will * be discarded. * * @hide */ @NonNull public VpnProfile toVpnProfile() throws IOException, GeneralSecurityException { final VpnProfile profile = new VpnProfile("" /* Key; value unused by IKEv2VpnProfile(s) */, mIsRestrictedToTestNetworks, mExcludeLocalRoutes, mRequiresInternetValidation, mIkeTunConnParams, mAutomaticNattKeepaliveTimerEnabled, mAutomaticIpVersionSelectionEnabled); profile.proxy = mProxyInfo; profile.isBypassable = mIsBypassable; profile.isMetered = mIsMetered; profile.maxMtu = mMaxMtu; profile.areAuthParamsInline = true; profile.saveLogin = true; // The other fields should come from mIkeTunConnParams if it's available. if (mIkeTunConnParams != null) { profile.type = VpnProfile.TYPE_IKEV2_FROM_IKE_TUN_CONN_PARAMS; return profile; } profile.type = mType; profile.server = getServerAddr(); profile.ipsecIdentifier = getUserIdentity(); profile.setAllowedAlgorithms(mAllowedAlgorithms); switch (mType) { case TYPE_IKEV2_IPSEC_USER_PASS: profile.username = mUsername; profile.password = mPassword; profile.ipsecCaCert = mServerRootCaCert == null ? "" : certificateToPemString(mServerRootCaCert); break; case TYPE_IKEV2_IPSEC_PSK: profile.ipsecSecret = encodeForIpsecSecret(mPresharedKey); break; case TYPE_IKEV2_IPSEC_RSA: profile.ipsecUserCert = certificateToPemString(mUserCert); profile.ipsecSecret = PREFIX_INLINE + encodeForIpsecSecret(mRsaPrivateKey.getEncoded()); profile.ipsecCaCert = mServerRootCaCert == null ? "" : certificateToPemString(mServerRootCaCert); break; default: throw new IllegalArgumentException("Invalid auth method set"); } return profile; } private static PrivateKey getPrivateKeyFromAndroidKeystore(String alias) { try { final KeyStore keystore = KeyStore.getInstance(ANDROID_KEYSTORE_PROVIDER); keystore.load(null); final Key key = keystore.getKey(alias, null); if (!(key instanceof PrivateKey)) { throw new IllegalStateException( "Unexpected key type returned from android keystore."); } return (PrivateKey) key; } catch (Exception e) { throw new IllegalStateException("Failed to load key from android keystore.", e); } } /** * Builds the Ikev2VpnProfile from the given profile. * * @param profile the source VpnProfile to build from * @return The IKEv2/IPsec VPN profile * @hide */ @NonNull public static Ikev2VpnProfile fromVpnProfile(@NonNull VpnProfile profile) throws GeneralSecurityException { final Builder builder; if (profile.ikeTunConnParams == null) { builder = new Builder(profile.server, profile.ipsecIdentifier); builder.setAllowedAlgorithms(profile.getAllowedAlgorithms()); switch (profile.type) { case TYPE_IKEV2_IPSEC_USER_PASS: builder.setAuthUsernamePassword( profile.username, profile.password, certificateFromPemString(profile.ipsecCaCert)); break; case TYPE_IKEV2_IPSEC_PSK: builder.setAuthPsk(decodeFromIpsecSecret(profile.ipsecSecret)); break; case TYPE_IKEV2_IPSEC_RSA: final PrivateKey key; if (profile.ipsecSecret.startsWith(PREFIX_KEYSTORE_ALIAS)) { final String alias = profile.ipsecSecret.substring(PREFIX_KEYSTORE_ALIAS.length()); key = getPrivateKeyFromAndroidKeystore(alias); } 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 serverRootCa = certificateFromPemString(profile.ipsecCaCert); builder.setAuthDigitalSignature(userCert, key, serverRootCa); break; default: throw new IllegalArgumentException("Invalid auth method set"); } } else { builder = new Builder(profile.ikeTunConnParams); } builder.setProxy(profile.proxy); builder.setBypassable(profile.isBypassable); builder.setMetered(profile.isMetered); builder.setMaxMtu(profile.maxMtu); if (profile.isRestrictedToTestNetworks) { builder.restrictToTestNetworks(); } if (profile.excludeLocalRoutes && !profile.isBypassable) { Log.w(TAG, "ExcludeLocalRoutes should only be set in the bypassable VPN"); } builder.setLocalRoutesExcluded(profile.excludeLocalRoutes && profile.isBypassable); builder.setRequiresInternetValidation(profile.requiresInternetValidation); builder.setAutomaticNattKeepaliveTimerEnabled(profile.automaticNattKeepaliveTimerEnabled); builder.setAutomaticIpVersionSelectionEnabled(profile.automaticIpVersionSelectionEnabled); 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. * *

Must be public due to runtime-package restrictions. * * @hide */ @NonNull @VisibleForTesting(visibility = Visibility.PRIVATE) public static String certificateToPemString(@Nullable X509Certificate cert) throws IOException, CertificateEncodingException { if (cert == null) { return EMPTY_CERT; } // Credentials.convertToPem outputs ASCII bytes. return new String(Credentials.convertToPem(cert), StandardCharsets.US_ASCII); } /** * Decodes the provided Certificate(s). * *

Will use the first one if the certStr encodes more than one certificate. */ @Nullable private static X509Certificate certificateFromPemString(@Nullable String certStr) throws CertificateException { if (certStr == null || EMPTY_CERT.equals(certStr)) { return null; } try { final List certs = Credentials.convertFromPem(certStr.getBytes(StandardCharsets.US_ASCII)); return certs.isEmpty() ? null : certs.get(0); } catch (IOException e) { throw new CertificateException(e); } } /** @hide */ @NonNull public static String encodeForIpsecSecret(@NonNull byte[] secret) { checkNotNull(secret, MISSING_PARAM_MSG_TMPL, "secret"); return Base64.getEncoder().encodeToString(secret); } @NonNull private static byte[] decodeFromIpsecSecret(@NonNull String encoded) { checkNotNull(encoded, MISSING_PARAM_MSG_TMPL, "encoded"); return Base64.getDecoder().decode(encoded); } @NonNull private static PrivateKey getPrivateKey(@NonNull String keyStr) throws InvalidKeySpecException, NoSuchAlgorithmException { final PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(decodeFromIpsecSecret(keyStr)); final KeyFactory keyFactory = KeyFactory.getInstance("RSA"); return keyFactory.generatePrivate(privateKeySpec); } private static void checkCert(@NonNull X509Certificate cert) { try { certificateToPemString(cert); } catch (GeneralSecurityException | IOException e) { throw new IllegalArgumentException("Certificate could not be encoded"); } } private static @NonNull T checkNotNull( final T reference, final String messageTemplate, final Object... messageArgs) { return Objects.requireNonNull(reference, String.format(messageTemplate, messageArgs)); } private static void checkBuilderSetter(boolean constructedFromIkeTunConParams, @NonNull String field) { if (constructedFromIkeTunConParams) { throw new IllegalArgumentException( field + " can't be set with IkeTunnelConnectionParams builder"); } } @NonNull private static String getUserIdentityFromIkeSession(@NonNull IkeSessionParams params) { final IkeIdentification ident = params.getLocalIdentification(); // Refer to VpnIkev2Utils.parseIkeIdentification(). if (ident instanceof IkeKeyIdIdentification) { return "@#" + new String(((IkeKeyIdIdentification) ident).keyId); } else if (ident instanceof IkeRfc822AddrIdentification) { return "@@" + ((IkeRfc822AddrIdentification) ident).rfc822Name; } else if (ident instanceof IkeFqdnIdentification) { return "@" + ((IkeFqdnIdentification) ident).fqdn; } else if (ident instanceof IkeIpv4AddrIdentification) { return ((IkeIpv4AddrIdentification) ident).ipv4Address.getHostAddress(); } else if (ident instanceof IkeIpv6AddrIdentification) { return ((IkeIpv6AddrIdentification) ident).ipv6Address.getHostAddress(); } else if (ident instanceof IkeDerAsn1DnIdentification) { throw new IllegalArgumentException("Unspported ASN.1 encoded identities"); } else { throw new IllegalArgumentException("Unknown IkeIdentification to get user identity"); } } @Override public String toString() { final StringBuilder sb = new StringBuilder("IkeV2VpnProfile ["); sb.append(" MaxMtu=" + mMaxMtu); if (mIsBypassable) sb.append(" Bypassable"); if (mRequiresInternetValidation) sb.append(" RequiresInternetValidation"); if (mIsRestrictedToTestNetworks) sb.append(" RestrictedToTestNetworks"); if (mAutomaticNattKeepaliveTimerEnabled) sb.append(" AutomaticNattKeepaliveTimerEnabled"); if (mAutomaticIpVersionSelectionEnabled) sb.append(" AutomaticIpVersionSelectionEnabled"); sb.append("]"); return sb.toString(); } /** A incremental builder for IKEv2 VPN profiles */ public static final class Builder { private int mType = -1; @Nullable private final String mServerAddr; @Nullable private final String mUserIdentity; // PSK authentication @Nullable private byte[] mPresharedKey; // Username/Password, RSA authentication @Nullable private X509Certificate mServerRootCaCert; // Username/Password authentication @Nullable private String mUsername; @Nullable private String mPassword; // RSA Certificate authentication @Nullable private PrivateKey mRsaPrivateKey; @Nullable private X509Certificate mUserCert; @Nullable private ProxyInfo mProxyInfo; @NonNull private List mAllowedAlgorithms = DEFAULT_ALGORITHMS; private boolean mRequiresInternetValidation = false; private boolean mIsBypassable = false; private boolean mIsMetered = true; private int mMaxMtu = PlatformVpnProfile.MAX_MTU_DEFAULT; private boolean mIsRestrictedToTestNetworks = false; private boolean mExcludeLocalRoutes = false; private boolean mAutomaticNattKeepaliveTimerEnabled = false; private boolean mAutomaticIpVersionSelectionEnabled = false; @Nullable private final IkeTunnelConnectionParams mIkeTunConnParams; /** * Creates a new builder with the basic parameters of an IKEv2/IPsec VPN. * * @param serverAddr the server that the VPN should connect to * @param identity the identity string to be used for IKEv2 authentication */ @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder(@NonNull String serverAddr, @NonNull String identity) { checkNotNull(serverAddr, MISSING_PARAM_MSG_TMPL, "serverAddr"); checkNotNull(identity, MISSING_PARAM_MSG_TMPL, "identity"); mServerAddr = serverAddr; mUserIdentity = identity; mIkeTunConnParams = null; } /** * Creates a new builder from a {@link IkeTunnelConnectionParams} * * @param ikeTunConnParams the {@link IkeTunnelConnectionParams} contains IKEv2 * configurations */ @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder(@NonNull IkeTunnelConnectionParams ikeTunConnParams) { checkNotNull(ikeTunConnParams, MISSING_PARAM_MSG_TMPL, "ikeTunConnParams"); mIkeTunConnParams = ikeTunConnParams; mServerAddr = null; mUserIdentity = null; } private void resetAuthParams() { mPresharedKey = null; mServerRootCaCert = null; mUsername = null; mPassword = null; mRsaPrivateKey = null; mUserCert = null; } /** * Set the IKEv2 authentication to use the provided username/password. * *

Setting this will configure IKEv2 authentication using EAP-MSCHAPv2. Only one * authentication method may be set. This method will overwrite any previously set * authentication method. * *

If this {@link Builder} is constructed with an {@link IkeTunnelConnectionParams}, * authentication details should be configured there, and calling this method will result * in an exception being thrown. * * @param user the username to be used for EAP-MSCHAPv2 authentication * @param pass the password to be used for EAP-MSCHAPv2 authentication * @param serverRootCa the root certificate to be used for verifying the identity of the * server * @return this {@link Builder} object to facilitate chaining of method calls * @throws IllegalArgumentException if any of the certificates were invalid or of an * unrecognized format */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setAuthUsernamePassword( @NonNull String user, @NonNull String pass, @Nullable X509Certificate serverRootCa) { checkNotNull(user, MISSING_PARAM_MSG_TMPL, "user"); checkNotNull(pass, MISSING_PARAM_MSG_TMPL, "pass"); checkBuilderSetter(mIkeTunConnParams != null, "authUsernamePassword"); // Test to make sure all auth params can be encoded safely. if (serverRootCa != null) checkCert(serverRootCa); resetAuthParams(); mUsername = user; mPassword = pass; mServerRootCaCert = serverRootCa; mType = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; return this; } /** * Set the IKEv2 authentication to use Digital Signature Authentication with the given key. * *

Setting this will configure IKEv2 authentication using a Digital Signature scheme. * Only one authentication method may be set. This method will overwrite any previously set * authentication method. * *

If this {@link Builder} is constructed with an {@link IkeTunnelConnectionParams}, * authentication details should be configured there, and calling this method will result in * an exception being thrown. * * @param userCert the username to be used for RSA Digital signiture authentication * @param key the PrivateKey instance associated with the user ceritificate, used for * constructing the signature * @param serverRootCa the root certificate to be used for verifying the identity of the * server * @return this {@link Builder} object to facilitate chaining of method calls * @throws IllegalArgumentException if any of the certificates were invalid or of an * unrecognized format */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setAuthDigitalSignature( @NonNull X509Certificate userCert, @NonNull PrivateKey key, @Nullable X509Certificate serverRootCa) { checkNotNull(userCert, MISSING_PARAM_MSG_TMPL, "userCert"); checkNotNull(key, MISSING_PARAM_MSG_TMPL, "key"); checkBuilderSetter(mIkeTunConnParams != null, "authDigitalSignature"); // Test to make sure all auth params can be encoded safely. checkCert(userCert); if (serverRootCa != null) checkCert(serverRootCa); resetAuthParams(); mUserCert = userCert; mRsaPrivateKey = key; mServerRootCaCert = serverRootCa; mType = VpnProfile.TYPE_IKEV2_IPSEC_RSA; return this; } /** * Set the IKEv2 authentication to use Preshared keys. * *

Setting this will configure IKEv2 authentication using a Preshared Key. Only one * authentication method may be set. This method will overwrite any previously set * authentication method. * *

If this {@link Builder} is constructed with an {@link IkeTunnelConnectionParams}, * authentication details should be configured there, and calling this method will result in * an exception being thrown. * * @param psk the key to be used for Pre-Shared Key authentication * @return this {@link Builder} object to facilitate chaining of method calls */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setAuthPsk(@NonNull byte[] psk) { checkNotNull(psk, MISSING_PARAM_MSG_TMPL, "psk"); checkBuilderSetter(mIkeTunConnParams != null, "authPsk"); resetAuthParams(); mPresharedKey = psk; mType = VpnProfile.TYPE_IKEV2_IPSEC_PSK; return this; } /** * Sets whether apps can bypass this VPN connection. * *

By default, all traffic from apps are forwarded through the VPN interface and it is * not possible for unprivileged apps to side-step the VPN. If a VPN is set to bypassable, * apps may use methods such as {@link Network#getSocketFactory} or {@link * Network#openConnection} to instead send/receive directly over the underlying network or * any other network they have permissions for. * * @param isBypassable Whether or not the VPN should be considered bypassable. Defaults to * {@code false}. * @return this {@link Builder} object to facilitate chaining of method calls */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setBypassable(boolean isBypassable) { mIsBypassable = isBypassable; return this; } /** * Sets a proxy for the VPN network. * *

Note that this proxy is only a recommendation and it may be ignored by apps. * * @param proxy the ProxyInfo to be set for the VPN network * @return this {@link Builder} object to facilitate chaining of method calls */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setProxy(@Nullable ProxyInfo proxy) { mProxyInfo = proxy; return this; } /** * Set the upper bound of the maximum transmission unit (MTU) of the VPN interface. * *

If it is not set, a safe value will be used. Additionally, the actual link MTU will be * dynamically calculated/updated based on the underlying link's mtu. * * @param mtu the MTU (in bytes) of the VPN interface * @return this {@link Builder} object to facilitate chaining of method calls * @throws IllegalArgumentException if the value is not at least the minimum IPv6 MTU (1280) */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setMaxMtu(int mtu) { // IPv6 MTU is greater; since profiles may be started by the system on IPv4 and IPv6 // networks, the VPN must provide a link fulfilling the stricter of the two conditions // (at least that of the IPv6 MTU). if (mtu < IPV6_MIN_MTU) { throw new IllegalArgumentException("Max MTU must be at least " + IPV6_MIN_MTU); } mMaxMtu = mtu; return this; } /** * Request that this VPN undergoes Internet validation. * * If this is true, the platform will perform basic validation checks for Internet * connectivity over this VPN. If and when they succeed, the VPN network capabilities will * reflect this by gaining the {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} * capability. * * If this is false, the platform assumes the VPN either is always capable of reaching the * Internet or intends not to. In this case, the VPN network capabilities will * always gain the {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} capability * immediately after it connects, whether it can reach public Internet destinations or not. * * @param requiresInternetValidation {@code true} if the framework should attempt to * validate this VPN for Internet connectivity. Defaults * to {@code false}. */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setRequiresInternetValidation(boolean requiresInternetValidation) { mRequiresInternetValidation = requiresInternetValidation; return this; } /** * Marks the VPN network as metered. * *

A VPN network is classified as metered when the user is sensitive to heavy data usage * due to monetary costs and/or data limitations. In such cases, you should set this to * {@code true} so that apps on the system can avoid doing large data transfers. Otherwise, * set this to {@code false}. Doing so would cause VPN network to inherit its meteredness * from the underlying network. * * @param isMetered {@code true} if the VPN network should be treated as metered regardless * of underlying network meteredness. Defaults to {@code true}. * @return this {@link Builder} object to facilitate chaining of method calls * @see NetworkCapabilities#NET_CAPABILITY_NOT_METERED */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setMetered(boolean isMetered) { mIsMetered = isMetered; return this; } /** * Sets the allowable set of IPsec algorithms * *

If set, this will constrain the set of algorithms that the IPsec tunnel will use for * integrity verification and encryption to the provided list. * *

The set of allowed IPsec algorithms is defined in {@link IpSecAlgorithm}. Adding of * algorithms that are considered insecure (such as AUTH_HMAC_MD5 and AUTH_HMAC_SHA1) is not * permitted, and will result in an IllegalArgumentException being thrown. * *

The provided algorithm list must contain at least one algorithm that provides * Authentication, and one that provides Encryption. Authenticated Encryption with * Associated Data (AEAD) algorithms provide both Authentication and Encryption. * *

If this {@link Builder} is constructed with an {@link IkeTunnelConnectionParams}, * authentication details should be configured there, and calling this method will result in * an exception being thrown. * *

By default, this profile will use any algorithm defined in {@link IpSecAlgorithm}, * with the exception of those considered insecure (as described above). * * @param algorithmNames the list of supported IPsec algorithms * @return this {@link Builder} object to facilitate chaining of method calls * @see IpSecAlgorithm */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setAllowedAlgorithms(@NonNull List algorithmNames) { checkNotNull(algorithmNames, MISSING_PARAM_MSG_TMPL, "algorithmNames"); checkBuilderSetter(mIkeTunConnParams != null, "algorithmNames"); validateAllowedAlgorithms(algorithmNames); mAllowedAlgorithms = algorithmNames; return this; } /** * Restricts this profile to use test networks (only). * *

This method is for testing only, and must not be used by apps. Calling * provisionVpnProfile() with a profile where test-network usage is enabled will require the * MANAGE_TEST_NETWORKS permission. * * @hide */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder restrictToTestNetworks() { mIsRestrictedToTestNetworks = true; return this; } /** * Sets the enabled state of the automatic NAT-T keepalive timers * * Note that if this builder was constructed with a {@link IkeTunnelConnectionParams}, * but this is called with {@code true}, the framework will automatically choose the * appropriate keepalive timer and ignore the settings in the session params embedded * in the connection params. * * @param isEnabled {@code true} to enable automatic keepalive timers, based on internal * platform signals. Defaults to {@code false}. * @return this {@link Builder} object to facilitate chaining of method calls */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setAutomaticNattKeepaliveTimerEnabled(boolean isEnabled) { mAutomaticNattKeepaliveTimerEnabled = isEnabled; return this; } /** * Sets the enabled state of the automatic IP version selection * * @param isEnabled {@code true} to enable automatic IP version selection, based on internal * platform signals. Defaults to {@code false}. * @return this {@link Builder} object to facilitate chaining of method calls */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setAutomaticIpVersionSelectionEnabled(boolean isEnabled) { mAutomaticIpVersionSelectionEnabled = isEnabled; return this; } /** * Sets whether the local traffic is exempted from the VPN. * * When this is set, the system will not use the VPN network when an app * tries to send traffic for an IP address that is on a local network. * * Note that there are important security implications. In particular, the * networks that the device connects to typically decides what IP addresses * are part of the local network. This means that for VPNs setting this * flag, it is possible for anybody to set up a public network in such a * way that traffic to arbitrary IP addresses will bypass the VPN, including * traffic to services like DNS. When using this API, please consider the * security implications for your particular case. * * Note that because the local traffic will always bypass the VPN, * it is not possible to set this flag on a non-bypassable VPN. */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Builder setLocalRoutesExcluded(boolean excludeLocalRoutes) { mExcludeLocalRoutes = excludeLocalRoutes; return this; } /** * Validates, builds and provisions the VpnProfile. * * @throws IllegalArgumentException if any of the required keys or values were invalid */ @NonNull @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS) public Ikev2VpnProfile build() { return new Ikev2VpnProfile( mType, mServerAddr, mUserIdentity, mPresharedKey, mServerRootCaCert, mUsername, mPassword, mRsaPrivateKey, mUserCert, mProxyInfo, mAllowedAlgorithms, mIsBypassable, mIsMetered, mMaxMtu, mIsRestrictedToTestNetworks, mExcludeLocalRoutes, mRequiresInternetValidation, mIkeTunConnParams, mAutomaticNattKeepaliveTimerEnabled, mAutomaticIpVersionSelectionEnabled); } } }