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