From 0a59b500147cc038541f6f2897de7e28c15a12c1 Mon Sep 17 00:00:00 2001 From: Hung-ying Tyan Date: Mon, 6 Jul 2009 17:26:34 +0800 Subject: [PATCH] Integrate VPN with new keystore and misc fixes. * Changes + Pass intent to keystore when needed and hooks to resume from it. + Generate random, unique ID for profile instead of base64 from its name. + Add VPN to "Wirless controls" description. + Add credential storage to "Security & location" description. + More hints to set password and unlock dialogs in credential storage settings for actions that come from other processes. + Sort VPN profiles according to the names. + Replace Keystore with CertTool in L2tpIpsecEditor --- res/layout/cstor_unlock_dialog_view.xml | 7 + res/values/strings.xml | 8 +- .../android/settings/SecuritySettings.java | 33 ++- src/com/android/settings/vpn/L2tpEditor.java | 6 - .../android/settings/vpn/L2tpIpsecEditor.java | 6 +- .../settings/vpn/L2tpIpsecPskEditor.java | 7 - src/com/android/settings/vpn/Util.java | 6 - src/com/android/settings/vpn/VpnEditor.java | 7 - .../settings/vpn/VpnProfileEditor.java | 7 - src/com/android/settings/vpn/VpnSettings.java | 189 ++++++++++++++++-- 10 files changed, 216 insertions(+), 60 deletions(-) diff --git a/res/layout/cstor_unlock_dialog_view.xml b/res/layout/cstor_unlock_dialog_view.xml index 66a21754d4a..895306a39e7 100644 --- a/res/layout/cstor_unlock_dialog_view.xml +++ b/res/layout/cstor_unlock_dialog_view.xml @@ -24,6 +24,13 @@ android:layout_height="fill_parent" android:padding="15dip"> + + Wireless controls - Manage Wi-Fi, Bluetooth, airplane mode, & mobile networks + Manage Wi-Fi, Bluetooth, airplane mode, mobile networks, & VPNs @@ -488,7 +488,7 @@ Security & location - Set My Location, screen unlock, SIM card lock + Set My Location, screen unlock, SIM card lock, credential storage lock Passwords @@ -1946,6 +1946,8 @@ found in the list of installed applications. Allow applications to access secure certificates and other credentials Enter password + + This action requires enabling the credential storage. Please enter the password to enable it. Set password @@ -1977,6 +1979,8 @@ found in the list of installed applications. Confirm new password: You must set a password for the credential storage. + + This action requires the credential storage but the storage has not been activated before. To activiate it, you must set a password for the credential storage. Please enter the correct password. Please enter the correct password. You have one more try to enter the correct password before the credential storage is erased. Please enter the correct password. You have %d more tries to enter the correct password before the credential storage is erased. diff --git a/src/com/android/settings/SecuritySettings.java b/src/com/android/settings/SecuritySettings.java index 4e77d6b3c14..73578c738ac 100644 --- a/src/com/android/settings/SecuritySettings.java +++ b/src/com/android/settings/SecuritySettings.java @@ -577,8 +577,7 @@ public class SecuritySettings extends PreferenceActivity implements : R.string.cstor_password_error); if (count <= 3) { if (count == 1) { - v.setText(getString( - R.string.cstor_password_error_reset_warning)); + v.setText(R.string.cstor_password_error_reset_warning); } else { String format = getString( R.string.cstor_password_error_reset_warning_plural); @@ -691,11 +690,15 @@ public class SecuritySettings extends PreferenceActivity implements return v; } - private void hideError() { - View v = mView.findViewById(R.id.cstor_error); + private void hide(int viewId) { + View v = mView.findViewById(viewId); if (v != null) v.setVisibility(View.GONE); } + private void hideError() { + hide(R.id.cstor_error); + } + private String getText(int viewId) { return ((TextView) mView.findViewById(viewId)).getText().toString(); } @@ -705,6 +708,11 @@ public class SecuritySettings extends PreferenceActivity implements if (v != null) v.setText(text); } + private void setText(int viewId, int textId) { + TextView v = (TextView) mView.findViewById(viewId); + if (v != null) v.setText(textId); + } + private void enablePreferences(boolean enabled) { mAccessCheckBox.setEnabled(enabled); mResetButton.setEnabled(enabled); @@ -773,6 +781,12 @@ public class SecuritySettings extends PreferenceActivity implements R.layout.cstor_unlock_dialog_view, null); hideError(); + // show extra hint only when the action comes from outside + if ((mSpecialIntent == null) + && (mCstorAddCredentialHelper == null)) { + hide(R.id.cstor_access_dialog_hint_from_action); + } + Dialog d = new AlertDialog.Builder(SecuritySettings.this) .setView(mView) .setTitle(R.string.cstor_access_dialog_title) @@ -790,6 +804,13 @@ public class SecuritySettings extends PreferenceActivity implements R.layout.cstor_set_password_dialog_view, null); hideError(); + // show extra hint only when the action comes from outside + if ((mSpecialIntent != null) + || (mCstorAddCredentialHelper != null)) { + setText(R.id.cstor_first_time_hint, + R.string.cstor_first_time_hint_from_action); + } + switch (id) { case CSTOR_INIT_DIALOG: mView.findViewById(R.id.cstor_old_password_block) @@ -835,9 +856,9 @@ public class SecuritySettings extends PreferenceActivity implements hideError(); setText(R.id.cstor_credential_name_title, - getString(R.string.cstor_credential_name)); + R.string.cstor_credential_name); setText(R.id.cstor_credential_info_title, - getString(R.string.cstor_credential_info)); + R.string.cstor_credential_info); setText(R.id.cstor_credential_info, mCstorAddCredentialHelper.getDescription().toString()); diff --git a/src/com/android/settings/vpn/L2tpEditor.java b/src/com/android/settings/vpn/L2tpEditor.java index 88a1142a156..c518dec46a6 100644 --- a/src/com/android/settings/vpn/L2tpEditor.java +++ b/src/com/android/settings/vpn/L2tpEditor.java @@ -60,12 +60,6 @@ class L2tpEditor extends VpnProfileEditor { : validate(mSecretString, R.string.vpn_l2tp_secret)); } - @Override - public void saveSecrets(String originalProfileName) { - L2tpProfile profile = (L2tpProfile) getProfile(); - // TODO: fill up the implementation after keystore is available - } - private Preference createSecretPreference(Context c) { final L2tpProfile profile = (L2tpProfile) getProfile(); CheckBoxPreference secret = mSecret = new CheckBoxPreference(c); diff --git a/src/com/android/settings/vpn/L2tpIpsecEditor.java b/src/com/android/settings/vpn/L2tpIpsecEditor.java index e79760c8dd8..b6b244f7ee5 100644 --- a/src/com/android/settings/vpn/L2tpIpsecEditor.java +++ b/src/com/android/settings/vpn/L2tpIpsecEditor.java @@ -24,7 +24,7 @@ import android.preference.EditTextPreference; import android.preference.ListPreference; import android.preference.Preference; import android.preference.PreferenceGroup; -import android.security.Keystore; +import android.security.CertTool; import android.text.TextUtils; /** @@ -67,7 +67,7 @@ class L2tpIpsecEditor extends L2tpEditor { mUserCertificate = createListPreference(c, R.string.vpn_user_certificate_title, mProfile.getUserCertificate(), - Keystore.getInstance().getAllUserCertificateKeys(), + CertTool.getInstance().getAllUserCertificateKeys(), new Preference.OnPreferenceChangeListener() { public boolean onPreferenceChange( Preference pref, Object newValue) { @@ -86,7 +86,7 @@ class L2tpIpsecEditor extends L2tpEditor { mCaCertificate = createListPreference(c, R.string.vpn_ca_certificate_title, mProfile.getCaCertificate(), - Keystore.getInstance().getAllCaCertificateKeys(), + CertTool.getInstance().getAllCaCertificateKeys(), new Preference.OnPreferenceChangeListener() { public boolean onPreferenceChange( Preference pref, Object newValue) { diff --git a/src/com/android/settings/vpn/L2tpIpsecPskEditor.java b/src/com/android/settings/vpn/L2tpIpsecPskEditor.java index 9c1d02c3903..fb67c987ffc 100644 --- a/src/com/android/settings/vpn/L2tpIpsecPskEditor.java +++ b/src/com/android/settings/vpn/L2tpIpsecPskEditor.java @@ -50,13 +50,6 @@ class L2tpIpsecPskEditor extends L2tpEditor { : validate(mPresharedKey, R.string.vpn_ipsec_presharedkey)); } - @Override - public void saveSecrets(String originalProfileName) { - L2tpIpsecPskProfile profile = (L2tpIpsecPskProfile) getProfile(); - profile.getPresharedKey(); - // TODO: fill up the implementation after keystore is available - } - private Preference createPresharedKeyPreference(Context c) { final L2tpIpsecPskProfile profile = (L2tpIpsecPskProfile) getProfile(); mPresharedKey = createSecretPreference(c, diff --git a/src/com/android/settings/vpn/Util.java b/src/com/android/settings/vpn/Util.java index e4316fd0831..a37049d2ca2 100644 --- a/src/com/android/settings/vpn/Util.java +++ b/src/com/android/settings/vpn/Util.java @@ -23,8 +23,6 @@ import android.content.Context; import android.content.DialogInterface; import android.widget.Toast; -import org.apache.commons.codec.binary.Base64; - import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; @@ -59,10 +57,6 @@ class Util { createErrorDialog(c, message, listener).show(); } - static String base64Encode(byte[] bytes) { - return new String(Base64.encodeBase64(bytes)); - } - static void deleteFile(String path) { deleteFile(new File(path)); } diff --git a/src/com/android/settings/vpn/VpnEditor.java b/src/com/android/settings/vpn/VpnEditor.java index e33ce550904..1d419eab215 100644 --- a/src/com/android/settings/vpn/VpnEditor.java +++ b/src/com/android/settings/vpn/VpnEditor.java @@ -46,7 +46,6 @@ public class VpnEditor extends PreferenceActivity { private VpnProfileEditor mProfileEditor; private boolean mAddingProfile; - private String mOriginalProfileName; @Override public void onCreate(Bundle savedInstanceState) { @@ -54,9 +53,6 @@ public class VpnEditor extends PreferenceActivity { VpnProfile p = (VpnProfile) ((savedInstanceState == null) ? getIntent().getParcelableExtra(VpnSettings.KEY_VPN_PROFILE) : savedInstanceState.getParcelable(KEY_PROFILE)); - mOriginalProfileName = (savedInstanceState == null) - ? p.getName() - : savedInstanceState.getString(KEY_ORIGINAL_PROFILE_NAME); mProfileEditor = getEditor(p); mAddingProfile = TextUtils.isEmpty(p.getName()); @@ -71,7 +67,6 @@ public class VpnEditor extends PreferenceActivity { if (mProfileEditor == null) return; outState.putParcelable(KEY_PROFILE, getProfile()); - outState.putString(KEY_ORIGINAL_PROFILE_NAME, mOriginalProfileName); } @Override @@ -126,13 +121,11 @@ public class VpnEditor extends PreferenceActivity { return false; } - mProfileEditor.saveSecrets(mOriginalProfileName); setResult(getProfile()); return true; } private void setResult(VpnProfile p) { - p.setId(Util.base64Encode(p.getName().getBytes())); Intent intent = new Intent(this, VpnSettings.class); intent.putExtra(VpnSettings.KEY_VPN_PROFILE, (Parcelable) p); setResult(RESULT_OK, intent); diff --git a/src/com/android/settings/vpn/VpnProfileEditor.java b/src/com/android/settings/vpn/VpnProfileEditor.java index b679bcb6550..a708a8c89d5 100644 --- a/src/com/android/settings/vpn/VpnProfileEditor.java +++ b/src/com/android/settings/vpn/VpnProfileEditor.java @@ -91,13 +91,6 @@ class VpnProfileEditor { : validate(mServerName, R.string.vpn_vpn_server)); } - /** - * Saves the secrets in this profile. - * @param originalProfileName the original profile name - */ - public void saveSecrets(String originalProfileName) { - } - /** * Creates a preference for users to input domain suffices. */ diff --git a/src/com/android/settings/vpn/VpnSettings.java b/src/com/android/settings/vpn/VpnSettings.java index bbbe9b92a5b..e429f9f37e5 100644 --- a/src/com/android/settings/vpn/VpnSettings.java +++ b/src/com/android/settings/vpn/VpnSettings.java @@ -17,6 +17,7 @@ package com.android.settings.vpn; import com.android.settings.R; +import com.android.settings.SecuritySettings; import android.app.AlertDialog; import android.app.Dialog; @@ -24,6 +25,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; +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; @@ -38,6 +41,7 @@ import android.preference.PreferenceCategory; import android.preference.PreferenceManager; import android.preference.PreferenceScreen; import android.preference.Preference.OnPreferenceClickListener; +import android.security.Keystore; import android.text.TextUtils; import android.util.Log; import android.view.ContextMenu; @@ -54,8 +58,8 @@ import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; +import java.util.Comparator; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashMap; @@ -113,6 +117,9 @@ public class VpnSettings extends PreferenceActivity implements private VpnProfileActor mConnectingActor; private boolean mStateSaved = false; + // states saved for unlocking keystore + private Runnable mUnlockAction; + private VpnManager mVpnManager = new VpnManager(this); private ConnectivityReceiver mConnectivityReceiver = @@ -169,6 +176,12 @@ public class VpnSettings extends PreferenceActivity implements public void onResume() { super.onResume(); mStatusChecker.onResume(); + + if ((mUnlockAction != null) && isKeystoreUnlocked()) { + Runnable action = mUnlockAction; + mUnlockAction = null; + runOnUiThread(action); + } } @Override @@ -279,9 +292,9 @@ public class VpnSettings extends PreferenceActivity implements } @Override - protected void onActivityResult(int requestCode, int resultCode, - Intent data) { - int index = mIndexOfEditedProfile; + protected void onActivityResult(final int requestCode, final int resultCode, + final Intent data) { + final int index = mIndexOfEditedProfile; mIndexOfEditedProfile = -1; if ((resultCode == RESULT_CANCELED) || (data == null)) { @@ -312,6 +325,16 @@ public class VpnSettings extends PreferenceActivity implements return; } + if (needKeystoreToSave(p)) { + Runnable action = new Runnable() { + public void run() { + mIndexOfEditedProfile = index; + onActivityResult(requestCode, resultCode, data); + } + }; + if (!unlockKeystore(p, action)) return; + } + try { if ((index < 0) || (index >= mVpnProfileList.size())) { addProfile(p); @@ -414,7 +437,27 @@ public class VpnSettings extends PreferenceActivity implements .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 : mVpnProfileList) { + if (p.getId().equals(id)) { + setProfileId(profile); + return; + } + } + profile.setId(id); + } + private void addProfile(VpnProfile p) throws IOException { + setProfileId(p); saveProfileToStorage(p); mVpnProfileList.add(p); addPreferenceFor(p); @@ -445,8 +488,11 @@ public class VpnSettings extends PreferenceActivity implements throw new RuntimeException("inconsistent state!"); } - // TODO: call saveSecret(String) after keystore is available + 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))) { @@ -463,25 +509,93 @@ public class VpnSettings extends PreferenceActivity implements startActivityForResult(intent, REQUEST_SELECT_VPN_TYPE); } - private void startVpnEditor(VpnProfile profile) { + private boolean isKeystoreUnlocked() { + return (Keystore.getInstance().getState() == Keystore.UNLOCKED); + } + + + // Returns true if the profile needs to access keystore + private boolean needKeystoreToSave(VpnProfile p) { + return needKeystoreToConnect(p); + } + + // Returns true if the profile needs to access keystore + private boolean needKeystoreToEdit(VpnProfile p) { + switch (p.getType()) { + case L2TP_IPSEC: + case L2TP_IPSEC_PSK: + return true; + + 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; + startActivity( + new Intent(SecuritySettings.ACTION_UNLOCK_CREDENTIAL_STORAGE)); + return false; + } + + private void startVpnEditor(final VpnProfile profile) { + if (needKeystoreToEdit(profile)) { + Runnable action = new Runnable() { + public void run() { + startVpnEditor(profile); + } + }; + if (!unlockKeystore(profile, action)) return; + } + Intent intent = new Intent(this, VpnEditor.class); intent.putExtra(KEY_VPN_PROFILE, (Parcelable) profile); startActivityForResult(intent, 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; + } + + mConnectingActor = getActor(p); + if (mConnectingActor.isConnectDialogNeeded()) { + removeDialog(DIALOG_CONNECT); + showDialog(DIALOG_CONNECT); + } else { + changeState(p, VpnState.CONNECTING); + 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: - mConnectingActor = getActor(p); - if (mConnectingActor.isConnectDialogNeeded()) { - removeDialog(DIALOG_CONNECT); - showDialog(DIALOG_CONNECT); - } else { - changeState(p, VpnState.CONNECTING); - mConnectingActor.connect(null); - } + connect(p); break; case CONNECTING: @@ -601,7 +715,6 @@ public class VpnSettings extends PreferenceActivity implements File root = new File(PROFILES_ROOT); String[] dirs = root.list(); if (dirs == null) return; - Arrays.sort(dirs); for (String dir : dirs) { File f = new File(new File(root, dir), PROFILE_OBJ_FILE); if (!f.exists()) continue; @@ -611,11 +724,21 @@ public class VpnSettings extends PreferenceActivity implements if (!checkIdConsistency(dir, p)) continue; mVpnProfileList.add(p); - addPreferenceFor(p); } catch (IOException e) { Log.e(TAG, "retrieveVpnListFromStorage()", e); } } + Collections.sort(mVpnProfileList, new Comparator() { + public int compare(VpnProfile p1, VpnProfile p2) { + return p1.getName().compareTo(p2.getName()); + } + + public boolean equals(VpnProfile p) { + // not used + return false; + } + }); + for (VpnProfile p : mVpnProfileList) addPreferenceFor(p); disableProfilePreferencesIfOneActive(); } @@ -668,6 +791,40 @@ public class VpnSettings extends PreferenceActivity implements return mVpnManager.createVpnProfile(Enum.valueOf(VpnType.class, type)); } + private static final String NAMESPACE_VPN = "vpn"; + private static final String KEY_PREFIX_IPSEC_PSK = "ipsk000"; + private static final String KEY_PREFIX_L2TP_SECRET = "lscrt000"; + + private void processSecrets(VpnProfile p) { + Keystore ks = Keystore.getInstance(); + switch (p.getType()) { + case L2TP_IPSEC_PSK: + L2tpIpsecPskProfile pskProfile = (L2tpIpsecPskProfile) p; + String keyName = KEY_PREFIX_IPSEC_PSK + p.getId(); + String presharedKey = pskProfile.getPresharedKey(); + if (!presharedKey.equals(keyName)) { + ks.put(NAMESPACE_VPN, keyName, presharedKey); + pskProfile.setPresharedKey(NAMESPACE_VPN + "_" + keyName); + } + // pass through + + case L2TP: + L2tpProfile l2tpProfile = (L2tpProfile) p; + keyName = KEY_PREFIX_L2TP_SECRET + p.getId(); + String secret = l2tpProfile.getSecretString(); + if (l2tpProfile.isSecretEnabled()) { + if (!secret.equals(keyName)) { + ks.put(NAMESPACE_VPN, keyName, secret); + l2tpProfile.setSecretString( + NAMESPACE_VPN + "_" + keyName); + } + } else { + ks.remove(NAMESPACE_VPN, keyName); + } + break; + } + } + private class VpnPreference extends Preference { VpnProfile mProfile; VpnPreference(Context c, VpnProfile p) {