1 /*
2  * Copyright (C) 2011 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.vpn2;
18 
19 import android.content.Context;
20 import android.content.DialogInterface;
21 import android.content.pm.PackageManager;
22 import android.net.ProxyInfo;
23 import android.os.Bundle;
24 import android.os.SystemProperties;
25 import android.text.Editable;
26 import android.text.TextWatcher;
27 import android.util.Log;
28 import android.view.View;
29 import android.view.WindowManager;
30 import android.widget.AdapterView;
31 import android.widget.ArrayAdapter;
32 import android.widget.CheckBox;
33 import android.widget.CompoundButton;
34 import android.widget.Spinner;
35 import android.widget.TextView;
36 
37 import androidx.appcompat.app.AlertDialog;
38 
39 import com.android.internal.net.VpnProfile;
40 import com.android.net.module.util.ProxyUtils;
41 import com.android.settings.R;
42 import com.android.settings.utils.AndroidKeystoreAliasLoader;
43 
44 import java.util.Collection;
45 import java.util.List;
46 
47 /**
48  * Dialog showing information about a VPN configuration. The dialog
49  * can be launched to either edit or prompt for credentials to connect
50  * to a user-added VPN.
51  *
52  * {@see AppDialog}
53  */
54 class ConfigDialog extends AlertDialog implements TextWatcher,
55         View.OnClickListener, AdapterView.OnItemSelectedListener,
56         CompoundButton.OnCheckedChangeListener {
57     private static final String TAG = "ConfigDialog";
58     // Vpn profile constants to match with R.array.vpn_types.
59     private static final List<Integer> VPN_TYPES = List.of(
60             VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS,
61             VpnProfile.TYPE_IKEV2_IPSEC_PSK,
62             VpnProfile.TYPE_IKEV2_IPSEC_RSA
63     );
64 
65     private final DialogInterface.OnClickListener mListener;
66     private final VpnProfile mProfile;
67 
68     private boolean mEditing;
69     private boolean mExists;
70 
71     private View mView;
72 
73     private TextView mName;
74     private Spinner mType;
75     private TextView mServer;
76     private TextView mUsername;
77     private TextView mPassword;
78     private Spinner mProxySettings;
79     private TextView mProxyHost;
80     private TextView mProxyPort;
81     private TextView mIpsecIdentifier;
82     private TextView mIpsecSecret;
83     private Spinner mIpsecUserCert;
84     private Spinner mIpsecCaCert;
85     private Spinner mIpsecServerCert;
86     private CheckBox mSaveLogin;
87     private CheckBox mShowOptions;
88     private CheckBox mAlwaysOnVpn;
89     private TextView mAlwaysOnInvalidReason;
90 
ConfigDialog(Context context, DialogInterface.OnClickListener listener, VpnProfile profile, boolean editing, boolean exists)91     ConfigDialog(Context context, DialogInterface.OnClickListener listener,
92             VpnProfile profile, boolean editing, boolean exists) {
93         super(context);
94 
95         mListener = listener;
96         mProfile = profile;
97         mEditing = editing;
98         mExists = exists;
99     }
100 
101     @Override
onCreate(Bundle savedState)102     protected void onCreate(Bundle savedState) {
103         mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null);
104         setView(mView);
105 
106         Context context = getContext();
107 
108         // First, find out all the fields.
109         mName = (TextView) mView.findViewById(R.id.name);
110         mType = (Spinner) mView.findViewById(R.id.type);
111         mServer = (TextView) mView.findViewById(R.id.server);
112         mUsername = (TextView) mView.findViewById(R.id.username);
113         mPassword = (TextView) mView.findViewById(R.id.password);
114         mProxySettings = (Spinner) mView.findViewById(R.id.vpn_proxy_settings);
115         mProxyHost = (TextView) mView.findViewById(R.id.vpn_proxy_host);
116         mProxyPort = (TextView) mView.findViewById(R.id.vpn_proxy_port);
117         mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier);
118         mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret);
119         mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert);
120         mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert);
121         mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert);
122         mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login);
123         mShowOptions = (CheckBox) mView.findViewById(R.id.show_options);
124         mAlwaysOnVpn = (CheckBox) mView.findViewById(R.id.always_on_vpn);
125         mAlwaysOnInvalidReason = (TextView) mView.findViewById(R.id.always_on_invalid_reason);
126 
127         // Second, copy values from the profile.
128         mName.setText(mProfile.name);
129         setTypesByFeature(mType);
130         mType.setSelection(convertVpnProfileConstantToTypeIndex(mProfile.type));
131         mServer.setText(mProfile.server);
132         if (mProfile.saveLogin) {
133             mUsername.setText(mProfile.username);
134             mPassword.setText(mProfile.password);
135         }
136         if (mProfile.proxy != null) {
137             mProxyHost.setText(mProfile.proxy.getHost());
138             int port = mProfile.proxy.getPort();
139             mProxyPort.setText(port == 0 ? "" : Integer.toString(port));
140         }
141         mIpsecIdentifier.setText(mProfile.ipsecIdentifier);
142         mIpsecSecret.setText(mProfile.ipsecSecret);
143         final AndroidKeystoreAliasLoader androidKeystoreAliasLoader =
144                 new AndroidKeystoreAliasLoader(null);
145         loadCertificates(mIpsecUserCert, androidKeystoreAliasLoader.getKeyCertAliases(), 0,
146                 mProfile.ipsecUserCert);
147         loadCertificates(mIpsecCaCert, androidKeystoreAliasLoader.getCaCertAliases(),
148                 R.string.vpn_no_ca_cert, mProfile.ipsecCaCert);
149         loadCertificates(mIpsecServerCert, androidKeystoreAliasLoader.getKeyCertAliases(),
150                 R.string.vpn_no_server_cert, mProfile.ipsecServerCert);
151         mSaveLogin.setChecked(mProfile.saveLogin);
152         mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn()));
153         mPassword.setTextAppearance(android.R.style.TextAppearance_DeviceDefault_Medium);
154 
155         // Hide lockdown VPN on devices that require IMS authentication
156         if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) {
157             mAlwaysOnVpn.setVisibility(View.GONE);
158         }
159 
160         // Third, add listeners to required fields.
161         mName.addTextChangedListener(this);
162         mType.setOnItemSelectedListener(this);
163         mServer.addTextChangedListener(this);
164         mUsername.addTextChangedListener(this);
165         mPassword.addTextChangedListener(this);
166         mProxySettings.setOnItemSelectedListener(this);
167         mProxyHost.addTextChangedListener(this);
168         mProxyPort.addTextChangedListener(this);
169         mIpsecIdentifier.addTextChangedListener(this);
170         mIpsecSecret.addTextChangedListener(this);
171         mIpsecUserCert.setOnItemSelectedListener(this);
172         mShowOptions.setOnClickListener(this);
173         mAlwaysOnVpn.setOnCheckedChangeListener(this);
174 
175         // Fourth, determine whether to do editing or connecting.
176         mEditing = mEditing || !validate(true /*editing*/);
177 
178         if (mEditing) {
179             setTitle(R.string.vpn_edit);
180 
181             // Show common fields.
182             mView.findViewById(R.id.editor).setVisibility(View.VISIBLE);
183 
184             // Show type-specific fields.
185             changeType(mProfile.type);
186 
187             // Hide 'save login' when we are editing.
188             mSaveLogin.setVisibility(View.GONE);
189 
190             configureAdvancedOptionsVisibility();
191 
192             if (mExists) {
193                 // Create a button to forget the profile if it has already been saved..
194                 setButton(DialogInterface.BUTTON_NEUTRAL,
195                         context.getString(R.string.vpn_forget), mListener);
196             }
197 
198             // Create a button to save the profile.
199             setButton(DialogInterface.BUTTON_POSITIVE,
200                     context.getString(R.string.vpn_save), mListener);
201         } else {
202             setTitle(context.getString(R.string.vpn_connect_to, mProfile.name));
203 
204             setUsernamePasswordVisibility(mProfile.type);
205 
206             // Create a button to connect the network.
207             setButton(DialogInterface.BUTTON_POSITIVE,
208                     context.getString(R.string.vpn_connect), mListener);
209         }
210 
211         // Always provide a cancel button.
212         setButton(DialogInterface.BUTTON_NEGATIVE,
213                 context.getString(R.string.vpn_cancel), mListener);
214 
215         // Let AlertDialog create everything.
216         super.onCreate(savedState);
217 
218         // Update UI controls according to the current configuration.
219         updateUiControls();
220 
221         // Workaround to resize the dialog for the input method.
222         getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
223                 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
224     }
225 
226     @Override
onRestoreInstanceState(Bundle savedState)227     public void onRestoreInstanceState(Bundle savedState) {
228         super.onRestoreInstanceState(savedState);
229 
230         // Visibility isn't restored by super.onRestoreInstanceState, so re-show the advanced
231         // options here if they were already revealed or set.
232         configureAdvancedOptionsVisibility();
233     }
234 
235     @Override
afterTextChanged(Editable field)236     public void afterTextChanged(Editable field) {
237         updateUiControls();
238     }
239 
240     @Override
beforeTextChanged(CharSequence s, int start, int count, int after)241     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
242     }
243 
244     @Override
onTextChanged(CharSequence s, int start, int before, int count)245     public void onTextChanged(CharSequence s, int start, int before, int count) {
246     }
247 
248     @Override
onClick(View view)249     public void onClick(View view) {
250         if (view == mShowOptions) {
251             configureAdvancedOptionsVisibility();
252         }
253     }
254 
255     @Override
onItemSelected(AdapterView<?> parent, View view, int position, long id)256     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
257         if (parent == mType) {
258             changeType(VPN_TYPES.get(position));
259         } else if (parent == mProxySettings) {
260             updateProxyFieldsVisibility(position);
261         }
262         updateUiControls();
263     }
264 
265     @Override
onNothingSelected(AdapterView<?> parent)266     public void onNothingSelected(AdapterView<?> parent) {
267     }
268 
269     @Override
onCheckedChanged(CompoundButton compoundButton, boolean b)270     public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
271         if (compoundButton == mAlwaysOnVpn) {
272             updateUiControls();
273         }
274     }
275 
isVpnAlwaysOn()276     public boolean isVpnAlwaysOn() {
277         return mAlwaysOnVpn.isChecked();
278     }
279 
280     /**
281      * Updates the UI according to the current configuration entered by the user.
282      *
283      * These include:
284      * "Always-on VPN" checkbox
285      * Reason for "Always-on VPN" being disabled, when necessary
286      * Proxy info if manually configured
287      * "Save account information" checkbox
288      * "Save" and "Connect" buttons
289      */
updateUiControls()290     private void updateUiControls() {
291         VpnProfile profile = getProfile();
292 
293         // Always-on VPN
294         if (profile.isValidLockdownProfile()) {
295             mAlwaysOnVpn.setEnabled(true);
296             mAlwaysOnInvalidReason.setVisibility(View.GONE);
297         } else {
298             mAlwaysOnVpn.setChecked(false);
299             mAlwaysOnVpn.setEnabled(false);
300             mAlwaysOnInvalidReason.setText(R.string.vpn_always_on_invalid_reason_other);
301             mAlwaysOnInvalidReason.setVisibility(View.VISIBLE);
302         }
303 
304         // Show proxy fields if any proxy field is filled.
305         if (mProfile.proxy != null && (!mProfile.proxy.getHost().isEmpty() ||
306                 mProfile.proxy.getPort() != 0)) {
307             mProxySettings.setSelection(VpnProfile.PROXY_MANUAL);
308             updateProxyFieldsVisibility(VpnProfile.PROXY_MANUAL);
309         }
310 
311         // Save account information
312         if (mAlwaysOnVpn.isChecked()) {
313             mSaveLogin.setChecked(true);
314             mSaveLogin.setEnabled(false);
315         } else {
316             mSaveLogin.setChecked(mProfile.saveLogin);
317             mSaveLogin.setEnabled(true);
318         }
319 
320         // Save or Connect button
321         getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
322     }
323 
updateProxyFieldsVisibility(int position)324     private void updateProxyFieldsVisibility(int position) {
325         final int visible = position == VpnProfile.PROXY_MANUAL ? View.VISIBLE : View.GONE;
326         mView.findViewById(R.id.vpn_proxy_fields).setVisibility(visible);
327     }
328 
isAdvancedOptionsEnabled()329     private boolean isAdvancedOptionsEnabled() {
330         return mProxyHost.getText().length() > 0 || mProxyPort.getText().length() > 0;
331     }
332 
configureAdvancedOptionsVisibility()333     private void configureAdvancedOptionsVisibility() {
334         if (mShowOptions.isChecked() || isAdvancedOptionsEnabled()) {
335             mView.findViewById(R.id.options).setVisibility(View.VISIBLE);
336             mShowOptions.setVisibility(View.GONE);
337             // TODO(b/149070123): Add ability for platform VPNs to support DNS & routes
338         } else {
339             mView.findViewById(R.id.options).setVisibility(View.GONE);
340             mShowOptions.setVisibility(View.VISIBLE);
341         }
342     }
343 
changeType(int type)344     private void changeType(int type) {
345         // First, hide everything.
346         mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE);
347         mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE);
348         mView.findViewById(R.id.ipsec_peer).setVisibility(View.GONE);
349         mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.GONE);
350 
351         setUsernamePasswordVisibility(type);
352 
353         // Always enable identity for IKEv2/IPsec profiles.
354         mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.VISIBLE);
355 
356         // Then, unhide type-specific fields.
357         switch (type) {
358             case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
359                 mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE);
360                 mView.findViewById(R.id.options_ipsec_identity).setVisibility(View.VISIBLE);
361                 break;
362             case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
363                 mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE);
364                 // fall through
365             case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
366                 mView.findViewById(R.id.ipsec_peer).setVisibility(View.VISIBLE);
367                 break;
368         }
369 
370         configureAdvancedOptionsVisibility();
371     }
372 
validate(boolean editing)373     private boolean validate(boolean editing) {
374         if (mAlwaysOnVpn.isChecked() && !getProfile().isValidLockdownProfile()) {
375             return false;
376         }
377 
378         final int position = mType.getSelectedItemPosition();
379         final int type = VPN_TYPES.get(position);
380         if (!editing && requiresUsernamePassword(type)) {
381             return mUsername.getText().length() != 0 && mPassword.getText().length() != 0;
382         }
383         if (mName.getText().length() == 0 || mServer.getText().length() == 0) {
384             return false;
385         }
386 
387         // All IKEv2 methods require an identifier
388         if (mIpsecIdentifier.getText().length() == 0) {
389             return false;
390         }
391 
392         if (!validateProxy()) {
393             return false;
394         }
395 
396         switch (type) {
397             case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
398                 return true;
399 
400             case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
401                 return mIpsecSecret.getText().length() != 0;
402 
403             case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
404                 return mIpsecUserCert.getSelectedItemPosition() != 0;
405         }
406         return false;
407     }
408 
setTypesByFeature(Spinner typeSpinner)409     private void setTypesByFeature(Spinner typeSpinner) {
410         String[] types = getContext().getResources().getStringArray(R.array.vpn_types);
411         if (types.length != VPN_TYPES.size()) {
412             Log.wtf(TAG, "VPN_TYPES array length does not match string array");
413         }
414         // Although FEATURE_IPSEC_TUNNELS should always be present in android S and beyond,
415         // keep this check here just to be safe.
416         if (!getContext().getPackageManager().hasSystemFeature(
417                 PackageManager.FEATURE_IPSEC_TUNNELS)) {
418             Log.wtf(TAG, "FEATURE_IPSEC_TUNNELS missing from system");
419         }
420         // If the vpn is new or is not already a legacy type,
421         // don't allow the user to set the type to a legacy option.
422 
423         // Set the mProfile.type to TYPE_IKEV2_IPSEC_USER_PASS if the VPN not exist
424         if (!mExists) {
425             mProfile.type = VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS;
426         }
427 
428         final ArrayAdapter<String> adapter = new ArrayAdapter<String>(
429                 getContext(), android.R.layout.simple_spinner_item, types);
430         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
431         typeSpinner.setAdapter(adapter);
432     }
433 
loadCertificates(Spinner spinner, Collection<String> choices, int firstId, String selected)434     private void loadCertificates(Spinner spinner, Collection<String> choices, int firstId,
435             String selected) {
436         Context context = getContext();
437         String first = (firstId == 0) ? "" : context.getString(firstId);
438         String[] myChoices;
439 
440         if (choices == null || choices.size() == 0) {
441             myChoices = new String[] {first};
442         } else {
443             myChoices = new String[choices.size() + 1];
444             myChoices[0] = first;
445             int i = 1;
446             for (String c : choices) {
447                 myChoices[i++] = c;
448             }
449         }
450 
451         ArrayAdapter<String> adapter = new ArrayAdapter<String>(
452                 context, android.R.layout.simple_spinner_item, myChoices);
453         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
454         spinner.setAdapter(adapter);
455 
456         for (int i = 1; i < myChoices.length; ++i) {
457             if (myChoices[i].equals(selected)) {
458                 spinner.setSelection(i);
459                 break;
460             }
461         }
462     }
463 
setUsernamePasswordVisibility(int type)464     private void setUsernamePasswordVisibility(int type) {
465         mView.findViewById(R.id.userpass).setVisibility(
466                 requiresUsernamePassword(type) ? View.VISIBLE : View.GONE);
467     }
468 
requiresUsernamePassword(int type)469     private boolean requiresUsernamePassword(int type) {
470         switch (type) {
471             case VpnProfile.TYPE_IKEV2_IPSEC_RSA: // fall through
472             case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
473                 return false;
474             default:
475                 return true;
476         }
477     }
478 
isEditing()479     boolean isEditing() {
480         return mEditing;
481     }
482 
hasProxy()483     boolean hasProxy() {
484         return mProxySettings.getSelectedItemPosition() == VpnProfile.PROXY_MANUAL;
485     }
486 
getProfile()487     VpnProfile getProfile() {
488         // First, save common fields.
489         VpnProfile profile = new VpnProfile(mProfile.key);
490         profile.name = mName.getText().toString();
491         final int position = mType.getSelectedItemPosition();
492         profile.type = VPN_TYPES.get(position);
493         profile.server = mServer.getText().toString().trim();
494         profile.username = mUsername.getText().toString();
495         profile.password = mPassword.getText().toString();
496 
497         // Save fields based on VPN type.
498         profile.ipsecIdentifier = mIpsecIdentifier.getText().toString();
499 
500         if (hasProxy()) {
501             String proxyHost = mProxyHost.getText().toString().trim();
502             String proxyPort = mProxyPort.getText().toString().trim();
503             // 0 is a last resort default, but the interface validates that the proxy port is
504             // present and non-zero.
505             int port = 0;
506             if (!proxyPort.isEmpty()) {
507                 try {
508                     port = Integer.parseInt(proxyPort);
509                 } catch (NumberFormatException e) {
510                     Log.e(TAG, "Could not parse proxy port integer ", e);
511                 }
512             }
513             profile.proxy = ProxyInfo.buildDirectProxy(proxyHost, port);
514         } else {
515             profile.proxy = null;
516         }
517         // Then, save type-specific fields.
518         switch (profile.type) {
519             case VpnProfile.TYPE_IKEV2_IPSEC_PSK:
520                 profile.ipsecSecret = mIpsecSecret.getText().toString();
521                 break;
522 
523             case VpnProfile.TYPE_IKEV2_IPSEC_RSA:
524                 if (mIpsecUserCert.getSelectedItemPosition() != 0) {
525                     profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem();
526                     profile.ipsecSecret = profile.ipsecUserCert;
527                 }
528                 // fall through
529             case VpnProfile.TYPE_IKEV2_IPSEC_USER_PASS:
530                 if (mIpsecCaCert.getSelectedItemPosition() != 0) {
531                     profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem();
532                 }
533                 if (mIpsecServerCert.getSelectedItemPosition() != 0) {
534                     profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem();
535                 }
536                 break;
537         }
538 
539         final boolean hasLogin = !profile.username.isEmpty() || !profile.password.isEmpty();
540         profile.saveLogin = mSaveLogin.isChecked() || (mEditing && hasLogin);
541         return profile;
542     }
543 
validateProxy()544     private boolean validateProxy() {
545         if (!hasProxy()) {
546             return true;
547         }
548 
549         final String host = mProxyHost.getText().toString().trim();
550         final String port = mProxyPort.getText().toString().trim();
551         return ProxyUtils.validate(host, port, "") == ProxyUtils.PROXY_VALID;
552     }
553 
convertVpnProfileConstantToTypeIndex(int vpnType)554     private int convertVpnProfileConstantToTypeIndex(int vpnType) {
555         final int typeIndex = VPN_TYPES.indexOf(vpnType);
556         if (typeIndex == -1) {
557             // Existing legacy profile type
558             Log.wtf(TAG, "Invalid existing profile type");
559             return 0;
560         }
561         return typeIndex;
562     }
563 }
564