• 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                     // 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 
doInBackground(Void... unused)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 
onPostExecute(Boolean success)215         @Override protected void onPostExecute(Boolean success) {
216             if (success) {
217                 setResult(RESULT_OK);
218             }
219             finish();
220         }
221     }
222 
installOthers()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 
sendUnlockKeyStoreIntent()248     private void sendUnlockKeyStoreIntent() {
249         Credentials.getInstance().unlock(this);
250     }
251 
nameCredential()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 
saveKeyPair()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 
savePkeyMap(Map<String, byte[]> map)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 
getPkeyMap()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 
extractPkcs12InBackground(final String password)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 
onExtractionDone(boolean success)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 
createPkcs12PasswordDialog()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 
createNameCredentialDialog()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 
getDefaultName()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 
toastErrorAndFinish(int msgId)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
removeEldestEntry(Map.Entry eldest)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 {
run(CertInstaller host)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 
Pkcs12ExtractAction(String password)475         Pkcs12ExtractAction(String password) {
476             mPassword = password;
477         }
478 
run(CertInstaller host)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 {
run(CertInstaller host)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 
OnExtractionDoneAction(boolean success)498         OnExtractionDoneAction(boolean success) {
499             mSuccess = success;
500         }
501 
run(CertInstaller host)502         public void run(CertInstaller host) {
503             host.onExtractionDone(mSuccess);
504         }
505     }
506 }
507