android: Add activity to import VPN profiles from JSON-encoded files
authorTobias Brunner <tobias@strongswan.org>
Thu, 29 Dec 2016 16:35:57 +0000 (17:35 +0100)
committerTobias Brunner <tobias@strongswan.org>
Fri, 20 Jan 2017 10:44:07 +0000 (11:44 +0100)
The file format is documented on the wiki.

URLs to .sswan files may be intercepted and downloaded files with a media
type of application/vnd.strongswan.profile may also be opened (the file
extension doesn't matter in that case).  Whether downloaded files for which
the media type is not correct but the extension is .sswan can be opened
depends on the app that issues the Intent.  For instance, from the default
Downloads app it won't work due to the content:// URLs that do not contain
the file name but when opening the downloaded file from within Chrome's
Downloads view it works as these Intents use file:// URLs, which contain
the complete file name (the latter requires a new permission).

12 files changed:
src/frontends/android/app/src/main/AndroidManifest.xml
src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java [new file with mode: 0644]
src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png [new file with mode: 0644]
src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png [new file with mode: 0644]
src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png [new file with mode: 0644]
src/frontends/android/app/src/main/res/layout/profile_import_view.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/menu/profile_import.xml [new file with mode: 0644]
src/frontends/android/app/src/main/res/values-de/strings.xml
src/frontends/android/app/src/main/res/values-pl/strings.xml
src/frontends/android/app/src/main/res/values-ru/strings.xml
src/frontends/android/app/src/main/res/values-ua/strings.xml
src/frontends/android/app/src/main/res/values/strings.xml

index da465ba..a0c7987 100644 (file)
@@ -20,6 +20,7 @@
 
     <uses-permission android:name="android.permission.INTERNET" />
     <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 
     <application
         android:name=".logic.StrongSwanApplication"
             </intent-filter>
         </activity>
         <activity
+            android:name=".ui.VpnProfileImportActivity"
+            android:label="@string/profile_import"
+            android:taskAffinity=""
+            android:excludeFromRecents="true" >
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:scheme="file" />
+                <data android:scheme="content" />
+                <data android:mimeType="application/vnd.strongswan.profile" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <category android:name="android.intent.category.DEFAULT" />
+                <category android:name="android.intent.category.BROWSABLE" />
+                <data android:scheme="http" />
+                <data android:scheme="https" />
+                <data android:scheme="file" />
+                <data android:scheme="content" />
+                <data android:host="*" />
+                <data android:pathPattern=".*\\.sswan" />
+            </intent-filter>
+        </activity>
+        <activity
             android:name=".ui.TrustedCertificateImportActivity"
             android:label="@string/import_certificate"
             android:theme="@style/AlertDialogTheme" >
diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java
new file mode 100644 (file)
index 0000000..1ed6e08
--- /dev/null
@@ -0,0 +1,770 @@
+/*
+ * Copyright (C) 2016 Tobias Brunner
+ * HSR Hochschule fuer Technik Rapperswil
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.ui;
+
+import android.app.Activity;
+import android.app.LoaderManager;
+import android.app.ProgressDialog;
+import android.content.AsyncTaskLoader;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.Intent;
+import android.content.Loader;
+import android.net.Uri;
+import android.os.Bundle;
+import android.security.KeyChain;
+import android.security.KeyChainAliasCallback;
+import android.security.KeyChainException;
+import android.support.v4.content.LocalBroadcastManager;
+import android.support.v7.app.AppCompatActivity;
+import android.util.Base64;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.strongswan.android.R;
+import org.strongswan.android.data.VpnProfile;
+import org.strongswan.android.data.VpnProfileDataSource;
+import org.strongswan.android.data.VpnType;
+import org.strongswan.android.data.VpnType.VpnTypeFeature;
+import org.strongswan.android.logic.TrustedCertificateManager;
+import org.strongswan.android.security.TrustedCertificateEntry;
+import org.strongswan.android.ui.widget.TextInputLayoutHelper;
+import org.strongswan.android.utils.Constants;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.UUID;
+
+public class VpnProfileImportActivity extends AppCompatActivity
+{
+       private static final String PKCS12_INSTALLED = "PKCS12_INSTALLED";
+       private static final int INSTALL_PKCS12 = 0;
+       private static final int PROFILE_LOADER = 0;
+       private static final int USER_CERT_LOADER = 1;
+
+       private VpnProfileDataSource mDataSource;
+       private ParsedVpnProfile mProfile;
+       private VpnProfile mExisting;
+       private TrustedCertificateEntry mCertEntry;
+       private TrustedCertificateEntry mUserCertEntry;
+       private String mUserCertLoading;
+       private boolean mHideImport;
+       private ProgressDialog mProgress;
+       private TextView mExistsWarning;
+       private ViewGroup mBasicDataGroup;
+       private TextView mName;
+       private TextView mGateway;
+       private TextView mSelectVpnType;
+       private ViewGroup mUsernamePassword;
+       private EditText mUsername;
+       private TextInputLayoutHelper mUsernameWrap;
+       private EditText mPassword;
+       private ViewGroup mUserCertificate;
+       private RelativeLayout mSelectUserCert;
+       private Button mImportUserCert;
+       private ViewGroup mRemoteCertificate;
+       private RelativeLayout mRemoteCert;
+
+       private LoaderManager.LoaderCallbacks<String> mProfileLoaderCallbacks = new LoaderManager.LoaderCallbacks<String>()
+       {
+               @Override
+               public Loader<String> onCreateLoader(int id, Bundle args)
+               {
+                       return new ProfileLoader(VpnProfileImportActivity.this, getIntent().getData());
+               }
+
+               @Override
+               public void onLoadFinished(Loader<String> loader, String data)
+               {
+                       handleProfile(data);
+               }
+
+               @Override
+               public void onLoaderReset(Loader<String> loader)
+               {
+
+               }
+       };
+
+       private LoaderManager.LoaderCallbacks<TrustedCertificateEntry> mUserCertificateLoaderCallbacks = new LoaderManager.LoaderCallbacks<TrustedCertificateEntry>()
+       {
+               @Override
+               public Loader<TrustedCertificateEntry> onCreateLoader(int id, Bundle args)
+               {
+                       return new UserCertificateLoader(VpnProfileImportActivity.this, mUserCertLoading);
+               }
+
+               @Override
+               public void onLoadFinished(Loader<TrustedCertificateEntry> loader, TrustedCertificateEntry data)
+               {
+                       handleUserCertificate(data);
+               }
+
+               @Override
+               public void onLoaderReset(Loader<TrustedCertificateEntry> loader)
+               {
+
+               }
+       };
+
+       @Override
+       public void onCreate(Bundle savedInstanceState)
+       {
+               super.onCreate(savedInstanceState);
+
+               getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp);
+               getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+
+               mDataSource = new VpnProfileDataSource(this);
+               mDataSource.open();
+
+               setContentView(R.layout.profile_import_view);
+
+               mExistsWarning = (TextView)findViewById(R.id.exists_warning);
+               mBasicDataGroup = (ViewGroup)findViewById(R.id.basic_data_group);
+               mName = (TextView)findViewById(R.id.name);
+               mGateway = (TextView)findViewById(R.id.gateway);
+               mSelectVpnType = (TextView)findViewById(R.id.vpn_type);
+
+               mUsernamePassword = (ViewGroup)findViewById(R.id.username_password_group);
+               mUsername = (EditText)findViewById(R.id.username);
+               mUsernameWrap = (TextInputLayoutHelper) findViewById(R.id.username_wrap);
+               mPassword = (EditText)findViewById(R.id.password);
+
+               mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group);
+               mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate);
+               mImportUserCert = (Button)findViewById(R.id.import_user_certificate);
+
+               mRemoteCertificate = (ViewGroup)findViewById(R.id.remote_certificate_group);
+               mRemoteCert = (RelativeLayout)findViewById(R.id.remote_certificate);
+
+               mExistsWarning.setVisibility(View.GONE);
+               mBasicDataGroup.setVisibility(View.GONE);
+               mUsernamePassword.setVisibility(View.GONE);
+               mUserCertificate.setVisibility(View.GONE);
+               mRemoteCertificate.setVisibility(View.GONE);
+
+               mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener());
+               mImportUserCert.setOnClickListener(new View.OnClickListener() {
+                       @Override
+                       public void onClick(View v)
+                       {
+                               Intent intent = KeyChain.createInstallIntent();
+                               intent.putExtra(KeyChain.EXTRA_NAME, getString(R.string.profile_cert_alias, mProfile.getName()));
+                               intent.putExtra(KeyChain.EXTRA_PKCS12, mProfile.PKCS12);
+                               startActivityForResult(intent, INSTALL_PKCS12);
+                       }
+               });
+
+               Intent intent = getIntent();
+               String action = intent.getAction();
+               if (Intent.ACTION_VIEW.equals(action))
+               {
+                       mProgress = ProgressDialog.show(this, null, getString(R.string.loading),
+                                                                                       true, true, new DialogInterface.OnCancelListener() {
+                               @Override
+                               public void onCancel(DialogInterface dialog)
+                               {
+                                       finish();
+                               }
+                       });
+
+                       getLoaderManager().initLoader(PROFILE_LOADER, null, mProfileLoaderCallbacks);
+               }
+
+               if (savedInstanceState != null)
+               {
+                       mUserCertLoading = savedInstanceState.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE);
+                       if (mUserCertLoading != null)
+                       {
+                               getLoaderManager().initLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
+                       }
+                       mImportUserCert.setEnabled(!savedInstanceState.getBoolean(PKCS12_INSTALLED));
+               }
+       }
+
+       @Override
+       protected void onDestroy()
+       {
+               super.onDestroy();
+               mDataSource.close();
+       }
+
+       @Override
+       protected void onSaveInstanceState(Bundle outState)
+       {
+               super.onSaveInstanceState(outState);
+               if (mUserCertEntry != null)
+               {
+                       outState.putString(VpnProfileDataSource.KEY_USER_CERTIFICATE, mUserCertEntry.getAlias());
+               }
+               outState.putBoolean(PKCS12_INSTALLED, !mImportUserCert.isEnabled());
+       }
+
+       @Override
+       public boolean onCreateOptionsMenu(Menu menu)
+       {
+               MenuInflater inflater = getMenuInflater();
+               inflater.inflate(R.menu.profile_import, menu);
+               if (mHideImport)
+               {
+                       MenuItem item = menu.findItem(R.id.menu_accept);
+                       item.setVisible(false);
+               }
+               return true;
+       }
+
+       @Override
+       public boolean onOptionsItemSelected(MenuItem item)
+       {
+               switch (item.getItemId())
+               {
+                       case android.R.id.home:
+                               finish();
+                               return true;
+                       case R.id.menu_accept:
+                               saveProfile();
+                               return true;
+                       default:
+                               return super.onOptionsItemSelected(item);
+               }
+       }
+
+       @Override
+       protected void onActivityResult(int requestCode, int resultCode, Intent data)
+       {
+               super.onActivityResult(requestCode, resultCode, data);
+               switch (requestCode)
+               {
+                       case INSTALL_PKCS12:
+                               if (resultCode == Activity.RESULT_OK)
+                               {       /* no need to import twice */
+                                       mImportUserCert.setEnabled(false);
+                                       mSelectUserCert.performClick();
+                               }
+               }
+       }
+
+       public void handleProfile(String data)
+       {
+               mProgress.dismiss();
+
+               mProfile = null;
+               if (data != null)
+               {
+                       try
+                       {
+                               JSONObject obj = new JSONObject(data);
+                               mProfile = parseProfile(obj);
+                       }
+                       catch (JSONException e)
+                       {
+                               mExistsWarning.setVisibility(View.VISIBLE);
+                               mExistsWarning.setText(e.getLocalizedMessage());
+                               mHideImport = true;
+                               invalidateOptionsMenu();
+                               return;
+                       }
+               }
+               if (mProfile == null)
+               {
+                       Toast.makeText(this, R.string.profile_import_failed, Toast.LENGTH_LONG).show();
+                       finish();
+                       return;
+               }
+               mExisting = mDataSource.getVpnProfile(mProfile.getUUID());
+               mExistsWarning.setVisibility(mExisting != null ? View.VISIBLE : View.GONE);
+
+               mBasicDataGroup.setVisibility(View.VISIBLE);
+               mName.setText(mProfile.getName());
+               mGateway.setText(mProfile.getGateway());
+               mSelectVpnType.setText(getResources().getStringArray(R.array.vpn_types)[mProfile.getVpnType().ordinal()]);
+
+               mUsernamePassword.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.USER_PASS) ? View.VISIBLE : View.GONE);
+               if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
+               {
+                       mUsername.setText(mProfile.getUsername());
+                       if (mProfile.getUsername() != null && !mProfile.getUsername().isEmpty())
+                       {
+                               mUsername.setEnabled(false);
+                       }
+               }
+
+               mUserCertificate.setVisibility(mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) ? View.VISIBLE : View.GONE);
+               mRemoteCertificate.setVisibility(mProfile.Certificate != null ? View.VISIBLE : View.GONE);
+               mImportUserCert.setVisibility(mProfile.PKCS12 != null ? View.VISIBLE : View.GONE);
+
+               updateUserCertView();
+
+               if (mProfile.Certificate != null)
+               {
+                       try
+                       {
+                               CertificateFactory factory = CertificateFactory.getInstance("X.509");
+                               X509Certificate certificate = (X509Certificate)factory.generateCertificate(new ByteArrayInputStream(mProfile.Certificate));
+                               KeyStore store = KeyStore.getInstance("LocalCertificateStore");
+                               store.load(null, null);
+                               String alias = store.getCertificateAlias(certificate);
+                               mCertEntry = new TrustedCertificateEntry(alias, certificate);
+                               ((TextView)mRemoteCert.findViewById(android.R.id.text1)).setText(mCertEntry.getSubjectPrimary());
+                               ((TextView)mRemoteCert.findViewById(android.R.id.text2)).setText(mCertEntry.getSubjectSecondary());
+                       }
+                       catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e)
+                       {
+                               e.printStackTrace();
+                               mRemoteCertificate.setVisibility(View.GONE);
+                       }
+               }
+       }
+
+       private void handleUserCertificate(TrustedCertificateEntry data)
+       {
+               mUserCertEntry = data;
+               mUserCertLoading = null;
+               updateUserCertView();
+       }
+
+       private void updateUserCertView()
+       {
+               if (mUserCertLoading != null)
+               {
+                       ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertLoading);
+                       ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.loading);
+               }
+               else if (mUserCertEntry != null)
+               {       /* clear any errors and set the new data */
+                       ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError(null);
+                       ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(mUserCertEntry.getAlias());
+                       ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(mUserCertEntry.getCertificate().getSubjectDN().toString());
+               }
+               else
+               {
+                       ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setText(R.string.profile_user_select_certificate_label);
+                       ((TextView)mSelectUserCert.findViewById(android.R.id.text2)).setText(R.string.profile_user_select_certificate);
+               }
+       }
+
+       private ParsedVpnProfile parseProfile(JSONObject obj) throws JSONException
+       {
+               UUID uuid;
+               try
+               {
+                       uuid = UUID.fromString(obj.getString("uuid"));
+               }
+               catch (IllegalArgumentException e)
+               {
+                       e.printStackTrace();
+                       return null;
+               }
+               ParsedVpnProfile profile = new ParsedVpnProfile();
+
+               profile.setUUID(uuid);
+               profile.setName(obj.getString("name"));
+               VpnType type = VpnType.fromIdentifier(obj.getString("type"));
+               profile.setVpnType(type);
+
+               JSONObject remote = obj.getJSONObject("remote");
+               profile.setGateway(remote.getString("addr"));
+               profile.setPort(getInteger(remote, "port", 1, 65535));
+               profile.setRemoteId(remote.optString("id", null));
+               profile.Certificate = decodeBase64(remote.optString("cert", null));
+
+               JSONObject local = obj.optJSONObject("local");
+               if (local != null)
+               {
+                       if (type.has(VpnTypeFeature.USER_PASS))
+                       {
+                               profile.setUsername(local.optString("eap_id", null));
+                       }
+
+                       if (type.has(VpnTypeFeature.CERTIFICATE))
+                       {
+                               profile.setLocalId(local.optString("id", null));
+                               profile.PKCS12 = decodeBase64(local.optString("p12", null));
+                       }
+               }
+
+               profile.setMTU(getInteger(obj, "mtu", Constants.MTU_MIN, Constants.MTU_MAX));
+               JSONObject split = obj.optJSONObject("split-tunneling");
+               if (split != null)
+               {
+                       int st = 0;
+                       st |= split.optBoolean("block-ipv4") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4 : 0;
+                       st |= split.optBoolean("block-ipv6") ? VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6 : 0;
+                       profile.setSplitTunneling(st == 0 ? null : st);
+               }
+               return profile;
+       }
+
+       private Integer getInteger(JSONObject obj, String key, int min, int max)
+       {
+               Integer res = obj.optInt(key);
+               return res < min || res > max ? null : res;
+       }
+
+       /**
+        * Save or update the profile depending on whether we actually have a
+        * profile object or not (this was created in updateProfileData)
+        */
+       private void saveProfile()
+       {
+               if (verifyInput())
+               {
+                       updateProfileData();
+                       if (mExisting != null)
+                       {
+                               mProfile.setId(mExisting.getId());
+                               mDataSource.updateVpnProfile(mProfile);
+                       }
+                       else
+                       {
+                               mDataSource.insertProfile(mProfile);
+                       }
+                       if (mCertEntry != null)
+                       {
+                               try
+                               {       /* store the CA/server certificate */
+                                       KeyStore store = KeyStore.getInstance("LocalCertificateStore");
+                                       store.load(null, null);
+                                       store.setCertificateEntry(null, mCertEntry.getCertificate());
+                                       TrustedCertificateManager.getInstance().reset();
+                               }
+                               catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | IOException e)
+                               {
+                                       e.printStackTrace();
+                               }
+                       }
+                       Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED);
+                       intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId());
+                       LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
+
+                       intent = new Intent(this, MainActivity.class);
+                       intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+                       startActivity(intent);
+
+                       setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId()));
+                       finish();
+               }
+       }
+
+       /**
+        * Verify the user input and display error messages.
+        * @return true if the input is valid
+        */
+       private boolean verifyInput()
+       {
+               boolean valid = true;
+               if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
+               {
+                       if (mUsername.getText().toString().trim().isEmpty())
+                       {
+                               mUsernameWrap.setError(getString(R.string.alert_text_no_input_username));
+                               valid = false;
+                       }
+               }
+               if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE) && mUserCertEntry == null)
+               {       /* let's show an error icon */
+                       ((TextView)mSelectUserCert.findViewById(android.R.id.text1)).setError("");
+                       valid = false;
+               }
+               return valid;
+       }
+
+       /**
+        * Update the profile object with the data entered by the user
+        */
+       private void updateProfileData()
+       {
+               if (mProfile.getVpnType().has(VpnTypeFeature.USER_PASS))
+               {
+                       mProfile.setUsername(mUsername.getText().toString().trim());
+                       String password = mPassword.getText().toString().trim();
+                       password = password.isEmpty() ? null : password;
+                       mProfile.setPassword(password);
+               }
+               if (mProfile.getVpnType().has(VpnTypeFeature.CERTIFICATE))
+               {
+                       mProfile.setUserCertificateAlias(mUserCertEntry.getAlias());
+               }
+               if (mCertEntry != null)
+               {
+                       mProfile.setCertificateAlias(mCertEntry.getAlias());
+               }
+       }
+
+       /**
+        * Load the JSON-encoded VPN profile from the given URI
+        */
+       private static class ProfileLoader extends AsyncTaskLoader<String>
+       {
+               private final Uri mUri;
+               private String mData;
+
+               public ProfileLoader(Context context, Uri uri)
+               {
+                       super(context);
+                       mUri = uri;
+               }
+
+               @Override
+               public String loadInBackground()
+               {
+                       InputStream in = null;
+
+                       if (ContentResolver.SCHEME_CONTENT.equals(mUri.getScheme()) ||
+                               ContentResolver.SCHEME_FILE.equals(mUri.getScheme()))
+                       {
+                               try
+                               {
+                                       in = getContext().getContentResolver().openInputStream(mUri);
+                               }
+                               catch (FileNotFoundException e)
+                               {
+                                       e.printStackTrace();
+                               }
+                       }
+                       else
+                       {
+                               try
+                               {
+                                       URL url = new URL(mUri.toString());
+                                       in = url.openStream();
+                               }
+                               catch (IOException e)
+                               {
+                                       e.printStackTrace();
+                               }
+                       }
+                       if (in != null)
+                       {
+                               return streamToString(in);
+                       }
+                       return null;
+               }
+
+               @Override
+               protected void onStartLoading()
+               {
+                       if (mData != null)
+                       {       /* if we have data ready, deliver it directly */
+                               deliverResult(mData);
+                       }
+                       if (takeContentChanged() || mData == null)
+                       {
+                               forceLoad();
+                       }
+               }
+
+               @Override
+               public void deliverResult(String data)
+               {
+                       if (isReset())
+                       {
+                               return;
+                       }
+                       mData = data;
+                       if (isStarted())
+                       {       /* if it is started we deliver the data directly,
+                                * otherwise this is handled in onStartLoading */
+                               super.deliverResult(data);
+                       }
+               }
+
+               @Override
+               protected void onReset()
+               {
+                       mData = null;
+                       super.onReset();
+               }
+
+               private String streamToString(InputStream in)
+               {
+                       ByteArrayOutputStream out = new ByteArrayOutputStream();
+                       byte[] buf = new byte[1024];
+                       int len;
+
+                       try
+                       {
+                               while ((len = in.read(buf)) != -1)
+                               {
+                                       out.write(buf, 0, len);
+                               }
+                               return out.toString("UTF-8");
+                       }
+                       catch (IOException e)
+                       {
+                               e.printStackTrace();
+                       }
+                       return null;
+               }
+       }
+
+       /**
+        * Ask the user to select an available certificate.
+        */
+       private class SelectUserCertOnClickListener implements View.OnClickListener, KeyChainAliasCallback
+       {
+               @Override
+               public void onClick(View v)
+               {
+                       String alias = null;
+                       if (mUserCertEntry != null)
+                       {
+                               alias = mUserCertEntry.getAlias();
+                               mUserCertEntry = null;
+                       }
+                       else if (mProfile != null)
+                       {
+                               alias = getString(R.string.profile_cert_alias, mProfile.getName());
+                       }
+                       KeyChain.choosePrivateKeyAlias(VpnProfileImportActivity.this, this, new String[] { "RSA" }, null, null, -1, alias);
+               }
+
+               @Override
+               public void alias(final String alias)
+               {
+                       /* alias() is not called from our main thread */
+                       runOnUiThread(new Runnable() {
+                               @Override
+                               public void run()
+                               {
+                                       mUserCertLoading = alias;
+                                       updateUserCertView();
+                                       if (alias != null)
+                                       {       /* otherwise the dialog was canceled, the request denied */
+                                               getLoaderManager().restartLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks);
+                                       }
+                               }
+                       });
+               }
+       }
+
+       /**
+        * Load the selected user certificate asynchronously.  This cannot be done
+        * from the main thread as getCertificateChain() calls back to our main
+        * thread to bind to the KeyChain service resulting in a deadlock.
+        */
+       private static class UserCertificateLoader extends AsyncTaskLoader<TrustedCertificateEntry>
+       {
+               private final String mAlias;
+               private TrustedCertificateEntry mData;
+
+               public UserCertificateLoader(Context context, String alias)
+               {
+                       super(context);
+                       mAlias = alias;
+               }
+
+               @Override
+               public TrustedCertificateEntry loadInBackground()
+               {
+                       X509Certificate[] chain = null;
+                       try
+                       {
+                               chain = KeyChain.getCertificateChain(getContext(), mAlias);
+                       }
+                       catch (KeyChainException | InterruptedException e)
+                       {
+                               e.printStackTrace();
+                       }
+                       if (chain != null && chain.length > 0)
+                       {
+                               return new TrustedCertificateEntry(mAlias, chain[0]);
+                       }
+                       return null;
+               }
+
+               @Override
+               protected void onStartLoading()
+               {
+                       if (mData != null)
+                       {       /* if we have data ready, deliver it directly */
+                               deliverResult(mData);
+                       }
+                       if (takeContentChanged() || mData == null)
+                       {
+                               forceLoad();
+                       }
+               }
+
+               @Override
+               public void deliverResult(TrustedCertificateEntry data)
+               {
+                       if (isReset())
+                       {
+                               return;
+                       }
+                       mData = data;
+                       if (isStarted())
+                       {       /* if it is started we deliver the data directly,
+                                * otherwise this is handled in onStartLoading */
+                               super.deliverResult(data);
+                       }
+               }
+
+               @Override
+               protected void onReset()
+               {
+                       mData = null;
+                       super.onReset();
+               }
+       }
+
+       private byte[] decodeBase64(String encoded)
+       {
+               if (encoded == null || encoded.isEmpty())
+               {
+                       return null;
+               }
+               byte[] data = null;
+               try
+               {
+                       data = Base64.decode(encoded, Base64.DEFAULT);
+               }
+               catch (IllegalArgumentException e)
+               {
+                       e.printStackTrace();
+               }
+               return data;
+       }
+
+       private class ParsedVpnProfile extends VpnProfile
+       {
+               public byte[] Certificate;
+               public byte[] PKCS12;
+       }
+}
diff --git a/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png
new file mode 100644 (file)
index 0000000..ceb1a1e
Binary files /dev/null and b/src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png differ
diff --git a/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png
new file mode 100644 (file)
index 0000000..af7f828
Binary files /dev/null and b/src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png differ
diff --git a/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png b/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png
new file mode 100644 (file)
index 0000000..b7c7ffd
Binary files /dev/null and b/src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png differ
diff --git a/src/frontends/android/app/src/main/res/layout/profile_import_view.xml b/src/frontends/android/app/src/main/res/layout/profile_import_view.xml
new file mode 100644 (file)
index 0000000..fc06aa5
--- /dev/null
@@ -0,0 +1,196 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2016 Tobias Brunner
+    HSR Hochschule fuer Technik Rapperswil
+
+    This program is free software; you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by the
+    Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+
+    This program is distributed in the hope that it will be useful, but
+    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+-->
+<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
+            xmlns:app="http://schemas.android.com/apk/res-auto"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent" >
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        android:padding="10dp"
+        android:animateLayoutChanges="true" >
+
+        <TextView
+            android:id="@+id/exists_warning"
+            android:background="@drawable/state_background"
+            android:padding="8dp"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="8dp"
+            android:drawableLeft="@android:drawable/ic_dialog_alert"
+            android:drawableStart="@android:drawable/ic_dialog_alert"
+            android:drawablePadding="8dp"
+            android:textStyle="bold"
+            android:text="@string/profile_import_exists"
+            android:textAppearance="?android:attr/textAppearanceSmall"
+            android:textColor="?android:attr/textColorPrimary" />
+
+        <LinearLayout
+            android:id="@+id/basic_data_group"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical" >
+
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:textSize="12sp"
+                android:text="@string/profile_name_label_simple" />
+
+            <TextView
+                android:id="@+id/name"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:textAppearance="?android:attr/textAppearanceMedium"
+                android:textColor="?android:attr/textColorPrimary" />
+
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:textSize="12sp"
+                android:text="@string/profile_gateway_label" />
+
+            <TextView
+                android:id="@+id/gateway"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:textAppearance="?android:attr/textAppearanceMedium"
+                android:textColor="?android:attr/textColorPrimary" />
+
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="6dp"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:textSize="12sp"
+                android:text="@string/profile_vpn_type_label" />
+
+            <TextView
+                android:id="@+id/vpn_type"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginBottom="4dp"
+                android:layout_marginLeft="4dp"
+                android:layout_marginStart="4dp"
+                android:textAppearance="?android:attr/textAppearanceMedium"
+                android:textColor="?android:attr/textColorPrimary" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/username_password_group"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical"
+            android:layout_marginTop="6dp">
+
+            <org.strongswan.android.ui.widget.TextInputLayoutHelper
+                android:id="@+id/username_wrap"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content" >
+
+                <android.support.design.widget.TextInputEditText
+                    android:id="@+id/username"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:singleLine="true"
+                    android:inputType="textNoSuggestions"
+                    android:hint="@string/profile_username_label" />
+
+            </org.strongswan.android.ui.widget.TextInputLayoutHelper>
+
+            <org.strongswan.android.ui.widget.TextInputLayoutHelper
+                android:id="@+id/password_wrap"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                app:helper_text="@string/profile_password_hint" >
+
+                <android.support.design.widget.TextInputEditText
+                    android:id="@+id/password"
+                    android:layout_width="match_parent"
+                    android:layout_height="wrap_content"
+                    android:singleLine="true"
+                    android:inputType="textPassword|textNoSuggestions"
+                    android:hint="@string/profile_password_label" />
+
+            </org.strongswan.android.ui.widget.TextInputLayoutHelper>
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/user_certificate_group"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
+            android:orientation="vertical" >
+
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginTop="4dp"
+                android:layout_marginLeft="4dp"
+                android:textSize="12sp"
+                android:text="@string/profile_user_certificate_label" />
+
+            <include
+                android:id="@+id/select_user_certificate"
+                layout="@layout/two_line_button" />
+
+            <Button
+                android:id="@+id/import_user_certificate"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="4dp"
+                android:layout_marginRight="4dp"
+                android:text="@string/profile_cert_import" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/remote_certificate_group"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_marginBottom="4dp"
+            android:orientation="vertical">
+
+            <TextView
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="4dp"
+                android:textSize="12sp"
+                android:text="@string/profile_ca_label" />
+
+            <include
+                android:id="@+id/remote_certificate"
+                layout="@layout/two_line_button" />
+        </LinearLayout>
+
+    </LinearLayout>
+
+</ScrollView>
diff --git a/src/frontends/android/app/src/main/res/menu/profile_import.xml b/src/frontends/android/app/src/main/res/menu/profile_import.xml
new file mode 100644 (file)
index 0000000..99893f7
--- /dev/null
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+    Copyright (C) 2016 Tobias Brunner
+    Hochschule fuer Technik Rapperswil
+
+    This program is free software; you can redistribute it and/or modify it
+    under the terms of the GNU General Public License as published by the
+    Free Software Foundation; either version 2 of the License, or (at your
+    option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+
+    This program is distributed in the hope that it will be useful, but
+    WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+    or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+    for more details.
+-->
+<menu xmlns:android="http://schemas.android.com/apk/res/android"
+      xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <item
+        android:id="@+id/menu_accept"
+        android:title="@string/profile_edit_import"
+        app:showAsAction="always|withText" />
+
+</menu>
index 857a18a..383fd24 100644 (file)
 
     <!-- VPN profile details -->
     <string name="profile_edit_save">Speichern</string>
+    <string name="profile_edit_import">Importieren</string>
     <string name="profile_edit_cancel">Abbrechen</string>
     <string name="profile_name_label">Profilname (optional)</string>
+    <string name="profile_name_label_simple">Profilname</string>
     <string name="profile_name_hint">Standardwert ist der konfigurierte Server</string>
     <string name="profile_name_hint_gateway">Standardwert ist \"%1$s\"</string>
     <string name="profile_gateway_label">Server</string>
     <string name="profile_split_tunneling_label">Split-Tunneling</string>
     <string name="profile_split_tunnelingv4_title">Blockiere IPv4 Verkehr der nicht für das VPN bestimmt ist</string>
     <string name="profile_split_tunnelingv6_title">Blockiere IPv6 Verkehr der nicht für das VPN bestimmt ist</string>
+    <string name="profile_import">VPN Profile importieren</string>
+    <string name="profile_import_failed">VPN Profil-Import fehlgeschlagen</string>
+    <string name="profile_import_exists">Dieses VPN Profil existiert bereits, die bestehenden Einstellungen werden ersetzt.</string>
+    <string name="profile_cert_import">Zertifikat aus VPN Profil importieren</string>
+    <string name="profile_cert_alias">Zertifikat für \"%1$s\"</string>
     <!-- Warnings/Notifications in the details view -->
     <string name="alert_text_no_input_gateway">Ein Wert wird benötigt, um die Verbindung aufbauen zu können</string>
     <string name="alert_text_no_input_username">Bitte geben Sie Ihren Benutzernamen ein</string>
index df44122..c2b2184 100644 (file)
 
     <!-- VPN profile details -->
     <string name="profile_edit_save">Zapisz</string>
+    <string name="profile_edit_import">Import</string>
     <string name="profile_edit_cancel">Anuluj</string>
     <string name="profile_name_label">Nazwa profilu (opcjonalny)</string>
+    <string name="profile_name_label_simple">Nazwa profilu</string>
     <string name="profile_name_hint">Defaults to the configured server</string>
     <string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string>
     <string name="profile_gateway_label">Serwer</string>
     <string name="profile_split_tunneling_label">Split tunneling</string>
     <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
     <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+    <string name="profile_import">Import VPN profile</string>
+    <string name="profile_import_failed">Failed to import VPN profile</string>
+    <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
+    <string name="profile_cert_import">Import certificate from VPN profile</string>
+    <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
     <!-- Warnings/Notifications in the details view -->
     <string name="alert_text_no_input_gateway">A value is required to initiate the connection</string>
     <string name="alert_text_no_input_username">Wprowadź swoją nazwę użytkownika</string>
index 933b2fb..e165aab 100644 (file)
 
     <!-- VPN profile details -->
     <string name="profile_edit_save">Сохранить</string>
+    <string name="profile_edit_import">Import</string>
     <string name="profile_edit_cancel">Отмена</string>
     <string name="profile_name_label">Название профиля (необязательный)</string>
+    <string name="profile_name_label_simple">Название профиля</string>
     <string name="profile_name_hint">Defaults to the configured server</string>
     <string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string>
     <string name="profile_gateway_label">Сервер</string>
     <string name="profile_split_tunneling_label">Split tunneling</string>
     <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
     <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+    <string name="profile_import">Import VPN profile</string>
+    <string name="profile_import_failed">Failed to import VPN profile</string>
+    <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
+    <string name="profile_cert_import">Import certificate from VPN profile</string>
+    <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
     <!-- Warnings/Notifications in the details view -->
     <string name="alert_text_no_input_gateway">A value is required to initiate the connection</string>
     <string name="alert_text_no_input_username">Пожалуйста введите имя пользователя</string>
index e48a921..c548069 100644 (file)
 
     <!-- VPN profile details -->
     <string name="profile_edit_save">Зберегти</string>
+    <string name="profile_edit_import">Import</string>
     <string name="profile_edit_cancel">Відміна</string>
     <string name="profile_name_label">Назва профілю (необов\'язковий)</string>
+    <string name="profile_name_label_simple">Назва профілю</string>
     <string name="profile_name_hint">Defaults to the configured server</string>
     <string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string>
     <string name="profile_gateway_label">Сервер</string>
     <string name="profile_split_tunneling_label">Split tunneling</string>
     <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
     <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+    <string name="profile_import">Import VPN profile</string>
+    <string name="profile_import_failed">Failed to import VPN profile</string>
+    <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
+    <string name="profile_cert_import">Import certificate from VPN profile</string>
+    <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
     <!-- Warnings/Notifications in the details view -->
     <string name="alert_text_no_input_gateway">A value is required to initiate the connection</string>
     <string name="alert_text_no_input_username">Введіть ім\'я користувача </string>
index 0ceace1..ddca469 100644 (file)
 
     <!-- VPN profile details -->
     <string name="profile_edit_save">Save</string>
+    <string name="profile_edit_import">Import</string>
     <string name="profile_edit_cancel">Cancel</string>
     <string name="profile_name_label">Profile name (optional)</string>
+    <string name="profile_name_label_simple">Profile name</string>
     <string name="profile_name_hint">Defaults to the configured server</string>
     <string name="profile_name_hint_gateway">Defaults to \"%1$s\"</string>
     <string name="profile_gateway_label">Server</string>
     <string name="profile_split_tunneling_label">Split tunneling</string>
     <string name="profile_split_tunnelingv4_title">Block IPv4 traffic not destined for the VPN</string>
     <string name="profile_split_tunnelingv6_title">Block IPv6 traffic not destined for the VPN</string>
+    <string name="profile_import">Import VPN profile</string>
+    <string name="profile_import_failed">Failed to import VPN profile</string>
+    <string name="profile_import_exists">This VPN profile already exists, its current settings will be replaced.</string>
+    <string name="profile_cert_import">Import certificate from VPN profile</string>
+    <string name="profile_cert_alias">Certificate for \"%1$s\"</string>
     <!-- Warnings/Notifications in the details view -->
     <string name="alert_text_no_input_gateway">A value is required to initiate the connection</string>
     <string name="alert_text_no_input_username">Please enter your username </string>