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