android: Show a confirmation dialog before importing certificates
authorTobias Brunner <tobias@strongswan.org>
Thu, 5 Jun 2014 17:06:34 +0000 (19:06 +0200)
committerTobias Brunner <tobias@strongswan.org>
Tue, 22 Jul 2014 08:41:51 +0000 (10:41 +0200)
Since the import activity can be triggered by any other app on the
system we shouldn't just import every certificate we get.

Also, in some situations (e.g. if no passphrase has been set yet for the
system-wide certificate store) we are the only application that can open
certificate files.  So if a user clicked on a certificate file she would
just get a confirmation Toast about a successful import, with no indication
whatsoever where the certificate was actually imported.  The new dialog
shows the app icon to indicate that strongSwan is involved.

src/frontends/android/AndroidManifest.xml
src/frontends/android/src/org/strongswan/android/ui/TrustedCertificateImportActivity.java

index 887063f..1a5af0d 100644 (file)
@@ -69,7 +69,8 @@
         </activity>
         <activity
             android:name=".ui.TrustedCertificateImportActivity"
-            android:label="@string/import_certificate" >
+            android:label="@string/import_certificate"
+            android:theme="@android:style/Theme.Holo.Dialog.NoActionBar" >
             <intent-filter>
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
index f8a9438..61bd2c9 100644 (file)
 
 package org.strongswan.android.ui;
 
+import java.io.FileNotFoundException;
 import java.io.InputStream;
 import java.security.KeyStore;
+import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 
 import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfileDataSource;
 import org.strongswan.android.logic.TrustedCertificateManager;
 
 import android.annotation.TargetApi;
 import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.app.FragmentTransaction;
+import android.content.DialogInterface;
 import android.content.Intent;
 import android.net.Uri;
 import android.os.Build;
@@ -34,6 +42,7 @@ import android.widget.Toast;
 public class TrustedCertificateImportActivity extends Activity
 {
        private static final int OPEN_DOCUMENT = 0;
+       private static final String DIALOG_TAG = "Dialog";
 
        /* same as those listed in the manifest */
        private static final String[] ACCEPTED_MIME_TYPES = {
@@ -49,6 +58,11 @@ public class TrustedCertificateImportActivity extends Activity
        {
                super.onCreate(savedInstanceState);
 
+               if (savedInstanceState != null)
+               {       /* do nothing when we are restoring */
+                       return;
+               }
+
                Intent intent = getIntent();
                String action = intent.getAction();
                if (Intent.ACTION_VIEW.equals(action))
@@ -61,9 +75,7 @@ public class TrustedCertificateImportActivity extends Activity
                        openIntent.setType("*/*");
                        openIntent.putExtra(Intent.EXTRA_MIME_TYPES, ACCEPTED_MIME_TYPES);
                        startActivityForResult(openIntent, OPEN_DOCUMENT);
-                       return;
                }
-               finish();
        }
 
        @Override
@@ -74,10 +86,8 @@ public class TrustedCertificateImportActivity extends Activity
                        case OPEN_DOCUMENT:
                                if (resultCode == Activity.RESULT_OK && data != null)
                                {
-                                       if (importCertificate(data.getData()))
-                                       {
-                                               setResult(Activity.RESULT_OK);
-                                       }
+                                       importCertificate(data.getData());
+                                       return;
                                }
                                finish();
                                return;
@@ -86,30 +96,128 @@ public class TrustedCertificateImportActivity extends Activity
        }
 
        /**
-        * Try to import the file pointed to by the given URI as a certificate.
+        * Import the file pointed to by the given URI as a certificate.
+        * @param uri
+        */
+       private void importCertificate(Uri uri)
+       {
+               X509Certificate certificate = parseCertificate(uri);
+               if (certificate == null)
+               {
+                       Toast.makeText(this, R.string.cert_import_failed, Toast.LENGTH_LONG).show();
+                       finish();
+                       return;
+               }
+               /* Ask the user whether to import the certificate.  This is particularly
+                * necessary because the import activity can be triggered by any app on
+                * the system.  Also, if our app is the only one that is registered to
+                * open certificate files by MIME type the user would have no idea really
+                * where the file was imported just by reading the Toast we display. */
+               ConfirmImportDialog dialog = new ConfirmImportDialog();
+               Bundle args = new Bundle();
+               args.putSerializable(VpnProfileDataSource.KEY_CERTIFICATE, certificate);
+               dialog.setArguments(args);
+               FragmentTransaction ft = getFragmentManager().beginTransaction();
+               ft.add(dialog, DIALOG_TAG);
+               ft.commit();
+       }
+
+       /**
+        * Load the file from the given URI and try to parse it as X.509 certificate.
         * @param uri
-        * @return whether the import was successful
+        * @return certificate or null
         */
