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.app.AlertDialog;
20 import android.content.Context;
21 import android.content.DialogInterface;
22 import android.os.Bundle;
23 import android.os.SystemProperties;
24 import android.security.Credentials;
25 import android.security.KeyStore;
26 import android.text.Editable;
27 import android.text.TextWatcher;
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 com.android.internal.net.VpnProfile;
38 import com.android.settings.R;
39 
40 import java.net.InetAddress;
41 
42 /**
43  * Dialog showing information about a VPN configuration. The dialog
44  * can be launched to either edit or prompt for credentials to connect
45  * to a user-added VPN.
46  *
47  * {@see AppDialog}
48  */
49 class ConfigDialog extends AlertDialog implements TextWatcher,
50         View.OnClickListener, AdapterView.OnItemSelectedListener,
51         CompoundButton.OnCheckedChangeListener {
52     private final KeyStore mKeyStore = KeyStore.getInstance();
53     private final DialogInterface.OnClickListener mListener;
54     private final VpnProfile mProfile;
55 
56     private boolean mEditing;
57     private boolean mExists;
58 
59     private View mView;
60 
61     private TextView mName;
62     private Spinner mType;
63     private TextView mServer;
64     private TextView mUsername;
65     private TextView mPassword;
66     private TextView mSearchDomains;
67     private TextView mDnsServers;
68     private TextView mRoutes;
69     private CheckBox mMppe;
70     private TextView mL2tpSecret;
71     private TextView mIpsecIdentifier;
72     private TextView mIpsecSecret;
73     private Spinner mIpsecUserCert;
74     private Spinner mIpsecCaCert;
75     private Spinner mIpsecServerCert;
76     private CheckBox mSaveLogin;
77     private CheckBox mShowOptions;
78     private CheckBox mAlwaysOnVpn;
79 
ConfigDialog(Context context, DialogInterface.OnClickListener listener, VpnProfile profile, boolean editing, boolean exists)80     ConfigDialog(Context context, DialogInterface.OnClickListener listener,
81             VpnProfile profile, boolean editing, boolean exists) {
82         super(context);
83 
84         mListener = listener;
85         mProfile = profile;
86         mEditing = editing;
87         mExists = exists;
88     }
89 
90     @Override
onCreate(Bundle savedState)91     protected void onCreate(Bundle savedState) {
92         mView = getLayoutInflater().inflate(R.layout.vpn_dialog, null);
93         setView(mView);
94 
95         Context context = getContext();
96 
97         // First, find out all the fields.
98         mName = (TextView) mView.findViewById(R.id.name);
99         mType = (Spinner) mView.findViewById(R.id.type);
100         mServer = (TextView) mView.findViewById(R.id.server);
101         mUsername = (TextView) mView.findViewById(R.id.username);
102         mPassword = (TextView) mView.findViewById(R.id.password);
103         mSearchDomains = (TextView) mView.findViewById(R.id.search_domains);
104         mDnsServers = (TextView) mView.findViewById(R.id.dns_servers);
105         mRoutes = (TextView) mView.findViewById(R.id.routes);
106         mMppe = (CheckBox) mView.findViewById(R.id.mppe);
107         mL2tpSecret = (TextView) mView.findViewById(R.id.l2tp_secret);
108         mIpsecIdentifier = (TextView) mView.findViewById(R.id.ipsec_identifier);
109         mIpsecSecret = (TextView) mView.findViewById(R.id.ipsec_secret);
110         mIpsecUserCert = (Spinner) mView.findViewById(R.id.ipsec_user_cert);
111         mIpsecCaCert = (Spinner) mView.findViewById(R.id.ipsec_ca_cert);
112         mIpsecServerCert = (Spinner) mView.findViewById(R.id.ipsec_server_cert);
113         mSaveLogin = (CheckBox) mView.findViewById(R.id.save_login);
114         mShowOptions = (CheckBox) mView.findViewById(R.id.show_options);
115         mAlwaysOnVpn = (CheckBox) mView.findViewById(R.id.always_on_vpn);
116 
117         // Second, copy values from the profile.
118         mName.setText(mProfile.name);
119         mType.setSelection(mProfile.type);
120         mServer.setText(mProfile.server);
121         if (mProfile.saveLogin) {
122             mUsername.setText(mProfile.username);
123             mPassword.setText(mProfile.password);
124         }
125         mSearchDomains.setText(mProfile.searchDomains);
126         mDnsServers.setText(mProfile.dnsServers);
127         mRoutes.setText(mProfile.routes);
128         mMppe.setChecked(mProfile.mppe);
129         mL2tpSecret.setText(mProfile.l2tpSecret);
130         mIpsecIdentifier.setText(mProfile.ipsecIdentifier);
131         mIpsecSecret.setText(mProfile.ipsecSecret);
132         loadCertificates(mIpsecUserCert, Credentials.USER_PRIVATE_KEY, 0, mProfile.ipsecUserCert);
133         loadCertificates(mIpsecCaCert, Credentials.CA_CERTIFICATE,
134                 R.string.vpn_no_ca_cert, mProfile.ipsecCaCert);
135         loadCertificates(mIpsecServerCert, Credentials.USER_CERTIFICATE,
136                 R.string.vpn_no_server_cert, mProfile.ipsecServerCert);
137         mSaveLogin.setChecked(mProfile.saveLogin);
138         mAlwaysOnVpn.setChecked(mProfile.key.equals(VpnUtils.getLockdownVpn()));
139         mAlwaysOnVpn.setOnCheckedChangeListener(this);
140         // Update SaveLogin checkbox after Always-on checkbox is updated
141         updateSaveLoginStatus();
142 
143         // Hide lockdown VPN on devices that require IMS authentication
144         if (SystemProperties.getBoolean("persist.radio.imsregrequired", false)) {
145             mAlwaysOnVpn.setVisibility(View.GONE);
146         }
147 
148         // Third, add listeners to required fields.
149         mName.addTextChangedListener(this);
150         mType.setOnItemSelectedListener(this);
151         mServer.addTextChangedListener(this);
152         mUsername.addTextChangedListener(this);
153         mPassword.addTextChangedListener(this);
154         mDnsServers.addTextChangedListener(this);
155         mRoutes.addTextChangedListener(this);
156         mIpsecSecret.addTextChangedListener(this);
157         mIpsecUserCert.setOnItemSelectedListener(this);
158         mShowOptions.setOnClickListener(this);
159 
160         // Fourth, determine whether to do editing or connecting.
161         boolean valid = validate(true);
162         mEditing = mEditing || !valid;
163 
164         if (mEditing) {
165             setTitle(R.string.vpn_edit);
166 
167             // Show common fields.
168             mView.findViewById(R.id.editor).setVisibility(View.VISIBLE);
169 
170             // Show type-specific fields.
171             changeType(mProfile.type);
172 
173             // Hide 'save login' when we are editing.
174             mSaveLogin.setVisibility(View.GONE);
175 
176             // Switch to advanced view immediately if any advanced options are on
177             if (!mProfile.searchDomains.isEmpty() || !mProfile.dnsServers.isEmpty() ||
178                     !mProfile.routes.isEmpty()) {
179                 showAdvancedOptions();
180             }
181 
182             // Create a button to forget the profile if it has already been saved..
183             if (mExists) {
184                 setButton(DialogInterface.BUTTON_NEUTRAL,
185                         context.getString(R.string.vpn_forget), mListener);
186             }
187 
188             // Create a button to save the profile.
189             setButton(DialogInterface.BUTTON_POSITIVE,
190                     context.getString(R.string.vpn_save), mListener);
191         } else {
192             setTitle(context.getString(R.string.vpn_connect_to, mProfile.name));
193 
194             // Create a button to connect the network.
195             setButton(DialogInterface.BUTTON_POSITIVE,
196                     context.getString(R.string.vpn_connect), mListener);
197         }
198 
199         // Always provide a cancel button.
200         setButton(DialogInterface.BUTTON_NEGATIVE,
201                 context.getString(R.string.vpn_cancel), mListener);
202 
203         // Let AlertDialog create everything.
204         super.onCreate(savedState);
205 
206         // Disable the action button if necessary.
207         getButton(DialogInterface.BUTTON_POSITIVE)
208                 .setEnabled(mEditing ? valid : validate(false));
209 
210         // Workaround to resize the dialog for the input method.
211         getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE |
212                 WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
213     }
214 
215     @Override
onRestoreInstanceState(Bundle savedState)216     public void onRestoreInstanceState(Bundle savedState) {
217         super.onRestoreInstanceState(savedState);
218 
219         // Visibility isn't restored by super.onRestoreInstanceState, so re-show the advanced
220         // options here if they were already revealed or set.
221         if (mShowOptions.isChecked()) {
222             showAdvancedOptions();
223         }
224     }
225 
226     @Override
afterTextChanged(Editable field)227     public void afterTextChanged(Editable field) {
228         getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
229     }
230 
231     @Override
beforeTextChanged(CharSequence s, int start, int count, int after)232     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
233     }
234 
235     @Override
onTextChanged(CharSequence s, int start, int before, int count)236     public void onTextChanged(CharSequence s, int start, int before, int count) {
237     }
238 
239     @Override
onClick(View view)240     public void onClick(View view) {
241         if (view == mShowOptions) {
242             showAdvancedOptions();
243         }
244     }
245 
246     @Override
onItemSelected(AdapterView<?> parent, View view, int position, long id)247     public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
248         if (parent == mType) {
249             changeType(position);
250         }
251         getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
252     }
253 
254     @Override
onNothingSelected(AdapterView<?> parent)255     public void onNothingSelected(AdapterView<?> parent) {
256     }
257 
258     @Override
onCheckedChanged(CompoundButton compoundButton, boolean b)259     public void onCheckedChanged(CompoundButton compoundButton, boolean b) {
260         if (compoundButton == mAlwaysOnVpn) {
261             updateSaveLoginStatus();
262             getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing));
263         }
264     }
265 
isVpnAlwaysOn()266     public boolean isVpnAlwaysOn() {
267         return mAlwaysOnVpn.isChecked();
268     }
269 
updateSaveLoginStatus()270     private void updateSaveLoginStatus() {
271         if (mAlwaysOnVpn.isChecked()) {
272             mSaveLogin.setChecked(true);
273             mSaveLogin.setEnabled(false);
274         } else {
275             mSaveLogin.setChecked(mProfile.saveLogin);
276             mSaveLogin.setEnabled(true);
277         }
278     }
279 
showAdvancedOptions()280     private void showAdvancedOptions() {
281         mView.findViewById(R.id.options).setVisibility(View.VISIBLE);
282         mShowOptions.setVisibility(View.GONE);
283     }
284 
changeType(int type)285     private void changeType(int type) {
286         // First, hide everything.
287         mMppe.setVisibility(View.GONE);
288         mView.findViewById(R.id.l2tp).setVisibility(View.GONE);
289         mView.findViewById(R.id.ipsec_psk).setVisibility(View.GONE);
290         mView.findViewById(R.id.ipsec_user).setVisibility(View.GONE);
291         mView.findViewById(R.id.ipsec_peer).setVisibility(View.GONE);
292 
293         // Then, unhide type-specific fields.
294         switch (type) {
295             case VpnProfile.TYPE_PPTP:
296                 mMppe.setVisibility(View.VISIBLE);
297                 break;
298 
299             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
300                 mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE);
301                 // fall through
302             case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
303                 mView.findViewById(R.id.ipsec_psk).setVisibility(View.VISIBLE);
304                 break;
305 
306             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
307                 mView.findViewById(R.id.l2tp).setVisibility(View.VISIBLE);
308                 // fall through
309             case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
310                 mView.findViewById(R.id.ipsec_user).setVisibility(View.VISIBLE);
311                 // fall through
312             case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
313                 mView.findViewById(R.id.ipsec_peer).setVisibility(View.VISIBLE);
314                 break;
315         }
316     }
317 
validate(boolean editing)318     private boolean validate(boolean editing) {
319         if (!editing) {
320             return mUsername.getText().length() != 0 && mPassword.getText().length() != 0;
321         }
322         if (mAlwaysOnVpn.isChecked() && !getProfile().isValidLockdownProfile()) {
323             return false;
324         }
325         if (mName.getText().length() == 0 || mServer.getText().length() == 0 ||
326                 !validateAddresses(mDnsServers.getText().toString(), false) ||
327                 !validateAddresses(mRoutes.getText().toString(), true)) {
328             return false;
329         }
330         switch (mType.getSelectedItemPosition()) {
331             case VpnProfile.TYPE_PPTP:
332             case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
333                 return true;
334 
335             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
336             case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
337                 return mIpsecSecret.getText().length() != 0;
338 
339             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
340             case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
341                 return mIpsecUserCert.getSelectedItemPosition() != 0;
342         }
343         return false;
344     }
345 
validateAddresses(String addresses, boolean cidr)346     private boolean validateAddresses(String addresses, boolean cidr) {
347         try {
348             for (String address : addresses.split(" ")) {
349                 if (address.isEmpty()) {
350                     continue;
351                 }
352                 // Legacy VPN currently only supports IPv4.
353                 int prefixLength = 32;
354                 if (cidr) {
355                     String[] parts = address.split("/", 2);
356                     address = parts[0];
357                     prefixLength = Integer.parseInt(parts[1]);
358                 }
359                 byte[] bytes = InetAddress.parseNumericAddress(address).getAddress();
360                 int integer = (bytes[3] & 0xFF) | (bytes[2] & 0xFF) << 8 |
361                         (bytes[1] & 0xFF) << 16 | (bytes[0] & 0xFF) << 24;
362                 if (bytes.length != 4 || prefixLength < 0 || prefixLength > 32 ||
363                         (prefixLength < 32 && (integer << prefixLength) != 0)) {
364                     return false;
365                 }
366             }
367         } catch (Exception e) {
368             return false;
369         }
370         return true;
371     }
372 
loadCertificates(Spinner spinner, String prefix, int firstId, String selected)373     private void loadCertificates(Spinner spinner, String prefix, int firstId, String selected) {
374         Context context = getContext();
375         String first = (firstId == 0) ? "" : context.getString(firstId);
376         String[] certificates = mKeyStore.list(prefix);
377 
378         if (certificates == null || certificates.length == 0) {
379             certificates = new String[] {first};
380         } else {
381             String[] array = new String[certificates.length + 1];
382             array[0] = first;
383             System.arraycopy(certificates, 0, array, 1, certificates.length);
384             certificates = array;
385         }
386 
387         ArrayAdapter<String> adapter = new ArrayAdapter<String>(
388                 context, android.R.layout.simple_spinner_item, certificates);
389         adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
390         spinner.setAdapter(adapter);
391 
392         for (int i = 1; i < certificates.length; ++i) {
393             if (certificates[i].equals(selected)) {
394                 spinner.setSelection(i);
395                 break;
396             }
397         }
398     }
399 
isEditing()400     boolean isEditing() {
401         return mEditing;
402     }
403 
getProfile()404     VpnProfile getProfile() {
405         // First, save common fields.
406         VpnProfile profile = new VpnProfile(mProfile.key);
407         profile.name = mName.getText().toString();
408         profile.type = mType.getSelectedItemPosition();
409         profile.server = mServer.getText().toString().trim();
410         profile.username = mUsername.getText().toString();
411         profile.password = mPassword.getText().toString();
412         profile.searchDomains = mSearchDomains.getText().toString().trim();
413         profile.dnsServers = mDnsServers.getText().toString().trim();
414         profile.routes = mRoutes.getText().toString().trim();
415 
416         // Then, save type-specific fields.
417         switch (profile.type) {
418             case VpnProfile.TYPE_PPTP:
419                 profile.mppe = mMppe.isChecked();
420                 break;
421 
422             case VpnProfile.TYPE_L2TP_IPSEC_PSK:
423                 profile.l2tpSecret = mL2tpSecret.getText().toString();
424                 // fall through
425             case VpnProfile.TYPE_IPSEC_XAUTH_PSK:
426                 profile.ipsecIdentifier = mIpsecIdentifier.getText().toString();
427                 profile.ipsecSecret = mIpsecSecret.getText().toString();
428                 break;
429 
430             case VpnProfile.TYPE_L2TP_IPSEC_RSA:
431                 profile.l2tpSecret = mL2tpSecret.getText().toString();
432                 // fall through
433             case VpnProfile.TYPE_IPSEC_XAUTH_RSA:
434                 if (mIpsecUserCert.getSelectedItemPosition() != 0) {
435                     profile.ipsecUserCert = (String) mIpsecUserCert.getSelectedItem();
436                 }
437                 // fall through
438             case VpnProfile.TYPE_IPSEC_HYBRID_RSA:
439                 if (mIpsecCaCert.getSelectedItemPosition() != 0) {
440                     profile.ipsecCaCert = (String) mIpsecCaCert.getSelectedItem();
441                 }
442                 if (mIpsecServerCert.getSelectedItemPosition() != 0) {
443                     profile.ipsecServerCert = (String) mIpsecServerCert.getSelectedItem();
444                 }
445                 break;
446         }
447 
448         final boolean hasLogin = !profile.username.isEmpty() || !profile.password.isEmpty();
449         profile.saveLogin = mSaveLogin.isChecked() || (mEditing && hasLogin);
450         return profile;
451     }
452 }
453