• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
17 package com.android.certinstaller;
18 
19 import android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.Dialog;
22 import android.app.ProgressDialog;
23 import android.content.ActivityNotFoundException;
24 import android.content.DialogInterface;
25 import android.content.Intent;
26 import android.os.AsyncTask;
27 import android.os.Bundle;
28 import android.security.Credentials;
29 import android.security.KeyChain;
30 import android.security.KeyChain.KeyChainConnection;
31 import android.security.KeyStore;
32 import android.text.TextUtils;
33 import android.util.Log;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.widget.EditText;
37 import android.widget.Toast;
38 
39 import java.io.Serializable;
40 import java.security.cert.X509Certificate;
41 import java.util.LinkedHashMap;
42 import java.util.Map;
43 
44 /**
45  * Installs certificates to the system keystore.
46  */
47 public 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 
createCredentialHelper(Intent intent)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
onCreate(Bundle savedStates)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
onResume()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 
needsKeyStoreAccess()126     private boolean needsKeyStoreAccess() {
127         return ((mCredentials.hasKeyPair() || mCredentials.hasUserCertificate())
128                 && (mKeyStore.state() != KeyStore.State.UNLOCKED));
129     }
130 
131     @Override
onPause()132     protected void onPause() {
133         super.onPause();
134         mState = STATE_PAUSED;
135     }
136 
137     @Override
onSaveInstanceState(Bundle outStates)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
onCreateDialog(int dialogId)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
onActivityResult(int requestCode, int resultCode, Intent data)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 
doInBackground(Void... unused)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 
onPostExecute(Boolean success)207         @Override protected void onPostExecute(Boolean success) {
208             if (success) {
209                 setResult(RESULT_OK);
210             }
211             finish();
212         }
213     }
214 
installOthers()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 
sendUnlockKeyStoreIntent()240     private void sendUnlockKeyStoreIntent() {
241         Credentials.getInstance().unlock(this);
242     }
243 
nameCredential()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 
saveKeyPair()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 
savePkeyMap(Map<String, byte[]> map)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 
getPkeyMap()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 
extractPkcs12InBackground(final String password)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 
onExtractionDone(boolean success)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 
createPkcs12PasswordDialog()317     private Dialog createPkcs12PasswordDialog() {
318         View view = View.inflate(this, R.layout.password_dialog, null);
319         mView.setView(view);
320         if (mView.getHasEmptyError()) {
321             mView.showError(R.string.password_empty_error);
322             mView.setHasEmptyError(false);
323         }
324 
325         String title = mCredentials.getName();
326         title = TextUtils.isEmpty(title)
327                 ? getString(R.string.pkcs12_password_dialog_title)
328                 : getString(R.string.pkcs12_file_password_dialog_title, title);
329         Dialog d = new AlertDialog.Builder(this)
330                 .setView(view)
331                 .setTitle(title)
332                 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
333                     public void onClick(DialogInterface dialog, int id) {
334                         String password = mView.getText(R.id.credential_password);
335                         mNextAction = new Pkcs12ExtractAction(password);
336                         mNextAction.run(CertInstaller.this);
337                      }
338                 })
339                 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
340                     public void onClick(DialogInterface dialog, int id) {
341                         toastErrorAndFinish(R.string.cert_not_saved);
342                     }
343                 })
344                 .create();
345         d.setOnCancelListener(new DialogInterface.OnCancelListener() {
346             @Override public void onCancel(DialogInterface dialog) {
347                 toastErrorAndFinish(R.string.cert_not_saved);
348             }
349         });
350         return d;
351     }
352 
createNameCredentialDialog()353     private Dialog createNameCredentialDialog() {
354         ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_credential_dialog, null);
355         mView.setView(view);
356         if (mView.getHasEmptyError()) {
357             mView.showError(R.string.name_empty_error);
358             mView.setHasEmptyError(false);
359         }
360         mView.setText(R.id.credential_info, mCredentials.getDescription(this).toString());
361         final EditText nameInput = (EditText) view.findViewById(R.id.credential_name);
362         nameInput.setText(getDefaultName());
363         nameInput.selectAll();
364         Dialog d = new AlertDialog.Builder(this)
365                 .setView(view)
366                 .setTitle(R.string.name_credential_dialog_title)
367                 .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
368                     public void onClick(DialogInterface dialog, int id) {
369                         String name = mView.getText(R.id.credential_name);
370                         if (TextUtils.isEmpty(name)) {
371                             mView.setHasEmptyError(true);
372                             removeDialog(NAME_CREDENTIAL_DIALOG);
373                             showDialog(NAME_CREDENTIAL_DIALOG);
374                         } else {
375                             removeDialog(NAME_CREDENTIAL_DIALOG);
376                             mCredentials.setName(name);
377 
378                             // install everything to system keystore
379                             try {
380                                 startActivityForResult(
381                                         mCredentials.createSystemInstallIntent(),
382                                         REQUEST_SYSTEM_INSTALL_CODE);
383                             } catch (ActivityNotFoundException e) {
384                                 Log.w(TAG, "systemInstall(): " + e);
385                                 toastErrorAndFinish(R.string.cert_not_saved);
386                             }
387                         }
388                     }
389                 })
390                 .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
391                     public void onClick(DialogInterface dialog, int id) {
392                         toastErrorAndFinish(R.string.cert_not_saved);
393                     }
394                 })
395                 .create();
396         d.setOnCancelListener(new DialogInterface.OnCancelListener() {
397             @Override public void onCancel(DialogInterface dialog) {
398                 toastErrorAndFinish(R.string.cert_not_saved);
399             }
400         });
401         return d;
402     }
403 
getDefaultName()404     private String getDefaultName() {
405         String name = mCredentials.getName();
406         if (TextUtils.isEmpty(name)) {
407             return null;
408         } else {
409             // remove the extension from the file name
410             int index = name.lastIndexOf(".");
411             if (index > 0) name = name.substring(0, index);
412             return name;
413         }
414     }
415 
toastErrorAndFinish(int msgId)416     private void toastErrorAndFinish(int msgId) {
417         Toast.makeText(this, msgId, Toast.LENGTH_SHORT).show();
418         finish();
419     }
420 
421     private static class MyMap extends LinkedHashMap<String, byte[]>
422             implements Serializable {
423         private static final long serialVersionUID = 1L;
424 
425         @Override
removeEldestEntry(Map.Entry eldest)426         protected boolean removeEldestEntry(Map.Entry eldest) {
427             // Note: one key takes about 1300 bytes in the keystore, so be
428             // cautious about allowing more outstanding keys in the map that
429             // may go beyond keystore's max length for one entry.
430             return (size() > 3);
431         }
432     }
433 
434     private interface MyAction extends Serializable {
run(CertInstaller host)435         void run(CertInstaller host);
436     }
437 
438     private static class Pkcs12ExtractAction implements MyAction {
439         private final String mPassword;
440         private transient boolean hasRun;
441 
Pkcs12ExtractAction(String password)442         Pkcs12ExtractAction(String password) {
443             mPassword = password;
444         }
445 
run(CertInstaller host)446         public void run(CertInstaller host) {
447             if (hasRun) {
448                 return;
449             }
450             hasRun = true;
451             host.extractPkcs12InBackground(mPassword);
452         }
453     }
454 
455     private static class InstallOthersAction implements MyAction {
run(CertInstaller host)456         public void run(CertInstaller host) {
457             host.mNextAction = null;
458             host.installOthers();
459         }
460     }
461 
462     private static class OnExtractionDoneAction implements MyAction {
463         private final boolean mSuccess;
464 
OnExtractionDoneAction(boolean success)465         OnExtractionDoneAction(boolean success) {
466             mSuccess = success;
467         }
468 
run(CertInstaller host)469         public void run(CertInstaller host) {
470             host.onExtractionDone(mSuccess);
471         }
472     }
473 }
474