-       private boolean importCertificate(Uri uri)
+       private X509Certificate parseCertificate(Uri uri)
        {
+               X509Certificate certificate = null;
                try
                {
                        CertificateFactory factory = CertificateFactory.getInstance("X.509");
                        InputStream in = getContentResolver().openInputStream(uri);
-                       X509Certificate certificate = (X509Certificate)factory.generateCertificate(in);
+                       certificate = (X509Certificate)factory.generateCertificate(in);
                        /* we don't check whether it's actually a CA certificate or not */
+               }
+               catch (CertificateException e)
+               {
+                       e.printStackTrace();
+               }
+               catch (FileNotFoundException e)
+               {
+                       e.printStackTrace();
+               }
+               return certificate;
+       }
+
+
+       /**
+        * Try to store the given certificate in the KeyStore.
+        * @param certificate
+        * @return whether it was successfully stored
+        */
+       private boolean storeCertificate(X509Certificate certificate)
+       {
+               try
+               {
                        KeyStore store = KeyStore.getInstance("LocalCertificateStore");
                        store.load(null, null);
                        store.setCertificateEntry(null, certificate);
                        TrustedCertificateManager.getInstance().reset();
-                       Toast.makeText(this, R.string.cert_imported_successfully, Toast.LENGTH_LONG).show();
                        return true;
                }
                catch (Exception e)
                {
-                       Toast.makeText(this, R.string.cert_import_failed, Toast.LENGTH_LONG).show();
                        e.printStackTrace();
+                       return false;
+               }
+       }
+
+       /**
+        * Class that displays a confirmation dialog when a certificate should get
+        * imported. If the user confirms the import we try to store it.
+        */
+       public static class ConfirmImportDialog extends DialogFragment
+       {
+               @Override
+               public Dialog onCreateDialog(Bundle savedInstanceState)
+               {
+                       final X509Certificate certificate;
+
+                       certificate = (X509Certificate)getArguments().getSerializable(VpnProfileDataSource.KEY_CERTIFICATE);
+
+                       return new AlertDialog.Builder(getActivity())
+                               .setIcon(R.drawable.ic_launcher)
+                               .setTitle(R.string.import_certificate)
+                               .setMessage(certificate.getSubjectDN().toString())
+                               .setPositiveButton(R.string.import_certificate, new DialogInterface.OnClickListener() {
+                                       @Override
+                                       public void onClick(DialogInterface dialog, int whichButton)
+                                       {
+                                               TrustedCertificateImportActivity activity = (TrustedCertificateImportActivity)getActivity();
+                                               if (activity.storeCertificate(certificate))
+                                               {
+                                                       Toast.makeText(getActivity(), R.string.cert_imported_successfully, Toast.LENGTH_LONG).show();
+                                                       getActivity().setResult(Activity.RESULT_OK);
+                                               }
+                                               else
+                                               {
+                                                       Toast.makeText(getActivity(), R.string.cert_import_failed, Toast.LENGTH_LONG).show();
+                                               }
+                                               getActivity().finish();
+                                       }
+                               })
+                               .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() {
+                                       @Override
+                                       public void onClick(DialogInterface dialog, int which)
+                                       {
+                                               getActivity().finish();
+                                       }
+                               }).create();
+               }
+
+               @Override
+               public void onCancel(DialogInterface dialog)
+               {
+                       getActivity().finish();
                }
-               return false;
        }
 }