/* * Copyright (C) 2011 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 com.android.settings.vpn2; import static com.android.internal.net.VpnProfile.isLegacyType; import android.content.Context; import android.content.DialogInterface; import android.content.pm.PackageManager; import android.net.ProxyInfo; import android.os.Bundle; import android.os.SystemProperties; import android.text.Editable; import android.text.TextWatcher; import android.util.Log; import android.view.View; import android.view.WindowManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.Spinner; import android.widget.TextView; import androidx.appcompat.app.AlertDialog; import com.android.internal.net.VpnProfile; import com.android.net.module.util.ProxyUtils; import com.android.settings.R; import com.android.settings.utils.AndroidKeystoreAliasLoader; import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; /** * Dialog showing information about a VPN configuration. The dialog * can be launched to either edit or prompt for credentials to connect * to a user-added VPN. * * {@see AppDialog} */ class ConfigDialog extends AlertDialog implements TextWatcher, View.OnClickListener, AdapterView.OnItemSelectedListener, CompoundButton.OnCheckedChangeListener { private static final String TAG = "ConfigDialog"; private final DialogInterface.OnClickListener mListener; private final VpnProfile mProfile; private boolean mEditing; private boolean mExists; private List mTotalTypes; private List mAllowedTypes; private View mView; private TextView mName; private Spinner mType; private TextView mServer; private TextView mUsername; private TextView mPassword; private TextView mSearchDomains; private TextView mDnsServers; private TextView mRoutes; private Spinner mProxySettings; private TextView mProxyHost; private TextView mProxyPort; private CheckBox mMppe; private TextView mL2tpSecret; private TextView mIpsecIdentifier; private TextView mIpsecSecret; private Spinner mIpsecUserCert; private Spinner mIpsecCaCert; private Spinner mIpsecServerCert; private CheckBox mSaveLogin; private CheckBox mShowOptions; private CheckBox mAlwaysOnVpn; private TextView mAlwaysOnInvalidReason; ConfigDialog(Context context, DialogInterface.OnClickListener listener, VpnProfile profile, boolean editing, boolean exists) { super(context); mListener = listener; mProfile = profile; mEditing = editing; mExists = exists; } @Override protected void onCreate(Bundle savedState) { mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null); setView(mView); Context context = getContext(); // First, find out all the fields. mName = (TextView) mView.findViewById(R.id.name); mType = (Spinner) mView.findViewById(R.id.type); mServer = (TextView) mView.findViewById(R.id.server); mUsername = (TextView) mView.findViewById(R.id.username); mPassword = (TextView) mView.findViewById(R.id.password); mSearchDomains = (TextView) mView.findViewById(R.id.search_domains); mDnsServers = (TextView) mView.findViewById(R.id.dns_servers); mRoutes = (TextView) mView.findViewById(R.id.routes); mProxySettings = (Spinner) mView.findViewById(R.id.vpn_proxy_settings); mProxyHost = (TextView) mView.findViewById(R.id.vpn_proxy_host); mProxyPort = (TextView) mView.findViewById(R.id.vpn_proxy_port); mMppe = (CheckBox) mView.findViewById(R.id.mppe); mL2tpSecret = (TextView) mView.findViewById(R.id.l2tp_secret); mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier); mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret); mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert); mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert); mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert); mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login); mShowOptions = (CheckBox) mView.findViewById(R.id.show_options); mAlwaysOnVpn = (CheckBox) mView.findViewById(R.id.always_on_vpn); mAlwaysOnInvalidReason = (TextView) mView.findViewById(R.id.always_on_invalid_reason); // Second, copy values from the profile. mName.setText(mProfile.name); setTypesByFeature(mType); // Not all types will be available to the user. Find the index corresponding to the // string of the profile's type. if (mAllowedTypes != null && mTotalTypes != null) { mType.setSelection(mAllowedTypes.indexOf(mTotalTypes.get(mProfile.type))); } else { Log.w(TAG, "Allowed or Total vpn types not initialized when setting initial selection"); } mServer.setText(mProfile.server); if (mProfile.saveLogin) { mUsername.setText(mProfile.username); mPassword.setText(mProfile.password); } mSearchDomains.setText(mProfile.searchDomains); mDnsServers.setText(mProfile.dnsServers); mRoutes.setText(mProfile.routes); if (mProfile.proxy != null) { mProxyHost.setText(mProfile.proxy.getHost()); int port = mProfile.proxy.getPort(); mProxyPort.setText(port == 0 ? "" : Integer.toString(port)); } mMppe.setChecked(mProfile.mppe); mL2tpSecret.setText(mProfile.l2tpSecret); mL2tpSecret.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium); mIpsecIdentifier.setText(mProfile.ipsecIdentifier); mIpsecSecret.setText(mProfile.ipsecSecret); final AndroidKeystoreAliasLoader androidKeystoreAliasLoader = new AndroidKeystoreAliasLoader(null); loadCertificates(mIpsecUserCert, androidKeystoreAliasLoader.getKeyCertAliases(), 0, mProfile.ipsecUserCert); loadCertificates(mIpsecCaCert, androidKeystoreAliasLoader.getCaCertAliases(), R.string.vpn_no_ca_cert, mProfile.ipsecCaCert); loadCertificates(mIpsecServerCert, androidKeystoreAliasLoader.getKeyCertAliases(), R.string.vpn_no_server_cert, mProfile.ipsecServerCert); mSaveLogin.setChecked(mProfile.saveLogin); mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn())); mPassword.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium); // Hide lockdown VPN on devices that require IMS authentication if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) { mAlwaysOnVpn.setVisibility(View.GONE); } // Third, add listeners to required fields. mName.addTextChangedListener(this); mType.setOnItemSelectedListener(this); mServer.addTextChangedListener(this); mUsername.addTextChangedListener(this); mPassword.addTextChangedListener(this); mDnsServers.addTextChangedListener(this); mRoutes.addTextChangedListener(this); mProxySettings.setOnItemSelectedListener(this); mProxyHost.addTextChangedListener(this); mProxyPort.addTextChangedListener(this); mIpsecIdentifier.addTextChangedListener(this); mIpsecSecret.addTextChangedListener(this); mIpsecUserCert.setOnItemSelectedListener(this); mShowOptions.setOnClickListener(this); mAlwaysOnVpn.setOnCheckedChangeListener(this); // Fourth, determine whether to do editing or connecting. mEditing = mEditing || !validate(true /*editing*/); if (mEditing) { setTitle(R.string.vpn_edit); // Show common fields. mView.findViewById(R.id.editor).setVisibility(View.VISIBLE); // Show type-specific fields. changeType(mProfile.type); // Hide 'save login' when we are editing. mSaveLogin.setVisibility(View.GONE); configureAdvancedOptionsVisibility(); if (mExists) { // Create a button to forget the profile if it has already been saved.. setButton(DialogInterface.BUTTON_NEUTRAL, context.getString(R.string.vpn_forget), mListener); // Display warning subtitle if the existing VPN is an insecure type... if (VpnProfile.isLegacyType(mProfile.type)) { TextView subtitle = mView.findViewById(R.id.dialog_alert_subtitle); subtitle.setVisibility(View.VISIBLE); } } // Create a button to save the profile. setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.vpn_save), mListener); } else { setTitle(context.getString(R.string.vpn_connect_to, mProfile.name)); setUsernamePasswordVisibility(mProfile.type); // Create a button to connect the network. setButton(DialogInterface.BUTTON_POSITIVE, context.getString(R.string.vpn_connect), mListener); } // Always provide a cancel button. setButton(DialogInterface.BUTTON_NEGATIVE, context.getString(R.string.vpn_cancel), mListener); // Let AlertDialog create everything. super.onCreate(savedState); // Update UI controls according to the current configuration. updateUiControls(); // Workaround to resize the dialog for the input method. getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE | WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); } @Override public void onRestoreInstanceState(Bundle savedState) { super.onRestoreInstanceState(savedState); // Visibility isn't restored by super.onRestoreInstanceState, so re-show the advanced // options here if they were already revealed or set. configureAdvancedOptionsVisibility(); } @Override public void afterTextChanged(Editable field) { updateUiControls(); } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void onClick(View view) { if (view == mShowOptions) { configureAdvancedOptionsVisibility(); } } @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { if (parent == mType) { // Because the spinner may not display all available types, // convert the selected position into the actual vpn profile type integer. final int profileType = convertAllowedIndexToProfileType(position); changeType(profileType); } else if (parent == mProxySettings) { updateProxyFieldsVisibility(position); } updateUiControls(); } @Override public void onNothingSelected(AdapterView parent) { } @Override public void onCheckedChanged(CompoundButton compoundButton, boolean b) { if (compoundButton == mAlwaysOnVpn) { updateUiControls(); } } public boolean isVpnAlwaysOn() { return mAlwaysOnVpn.isChecked(); } /** * Updates the UI according to the current configuration entered by the user. * * These include: * "Always-on VPN" checkbox * Reason for "Always-on VPN" being disabled, when necessary * Proxy info if manually configured * "Save account information" checkbox * "Save" and "Connect" buttons */ private void updateUiControls() { VpnProfile profile = getProfile(); // Always-on VPN if (profile.isValidLockdownProfile()) { mAlwaysOnVpn.setEnabled(true); mAlwaysOnInvalidReason.setVisibility(View.GONE); } else { mAlwaysOnVpn.setChecked(false); mAlwaysOnVpn.setEnabled(false); if (!profile.isTypeValidForLockdown()) { mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_type); } else if (isLegacyType(profile.type) && !profile.isServerAddressNumeric()) { mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_server); } else if (isLegacyType(profile.type) && !profile.hasDns()) { mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_no_dns); } else if (isLegacyType(profile.type) && !profile.areDnsAddressesNumeric()) { mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_dns); } else { mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_other); } mAlwaysOnInvalidReason.setVisibility(View.VISIBLE); } // Show proxy fields if any proxy field is filled. if (mProfile.proxy != null && (!mProfile.proxy.getHost().isEmpty() || mProfile.proxy.getPort() != 0)) { mProxySettings.setSelection(VpnProfile.PROXY_MANUAL); updateProxyFieldsVisibility(VpnProfile.PROXY_MANUAL); } // Save account information if (mAlwaysOnVpn.isChecked()) { mSaveLogin.setChecked(true); mSaveLogin.setEnabled(false); } else { mSaveLogin.setChecked(mProfile.saveLogin); mSaveLogin.setEnabled(true); } // Save or Connect button getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing)); } private void updateProxyFieldsVisibility(int position) { final int visible = position == VpnProfile.PROXY_MANUAL ? View.VISIBLE : View.GONE; mView.findViewById(R.id.vpn_proxy_fields).setVisibility(visible); } private boolean isAdvancedOptionsEnabled() { return mSearchDomains.getText().length() > 0 || mDnsServers.getText().length() > 0 || mRoutes.getText().length() > 0 || mProxyHost.getText().length() > 0 || mProxyPort.getText().length() > 0; } private void configureAdvancedOptionsVisibility() { if (mShowOptions.isChecked() || isAdvancedOptionsEnabled()) { mView.findViewById(R.id.options).setVisibility(View.VISIBLE); mShowOptions.setVisibility(View.GONE); // Configure networking option visibility // TODO(b/149070123): Add ability for platform VPNs to support DNS & routes final int visibility = isLegacyType(getSelectedVpnType()) ? View.VISIBLE : View.GONE; mView.findViewById(R.id.network_options).setVisibility(visibility); } else { mView.findViewById(R.id.options).setVisibility(View.GONE); mShowOptions.setVisibility(View.VISIBLE); } } private void changeType(int type) { // First, hide everything. mMppe.setVisibility(View.GONE); mView.findViewById(R.id.l2tp).setVisibility(View.GONE); mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE); mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE); mView.findViewById(R.id.ipsec_peer).setVisibility(View.GONE); mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.GONE); setUsernamePasswordVisibility(type); // Always enable identity for IKEv2/IPsec profiles. if (!isLegacyType(type)) { mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.VISIBLE); } // Then, unhide type-specific fields. switch (type) { case VpnProfile.TYPE_PPTP: mMppe.setVisibility(View.VISIBLE); break; case VpnProfile.TYPE_L2TP_IPSEC_PSK: mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE); // fall through case VpnProfile.TYPE_IKEV2_IPSEC_PSK: // fall through case VpnProfile.TYPE_IPSEC_XAUTH_PSK: mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE); mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.VISIBLE); break; case VpnProfile.TYPE_L2TP_IPSEC_RSA: mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE); // fall through case VpnProfile.TYPE_IKEV2_IPSEC_RSA: // fall through case VpnProfile.TYPE_IPSEC_XAUTH_RSA: mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE); // fall through case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: // fall through case VpnProfile.TYPE_IPSEC_HYBRID_RSA: mView.findViewById(R.id.ipsec_peer).setVisibility(View.VISIBLE); break; } configureAdvancedOptionsVisibility(); } private boolean validate(boolean editing) { if (mAlwaysOnVpn.isChecked() && !getProfile().isValidLockdownProfile()) { return false; } final int type = getSelectedVpnType(); if (!editing && requiresUsernamePassword(type)) { return mUsername.getText().length() != 0 && mPassword.getText().length() != 0; } if (mName.getText().length() == 0 || mServer.getText().length() == 0) { return false; } // TODO(b/149070123): Add ability for platform VPNs to support DNS & routes if (isLegacyType(mProfile.type) && (!validateAddresses(mDnsServers.getText().toString(), false) || !validateAddresses(mRoutes.getText().toString(), true))) { return false; } // All IKEv2 methods require an identifier if (!isLegacyType(mProfile.type) && mIpsecIdentifier.getText().length() == 0) { return false; } if (!validateProxy()) { return false; } switch (type) { case VpnProfile.TYPE_PPTP: // fall through case VpnProfile.TYPE_IPSEC_HYBRID_RSA: // fall through case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: return true; case VpnProfile.TYPE_IKEV2_IPSEC_PSK: // fall through case VpnProfile.TYPE_L2TP_IPSEC_PSK: // fall through case VpnProfile.TYPE_IPSEC_XAUTH_PSK: return mIpsecSecret.getText().length() != 0; case VpnProfile.TYPE_IKEV2_IPSEC_RSA: // fall through case VpnProfile.TYPE_L2TP_IPSEC_RSA: // fall through case VpnProfile.TYPE_IPSEC_XAUTH_RSA: return mIpsecUserCert.getSelectedItemPosition() != 0; } return false; } private boolean validateAddresses(String addresses, boolean cidr) { try { for (String address : addresses.split(" ")) { if (address.isEmpty()) { continue; } // Legacy VPN currently only supports IPv4. int prefixLength = 32; if (cidr) { String[] parts = address.split("/", 2); address = parts[0]; prefixLength = Integer.parseInt(parts[1]); } byte[] bytes = InetAddress.parseNumericAddress(address).getAddress(); int integer = (bytes[3] & 0xFF) | (bytes[2] & 0xFF) << 8 | (bytes[1] & 0xFF) << 16 | (bytes[0] & 0xFF) << 24; if (bytes.length != 4 || prefixLength < 0 || prefixLength > 32 || (prefixLength < 32 && (integer << prefixLength) != 0)) { return false; } } } catch (Exception e) { return false; } return true; } private void setTypesByFeature(Spinner typeSpinner) { String[] types = getContext().getResources().getStringArray(R.array.vpn_types); mTotalTypes = new ArrayList<>(Arrays.asList(types)); mAllowedTypes = new ArrayList<>(Arrays.asList(types)); // Although FEATURE_IPSEC_TUNNELS should always be present in android S and beyond, // keep this check here just to be safe. if (!getContext().getPackageManager().hasSystemFeature( PackageManager.FEATURE_IPSEC_TUNNELS)) { Log.wtf(TAG, "FEATURE_IPSEC_TUNNELS missing from system"); } // If the vpn is new or is not already a legacy type, // don't allow the user to set the type to a legacy option. // Set the mProfile.type to TYPE_IKEV2_IPSEC_USER_PASS if the VPN not exist if (!mExists) { mProfile.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS; } // Remove all types which are legacy types from the typesList if (!VpnProfile.isLegacyType(mProfile.type)) { for (int i = mAllowedTypes.size() - 1; i >= 0; i--) { // This must be removed from back to front in order to ensure index consistency if (VpnProfile.isLegacyType(i)) { mAllowedTypes.remove(i); } } types = mAllowedTypes.toArray(new String[0]); } final ArrayAdapter adapter = new ArrayAdapter( getContext(), android.R.layout.simple_spinner_item, types); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); typeSpinner.setAdapter(adapter); } private void loadCertificates(Spinner spinner, Collection choices, int firstId, String selected) { Context context = getContext(); String first = (firstId == 0) ? "" : context.getString(firstId); String[] myChoices; if (choices == null || choices.size() == 0) { myChoices = new String[] {first}; } else { myChoices = new String[choices.size() + 1]; myChoices[0] = first; int i = 1; for (String c : choices) { myChoices[i++] = c; } } ArrayAdapter adapter = new ArrayAdapter( context, android.R.layout.simple_spinner_item, myChoices); adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinner.setAdapter(adapter); for (int i = 1; i < myChoices.length; ++i) { if (myChoices[i].equals(selected)) { spinner.setSelection(i); break; } } } private void setUsernamePasswordVisibility(int type) { mView.findViewById(R.id.userpass).setVisibility( requiresUsernamePassword(type) ? View.VISIBLE : View.GONE); } private boolean requiresUsernamePassword(int type) { switch (type) { case VpnProfile.TYPE_IKEV2_IPSEC_RSA: // fall through case VpnProfile.TYPE_IKEV2_IPSEC_PSK: return false; default: return true; } } boolean isEditing() { return mEditing; } boolean hasProxy() { return mProxySettings.getSelectedItemPosition() == VpnProfile.PROXY_MANUAL; } VpnProfile getProfile() { // First, save common fields. VpnProfile profile = new VpnProfile(mProfile.key); profile.name = mName.getText().toString(); profile.type = getSelectedVpnType(); profile.server = mServer.getText().toString().trim(); profile.username = mUsername.getText().toString(); profile.password = mPassword.getText().toString(); // Save fields based on VPN type. if (isLegacyType(profile.type)) { // TODO(b/149070123): Add ability for platform VPNs to support DNS & routes profile.searchDomains = mSearchDomains.getText().toString().trim(); profile.dnsServers = mDnsServers.getText().toString().trim(); profile.routes = mRoutes.getText().toString().trim(); } else { profile.ipsecIdentifier = mIpsecIdentifier.getText().toString(); } if (hasProxy()) { String proxyHost = mProxyHost.getText().toString().trim(); String proxyPort = mProxyPort.getText().toString().trim(); // 0 is a last resort default, but the interface validates that the proxy port is // present and non-zero. int port = proxyPort.isEmpty() ? 0 : Integer.parseInt(proxyPort); profile.proxy = ProxyInfo.buildDirectProxy(proxyHost, port); } else { profile.proxy = null; } // Then, save type-specific fields. switch (profile.type) { case VpnProfile.TYPE_PPTP: profile.mppe = mMppe.isChecked(); break; case VpnProfile.TYPE_L2TP_IPSEC_PSK: profile.l2tpSecret = mL2tpSecret.getText().toString(); // fall through case VpnProfile.TYPE_IKEV2_IPSEC_PSK: // fall through case VpnProfile.TYPE_IPSEC_XAUTH_PSK: profile.ipsecIdentifier = mIpsecIdentifier.getText().toString(); profile.ipsecSecret = mIpsecSecret.getText().toString(); break; case VpnProfile.TYPE_L2TP_IPSEC_RSA: profile.l2tpSecret = mL2tpSecret.getText().toString(); // fall through case VpnProfile.TYPE_IKEV2_IPSEC_RSA: // fall through case VpnProfile.TYPE_IPSEC_XAUTH_RSA: if (mIpsecUserCert.getSelectedItemPosition() != 0) { profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem(); } // fall through case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS: // fall through case VpnProfile.TYPE_IPSEC_HYBRID_RSA: if (mIpsecCaCert.getSelectedItemPosition() != 0) { profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem(); } if (mIpsecServerCert.getSelectedItemPosition() != 0) { profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem(); } break; } final boolean hasLogin = !profile.username.isEmpty() || !profile.password.isEmpty(); profile.saveLogin = mSaveLogin.isChecked() || (mEditing && hasLogin); return profile; } private boolean validateProxy() { if (!hasProxy()) { return true; } final String host = mProxyHost.getText().toString().trim(); final String port = mProxyPort.getText().toString().trim(); return ProxyUtils.validate(host, port, "") == ProxyUtils.PROXY_VALID; } private int getSelectedVpnType() { return convertAllowedIndexToProfileType(mType.getSelectedItemPosition()); } private int convertAllowedIndexToProfileType(int allowedSelectedPosition) { if (mAllowedTypes != null && mTotalTypes != null) { final String typeString = mAllowedTypes.get(allowedSelectedPosition); final int profileType = mTotalTypes.indexOf(typeString); return profileType; } else { Log.w(TAG, "Allowed or Total vpn types not initialized when converting protileType"); return allowedSelectedPosition; } } }