From c4ab9af74e1c2532e03358f58aede47104e81917 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Tue, 27 Dec 2016 15:17:49 +0100 Subject: [PATCH 1/6] android: Add a UUID property to the VPN profiles All new or edited profiles get a random UUID. We currently don't enforce one, though. Later we might change that and use the UUID as primary key. --- .../strongswan/android/data/VpnProfile.java | 25 ++++++++++- .../android/data/VpnProfileDataSource.java | 44 ++++++++++++++++++- .../android/ui/VpnProfileDetailActivity.java | 5 +++ 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java index 8a9d319b5..54bdfcbdd 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfile.java @@ -18,6 +18,8 @@ package org.strongswan.android.data; +import java.util.UUID; + public class VpnProfile implements Cloneable { /* While storing this as EnumSet would be nicer this simplifies storing it in a database */ @@ -28,8 +30,14 @@ public class VpnProfile implements Cloneable private String mRemoteId, mLocalId; private Integer mMTU, mPort, mSplitTunneling; private VpnType mVpnType; + private UUID mUUID; private long mId = -1; + public VpnProfile() + { + this.mUUID = UUID.randomUUID(); + } + public long getId() { return mId; @@ -40,6 +48,16 @@ public class VpnProfile implements Cloneable this.mId = id; } + public void setUUID(UUID uuid) + { + this.mUUID = uuid; + } + + public UUID getUUID() + { + return mUUID; + } + public String getName() { return mName; @@ -171,7 +189,12 @@ public class VpnProfile implements Cloneable { if (o != null && o instanceof VpnProfile) { - return this.mId == ((VpnProfile)o).getId(); + VpnProfile other = (VpnProfile)o; + if (this.mUUID != null && other.getUUID() != null) + { + return this.mUUID.equals(other.getUUID()); + } + return this.mId == other.getId(); } return false; } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java index 17026536b..1c509a302 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileDataSource.java @@ -19,6 +19,7 @@ package org.strongswan.android.data; import java.util.ArrayList; import java.util.List; +import java.util.UUID; import android.content.ContentValues; import android.content.Context; @@ -33,6 +34,7 @@ public class VpnProfileDataSource { private static final String TAG = VpnProfileDataSource.class.getSimpleName(); public static final String KEY_ID = "_id"; + public static final String KEY_UUID = "_uuid"; public static final String KEY_NAME = "name"; public static final String KEY_GATEWAY = "gateway"; public static final String KEY_VPN_TYPE = "vpn_type"; @@ -53,11 +55,12 @@ public class VpnProfileDataSource private static final String DATABASE_NAME = "strongswan.db"; private static final String TABLE_VPNPROFILE = "vpnprofile"; - private static final int DATABASE_VERSION = 8; + private static final int DATABASE_VERSION = 9; public static final String DATABASE_CREATE = "CREATE TABLE " + TABLE_VPNPROFILE + " (" + KEY_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + KEY_UUID + " TEXT UNIQUE," + KEY_NAME + " TEXT NOT NULL," + KEY_GATEWAY + " TEXT NOT NULL," + KEY_VPN_TYPE + " TEXT NOT NULL," + @@ -73,6 +76,7 @@ public class VpnProfileDataSource ");"; private static final String[] ALL_COLUMNS = new String[] { KEY_ID, + KEY_UUID, KEY_NAME, KEY_GATEWAY, KEY_VPN_TYPE, @@ -141,6 +145,12 @@ public class VpnProfileDataSource db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_REMOTE_ID + " TEXT;"); } + if (oldVersion < 9) + { + db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_UUID + + " TEXT;"); + updateColumns(db); + } } private void updateColumns(SQLiteDatabase db) @@ -261,6 +271,24 @@ public class VpnProfileDataSource return profile; } + /** + * Get a single VPN profile from the database by its UUID. + * @param uuid the UUID of the VPN profile + * @return the profile or null, if not found + */ + public VpnProfile getVpnProfile(UUID uuid) + { + VpnProfile profile = null; + Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS, + KEY_UUID + "='" + uuid.toString() + "'", null, null, null, null); + if (cursor.moveToFirst()) + { + profile = VpnProfileFromCursor(cursor); + } + cursor.close(); + return profile; + } + /** * Get a list of all VPN profiles stored in the database. * @return list of VPN profiles @@ -285,6 +313,7 @@ public class VpnProfileDataSource { VpnProfile profile = new VpnProfile(); profile.setId(cursor.getLong(cursor.getColumnIndex(KEY_ID))); + profile.setUUID(getUUID(cursor, cursor.getColumnIndex(KEY_UUID))); profile.setName(cursor.getString(cursor.getColumnIndex(KEY_NAME))); profile.setGateway(cursor.getString(cursor.getColumnIndex(KEY_GATEWAY))); profile.setVpnType(VpnType.fromIdentifier(cursor.getString(cursor.getColumnIndex(KEY_VPN_TYPE)))); @@ -303,6 +332,7 @@ public class VpnProfileDataSource private ContentValues ContentValuesFromVpnProfile(VpnProfile profile) { ContentValues values = new ContentValues(); + values.put(KEY_UUID, profile.getUUID() != null ? profile.getUUID().toString() : null); values.put(KEY_NAME, profile.getName()); values.put(KEY_GATEWAY, profile.getGateway()); values.put(KEY_VPN_TYPE, profile.getVpnType().getIdentifier()); @@ -322,4 +352,16 @@ public class VpnProfileDataSource { return cursor.isNull(columnIndex) ? null : cursor.getInt(columnIndex); } + + private UUID getUUID(Cursor cursor, int columnIndex) + { + try + { + return cursor.isNull(columnIndex) ? null : UUID.fromString(cursor.getString(columnIndex)); + } + catch (Exception e) + { + return null; + } + } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java index 30fb101be..6405c0dda 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java @@ -65,6 +65,7 @@ import org.strongswan.android.ui.adapter.CertificateIdentitiesAdapter; import org.strongswan.android.ui.widget.TextInputLayoutHelper; import java.security.cert.X509Certificate; +import java.util.UUID; public class VpnProfileDetailActivity extends AppCompatActivity { @@ -453,6 +454,10 @@ public class VpnProfileDetailActivity extends AppCompatActivity if (mProfile != null) { updateProfileData(); + if (mProfile.getUUID() == null) + { + mProfile.setUUID(UUID.randomUUID()); + } mDataSource.updateVpnProfile(mProfile); } else From cf6110f1520f3030bcb34a50532991fdbb0e72c5 Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 29 Dec 2016 17:02:22 +0100 Subject: [PATCH 2/6] android: Use a local broadcast to notify about profile changes This allows other components to modify the profiles and notify about changes. --- .../android/ui/VpnProfileDetailActivity.java | 28 +++--- .../android/ui/VpnProfileListFragment.java | 86 ++++++++++++------- .../strongswan/android/utils/Constants.java | 40 +++++++++ 3 files changed, 107 insertions(+), 47 deletions(-) create mode 100644 src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java index 6405c0dda..bf64370cf 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileDetailActivity.java @@ -26,6 +26,7 @@ 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.AlertDialog; import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatDialogFragment; @@ -63,6 +64,7 @@ import org.strongswan.android.logic.TrustedCertificateManager; import org.strongswan.android.security.TrustedCertificateEntry; import org.strongswan.android.ui.adapter.CertificateIdentitiesAdapter; import org.strongswan.android.ui.widget.TextInputLayoutHelper; +import org.strongswan.android.utils.Constants; import java.security.cert.X509Certificate; import java.util.UUID; @@ -70,8 +72,6 @@ import java.util.UUID; public class VpnProfileDetailActivity extends AppCompatActivity { private static final int SELECT_TRUSTED_CERTIFICATE = 0; - private static final int MTU_MIN = 1280; - private static final int MTU_MAX = 1500; private VpnProfileDataSource mDataSource; private Long mId; @@ -466,6 +466,10 @@ public class VpnProfileDetailActivity extends AppCompatActivity updateProfileData(); mDataSource.insertProfile(mProfile); } + Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); + intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId()); + LocalBroadcastManager.getInstance(this).sendBroadcast(intent); + setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId())); finish(); } @@ -501,9 +505,9 @@ public class VpnProfileDetailActivity extends AppCompatActivity showCertificateAlert(); valid = false; } - if (!validateInteger(mMTU, MTU_MIN, MTU_MAX)) + if (!validateInteger(mMTU, Constants.MTU_MIN, Constants.MTU_MAX)) { - mMTUWrap.setError(String.format(getString(R.string.alert_text_out_of_range), MTU_MIN, MTU_MAX)); + mMTUWrap.setError(String.format(getString(R.string.alert_text_out_of_range), Constants.MTU_MIN, Constants.MTU_MAX)); valid = false; } if (!validateInteger(mPort, 1, 65535)) @@ -572,8 +576,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity mRemoteId.setText(mProfile.getRemoteId()); mMTU.setText(mProfile.getMTU() != null ? mProfile.getMTU().toString() : null); mPort.setText(mProfile.getPort() != null ? mProfile.getPort().toString() : null); - mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null ? (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0 : false); - mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null ? (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0 : false); + mBlockIPv4.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4) != 0); + mBlockIPv6.setChecked(mProfile.getSplitTunneling() != null && (mProfile.getSplitTunneling() & VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6) != 0); useralias = mProfile.getUserCertificateAlias(); local_id = mProfile.getLocalId(); alias = mProfile.getCertificateAlias(); @@ -691,11 +695,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity } }); } - catch (KeyChainException e) - { - e.printStackTrace(); - } - catch (InterruptedException e) + catch (KeyChainException | InterruptedException e) { e.printStackTrace(); } @@ -727,11 +727,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity { chain = KeyChain.getCertificateChain(mContext, mAlias); } - catch (KeyChainException e) - { - e.printStackTrace(); - } - catch (InterruptedException e) + catch (KeyChainException | InterruptedException e) { e.printStackTrace(); } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java index d8d99ff00..8210d597c 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileListFragment.java @@ -17,12 +17,14 @@ package org.strongswan.android.ui; -import android.app.Activity; +import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; +import android.content.IntentFilter; import android.content.res.TypedArray; import android.os.Bundle; import android.support.v4.app.Fragment; +import android.support.v4.content.LocalBroadcastManager; import android.util.AttributeSet; import android.view.ActionMode; import android.view.LayoutInflater; @@ -41,9 +43,11 @@ import org.strongswan.android.R; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfileDataSource; import org.strongswan.android.ui.adapter.VpnProfileAdapter; +import org.strongswan.android.utils.Constants; import java.util.ArrayList; import java.util.HashSet; +import java.util.Iterator; import java.util.List; public class VpnProfileListFragment extends Fragment @@ -58,11 +62,47 @@ public class VpnProfileListFragment extends Fragment private OnVpnProfileSelectedListener mListener; private boolean mReadOnly; + private BroadcastReceiver mProfilesChanged = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + long id, ids[]; + if ((id = intent.getLongExtra(Constants.VPN_PROFILES_SINGLE, 0)) > 0) + { + VpnProfile profile = mDataSource.getVpnProfile(id); + if (profile != null) + { /* in case this was an edit, we remove it first */ + mVpnProfiles.remove(profile); + mVpnProfiles.add(profile); + mListAdapter.notifyDataSetChanged(); + } + } + else if ((ids = intent.getLongArrayExtra(Constants.VPN_PROFILES_MULTIPLE)) != null) + { + for (long i : ids) + { + Iterator profiles = mVpnProfiles.iterator(); + while (profiles.hasNext()) + { + VpnProfile profile = profiles.next(); + if (profile.getId() == i) + { + profiles.remove(); + break; + } + } + } + mListAdapter.notifyDataSetChanged(); + } + } + }; + /** * The activity containing this fragment should implement this interface */ public interface OnVpnProfileSelectedListener { - public void onVpnProfileSelected(VpnProfile profile); + void onVpnProfileSelected(VpnProfile profile); } @Override @@ -116,6 +156,9 @@ public class VpnProfileListFragment extends Fragment mVpnProfiles = mDataSource.getAllVpnProfiles(); mListAdapter = new VpnProfileAdapter(getActivity(), R.layout.profile_list_item, mVpnProfiles); + + IntentFilter profileChangesFilter = new IntentFilter(Constants.VPN_PROFILES_CHANGED); + LocalBroadcastManager.getInstance(getActivity()).registerReceiver(mProfilesChanged, profileChangesFilter); } @Override @@ -123,6 +166,7 @@ public class VpnProfileListFragment extends Fragment { super.onDestroy(); mDataSource.close(); + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(mProfilesChanged); } @Override @@ -157,30 +201,6 @@ public class VpnProfileListFragment extends Fragment } } - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) - { - switch (requestCode) - { - case ADD_REQUEST: - case EDIT_REQUEST: - if (resultCode != Activity.RESULT_OK) - { - return; - } - long id = data.getLongExtra(VpnProfileDataSource.KEY_ID, 0); - VpnProfile profile = mDataSource.getVpnProfile(id); - if (profile != null) - { /* in case this was an edit, we remove it first */ - mVpnProfiles.remove(profile); - mVpnProfiles.add(profile); - mListAdapter.notifyDataSetChanged(); - } - return; - } - super.onActivityResult(requestCode, resultCode, data); - } - private final OnItemClickListener mVpnProfileClicked = new OnItemClickListener() { @Override public void onItemClick(AdapterView a, View v, int position, long id) @@ -213,7 +233,7 @@ public class VpnProfileListFragment extends Fragment MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.profile_list_context, menu); mEditProfile = menu.findItem(R.id.edit_profile); - mSelected = new HashSet(); + mSelected = new HashSet<>(); mode.setTitle(R.string.select_profiles); return true; } @@ -234,17 +254,21 @@ public class VpnProfileListFragment extends Fragment } case R.id.delete_profile: { - ArrayList profiles = new ArrayList(); + ArrayList profiles = new ArrayList<>(); for (int position : mSelected) { profiles.add((VpnProfile)mListView.getItemAtPosition(position)); } - for (VpnProfile profile : profiles) + long ids[] = new long[profiles.size()]; + for (int i = 0; i < profiles.size(); i++) { + VpnProfile profile = profiles.get(i); + ids[i] = profile.getId(); mDataSource.deleteVpnProfile(profile); - mVpnProfiles.remove(profile); } - mListAdapter.notifyDataSetChanged(); + Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); + intent.putExtra(Constants.VPN_PROFILES_MULTIPLE, ids); + LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); Toast.makeText(VpnProfileListFragment.this.getActivity(), R.string.profiles_deleted, Toast.LENGTH_SHORT).show(); break; diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java new file mode 100644 index 000000000..413ecae97 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Constants.java @@ -0,0 +1,40 @@ +/* + * 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 . + * + * 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.utils; + +public final class Constants +{ + /** + * Intent action used to notify about changes to the VPN profiles + */ + public static final String VPN_PROFILES_CHANGED = "org.strongswan.android.VPN_PROFILES_CHANGED"; + + /** + * Used in the intent above to notify about edits or inserts of a VPN profile (long) + */ + public static final String VPN_PROFILES_SINGLE = "org.strongswan.android.VPN_PROFILES_SINGLE"; + + /** + * Used in the intent above to notify about the deletion of multiple VPN profiles (array of longs) + */ + public static final String VPN_PROFILES_MULTIPLE = "org.strongswan.android.VPN_PROFILES_MULTIPLE"; + + /** + * Limits for MTU + */ + public static final int MTU_MAX = 1500; + public static final int MTU_MIN = 1280; +} From 3107634e305b37a5bfa7d13e09409879358b9b9c Mon Sep 17 00:00:00 2001 From: Tobias Brunner Date: Thu, 29 Dec 2016 17:35:57 +0100 Subject: [PATCH 3/6] android: Add activity to import VPN profiles from JSON-encoded files 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). --- .../android/app/src/main/AndroidManifest.xml | 28 + .../android/ui/VpnProfileImportActivity.java | 770 ++++++++++++++++++ .../res/drawable-hdpi/ic_close_white_24dp.png | Bin 0 -> 221 bytes .../res/drawable-mdpi/ic_close_white_24dp.png | Bin 0 -> 175 bytes .../drawable-xhdpi/ic_close_white_24dp.png | Bin 0 -> 257 bytes .../main/res/layout/profile_import_view.xml | 196 +++++ .../app/src/main/res/menu/profile_import.xml | 24 + .../app/src/main/res/values-de/strings.xml | 7 + .../app/src/main/res/values-pl/strings.xml | 7 + .../app/src/main/res/values-ru/strings.xml | 7 + .../app/src/main/res/values-ua/strings.xml | 7 + .../app/src/main/res/values/strings.xml | 7 + 12 files changed, 1053 insertions(+) create mode 100644 src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileImportActivity.java create mode 100644 src/frontends/android/app/src/main/res/drawable-hdpi/ic_close_white_24dp.png create mode 100644 src/frontends/android/app/src/main/res/drawable-mdpi/ic_close_white_24dp.png create mode 100644 src/frontends/android/app/src/main/res/drawable-xhdpi/ic_close_white_24dp.png create mode 100644 src/frontends/android/app/src/main/res/layout/profile_import_view.xml create mode 100644 src/frontends/android/app/src/main/res/menu/profile_import.xml diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml index da465ba74..a0c79875f 100644 --- a/src/frontends/android/app/src/main/AndroidManifest.xml +++ b/src/frontends/android/app/src/main/AndroidManifest.xml @@ -20,6 +20,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + . + * + * 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 mProfileLoaderCallbacks = new LoaderManager.LoaderCallbacks() + { + @Override + public Loader onCreateLoader(int id, Bundle args) + { + return new ProfileLoader(VpnProfileImportActivity.this, getIntent().getData()); + } + + @Override + public void onLoadFinished(Loader loader, String data) + { + handleProfile(data); + } + + @Override + public void onLoaderReset(Loader loader) + { + + } + }; + + private LoaderManager.LoaderCallbacks mUserCertificateLoaderCallbacks = new LoaderManager.LoaderCallbacks() + { + @Override + public Loader onCreateLoader(int id, Bundle args) + { + return new UserCertificateLoader(VpnProfileImportActivity.this, mUserCertLoading); + } + + @Override + public void onLoadFinished(Loader loader, TrustedCertificateEntry data) + { + handleUserCertificate(data); + } + + @Override + public void onLoaderReset(Loader 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 + { + 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 + { + 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 index 0000000000000000000000000000000000000000..ceb1a1eebf2b2cc9a008f42010e144f4dab968de GIT binary patch literal 221 zcmV<303!d1P)og+*{ z>6z1@lfD*AYSPav72B5kK^|J))MUC zlz7ydwCsb8xn}W?^r?}5wtsjdmvX{3=JbT!J9sKPrYmk={m|7^>A|Z*7j^D$`>}ry Zqi^Z9GQN4ct^!@a;OXk;vd$@?2>?I&LQeny literal 0 HcmV?d00001 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 index 0000000000000000000000000000000000000000..b7c7ffd0e795ba76ed3a062566c9016448795f7a GIT binary patch literal 257 zcmV+c0sj7pP) zOA5m<3jYBNi%A&<@ZO?$P9};P4Y5CjG5$M&YXI45J{s}~# zf|&?x1_gn4B7+hS@X!l}&!voFhmZP^sujifL@~PKMMM~{6xH}^g$q7WOzwCQ5vHTU z6`v~H@rlA8e;CUh_(b84zg=+ih`wG<)HiJjzSlQx5#CnjMR;A)R^jtaTa9;7rSy)7O%~`cm?ZjXImW?6TYRT<;U^@VKiSj`soFk00000NkvXX Hu0mjfhD&W| literal 0 HcmV?d00001 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 index 000000000..fc06aa5d4 --- /dev/null +++ b/src/frontends/android/app/src/main/res/layout/profile_import_view.xml @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +