Files
packages_apps_Settings/src/com/android/settings/vpn/VpnSettings.java
Brian Carlstrom d4023b7cca Integrating keystore with keyguard (Part 4 of 4)
Summary:

frameworks/base
  keystore rewrite
  keyguard integration with keystore on keyguard entry or keyguard change
  KeyStore API simplification

packages/apps/Settings
  Removed com.android.credentials.SET_PASSWORD intent support
  Added keyguard requirement for keystore use

packages/apps/CertInstaller
  Tracking KeyStore API changes
  Fix for NPE in CertInstaller when certificate lacks basic constraints

packages/apps/KeyChain
  Tracking KeyStore API changes

Details:

frameworks/base

   Move keystore from C to C++ while rewriting password
   implementation. Removed global variables. Added many comments.

	cmds/keystore/Android.mk
	cmds/keystore/keystore.h
	cmds/keystore/keystore.c => cmds/keystore/keystore.cpp
	cmds/keystore/keystore_cli.c => cmds/keystore/keystore_cli.cpp

   Changed saveLockPattern and saveLockPassword to notify the keystore
   on changes so that the keystore master key can be reencrypted when
   the keyguard changes.

	core/java/com/android/internal/widget/LockPatternUtils.java

   Changed unlock screens to pass values for keystore unlock or initialization

	policy/src/com/android/internal/policy/impl/PasswordUnlockScreen.java
	policy/src/com/android/internal/policy/impl/PatternUnlockScreen.java

   KeyStore API changes
   - renamed test() to state(), which now return a State enum
   - made APIs with byte[] key arguments private
   - added new KeyStore.isEmpty used to determine if a keyguard is required

	keystore/java/android/security/KeyStore.java

   In addition to tracking KeyStore API changes, added new testIsEmpty
   and improved some existing tests to validate expect values.

	keystore/tests/src/android/security/KeyStoreTest.java

packages/apps/Settings

    Removing com.android.credentials.SET_PASSWORD intent with the
    removal of the ability to set an explicit keystore password now
    that the keyguard value is used. Changed to ensure keyguard is
    enabled for keystore install or unlock. Cleaned up interwoven
    dialog handing into discrete dialog helper classes.

	AndroidManifest.xml
	src/com/android/settings/CredentialStorage.java

    Remove layout for entering new password

	res/layout/credentials_dialog.xml

    Remove enable credentials checkbox

	res/xml/security_settings_misc.xml
	src/com/android/settings/SecuritySettings.java

    Added ability to specify minimum quality key to ChooseLockGeneric
    Activity. Used by CredentialStorage, but could also be used by
    CryptKeeperSettings. Changed ChooseLockGeneric to understand
    minimum quality for keystore in addition to DPM and device
    encryption.

	src/com/android/settings/ChooseLockGeneric.java

    Changed to use getActivePasswordQuality from
    getKeyguardStoredPasswordQuality based on experience in
    CredentialStorage. Removed bogus class javadoc.

	src/com/android/settings/CryptKeeperSettings.java

    Tracking KeyStore API changes

	src/com/android/settings/vpn/VpnSettings.java
	src/com/android/settings/wifi/WifiSettings.java

   Removing now unused string resources

	res/values-af/strings.xml
	res/values-am/strings.xml
	res/values-ar/strings.xml
	res/values-bg/strings.xml
	res/values-ca/strings.xml
	res/values-cs/strings.xml
	res/values-da/strings.xml
	res/values-de/strings.xml
	res/values-el/strings.xml
	res/values-en-rGB/strings.xml
	res/values-es-rUS/strings.xml
	res/values-es/strings.xml
	res/values-fa/strings.xml
	res/values-fi/strings.xml
	res/values-fr/strings.xml
	res/values-hr/strings.xml
	res/values-hu/strings.xml
	res/values-in/strings.xml
	res/values-it/strings.xml
	res/values-iw/strings.xml
	res/values-ja/strings.xml
	res/values-ko/strings.xml
	res/values-lt/strings.xml
	res/values-lv/strings.xml
	res/values-ms/strings.xml
	res/values-nb/strings.xml
	res/values-nl/strings.xml
	res/values-pl/strings.xml
	res/values-pt-rPT/strings.xml
	res/values-pt/strings.xml
	res/values-rm/strings.xml
	res/values-ro/strings.xml
	res/values-ru/strings.xml
	res/values-sk/strings.xml
	res/values-sl/strings.xml
	res/values-sr/strings.xml
	res/values-sv/strings.xml
	res/values-sw/strings.xml
	res/values-th/strings.xml
	res/values-tl/strings.xml
	res/values-tr/strings.xml
	res/values-uk/strings.xml
	res/values-vi/strings.xml
	res/values-zh-rCN/strings.xml
	res/values-zh-rTW/strings.xml
	res/values-zu/strings.xml
	res/values/strings.xml

