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 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 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 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 227 public void afterTextChanged(Editable field) { 228 getButton(DialogInterface.BUTTON_POSITIVE).setEnabled(validate(mEditing)); 229 } 230 231 @Override 232 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 233 } 234 235 @Override 236 public void onTextChanged(CharSequence s, int start, int before, int count) { 237 } 238 239 @Override 240 public void onClick(View view) { 241 if (view == mShowOptions) { 242 showAdvancedOptions(); 243 } 244 } 245 246 @Override 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 255 public void onNothingSelected(AdapterView<?> parent) { 256 } 257 258 @Override 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 266 public boolean isVpnAlwaysOn() { 267 return mAlwaysOnVpn.isChecked(); 268 } 269 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 280 private void showAdvancedOptions() { 281 mView.findViewById(R.id.options).setVisibility(View.VISIBLE); 282 mShowOptions.setVisibility(View.GONE); 283 } 284 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 318 private boolean validate(boolean editing) { 319 if (mAlwaysOnVpn.isChecked() && !getProfile().isValidLockdownProfile()) { 320 return false; 321 } 322 if (!editing) { 323 return mUsername.getText().length() != 0 && mPassword.getText().length() != 0; 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 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 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 400 boolean isEditing() { 401 return mEditing; 402 } 403 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