• 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.SYSTEM_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.ProgressDialog;
25 import android.content.ActivityNotFoundException;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.PackageManager;
30 import android.os.AsyncTask;
31 import android.os.Bundle;
32 import android.security.Credentials;
33 import android.security.KeyChain;
34 import android.security.KeyChain.KeyChainConnection;
35 import android.text.TextUtils;
36 import android.util.Log;
37 import android.util.Slog;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.EditText;
41 import android.widget.RadioGroup;
42 import android.widget.Toast;
43 
44 import java.io.Serializable;
45 
46 /**
47  * Installs certificates to the system keystore.
48  */
49 public class CertInstaller extends Activity {
50     private static final String TAG = "CertInstaller";
51 
52     private static final int STATE_INIT = 1;
53     private static final int STATE_RUNNING = 2;
54     private static final int STATE_PAUSED = 3;
55 
56     private static final int NAME_CREDENTIAL_DIALOG = 1;
57     private static final int PKCS12_PASSWORD_DIALOG = 2;
58     private static final int PROGRESS_BAR_DIALOG = 3;
59     private static final int REDIRECT_CA_CERTIFICATE_DIALOG = 4;
60     private static final int SELECT_CERTIFICATE_USAGE_DIALOG = 5;
61     private static final int INVALID_CERTIFICATE_DIALOG = 6;
62 
63     private static final int REQUEST_SYSTEM_INSTALL_CODE = 1;
64 
65     // key to states Bundle
66     private static final String NEXT_ACTION_KEY = "na";
67 
68     private final ViewHelper mView = new ViewHelper();
69 
70     private int mState;
71     private CredentialHelper mCredentials;
72     private MyAction mNextAction;
73 
createCredentialHelper(Intent intent)74     private CredentialHelper createCredentialHelper(Intent intent) {
75         try {
76             return new CredentialHelper(intent);
77         } catch (Throwable t) {
78             Log.w(TAG, "createCredentialHelper", t);
79             toastErrorAndFinish(R.string.invalid_cert);
80             return new CredentialHelper();
81         }
82     }
83 
84     @Override
onCreate(Bundle savedStates)85     protected void onCreate(Bundle savedStates) {
86         super.onCreate(savedStates);
87         getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
88 
89         mCredentials = createCredentialHelper(getIntent());
90 
91         mState = (savedStates == null) ? STATE_INIT : STATE_RUNNING;
92 
93         if (mState == STATE_INIT) {
94             if (!mCredentials.containsAnyRawData()) {
95                 toastErrorAndFinish(R.string.no_cert_to_saved);
96                 finish();
97             } else {
98                 if (installingCaCertificate()) {
99                     extractPkcs12OrInstall();
100                 } else {
101                     if (mCredentials.hasUserCertificate() && !mCredentials.hasPrivateKey()) {
102                         toastErrorAndFinish(R.string.action_missing_private_key);
103                     } else if (mCredentials.hasPrivateKey() && !mCredentials.hasUserCertificate()) {
104                         toastErrorAndFinish(R.string.action_missing_user_cert);
105                     } else {
106                         extractPkcs12OrInstall();
107                     }
108                 }
109             }
110         } else {
111             mCredentials.onRestoreStates(savedStates);
112             mNextAction = (MyAction)
113                     savedStates.getSerializable(NEXT_ACTION_KEY);
114         }
115     }
116 
installingCaCertificate()117     private boolean installingCaCertificate() {
118         return mCredentials.hasCaCerts() && !mCredentials.hasPrivateKey() &&
119                 !mCredentials.hasUserCertificate();
120     }
121 
122     @Override
onResume()123     protected void onResume() {
124         super.onResume();
125 
126         if (mState == STATE_INIT) {
127             mState = STATE_RUNNING;
128         } else {
129             if (mNextAction != null) {
130                 mNextAction.run(this);
131             }
132         }
133     }
134 
135     @Override
onPause()136     protected void onPause() {
137         super.onPause();
138         mState = STATE_PAUSED;
139     }
140 
141     @Override
onSaveInstanceState(Bundle outStates)142     protected void onSaveInstanceState(Bundle outStates) {
143         super.onSaveInstanceState(outStates);
144         mCredentials.onSaveStates(outStates);
145         if (mNextAction != null) {
146             outStates.putSerializable(NEXT_ACTION_KEY, mNextAction);
147         }
148     }
149 
150     @Override
onCreateDialog(int dialogId)151     protected Dialog onCreateDialog (int dialogId) {
152         switch (dialogId) {
153             case PKCS12_PASSWORD_DIALOG:
154                 return createPkcs12PasswordDialog();
155 
156             case NAME_CREDENTIAL_DIALOG:
157                 return createNameCertificateDialog();
158 
159             case PROGRESS_BAR_DIALOG:
160                 ProgressDialog dialog = new ProgressDialog(this);
161                 dialog.setMessage(getString(R.string.extracting_pkcs12));
162                 dialog.setIndeterminate(true);
163                 dialog.setCancelable(false);
164                 return dialog;
165 
166             case REDIRECT_CA_CERTIFICATE_DIALOG:
167                 return createRedirectCaCertificateDialog();
168 
169             case SELECT_CERTIFICATE_USAGE_DIALOG:
170                 return createSelectCertificateUsageDialog();
171 
172             case INVALID_CERTIFICATE_DIALOG:
173                 return createInvalidCertificateDialog();
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         switch (requestCode) {
183             case REQUEST_SYSTEM_INSTALL_CODE:
184                 if (resultCode != RESULT_OK) {
185                     Log.d(TAG, "credential not saved, err: " + resultCode);
186                     toastErrorAndFinish(R.string.cert_not_saved);
187                     return;
188                 }
189 
190                 Log.d(TAG, "credential is added: " + mCredentials.getName());
191                 if (mCredentials.getCertUsageSelected().equals(Credentials.CERTIFICATE_USAGE_WIFI)) {
192                     Toast.makeText(this, R.string.wifi_cert_is_added, Toast.LENGTH_LONG).show();
193                 } else {
194                     Toast.makeText(this, R.string.user_cert_is_added, Toast.LENGTH_LONG).show();
195                 }
196                 setResult(RESULT_OK);
197                 finish();
198                 break;
199             default:
200                 Log.w(TAG, "unknown request code: " + requestCode);
201                 finish();
202                 break;
203         }
204     }
205 
extractPkcs12OrInstall()206     private void extractPkcs12OrInstall() {
207         if (mCredentials.hasPkcs12KeyStore()) {
208             if (mCredentials.hasPassword()) {
209                 showDialog(PKCS12_PASSWORD_DIALOG);
210             } else {
211                 new Pkcs12ExtractAction("").run(this);
212             }
213         } else {
214             if (mCredentials.calledBySettings()) {
215                 MyAction action = new InstallOthersAction();
216                 action.run(this);
217             } else {
218                 createRedirectOrSelectUsageDialog();
219             }
220         }
221     }
222 
223     private class InstallVpnAndAppsTrustAnchorsTask extends AsyncTask<Void, Void, Boolean> {
224 
225         @Override
doInBackground(Void... unused)226         protected Boolean doInBackground(Void... unused) {
227             try {
228                 try (KeyChainConnection keyChainConnection = KeyChain.bind(CertInstaller.this)) {
229                     return mCredentials.installVpnAndAppsTrustAnchors(CertInstaller.this,
230                             keyChainConnection.getService());
231                 }
232             } catch (InterruptedException e) {
233                 Thread.currentThread().interrupt();
234                 return false;
235             }
236         }
237 
238         @Override
onPostExecute(Boolean success)239         protected void onPostExecute(Boolean success) {
240             if (success) {
241                 Toast.makeText(getApplicationContext(), R.string.ca_cert_is_added,
242                         Toast.LENGTH_LONG).show();
243                 setResult(RESULT_OK);
244             }
245             finish();
246         }
247     }
248 
installOthers()249     private void installOthers() {
250         // Sanity check: Check that there's either:
251         // * A private key AND a user certificate, or
252         // * A CA cert.
253         boolean hasPrivateKeyAndUserCertificate =
254                 mCredentials.hasPrivateKey() && mCredentials.hasUserCertificate();
255         boolean hasCaCertificate = mCredentials.hasCaCerts();
256         Log.d(TAG,
257                 String.format(
258                         "Attempting credentials installation, has ca cert? %b, has user cert? %b",
259                         hasCaCertificate, hasPrivateKeyAndUserCertificate));
260         if (!(hasPrivateKeyAndUserCertificate || hasCaCertificate)) {
261             finish();
262             return;
263         }
264 
265         if (validCertificateSelected()) {
266             installCertificateOrShowNameDialog();
267         } else {
268             showDialog(INVALID_CERTIFICATE_DIALOG);
269         }
270     }
271 
validCertificateSelected()272     private boolean validCertificateSelected() {
273         switch (mCredentials.getCertUsageSelected()) {
274             case Credentials.CERTIFICATE_USAGE_CA:
275                 return mCredentials.hasOnlyVpnAndAppsTrustAnchors();
276             case Credentials.CERTIFICATE_USAGE_USER:
277                 return mCredentials.hasUserCertificate()
278                         && !mCredentials.hasOnlyVpnAndAppsTrustAnchors();
279             case Credentials.CERTIFICATE_USAGE_WIFI:
280                 return true;
281             default:
282                 return false;
283         }
284     }
285 
installCertificateOrShowNameDialog()286     private void installCertificateOrShowNameDialog() {
287         if (!mCredentials.hasAnyForSystemInstall()) {
288             toastErrorAndFinish(R.string.no_cert_to_saved);
289         } else if (mCredentials.hasOnlyVpnAndAppsTrustAnchors()) {
290             // If there's only a CA certificate to install, then it's going to be used
291             // as a trust anchor. Install it and skip importing to Keystore.
292 
293             // more work to do, don't finish just yet
294             new InstallVpnAndAppsTrustAnchorsTask().execute();
295         } else {
296             // Name is required if installing User certificate
297             showDialog(NAME_CREDENTIAL_DIALOG);
298         }
299     }
300 
extractPkcs12InBackground(final String password)301     private void extractPkcs12InBackground(final String password) {
302         // show progress bar and extract certs in a background thread
303         showDialog(PROGRESS_BAR_DIALOG);
304 
305         new AsyncTask<Void,Void,Boolean>() {
306             @Override protected Boolean doInBackground(Void... unused) {
307                 return mCredentials.extractPkcs12(password);
308             }
309             @Override protected void onPostExecute(Boolean success) {
310                 MyAction action = new OnExtractionDoneAction(success);
311                 if (mState == STATE_PAUSED) {
312                     // activity is paused; run it in next onResume()
313                     mNextAction = action;
314                 } else {
315                     action.run(CertInstaller.this);
316                 }
317             }
318         }.execute();
319     }
320 
onExtractionDone(boolean success)321     private void onExtractionDone(boolean success) {
322         mNextAction = null;
323         removeDialog(PROGRESS_BAR_DIALOG);
324         if (success) {
325             removeDialog(PKCS12_PASSWORD_DIALOG);
326             if (mCredentials.calledBySettings()) {
327                 if (validCertificateSelected()) {
328                     installCertificateOrShowNameDialog();
329                 } else {
330                     showDialog(INVALID_CERTIFICATE_DIALOG);
331                 }
332             } else {
333                 createRedirectOrSelectUsageDialog();
334             }
335         } else {
336             showDialog(PKCS12_PASSWORD_DIALOG);
337             mView.setText(R.id.credential_password, "");
338             mView.showError(R.string.password_error);
339         }
340     }
341 
createRedirectOrSelectUsageDialog()342     private void createRedirectOrSelectUsageDialog() {
343         if (mCredentials.hasOnlyVpnAndAppsTrustAnchors()) {
344             showDialog(REDIRECT_CA_CERTIFICATE_DIALOG);
345         } else {
346             showDialog(SELECT_CERTIFICATE_USAGE_DIALOG);
347         }
348     }
349 
getCallingAppLabel()350     public CharSequence getCallingAppLabel() {
351         final String callingPkg = mCredentials.getReferrer();
352         if (callingPkg == null) {
353             Log.e(TAG, "Cannot get calling calling AppPackage");
354             return null;
355         }
356 
357         final PackageManager pm = getPackageManager();
358         final ApplicationInfo appInfo;
359         try {
360             appInfo = pm.getApplicationInfo(callingPkg, PackageManager.MATCH_DISABLED_COMPONENTS);
361         } catch (PackageManager.NameNotFoundException e) {
362             Log.e(TAG, "Unable to find info for package: " + callingPkg);
363             return null;
364         }
365 
366         return appInfo.loadLabel(pm);
367     }
368 
createRedirectCaCertificateDialog()369     private Dialog createRedirectCaCertificateDialog() {
370         final String message = getString(
371                 R.string.redirect_ca_certificate_with_app_info_message, getCallingAppLabel());
372         Dialog d = new AlertDialog.Builder(this)
373                 .setTitle(R.string.redirect_ca_certificate_title)
374                 .setMessage(message)
375                 .setPositiveButton(R.string.redirect_ca_certificate_close_button,
376                         (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved))
377                 .create();
378         d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved));
379         return d;
380     }
381 
createSelectCertificateUsageDialog()382     private Dialog createSelectCertificateUsageDialog() {
383         ViewGroup view = (ViewGroup) View.inflate(this, R.layout.select_certificate_usage_dialog,
384                 null);
385         mView.setView(view);
386 
387         RadioGroup radioGroup = view.findViewById(R.id.certificate_usage);
388         radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
389             switch (checkedId) {
390                 case R.id.user_certificate:
391                     mCredentials.setCertUsageSelectedAndUid(Credentials.CERTIFICATE_USAGE_USER);
392                     break;
393                 case R.id.wifi_certificate:
394                     mCredentials.setCertUsageSelectedAndUid(Credentials.CERTIFICATE_USAGE_WIFI);
395                 default:
396                     Slog.i(TAG, "Unknown selection for scope");
397             }
398         });
399 
400 
401         final Context appContext = getApplicationContext();
402         Dialog d = new AlertDialog.Builder(this)
403                 .setView(view)
404                 .setPositiveButton(android.R.string.ok, (dialog, id) -> {
405                     showDialog(NAME_CREDENTIAL_DIALOG);
406                 })
407                 .setNegativeButton(android.R.string.cancel,
408                         (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved))
409                 .create();
410         d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved));
411         return d;
412     }
413 
createInvalidCertificateDialog()414     private Dialog createInvalidCertificateDialog() {
415         Dialog d = new AlertDialog.Builder(this)
416                 .setTitle(R.string.invalid_certificate_title)
417                 .setMessage(getString(R.string.invalid_certificate_message,
418                         getCertificateUsageName()))
419                 .setPositiveButton(R.string.invalid_certificate_close_button,
420                         (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved))
421                 .create();
422         d.setOnCancelListener(dialog -> finish());
423         return d;
424     }
425 
getCertificateUsageName()426     String getCertificateUsageName() {
427         switch (mCredentials.getCertUsageSelected()) {
428             case Credentials.CERTIFICATE_USAGE_CA:
429                 return getString(R.string.ca_certificate);
430             case Credentials.CERTIFICATE_USAGE_USER:
431                 return getString(R.string.user_certificate);
432             case Credentials.CERTIFICATE_USAGE_WIFI:
433                 return getString(R.string.wifi_certificate);
434             default:
435                 return getString(R.string.certificate);
436         }
437     }
438 
createPkcs12PasswordDialog()439     private Dialog createPkcs12PasswordDialog() {
440         View view = View.inflate(this, R.layout.password_dialog, null);
441         mView.setView(view);
442         if (mView.getHasEmptyError()) {
443             mView.showError(R.string.password_empty_error);
444             mView.setHasEmptyError(false);
445         }
446 
447         String title = mCredentials.getName();
448         title = TextUtils.isEmpty(title)
449                 ? getString(R.string.pkcs12_password_dialog_title)
450                 : getString(R.string.pkcs12_file_password_dialog_title, title);
451         Dialog d = new AlertDialog.Builder(this)
452                 .setView(view)
453                 .setTitle(title)
454                 .setPositiveButton(android.R.string.ok, (dialog, id) -> {
455                     String password = mView.getText(R.id.credential_password);
456                     mNextAction = new Pkcs12ExtractAction(password);
457                     mNextAction.run(CertInstaller.this);
458                  })
459                 .setNegativeButton(android.R.string.cancel,
460                         (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved))
461                 .create();
462         d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved));
463         return d;
464     }
465 
createNameCertificateDialog()466     private Dialog createNameCertificateDialog() {
467         ViewGroup view = (ViewGroup) View.inflate(this, R.layout.name_certificate_dialog, null);
468         mView.setView(view);
469         if (mView.getHasEmptyError()) {
470             mView.showError(R.string.name_empty_error);
471             mView.setHasEmptyError(false);
472         }
473         final EditText nameInput = view.findViewById(R.id.certificate_name);
474         nameInput.setText(getDefaultName());
475         nameInput.selectAll();
476         final Context appContext = getApplicationContext();
477 
478         Dialog d = new AlertDialog.Builder(this)
479                 .setView(view)
480                 .setTitle(R.string.name_credential_dialog_title)
481                 .setPositiveButton(android.R.string.ok, (dialog, id) -> {
482                     String name = mView.getText(R.id.certificate_name);
483                     if (TextUtils.isEmpty(name)) {
484                         mView.setHasEmptyError(true);
485                         removeDialog(NAME_CREDENTIAL_DIALOG);
486                         showDialog(NAME_CREDENTIAL_DIALOG);
487                     } else {
488                         removeDialog(NAME_CREDENTIAL_DIALOG);
489                         mCredentials.setName(name);
490                         installCertificateToKeystore(appContext);
491                     }
492                 })
493                 .setNegativeButton(android.R.string.cancel,
494                         (dialog, id) -> toastErrorAndFinish(R.string.cert_not_saved))
495                 .create();
496         d.setOnCancelListener(dialog -> toastErrorAndFinish(R.string.cert_not_saved));
497         return d;
498     }
499 
installCertificateToKeystore(Context context)500     private void installCertificateToKeystore(Context context) {
501         try {
502             startActivityForResult(
503                     mCredentials.createSystemInstallIntent(context),
504                     REQUEST_SYSTEM_INSTALL_CODE);
505         } catch (ActivityNotFoundException e) {
506             Log.w(TAG, "installCertificateToKeystore(): ", e);
507             toastErrorAndFinish(R.string.cert_not_saved);
508         }
509     }
510 
getDefaultName()511     private String getDefaultName() {
512         String name = mCredentials.getName();
513         if (TextUtils.isEmpty(name)) {
514             return null;
515         } else {
516             // remove the extension from the file name
517             int index = name.lastIndexOf(".");
518             if (index > 0) name = name.substring(0, index);
519             return name;
520         }
521     }
522 
toastErrorAndFinish(int msgId)523     private void toastErrorAndFinish(int msgId) {
524         Toast.makeText(this, msgId, Toast.LENGTH_SHORT).show();
525         finish();
526     }
527 
528     private interface MyAction extends Serializable {
run(CertInstaller host)529         void run(CertInstaller host);
530     }
531 
532     private static class Pkcs12ExtractAction implements MyAction {
533         private final String mPassword;
534         private transient boolean hasRun;
535 
Pkcs12ExtractAction(String password)536         Pkcs12ExtractAction(String password) {
537             mPassword = password;
538         }
539 
run(CertInstaller host)540         public void run(CertInstaller host) {
541             if (hasRun) {
542                 return;
543             }
544             hasRun = true;
545             host.extractPkcs12InBackground(mPassword);
546         }
547     }
548 
549     private static class InstallOthersAction implements MyAction {
run(CertInstaller host)550         public void run(CertInstaller host) {
551             host.mNextAction = null;
552             host.installOthers();
553         }
554     }
555 
556     private static class OnExtractionDoneAction implements MyAction {
557         private final boolean mSuccess;
558 
OnExtractionDoneAction(boolean success)559         OnExtractionDoneAction(boolean success) {
560             mSuccess = success;
561         }
562 
run(CertInstaller host)563         public void run(CertInstaller host) {
564             host.onExtractionDone(mSuccess);
565         }
566     }
567 }
568