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