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 com.android.settings.R; 20 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.net.IConnectivityManager; 24 import android.net.LinkProperties; 25 import android.net.RouteInfo; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.Message; 29 import android.os.ServiceManager; 30 import android.preference.Preference; 31 import android.preference.PreferenceGroup; 32 import android.security.Credentials; 33 import android.security.KeyStore; 34 import android.util.Log; 35 import android.view.ContextMenu; 36 import android.view.ContextMenu.ContextMenuInfo; 37 import android.view.Menu; 38 import android.view.MenuItem; 39 import android.view.View; 40 import android.widget.AdapterView.AdapterContextMenuInfo; 41 42 import com.android.internal.net.LegacyVpnInfo; 43 import com.android.internal.net.VpnConfig; 44 import com.android.settings.SettingsPreferenceFragment; 45 46 import java.net.Inet4Address; 47 import java.nio.charset.Charsets; 48 import java.util.Arrays; 49 import java.util.HashMap; 50 51 public class VpnSettings extends SettingsPreferenceFragment implements 52 Handler.Callback, Preference.OnPreferenceClickListener, 53 DialogInterface.OnClickListener, DialogInterface.OnDismissListener { 54 55 private static final String TAG = "VpnSettings"; 56 57 private final IConnectivityManager mService = IConnectivityManager.Stub 58 .asInterface(ServiceManager.getService(Context.CONNECTIVITY_SERVICE)); 59 private final KeyStore mKeyStore = KeyStore.getInstance(); 60 private boolean mUnlocking = false; 61 62 private HashMap<String, VpnPreference> mPreferences; 63 private VpnDialog mDialog; 64 65 private Handler mUpdater; 66 private LegacyVpnInfo mInfo; 67 68 // The key of the profile for the current ContextMenu. 69 private String mSelectedKey; 70 71 @Override 72 public void onCreate(Bundle savedState) { 73 super.onCreate(savedState); 74 addPreferencesFromResource(R.xml.vpn_settings2); 75 getPreferenceScreen().setOrderingAsAdded(false); 76 77 if (savedState != null) { 78 VpnProfile profile = VpnProfile.decode(savedState.getString("VpnKey"), 79 savedState.getByteArray("VpnProfile")); 80 if (profile != null) { 81 mDialog = new VpnDialog(getActivity(), this, profile, 82 savedState.getBoolean("VpnEditing")); 83 } 84 } 85 } 86 87 @Override 88 public void onSaveInstanceState(Bundle savedState) { 89 // We do not save view hierarchy, as they are just profiles. 90 if (mDialog != null) { 91 VpnProfile profile = mDialog.getProfile(); 92 savedState.putString("VpnKey", profile.key); 93 savedState.putByteArray("VpnProfile", profile.encode()); 94 savedState.putBoolean("VpnEditing", mDialog.isEditing()); 95 } 96 // else? 97 } 98 99 @Override 100 public void onResume() { 101 super.onResume(); 102 103 // Check KeyStore here, so others do not need to deal with it. 104 if (mKeyStore.state() != KeyStore.State.UNLOCKED) { 105 if (!mUnlocking) { 106 // Let us unlock KeyStore. See you later! 107 Credentials.getInstance().unlock(getActivity()); 108 } else { 109 // We already tried, but it is still not working! 110 finishFragment(); 111 } 112 mUnlocking = !mUnlocking; 113 return; 114 } 115 116 // Now KeyStore is always unlocked. Reset the flag. 117 mUnlocking = false; 118 119 // Currently we are the only user of profiles in KeyStore. 120 // Assuming KeyStore and KeyGuard do the right thing, we can 121 // safely cache profiles in the memory. 122 if (mPreferences == null) { 123 mPreferences = new HashMap<String, VpnPreference>(); 124 PreferenceGroup group = getPreferenceScreen(); 125 126 String[] keys = mKeyStore.saw(Credentials.VPN); 127 if (keys != null && keys.length > 0) { 128 Context context = getActivity(); 129 130 for (String key : keys) { 131 VpnProfile profile = VpnProfile.decode(key, 132 mKeyStore.get(Credentials.VPN + key)); 133 if (profile == null) { 134 Log.w(TAG, "bad profile: key = " + key); 135 mKeyStore.delete(Credentials.VPN + key); 136 } else { 137 VpnPreference preference = new VpnPreference(context, profile); 138 mPreferences.put(key, preference); 139 group.addPreference(preference); 140 } 141 } 142 } 143 group.findPreference("add_network").setOnPreferenceClickListener(this); 144 } 145 146 // Show the dialog if there is one. 147 if (mDialog != null) { 148 mDialog.setOnDismissListener(this); 149 mDialog.show(); 150 } 151 152 // Start monitoring. 153 if (mUpdater == null) { 154 mUpdater = new Handler(this); 155 } 156 mUpdater.sendEmptyMessage(0); 157 158 // Register for context menu. Hmmm, getListView() is hidden? 159 registerForContextMenu(getListView()); 160 } 161 162 @Override 163 public void onPause() { 164 super.onPause(); 165 166 // Hide the dialog if there is one. 167 if (mDialog != null) { 168 mDialog.setOnDismissListener(null); 169 mDialog.dismiss(); 170 } 171 172 // Unregister for context menu. 173 if (getView() != null) { 174 unregisterForContextMenu(getListView()); 175 } 176 } 177 178 @Override 179 public void onDismiss(DialogInterface dialog) { 180 // Here is the exit of a dialog. 181 mDialog = null; 182 } 183 184 @Override 185 public void onClick(DialogInterface dialog, int button) { 186 if (button == DialogInterface.BUTTON_POSITIVE) { 187 // Always save the profile. 188 VpnProfile profile = mDialog.getProfile(); 189 mKeyStore.put(Credentials.VPN + profile.key, profile.encode()); 190 191 // Update the preference. 192 VpnPreference preference = mPreferences.get(profile.key); 193 if (preference != null) { 194 disconnect(profile.key); 195 preference.update(profile); 196 } else { 197 preference = new VpnPreference(getActivity(), profile); 198 mPreferences.put(profile.key, preference); 199 getPreferenceScreen().addPreference(preference); 200 } 201 202 // If we are not editing, connect! 203 if (!mDialog.isEditing()) { 204 try { 205 connect(profile); 206 } catch (Exception e) { 207 Log.e(TAG, "connect", e); 208 } 209 } 210 } 211 } 212 213 @Override 214 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo info) { 215 if (mDialog != null) { 216 Log.v(TAG, "onCreateContextMenu() is called when mDialog != null"); 217 return; 218 } 219 220 if (info instanceof AdapterContextMenuInfo) { 221 Preference preference = (Preference) getListView().getItemAtPosition( 222 ((AdapterContextMenuInfo) info).position); 223 if (preference instanceof VpnPreference) { 224 VpnProfile profile = ((VpnPreference) preference).getProfile(); 225 mSelectedKey = profile.key; 226 menu.setHeaderTitle(profile.name); 227 menu.add(Menu.NONE, R.string.vpn_menu_edit, 0, R.string.vpn_menu_edit); 228 menu.add(Menu.NONE, R.string.vpn_menu_delete, 0, R.string.vpn_menu_delete); 229 } 230 } 231 } 232 233 @Override 234 public boolean onContextItemSelected(MenuItem item) { 235 if (mDialog != null) { 236 Log.v(TAG, "onContextItemSelected() is called when mDialog != null"); 237 return false; 238 } 239 240 VpnPreference preference = mPreferences.get(mSelectedKey); 241 if (preference == null) { 242 Log.v(TAG, "onContextItemSelected() is called but no preference is found"); 243 return false; 244 } 245 246 switch (item.getItemId()) { 247 case R.string.vpn_menu_edit: 248 mDialog = new VpnDialog(getActivity(), this, preference.getProfile(), true); 249 mDialog.setOnDismissListener(this); 250 mDialog.show(); 251 return true; 252 case R.string.vpn_menu_delete: 253 disconnect(mSelectedKey); 254 getPreferenceScreen().removePreference(preference); 255 mPreferences.remove(mSelectedKey); 256 mKeyStore.delete(Credentials.VPN + mSelectedKey); 257 return true; 258 } 259 return false; 260 } 261 262 @Override 263 public boolean onPreferenceClick(Preference preference) { 264 if (mDialog != null) { 265 Log.v(TAG, "onPreferenceClick() is called when mDialog != null"); 266 return true; 267 } 268 269 if (preference instanceof VpnPreference) { 270 VpnProfile profile = ((VpnPreference) preference).getProfile(); 271 if (mInfo != null && profile.key.equals(mInfo.key) && 272 mInfo.state == LegacyVpnInfo.STATE_CONNECTED) { 273 try { 274 mInfo.intent.send(); 275 return true; 276 } catch (Exception e) { 277 // ignore 278 } 279 } 280 mDialog = new VpnDialog(getActivity(), this, profile, false); 281 } else { 282 // Generate a new key. Here we just use the current time. 283 long millis = System.currentTimeMillis(); 284 while (mPreferences.containsKey(Long.toHexString(millis))) { 285 ++millis; 286 } 287 mDialog = new VpnDialog(getActivity(), this, 288 new VpnProfile(Long.toHexString(millis)), true); 289 } 290 mDialog.setOnDismissListener(this); 291 mDialog.show(); 292 return true; 293 } 294 295 @Override 296 public boolean handleMessage(Message message) { 297 mUpdater.removeMessages(0); 298 299 if (isResumed()) { 300 try { 301 LegacyVpnInfo info = mService.getLegacyVpnInfo(); 302 if (mInfo != null) { 303 VpnPreference preference = mPreferences.get(mInfo.key); 304 if (preference != null) { 305 preference.update(-1); 306 } 307 mInfo = null; 308 } 309 if (info != null) { 310 VpnPreference preference = mPreferences.get(info.key); 311 if (preference != null) { 312 preference.update(info.state); 313 mInfo = info; 314 } 315 } 316 } catch (Exception e) { 317 // ignore 318 } 319 mUpdater.sendEmptyMessageDelayed(0, 1000); 320 } 321 return true; 322 } 323 324 private String[] getDefaultNetwork() throws Exception { 325 LinkProperties network = mService.getActiveLinkProperties(); 326 if (network == null) { 327 throw new IllegalStateException("Network is not available"); 328 } 329 String interfaze = network.getInterfaceName(); 330 if (interfaze == null) { 331 throw new IllegalStateException("Cannot get the default interface"); 332 } 333 String gateway = null; 334 for (RouteInfo route : network.getRoutes()) { 335 // Currently legacy VPN only works on IPv4. 336 if (route.isDefaultRoute() && route.getGateway() instanceof Inet4Address) { 337 gateway = route.getGateway().getHostAddress(); 338 break; 339 } 340 } 341 if (gateway == null) { 342 throw new IllegalStateException("Cannot get the default gateway"); 343 } 344 return new String[] {interfaze, gateway}; 345 } 346 347 private void connect(VpnProfile profile) throws Exception { 348 // Get the default interface and the default gateway. 349 String[] network = getDefaultNetwork(); 350 String interfaze = network[0]; 351 String gateway = network[1]; 352 353 // Load certificates. 354 String privateKey = ""; 355 String userCert = ""; 356 String caCert = ""; 357 if (!profile.ipsecUserCert.isEmpty()) { 358 byte[] value = mKeyStore.get(Credentials.USER_PRIVATE_KEY + profile.ipsecUserCert); 359 privateKey = (value == null) ? null : new String(value, Charsets.UTF_8); 360 value = mKeyStore.get(Credentials.USER_CERTIFICATE + profile.ipsecUserCert); 361 userCert = (value == null) ? null : new String(value, Charsets.UTF_8); 362 } 363 if (!profile.ipsecCaCert.isEmpty()) { 364 byte[] value = mKeyStore.get(Credentials.CA_CERTIFICATE + profile.ipsecCaCert); 365 caCert = (value == null) ? null : new String(value, Charsets.UTF_8); 366 } 367 if (privateKey == null || userCert == null || caCert == null) { 368 // TODO: find out a proper way to handle this. Delete these keys? 369 throw new IllegalStateException("Cannot load credentials"); 370 } 371 372 // Prepare arguments for racoon. 373 String[] racoon = null; 374 switch (profile.type) { 375 case VpnProfile.TYPE_L2TP_IPSEC_PSK: 376 racoon = new String[] { 377 interfaze, profile.server, "udppsk", profile.ipsecIdentifier, 378 profile.ipsecSecret, "1701", 379 }; 380 break; 381 case VpnProfile.TYPE_L2TP_IPSEC_RSA: 382 racoon = new String[] { 383 interfaze, profile.server, "udprsa", privateKey, userCert, caCert, "1701", 384 }; 385 break; 386 case VpnProfile.TYPE_IPSEC_XAUTH_PSK: 387 racoon = new String[] { 388 interfaze, profile.server, "xauthpsk", profile.ipsecIdentifier, 389 profile.ipsecSecret, profile.username, profile.password, "", gateway, 390 }; 391 break; 392 case VpnProfile.TYPE_IPSEC_XAUTH_RSA: 393 racoon = new String[] { 394 interfaze, profile.server, "xauthrsa", privateKey, userCert, caCert, 395 profile.username, profile.password, "", gateway, 396 }; 397 break; 398 case VpnProfile.TYPE_IPSEC_HYBRID_RSA: 399 racoon = new String[] { 400 interfaze, profile.server, "hybridrsa", caCert, 401 profile.username, profile.password, "", gateway, 402 }; 403 break; 404 } 405 406 // Prepare arguments for mtpd. 407 String[] mtpd = null; 408 switch (profile.type) { 409 case VpnProfile.TYPE_PPTP: 410 mtpd = new String[] { 411 interfaze, "pptp", profile.server, "1723", 412 "name", profile.username, "password", profile.password, 413 "linkname", "vpn", "refuse-eap", "nodefaultroute", 414 "usepeerdns", "idle", "1800", "mtu", "1400", "mru", "1400", 415 (profile.mppe ? "+mppe" : "nomppe"), 416 }; 417 break; 418 case VpnProfile.TYPE_L2TP_IPSEC_PSK: 419 case VpnProfile.TYPE_L2TP_IPSEC_RSA: 420 mtpd = new String[] { 421 interfaze, "l2tp", profile.server, "1701", profile.l2tpSecret, 422 "name", profile.username, "password", profile.password, 423 "linkname", "vpn", "refuse-eap", "nodefaultroute", 424 "usepeerdns", "idle", "1800", "mtu", "1400", "mru", "1400", 425 }; 426 break; 427 } 428 429 VpnConfig config = new VpnConfig(); 430 config.user = profile.key; 431 config.interfaze = interfaze; 432 config.session = profile.name; 433 config.routes = profile.routes; 434 if (!profile.dnsServers.isEmpty()) { 435 config.dnsServers = Arrays.asList(profile.dnsServers.split(" +")); 436 } 437 if (!profile.searchDomains.isEmpty()) { 438 config.searchDomains = Arrays.asList(profile.searchDomains.split(" +")); 439 } 440 441 mService.startLegacyVpn(config, racoon, mtpd); 442 } 443 444 private void disconnect(String key) { 445 if (mInfo != null && key.equals(mInfo.key)) { 446 try { 447 mService.prepareVpn(VpnConfig.LEGACY_VPN, VpnConfig.LEGACY_VPN); 448 } catch (Exception e) { 449 // ignore 450 } 451 } 452 } 453 454 private class VpnPreference extends Preference { 455 private VpnProfile mProfile; 456 private int mState = -1; 457 458 VpnPreference(Context context, VpnProfile profile) { 459 super(context); 460 setPersistent(false); 461 setOrder(0); 462 setOnPreferenceClickListener(VpnSettings.this); 463 464 mProfile = profile; 465 update(); 466 } 467 468 VpnProfile getProfile() { 469 return mProfile; 470 } 471 472 void update(VpnProfile profile) { 473 mProfile = profile; 474 update(); 475 } 476 477 void update(int state) { 478 mState = state; 479 update(); 480 } 481 482 void update() { 483 if (mState < 0) { 484 String[] types = getContext().getResources() 485 .getStringArray(R.array.vpn_types_long); 486 setSummary(types[mProfile.type]); 487 } else { 488 String[] states = getContext().getResources() 489 .getStringArray(R.array.vpn_states); 490 setSummary(states[mState]); 491 } 492 setTitle(mProfile.name); 493 notifyHierarchyChanged(); 494 } 495 496 @Override 497 public int compareTo(Preference preference) { 498 int result = -1; 499 if (preference instanceof VpnPreference) { 500 VpnPreference another = (VpnPreference) preference; 501 if ((result = another.mState - mState) == 0 && 502 (result = mProfile.name.compareTo(another.mProfile.name)) == 0 && 503 (result = mProfile.type - another.mProfile.type) == 0) { 504 result = mProfile.key.compareTo(another.mProfile.key); 505 } 506 } 507 return result; 508 } 509 } 510 } 511