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