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