packages/apps/CertInstaller

  Tracking KeyStore API changes
	src/com/android/certinstaller/CertInstaller.java

  Fix for NPE in CertInstaller when certificate lacks basic constraints
	src/com/android/certinstaller/CredentialHelper.java

packages/apps/KeyChain

  Tracking KeyStore API changes
	src/com/android/keychain/KeyChainActivity.java
	src/com/android/keychain/KeyChainService.java
	support/src/com/android/keychain/tests/support/IKeyChainServiceTestSupport.aidl
	support/src/com/android/keychain/tests/support/KeyChainServiceTestSupport.java
	tests/src/com/android/keychain/tests/KeyChainServiceTest.java

Change-Id: I80533bf8986a92b0b99cd5fb1c4943e0f23fc1c8
2011-06-01 10:47:42 -07:00

1110 lines
40 KiB
Java

/*
* Copyright (C) 2009 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.vpn;
import com.android.settings.R;
import com.android.settings.SettingsPreferenceFragment;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.vpn.L2tpIpsecProfile;
import android.net.vpn.L2tpIpsecPskProfile;
import android.net.vpn.L2tpProfile;
import android.net.vpn.VpnManager;
import android.net.vpn.VpnProfile;
import android.net.vpn.VpnState;
import android.net.vpn.VpnType;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceCategory;
import android.preference.PreferenceScreen;
import android.preference.Preference.OnPreferenceClickListener;
import android.security.Credentials;
import android.security.KeyStore;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.MenuItem;
import android.view.View;
import android.view.ContextMenu.ContextMenuInfo;
import android.widget.AdapterView.AdapterContextMenuInfo;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.charset.Charsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* The preference activity for configuring VPN settings.
*/
public class VpnSettings extends SettingsPreferenceFragment
implements DialogInterface.OnClickListener {
private static final boolean DEBUG = false;
// Key to the field exchanged for profile editing.
static final String KEY_VPN_PROFILE = "vpn_profile";
// Key to the field exchanged for VPN type selection.
static final String KEY_VPN_TYPE = "vpn_type";
private static final String TAG = VpnSettings.class.getSimpleName();
private static final String PREF_ADD_VPN = "add_new_vpn";
private static final String PREF_VPN_LIST = "vpn_list";
private static final String PROFILES_ROOT = VpnManager.getProfilePath() + "/";
private static final String PROFILE_OBJ_FILE = ".pobj";
private static final String KEY_ACTIVE_PROFILE = "ActiveProfile";
private static final String KEY_PROFILE_CONNECTING = "ProfileConnecting";
private static final String KEY_CONNECT_DIALOG_SHOWING = "ConnectDialogShowing";
private static final int REQUEST_ADD_OR_EDIT_PROFILE = 1;
static final int REQUEST_SELECT_VPN_TYPE = 2;
private static final int CONTEXT_MENU_CONNECT_ID = ContextMenu.FIRST + 0;
private static final int CONTEXT_MENU_DISCONNECT_ID = ContextMenu.FIRST + 1;
private static final int CONTEXT_MENU_EDIT_ID = ContextMenu.FIRST + 2;
private static final int CONTEXT_MENU_DELETE_ID = ContextMenu.FIRST + 3;
private static final int CONNECT_BUTTON = DialogInterface.BUTTON_POSITIVE;
private static final int OK_BUTTON = DialogInterface.BUTTON_POSITIVE;
private static final int DIALOG_CONNECT = VpnManager.VPN_ERROR_LARGEST + 1;
private static final int DIALOG_SECRET_NOT_SET = DIALOG_CONNECT + 1;
private static final int NO_ERROR = VpnManager.VPN_ERROR_NO_ERROR;
private static final String KEY_PREFIX_IPSEC_PSK = Credentials.VPN + 'i';
private static final String KEY_PREFIX_L2TP_SECRET = Credentials.VPN + 'l';
private static List<VpnProfile> sVpnProfileList = new ArrayList<VpnProfile>();
private PreferenceScreen mAddVpn;
private PreferenceCategory mVpnListContainer;
// profile name --> VpnPreference
private Map<String, VpnPreference> mVpnPreferenceMap;
// profile engaged in a connection
private VpnProfile mActiveProfile;
// actor engaged in connecting
private VpnProfileActor mConnectingActor;
// states saved for unlocking keystore
private Runnable mUnlockAction;
private KeyStore mKeyStore = KeyStore.getInstance();
private VpnManager mVpnManager;
private ConnectivityReceiver mConnectivityReceiver =
new ConnectivityReceiver();
private int mConnectingErrorCode = NO_ERROR;
private Dialog mShowingDialog;
private boolean mConnectDialogShowing = false;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.vpn_settings);
mVpnManager = new VpnManager(getActivity());
// restore VpnProfile list and construct VpnPreference map
mVpnListContainer = (PreferenceCategory) findPreference(PREF_VPN_LIST);
// set up the "add vpn" preference
mAddVpn = (PreferenceScreen) findPreference(PREF_ADD_VPN);
mAddVpn.setOnPreferenceClickListener(
new OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference preference) {
startVpnTypeSelection();
return true;
}
});
retrieveVpnListFromStorage();
restoreInstanceState(savedInstanceState);
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
if (mActiveProfile != null) {
savedInstanceState.putString(KEY_ACTIVE_PROFILE,
mActiveProfile.getId());
savedInstanceState.putBoolean(KEY_PROFILE_CONNECTING,
(mConnectingActor != null));
savedInstanceState.putBoolean(KEY_CONNECT_DIALOG_SHOWING,
mConnectDialogShowing);
}
super.onSaveInstanceState(savedInstanceState);
}
private void restoreInstanceState(Bundle savedInstanceState) {
if (savedInstanceState == null) return;
String profileId = savedInstanceState.getString(KEY_ACTIVE_PROFILE);
if (profileId != null) {
mActiveProfile = getProfile(getProfileIndexFromId(profileId));
if (savedInstanceState.getBoolean(KEY_PROFILE_CONNECTING)) {
mConnectingActor = getActor(mActiveProfile);
}
mConnectDialogShowing = savedInstanceState.getBoolean(KEY_CONNECT_DIALOG_SHOWING);
}
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// for long-press gesture on a profile preference
registerForContextMenu(getListView());
}
@Override
public void onPause() {
// ignore vpn connectivity event
mVpnManager.unregisterConnectivityReceiver(mConnectivityReceiver);
if ((mShowingDialog != null) && mShowingDialog.isShowing()) {
mShowingDialog.dismiss();
mShowingDialog = null;
}
super.onPause();
}
@Override
public void onResume() {
super.onResume();
updatePreferenceMap();
if (DEBUG) Log.d(TAG, "onResume");
// listen to vpn connectivity event
mVpnManager.registerConnectivityReceiver(mConnectivityReceiver);
if ((mUnlockAction != null) && isKeyStoreUnlocked()) {
Runnable action = mUnlockAction;
mUnlockAction = null;
getActivity().runOnUiThread(action);
}
if (!mConnectDialogShowing) {
// If mActiveProfile is not null but it's in IDLE state, then a
// retry dialog must be showing now as the previous connection
// attempt failed. In this case, don't call checkVpnConnectionStatus()
// as it will clean up mActiveProfile due to the IDLE state.
if ((mActiveProfile == null)
|| (mActiveProfile.getState() != VpnState.IDLE)) {
checkVpnConnectionStatus();
}
} else {
// Dismiss the connect dialog in case there is another instance
// trying to operate a vpn connection.
if (!mVpnManager.isIdle() || (mActiveProfile == null)) {
removeDialog(DIALOG_CONNECT);
checkVpnConnectionStatus();
}
}
}
@Override
public void onDestroyView() {
unregisterForContextMenu(getListView());
// This should be called after the procedure above as ListView inside this Fragment
// will be deleted here.
super.onDestroyView();
}
@Override
public void onDestroy() {
super.onDestroy();
// Remove any onClick listeners
if (mVpnListContainer != null) {
for (int i = 0; i < mVpnListContainer.getPreferenceCount(); i++) {
mVpnListContainer.getPreference(i).setOnPreferenceClickListener(null);
}
}
}
@Override
public Dialog onCreateDialog (int id) {
setOnCancelListener(new DialogInterface.OnCancelListener() {
public void onCancel(DialogInterface dialog) {
if (mActiveProfile != null) {
changeState(mActiveProfile, VpnState.IDLE);
}
// Make sure onIdle() is called as the above changeState()
// may not be effective if the state is already IDLE.
// XXX: VpnService should broadcast non-IDLE state, say UNUSABLE,
// when an error occurs.
onIdle();
}
});
switch (id) {
case DIALOG_CONNECT:
mConnectDialogShowing = true;
setOnDismissListener(new DialogInterface.OnDismissListener() {
public void onDismiss(DialogInterface dialog) {
mConnectDialogShowing = false;
}
});
return createConnectDialog();
case DIALOG_SECRET_NOT_SET:
return createSecretNotSetDialog();
case VpnManager.VPN_ERROR_CHALLENGE:
case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
return createEditDialog(id);
default:
Log.d(TAG, "create reconnect dialog for event " + id);
return createReconnectDialog(id);
}
}
private Dialog createConnectDialog() {
final Activity activity = getActivity();
return new AlertDialog.Builder(activity)
.setView(mConnectingActor.createConnectView())
.setTitle(String.format(activity.getString(R.string.vpn_connect_to),
mConnectingActor.getProfile().getName()))
.setPositiveButton(activity.getString(R.string.vpn_connect_button),
this)
.setNegativeButton(activity.getString(android.R.string.cancel),
this)
.create();
}
private Dialog createReconnectDialog(int id) {
int msgId;
switch (id) {
case VpnManager.VPN_ERROR_AUTH:
msgId = R.string.vpn_auth_error_dialog_msg;
break;
case VpnManager.VPN_ERROR_REMOTE_HUNG_UP:
msgId = R.string.vpn_remote_hung_up_error_dialog_msg;
break;
case VpnManager.VPN_ERROR_CONNECTION_LOST:
msgId = R.string.vpn_reconnect_from_lost;
break;
case VpnManager.VPN_ERROR_REMOTE_PPP_HUNG_UP:
msgId = R.string.vpn_remote_ppp_hung_up_error_dialog_msg;
break;
default:
msgId = R.string.vpn_confirm_reconnect;
}
return createCommonDialogBuilder().setMessage(msgId).create();
}
private Dialog createEditDialog(int id) {
int msgId;
switch (id) {
case VpnManager.VPN_ERROR_CHALLENGE:
msgId = R.string.vpn_challenge_error_dialog_msg;
break;
case VpnManager.VPN_ERROR_UNKNOWN_SERVER:
msgId = R.string.vpn_unknown_server_dialog_msg;
break;
case VpnManager.VPN_ERROR_PPP_NEGOTIATION_FAILED:
msgId = R.string.vpn_ppp_negotiation_failed_dialog_msg;
break;
default:
return null;
}
return createCommonEditDialogBuilder().setMessage(msgId).create();
}
private Dialog createSecretNotSetDialog() {
return createCommonDialogBuilder()
.setMessage(R.string.vpn_secret_not_set_dialog_msg)
.setPositiveButton(R.string.vpn_yes_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
startVpnEditor(mActiveProfile, false);
}
})
.create();
}
private AlertDialog.Builder createCommonEditDialogBuilder() {
return createCommonDialogBuilder()
.setPositiveButton(R.string.vpn_yes_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
VpnProfile p = mActiveProfile;
onIdle();
startVpnEditor(p, false);
}
});
}
private AlertDialog.Builder createCommonDialogBuilder() {
return new AlertDialog.Builder(getActivity())
.setTitle(android.R.string.dialog_alert_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setPositiveButton(R.string.vpn_yes_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
connectOrDisconnect(mActiveProfile);
}
})
.setNegativeButton(R.string.vpn_no_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
onIdle();
}
});
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
VpnProfile p = getProfile(getProfilePositionFrom(
(AdapterContextMenuInfo) menuInfo));
if (p != null) {
VpnState state = p.getState();
menu.setHeaderTitle(p.getName());
boolean isIdle = (state == VpnState.IDLE);
boolean isNotConnect = (isIdle || (state == VpnState.DISCONNECTING)
|| (state == VpnState.CANCELLED));
menu.add(0, CONTEXT_MENU_CONNECT_ID, 0, R.string.vpn_menu_connect)
.setEnabled(isIdle && (mActiveProfile == null));
menu.add(0, CONTEXT_MENU_DISCONNECT_ID, 0,
R.string.vpn_menu_disconnect)
.setEnabled(state == VpnState.CONNECTED);
menu.add(0, CONTEXT_MENU_EDIT_ID, 0, R.string.vpn_menu_edit)
.setEnabled(isNotConnect);
menu.add(0, CONTEXT_MENU_DELETE_ID, 0, R.string.vpn_menu_delete)
.setEnabled(isNotConnect);
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
int position = getProfilePositionFrom(
(AdapterContextMenuInfo) item.getMenuInfo());
VpnProfile p = getProfile(position);
switch(item.getItemId()) {
case CONTEXT_MENU_CONNECT_ID:
case CONTEXT_MENU_DISCONNECT_ID:
connectOrDisconnect(p);
return true;
case CONTEXT_MENU_EDIT_ID:
startVpnEditor(p, false);
return true;
case CONTEXT_MENU_DELETE_ID:
deleteProfile(position);
return true;
}
return super.onContextItemSelected(item);
}
@Override
public void onActivityResult(final int requestCode, final int resultCode,
final Intent data) {
if (DEBUG) Log.d(TAG, "onActivityResult , result = " + resultCode + ", data = " + data);
if ((resultCode == Activity.RESULT_CANCELED) || (data == null)) {
Log.d(TAG, "no result returned by editor");
return;
}
if (requestCode == REQUEST_SELECT_VPN_TYPE) {
final String typeName = data.getStringExtra(KEY_VPN_TYPE);
startVpnEditor(createVpnProfile(typeName), true);
} else if (requestCode == REQUEST_ADD_OR_EDIT_PROFILE) {
VpnProfile p = data.getParcelableExtra(KEY_VPN_PROFILE);
if (p == null) {
Log.e(TAG, "null object returned by editor");
return;
}
final Activity activity = getActivity();
int index = getProfileIndexFromId(p.getId());
if (checkDuplicateName(p, index)) {
final VpnProfile profile = p;
Util.showErrorMessage(activity, String.format(
activity.getString(R.string.vpn_error_duplicate_name),
p.getName()),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
startVpnEditor(profile, false);
}
});
return;
}
if (needKeyStoreToSave(p)) {
Runnable action = new Runnable() {
public void run() {
onActivityResult(requestCode, resultCode, data);
}
};
if (!unlockKeyStore(p, action)) return;
}
try {
if (index < 0) {
addProfile(p);
Util.showShortToastMessage(activity, String.format(
activity.getString(R.string.vpn_profile_added), p.getName()));
} else {
replaceProfile(index, p);
Util.showShortToastMessage(activity, String.format(
activity.getString(R.string.vpn_profile_replaced),
p.getName()));
}
} catch (IOException e) {
final VpnProfile profile = p;
Util.showErrorMessage(activity, e + ": " + e.getMessage(),
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int w) {
startVpnEditor(profile, false);
}
});
}
// Remove cached VpnEditor as it is needless anymore.
} else {
throw new RuntimeException("unknown request code: " + requestCode);
}
}
// Called when the buttons on the connect dialog are clicked.
@Override
public synchronized void onClick(DialogInterface dialog, int which) {
if (which == CONNECT_BUTTON) {
Dialog d = (Dialog) dialog;
String error = mConnectingActor.validateInputs(d);
if (error == null) {
mConnectingActor.connect(d);
} else {
// show error dialog
final Activity activity = getActivity();
mShowingDialog = new AlertDialog.Builder(activity)
.setTitle(android.R.string.dialog_alert_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(String.format(activity.getString(
R.string.vpn_error_miss_entering), error))
.setPositiveButton(R.string.vpn_back_button,
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int which) {
showDialog(DIALOG_CONNECT);
}
})
.create();
// The profile state is "connecting". If we allow the dialog to
// be cancelable, then we need to clear the state in the
// onCancel handler.
mShowingDialog.setCancelable(false);
mShowingDialog.show();
}
} else {
changeState(mActiveProfile, VpnState.IDLE);
}
}
private int getProfileIndexFromId(String id) {
int index = 0;
for (VpnProfile p : sVpnProfileList) {
if (p.getId().equals(id)) {
return index;
} else {
index++;
}
}
return -1;
}
// Replaces the profile at index in sVpnProfileList with p.
// Returns true if p's name is a duplicate.
private boolean checkDuplicateName(VpnProfile p, int index) {
List<VpnProfile> list = sVpnProfileList;
VpnPreference pref = mVpnPreferenceMap.get(p.getName());
if ((pref != null) && (index >= 0) && (index < list.size())) {
// not a duplicate if p is to replace the profile at index
if (pref.mProfile == list.get(index)) pref = null;
}
return (pref != null);
}
private int getProfilePositionFrom(AdapterContextMenuInfo menuInfo) {
// excludes mVpnListContainer and the preferences above it
return menuInfo.position - mVpnListContainer.getOrder() - 1;
}
// position: position in sVpnProfileList
private VpnProfile getProfile(int position) {
return ((position >= 0) ? sVpnProfileList.get(position) : null);
}
// position: position in sVpnProfileList
private void deleteProfile(final int position) {
if ((position < 0) || (position >= sVpnProfileList.size())) return;
final VpnProfile target = sVpnProfileList.get(position);
DialogInterface.OnClickListener onClickListener =
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
// Double check if the target is still the one we want
// to remove.
VpnProfile p = sVpnProfileList.get(position);
if (p != target) return;
if (which == OK_BUTTON) {
sVpnProfileList.remove(position);
VpnPreference pref =
mVpnPreferenceMap.remove(p.getName());
mVpnListContainer.removePreference(pref);
removeProfileFromStorage(p);
}
}
};
mShowingDialog = new AlertDialog.Builder(getActivity())
.setTitle(android.R.string.dialog_alert_title)
.setIcon(android.R.drawable.ic_dialog_alert)
.setMessage(R.string.vpn_confirm_profile_deletion)
.setPositiveButton(android.R.string.ok, onClickListener)
.setNegativeButton(R.string.vpn_no_button, onClickListener)
.create();
mShowingDialog.show();
}
// Randomly generates an ID for the profile.
// The ID is unique and only set once when the profile is created.
private void setProfileId(VpnProfile profile) {
String id;
while (true) {
id = String.valueOf(Math.abs(
Double.doubleToLongBits(Math.random())));
if (id.length() >= 8) break;
}
for (VpnProfile p : sVpnProfileList) {
if (p.getId().equals(id)) {
setProfileId(profile);
return;
}
}
profile.setId(id);
}
private void addProfile(VpnProfile p) throws IOException {
setProfileId(p);
processSecrets(p);
saveProfileToStorage(p);
sVpnProfileList.add(p);
addPreferenceFor(p, true);
disableProfilePreferencesIfOneActive();
}
// Adds a preference in mVpnListContainer
private VpnPreference addPreferenceFor(
VpnProfile p, boolean addToContainer) {
VpnPreference pref = new VpnPreference(getActivity(), p);
mVpnPreferenceMap.put(p.getName(), pref);
if (addToContainer) mVpnListContainer.addPreference(pref);
pref.setOnPreferenceClickListener(
new Preference.OnPreferenceClickListener() {
public boolean onPreferenceClick(Preference pref) {
connectOrDisconnect(((VpnPreference) pref).mProfile);
return true;
}
});
return pref;
}
// index: index to sVpnProfileList
private void replaceProfile(int index, VpnProfile p) throws IOException {
Map<String, VpnPreference> map = mVpnPreferenceMap;
VpnProfile oldProfile = sVpnProfileList.set(index, p);
VpnPreference pref = map.remove(oldProfile.getName());
if (pref.mProfile != oldProfile) {
throw new RuntimeException("inconsistent state!");
}
p.setId(oldProfile.getId());
processSecrets(p);
// TODO: remove copyFiles once the setId() code propagates.
// Copy config files and remove the old ones if they are in different
// directories.
if (Util.copyFiles(getProfileDir(oldProfile), getProfileDir(p))) {
removeProfileFromStorage(oldProfile);
}
saveProfileToStorage(p);
pref.setProfile(p);
map.put(p.getName(), pref);
}
private void startVpnTypeSelection() {
if ((getActivity() == null) || isRemoving()) return;
((PreferenceActivity) getActivity()).startPreferencePanel(
VpnTypeSelection.class.getCanonicalName(), null, R.string.vpn_type_title, null,
this, REQUEST_SELECT_VPN_TYPE);
}
private boolean isKeyStoreUnlocked() {
return mKeyStore.state() == KeyStore.State.UNLOCKED;
}
// Returns true if the profile needs to access keystore
private boolean needKeyStoreToSave(VpnProfile p) {
switch (p.getType()) {
case L2TP_IPSEC_PSK:
L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
String presharedKey = pskProfile.getPresharedKey();
if (!TextUtils.isEmpty(presharedKey)) return true;
// $FALL-THROUGH$
case L2TP:
L2tpProfile l2tpProfile = (L2tpProfile) p;
if (l2tpProfile.isSecretEnabled() &&
!TextUtils.isEmpty(l2tpProfile.getSecretString())) {
return true;
}
// $FALL-THROUGH$
default:
return false;
}
}
// Returns true if the profile needs to access keystore
private boolean needKeyStoreToConnect(VpnProfile p) {
switch (p.getType()) {
case L2TP_IPSEC:
case L2TP_IPSEC_PSK:
return true;
case L2TP:
return ((L2tpProfile) p).isSecretEnabled();
default:
return false;
}
}
// Returns true if keystore is unlocked or keystore is not a concern
private boolean unlockKeyStore(VpnProfile p, Runnable action) {
if (isKeyStoreUnlocked()) return true;
mUnlockAction = action;
Credentials.getInstance().unlock(getActivity());
return false;
}
private void startVpnEditor(final VpnProfile profile, boolean add) {
if ((getActivity() == null) || isRemoving()) return;
Bundle args = new Bundle();
args.putParcelable(KEY_VPN_PROFILE, profile);
// TODO: Show different titles for add and edit.
((PreferenceActivity)getActivity()).startPreferencePanel(
VpnEditor.class.getCanonicalName(), args,
0, VpnEditor.getTitle(getActivity(), profile, add),
this, REQUEST_ADD_OR_EDIT_PROFILE);
}
private synchronized void connect(final VpnProfile p) {
if (needKeyStoreToConnect(p)) {
Runnable action = new Runnable() {
public void run() {
connect(p);
}
};
if (!unlockKeyStore(p, action)) return;
}
if (!checkSecrets(p)) return;
changeState(p, VpnState.CONNECTING);
if (mConnectingActor.isConnectDialogNeeded()) {
showDialog(DIALOG_CONNECT);
} else {
mConnectingActor.connect(null);
}
}
// Do connect or disconnect based on the current state.
private synchronized void connectOrDisconnect(VpnProfile p) {
VpnPreference pref = mVpnPreferenceMap.get(p.getName());
switch (p.getState()) {
case IDLE:
connect(p);
break;
case CONNECTING:
// do nothing
break;
case CONNECTED:
case DISCONNECTING:
changeState(p, VpnState.DISCONNECTING);
getActor(p).disconnect();
break;
}
}
private void changeState(VpnProfile p, VpnState state) {
VpnState oldState = p.getState();
p.setState(state);
mVpnPreferenceMap.get(p.getName()).setSummary(
getProfileSummaryString(p));
switch (state) {
case CONNECTED:
mConnectingActor = null;
mActiveProfile = p;
disableProfilePreferencesIfOneActive();
break;
case CONNECTING:
if (mConnectingActor == null) {
mConnectingActor = getActor(p);
}
// $FALL-THROUGH$
case DISCONNECTING:
mActiveProfile = p;
disableProfilePreferencesIfOneActive();
break;
case CANCELLED:
changeState(p, VpnState.IDLE);
break;
case IDLE:
assert(mActiveProfile == p);
if (mConnectingErrorCode == NO_ERROR) {
onIdle();
} else {
showDialog(mConnectingErrorCode);
mConnectingErrorCode = NO_ERROR;
}
break;
}
}
private void onIdle() {
if (DEBUG) Log.d(TAG, " onIdle()");
mActiveProfile = null;
mConnectingActor = null;
enableProfilePreferences();
}
private void disableProfilePreferencesIfOneActive() {
if (mActiveProfile == null) return;
for (VpnProfile p : sVpnProfileList) {
switch (p.getState()) {
case CONNECTING:
case DISCONNECTING:
case IDLE:
mVpnPreferenceMap.get(p.getName()).setEnabled(false);
break;
default:
mVpnPreferenceMap.get(p.getName()).setEnabled(true);
}
}
}
private void enableProfilePreferences() {
for (VpnProfile p : sVpnProfileList) {
mVpnPreferenceMap.get(p.getName()).setEnabled(true);
}
}
static String getProfileDir(VpnProfile p) {
return PROFILES_ROOT + p.getId();
}
static void saveProfileToStorage(VpnProfile p) throws IOException {
File f = new File(getProfileDir(p));
if (!f.exists()) f.mkdirs();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(
new File(f, PROFILE_OBJ_FILE)));
oos.writeObject(p);
oos.close();
}
private void removeProfileFromStorage(VpnProfile p) {
Util.deleteFile(getProfileDir(p));
}
private void updatePreferenceMap() {
mVpnPreferenceMap = new LinkedHashMap<String, VpnPreference>();
mVpnListContainer.removeAll();
for (VpnProfile p : sVpnProfileList) {
addPreferenceFor(p, true);
}
// reset the mActiveProfile if the profile has been removed from the
// other instance.
if ((mActiveProfile != null)
&& !mVpnPreferenceMap.containsKey(mActiveProfile.getName())) {
onIdle();
}
}
private void retrieveVpnListFromStorage() {
// skip the loop if the profile is loaded already.
if (sVpnProfileList.size() > 0) return;
File root = new File(PROFILES_ROOT);
String[] dirs = root.list();
if (dirs == null) return;
for (String dir : dirs) {
File f = new File(new File(root, dir), PROFILE_OBJ_FILE);
if (!f.exists()) continue;
try {
VpnProfile p = deserialize(f);
if (p == null) continue;
if (!checkIdConsistency(dir, p)) continue;
sVpnProfileList.add(p);
} catch (IOException e) {
Log.e(TAG, "retrieveVpnListFromStorage()", e);
}
}
Collections.sort(sVpnProfileList, new Comparator<VpnProfile>() {
public int compare(VpnProfile p1, VpnProfile p2) {
return p1.getName().compareTo(p2.getName());
}
});
disableProfilePreferencesIfOneActive();
}
private void checkVpnConnectionStatus() {
for (VpnProfile p : sVpnProfileList) {
changeState(p, mVpnManager.getState(p));
}
}
// A sanity check. Returns true if the profile directory name and profile ID
// are consistent.
private boolean checkIdConsistency(String dirName, VpnProfile p) {
if (!dirName.equals(p.getId())) {
Log.d(TAG, "ID inconsistent: " + dirName + " vs " + p.getId());
return false;
} else {
return true;
}
}
private VpnProfile deserialize(File profileObjectFile) throws IOException {
try {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(
profileObjectFile));
VpnProfile p = (VpnProfile) ois.readObject();
ois.close();
return p;
} catch (ClassNotFoundException e) {
Log.d(TAG, "deserialize a profile", e);
return null;
}
}
private String getProfileSummaryString(VpnProfile p) {
final Activity activity = getActivity();
switch (p.getState()) {
case CONNECTING:
return activity.getString(R.string.vpn_connecting);
case DISCONNECTING:
return activity.getString(R.string.vpn_disconnecting);
case CONNECTED:
return activity.getString(R.string.vpn_connected);
default:
return activity.getString(R.string.vpn_connect_hint);
}
}
private VpnProfileActor getActor(VpnProfile p) {
return new AuthenticationActor(getActivity(), p);
}
private VpnProfile createVpnProfile(String type) {
return mVpnManager.createVpnProfile(Enum.valueOf(VpnType.class, type));
}
private boolean checkSecrets(VpnProfile p) {
boolean secretMissing = false;
if (p instanceof L2tpIpsecProfile) {
L2tpIpsecProfile certProfile = (L2tpIpsecProfile) p;
String cert = certProfile.getCaCertificate();
if (TextUtils.isEmpty(cert) ||
!mKeyStore.contains(Credentials.CA_CERTIFICATE + cert)) {
certProfile.setCaCertificate(null);
secretMissing = true;
}
cert = certProfile.getUserCertificate();
if (TextUtils.isEmpty(cert) ||
!mKeyStore.contains(Credentials.USER_CERTIFICATE + cert)) {
certProfile.setUserCertificate(null);
secretMissing = true;
}
}
if (p instanceof L2tpIpsecPskProfile) {
L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
String presharedKey = pskProfile.getPresharedKey();
String key = KEY_PREFIX_IPSEC_PSK + p.getId();
if (TextUtils.isEmpty(presharedKey) || !mKeyStore.contains(key)) {
pskProfile.setPresharedKey(null);
secretMissing = true;
}
}
if (p instanceof L2tpProfile) {
L2tpProfile l2tpProfile = (L2tpProfile) p;
if (l2tpProfile.isSecretEnabled()) {
String secret = l2tpProfile.getSecretString();
String key = KEY_PREFIX_L2TP_SECRET + p.getId();
if (TextUtils.isEmpty(secret) || !mKeyStore.contains(key)) {
l2tpProfile.setSecretString(null);
secretMissing = true;
}
}
}
if (secretMissing) {
mActiveProfile = p;
showDialog(DIALOG_SECRET_NOT_SET);
return false;
} else {
return true;
}
}
private void processSecrets(VpnProfile p) {
switch (p.getType()) {
case L2TP_IPSEC_PSK:
L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p;
String presharedKey = pskProfile.getPresharedKey();
String key = KEY_PREFIX_IPSEC_PSK + p.getId();
if (!TextUtils.isEmpty(presharedKey) &&
!mKeyStore.put(key, presharedKey.getBytes(Charsets.UTF_8))) {
Log.e(TAG, "keystore write failed: key=" + key);
}
pskProfile.setPresharedKey(key);
// $FALL-THROUGH$
case L2TP_IPSEC:
case L2TP:
L2tpProfile l2tpProfile = (L2tpProfile) p;
key = KEY_PREFIX_L2TP_SECRET + p.getId();
if (l2tpProfile.isSecretEnabled()) {
String secret = l2tpProfile.getSecretString();
if (!TextUtils.isEmpty(secret) &&
!mKeyStore.put(key, secret.getBytes(Charsets.UTF_8))) {
Log.e(TAG, "keystore write failed: key=" + key);
}
l2tpProfile.setSecretString(key);
} else {
mKeyStore.delete(key);
}
break;
}
}
private class VpnPreference extends Preference {
VpnProfile mProfile;
VpnPreference(Context c, VpnProfile p) {
super(c);
setProfile(p);
}
void setProfile(VpnProfile p) {
mProfile = p;
setTitle(p.getName());
setSummary(getProfileSummaryString(p));
}
}
// to receive vpn connectivity events broadcast by VpnService
private class ConnectivityReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String profileName = intent.getStringExtra(
VpnManager.BROADCAST_PROFILE_NAME);
if (profileName == null) return;
VpnState s = (VpnState) intent.getSerializableExtra(
VpnManager.BROADCAST_CONNECTION_STATE);
if (s == null) {
Log.e(TAG, "received null connectivity state");
return;
}
mConnectingErrorCode = intent.getIntExtra(
VpnManager.BROADCAST_ERROR_CODE, NO_ERROR);
VpnPreference pref = mVpnPreferenceMap.get(profileName);
if (pref != null) {
Log.d(TAG, "received connectivity: " + profileName
+ ": connected? " + s
+ " err=" + mConnectingErrorCode);
// XXX: VpnService should broadcast non-IDLE state, say UNUSABLE,
// when an error occurs.
changeState(pref.mProfile, s);
} else {
Log.e(TAG, "received connectivity: " + profileName
+ ": connected? " + s + ", but profile does not exist;"
+ " just ignore it");
}
}
}
}