UserCredentialsSettings.java revision e68d9577772d312a4a8e7f47d6f2a74e0a5c2615
1/*
2 * Copyright (C) 2015 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
17package com.android.settings;
18
19import android.annotation.LayoutRes;
20import android.annotation.Nullable;
21import android.app.AlertDialog;
22import android.app.Dialog;
23import android.app.DialogFragment;
24import android.app.Fragment;
25import android.content.Context;
26import android.content.DialogInterface;
27import android.os.AsyncTask;
28import android.os.Bundle;
29import android.os.Parcel;
30import android.os.Parcelable;
31import android.os.Process;
32import android.os.RemoteException;
33import android.os.UserHandle;
34import android.os.UserManager;
35import android.security.Credentials;
36import android.security.IKeyChainService;
37import android.security.KeyChain;
38import android.security.KeyChain.KeyChainConnection;
39import android.security.KeyStore;
40import android.util.Log;
41import android.util.SparseArray;
42import android.view.LayoutInflater;
43import android.view.View;
44import android.view.ViewGroup;
45import android.widget.AdapterView;
46import android.widget.AdapterView.OnItemClickListener;
47import android.widget.ArrayAdapter;
48import android.widget.ListView;
49import android.widget.TextView;
50
51import com.android.internal.logging.MetricsProto.MetricsEvent;
52import com.android.internal.widget.LockPatternUtils;
53import com.android.settingslib.RestrictedLockUtils;
54import com.android.settingslib.RestrictedLockUtils.EnforcedAdmin;
55
56import java.util.ArrayList;
57import java.util.EnumSet;
58import java.util.List;
59import java.util.SortedMap;
60import java.util.TreeMap;
61
62import static android.view.View.GONE;
63import static android.view.View.VISIBLE;
64
65public class UserCredentialsSettings extends OptionsMenuFragment implements OnItemClickListener {
66    private static final String TAG = "UserCredentialsSettings";
67
68    private ListView mListView;
69
70    @Override
71    protected int getMetricsCategory() {
72        return MetricsEvent.USER_CREDENTIALS;
73    }
74
75    @Override
76    public void onResume() {
77        super.onResume();
78        refreshItems();
79    }
80
81    @Override
82    public View onCreateView(
83            LayoutInflater inflater, ViewGroup parent, Bundle savedInstanceState) {
84        final View rootView = inflater.inflate(R.layout.user_credentials, parent, false);
85
86        // Set up an OnItemClickListener for the credential list.
87        mListView = (ListView) rootView.findViewById(R.id.credential_list);
88        mListView.setOnItemClickListener(this);
89
90        return rootView;
91    }
92
93    @Override
94    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
95        final Credential item = (Credential) parent.getItemAtPosition(position);
96        CredentialDialogFragment.show(this, item);
97    }
98
99    protected void announceRemoval(String alias) {
100        if (isAdded()) {
101            mListView.announceForAccessibility(getString(R.string.user_credential_removed, alias));
102        }
103    }
104
105    protected void refreshItems() {
106        if (isAdded()) {
107            new AliasLoader().execute();
108        }
109    }
110
111    public static class CredentialDialogFragment extends DialogFragment {
112        private static final String TAG = "CredentialDialogFragment";
113        private static final String ARG_CREDENTIAL = "credential";
114
115        public static void show(Fragment target, Credential item) {
116            final Bundle args = new Bundle();
117            args.putParcelable(ARG_CREDENTIAL, item);
118
119            if (target.getFragmentManager().findFragmentByTag(TAG) == null) {
120                final DialogFragment frag = new CredentialDialogFragment();
121                frag.setTargetFragment(target, /* requestCode */ -1);
122                frag.setArguments(args);
123                frag.show(target.getFragmentManager(), TAG);
124            }
125        }
126
127        @Override
128        public Dialog onCreateDialog(Bundle savedInstanceState) {
129            final Credential item = (Credential) getArguments().getParcelable(ARG_CREDENTIAL);
130
131            View root = getActivity().getLayoutInflater()
132                    .inflate(R.layout.user_credential_dialog, null);
133            ViewGroup infoContainer = (ViewGroup) root.findViewById(R.id.credential_container);
134            View contentView = getCredentialView(item, R.layout.user_credential, null,
135                    infoContainer, /* expanded */ true);
136            infoContainer.addView(contentView);
137
138            AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
139                    .setView(root)
140                    .setTitle(R.string.user_credential_title)
141                    .setPositiveButton(R.string.done, null);
142
143            final String restriction = UserManager.DISALLOW_CONFIG_CREDENTIALS;
144            final int myUserId = UserHandle.myUserId();
145            if (!RestrictedLockUtils.hasBaseUserRestriction(getContext(), restriction, myUserId)) {
146                DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
147                    @Override public void onClick(DialogInterface dialog, int id) {
148                        final EnforcedAdmin admin = RestrictedLockUtils.checkIfRestrictionEnforced(
149                                getContext(), restriction, myUserId);
150                        if (admin != null) {
151                            RestrictedLockUtils.sendShowAdminSupportDetailsIntent(getContext(),
152                                    admin);
153                        } else {
154                            new RemoveCredentialsTask(getContext(), getTargetFragment())
155                                    .execute(item);
156                        }
157                        dialog.dismiss();
158                    }
159                };
160                if (item.isSystem()) {
161                    // TODO: a safe means of clearing wifi certificates. Configs refer to aliases
162                    //       directly so deleting certs will break dependent access points.
163                    builder.setNegativeButton(R.string.trusted_credentials_remove_label, listener);
164                }
165            }
166            return builder.create();
167        }
168
169        /**
170         * Deletes all certificates and keys under a given alias.
171         *
172         * If the {@link Credential} is for a system alias, all active grants to the alias will be
173         * removed using {@link KeyChain}.
174         */
175        private class RemoveCredentialsTask extends AsyncTask<Credential, Void, Credential[]> {
176            private Context context;
177            private Fragment targetFragment;
178
179            public RemoveCredentialsTask(Context context, Fragment targetFragment) {
180                this.context = context;
181                this.targetFragment = targetFragment;
182            }
183
184            @Override
185            protected Credential[] doInBackground(Credential... credentials) {
186                for (final Credential credential : credentials) {
187                    if (credential.isSystem()) {
188                        removeGrantsAndDelete(credential);
189                        continue;
190                    }
191                    throw new UnsupportedOperationException(
192                            "Not implemented for wifi certificates. This should not be reachable.");
193                }
194                return credentials;
195            }
196
197            private void removeGrantsAndDelete(final Credential credential) {
198                final KeyChainConnection conn;
199                try {
200                    conn = KeyChain.bind(getContext());
201                } catch (InterruptedException e) {
202                    Log.w(TAG, "Connecting to KeyChain", e);
203                    return;
204                }
205
206                try {
207                    IKeyChainService keyChain = conn.getService();
208                    keyChain.removeKeyPair(credential.alias);
209                } catch (RemoteException e) {
210                    Log.w(TAG, "Removing credentials", e);
211                } finally {
212                    conn.close();
213                }
214            }
215
216            @Override
217            protected void onPostExecute(Credential... credentials) {
218                if (targetFragment instanceof UserCredentialsSettings && targetFragment.isAdded()) {
219                    final UserCredentialsSettings target = (UserCredentialsSettings) targetFragment;
220                    for (final Credential credential : credentials) {
221                        target.announceRemoval(credential.alias);
222                    }
223                    target.refreshItems();
224                }
225            }
226        }
227    }
228
229    /**
230     * Opens a background connection to KeyStore to list user credentials.
231     * The credentials are stored in a {@link CredentialAdapter} attached to the main
232     * {@link ListView} in the fragment.
233     */
234    private class AliasLoader extends AsyncTask<Void, Void, List<Credential>> {
235        /**
236         * @return a list of credentials ordered:
237         * <ol>
238         *   <li>first by purpose;</li>
239         *   <li>then by alias.</li>
240         * </ol>
241         */
242        @Override
243        protected List<Credential> doInBackground(Void... params) {
244            final KeyStore keyStore = KeyStore.getInstance();
245
246            // Certificates can be installed into SYSTEM_UID or WIFI_UID through CertInstaller.
247            final int myUserId = UserHandle.myUserId();
248            final int systemUid = UserHandle.getUid(myUserId, Process.SYSTEM_UID);
249            final int wifiUid = UserHandle.getUid(myUserId, Process.WIFI_UID);
250
251            List<Credential> credentials = new ArrayList<>();
252            credentials.addAll(getCredentialsForUid(keyStore, systemUid).values());
253            credentials.addAll(getCredentialsForUid(keyStore, wifiUid).values());
254            return credentials;
255        }
256
257        private SortedMap<String, Credential> getCredentialsForUid(KeyStore keyStore, int uid) {
258            final SortedMap<String, Credential> aliasMap = new TreeMap<>();
259            for (final Credential.Type type : Credential.Type.values()) {
260                for (final String alias : keyStore.list(type.prefix, uid)) {
261                    // Do not show work profile keys in user credentials
262                    if (alias.startsWith(LockPatternUtils.PROFILE_KEY_NAME_ENCRYPT) ||
263                            alias.startsWith(LockPatternUtils.PROFILE_KEY_NAME_DECRYPT)) {
264                        continue;
265                    }
266                    Credential c = aliasMap.get(alias);
267                    if (c == null) {
268                        c = new Credential(alias, uid);
269                        aliasMap.put(alias, c);
270                    }
271                    c.storedTypes.add(type);
272                }
273            }
274            return aliasMap;
275        }
276
277        @Override
278        protected void onPostExecute(List<Credential> credentials) {
279            final Credential[] credentialArray = credentials.toArray(new Credential[0]);
280            mListView.setAdapter(new CredentialAdapter(getContext(), credentialArray));
281        }
282    }
283
284    /**
285     * Helper class to display {@link Credential}s in a list.
286     */
287    private static class CredentialAdapter extends ArrayAdapter<Credential> {
288        private static final int LAYOUT_RESOURCE = R.layout.user_credential_preference;
289
290        public CredentialAdapter(Context context, final Credential[] objects) {
291            super(context, LAYOUT_RESOURCE, objects);
292        }
293
294        @Override
295        public View getView(int position, @Nullable View view, ViewGroup parent) {
296            return getCredentialView(getItem(position), LAYOUT_RESOURCE, view, parent,
297                    /* expanded */ false);
298        }
299    }
300
301    /**
302     * Mapping from View IDs in {@link R} to the types of credentials they describe.
303     */
304    private static final SparseArray<Credential.Type> credentialViewTypes = new SparseArray<>();
305    static {
306        credentialViewTypes.put(R.id.contents_userkey, Credential.Type.USER_PRIVATE_KEY);
307        credentialViewTypes.put(R.id.contents_usercrt, Credential.Type.USER_CERTIFICATE);
308        credentialViewTypes.put(R.id.contents_cacrt, Credential.Type.CA_CERTIFICATE);
309    }
310
311    protected static View getCredentialView(Credential item, @LayoutRes int layoutResource,
312            @Nullable View view, ViewGroup parent, boolean expanded) {
313        if (view == null) {
314            view = LayoutInflater.from(parent.getContext()).inflate(layoutResource, parent, false);
315        }
316
317        ((TextView) view.findViewById(R.id.alias)).setText(item.alias);
318        ((TextView) view.findViewById(R.id.purpose)).setText(item.isSystem()
319                ? R.string.credential_for_vpn_and_apps
320                : R.string.credential_for_wifi);
321
322        view.findViewById(R.id.contents).setVisibility(expanded ? View.VISIBLE : View.GONE);
323        if (expanded) {
324            for (int i = 0; i < credentialViewTypes.size(); i++) {
325                final View detail = view.findViewById(credentialViewTypes.keyAt(i));
326                detail.setVisibility(item.storedTypes.contains(credentialViewTypes.valueAt(i))
327                        ? View.VISIBLE : View.GONE);
328            }
329        }
330        return view;
331    }
332
333    static class AliasEntry {
334        public String alias;
335        public int uid;
336    }
337
338    static class Credential implements Parcelable {
339        static enum Type {
340            CA_CERTIFICATE (Credentials.CA_CERTIFICATE),
341            USER_CERTIFICATE (Credentials.USER_CERTIFICATE),
342            USER_PRIVATE_KEY (Credentials.USER_PRIVATE_KEY),
343            USER_SECRET_KEY (Credentials.USER_SECRET_KEY);
344
345            final String prefix;
346
347            Type(String prefix) {
348                this.prefix = prefix;
349            }
350        }
351
352        /**
353         * Main part of the credential's alias. To fetch an item from KeyStore, prepend one of the
354         * prefixes from {@link CredentialItem.storedTypes}.
355         */
356        final String alias;
357
358        /**
359         * UID under which this credential is stored. Typically {@link Process#SYSTEM_UID} but can
360         * also be {@link Process#WIFI_UID} for credentials installed as wifi certificates.
361         */
362        final int uid;
363
364        /**
365         * Should contain some non-empty subset of:
366         * <ul>
367         *   <li>{@link Credentials.CA_CERTIFICATE}</li>
368         *   <li>{@link Credentials.USER_CERTIFICATE}</li>
369         *   <li>{@link Credentials.USER_PRIVATE_KEY}</li>
370         *   <li>{@link Credentials.USER_SECRET_KEY}</li>
371         * </ul>
372         */
373        final EnumSet<Type> storedTypes = EnumSet.noneOf(Type.class);
374
375        Credential(final String alias, final int uid) {
376            this.alias = alias;
377            this.uid = uid;
378        }
379
380        Credential(Parcel in) {
381            this(in.readString(), in.readInt());
382
383            long typeBits = in.readLong();
384            for (Type i : Type.values()) {
385                if ((typeBits & (1L << i.ordinal())) != 0L) {
386                    storedTypes.add(i);
387                }
388            }
389        }
390
391        public void writeToParcel(Parcel out, int flags) {
392            out.writeString(alias);
393            out.writeInt(uid);
394
395            long typeBits = 0;
396            for (Type i : storedTypes) {
397                typeBits |= 1L << i.ordinal();
398            }
399            out.writeLong(typeBits);
400        }
401
402        public int describeContents() {
403            return 0;
404        }
405
406        public static final Parcelable.Creator<Credential> CREATOR
407                = new Parcelable.Creator<Credential>() {
408            public Credential createFromParcel(Parcel in) {
409                return new Credential(in);
410            }
411
412            public Credential[] newArray(int size) {
413                return new Credential[size];
414            }
415        };
416
417        public boolean isSystem() {
418            return UserHandle.getAppId(uid) == Process.SYSTEM_UID;
419        }
420    }
421}
422