diff --git a/res/layout/user_credential.xml b/res/layout/user_credential.xml index 905822dc2de..f441bdac825 100644 --- a/res/layout/user_credential.xml +++ b/res/layout/user_credential.xml @@ -13,52 +13,65 @@ See the License for the specific language governing permissions and limitations under the License. --> - + android:orientation="vertical"> - + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="?android:attr/textColorSecondary" /> + + + + + android:textColor="?android:attr/textColorTertiary" + android:paddingStart="?android:attr/listPreferredItemPaddingStart"/> + android:textColor="?android:attr/textColorTertiary" + android:paddingStart="?android:attr/listPreferredItemPaddingStart"/> - + android:textColor="?android:attr/textColorTertiary" + android:paddingStart="?android:attr/listPreferredItemPaddingStart"/> diff --git a/res/layout/user_credential_preference.xml b/res/layout/user_credential_preference.xml new file mode 100644 index 00000000000..3671b7fac5b --- /dev/null +++ b/res/layout/user_credential_preference.xml @@ -0,0 +1,35 @@ + + + + + + + + diff --git a/res/values/strings.xml b/res/values/strings.xml index 7d04744bb18..5163dec0b71 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -4643,6 +4643,10 @@ Software only Credentials are not available for this user + + Installed for VPN and apps + + Installed for Wi-Fi @@ -5331,6 +5335,8 @@ Permanently remove the user CA certificate? + + This entry contains: one user key diff --git a/src/com/android/settings/UserCredentialsSettings.java b/src/com/android/settings/UserCredentialsSettings.java index 2681da6f214..bbab0c53b21 100644 --- a/src/com/android/settings/UserCredentialsSettings.java +++ b/src/com/android/settings/UserCredentialsSettings.java @@ -16,6 +16,8 @@ package com.android.settings; +import android.annotation.LayoutRes; +import android.annotation.Nullable; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; @@ -26,6 +28,7 @@ import android.os.AsyncTask; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.os.Process; import android.os.RemoteException; import android.os.UserHandle; import android.os.UserManager; @@ -35,6 +38,7 @@ import android.security.KeyChain; import android.security.KeyChain.KeyChainConnection; import android.security.KeyStore; import android.util.Log; +import android.util.SparseArray; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -49,7 +53,9 @@ import com.android.internal.widget.LockPatternUtils; import com.android.settingslib.RestrictedLockUtils; import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin; +import java.util.ArrayList; import java.util.EnumSet; +import java.util.List; import java.util.SortedMap; import java.util.TreeMap; @@ -59,7 +65,6 @@ import static android.view.View.VISIBLE; public class UserCredentialsSettings extends OptionsMenuFragment implements OnItemClickListener { private static final String TAG = "UserCredentialsSettings"; - private View mRootView; private ListView mListView; @Override @@ -76,13 +81,13 @@ public class UserCredentialsSettings extends OptionsMenuFragment implements OnIt @Override public View onCreateView( LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) { - mRootView = inflater.inflate(R.layout.user_credentials, parent, false); + final View rootView = inflater.inflate(R.layout.user_credentials, parent, false); // Set up an OnItemClickListener for the credential list. - mListView = (ListView) mRootView.findViewById(R.id.credential_list); + mListView = (ListView) rootView.findViewById(R.id.credential_list); mListView.setOnItemClickListener(this); - return mRootView; + return rootView; } @Override @@ -122,15 +127,13 @@ public class UserCredentialsSettings extends OptionsMenuFragment implements OnIt @Override public Dialog onCreateDialog(Bundle savedInstanceState) { final Credential item = (Credential) getArguments().getParcelable(ARG_CREDENTIAL); + View root = getActivity().getLayoutInflater() .inflate(R.layout.user_credential_dialog, null); ViewGroup infoContainer = (ViewGroup) root.findViewById(R.id.credential_container); - View view = new CredentialAdapter(getActivity(), R.layout.user_credential, - new Credential[] {item}).getView(0, null, null); - infoContainer.addView(view); - - UserManager userManager - = (UserManager) getContext().getSystemService(Context.USER_SERVICE); + View contentView = getCredentialView(item, R.layout.user_credential, null, + infoContainer, /* expanded */ true); + infoContainer.addView(contentView); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) .setView(root) @@ -148,49 +151,74 @@ public class UserCredentialsSettings extends OptionsMenuFragment implements OnIt RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(), admin); } else { - new RemoveCredentialsTask(getTargetFragment()).execute(item.alias); + new RemoveCredentialsTask(getContext(), getTargetFragment()) + .execute(item); } dialog.dismiss(); } }; - builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener); + if (item.isSystem()) { + // TODO: a safe means of clearing wifi certificates. Configs refer to aliases + // directly so deleting certs will break dependent access points. + builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener); + } } return builder.create(); } - private class RemoveCredentialsTask extends AsyncTask { + /** + * Deletes all certificates and keys under a given alias. + * + * If the {@link Credential} is for a system alias, all active grants to the alias will be + * removed using {@link KeyChain}. + */ + private class RemoveCredentialsTask extends AsyncTask { + private Context context; private Fragment targetFragment; - public RemoveCredentialsTask(Fragment targetFragment) { + public RemoveCredentialsTask(Context context, Fragment targetFragment) { + this.context = context; this.targetFragment = targetFragment; } @Override - protected String[] doInBackground(String... aliases) { - try { - final KeyChainConnection conn = KeyChain.bind(getContext()); - try { - IKeyChainService keyChain = conn.getService(); - for (String alias : aliases) { - keyChain.removeKeyPair(alias); - } - } catch (RemoteException e) { - Log.w(TAG, "Removing credentials", e); - } finally { - conn.close(); + protected Credential[] doInBackground(Credential... credentials) { + for (final Credential credential : credentials) { + if (credential.isSystem()) { + removeGrantsAndDelete(credential); + continue; } - } catch (InterruptedException e) { - Log.w(TAG, "Connecting to keychain", e); + throw new UnsupportedOperationException( + "Not implemented for wifi certificates. This should not be reachable."); + } + return credentials; + } + + private void removeGrantsAndDelete(final Credential credential) { + final KeyChainConnection conn; + try { + conn = KeyChain.bind(getContext()); + } catch (InterruptedException e) { + Log.w(TAG, "Connecting to KeyChain", e); + return; + } + + try { + IKeyChainService keyChain = conn.getService(); + keyChain.removeKeyPair(credential.alias); + } catch (RemoteException e) { + Log.w(TAG, "Removing credentials", e); + } finally { + conn.close(); } - return aliases; } @Override - protected void onPostExecute(String... aliases) { + protected void onPostExecute(Credential... credentials) { if (targetFragment instanceof UserCredentialsSettings && targetFragment.isAdded()) { final UserCredentialsSettings target = (UserCredentialsSettings) targetFragment; - for (final String alias : aliases) { - target.announceRemoval(alias); + for (final Credential credential : credentials) { + target.announceRemoval(credential.alias); } target.refreshItems(); } @@ -203,34 +231,53 @@ public class UserCredentialsSettings extends OptionsMenuFragment implements OnIt * The credentials are stored in a {@link CredentialAdapter} attached to the main * {@link ListView} in the fragment. */ - private class AliasLoader extends AsyncTask> { + private class AliasLoader extends AsyncTask> { + /** + * @return a list of credentials ordered: + *
    + *
  1. first by purpose;
  2. + *
  3. then by alias.
  4. + *
+ */ @Override - protected SortedMap doInBackground(Void... params) { - // Create a list of names for credential sets, ordered by name. - SortedMap credentials = new TreeMap<>(); - KeyStore keyStore = KeyStore.getInstance(); + protected List doInBackground(Void... params) { + final KeyStore keyStore = KeyStore.getInstance(); + + // Certificates can be installed into SYSTEM_UID or WIFI_UID through CertInstaller. + final int myUserId = UserHandle.myUserId(); + final int systemUid = UserHandle.getUid(myUserId, Process.SYSTEM_UID); + final int wifiUid = UserHandle.getUid(myUserId, Process.WIFI_UID); + + List credentials = new ArrayList<>(); + credentials.addAll(getCredentialsForUid(keyStore, systemUid).values()); + credentials.addAll(getCredentialsForUid(keyStore, wifiUid).values()); + return credentials; + } + + private SortedMap getCredentialsForUid(KeyStore keyStore, int uid) { + final SortedMap aliasMap = new TreeMap<>(); for (final Credential.Type type : Credential.Type.values()) { - for (final String alias : keyStore.list(type.prefix)) { + for (final String alias : keyStore.list(type.prefix, uid)) { // Do not show work profile keys in user credentials if (alias.startsWith(LockPatternUtils.PROFILE_KEY_NAME_ENCRYPT) || alias.startsWith(LockPatternUtils.PROFILE_KEY_NAME_DECRYPT)) { continue; } - Credential c = credentials.get(alias); + Credential c = aliasMap.get(alias); if (c == null) { - credentials.put(alias, (c = new Credential(alias))); + c = new Credential(alias, uid); + aliasMap.put(alias, c); } c.storedTypes.add(type); } } - return credentials; + return aliasMap; } @Override - protected void onPostExecute(SortedMap credentials) { - // Convert the results to an array and present them using an ArrayAdapter. - mListView.setAdapter(new CredentialAdapter(getContext(), R.layout.user_credential, - credentials.values().toArray(new Credential[0]))); + protected void onPostExecute(List credentials) { + final Credential[] credentialArray = credentials.toArray(new Credential[0]); + mListView.setAdapter(new CredentialAdapter(getContext(), credentialArray)); } } @@ -238,28 +285,56 @@ public class UserCredentialsSettings extends OptionsMenuFragment implements OnIt * Helper class to display {@link Credential}s in a list. */ private static class CredentialAdapter extends ArrayAdapter { - public CredentialAdapter(Context context, int resource, Credential[] objects) { - super(context, resource, objects); + private static final int LAYOUT_RESOURCE = R.layout.user_credential_preference; + + public CredentialAdapter(Context context, final Credential[] objects) { + super(context, LAYOUT_RESOURCE, objects); } @Override - public View getView(int position, View view, ViewGroup parent) { - if (view == null) { - view = LayoutInflater.from(getContext()) - .inflate(R.layout.user_credential, parent, false); - } - Credential item = getItem(position); - ((TextView) view.findViewById(R.id.alias)).setText(item.alias); - view.findViewById(R.id.contents_userkey).setVisibility( - item.storedTypes.contains(Credential.Type.USER_PRIVATE_KEY) ? VISIBLE : GONE); - view.findViewById(R.id.contents_usercrt).setVisibility( - item.storedTypes.contains(Credential.Type.USER_CERTIFICATE) ? VISIBLE : GONE); - view.findViewById(R.id.contents_cacrt).setVisibility( - item.storedTypes.contains(Credential.Type.CA_CERTIFICATE) ? VISIBLE : GONE); - return view; + public View getView(int position, @Nullable View view, ViewGroup parent) { + return getCredentialView(getItem(position), LAYOUT_RESOURCE, view, parent, + /* expanded */ false); } } + /** + * Mapping from View IDs in {@link R} to the types of credentials they describe. + */ + private static final SparseArray credentialViewTypes = new SparseArray<>(); + static { + credentialViewTypes.put(R.id.contents_userkey, Credential.Type.USER_PRIVATE_KEY); + credentialViewTypes.put(R.id.contents_usercrt, Credential.Type.USER_CERTIFICATE); + credentialViewTypes.put(R.id.contents_cacrt, Credential.Type.CA_CERTIFICATE); + } + + protected static View getCredentialView(Credential item, @LayoutRes int layoutResource, + @Nullable View view, ViewGroup parent, boolean expanded) { + if (view == null) { + view = LayoutInflater.from(parent.getContext()).inflate(layoutResource, parent, false); + } + + ((TextView) view.findViewById(R.id.alias)).setText(item.alias); + ((TextView) view.findViewById(R.id.purpose)).setText(item.isSystem() + ? R.string.credential_for_vpn_and_apps + : R.string.credential_for_wifi); + + view.findViewById(R.id.contents).setVisibility(expanded ? View.VISIBLE : View.GONE); + if (expanded) { + for (int i = 0; i < credentialViewTypes.size(); i++) { + final View detail = view.findViewById(credentialViewTypes.keyAt(i)); + detail.setVisibility(item.storedTypes.contains(credentialViewTypes.valueAt(i)) + ? View.VISIBLE : View.GONE); + } + } + return view; + } + + static class AliasEntry { + public String alias; + public int uid; + } + static class Credential implements Parcelable { static enum Type { CA_CERTIFICATE (Credentials.CA_CERTIFICATE), @@ -280,6 +355,12 @@ public class UserCredentialsSettings extends OptionsMenuFragment implements OnIt */ final String alias; + /** + * UID under which this credential is stored. Typically {@link Process#SYSTEM_UID} but can + * also be {@link Process#WIFI_UID} for credentials installed as wifi certificates. + */ + final int uid; + /** * Should contain some non-empty subset of: *
    @@ -291,12 +372,13 @@ public class UserCredentialsSettings extends OptionsMenuFragment implements OnIt */ final EnumSet storedTypes = EnumSet.noneOf(Type.class); - Credential(final String alias) { + Credential(final String alias, final int uid) { this.alias = alias; + this.uid = uid; } Credential(Parcel in) { - this(in.readString()); + this(in.readString(), in.readInt()); long typeBits = in.readLong(); for (Type i : Type.values()) { @@ -308,6 +390,7 @@ public class UserCredentialsSettings extends OptionsMenuFragment implements OnIt public void writeToParcel(Parcel out, int flags) { out.writeString(alias); + out.writeInt(uid); long typeBits = 0; for (Type i : storedTypes) { @@ -330,5 +413,9 @@ public class UserCredentialsSettings extends OptionsMenuFragment implements OnIt return new Credential[size]; } }; + + public boolean isSystem() { + return UserHandle.getAppId(uid) == Process.SYSTEM_UID; + } } } diff --git a/tests/unit/src/com/android/settings/UserCredentialsTest.java b/tests/unit/src/com/android/settings/UserCredentialsTest.java index 6de252e0901..41ef4dec2f5 100644 --- a/tests/unit/src/com/android/settings/UserCredentialsTest.java +++ b/tests/unit/src/com/android/settings/UserCredentialsTest.java @@ -17,6 +17,7 @@ package com.android.settings; import android.os.Parcel; +import android.os.Process; import android.test.InstrumentationTestCase; import android.test.suitebuilder.annotation.SmallTest; @@ -36,7 +37,7 @@ public class UserCredentialsTest extends InstrumentationTestCase { @SmallTest public void testCredentialIsParcelable() { final String alias = "credential-test-alias"; - Credential c = new Credential(alias); + Credential c = new Credential(alias, Process.SYSTEM_UID); c.storedTypes.add(Credential.Type.CA_CERTIFICATE); c.storedTypes.add(Credential.Type.USER_SECRET_KEY); @@ -47,6 +48,7 @@ public class UserCredentialsTest extends InstrumentationTestCase { Credential r = Credential.CREATOR.createFromParcel(p); assertEquals(c.alias, r.alias); + assertEquals(c.uid, r.uid); assertEquals(c.storedTypes, r.storedTypes); } }