1/*
2 * Copyright (C) 2009 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.certinstaller;
18
19import android.app.Activity;
20import android.app.AlertDialog;
21import android.app.Dialog;
22import android.app.ProgressDialog;
23import android.content.ActivityNotFoundException;
24import android.content.DialogInterface;
25import android.content.Intent;
26import android.os.AsyncTask;
27import android.os.Bundle;
28import android.os.Process;
29import android.security.Credentials;
30import android.security.KeyChain;
31import android.security.KeyChain.KeyChainConnection;
32import android.security.KeyStore;
33import android.text.TextUtils;
34import android.util.Log;
35import android.view.View;
36import android.view.ViewGroup;
37import android.widget.AdapterView;
38import android.widget.AdapterView.OnItemSelectedListener;
39import android.widget.EditText;
40import android.widget.Spinner;
41import android.widget.Toast;
42
43import java.io.Serializable;
44import java.security.cert.X509Certificate;
45import java.util.LinkedHashMap;
46import java.util.Map;
47
48/**
49 * Installs certificates to the system keystore.
50 */
51public class CertInstaller extends Activity {
52    private static final String TAG = "CertInstaller";
53
54    private static final int STATE_INIT = 1;
55    private static final int STATE_RUNNING = 2;
56    private static final int STATE_PAUSED = 3;
57
58    private static final int NAME_CREDENTIAL_DIALOG = 1;
59    private static final int PKCS12_PASSWORD_DIALOG = 2;
60    private static final int PROGRESS_BAR_DIALOG = 3;
61
62    private static final int REQUEST_SYSTEM_INSTALL_CODE = 1;
63
64    // key to states Bundle
65    private static final String NEXT_ACTION_KEY = "na";
66
67    // key to KeyStore
68    private static final String PKEY_MAP_KEY = "PKEY_MAP";
69
70    // Values for usage type spinner
71    private static final int USAGE_TYPE_SYSTEM = 0;
72    private static final int USAGE_TYPE_WIFI = 1;
73
74    private final KeyStore mKeyStore = KeyStore.getInstance();
75    private final ViewHelper mView = new ViewHelper();
76
77    private int mState;
78    private CredentialHelper mCredentials;
79    private MyAction mNextAction;
80
81    private CredentialHelper createCredentialHelper(Intent intent) {
82        try {
83            return new CredentialHelper(intent);
84        } catch (Throwable t) {
85            Log.w(TAG, "createCredentialHelper", t);
86            toastErrorAndFinish(R.string.invalid_cert);
87            return new CredentialHelper();
88        }
89    }
90
91    @Override
92    protected void onCreate(Bundle savedStates) {
93        super.onCreate(savedStates);
94
95        mCredentials = createCredentialHelper(getIntent());
96
97        mState = (savedStates == null) ? STATE_INIT : STATE_RUNNING;
98
99        if (mState == STATE_INIT) {
100            if (!mCredentials.containsAnyRawData()) {
101                toastErrorAndFinish(R.string.no_cert_to_saved);
102                finish();
103            } else if (mCredentials.hasPkcs12KeyStore()) {
104                showDialog(PKCS12_PASSWORD_DIALOG);
105            } else {
106                MyAction action = new InstallOthersAction();
107                if (needsKeyStoreAccess()) {
108                    sendUnlockKeyStoreIntent();
109                    mNextAction = action;
110                } else {
111                    action.run(this);
112                }
113            }
114        } else {
115            mCredentials.onRestoreStates(savedStates);
116            mNextAction = (MyAction)
117                    savedStates.getSerializable(NEXT_ACTION_KEY);
118        }
119    }
120
121    @Override
122    protected void onResume() {
123        super.onResume();
124
125        if (mState == STATE_INIT) {
126            mState = STATE_RUNNING;
127        } else {
128            if (mNextAction != null) {
129                mNextAction.run(this);
130            }
131        }
132    }
133
134    private boolean needsKeyStoreAccess() {
135        return ((mCredentials.hasKeyPair() || mCredentials.hasUserCertificate())
136                && !mKeyStore.isUnlocked());
137    }
138
139    @Override
140    protected void onPause() {
141        super.onPause();
142        mState = STATE_PAUSED;
143    }
144
145    @Override
146    protected void onSaveInstanceState(Bundle outStates) {
147        super.onSaveInstanceState(outStates);
148        mCredentials.onSaveStates(outStates);
149        if (mNextAction != null) {
150            outStates.putSerializable(NEXT_ACTION_KEY, mNextAction);
151        }
152    }
153
154    @Override
155    protected Dialog onCreateDialog (int dialogId) {
156        switch (dialogId) {
157            case PKCS12_PASSWORD_DIALOG:
158                return createPkcs12PasswordDialog();
159
160            case NAME_CREDENTIAL_DIALOG:
161                return createNameCredentialDialog();
162
163            case PROGRESS_BAR_DIALOG:
164                ProgressDialog dialog = new ProgressDialog(this);
165                dialog.setMessage(getString(R.string.extracting_pkcs12));
166                dialog.setIndeterminate(true);
167                dialog.setCancelable(false);
168                return dialog;
169
170            default:
171                return null;
172        }
173    }
174
175    @Override
176    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
177        if (requestCode == REQUEST_SYSTEM_INSTALL_CODE) {
178            if (resultCode == RESULT_OK) {
179                Log.d(TAG, "credential is added: " + mCredentials.getName());
180                Toast.makeText(this, getString(R.string.cert_is_added,
181                        mCredentials.getName()), Toast.LENGTH_LONG).show();
182
183                if (mCredentials.hasCaCerts()) {
184                    // more work to do, don't finish just yet
185                    new InstallCaCertsToKeyChainTask().execute();
186                    return;
187                }
188                setResult(RESULT_OK);
189            } else {
190                Log.d(TAG, "credential not saved, err: " + resultCode);
191                toastErrorAndFinish(R.string.cert_not_saved);
192            }
193        } else {
194            Log.w(TAG, "unknown request code: " + requestCode);
195        }
196        finish();
197    }
198
199    private class InstallCaCertsToKeyChainTask extends AsyncTask<Void, Void, Boolean> {
200
201        @Override protected Boolean doInBackground(Void... unused) {
202            try {
203                KeyChainConnection keyChainConnection = KeyChain.bind(CertInstaller.this);
204                try {
205                    return mCredentials.installCaCertsToKeyChain(keyChainConnection.getService());
206                } finally {
207                    keyChainConnection.close();
208                }
209            } catch (InterruptedException e) {
210                Thread.currentThread().interrupt();
211                return false;
212            }
213        }
214
215        @Override protected void onPostExecute(Boolean success) {
216            if (success) {
217                setResult(RESULT_OK);
218            }
219            finish();
220        }
221    }
222
223    void installOthers() {
224        if (mCredentials.hasKeyPair()) {
225            saveKeyPair();
226            finish();
227        } else {
228            X509Certificate cert = mCredentials.getUserCertificate();
229            if (cert != null) {
230                // find matched private key
231                String key = Util.toMd5(cert.getPublicKey().getEncoded());
232                Map<String, byte[]> map = getPkeyMap();
233                byte[] privatekey = map.get(key);
234                if (privatekey != null) {
235                    Log.d(TAG, "found matched key: " + privatekey);
236                    map.remove(key);
237                    savePkeyMap(map);
238
239                    mCredentials.setPrivateKey(privatekey);
240                } else {
241                    Log.d(TAG, "didn't find matched private key: " + key);
242                }
243            }
244            nameCredential();
245        }
246    }
247
248    private void sendUnlockKeyStoreIntent() {
249        Credentials.getInstance().unlock(this);
250    }
251
252    private void nameCredential() {
253        if (!mCredentials.hasAnyForSystemInstall()) {
254            toastErrorAndFinish(R.string.no_cert_to_saved);
255        } else {
256            showDialog(NAME_CREDENTIAL_DIALOG);
257        }
258    }
259
260    private void saveKeyPair() {
261        byte[] privatekey = mCredentials.getData(Credentials.EXTRA_PRIVATE_KEY);
262        String key = Util.toMd5(mCredentials.getData(Credentials.EXTRA_PUBLIC_KEY));
263        Map<String, byte[]> map = getPkeyMap();
264        map.put(key, privatekey);
265        savePkeyMap(map);
266        Log.d(TAG, "save privatekey: " + key + " --> #keys:" + map.size());
267    }
268
269    private void savePkeyMap(Map<String, byte[]> map) {
270        if (map.isEmpty()) {
271            if (!mKeyStore.delete(PKEY_MAP_KEY)) {
272                Log.w(TAG, "savePkeyMap(): failed to delete pkey map");
273            }
274            return;
275        }
276        byte[] bytes = Util.toBytes(map);
277        if (!mKeyStore.put(PKEY_MAP_KEY, bytes, KeyStore.UID_SELF, KeyStore.FLAG_ENCRYPTED)) {
278            Log.w(TAG, "savePkeyMap(): failed to write pkey map");
279        }
280    }
281
282    private Map<String, byte[]> getPkeyMap() {
283        byte[] bytes = mKeyStore.get(PKEY_MAP_KEY);
284        if (bytes != null) {
285            Map<String, byte[]> map =
286                    (Map<String, byte[]>) Util.fromBytes(bytes);
287            if (map != null) return map;
288        }
289        return new MyMap();
290    }
291
292    void extractPkcs12InBackground(final String password) {
293        // show progress bar and extract certs in a background thread
294        showDialog(PROGRESS_BAR_DIALOG);
295
296        new AsyncTask<Void,Void,Boolean>() {
297            @Override protected Boolean doInBackground(Void... unused) {
298                return mCredentials.extractPkcs12(password);
299            }
300            @Override protected void onPostExecute(Boolean success) {
301                MyAction action = new OnExtractionDoneAction(success);
302                if (mState == STATE_PAUSED) {
303                    // activity is paused; run it in next onResume()
304                    mNextAction = action;
305                } else {
306                    action.run(CertInstaller.this);
307                }
308            }
309        }.execute();
310    }
311
312    void onExtractionDone(boolean success) {
313        mNextAction = null;
314        removeDialog(PROGRESS_BAR_DIALOG);
315        if (success) {
316            removeDialog(PKCS12_PASSWORD_DIALOG);
317            nameCredential();
318        } else {
319            mView.setText(R.id.credential_password, "");
320            mView.showError(R.string.password_error);
321            showDialog(PKCS12_PASSWORD_DIALOG);
322        }
323    }
324
325    private Dialog createPkcs12PasswordDialog() {
326        View view = View.inflate(this, R.layout.password_dialog, null);
327        mView.setView(view);
328        if (mView.getHasEmptyError()) {
329            mView.showError(R.string.password_empty_error);
330            mView.setHasEmptyError(false);
331        }
332
333        String title = mCredentials.getName();
334        title = TextUtils.isEmpty(title)
335                ? getString(R.string.pkcs12_password_dialog_title)
336                : getString(R.string.pkcs12_file_password_dialog_title, title);
337        Dialog d = new AlertDialog.Builder(this)
338                .setView(view)
339                .setTitle(title)
340                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
341                    public void onClick(DialogInterface dialog, int id) {
342                        String password = mView.getText(R.id.credential_password);
343                        mNextAction = new Pkcs12ExtractAction(password);
344                        mNextAction.run(CertInstaller.this);
345                     }
346                })
347                .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
348                    public void onClick(DialogInterface dialog, int id) {
349                        toastErrorAndFinish(R.string.cert_not_saved);
350                    }
351                })
352                .create();
353        d.setOnCancelListener(new DialogInterface.OnCancelListener() {
354            @Override public void onCancel(DialogInterface dialog) {
355                toastErrorAndFinish(R.string.cert_not_saved);
356            }
357        });
358        return d;
359    }
360
361    private Dialog createNameCredentialDialog() {
362        ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_credential_dialog, null);
363        mView.setView(view);
364        if (mView.getHasEmptyError()) {
365            mView.showError(R.string.name_empty_error);
366            mView.setHasEmptyError(false);
367        }
368        mView.setText(R.id.credential_info, mCredentials.getDescription(this).toString());
369        final EditText nameInput = (EditText) view.findViewById(R.id.credential_name);
370        if (mCredentials.isInstallAsUidSet()) {
371            view.findViewById(R.id.credential_usage_group).setVisibility(View.GONE);
372        } else {
373            final Spinner usageSpinner = (Spinner) view.findViewById(R.id.credential_usage);
374
375            usageSpinner.setOnItemSelectedListener(new OnItemSelectedListener() {
376                @Override
377                public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
378                    switch ((int) id) {
379                        case USAGE_TYPE_SYSTEM:
380                            mCredentials.setInstallAsUid(KeyStore.UID_SELF);
381                            break;
382                        case USAGE_TYPE_WIFI:
383                            mCredentials.setInstallAsUid(Process.WIFI_UID);
384                            break;
385                        default:
386                            Log.w(TAG, "Unknown selection for scope: " + id);
387                    }
388                }
389
390                @Override
391                public void onNothingSelected(AdapterView<?> parent) {
392                }
393            });
394        }
395        nameInput.setText(getDefaultName());
396        nameInput.selectAll();
397        Dialog d = new AlertDialog.Builder(this)
398                .setView(view)
399                .setTitle(R.string.name_credential_dialog_title)
400                .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
401                    public void onClick(DialogInterface dialog, int id) {
402                        String name = mView.getText(R.id.credential_name);
403                        if (TextUtils.isEmpty(name)) {
404                            mView.setHasEmptyError(true);
405                            removeDialog(NAME_CREDENTIAL_DIALOG);
406                            showDialog(NAME_CREDENTIAL_DIALOG);
407                        } else {
408                            removeDialog(NAME_CREDENTIAL_DIALOG);
409                            mCredentials.setName(name);
410
411                            // install everything to system keystore
412                            try {
413                                startActivityForResult(
414                                        mCredentials.createSystemInstallIntent(),
415                                        REQUEST_SYSTEM_INSTALL_CODE);
416                            } catch (ActivityNotFoundException e) {
417                                Log.w(TAG, "systemInstall(): " + e);
418                                toastErrorAndFinish(R.string.cert_not_saved);
419                            }
420                        }
421                    }
422                })
423                .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
424                    public void onClick(DialogInterface dialog, int id) {
425                        toastErrorAndFinish(R.string.cert_not_saved);
426                    }
427                })
428                .create();
429        d.setOnCancelListener(new DialogInterface.OnCancelListener() {
430            @Override public void onCancel(DialogInterface dialog) {
431                toastErrorAndFinish(R.string.cert_not_saved);
432            }
433        });
434        return d;
435    }
436
437    private String getDefaultName() {
438        String name = mCredentials.getName();
439        if (TextUtils.isEmpty(name)) {
440            return null;
441        } else {
442            // remove the extension from the file name
443            int index = name.lastIndexOf(".");
444            if (index > 0) name = name.substring(0, index);
445            return name;
446        }
447    }
448
449    private void toastErrorAndFinish(int msgId) {
450        Toast.makeText(this, msgId, Toast.LENGTH_SHORT).show();
451        finish();
452    }
453
454    private static class MyMap extends LinkedHashMap<String, byte[]>
455            implements Serializable {
456        private static final long serialVersionUID = 1L;
457
458        @Override
459        protected boolean removeEldestEntry(Map.Entry eldest) {
460            // Note: one key takes about 1300 bytes in the keystore, so be
461            // cautious about allowing more outstanding keys in the map that
462            // may go beyond keystore's max length for one entry.
463            return (size() > 3);
464        }
465    }
466
467    private interface MyAction extends Serializable {
468        void run(CertInstaller host);
469    }
470
471    private static class Pkcs12ExtractAction implements MyAction {
472        private final String mPassword;
473        private transient boolean hasRun;
474
475        Pkcs12ExtractAction(String password) {
476            mPassword = password;
477        }
478
479        public void run(CertInstaller host) {
480            if (hasRun) {
481                return;
482            }
483            hasRun = true;
484            host.extractPkcs12InBackground(mPassword);
485        }
486    }
487
488    private static class InstallOthersAction implements MyAction {
489        public void run(CertInstaller host) {
490            host.mNextAction = null;
491            host.installOthers();
492        }
493    }
494
495    private static class OnExtractionDoneAction implements MyAction {
496        private final boolean mSuccess;
497
498        OnExtractionDoneAction(boolean success) {
499            mSuccess = success;
500        }
501
502        public void run(CertInstaller host) {
503            host.onExtractionDone(mSuccess);
504        }
505    }
506}
507