diff --git a/.gitignore b/.gitignore index fc41250c4..067431411 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ fuzzing-corpora/ *.tar.bz2 *.tar.gz .DS_Store +._.DS_Store coverage/ *.gcno *.gcda diff --git a/src/frontends/android/app/build.gradle b/src/frontends/android/app/build.gradle index 7aa4d8454..44b9695a3 100644 --- a/src/frontends/android/app/build.gradle +++ b/src/frontends/android/app/build.gradle @@ -8,8 +8,9 @@ android { compileSdk 34 minSdkVersion 21 targetSdkVersion 33 - versionCode 80 - versionName "2.4.2" + + versionCode 81 + versionName "2.5.0" externalNativeBuild { ndkBuild { @@ -45,9 +46,10 @@ android { dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.lifecycle:lifecycle-process:2.7.0' implementation 'androidx.preference:preference:1.2.1' - implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'com.google.android.material:material:1.10.0' testImplementation 'junit:junit:4.13.2' + testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'org.mockito:mockito-core:5.8.0' } diff --git a/src/frontends/android/app/src/main/AndroidManifest.xml b/src/frontends/android/app/src/main/AndroidManifest.xml index 89e3fb110..ec4bf65cf 100644 --- a/src/frontends/android/app/src/main/AndroidManifest.xml +++ b/src/frontends/android/app/src/main/AndroidManifest.xml @@ -161,9 +161,17 @@ + + + + android:exported="false"> . + * + * 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.data; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.database.sqlite.SQLiteQueryBuilder; +import android.util.Log; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +public class DatabaseHelper extends SQLiteOpenHelper +{ + private static final String TAG = DatabaseHelper.class.getSimpleName(); + + private static final String DATABASE_NAME = "strongswan.db"; + + private static final String TABLE_NAME_VPN_PROFILE = "vpnprofile"; + private static final String TABLE_NAME_TRUSTED_CERTIFICATE = "trustedcertificate"; + private static final String TABLE_NAME_USER_CERTIFICATE = "usercertificate"; + + static final DbTable TABLE_VPN_PROFILE = new DbTable(TABLE_NAME_VPN_PROFILE, 1, new DbColumn[]{ + new DbColumn(VpnProfileDataSource.KEY_ID, "INTEGER PRIMARY KEY AUTOINCREMENT", 1), + new DbColumn(VpnProfileDataSource.KEY_UUID, "TEXT UNIQUE", 9), + new DbColumn(VpnProfileDataSource.KEY_NAME, "TEXT NOT NULL", 1), + new DbColumn(VpnProfileDataSource.KEY_GATEWAY, "TEXT NOT NULL", 1), + new DbColumn(VpnProfileDataSource.KEY_VPN_TYPE, "TEXT NOT NULL DEFAULT ''", 3), + new DbColumn(VpnProfileDataSource.KEY_USERNAME, "TEXT", 1), + new DbColumn(VpnProfileDataSource.KEY_PASSWORD, "TEXT", 1), + new DbColumn(VpnProfileDataSource.KEY_CERTIFICATE, "TEXT", 1), + new DbColumn(VpnProfileDataSource.KEY_USER_CERTIFICATE, "TEXT", 2), + new DbColumn(VpnProfileDataSource.KEY_MTU, "INTEGER", 5), + new DbColumn(VpnProfileDataSource.KEY_PORT, "INTEGER", 6), + new DbColumn(VpnProfileDataSource.KEY_SPLIT_TUNNELING, "INTEGER", 7), + new DbColumn(VpnProfileDataSource.KEY_LOCAL_ID, "TEXT", 8), + new DbColumn(VpnProfileDataSource.KEY_REMOTE_ID, "TEXT", 8), + new DbColumn(VpnProfileDataSource.KEY_EXCLUDED_SUBNETS, "TEXT", 10), + new DbColumn(VpnProfileDataSource.KEY_INCLUDED_SUBNETS, "TEXT", 11), + new DbColumn(VpnProfileDataSource.KEY_SELECTED_APPS, "INTEGER", 12), + new DbColumn(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, "TEXT", 12), + new DbColumn(VpnProfileDataSource.KEY_NAT_KEEPALIVE, "INTEGER", 13), + new DbColumn(VpnProfileDataSource.KEY_FLAGS, "INTEGER", 14), + new DbColumn(VpnProfileDataSource.KEY_IKE_PROPOSAL, "TEXT", 15), + new DbColumn(VpnProfileDataSource.KEY_ESP_PROPOSAL, "TEXT", 15), + new DbColumn(VpnProfileDataSource.KEY_DNS_SERVERS, "TEXT", 17), + }); + + public static final DbTable TABLE_TRUSTED_CERTIFICATE = new DbTable(TABLE_NAME_TRUSTED_CERTIFICATE, 18, new DbColumn[]{ + new DbColumn(ManagedCertificate.KEY_ID, "INTEGER PRIMARY KEY AUTOINCREMENT", 18), + new DbColumn(ManagedCertificate.KEY_VPN_PROFILE_UUID, "TEXT UNIQUE", 18), + new DbColumn(ManagedCertificate.KEY_ALIAS, "TEXT NOT NULL", 18), + new DbColumn(ManagedCertificate.KEY_DATA, "TEXT NOT NULL", 18), + }); + + public static final DbTable TABLE_USER_CERTIFICATE = new DbTable(TABLE_NAME_USER_CERTIFICATE, 18, new DbColumn[]{ + new DbColumn(ManagedCertificate.KEY_ID, "INTEGER PRIMARY KEY AUTOINCREMENT", 18), + new DbColumn(ManagedCertificate.KEY_VPN_PROFILE_UUID, "TEXT UNIQUE", 18), + new DbColumn(ManagedCertificate.KEY_ALIAS, "TEXT NOT NULL", 18), + new DbColumn(ManagedCertificate.KEY_DATA, "TEXT NOT NULL", 18), + new DbColumn(ManagedUserCertificate.KEY_PASSWORD, "TEXT", 18), + }); + + private static final int DATABASE_VERSION = 18; + + private static final Set TABLES; + + static + { + TABLES = new HashSet<>(); + TABLES.add(TABLE_VPN_PROFILE); + TABLES.add(TABLE_TRUSTED_CERTIFICATE); + TABLES.add(TABLE_USER_CERTIFICATE); + } + + public DatabaseHelper(Context context) + { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase database) + { + addNewTables(database, 0); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) + { + Log.w(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); + addNewTables(db, oldVersion); + addNewColumns(db, oldVersion); + + if (oldVersion < 9) + { + updateColumns(db, TABLE_VPN_PROFILE); + } + if (oldVersion < 16) + { /* add a UUID to all entries that haven't one yet */ + db.beginTransaction(); + try + { + Cursor cursor = db.query(TABLE_VPN_PROFILE.Name, TABLE_VPN_PROFILE.columnNames(), VpnProfileDataSource.KEY_UUID + " is NULL", null, null, null, null); + for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) + { + ContentValues values = new ContentValues(); + values.put(VpnProfileDataSource.KEY_UUID, UUID.randomUUID().toString()); + db.update(TABLE_VPN_PROFILE.Name, values, VpnProfileDataSource.KEY_ID + " = " + cursor.getLong(cursor.getColumnIndexOrThrow(VpnProfileDataSource.KEY_ID)), null); + } + cursor.close(); + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + } + } + + private void updateColumns(SQLiteDatabase db, DbTable table) + { + db.beginTransaction(); + try + { + db.execSQL("ALTER TABLE " + table.Name + " RENAME TO tmp_" + table.Name + ";"); + db.execSQL(getTableCreate(table)); + StringBuilder insert = new StringBuilder("INSERT INTO " + table.Name + " SELECT "); + SQLiteQueryBuilder.appendColumns(insert, table.columnNames()); + db.execSQL(insert.append(" FROM tmp_" + table.Name + ";").toString()); + db.execSQL("DROP TABLE tmp_" + table.Name + ";"); + db.setTransactionSuccessful(); + } + finally + { + db.endTransaction(); + } + } + + private static String getTableCreate(DbTable table) + { + boolean first = true; + StringBuilder create = new StringBuilder("CREATE TABLE IF NOT EXISTS "); + create.append(table.Name); + create.append(" ("); + + for (final DbColumn column : table.getColumns()) + { + if (!first) + { + create.append(","); + } + first = false; + create.append(column.Name); + create.append(" "); + create.append(column.Type); + } + create.append(");"); + return create.toString(); + } + + private void addNewTables(final SQLiteDatabase database, final int oldVersion) + { + for (final String sql : getTableCreates(oldVersion)) + { + database.execSQL(sql); + } + } + + private List getTableCreates(final int oldVersion) + { + List statements = new ArrayList<>(TABLES.size()); + for (final DbTable table : TABLES) + { + if (table.Since > oldVersion) + { + statements.add(getTableCreate(table)); + } + } + return statements; + } + + private void addNewColumns(final SQLiteDatabase database, final int oldVersion) + { + for (final String sql : getAlterTables(oldVersion)) + { + database.execSQL(sql); + } + } + + private List getAlterTables(final int oldVersion) + { + List statements = new ArrayList<>(TABLES.size()); + for (final DbTable table : TABLES) + { + statements.addAll(getAlterTables(table, oldVersion)); + } + return statements; + } + + private static List getAlterTables(DbTable table, final int oldVersion) + { + final List sql = new ArrayList<>(); + + for (final DbColumn column : table.getColumns()) + { + if (column.Since > table.Since && column.Since > oldVersion) + { + sql.add("ALTER TABLE " + table.Name + " ADD " + column.Name + " " + column.Type + ";"); + } + } + return sql; + } + + public static class DbTable + { + public final String Name; + public final int Since; + public final DbColumn[] Columns; + + private DbTable(final String name, final int since, final DbColumn[] columns) + { + Name = name; + Since = since; + Columns = columns; + } + + private DbColumn[] getColumns() + { + return Columns; + } + + public String[] columnNames() + { + final List columnNames = new ArrayList<>(Columns.length); + for (DbColumn column : Columns) + { + columnNames.add(column.Name); + } + return columnNames.toArray(new String[0]); + } + } + + public static class DbColumn + { + public final String Name; + public final String Type; + public final int Since; + + private DbColumn(String name, String type, int since) + { + Name = name; + Type = type; + Since = since; + } + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedCertificate.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedCertificate.java new file mode 100644 index 000000000..df1b4eac1 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedCertificate.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.content.ContentValues; +import android.database.Cursor; + +import androidx.annotation.NonNull; + +public abstract class ManagedCertificate +{ + public static final String KEY_ID = "_id"; + public static final String KEY_VPN_PROFILE_UUID = "vpn_profile_uuid"; + public static final String KEY_ALIAS = "alias"; + public static final String KEY_DATA = "data"; + + long id = -1; + + @NonNull + final String vpnProfileUuid; + + @NonNull + String alias; + + @NonNull + final String data; + + ManagedCertificate( + @NonNull final String vpnProfileUuid, + @NonNull final String alias, + @NonNull final String data) + { + this.vpnProfileUuid = vpnProfileUuid; + this.alias = alias; + this.data = data; + } + + ManagedCertificate(@NonNull final Cursor cursor) + { + id = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_ID)); + vpnProfileUuid = cursor.getString(cursor.getColumnIndexOrThrow(KEY_VPN_PROFILE_UUID)); + alias = cursor.getString(cursor.getColumnIndexOrThrow(KEY_ALIAS)); + data = cursor.getString(cursor.getColumnIndexOrThrow(KEY_DATA)); + } + + @NonNull + public ContentValues asContentValues() + { + final ContentValues values = new ContentValues(); + values.put(KEY_VPN_PROFILE_UUID, vpnProfileUuid); + values.put(KEY_ALIAS, alias); + values.put(KEY_DATA, data); + return values; + } + + public long getId() + { + return id; + } + + public void setId(long id) + { + this.id = id; + } + + @NonNull + public String getVpnProfileUuid() + { + return vpnProfileUuid; + } + + @NonNull + public String getAlias() + { + return alias; + } + + @NonNull + public String getData() + { + return data; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedCertificateRepository.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedCertificateRepository.java new file mode 100644 index 000000000..00f30b460 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedCertificateRepository.java @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public abstract class ManagedCertificateRepository +{ + @NonNull + private final ManagedConfigurationService managedConfigurationService; + + @NonNull + private final SQLiteDatabase database; + @NonNull + private final DatabaseHelper.DbTable table; + + protected ManagedCertificateRepository( + @NonNull final ManagedConfigurationService managedConfigurationService, + @NonNull final DatabaseHelper databaseHelper, + @NonNull final DatabaseHelper.DbTable table) + { + this.managedConfigurationService = managedConfigurationService; + + this.database = databaseHelper.getReadableDatabase(); + this.table = table; + } + + @Nullable + protected abstract T getCertificate(@NonNull final ManagedVpnProfile vpnProfile); + + @NonNull + protected abstract T createCertificate(@NonNull Cursor cursor); + + protected abstract boolean isInstalled(@NonNull T certificate); + + private boolean exists(@NonNull T certificate) + { + final String vpnProfileUuid = certificate.getVpnProfileUuid(); + try (final Cursor cursor = database.query(table.Name, table.columnNames(), ManagedCertificate.KEY_VPN_PROFILE_UUID + " = ?", new String[]{vpnProfileUuid}, null, null, null)) + { + cursor.moveToFirst(); + if (!cursor.isAfterLast()) + { + return true; + } + } + return false; + } + + @NonNull + public List getConfiguredCertificates() + { + managedConfigurationService.loadConfiguration(); + + final Map managedVpnProfiles = managedConfigurationService.getManagedProfiles(); + final List certificates = new ArrayList<>(managedVpnProfiles.size()); + + for (final ManagedVpnProfile vpnProfile : managedVpnProfiles.values()) + { + final T certificate = getCertificate(vpnProfile); + if (certificate != null) + { + certificates.add(certificate); + } + } + + return certificates; + } + + /** + * @return the collection of certificates that were previously installed. + * @see #addInstalledCertificate(ManagedCertificate) + */ + @NonNull + private List getCertificates() + { + try (final Cursor cursor = database.query(table.Name, table.columnNames(), null, null, null, null, null)) + { + final List certificates = new ArrayList<>(); + + cursor.moveToFirst(); + while (!cursor.isAfterLast()) + { + final T certificate = createCertificate(cursor); + certificates.add(certificate); + cursor.moveToNext(); + } + return certificates; + } + } + + /** + * Returns the collection of certificates that were previously marked as installed and are still + * reported as installed by the OS. + * + * @return the collection of installed certificates. + * @see #addInstalledCertificate(ManagedCertificate) + */ + @NonNull + public List getInstalledCertificates() + { + final List certificates = getCertificates(); + final List installed = new ArrayList<>(certificates.size()); + + for (final T certificate : certificates) + { + if (isInstalled(certificate)) + { + installed.add(certificate); + } + } + + return installed; + } + + /** + * Returns a map containing certificates previously marked as installed, indexed by the + * unique identifier of the VPN profile they are associated with. + * + * @return a map containing installed certificates, index by the VPN profile's unique + * identifier. + */ + @NonNull + public Map getCertificateMap() + { + final List certificates = getCertificates(); + final Map map = new HashMap<>(certificates.size()); + + for (final T certificate : certificates) + { + map.put(certificate.getVpnProfileUuid(), certificate); + } + + return map; + } + + public void addInstalledCertificate(@NonNull final T certificate) + { + final ContentValues values = certificate.asContentValues(); + + if (exists(certificate)) + { + final String vpnProfileUuid = certificate.getVpnProfileUuid(); + database.update(table.Name, values, ManagedCertificate.KEY_VPN_PROFILE_UUID + " = ?", new String[]{vpnProfileUuid}); + } + else + { + database.insert(table.Name, null, values); + } + } + + public void removeInstalledCertificate(@NonNull final T certificate) + { + final String vpnProfileUuid = certificate.getVpnProfileUuid(); + database.delete(table.Name, ManagedCertificate.KEY_VPN_PROFILE_UUID + " = ?", new String[]{vpnProfileUuid}); + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedConfiguration.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedConfiguration.java new file mode 100644 index 000000000..a429f5fc8 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedConfiguration.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2024 Tobias Brunner + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.os.Build; +import android.os.Bundle; +import android.os.Parcelable; + +import org.strongswan.android.utils.Constants; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import androidx.annotation.NonNull; + +public class ManagedConfiguration +{ + private static final String KEY_ALLOW_PROFILE_CREATE = "allow_profile_create"; + private static final String KEY_ALLOW_PROFILE_IMPORT = "allow_profile_import"; + private static final String KEY_ALLOW_EXISTING_PROFILES = "allow_existing_profiles"; + private static final String KEY_ALLOW_CERTIFICATE_IMPORT = "allow_certificate_import"; + private static final String KEY_ALLOW_SETTINGS_ACCESS = "allow_settings_access"; + private static final String KEY_MANAGED_PROFILES = "managed_profiles"; + + private final boolean mAllowProfileCreation; + private final boolean mAllowProfileImport; + private final boolean mAllowExistingProfiles; + private final boolean mAllowCertificateImport; + + private final boolean mAllowSettingsAccess; + private final String mDefaultVpnProfile; + private final boolean mIgnoreBatteryOptimizations; + + private final Map mManagedVpnProfiles; + + ManagedConfiguration() + { + mAllowProfileCreation = true; + mAllowProfileImport = true; + mAllowExistingProfiles = true; + mAllowCertificateImport = true; + + mAllowSettingsAccess = true; + mDefaultVpnProfile = null; + mIgnoreBatteryOptimizations = false; + + mManagedVpnProfiles = Collections.emptyMap(); + } + + ManagedConfiguration(final Bundle bundle) + { + mAllowProfileCreation = bundle.getBoolean(KEY_ALLOW_PROFILE_CREATE, true); + mAllowProfileImport = bundle.getBoolean(KEY_ALLOW_PROFILE_IMPORT, true); + mAllowExistingProfiles = bundle.getBoolean(KEY_ALLOW_EXISTING_PROFILES, true); + mAllowCertificateImport = bundle.getBoolean(KEY_ALLOW_CERTIFICATE_IMPORT, true); + + mAllowSettingsAccess = bundle.getBoolean(KEY_ALLOW_SETTINGS_ACCESS, true); + mDefaultVpnProfile = bundle.getString(Constants.PREF_DEFAULT_VPN_PROFILE, null); + mIgnoreBatteryOptimizations = bundle.getBoolean(Constants.PREF_IGNORE_POWER_WHITELIST, false); + + final List managedProfileBundles = getBundleArrayList(bundle, KEY_MANAGED_PROFILES); + mManagedVpnProfiles = new HashMap<>(managedProfileBundles.size()); + + for (final Bundle managedProfileBundle : managedProfileBundles) + { + addManagedProfile(managedProfileBundle); + } + } + + private void addManagedProfile(Bundle managedProfileBundle) + { + UUID uuid; + try + { + uuid = UUID.fromString(managedProfileBundle.getString(VpnProfileDataSource.KEY_UUID)); + } + catch (IllegalArgumentException e) + { + return; + } + if (mManagedVpnProfiles.containsKey(uuid.toString())) + { + return; + } + + final ManagedVpnProfile vpnProfile = new ManagedVpnProfile(managedProfileBundle, uuid); + mManagedVpnProfiles.put(uuid.toString(), vpnProfile); + } + + private List getBundleArrayList(final Bundle bundle, final String key) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) + { + return getBundleArrayListCompat(bundle, key); + } + + final Bundle[] bundles = bundle.getParcelableArray(key, Bundle.class); + if (bundles == null) + { + return Collections.emptyList(); + } + return Arrays.asList(bundles); + } + + @NonNull + private static List getBundleArrayListCompat(final Bundle bundle, final String key) + { + final Parcelable[] parcelables = bundle.getParcelableArray(key); + if (parcelables == null) + { + return Collections.emptyList(); + } + final Bundle[] bundles = Arrays.copyOf(parcelables, parcelables.length, Bundle[].class); + return Arrays.asList(bundles); + } + + public boolean isAllowProfileCreation() + { + return mAllowProfileCreation; + } + + public boolean isAllowProfileImport() + { + return mAllowProfileImport; + } + + public boolean isAllowExistingProfiles() + { + return mAllowExistingProfiles; + } + + public boolean isAllowCertificateImport() + { + return mAllowCertificateImport; + } + + public boolean isAllowSettingsAccess() + { + return mAllowSettingsAccess; + } + + public String getDefaultVpnProfile() + { + if (mDefaultVpnProfile != null && mDefaultVpnProfile.equalsIgnoreCase("mru")) + { + return Constants.PREF_DEFAULT_VPN_PROFILE_MRU; + } + return mDefaultVpnProfile; + } + + public boolean isIgnoreBatteryOptimizations() + { + return mIgnoreBatteryOptimizations; + } + + public Map getVpnProfiles() + { + return mManagedVpnProfiles; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedConfigurationService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedConfigurationService.java new file mode 100644 index 000000000..ac8e73d9b --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedConfigurationService.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.content.Context; +import android.content.RestrictionsManager; +import android.content.SharedPreferences; +import android.os.Build; +import android.os.Bundle; + +import org.strongswan.android.utils.Constants; + +import java.util.Collections; +import java.util.Map; +import java.util.UUID; + +import androidx.preference.PreferenceManager; + +public class ManagedConfigurationService +{ + private final Context mContext; + + private ManagedConfiguration mManagedConfiguration = new ManagedConfiguration(); + private Map mManagedVpnProfiles = Collections.emptyMap(); + + public ManagedConfigurationService(final Context context) + { + this.mContext = context; + } + + public void loadConfiguration() + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) + { + return; + } + + final RestrictionsManager restrictionsService = mContext.getSystemService(RestrictionsManager.class); + if (restrictionsService == null) + { + return; + } + + final Bundle configuration = restrictionsService.getApplicationRestrictions(); + if (configuration == null) + { + return; + } + + final ManagedConfiguration managedConfiguration = new ManagedConfiguration(configuration); + mManagedConfiguration = managedConfiguration; + mManagedVpnProfiles = managedConfiguration.getVpnProfiles(); + } + + public void updateSettings() + { + if (!mManagedConfiguration.isAllowSettingsAccess()) + { + final SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(mContext); + final SharedPreferences.Editor editor = pref.edit(); + editor.putBoolean(Constants.PREF_IGNORE_POWER_WHITELIST, mManagedConfiguration.isIgnoreBatteryOptimizations()); + editor.putString(Constants.PREF_DEFAULT_VPN_PROFILE, mManagedConfiguration.getDefaultVpnProfile()); + editor.apply(); + } + } + + public ManagedConfiguration getManagedConfiguration() + { + return mManagedConfiguration; + } + + public Map getManagedProfiles() + { + return mManagedVpnProfiles; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedTrustedCertificate.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedTrustedCertificate.java new file mode 100644 index 000000000..8ccd4021d --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedTrustedCertificate.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.database.Cursor; + +import org.strongswan.android.utils.Certificates; + +import java.security.KeyStore; +import java.security.cert.X509Certificate; +import java.util.Objects; + +import androidx.annotation.NonNull; + +public class ManagedTrustedCertificate extends ManagedCertificate +{ + public ManagedTrustedCertificate( + @NonNull final String vpnProfileUuid, + @NonNull final String data) + { + super(vpnProfileUuid, determineAlias(vpnProfileUuid, data), data); + } + + public ManagedTrustedCertificate(@NonNull final Cursor cursor) + { + super(cursor); + } + + private static String determineAlias(String vpnProfileUuid, String data) + { + /* fallback in case the certificate is invalid */ + String certAlias = "trusted:" + vpnProfileUuid; + try + { + X509Certificate cert = Certificates.from(data); + KeyStore store = KeyStore.getInstance("LocalCertificateStore"); + store.load(null, null); + certAlias = store.getCertificateAlias(cert); + } + catch (Exception e) + { + e.printStackTrace(); + } + return certAlias; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + ManagedTrustedCertificate that = (ManagedTrustedCertificate)o; + return Objects.equals(vpnProfileUuid, that.vpnProfileUuid) && + Objects.equals(data, that.data); + } + + @Override + public int hashCode() + { + return Objects.hash(vpnProfileUuid, data); + } + + @NonNull + @Override + public String toString() + { + return "ManagedTrustedCertificate {" + vpnProfileUuid + ", " + alias + "}"; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedTrustedCertificateRepository.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedTrustedCertificateRepository.java new file mode 100644 index 000000000..e261c4284 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedTrustedCertificateRepository.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.database.Cursor; + +import org.strongswan.android.logic.TrustedCertificateManager; + +import java.security.cert.X509Certificate; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ManagedTrustedCertificateRepository extends ManagedCertificateRepository +{ + private static final DatabaseHelper.DbTable TABLE = DatabaseHelper.TABLE_TRUSTED_CERTIFICATE; + + public ManagedTrustedCertificateRepository( + @NonNull final ManagedConfigurationService managedConfigurationService, + @NonNull final DatabaseHelper databaseHelper) + { + super(managedConfigurationService, databaseHelper, TABLE); + } + + @Nullable + @Override + protected ManagedTrustedCertificate getCertificate(@NonNull ManagedVpnProfile vpnProfile) + { + return vpnProfile.getTrustedCertificate(); + } + + @NonNull + @Override + protected ManagedTrustedCertificate createCertificate(@NonNull Cursor cursor) + { + return new ManagedTrustedCertificate(cursor); + } + + @Override + protected boolean isInstalled(@NonNull ManagedTrustedCertificate certificate) + { + TrustedCertificateManager certificateManager = TrustedCertificateManager.getInstance(); + final X509Certificate x509Certificate = certificateManager.getCACertificateFromAlias(certificate.getAlias()); + + return x509Certificate != null; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedUserCertificate.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedUserCertificate.java new file mode 100644 index 000000000..cad28e884 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedUserCertificate.java @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.content.ContentValues; +import android.database.Cursor; + +import java.util.Objects; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ManagedUserCertificate extends ManagedCertificate +{ + public static final String KEY_PASSWORD = "password"; + + private final String privateKeyPassword; + + public ManagedUserCertificate( + @NonNull final String vpnProfileUuid, + @NonNull final String data, + @Nullable final String password) + { + super(vpnProfileUuid, "user:" + vpnProfileUuid, data); + privateKeyPassword = password; + } + + public ManagedUserCertificate(@NonNull final Cursor cursor) + { + super(cursor); + privateKeyPassword = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PASSWORD)); + } + + @NonNull + @Override + public ContentValues asContentValues() + { + final ContentValues values = super.asContentValues(); + values.put(KEY_PASSWORD, privateKeyPassword); + return values; + } + + @Nullable + public String getPrivateKeyPassword() + { + return privateKeyPassword; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + ManagedUserCertificate that = (ManagedUserCertificate)o; + return Objects.equals(vpnProfileUuid, that.vpnProfileUuid) && + Objects.equals(data, that.data) && + Objects.equals(privateKeyPassword, that.privateKeyPassword); + } + + @Override + public int hashCode() + { + return Objects.hash(vpnProfileUuid, data); + } + + @NonNull + @Override + public String toString() + { + return "ManagedUserCertificate {" + vpnProfileUuid + ", " + alias + "}"; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedUserCertificateRepository.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedUserCertificateRepository.java new file mode 100644 index 000000000..55804f1e2 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedUserCertificateRepository.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.app.admin.DevicePolicyManager; +import android.database.Cursor; +import android.os.Build; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ManagedUserCertificateRepository extends ManagedCertificateRepository +{ + private static final DatabaseHelper.DbTable TABLE = DatabaseHelper.TABLE_USER_CERTIFICATE; + + @NonNull + private final DevicePolicyManager devicePolicyManager; + + public ManagedUserCertificateRepository( + @NonNull final ManagedConfigurationService managedConfigurationService, + @NonNull final DevicePolicyManager devicePolicyManager, + @NonNull final DatabaseHelper databaseHelper) + { + super(managedConfigurationService, databaseHelper, TABLE); + this.devicePolicyManager = devicePolicyManager; + } + + @Nullable + @Override + protected ManagedUserCertificate getCertificate(@NonNull ManagedVpnProfile vpnProfile) + { + return vpnProfile.getUserCertificate(); + } + + @NonNull + @Override + protected ManagedUserCertificate createCertificate(@NonNull Cursor cursor) + { + return new ManagedUserCertificate(cursor); + } + + @Override + protected boolean isInstalled(@NonNull ManagedUserCertificate certificate) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + { + return devicePolicyManager.hasKeyPair(certificate.getAlias()); + } + /* We don't know, so we assume a certificate we installed may have been removed by the + * user, so we install it again to make sure it's still there */ + return false; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedVpnProfile.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedVpnProfile.java new file mode 100644 index 000000000..054dde19f --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/ManagedVpnProfile.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.os.Bundle; +import android.text.TextUtils; + +import org.strongswan.android.utils.Constants; + +import java.util.Objects; +import java.util.UUID; + +import androidx.annotation.Nullable; + +public class ManagedVpnProfile extends VpnProfile +{ + private static final String KEY_REMOTE = "remote"; + private static final String KEY_LOCAL = "local"; + private static final String KEY_INCLUDED_APPS = "included_apps"; + private static final String KEY_EXCLUDED_APPS = "excluded_apps"; + + private static final String KEY_TRANSPORT_IPV6_FLAG = "transport_ipv6"; + private static final String KEY_REMOTE_CERT_REQ_FLAG = "remote_cert_req"; + private static final String KEY_REMOTE_REVOCATION_CRL_FLAG = "remote_revocation_crl"; + private static final String KEY_REMOTE_REVOCATION_OCSP_FLAG = "remote_revocation_ocsp"; + private static final String KEY_REMOTE_REVOCATION_STRICT_FLAG = "remote_revocation_strict"; + private static final String KEY_LOCAL_RSA_PSS_FLAG = "local_rsa_pss"; + + private static final String KEY_SPLIT_TUNNELLING_BLOCK_IPV4_FLAG = "split_tunnelling_block_ipv4"; + private static final String KEY_SPLIT_TUNNELLING_BLOCK_IPV6_FLAG = "split_tunnelling_block_ipv6"; + + private ManagedTrustedCertificate trustedCertificate; + private ManagedUserCertificate userCertificate; + + ManagedVpnProfile(final Bundle bundle, final UUID uuid) + { + int flags = 0; + int splitFlags = 0; + + setReadOnly(true); + setUUID(uuid); + setName(bundle.getString(VpnProfileDataSource.KEY_NAME)); + setVpnType(VpnType.fromIdentifier(bundle.getString(VpnProfileDataSource.KEY_VPN_TYPE))); + + final Bundle remote = bundle.getBundle(KEY_REMOTE); + flags = configureRemote(uuid, remote, flags); + + final Bundle local = bundle.getBundle(KEY_LOCAL); + flags = configureLocal(uuid, local, flags); + + final String includedPackageNames = bundle.getString(KEY_INCLUDED_APPS); + final String excludedPackageNames = bundle.getString(KEY_EXCLUDED_APPS); + configureSelectedApps(includedPackageNames, excludedPackageNames); + + setMTU(getInt(bundle, VpnProfileDataSource.KEY_MTU, Constants.MTU_MIN, Constants.MTU_MAX)); + setNATKeepAlive(getInt(bundle, VpnProfileDataSource.KEY_NAT_KEEPALIVE, Constants.NAT_KEEPALIVE_MIN, Constants.NAT_KEEPALIVE_MAX)); + setIkeProposal(bundle.getString(VpnProfileDataSource.KEY_IKE_PROPOSAL)); + setEspProposal(bundle.getString(VpnProfileDataSource.KEY_ESP_PROPOSAL)); + setDnsServers(bundle.getString(VpnProfileDataSource.KEY_DNS_SERVERS)); + flags = addPositiveFlag(flags, bundle, KEY_TRANSPORT_IPV6_FLAG, VpnProfile.FLAGS_IPv6_TRANSPORT); + + final Bundle splitTunneling = bundle.getBundle(VpnProfileDataSource.KEY_SPLIT_TUNNELING); + if (splitTunneling != null) + { + splitFlags = addPositiveFlag(splitFlags, splitTunneling, KEY_SPLIT_TUNNELLING_BLOCK_IPV4_FLAG, VpnProfile.SPLIT_TUNNELING_BLOCK_IPV4); + splitFlags = addPositiveFlag(splitFlags, splitTunneling, KEY_SPLIT_TUNNELLING_BLOCK_IPV6_FLAG, VpnProfile.SPLIT_TUNNELING_BLOCK_IPV6); + + setExcludedSubnets(splitTunneling.getString(VpnProfileDataSource.KEY_EXCLUDED_SUBNETS)); + setIncludedSubnets(splitTunneling.getString(VpnProfileDataSource.KEY_INCLUDED_SUBNETS)); + } + + setSplitTunneling(splitFlags); + setFlags(flags); + } + + private void configureSelectedApps(String includedPackageNames, String excludedPackageNames) + { + if (!TextUtils.isEmpty(includedPackageNames)) + { + setSelectedAppsHandling(SelectedAppsHandling.SELECTED_APPS_ONLY); + setSelectedApps(includedPackageNames); + } + else if (!TextUtils.isEmpty(excludedPackageNames)) + { + setSelectedAppsHandling(SelectedAppsHandling.SELECTED_APPS_EXCLUDE); + setSelectedApps(excludedPackageNames); + } + } + + private int configureRemote(final UUID uuid, @Nullable Bundle remote, int flags) + { + if (remote == null) + { + return flags; + } + + setGateway(remote.getString(VpnProfileDataSource.KEY_GATEWAY)); + setPort(getInt(remote, VpnProfileDataSource.KEY_PORT, 1, 65_535)); + setRemoteId(remote.getString(VpnProfileDataSource.KEY_REMOTE_ID)); + + final String certificateData = remote.getString(VpnProfileDataSource.KEY_CERTIFICATE); + if (!TextUtils.isEmpty(certificateData)) + { + trustedCertificate = new ManagedTrustedCertificate(uuid.toString(), certificateData); + setCertificateAlias(trustedCertificate.getAlias()); + } + + flags = addNegativeFlag(flags, remote, KEY_REMOTE_CERT_REQ_FLAG, VpnProfile.FLAGS_SUPPRESS_CERT_REQS); + flags = addNegativeFlag(flags, remote, KEY_REMOTE_REVOCATION_CRL_FLAG, VpnProfile.FLAGS_DISABLE_CRL); + flags = addNegativeFlag(flags, remote, KEY_REMOTE_REVOCATION_OCSP_FLAG, VpnProfile.FLAGS_DISABLE_OCSP); + flags = addPositiveFlag(flags, remote, KEY_REMOTE_REVOCATION_STRICT_FLAG, VpnProfile.FLAGS_STRICT_REVOCATION); + return flags; + } + + private int configureLocal(final UUID uuid, @Nullable Bundle local, int flags) + { + if (local == null) + { + return flags; + } + + setLocalId(local.getString(VpnProfileDataSource.KEY_LOCAL_ID)); + setUsername(local.getString(VpnProfileDataSource.KEY_USERNAME)); + + final String userCertificateData = local.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE); + final String userCertificatePassword = local.getString(VpnProfileDataSource.KEY_USER_CERTIFICATE_PASSWORD, ""); + if (!TextUtils.isEmpty(userCertificateData)) + { + userCertificate = new ManagedUserCertificate(uuid.toString(), userCertificateData, userCertificatePassword); + setUserCertificateAlias(userCertificate.getAlias()); + } + + flags = addPositiveFlag(flags, local, KEY_LOCAL_RSA_PSS_FLAG, VpnProfile.FLAGS_RSA_PSS); + return flags; + } + + private static Integer getInt(final Bundle bundle, final String key, final int min, final int max) + { + final int value = bundle.getInt(key); + return value < min || value > max ? null : value; + } + + private static int addPositiveFlag(int flags, Bundle bundle, String key, int flag) + { + if (bundle.getBoolean(key)) + { + flags |= flag; + } + return flags; + } + + private static int addNegativeFlag(int flags, Bundle bundle, String key, int flag) + { + if (!bundle.getBoolean(key)) + { + flags |= flag; + } + return flags; + } + + public ManagedTrustedCertificate getTrustedCertificate() + { + return trustedCertificate; + } + + public ManagedUserCertificate getUserCertificate() + { + return userCertificate; + } + + @Override + public boolean equals(Object o) + { + if (o == this) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + ManagedVpnProfile that = (ManagedVpnProfile)o; + return Objects.equals(getUUID(), that.getUUID()); + } + + @Override + public int hashCode() + { + return Objects.hash(getUUID()); + } +} 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 f95082711..e3ce9d4b8 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 @@ -22,6 +22,7 @@ package org.strongswan.android.data; import android.text.TextUtils; import java.util.Arrays; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; import java.util.UUID; @@ -47,6 +48,8 @@ public class VpnProfile implements Cloneable private VpnType mVpnType; private UUID mUUID; private long mId = -1; + private boolean mReadOnly; + private VpnProfileDataSource mDataSource; public enum SelectedAppsHandling { @@ -54,7 +57,7 @@ public class VpnProfile implements Cloneable SELECTED_APPS_EXCLUDE(1), SELECTED_APPS_ONLY(2); - private Integer mValue; + private final Integer mValue; SelectedAppsHandling(int value) { @@ -330,6 +333,26 @@ public class VpnProfile implements Cloneable this.mFlags = flags; } + public boolean isReadOnly() + { + return mReadOnly; + } + + public void setReadOnly(boolean readOnly) + { + this.mReadOnly = readOnly; + } + + public VpnProfileDataSource getDataSource() + { + return mDataSource; + } + + public void setDataSource(VpnProfileDataSource mDataSource) + { + this.mDataSource = mDataSource; + } + @Override public String toString() { @@ -339,16 +362,22 @@ public class VpnProfile implements Cloneable @Override public boolean equals(Object o) { - if (o != null && o instanceof VpnProfile) + if (o == this) { - VpnProfile other = (VpnProfile)o; - if (this.mUUID != null && other.getUUID() != null) - { - return this.mUUID.equals(other.getUUID()); - } - return this.mId == other.getId(); + return true; } - return false; + if (o == null || getClass() != o.getClass()) + { + return false; + } + VpnProfile that = (VpnProfile)o; + return Objects.equals(mUUID, that.mUUID); + } + + @Override + public int hashCode() + { + return Objects.hash(mUUID); } @Override 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 5604f67a1..f95ce4734 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 @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Relution GmbH * Copyright (C) 2012-2019 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager @@ -18,298 +19,52 @@ package org.strongswan.android.data; -import android.content.ContentValues; -import android.content.Context; -import android.database.Cursor; import android.database.SQLException; -import android.database.sqlite.SQLiteDatabase; -import android.database.sqlite.SQLiteOpenHelper; -import android.database.sqlite.SQLiteQueryBuilder; -import android.util.Log; -import java.util.ArrayList; import java.util.List; import java.util.UUID; -public class VpnProfileDataSource +public interface 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"; - public static final String KEY_USERNAME = "username"; - public static final String KEY_PASSWORD = "password"; - public static final String KEY_CERTIFICATE = "certificate"; - public static final String KEY_USER_CERTIFICATE = "user_certificate"; - public static final String KEY_MTU = "mtu"; - public static final String KEY_PORT = "port"; - public static final String KEY_SPLIT_TUNNELING = "split_tunneling"; - public static final String KEY_LOCAL_ID = "local_id"; - public static final String KEY_REMOTE_ID = "remote_id"; - public static final String KEY_EXCLUDED_SUBNETS = "excluded_subnets"; - public static final String KEY_INCLUDED_SUBNETS = "included_subnets"; - public static final String KEY_SELECTED_APPS = "selected_apps"; - public static final String KEY_SELECTED_APPS_LIST = "selected_apps_list"; - public static final String KEY_NAT_KEEPALIVE = "nat_keepalive"; - public static final String KEY_FLAGS = "flags"; - public static final String KEY_IKE_PROPOSAL = "ike_proposal"; - public static final String KEY_ESP_PROPOSAL = "esp_proposal"; - public static final String KEY_DNS_SERVERS = "dns_servers"; - - private DatabaseHelper mDbHelper; - private SQLiteDatabase mDatabase; - private final Context mContext; - - private static final String DATABASE_NAME = "strongswan.db"; - private static final String TABLE_VPNPROFILE = "vpnprofile"; - - private static final int DATABASE_VERSION = 17; - - public static final DbColumn[] COLUMNS = new DbColumn[] { - new DbColumn(KEY_ID, "INTEGER PRIMARY KEY AUTOINCREMENT", 1), - new DbColumn(KEY_UUID, "TEXT UNIQUE", 9), - new DbColumn(KEY_NAME, "TEXT NOT NULL", 1), - new DbColumn(KEY_GATEWAY, "TEXT NOT NULL", 1), - new DbColumn(KEY_VPN_TYPE, "TEXT NOT NULL", 3), - new DbColumn(KEY_USERNAME, "TEXT", 1), - new DbColumn(KEY_PASSWORD, "TEXT", 1), - new DbColumn(KEY_CERTIFICATE, "TEXT", 1), - new DbColumn(KEY_USER_CERTIFICATE, "TEXT", 2), - new DbColumn(KEY_MTU, "INTEGER", 5), - new DbColumn(KEY_PORT, "INTEGER", 5), - new DbColumn(KEY_SPLIT_TUNNELING, "INTEGER", 7), - new DbColumn(KEY_LOCAL_ID, "TEXT", 8), - new DbColumn(KEY_REMOTE_ID, "TEXT", 8), - new DbColumn(KEY_EXCLUDED_SUBNETS, "TEXT", 10), - new DbColumn(KEY_INCLUDED_SUBNETS, "TEXT", 11), - new DbColumn(KEY_SELECTED_APPS, "INTEGER", 12), - new DbColumn(KEY_SELECTED_APPS_LIST, "TEXT", 12), - new DbColumn(KEY_NAT_KEEPALIVE, "INTEGER", 13), - new DbColumn(KEY_FLAGS, "INTEGER", 14), - new DbColumn(KEY_IKE_PROPOSAL, "TEXT", 15), - new DbColumn(KEY_ESP_PROPOSAL, "TEXT", 15), - new DbColumn(KEY_DNS_SERVERS, "TEXT", 17), - }; - - private static final String[] ALL_COLUMNS = getColumns(DATABASE_VERSION); - - private static String getDatabaseCreate(int version) - { - boolean first = true; - StringBuilder create = new StringBuilder("CREATE TABLE "); - create.append(TABLE_VPNPROFILE); - create.append(" ("); - for (DbColumn column : COLUMNS) - { - if (column.Since <= version) - { - if (!first) - { - create.append(","); - } - first = false; - create.append(column.Name); - create.append(" "); - create.append(column.Type); - } - } - create.append(");"); - return create.toString(); - } - - private static String[] getColumns(int version) - { - ArrayList columns = new ArrayList<>(); - for (DbColumn column : COLUMNS) - { - if (column.Since <= version) - { - columns.add(column.Name); - } - } - return columns.toArray(new String[0]); - } - - private static class DatabaseHelper extends SQLiteOpenHelper - { - public DatabaseHelper(Context context) - { - super(context, DATABASE_NAME, null, DATABASE_VERSION); - } - - @Override - public void onCreate(SQLiteDatabase database) - { - database.execSQL(getDatabaseCreate(DATABASE_VERSION)); - } - - @Override - public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) - { - Log.w(TAG, "Upgrading database from version " + oldVersion + - " to " + newVersion); - if (oldVersion < 2) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_USER_CERTIFICATE + - " TEXT;"); - } - if (oldVersion < 3) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_VPN_TYPE + - " TEXT DEFAULT '';"); - } - if (oldVersion < 4) - { /* remove NOT NULL constraint from username column */ - updateColumns(db, 4); - } - if (oldVersion < 5) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_MTU + - " INTEGER;"); - } - if (oldVersion < 6) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_PORT + - " INTEGER;"); - } - if (oldVersion < 7) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_SPLIT_TUNNELING + - " INTEGER;"); - } - if (oldVersion < 8) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_LOCAL_ID + - " TEXT;"); - 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, 9); - } - if (oldVersion < 10) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_EXCLUDED_SUBNETS + - " TEXT;"); - } - if (oldVersion < 11) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_INCLUDED_SUBNETS + - " TEXT;"); - } - if (oldVersion < 12) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_SELECTED_APPS + - " INTEGER;"); - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_SELECTED_APPS_LIST + - " TEXT;"); - } - if (oldVersion < 13) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_NAT_KEEPALIVE + - " INTEGER;"); - } - if (oldVersion < 14) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_FLAGS + - " INTEGER;"); - } - if (oldVersion < 15) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_IKE_PROPOSAL + - " TEXT;"); - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_ESP_PROPOSAL + - " TEXT;"); - } - if (oldVersion < 16) - { /* add a UUID to all entries that haven't one yet */ - db.beginTransaction(); - try - { - Cursor cursor = db.query(TABLE_VPNPROFILE, getColumns(16), KEY_UUID + " is NULL", null, null, null, null); - for (cursor.moveToFirst(); !cursor.isAfterLast(); cursor.moveToNext()) - { - ContentValues values = new ContentValues(); - values.put(KEY_UUID, UUID.randomUUID().toString()); - db.update(TABLE_VPNPROFILE, values, KEY_ID + " = " + cursor.getLong(cursor.getColumnIndexOrThrow(KEY_ID)), null); - } - cursor.close(); - db.setTransactionSuccessful(); - } - finally - { - db.endTransaction(); - } - } - if (oldVersion < 17) - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " ADD " + KEY_DNS_SERVERS + - " TEXT;"); - } - } - - private void updateColumns(SQLiteDatabase db, int version) - { - db.beginTransaction(); - try - { - db.execSQL("ALTER TABLE " + TABLE_VPNPROFILE + " RENAME TO tmp_" + TABLE_VPNPROFILE + ";"); - db.execSQL(getDatabaseCreate(version)); - StringBuilder insert = new StringBuilder("INSERT INTO " + TABLE_VPNPROFILE + " SELECT "); - SQLiteQueryBuilder.appendColumns(insert, getColumns(version)); - db.execSQL(insert.append(" FROM tmp_" + TABLE_VPNPROFILE + ";").toString()); - db.execSQL("DROP TABLE tmp_" + TABLE_VPNPROFILE + ";"); - db.setTransactionSuccessful(); - } - finally - { - db.endTransaction(); - } - } - } - - /** - * Construct a new VPN profile data source. The context is used to - * open/create the database. - * @param context context used to access the database - */ - public VpnProfileDataSource(Context context) - { - this.mContext = context; - } + String KEY_ID = "_id"; + String KEY_UUID = "_uuid"; + String KEY_NAME = "name"; + String KEY_GATEWAY = "gateway"; + String KEY_VPN_TYPE = "vpn_type"; + String KEY_USERNAME = "username"; + String KEY_PASSWORD = "password"; + String KEY_CERTIFICATE = "certificate"; + String KEY_USER_CERTIFICATE = "user_certificate"; + String KEY_USER_CERTIFICATE_PASSWORD = "user_certificate_password"; + String KEY_MTU = "mtu"; + String KEY_PORT = "port"; + String KEY_SPLIT_TUNNELING = "split_tunneling"; + String KEY_LOCAL_ID = "local_id"; + String KEY_REMOTE_ID = "remote_id"; + String KEY_EXCLUDED_SUBNETS = "excluded_subnets"; + String KEY_INCLUDED_SUBNETS = "included_subnets"; + String KEY_SELECTED_APPS = "selected_apps"; + String KEY_SELECTED_APPS_LIST = "selected_apps_list"; + String KEY_NAT_KEEPALIVE = "nat_keepalive"; + String KEY_FLAGS = "flags"; + String KEY_IKE_PROPOSAL = "ike_proposal"; + String KEY_ESP_PROPOSAL = "esp_proposal"; + String KEY_DNS_SERVERS = "dns_servers"; + String KEY_READ_ONLY = "read_only"; /** * Open the VPN profile data source. The database is automatically created * if it does not yet exist. If that fails an exception is thrown. + * * @return itself (allows to chain initialization calls) * @throws SQLException if the database could not be opened or created */ - public VpnProfileDataSource open() throws SQLException - { - if (mDbHelper == null) - { - mDbHelper = new DatabaseHelper(mContext); - mDatabase = mDbHelper.getWritableDatabase(); - } - return this; - } + VpnProfileDataSource open() throws SQLException; /** * Close the data source. */ - public void close() - { - if (mDbHelper != null) - { - mDbHelper.close(); - mDbHelper = null; - } - } + void close(); /** * Insert the given VPN profile into the database. On success the Id of @@ -318,83 +73,39 @@ public class VpnProfileDataSource * @param profile the profile to add * @return the added VPN profile or null, if failed */ - public VpnProfile insertProfile(VpnProfile profile) - { - ContentValues values = ContentValuesFromVpnProfile(profile); - long insertId = mDatabase.insert(TABLE_VPNPROFILE, null, values); - if (insertId == -1) - { - return null; - } - profile.setId(insertId); - return profile; - } + VpnProfile insertProfile(VpnProfile profile); /** * Updates the given VPN profile in the database. + * * @param profile the profile to update * @return true if update succeeded, false otherwise */ - public boolean updateVpnProfile(VpnProfile profile) - { - long id = profile.getId(); - ContentValues values = ContentValuesFromVpnProfile(profile); - return mDatabase.update(TABLE_VPNPROFILE, values, KEY_ID + " = " + id, null) > 0; - } + boolean updateVpnProfile(VpnProfile profile); /** * Delete the given VPN profile from the database. + * * @param profile the profile to delete * @return true if deleted, false otherwise */ - public boolean deleteVpnProfile(VpnProfile profile) - { - long id = profile.getId(); - return mDatabase.delete(TABLE_VPNPROFILE, KEY_ID + " = " + id, null) > 0; - } - - /** - * Get a single VPN profile from the database. - * @param id the ID of the VPN profile - * @return the profile or null, if not found - */ - public VpnProfile getVpnProfile(long id) - { - VpnProfile profile = null; - Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS, - KEY_ID + "=" + id, null, null, null, null); - if (cursor.moveToFirst()) - { - profile = VpnProfileFromCursor(cursor); - } - cursor.close(); - return profile; - } + boolean deleteVpnProfile(VpnProfile 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; - } + VpnProfile getVpnProfile(UUID uuid); /** * Get a single VPN profile from the database by its UUID as String. + * * @param uuid the UUID of the VPN profile as String * @return the profile or null, if not found */ - public VpnProfile getVpnProfile(String uuid) + default VpnProfile getVpnProfile(String uuid) { try { @@ -413,97 +124,8 @@ public class VpnProfileDataSource /** * Get a list of all VPN profiles stored in the database. + * * @return list of VPN profiles */ - public List getAllVpnProfiles() - { - List vpnProfiles = new ArrayList(); - - Cursor cursor = mDatabase.query(TABLE_VPNPROFILE, ALL_COLUMNS, null, null, null, null, null); - cursor.moveToFirst(); - while (!cursor.isAfterLast()) - { - VpnProfile vpnProfile = VpnProfileFromCursor(cursor); - vpnProfiles.add(vpnProfile); - cursor.moveToNext(); - } - cursor.close(); - return vpnProfiles; - } - - private VpnProfile VpnProfileFromCursor(Cursor cursor) - { - VpnProfile profile = new VpnProfile(); - profile.setId(cursor.getLong(cursor.getColumnIndexOrThrow(KEY_ID))); - profile.setUUID(UUID.fromString(cursor.getString(cursor.getColumnIndexOrThrow(KEY_UUID)))); - profile.setName(cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME))); - profile.setGateway(cursor.getString(cursor.getColumnIndexOrThrow(KEY_GATEWAY))); - profile.setVpnType(VpnType.fromIdentifier(cursor.getString(cursor.getColumnIndexOrThrow(KEY_VPN_TYPE)))); - profile.setUsername(cursor.getString(cursor.getColumnIndexOrThrow(KEY_USERNAME))); - profile.setPassword(cursor.getString(cursor.getColumnIndexOrThrow(KEY_PASSWORD))); - profile.setCertificateAlias(cursor.getString(cursor.getColumnIndexOrThrow(KEY_CERTIFICATE))); - profile.setUserCertificateAlias(cursor.getString(cursor.getColumnIndexOrThrow(KEY_USER_CERTIFICATE))); - profile.setMTU(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_MTU))); - profile.setPort(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_PORT))); - profile.setSplitTunneling(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_SPLIT_TUNNELING))); - profile.setLocalId(cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCAL_ID))); - profile.setRemoteId(cursor.getString(cursor.getColumnIndexOrThrow(KEY_REMOTE_ID))); - profile.setExcludedSubnets(cursor.getString(cursor.getColumnIndexOrThrow(KEY_EXCLUDED_SUBNETS))); - profile.setIncludedSubnets(cursor.getString(cursor.getColumnIndexOrThrow(KEY_INCLUDED_SUBNETS))); - profile.setSelectedAppsHandling(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_SELECTED_APPS))); - profile.setSelectedApps(cursor.getString(cursor.getColumnIndexOrThrow(KEY_SELECTED_APPS_LIST))); - profile.setNATKeepAlive(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_NAT_KEEPALIVE))); - profile.setFlags(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_FLAGS))); - profile.setIkeProposal(cursor.getString(cursor.getColumnIndexOrThrow(KEY_IKE_PROPOSAL))); - profile.setEspProposal(cursor.getString(cursor.getColumnIndexOrThrow(KEY_ESP_PROPOSAL))); - profile.setDnsServers(cursor.getString(cursor.getColumnIndexOrThrow(KEY_DNS_SERVERS))); - return profile; - } - - private ContentValues ContentValuesFromVpnProfile(VpnProfile profile) - { - ContentValues values = new ContentValues(); - values.put(KEY_UUID, profile.getUUID().toString()); - values.put(KEY_NAME, profile.getName()); - values.put(KEY_GATEWAY, profile.getGateway()); - values.put(KEY_VPN_TYPE, profile.getVpnType().getIdentifier()); - values.put(KEY_USERNAME, profile.getUsername()); - values.put(KEY_PASSWORD, profile.getPassword()); - values.put(KEY_CERTIFICATE, profile.getCertificateAlias()); - values.put(KEY_USER_CERTIFICATE, profile.getUserCertificateAlias()); - values.put(KEY_MTU, profile.getMTU()); - values.put(KEY_PORT, profile.getPort()); - values.put(KEY_SPLIT_TUNNELING, profile.getSplitTunneling()); - values.put(KEY_LOCAL_ID, profile.getLocalId()); - values.put(KEY_REMOTE_ID, profile.getRemoteId()); - values.put(KEY_EXCLUDED_SUBNETS, profile.getExcludedSubnets()); - values.put(KEY_INCLUDED_SUBNETS, profile.getIncludedSubnets()); - values.put(KEY_SELECTED_APPS, profile.getSelectedAppsHandling().getValue()); - values.put(KEY_SELECTED_APPS_LIST, profile.getSelectedApps()); - values.put(KEY_NAT_KEEPALIVE, profile.getNATKeepAlive()); - values.put(KEY_FLAGS, profile.getFlags()); - values.put(KEY_IKE_PROPOSAL, profile.getIkeProposal()); - values.put(KEY_ESP_PROPOSAL, profile.getEspProposal()); - values.put(KEY_DNS_SERVERS, profile.getDnsServers()); - return values; - } - - private Integer getInt(Cursor cursor, int columnIndex) - { - return cursor.isNull(columnIndex) ? null : cursor.getInt(columnIndex); - } - - private static class DbColumn - { - public final String Name; - public final String Type; - public final Integer Since; - - public DbColumn(String name, String type, Integer since) - { - Name = name; - Type = type; - Since = since; - } - } + List getAllVpnProfiles(); } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileManagedDataSource.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileManagedDataSource.java new file mode 100644 index 000000000..414d5bc4e --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileManagedDataSource.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.SQLException; + +import org.strongswan.android.logic.StrongSwanApplication; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; + +public class VpnProfileManagedDataSource implements VpnProfileDataSource +{ + private static final String NAME_MANAGED_VPN_PROFILES = "org.strongswan.android.data.VpnProfileManagedDataSource.preferences"; + + private final ManagedConfigurationService mManagedConfigurationService; + private final SharedPreferences mSharedPreferences; + + public VpnProfileManagedDataSource(final Context context) + { + this.mManagedConfigurationService = StrongSwanApplication.getInstance().getManagedConfigurationService(); + this.mSharedPreferences = context.getSharedPreferences(NAME_MANAGED_VPN_PROFILES, Context.MODE_PRIVATE); + } + + @Override + public VpnProfileDataSource open() throws SQLException + { + return this; + } + + @Override + public void close() + { + /* remove passwords that are no longer referenced by a VPN profile */ + final Set actualKeys = mManagedConfigurationService.getManagedProfiles().keySet(); + + final Set storedKeys = new HashSet<>(mSharedPreferences.getAll().keySet()); + storedKeys.removeAll(actualKeys); + + final SharedPreferences.Editor editor = mSharedPreferences.edit(); + for (String key : storedKeys) + { + editor.remove(key); + } + + editor.apply(); + } + + @Override + public VpnProfile insertProfile(VpnProfile profile) + { + return null; + } + + @Override + public boolean updateVpnProfile(VpnProfile profile) + { + final VpnProfile existingProfile = getVpnProfile(profile.getUUID()); + if (existingProfile == null) + { + return false; + } + + final String password = profile.getPassword(); + existingProfile.setPassword(password); + + final SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putString(profile.getUUID().toString(), password); + return editor.commit(); + } + + @Override + public boolean deleteVpnProfile(VpnProfile profile) + { + return false; + } + + @Override + public VpnProfile getVpnProfile(UUID uuid) + { + return mManagedConfigurationService.getManagedProfiles().get(uuid.toString()); + } + + @Override + public List getAllVpnProfiles() + { + final Map managedVpnProfiles = mManagedConfigurationService.getManagedProfiles(); + final List vpnProfiles = new ArrayList<>(); + for (final VpnProfile vpnProfile : managedVpnProfiles.values()) + { + final String password = mSharedPreferences.getString(vpnProfile.getUUID().toString(), vpnProfile.getPassword()); + vpnProfile.setPassword(password); + vpnProfile.setDataSource(this); + vpnProfiles.add(vpnProfile); + } + return vpnProfiles; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileSource.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileSource.java new file mode 100644 index 000000000..299d0a51a --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileSource.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.content.Context; +import android.database.SQLException; + +import org.strongswan.android.logic.StrongSwanApplication; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class VpnProfileSource implements VpnProfileDataSource +{ + private final ManagedConfigurationService mManagedConfigurationService; + private final List dataSources = new ArrayList<>(); + private final VpnProfileSqlDataSource vpnProfileSqlDataSource; + + public VpnProfileSource(Context context) + { + mManagedConfigurationService = StrongSwanApplication.getInstance().getManagedConfigurationService(); + + vpnProfileSqlDataSource = new VpnProfileSqlDataSource(); + + dataSources.add(vpnProfileSqlDataSource); + dataSources.add(new VpnProfileManagedDataSource(context)); + } + + @Override + public VpnProfileDataSource open() throws SQLException + { + for (final VpnProfileDataSource source : dataSources) + { + source.open(); + } + return this; + } + + @Override + public void close() + { + for (final VpnProfileDataSource source : dataSources) + { + source.close(); + } + } + + @Override + public VpnProfile insertProfile(VpnProfile profile) + { + return vpnProfileSqlDataSource.insertProfile(profile); + } + + @Override + public boolean updateVpnProfile(VpnProfile profile) + { + return profile.getDataSource().updateVpnProfile(profile); + } + + @Override + public boolean deleteVpnProfile(VpnProfile profile) + { + return profile.getDataSource().deleteVpnProfile(profile); + } + + private List getAccessibleSources() + { + final ManagedConfiguration managedConfiguration = mManagedConfigurationService.getManagedConfiguration(); + ArrayList sources = new ArrayList<>(dataSources); + if (!managedConfiguration.isAllowExistingProfiles()) + { + sources.remove(vpnProfileSqlDataSource); + } + return sources; + } + + @Override + public VpnProfile getVpnProfile(UUID uuid) + { + for (final VpnProfileDataSource source : getAccessibleSources()) + { + final VpnProfile profile = source.getVpnProfile(uuid); + if (profile != null) + { + return profile; + } + } + return null; + } + + @Override + public List getAllVpnProfiles() + { + final List profiles = new ArrayList<>(); + + for (final VpnProfileDataSource source : getAccessibleSources()) + { + profiles.addAll(source.getAllVpnProfiles()); + } + + return profiles; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileSqlDataSource.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileSqlDataSource.java new file mode 100644 index 000000000..50ffc6297 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnProfileSqlDataSource.java @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2012-2019 Tobias Brunner + * Copyright (C) 2012 Giuliano Grassi + * Copyright (C) 2012 Ralf Sager + * + * Copyright (C) secunet Security Networks AG + * + * 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.data; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; + +import org.strongswan.android.logic.StrongSwanApplication; + +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +public class VpnProfileSqlDataSource implements VpnProfileDataSource +{ + private final DatabaseHelper mDbHelper; + + private SQLiteDatabase mDatabase; + + /** + * Construct a new VPN profile data source. The context is used to + * open/create the database. + */ + public VpnProfileSqlDataSource() + { + mDbHelper = StrongSwanApplication.getInstance().getDatabaseHelper(); + } + + @Override + public VpnProfileDataSource open() throws SQLException + { + if (mDatabase == null) + { + mDatabase = mDbHelper.getWritableDatabase(); + } + return this; + } + + @Override + public void close() + { + if (mDatabase != null) + { + mDatabase = null; + } + } + + @Override + public VpnProfile insertProfile(VpnProfile profile) + { + ContentValues values = ContentValuesFromVpnProfile(profile); + long insertId = mDatabase.insert(DatabaseHelper.TABLE_VPN_PROFILE.Name, null, values); + if (insertId == -1) + { + return null; + } + profile.setDataSource(this); + profile.setId(insertId); + return profile; + } + + @Override + public boolean updateVpnProfile(VpnProfile profile) + { + final UUID uuid = profile.getUUID(); + ContentValues values = ContentValuesFromVpnProfile(profile); + return mDatabase.update(DatabaseHelper.TABLE_VPN_PROFILE.Name, values, KEY_UUID + " = ?", new String[]{uuid.toString()}) > 0; + } + + @Override + public boolean deleteVpnProfile(VpnProfile profile) + { + final UUID uuid = profile.getUUID(); + return mDatabase.delete(DatabaseHelper.TABLE_VPN_PROFILE.Name, KEY_UUID + " = ?", new String[]{uuid.toString()}) > 0; + } + + @Override + public VpnProfile getVpnProfile(UUID uuid) + { + VpnProfile profile = null; + DatabaseHelper.DbTable table = DatabaseHelper.TABLE_VPN_PROFILE; + Cursor cursor = mDatabase.query(table.Name, table.columnNames(), KEY_UUID + " = ?", new String[]{uuid.toString()}, null, null, null); + if (cursor.moveToFirst()) + { + profile = VpnProfileFromCursor(cursor); + profile.setDataSource(this); + } + cursor.close(); + return profile; + } + + @Override + public List getAllVpnProfiles() + { + List vpnProfiles = new ArrayList<>(); + + DatabaseHelper.DbTable table = DatabaseHelper.TABLE_VPN_PROFILE; + Cursor cursor = mDatabase.query(table.Name, table.columnNames(), null, null, null, null, null); + cursor.moveToFirst(); + while (!cursor.isAfterLast()) + { + VpnProfile vpnProfile = VpnProfileFromCursor(cursor); + vpnProfile.setDataSource(this); + vpnProfiles.add(vpnProfile); + cursor.moveToNext(); + } + cursor.close(); + return vpnProfiles; + } + + private VpnProfile VpnProfileFromCursor(Cursor cursor) + { + VpnProfile profile = new VpnProfile(); + profile.setUUID(UUID.fromString(cursor.getString(cursor.getColumnIndexOrThrow(KEY_UUID)))); + profile.setName(cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME))); + profile.setGateway(cursor.getString(cursor.getColumnIndexOrThrow(KEY_GATEWAY))); + profile.setVpnType(VpnType.fromIdentifier(cursor.getString(cursor.getColumnIndexOrThrow(KEY_VPN_TYPE)))); + profile.setUsername(cursor.getString(cursor.getColumnIndexOrThrow(KEY_USERNAME))); + profile.setPassword(cursor.getString(cursor.getColumnIndexOrThrow(KEY_PASSWORD))); + profile.setCertificateAlias(cursor.getString(cursor.getColumnIndexOrThrow(KEY_CERTIFICATE))); + profile.setUserCertificateAlias(cursor.getString(cursor.getColumnIndexOrThrow(KEY_USER_CERTIFICATE))); + profile.setMTU(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_MTU))); + profile.setPort(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_PORT))); + profile.setSplitTunneling(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_SPLIT_TUNNELING))); + profile.setLocalId(cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCAL_ID))); + profile.setRemoteId(cursor.getString(cursor.getColumnIndexOrThrow(KEY_REMOTE_ID))); + profile.setExcludedSubnets(cursor.getString(cursor.getColumnIndexOrThrow(KEY_EXCLUDED_SUBNETS))); + profile.setIncludedSubnets(cursor.getString(cursor.getColumnIndexOrThrow(KEY_INCLUDED_SUBNETS))); + profile.setSelectedAppsHandling(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_SELECTED_APPS))); + profile.setSelectedApps(cursor.getString(cursor.getColumnIndexOrThrow(KEY_SELECTED_APPS_LIST))); + profile.setNATKeepAlive(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_NAT_KEEPALIVE))); + profile.setFlags(getInt(cursor, cursor.getColumnIndexOrThrow(KEY_FLAGS))); + profile.setIkeProposal(cursor.getString(cursor.getColumnIndexOrThrow(KEY_IKE_PROPOSAL))); + profile.setEspProposal(cursor.getString(cursor.getColumnIndexOrThrow(KEY_ESP_PROPOSAL))); + profile.setDnsServers(cursor.getString(cursor.getColumnIndexOrThrow(KEY_DNS_SERVERS))); + return profile; + } + + private ContentValues ContentValuesFromVpnProfile(VpnProfile profile) + { + ContentValues values = new ContentValues(); + values.put(KEY_UUID, profile.getUUID().toString()); + values.put(KEY_NAME, profile.getName()); + values.put(KEY_GATEWAY, profile.getGateway()); + values.put(KEY_VPN_TYPE, profile.getVpnType().getIdentifier()); + values.put(KEY_USERNAME, profile.getUsername()); + values.put(KEY_PASSWORD, profile.getPassword()); + values.put(KEY_CERTIFICATE, profile.getCertificateAlias()); + values.put(KEY_USER_CERTIFICATE, profile.getUserCertificateAlias()); + values.put(KEY_MTU, profile.getMTU()); + values.put(KEY_PORT, profile.getPort()); + values.put(KEY_SPLIT_TUNNELING, profile.getSplitTunneling()); + values.put(KEY_LOCAL_ID, profile.getLocalId()); + values.put(KEY_REMOTE_ID, profile.getRemoteId()); + values.put(KEY_EXCLUDED_SUBNETS, profile.getExcludedSubnets()); + values.put(KEY_INCLUDED_SUBNETS, profile.getIncludedSubnets()); + values.put(KEY_SELECTED_APPS, profile.getSelectedAppsHandling().getValue()); + values.put(KEY_SELECTED_APPS_LIST, profile.getSelectedApps()); + values.put(KEY_NAT_KEEPALIVE, profile.getNATKeepAlive()); + values.put(KEY_FLAGS, profile.getFlags()); + values.put(KEY_IKE_PROPOSAL, profile.getIkeProposal()); + values.put(KEY_ESP_PROPOSAL, profile.getEspProposal()); + values.put(KEY_DNS_SERVERS, profile.getDnsServers()); + return values; + } + + private Integer getInt(Cursor cursor, int columnIndex) + { + return cursor.isNull(columnIndex) ? null : cursor.getInt(columnIndex); + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnType.java b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnType.java index 1a666734b..0552a645f 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnType.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/data/VpnType.java @@ -17,6 +17,10 @@ package org.strongswan.android.data; import java.util.EnumSet; +import java.util.Objects; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; public enum VpnType { @@ -32,11 +36,19 @@ public enum VpnType */ public enum VpnTypeFeature { - /** client certificate is required */ + /** + * Client certificate is required + */ CERTIFICATE, - /** username and password are required */ + + /** + * Username and password are required + */ USER_PASS, - /** enable BYOD features */ + + /** + * Enable BYOD features + */ BYOD; } @@ -48,7 +60,6 @@ public enum VpnType * * @param id identifier used to store and transmit this specific type * @param features of the given VPN type - * @param certificate true if a client certificate is required */ VpnType(String id, EnumSet features) { @@ -58,6 +69,7 @@ public enum VpnType /** * The identifier used to store this value in the database + * * @return identifier */ public String getIdentifier() @@ -81,11 +93,12 @@ public enum VpnType * @param identifier get the enum entry with this identifier * @return the enum entry, or the default if not found */ - public static VpnType fromIdentifier(String identifier) + @NonNull + public static VpnType fromIdentifier(@Nullable String identifier) { for (VpnType type : VpnType.values()) { - if (identifier.equals(type.mIdentifier)) + if (Objects.equals(identifier, type.mIdentifier)) { return type; } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java index 165f479dc..03e59f7ee 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/CharonVpnService.java @@ -45,6 +45,7 @@ import org.strongswan.android.R; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfile.SelectedAppsHandling; import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.data.VpnProfileSource; import org.strongswan.android.data.VpnType.VpnTypeFeature; import org.strongswan.android.logic.VpnStateService.ErrorState; import org.strongswan.android.logic.VpnStateService.State; @@ -101,11 +102,12 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe private volatile boolean mTerminate; private volatile boolean mIsDisconnecting; private volatile boolean mShowNotification; - private BuilderAdapter mBuilderAdapter = new BuilderAdapter(); + private final BuilderAdapter mBuilderAdapter = new BuilderAdapter(); private Handler mHandler; private VpnStateService mService; private final Object mServiceLock = new Object(); - private final ServiceConnection mServiceConnection = new ServiceConnection() { + private final ServiceConnection mServiceConnection = new ServiceConnection() + { @Override public void onServiceDisconnected(ComponentName name) { /* since the service is local this is theoretically only called when the process is terminated */ @@ -195,7 +197,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe /* handler used to do changes in the main UI thread */ mHandler = new Handler(getMainLooper()); - mDataSource = new VpnProfileDataSource(this); + mDataSource = new VpnProfileSource(this); mDataSource.open(); /* use a separate thread as main thread for charon */ mConnectionHandler = new Thread(this); @@ -346,7 +348,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe { synchronized (this) { - if (mNextProfile != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + if (mNextProfile != null) { mBuilderAdapter.setProfile(mNextProfile); mBuilderAdapter.establishBlocking(); @@ -437,10 +439,10 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe name = profile.getName(); } NotificationCompat.Builder builder = new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL) - .setSmallIcon(R.drawable.ic_notification) - .setCategory(NotificationCompat.CATEGORY_SERVICE) - .setVisibility(publicVersion ? NotificationCompat.VISIBILITY_PUBLIC - : NotificationCompat.VISIBILITY_PRIVATE); + .setSmallIcon(R.drawable.ic_notification) + .setCategory(NotificationCompat.CATEGORY_SERVICE) + .setVisibility(publicVersion ? NotificationCompat.VISIBILITY_PUBLIC + : NotificationCompat.VISIBILITY_PRIVATE); int s = R.string.state_disabled; if (error != ErrorState.NO_ERROR) { @@ -460,7 +462,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe Intent intent = new Intent(getApplicationContext(), VpnProfileControlActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setAction(VpnProfileControlActivity.START_PROFILE); - intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_ID, profile.getUUID().toString()); + intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, profile.getUUID().toString()); int flags = PendingIntent.FLAG_UPDATE_CURRENT; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { @@ -527,10 +529,11 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe } @Override - public void stateChanged() { + public void stateChanged() + { if (mShowNotification) { - NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); + NotificationManager manager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE); manager.notify(VPN_STATE_NOTIFICATION_ID, buildNotification(false)); } } @@ -813,7 +816,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe private VpnService.Builder mBuilder; private BuilderCache mCache; private BuilderCache mEstablishedCache; - private PacketDropper mDropper = new PacketDropper(); + private final PacketDropper mDropper = new PacketDropper(); public synchronized void setProfile(VpnProfile profile) { @@ -1035,9 +1038,8 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe @Override public synchronized void run() { - try + try (FileInputStream plain = new FileInputStream(mFd.getFileDescriptor())) { - FileInputStream plain = new FileInputStream(mFd.getFileDescriptor()); ByteBuffer packet = ByteBuffer.allocate(mCache.mMtu); while (true) { @@ -1071,7 +1073,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe } } } - catch (ClosedByInterruptException|InterruptedException e) + catch (final ClosedByInterruptException | InterruptedException e) { /* regular interruption */ } @@ -1234,7 +1236,6 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe } } - @TargetApi(Build.VERSION_CODES.LOLLIPOP) public void applyData(VpnService.Builder builder) { for (IPRange address : mAddresses) @@ -1277,7 +1278,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe } } } - else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + else { /* allow traffic that would otherwise be blocked to bypass the VPN */ builder.allowFamily(OsConstants.AF_INET); } @@ -1317,7 +1318,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe } } } - else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + else { builder.allowFamily(OsConstants.AF_INET6); } @@ -1327,8 +1328,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe builder.addRoute("::", 0); } /* apply selected applications */ - if (mSelectedApps.size() > 0 && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + if (mSelectedApps.size() > 0) { switch (mAppHandling) { @@ -1372,11 +1372,7 @@ public class CharonVpnService extends VpnService implements Runnable, VpnStateSe { return false; } - else if (addr instanceof Inet6Address) - { - return true; - } - return false; + return addr instanceof Inet6Address; } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedTrustedCertificateInstaller.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedTrustedCertificateInstaller.java new file mode 100644 index 000000000..ed774133f --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedTrustedCertificateInstaller.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.logic; + +import android.content.Context; +import android.util.Log; + +import org.strongswan.android.data.ManagedTrustedCertificate; +import org.strongswan.android.utils.Certificates; + +import java.io.IOException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import androidx.annotation.NonNull; + +public class ManagedTrustedCertificateInstaller +{ + private static final String TAG = ManagedTrustedCertificateInstaller.class.getSimpleName(); + + public ManagedTrustedCertificateInstaller(@NonNull final Context context) + { + } + + private boolean installTrustedCert(@NonNull ManagedTrustedCertificate trustedCertificate) + throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException + { + Log.d(TAG, "Install trusted certificate " + trustedCertificate); + final X509Certificate certificate = Certificates.from(trustedCertificate.getData()); + + KeyStore store = KeyStore.getInstance("LocalCertificateStore"); + store.load(null, null); + store.setCertificateEntry(trustedCertificate.getAlias(), certificate); + return true; + } + + private void uninstallTrustedCert(@NonNull ManagedTrustedCertificate trustedCertificate) + throws CertificateException, IOException, NoSuchAlgorithmException, KeyStoreException + { + Log.d(TAG, "Remove trusted certificate " + trustedCertificate); + KeyStore store = KeyStore.getInstance("LocalCertificateStore"); + store.load(null, null); + store.deleteEntry(trustedCertificate.getAlias()); + } + + public synchronized boolean tryInstall(@NonNull ManagedTrustedCertificate trustedCertificate) + { + try + { + return installTrustedCert(trustedCertificate); + } + catch (final Exception e) + { + Log.e(TAG, "Could not install trusted certificate " + trustedCertificate, e); + return false; + } + } + + public synchronized void tryRemove(@NonNull ManagedTrustedCertificate trustedCertificate) + { + try + { + uninstallTrustedCert(trustedCertificate); + } + catch (final Exception e) + { + Log.e(TAG, "Could not remove trusted certificate " + trustedCertificate, e); + } + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedTrustedCertificateManager.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedTrustedCertificateManager.java new file mode 100644 index 000000000..395949a3d --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedTrustedCertificateManager.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.logic; + +import android.content.Context; +import android.os.Handler; +import android.util.Log; + +import org.strongswan.android.data.DatabaseHelper; +import org.strongswan.android.data.ManagedConfigurationService; +import org.strongswan.android.data.ManagedTrustedCertificate; +import org.strongswan.android.data.ManagedTrustedCertificateRepository; +import org.strongswan.android.utils.Difference; + +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutorService; + +import androidx.annotation.NonNull; +import androidx.core.util.Pair; + +public class ManagedTrustedCertificateManager +{ + private static final String TAG = ManagedTrustedCertificateManager.class.getSimpleName(); + + @NonNull + private final ExecutorService executorService; + @NonNull + private final Handler handler; + + @NonNull + private final ManagedTrustedCertificateRepository certificateRepository; + @NonNull + private final ManagedTrustedCertificateInstaller certificateInstaller; + + public ManagedTrustedCertificateManager( + @NonNull final Context context, + @NonNull final ExecutorService executorService, + @NonNull final Handler handler, + @NonNull final ManagedConfigurationService managedConfigurationService, + @NonNull final DatabaseHelper databaseHelper) + { + this.executorService = executorService; + this.handler = handler; + + this.certificateRepository = new ManagedTrustedCertificateRepository(managedConfigurationService, databaseHelper); + this.certificateInstaller = new ManagedTrustedCertificateInstaller(context); + } + + public void update(@NonNull final Runnable onUpdateCompleted) + { + executorService.execute(() -> { + final List configured = certificateRepository.getConfiguredCertificates(); + final List installed = certificateRepository.getInstalledCertificates(); + + final Difference diff = Difference.between(installed, configured, ManagedTrustedCertificate::getVpnProfileUuid); + if (diff.isEmpty()) + { + Log.d(TAG, "No trusted certificates changed, nothing to do"); + handler.post(onUpdateCompleted); + return; + } + Log.d(TAG, "Trusted certificates changed " + diff); + + final Set protectedAliases = new HashSet<>(); + for (final ManagedTrustedCertificate unchanged : diff.getUnchanged()) + { + protectedAliases.add(unchanged.getAlias()); + } + + for (final ManagedTrustedCertificate delete : diff.getDeletes()) + { + remove(delete, !protectedAliases.contains(delete.getAlias())); + } + + for (final Pair update : diff.getUpdates()) + { + remove(update.first, !protectedAliases.contains(update.first.getAlias())); + install(update.second); + } + + for (final ManagedTrustedCertificate insert : diff.getInserts()) + { + install(insert); + } + + TrustedCertificateManager.getInstance().reset(); + TrustedCertificateManager.getInstance().load(); + handler.post(onUpdateCompleted); + }); + } + + private void install(@NonNull final ManagedTrustedCertificate trustedCertificate) + { + if (certificateInstaller.tryInstall(trustedCertificate)) + { + certificateRepository.addInstalledCertificate(trustedCertificate); + } + } + + private void remove(@NonNull final ManagedTrustedCertificate trustedCertificate, boolean uninstall) + { + if (uninstall) + { + certificateInstaller.tryRemove(trustedCertificate); + } + certificateRepository.removeInstalledCertificate(trustedCertificate); + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedUserCertificateInstaller.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedUserCertificateInstaller.java new file mode 100644 index 000000000..249cb9310 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedUserCertificateInstaller.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.logic; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.Build; +import android.util.Log; + +import org.strongswan.android.data.ManagedUserCertificate; +import org.strongswan.android.utils.KeyPair; +import org.strongswan.android.utils.KeyPairs; + +import java.io.IOException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; + +import androidx.annotation.NonNull; + +public class ManagedUserCertificateInstaller +{ + private static final String TAG = ManagedUserCertificateInstaller.class.getSimpleName(); + + private final DevicePolicyManager policyManager; + + public ManagedUserCertificateInstaller(final Context context) + { + this.policyManager = (DevicePolicyManager)context.getSystemService(Context.DEVICE_POLICY_SERVICE); + } + + private boolean installKeyPair(@NonNull ManagedUserCertificate userCertificate, @NonNull KeyPair keyPair) + { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) + { + int flags = DevicePolicyManager.INSTALLKEY_REQUEST_CREDENTIALS_ACCESS | DevicePolicyManager.INSTALLKEY_SET_USER_SELECTABLE; + return policyManager.installKeyPair( + null, + keyPair.privateKey, + new Certificate[]{keyPair.certificate}, + userCertificate.getAlias(), + flags); + } + else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) + { + return policyManager.installKeyPair( + null, + keyPair.privateKey, + new Certificate[]{keyPair.certificate}, + userCertificate.getAlias(), + true); + } + + /* This effectively prevents the app from using its own certificate, so certificate based + * authentication can only really work on Android 6+. The certificate chooser is currently + * never shown on devices that are enrolled */ + return policyManager.installKeyPair( + null, + keyPair.privateKey, + keyPair.certificate, + userCertificate.getAlias()); + } + + private boolean installKeyPair(@NonNull ManagedUserCertificate userCertificate) + throws UnrecoverableKeyException, CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException + { + final KeyPair keyPair = KeyPairs.from(userCertificate.getData(), userCertificate.getPrivateKeyPassword()); + if (keyPair == null) + { + return false; + } + Log.d(TAG, "Install key pair " + userCertificate); + return installKeyPair(userCertificate, keyPair); + } + + private void removeKeyPair(@NonNull ManagedUserCertificate userCertificate) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) + { + Log.w(TAG, "Cannot remove key pair, unsupported on API level " + Build.VERSION.SDK_INT); + return; + } + + Log.d(TAG, "Remove key pair " + userCertificate); + policyManager.removeKeyPair(null, userCertificate.getAlias()); + } + + public synchronized boolean tryInstall(@NonNull ManagedUserCertificate userCertificate) + { + try + { + return installKeyPair(userCertificate); + } + catch (final Exception e) + { + Log.e(TAG, "Could not install key pair " + userCertificate, e); + return false; + } + } + + public synchronized void tryRemove(@NonNull ManagedUserCertificate userCertificate) + { + try + { + removeKeyPair(userCertificate); + } + catch (final Exception e) + { + Log.e(TAG, "Could not remove key pair " + userCertificate, e); + } + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedUserCertificateManager.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedUserCertificateManager.java new file mode 100644 index 000000000..78424e2aa --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/ManagedUserCertificateManager.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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.logic; + +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.util.Log; + +import org.strongswan.android.data.DatabaseHelper; +import org.strongswan.android.data.ManagedConfigurationService; +import org.strongswan.android.data.ManagedUserCertificate; +import org.strongswan.android.data.ManagedUserCertificateRepository; +import org.strongswan.android.utils.Difference; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.core.util.Pair; + +public class ManagedUserCertificateManager +{ + private static final String TAG = ManagedUserCertificateManager.class.getSimpleName(); + + @NonNull + private final ManagedUserCertificateRepository certificateRepository; + @NonNull + private final ManagedUserCertificateInstaller certificateInstaller; + + public ManagedUserCertificateManager( + @NonNull final Context context, + @NonNull final ManagedConfigurationService managedConfigurationService, + @NonNull final DatabaseHelper databaseHelper) + { + final DevicePolicyManager devicePolicyManager = (DevicePolicyManager)context.getSystemService(Context.DEVICE_POLICY_SERVICE); + + this.certificateRepository = new ManagedUserCertificateRepository(managedConfigurationService, devicePolicyManager, databaseHelper); + this.certificateInstaller = new ManagedUserCertificateInstaller(context); + } + + public void update() + { + final List configured = certificateRepository.getConfiguredCertificates(); + final List installed = certificateRepository.getInstalledCertificates(); + + final Difference diff = Difference.between(installed, configured, ManagedUserCertificate::getVpnProfileUuid); + if (diff.isEmpty()) + { + Log.d(TAG, "No key pairs changed, nothing to do"); + return; + } + Log.d(TAG, "Key pairs changed " + diff); + + for (final ManagedUserCertificate delete : diff.getDeletes()) + { + remove(delete); + } + + for (final Pair update : diff.getUpdates()) + { + remove(update.first); + install(update.second); + } + + for (final ManagedUserCertificate insert : diff.getInserts()) + { + install(insert); + } + } + + private void install(@NonNull final ManagedUserCertificate userCertificate) + { + if (certificateInstaller.tryInstall(userCertificate)) + { + certificateRepository.addInstalledCertificate(userCertificate); + } + } + + private void remove(@NonNull final ManagedUserCertificate userCertificate) + { + certificateInstaller.tryRemove(userCertificate); + certificateRepository.removeInstalledCertificate(userCertificate); + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/StrongSwanApplication.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/StrongSwanApplication.java index 92a112a6d..7f73dbfb8 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/StrongSwanApplication.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/StrongSwanApplication.java @@ -1,5 +1,6 @@ /* - * Copyright (C) 2014 Tobias Brunner + * Copyright (C) 2023 Relution GmbH + * Copyright (C) 2014-2024 Tobias Brunner * * Copyright (C) secunet Security Networks AG * @@ -16,29 +17,62 @@ package org.strongswan.android.logic; +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import org.strongswan.android.data.DatabaseHelper; +import org.strongswan.android.data.ManagedConfigurationService; +import org.strongswan.android.security.LocalCertificateKeyStoreProvider; +import org.strongswan.android.utils.Constants; + import java.security.Security; +import java.util.HashSet; +import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import org.strongswan.android.security.LocalCertificateKeyStoreProvider; -import org.strongswan.android.ui.MainActivity; - -import android.app.Application; -import android.content.Context; -import android.os.Build; -import android.os.Handler; -import android.os.Looper; - +import androidx.annotation.NonNull; import androidx.core.os.HandlerCompat; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.ProcessLifecycleOwner; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; -public class StrongSwanApplication extends Application +public class StrongSwanApplication extends Application implements DefaultLifecycleObserver { + private static final String TAG = StrongSwanApplication.class.getSimpleName(); + private static Context mContext; + private static StrongSwanApplication mInstance; + private final ExecutorService mExecutorService = Executors.newFixedThreadPool(4); private final Handler mMainHandler = HandlerCompat.createAsync(Looper.getMainLooper()); - static { + private ManagedConfigurationService mManagedConfigurationService; + private ManagedTrustedCertificateManager mTrustedCertificateManager; + private ManagedUserCertificateManager mUserCertificateManager; + + private DatabaseHelper mDatabaseHelper; + + private final BroadcastReceiver mRestrictionsReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + Log.d(TAG, "Managed configuration changed"); + reloadManagedConfigurationAndNotifyListeners(); + } + }; + + static + { Security.addProvider(new LocalCertificateKeyStoreProvider()); } @@ -47,10 +81,56 @@ public class StrongSwanApplication extends Application { super.onCreate(); StrongSwanApplication.mContext = getApplicationContext(); + StrongSwanApplication.mInstance = this; + + mDatabaseHelper = new DatabaseHelper(mContext); + + mManagedConfigurationService = new ManagedConfigurationService(mContext); + + mTrustedCertificateManager = new ManagedTrustedCertificateManager(mContext, mExecutorService, mMainHandler, + mManagedConfigurationService, mDatabaseHelper); + + mUserCertificateManager = new ManagedUserCertificateManager(mContext, mManagedConfigurationService, mDatabaseHelper); + + ProcessLifecycleOwner.get().getLifecycle().addObserver(this); + } + + @Override + public void onResume(@NonNull LifecycleOwner owner) + { + reloadManagedConfigurationAndNotifyListeners(); + + final IntentFilter restrictionsFilter = new IntentFilter(Intent.ACTION_APPLICATION_RESTRICTIONS_CHANGED); + registerReceiver(mRestrictionsReceiver, restrictionsFilter); + } + + @Override + public void onPause(@NonNull LifecycleOwner owner) + { + unregisterReceiver(mRestrictionsReceiver); + } + + private void reloadManagedConfigurationAndNotifyListeners() + { + final Set uuids = new HashSet<>(mManagedConfigurationService.getManagedProfiles().keySet()); + + mManagedConfigurationService.loadConfiguration(); + mManagedConfigurationService.updateSettings(); + + mUserCertificateManager.update(); + mTrustedCertificateManager.update(() -> { + uuids.addAll(mManagedConfigurationService.getManagedProfiles().keySet()); + + Log.d(TAG, "Send profiles changed broadcast"); + Intent profilesChanged = new Intent(Constants.VPN_PROFILES_CHANGED); + profilesChanged.putExtra(Constants.VPN_PROFILES_MULTIPLE, uuids.toArray(new String[0])); + LocalBroadcastManager.getInstance(mContext).sendBroadcast(profilesChanged); + }); } /** * Returns the current application context + * * @return context */ public static Context getContext() @@ -58,8 +138,19 @@ public class StrongSwanApplication extends Application return StrongSwanApplication.mContext; } + /** + * Returns the current application object + * + * @return application + */ + public static StrongSwanApplication getInstance() + { + return StrongSwanApplication.mInstance; + } + /** * Returns a thread pool to run tasks in separate threads + * * @return thread pool */ public Executor getExecutor() @@ -69,6 +160,7 @@ public class StrongSwanApplication extends Application /** * Returns a handler to execute stuff by the main thread. + * * @return handler */ public Handler getHandler() @@ -76,27 +168,30 @@ public class StrongSwanApplication extends Application return mMainHandler; } + /** + * Returns a service providing access to the app's managed configuration. + * + * @return managed configuration + */ + public ManagedConfigurationService getManagedConfigurationService() + { + return mManagedConfigurationService; + } + + /** + * @return the application's database helper used to access its SQLite database + */ + public DatabaseHelper getDatabaseHelper() + { + return mDatabaseHelper; + } + /* * The libraries are extracted to /data/data/org.strongswan.android/... * during installation. On newer releases most are loaded in JNI_OnLoad. */ static { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) - { - System.loadLibrary("strongswan"); - - if (MainActivity.USE_BYOD) - { - System.loadLibrary("tpmtss"); - System.loadLibrary("tncif"); - System.loadLibrary("tnccs"); - System.loadLibrary("imcv"); - } - - System.loadLibrary("charon"); - System.loadLibrary("ipsec"); - } System.loadLibrary("androidbridge"); } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java index 53c22d45a..ccce06e4c 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/logic/VpnStateService.java @@ -55,11 +55,11 @@ public class VpnStateService extends Service private ErrorState mError = ErrorState.NO_ERROR; private ImcState mImcState = ImcState.UNKNOWN; private final LinkedList mRemediationInstructions = new LinkedList(); - private static long RETRY_INTERVAL = 1000; + private static final long RETRY_INTERVAL = 1000; /* cap the retry interval at 2 minutes */ - private static long MAX_RETRY_INTERVAL = 120000; - private static int RETRY_MSG = 1; - private RetryTimeoutProvider mTimeoutProvider = new RetryTimeoutProvider(); + private static final long MAX_RETRY_INTERVAL = 120000; + private static final int RETRY_MSG = 1; + private final RetryTimeoutProvider mTimeoutProvider = new RetryTimeoutProvider(); private long mRetryTimeout; private long mRetryIn; @@ -89,7 +89,7 @@ public class VpnStateService extends Service */ public interface VpnStateListener { - public void stateChanged(); + void stateChanged(); } /** @@ -169,6 +169,7 @@ public class VpnStateService extends Service /** * Get the total number of seconds until there is an automatic retry to reconnect. + * * @return total number of seconds until the retry */ public int getRetryTimeout() @@ -178,6 +179,7 @@ public class VpnStateService extends Service /** * Get the number of seconds until there is an automatic retry to reconnect. + * * @return number of seconds until the retry */ public int getRetryIn() @@ -283,8 +285,9 @@ public class VpnStateService extends Service /** * Connect (or reconnect) a profile + * * @param profileInfo optional profile info (basically the UUID and password), taken from the - * previous profile if null + * previous profile if null * @param fromScratch true if this is a manual retry/reconnect or a completely new connection */ public void connect(Bundle profileInfo, boolean fromScratch) @@ -330,7 +333,7 @@ public class VpnStateService extends Service Intent intent = new Intent(this, VpnProfileControlActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setAction(VpnProfileControlActivity.START_PROFILE); - intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_ID, mProfile.getUUID().toString()); + intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, mProfile.getUUID().toString()); startActivity(intent); /* reset the retry timer immediately in case the user needs more time to enter the password */ notifyListeners(() -> { @@ -353,7 +356,8 @@ public class VpnStateService extends Service */ private void notifyListeners(final Callable change) { - mHandler.post(new Runnable() { + mHandler.post(new Runnable() + { @Override public void run() { @@ -386,7 +390,8 @@ public class VpnStateService extends Service */ public void startConnection(final VpnProfile profile) { - notifyListeners(new Callable() { + notifyListeners(new Callable() + { @Override public Boolean call() throws Exception { @@ -411,7 +416,8 @@ public class VpnStateService extends Service */ public void setState(final State state) { - notifyListeners(new Callable() { + notifyListeners(new Callable() + { @Override public Boolean call() throws Exception { @@ -438,7 +444,8 @@ public class VpnStateService extends Service */ public void setError(final ErrorState error) { - notifyListeners(new Callable() { + notifyListeners(new Callable() + { @Override public Boolean call() throws Exception { @@ -471,7 +478,8 @@ public class VpnStateService extends Service */ public void setImcState(final ImcState state) { - notifyListeners(new Callable() { + notifyListeners(new Callable() + { @Override public Boolean call() throws Exception { @@ -501,7 +509,8 @@ public class VpnStateService extends Service */ public void addRemediationInstruction(final RemediationInstruction instruction) { - mHandler.post(new Runnable() { + mHandler.post(new Runnable() + { @Override public void run() { @@ -535,7 +544,8 @@ public class VpnStateService extends Service /** * Special Handler subclass that handles the retry countdown (more accurate than CountDownTimer) */ - private static class RetryHandler extends Handler { + private static class RetryHandler extends Handler + { WeakReference mService; public RetryHandler(Looper looper, VpnStateService service) @@ -604,6 +614,7 @@ public class VpnStateService extends Service /** * Called each time a new retry timeout is started. The timeout increases until reset() is * called and the base timeout is returned again. + * * @param error Error state */ public long getTimeout(ErrorState error) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java index f2d8939ec..603dedef9 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/MainActivity.java @@ -21,7 +21,6 @@ package org.strongswan.android.ui; import android.app.Dialog; import android.content.DialogInterface; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.text.format.Formatter; import android.view.Menu; @@ -29,6 +28,8 @@ import android.view.MenuItem; import android.widget.Toast; import org.strongswan.android.R; +import org.strongswan.android.data.ManagedConfiguration; +import org.strongswan.android.data.ManagedConfigurationService; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.logic.StrongSwanApplication; import org.strongswan.android.logic.TrustedCertificateManager; @@ -58,6 +59,8 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec private static final String DIALOG_TAG = "Dialog"; + private ManagedConfigurationService mManagedConfigurationService; + @Override public void onCreate(Bundle savedInstanceState) { @@ -73,6 +76,8 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec ((StrongSwanApplication)getApplication()).getExecutor().execute(() -> { TrustedCertificateManager.getInstance().load(); }); + + mManagedConfigurationService = StrongSwanApplication.getInstance().getManagedConfigurationService(); } @Override @@ -85,9 +90,12 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec @Override public boolean onPrepareOptionsMenu(Menu menu) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) + final MenuItem importProfile = menu.findItem(R.id.menu_import_profile); + if (importProfile != null) { - menu.removeItem(R.id.menu_import_profile); + final ManagedConfiguration managedConfiguration = mManagedConfigurationService.getManagedConfiguration(); + importProfile.setVisible(managedConfiguration.isAllowProfileImport()); + importProfile.setEnabled(managedConfiguration.isAllowProfileImport()); } return true; } @@ -126,7 +134,7 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec { Intent intent = new Intent(this, VpnProfileControlActivity.class); intent.setAction(VpnProfileControlActivity.START_PROFILE); - intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_ID, profile.getUUID().toString()); + intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, profile.getUUID().toString()); startActivity(intent); } @@ -195,26 +203,26 @@ public class MainActivity extends AppCompatActivity implements OnVpnProfileSelec size = Formatter.formatFileSize(getActivity(), s); AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) - .setTitle(R.string.clear_crl_cache_title) - .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() + .setTitle(R.string.clear_crl_cache_title) + .setNegativeButton(android.R.string.cancel, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int which) { - @Override - public void onClick(DialogInterface dialog, int which) - { - dismiss(); - } - }) - .setPositiveButton(R.string.clear, new DialogInterface.OnClickListener() + dismiss(); + } + }) + .setPositiveButton(R.string.clear, new DialogInterface.OnClickListener() + { + @Override + public void onClick(DialogInterface dialog, int whichButton) { - @Override - public void onClick(DialogInterface dialog, int whichButton) + for (String file : list) { - for (String file : list) - { - getActivity().deleteFile(file); - } + getActivity().deleteFile(file); } - }); + } + }); builder.setMessage(getActivity().getResources().getQuantityString(R.plurals.clear_crl_cache_msg, list.size(), list.size(), size)); return builder.create(); } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsListFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsListFragment.java index e2d949a68..5ef78b295 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsListFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SelectedApplicationsListFragment.java @@ -60,9 +60,11 @@ public class SelectedApplicationsListFragment extends ListFragment implements Lo super.onViewCreated(view, savedInstanceState); setHasOptionsMenu(true); - getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); + final boolean readOnly = getActivity().getIntent().getBooleanExtra(VpnProfileDataSource.KEY_READ_ONLY, false); + getListView().setChoiceMode(readOnly ? ListView.CHOICE_MODE_NONE : ListView.CHOICE_MODE_MULTIPLE); mAdapter = new SelectedApplicationsAdapter(getActivity()); + mAdapter.setReadOnly(readOnly); setListAdapter(mAdapter); setListShown(false); @@ -101,6 +103,11 @@ public class SelectedApplicationsListFragment extends ListFragment implements Lo @Override public void onListItemClick(ListView l, View v, int position, long id) { + if (mAdapter.isReadOnly()) + { + return; + } + super.onListItemClick(l, v, position, id); SelectedApplicationEntry item = (SelectedApplicationEntry)getListView().getItemAtPosition(position); item.setSelected(!item.isSelected()); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SettingsFragment.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SettingsFragment.java index 98e399256..771e5a20b 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SettingsFragment.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/SettingsFragment.java @@ -16,13 +16,21 @@ package org.strongswan.android.ui; +import static org.strongswan.android.utils.Constants.PREF_DEFAULT_VPN_PROFILE; +import static org.strongswan.android.utils.Constants.PREF_DEFAULT_VPN_PROFILE_MRU; +import static org.strongswan.android.utils.Constants.PREF_IGNORE_POWER_WHITELIST; + import android.content.SharedPreferences; import android.os.Build; import android.os.Bundle; import org.strongswan.android.R; +import org.strongswan.android.data.ManagedConfiguration; +import org.strongswan.android.data.ManagedConfigurationService; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.data.VpnProfileSource; +import org.strongswan.android.logic.StrongSwanApplication; import java.util.ArrayList; import java.util.Collections; @@ -33,25 +41,31 @@ import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; import androidx.preference.PreferenceManager; - -import static org.strongswan.android.utils.Constants.PREF_DEFAULT_VPN_PROFILE; -import static org.strongswan.android.utils.Constants.PREF_DEFAULT_VPN_PROFILE_MRU; +import androidx.preference.SwitchPreference; public class SettingsFragment extends PreferenceFragmentCompat implements Preference.OnPreferenceChangeListener { + private ManagedConfigurationService mManagedConfigurationService; + private ListPreference mDefaultVPNProfile; + private SwitchPreference mIgnorePowerWhitelist; @Override public void onCreatePreferences(Bundle bundle, String s) { + mManagedConfigurationService = StrongSwanApplication.getInstance().getManagedConfigurationService(); + mManagedConfigurationService.updateSettings(); + setPreferencesFromResource(R.xml.settings, s); - mDefaultVPNProfile = (ListPreference)findPreference(PREF_DEFAULT_VPN_PROFILE); + mDefaultVPNProfile = findPreference(PREF_DEFAULT_VPN_PROFILE); mDefaultVPNProfile.setOnPreferenceChangeListener(this); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) { mDefaultVPNProfile.setEnabled(false); } + + mIgnorePowerWhitelist = findPreference(PREF_IGNORE_POWER_WHITELIST); } @Override @@ -59,11 +73,12 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Prefer { super.onResume(); - VpnProfileDataSource profiles = new VpnProfileDataSource(getActivity()); + VpnProfileDataSource profiles = new VpnProfileSource(getActivity()); profiles.open(); List all = profiles.getAllVpnProfiles(); - Collections.sort(all, new Comparator() { + Collections.sort(all, new Comparator() + { @Override public int compare(VpnProfile lhs, VpnProfile rhs) { @@ -84,7 +99,8 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Prefer } profiles.close(); - if (entries.size() <= 1) + final ManagedConfiguration managedConfiguration = mManagedConfigurationService.getManagedConfiguration(); + if (entries.size() <= 1 || !managedConfiguration.isAllowSettingsAccess()) { mDefaultVPNProfile.setEnabled(false); } @@ -94,6 +110,10 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Prefer mDefaultVPNProfile.setEntries(entries.toArray(new CharSequence[0])); mDefaultVPNProfile.setEntryValues(entryvalues.toArray(new CharSequence[0])); } + if (!managedConfiguration.isAllowSettingsAccess()) + { + mIgnorePowerWhitelist.setEnabled(false); + } SharedPreferences pref = PreferenceManager.getDefaultSharedPreferences(getActivity()); setCurrentProfileName(pref.getString(PREF_DEFAULT_VPN_PROFILE, PREF_DEFAULT_VPN_PROFILE_MRU)); @@ -111,7 +131,7 @@ public class SettingsFragment extends PreferenceFragmentCompat implements Prefer private void setCurrentProfileName(String uuid) { - VpnProfileDataSource profiles = new VpnProfileDataSource(getActivity()); + VpnProfileDataSource profiles = new VpnProfileSource(getActivity()); profiles.open(); if (!uuid.equals(PREF_DEFAULT_VPN_PROFILE_MRU)) diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificateImportActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificateImportActivity.java index def0b88a5..60e57b03b 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificateImportActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificateImportActivity.java @@ -16,13 +16,11 @@ package org.strongswan.android.ui; -import android.annotation.TargetApi; import android.app.Dialog; import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.widget.Toast; @@ -61,7 +59,6 @@ public class TrustedCertificateImportActivity extends AppCompatActivity } ); - @TargetApi(Build.VERSION_CODES.KITKAT) @Override public void onCreate(Bundle savedInstanceState) { @@ -78,7 +75,7 @@ public class TrustedCertificateImportActivity extends AppCompatActivity { importCertificate(intent.getData()); } - else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + else { Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); openIntent.setType("*/*"); @@ -89,7 +86,6 @@ public class TrustedCertificateImportActivity extends AppCompatActivity catch (ActivityNotFoundException e) { /* some devices are unable to browse for files */ finish(); - return; } } } diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificatesActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificatesActivity.java index c941a49e9..0a0b26a8a 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificatesActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/TrustedCertificatesActivity.java @@ -17,7 +17,6 @@ package org.strongswan.android.ui; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; @@ -26,7 +25,10 @@ import com.google.android.material.tabs.TabLayout; import com.google.android.material.tabs.TabLayoutMediator; import org.strongswan.android.R; +import org.strongswan.android.data.ManagedConfiguration; +import org.strongswan.android.data.ManagedConfigurationService; import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.logic.StrongSwanApplication; import org.strongswan.android.logic.TrustedCertificateManager; import org.strongswan.android.logic.TrustedCertificateManager.TrustedCertificateSource; import org.strongswan.android.security.TrustedCertificateEntry; @@ -52,6 +54,8 @@ public class TrustedCertificatesActivity extends AppCompatActivity implements Tr private ViewPager2 mPager; private boolean mSelect; + private ManagedConfigurationService mManagedConfigurationService; + private final ActivityResultLauncher mImportCertificate = registerForActivityResult( new ActivityResultContracts.StartActivityForResult(), result -> { @@ -73,15 +77,16 @@ public class TrustedCertificatesActivity extends AppCompatActivity implements Tr mAdapter = new TrustedCertificatesPagerAdapter(this); - mPager = (ViewPager2)findViewById(R.id.viewpager); + mPager = findViewById(R.id.viewpager); mPager.setAdapter(mAdapter); - TabLayout tabs = (TabLayout)findViewById(R.id.tabs); + TabLayout tabs = findViewById(R.id.tabs); new TabLayoutMediator(tabs, mPager, (tab, position) -> { tab.setText(mAdapter.getTitle(position)); }).attach(); mSelect = SELECT_CERTIFICATE.equals(getIntent().getAction()); + mManagedConfigurationService = StrongSwanApplication.getInstance().getManagedConfigurationService(); } @Override @@ -94,9 +99,12 @@ public class TrustedCertificatesActivity extends AppCompatActivity implements Tr @Override public boolean onPrepareOptionsMenu(Menu menu) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) + final MenuItem importCertificate = menu.findItem(R.id.menu_import_certificate); + if (importCertificate != null) { - menu.removeItem(R.id.menu_import_certificate); + final ManagedConfiguration managedConfiguration = mManagedConfigurationService.getManagedConfiguration(); + importCertificate.setVisible(managedConfiguration.isAllowCertificateImport()); + importCertificate.setEnabled(managedConfiguration.isAllowCertificateImport()); } return true; } @@ -164,7 +172,7 @@ public class TrustedCertificatesActivity extends AppCompatActivity implements Tr public static class TrustedCertificatesPagerAdapter extends FragmentStateAdapter { - private TrustedCertificatesTab mTabs[]; + private final TrustedCertificatesTab[] mTabs; public TrustedCertificatesPagerAdapter(@NonNull FragmentActivity fragmentActivity) { diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileControlActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileControlActivity.java index 1913bbb5f..d9ef939ba 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileControlActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileControlActivity.java @@ -42,12 +42,12 @@ import android.widget.Toast; import org.strongswan.android.R; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.data.VpnProfileSource; import org.strongswan.android.data.VpnType.VpnTypeFeature; import org.strongswan.android.logic.VpnStateService; import org.strongswan.android.logic.VpnStateService.State; import org.strongswan.android.utils.Constants; -import androidx.activity.result.ActivityResultCallback; import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; @@ -64,7 +64,7 @@ public class VpnProfileControlActivity extends AppCompatActivity { public static final String START_PROFILE = "org.strongswan.android.action.START_PROFILE"; public static final String DISCONNECT = "org.strongswan.android.action.DISCONNECT"; - public static final String EXTRA_VPN_PROFILE_ID = "org.strongswan.android.VPN_PROFILE_ID"; + public static final String EXTRA_VPN_PROFILE_UUID = "org.strongswan.android.VPN_PROFILE_UUID"; private static final String WAITING_FOR_RESULT = "WAITING_FOR_RESULT"; private static final String PROFILE_NAME = "PROFILE_NAME"; @@ -257,6 +257,7 @@ public class VpnProfileControlActivity extends AppCompatActivity /** * Check if we have permission to display notifications to the user, if necessary, * ask the user to allow this. + * * @return true if profile can be initiated immediately */ private boolean checkNotificationPermission() @@ -274,6 +275,7 @@ public class VpnProfileControlActivity extends AppCompatActivity /** * Check if we are on the system's power whitelist, if necessary, or ask the user * to add us. + * * @return true if profile can be initiated immediately */ private boolean checkPowerWhitelist() @@ -373,21 +375,13 @@ public class VpnProfileControlActivity extends AppCompatActivity { VpnProfile profile = null; - VpnProfileDataSource dataSource = new VpnProfileDataSource(this); + VpnProfileDataSource dataSource = new VpnProfileSource(this); dataSource.open(); - String profileUUID = intent.getStringExtra(EXTRA_VPN_PROFILE_ID); + String profileUUID = intent.getStringExtra(EXTRA_VPN_PROFILE_UUID); if (profileUUID != null) { profile = dataSource.getVpnProfile(profileUUID); } - else - { - long profileId = intent.getLongExtra(EXTRA_VPN_PROFILE_ID, 0); - if (profileId > 0) - { - profile = dataSource.getVpnProfile(profileId); - } - } dataSource.close(); if (profile != null) @@ -412,10 +406,10 @@ public class VpnProfileControlActivity extends AppCompatActivity removeFragmentByTag(DIALOG_TAG); - String profileUUID = intent.getStringExtra(EXTRA_VPN_PROFILE_ID); + String profileUUID = intent.getStringExtra(EXTRA_VPN_PROFILE_UUID); if (profileUUID != null) { - VpnProfileDataSource dataSource = new VpnProfileDataSource(this); + VpnProfileDataSource dataSource = new VpnProfileSource(this); dataSource.open(); profile = dataSource.getVpnProfile(profileUUID); dataSource.close(); @@ -583,9 +577,9 @@ public class VpnProfileControlActivity extends AppCompatActivity final Bundle profileInfo = getArguments(); LayoutInflater inflater = getActivity().getLayoutInflater(); View view = inflater.inflate(R.layout.login_dialog, null); - EditText username = (EditText)view.findViewById(R.id.username); + EditText username = view.findViewById(R.id.username); username.setText(profileInfo.getString(VpnProfileDataSource.KEY_USERNAME)); - final EditText password = (EditText)view.findViewById(R.id.password); + final EditText password = view.findViewById(R.id.password); AlertDialog.Builder adb = new AlertDialog.Builder(getActivity()); adb.setView(view); 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 dc3bc1cc7..4a6aac45f 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 @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Relution GmbH * Copyright (C) 2012-2020 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager @@ -22,7 +23,6 @@ import android.app.Dialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; -import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.security.KeyChain; @@ -44,7 +44,6 @@ import android.view.ViewGroup; import android.widget.AdapterView; import android.widget.AdapterView.OnItemSelectedListener; import android.widget.ArrayAdapter; -import android.widget.Button; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.CompoundButton.OnCheckedChangeListener; @@ -58,6 +57,7 @@ import org.strongswan.android.R; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfile.SelectedAppsHandling; import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.data.VpnProfileSource; import org.strongswan.android.data.VpnType; import org.strongswan.android.data.VpnType.VpnTypeFeature; import org.strongswan.android.logic.StrongSwanApplication; @@ -89,7 +89,7 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager; public class VpnProfileDetailActivity extends AppCompatActivity { private VpnProfileDataSource mDataSource; - private Long mId; + private String mUuid; private TrustedCertificateEntry mCertEntry; private String mUserCertLoading; private CertificateIdentitiesAdapter mSelectUserIdAdapter; @@ -98,6 +98,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity private SelectedAppsHandling mSelectedAppsHandling = SelectedAppsHandling.SELECTED_APPS_DISABLE; private SortedSet mSelectedApps = new TreeSet<>(); private VpnProfile mProfile; + private View mManagedProfile; private MultiAutoCompleteTextView mName; private TextInputLayoutHelper mNameWrap; private EditText mGateway; @@ -190,69 +191,71 @@ public class VpnProfileDetailActivity extends AppCompatActivity /* the title is set when we load the profile, if any */ getSupportActionBar().setDisplayHomeAsUpEnabled(true); - mDataSource = new VpnProfileDataSource(this); + mDataSource = new VpnProfileSource(this); mDataSource.open(); setContentView(R.layout.profile_detail_view); - mName = (MultiAutoCompleteTextView)findViewById(R.id.name); - mNameWrap = (TextInputLayoutHelper)findViewById(R.id.name_wrap); - mGateway = (EditText)findViewById(R.id.gateway); - mGatewayWrap = (TextInputLayoutHelper) findViewById(R.id.gateway_wrap); - mSelectVpnType = (Spinner)findViewById(R.id.vpn_type); - mTncNotice = (RelativeLayout)findViewById(R.id.tnc_notice); + mManagedProfile = findViewById(R.id.managed_profile); - 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); + mName = findViewById(R.id.name); + mNameWrap = findViewById(R.id.name_wrap); + mGateway = findViewById(R.id.gateway); + mGatewayWrap = findViewById(R.id.gateway_wrap); + mSelectVpnType = findViewById(R.id.vpn_type); + mTncNotice = findViewById(R.id.tnc_notice); - mUserCertificate = (ViewGroup)findViewById(R.id.user_certificate_group); - mSelectUserCert = (RelativeLayout)findViewById(R.id.select_user_certificate); + mUsernamePassword = findViewById(R.id.username_password_group); + mUsername = findViewById(R.id.username); + mUsernameWrap = findViewById(R.id.username_wrap); + mPassword = findViewById(R.id.password); - mCheckAuto = (CheckBox)findViewById(R.id.ca_auto); - mSelectCert = (RelativeLayout)findViewById(R.id.select_certificate); + mUserCertificate = findViewById(R.id.user_certificate_group); + mSelectUserCert = findViewById(R.id.select_user_certificate); - mShowAdvanced = (CheckBox)findViewById(R.id.show_advanced); - mAdvancedSettings = (ViewGroup)findViewById(R.id.advanced_settings); + mCheckAuto = findViewById(R.id.ca_auto); + mSelectCert = findViewById(R.id.select_certificate); - mRemoteId = (MultiAutoCompleteTextView)findViewById(R.id.remote_id); - mRemoteIdWrap = (TextInputLayoutHelper) findViewById(R.id.remote_id_wrap); + mShowAdvanced = findViewById(R.id.show_advanced); + mAdvancedSettings = findViewById(R.id.advanced_settings); + + mRemoteId = findViewById(R.id.remote_id); + mRemoteIdWrap = findViewById(R.id.remote_id_wrap); mLocalId = findViewById(R.id.local_id); mLocalIdWrap = findViewById(R.id.local_id_wrap); mDnsServers = findViewById(R.id.dns_servers); mDnsServersWrap = findViewById(R.id.dns_servers_wrap); - mMTU = (EditText)findViewById(R.id.mtu); - mMTUWrap = (TextInputLayoutHelper) findViewById(R.id.mtu_wrap); - mPort = (EditText)findViewById(R.id.port); - mPortWrap = (TextInputLayoutHelper) findViewById(R.id.port_wrap); - mNATKeepalive = (EditText)findViewById(R.id.nat_keepalive); - mNATKeepaliveWrap = (TextInputLayoutHelper) findViewById(R.id.nat_keepalive_wrap); + mMTU = findViewById(R.id.mtu); + mMTUWrap = findViewById(R.id.mtu_wrap); + mPort = findViewById(R.id.port); + mPortWrap = findViewById(R.id.port_wrap); + mNATKeepalive = findViewById(R.id.nat_keepalive); + mNATKeepaliveWrap = findViewById(R.id.nat_keepalive_wrap); mCertReq = findViewById(R.id.cert_req); mUseCrl = findViewById(R.id.use_crl); mUseOcsp = findViewById(R.id.use_ocsp); - mStrictRevocation= findViewById(R.id.strict_revocation); - mRsaPss= findViewById(R.id.rsa_pss); - mIPv6Transport= findViewById(R.id.ipv6_transport); - mIncludedSubnets = (EditText)findViewById(R.id.included_subnets); - mIncludedSubnetsWrap = (TextInputLayoutHelper)findViewById(R.id.included_subnets_wrap); - mExcludedSubnets = (EditText)findViewById(R.id.excluded_subnets); - mExcludedSubnetsWrap = (TextInputLayoutHelper)findViewById(R.id.excluded_subnets_wrap); - mBlockIPv4 = (CheckBox)findViewById(R.id.split_tunneling_v4); - mBlockIPv6 = (CheckBox)findViewById(R.id.split_tunneling_v6); + mStrictRevocation = findViewById(R.id.strict_revocation); + mRsaPss = findViewById(R.id.rsa_pss); + mIPv6Transport = findViewById(R.id.ipv6_transport); + mIncludedSubnets = findViewById(R.id.included_subnets); + mIncludedSubnetsWrap = findViewById(R.id.included_subnets_wrap); + mExcludedSubnets = findViewById(R.id.excluded_subnets); + mExcludedSubnetsWrap = findViewById(R.id.excluded_subnets_wrap); + mBlockIPv4 = findViewById(R.id.split_tunneling_v4); + mBlockIPv6 = findViewById(R.id.split_tunneling_v6); - mSelectSelectedAppsHandling = (Spinner)findViewById(R.id.apps_handling); - mSelectApps = (RelativeLayout)findViewById(R.id.select_applications); + mSelectSelectedAppsHandling = findViewById(R.id.apps_handling); + mSelectApps = findViewById(R.id.select_applications); - mIkeProposal = (EditText)findViewById(R.id.ike_proposal); - mIkeProposalWrap = (TextInputLayoutHelper)findViewById(R.id.ike_proposal_wrap); - mEspProposal = (EditText)findViewById(R.id.esp_proposal); - mEspProposalWrap = (TextInputLayoutHelper)findViewById(R.id.esp_proposal_wrap); + mIkeProposal = findViewById(R.id.ike_proposal); + mIkeProposalWrap = findViewById(R.id.ike_proposal_wrap); + mEspProposal = findViewById(R.id.esp_proposal); + mEspProposalWrap = findViewById(R.id.esp_proposal_wrap); /* make the link clickable */ ((TextView)findViewById(R.id.proposal_intro)).setMovementMethod(LinkMovementMethod.getInstance()); - mProfileIdLabel = (TextView)findViewById(R.id.profile_id_label); - mProfileId = (TextView)findViewById(R.id.profile_id); + mProfileIdLabel = findViewById(R.id.profile_id_label); + mProfileId = findViewById(R.id.profile_id); final SpaceTokenizer spaceTokenizer = new SpaceTokenizer(); mName.setTokenizer(spaceTokenizer); @@ -262,14 +265,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity mName.setAdapter(gatewayAdapter); mRemoteId.setAdapter(gatewayAdapter); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + mGateway.addTextChangedListener(new TextWatcher() { - findViewById(R.id.apps).setVisibility(View.GONE); - mSelectSelectedAppsHandling.setVisibility(View.GONE); - mSelectApps.setVisibility(View.GONE); - } - - mGateway.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @@ -294,7 +291,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity } }); - mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() { + mSelectVpnType.setOnItemSelectedListener(new OnItemSelectedListener() + { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { @@ -312,7 +310,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity ((TextView)mTncNotice.findViewById(android.R.id.text1)).setText(R.string.tnc_notice_title); ((TextView)mTncNotice.findViewById(android.R.id.text2)).setText(R.string.tnc_notice_subtitle); - mTncNotice.setOnClickListener(new OnClickListener() { + mTncNotice.setOnClickListener(new OnClickListener() + { @Override public void onClick(View v) { @@ -321,14 +320,15 @@ public class VpnProfileDetailActivity extends AppCompatActivity }); mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener()); - ((Button)findViewById(R.id.install_user_certificate)).setOnClickListener(v -> { + findViewById(R.id.install_user_certificate).setOnClickListener(v -> { Intent intent = KeyChain.createInstallIntent(); mInstallPKCS12.launch(intent); }); mSelectUserIdAdapter = new CertificateIdentitiesAdapter(this); mLocalId.setAdapter(mSelectUserIdAdapter); - mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() { + mCheckAuto.setOnCheckedChangeListener(new OnCheckedChangeListener() + { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @@ -336,7 +336,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity } }); - mSelectCert.setOnClickListener(new OnClickListener() { + mSelectCert.setOnClickListener(new OnClickListener() + { @Override public void onClick(View v) { @@ -346,7 +347,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity } }); - mShowAdvanced.setOnCheckedChangeListener(new OnCheckedChangeListener() { + mShowAdvanced.setOnCheckedChangeListener(new OnCheckedChangeListener() + { @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { @@ -354,7 +356,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity } }); - mSelectSelectedAppsHandling.setOnItemSelectedListener(new OnItemSelectedListener() { + mSelectSelectedAppsHandling.setOnItemSelectedListener(new OnItemSelectedListener() + { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { @@ -370,21 +373,23 @@ public class VpnProfileDetailActivity extends AppCompatActivity } }); - mSelectApps.setOnClickListener(new OnClickListener() { + mSelectApps.setOnClickListener(new OnClickListener() + { @Override public void onClick(View v) { Intent intent = new Intent(VpnProfileDetailActivity.this, SelectedApplicationsActivity.class); intent.putExtra(VpnProfileDataSource.KEY_SELECTED_APPS_LIST, new ArrayList<>(mSelectedApps)); + intent.putExtra(VpnProfileDataSource.KEY_READ_ONLY, mProfile.isReadOnly()); mSelectApplications.launch(intent); } }); - mId = savedInstanceState == null ? null : savedInstanceState.getLong(VpnProfileDataSource.KEY_ID); - if (mId == null) + mUuid = savedInstanceState == null ? null : savedInstanceState.getString(VpnProfileDataSource.KEY_UUID); + if (mUuid == null) { Bundle extras = getIntent().getExtras(); - mId = extras == null ? null : extras.getLong(VpnProfileDataSource.KEY_ID); + mUuid = extras == null ? null : extras.getString(VpnProfileDataSource.KEY_UUID); } loadProfileData(savedInstanceState); @@ -406,9 +411,9 @@ public class VpnProfileDetailActivity extends AppCompatActivity protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - if (mId != null) + if (mUuid != null) { - outState.putLong(VpnProfileDataSource.KEY_ID, mId); + outState.putString(VpnProfileDataSource.KEY_UUID, mUuid); } if (mUserCertEntry != null) { @@ -489,7 +494,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity AlertDialog.Builder adb = new AlertDialog.Builder(VpnProfileDetailActivity.this); adb.setTitle(R.string.alert_text_nocertfound_title); adb.setMessage(R.string.alert_text_nocertfound); - adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + adb.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() + { @Override public void onClick(DialogInterface dialog, int id) { @@ -614,16 +620,17 @@ public class VpnProfileDetailActivity extends AppCompatActivity mDataSource.insertProfile(mProfile); } Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); - intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId()); + intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getUUID().toString()); LocalBroadcastManager.getInstance(this).sendBroadcast(intent); - setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_ID, mProfile.getId())); + setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_UUID, mProfile.getUUID().toString())); finish(); } } /** * Verify the user input and display error messages. + * * @return true if the input is valid */ private boolean verifyInput() @@ -755,9 +762,9 @@ public class VpnProfileDetailActivity extends AppCompatActivity Integer flags = null; getSupportActionBar().setTitle(R.string.add_profile); - if (mId != null && mId != 0) + if (mUuid != null) { - mProfile = mDataSource.getVpnProfile(mId); + mProfile = mDataSource.getVpnProfile(mUuid); if (mProfile != null) { mName.setText(mProfile.getName()); @@ -785,11 +792,13 @@ public class VpnProfileDetailActivity extends AppCompatActivity local_id = mProfile.getLocalId(); alias = mProfile.getCertificateAlias(); getSupportActionBar().setTitle(mProfile.getName()); + + setReadOnly(mProfile.isReadOnly()); } else { Log.e(VpnProfileDetailActivity.class.getSimpleName(), - "VPN profile with id " + mId + " not found"); + "VPN profile with UUID " + mUuid + " not found"); finish(); } } @@ -849,6 +858,46 @@ public class VpnProfileDetailActivity extends AppCompatActivity } } + private void setReadOnly(final boolean readOnly) + { + mManagedProfile.setVisibility(readOnly ? View.VISIBLE : View.GONE); + + mName.setEnabled(!readOnly); + mGateway.setEnabled(!readOnly); + mUsername.setEnabled(!readOnly); + mRemoteId.setEnabled(!readOnly); + mLocalId.setEnabled(!readOnly); + mMTU.setEnabled(!readOnly); + mPort.setEnabled(!readOnly); + mNATKeepalive.setEnabled(!readOnly); + mIncludedSubnets.setEnabled(!readOnly); + mExcludedSubnets.setEnabled(!readOnly); + mBlockIPv4.setEnabled(!readOnly); + mBlockIPv6.setEnabled(!readOnly); + mIkeProposal.setEnabled(!readOnly); + mEspProposal.setEnabled(!readOnly); + mDnsServers.setEnabled(!readOnly); + + mSelectVpnType.setEnabled(!readOnly); + mCertReq.setEnabled(!readOnly); + mUseCrl.setEnabled(!readOnly); + mUseOcsp.setEnabled(!readOnly); + mStrictRevocation.setEnabled(!readOnly); + mRsaPss.setEnabled(!readOnly); + mIPv6Transport.setEnabled(!readOnly); + + mCheckAuto.setEnabled(!readOnly); + mSelectSelectedAppsHandling.setEnabled(!readOnly); + + findViewById(R.id.install_user_certificate).setEnabled(!readOnly); + + if (readOnly) + { + mSelectCert.setOnClickListener(null); + mSelectUserCert.setOnClickListener(null); + } + } + /** * Get the string value in the given text box or null if empty * @@ -969,7 +1018,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity { final X509Certificate[] chain = KeyChain.getCertificateChain(VpnProfileDetailActivity.this, alias); /* alias() is not called from our main thread */ - runOnUiThread(new Runnable() { + runOnUiThread(new Runnable() + { @Override public void run() { @@ -992,7 +1042,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity /** * Callback interface for the user certificate loader. */ - private interface UserCertificateLoaderCallback { + private interface UserCertificateLoaderCallback + { void onComplete(X509Certificate result); } @@ -1050,7 +1101,8 @@ public class VpnProfileDetailActivity extends AppCompatActivity return new AlertDialog.Builder(getActivity()) .setTitle(R.string.tnc_notice_title) .setMessage(HtmlCompat.fromHtml(getString(R.string.tnc_notice_details), HtmlCompat.FROM_HTML_MODE_LEGACY)) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() + { @Override public void onClick(DialogInterface dialog, int id) { @@ -1111,7 +1163,7 @@ public class VpnProfileDetailActivity extends AppCompatActivity if (text instanceof Spanned) { SpannableString sp = new SpannableString(text + " "); - TextUtils.copySpansFrom((Spanned) text, 0, text.length(), Object.class, sp, 0); + TextUtils.copySpansFrom((Spanned)text, 0, text.length(), Object.class, sp, 0); return sp; } else 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 index d7a069d0b..c62383ec0 100644 --- 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 @@ -21,7 +21,6 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.net.Uri; -import android.os.Build; import android.os.Bundle; import android.security.KeyChain; import android.security.KeyChainAliasCallback; @@ -46,6 +45,7 @@ import org.strongswan.android.R; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfile.SelectedAppsHandling; import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.data.VpnProfileSource; import org.strongswan.android.data.VpnType; import org.strongswan.android.data.VpnType.VpnTypeFeature; import org.strongswan.android.logic.TrustedCertificateManager; @@ -135,12 +135,12 @@ public class VpnProfileImportActivity extends AppCompatActivity } ); - private LoaderManager.LoaderCallbacks mProfileLoaderCallbacks = new LoaderManager.LoaderCallbacks() + private final LoaderManager.LoaderCallbacks mProfileLoaderCallbacks = new LoaderManager.LoaderCallbacks() { @Override public Loader onCreateLoader(int id, Bundle args) { - return new ProfileLoader(VpnProfileImportActivity.this, (Uri)args.getParcelable(PROFILE_URI)); + return new ProfileLoader(VpnProfileImportActivity.this, args.getParcelable(PROFILE_URI)); } @Override @@ -156,7 +156,7 @@ public class VpnProfileImportActivity extends AppCompatActivity } }; - private LoaderManager.LoaderCallbacks mUserCertificateLoaderCallbacks = new LoaderManager.LoaderCallbacks() + private final LoaderManager.LoaderCallbacks mUserCertificateLoaderCallbacks = new LoaderManager.LoaderCallbacks() { @Override public Loader onCreateLoader(int id, Bundle args) @@ -185,29 +185,29 @@ public class VpnProfileImportActivity extends AppCompatActivity getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_close_white_24dp); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - mDataSource = new VpnProfileDataSource(this); + mDataSource = new VpnProfileSource(this); mDataSource.open(); setContentView(R.layout.profile_import_view); mProgressBar = findViewById(R.id.progress_bar); - 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); + mExistsWarning = findViewById(R.id.exists_warning); + mBasicDataGroup = findViewById(R.id.basic_data_group); + mName = findViewById(R.id.name); + mGateway = findViewById(R.id.gateway); + mSelectVpnType = 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); + mUsernamePassword = findViewById(R.id.username_password_group); + mUsername = findViewById(R.id.username); + mUsernameWrap = findViewById(R.id.username_wrap); + mPassword = 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); + mUserCertificate = findViewById(R.id.user_certificate_group); + mSelectUserCert = findViewById(R.id.select_user_certificate); + mImportUserCert = findViewById(R.id.import_user_certificate); - mRemoteCertificate = (ViewGroup)findViewById(R.id.remote_certificate_group); - mRemoteCert = (RelativeLayout)findViewById(R.id.remote_certificate); + mRemoteCertificate = findViewById(R.id.remote_certificate_group); + mRemoteCert = findViewById(R.id.remote_certificate); mExistsWarning.setVisibility(View.GONE); mBasicDataGroup.setVisibility(View.GONE); @@ -216,7 +216,8 @@ public class VpnProfileImportActivity extends AppCompatActivity mRemoteCertificate.setVisibility(View.GONE); mSelectUserCert.setOnClickListener(new SelectUserCertOnClickListener()); - mImportUserCert.setOnClickListener(new View.OnClickListener() { + mImportUserCert.setOnClickListener(new View.OnClickListener() + { @Override public void onClick(View v) { @@ -233,7 +234,7 @@ public class VpnProfileImportActivity extends AppCompatActivity { loadProfile(getIntent().getData()); } - else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + else { Intent openIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); openIntent.setType("*/*"); @@ -535,9 +536,9 @@ public class VpnProfileImportActivity extends AppCompatActivity if (split != null) { String included = getSubnets(split, "subnets"); - profile.setIncludedSubnets(included != null ? included : null); + profile.setIncludedSubnets(included); String excluded = getSubnets(split, "excluded"); - profile.setExcludedSubnets(excluded != null ? excluded : null); + profile.setExcludedSubnets(excluded); 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; @@ -674,7 +675,7 @@ public class VpnProfileImportActivity extends AppCompatActivity updateProfileData(); if (mExisting != null) { - mProfile.setId(mExisting.getId()); + mProfile.setUUID(mExisting.getUUID()); mDataSource.updateVpnProfile(mProfile); } else @@ -696,20 +697,21 @@ public class VpnProfileImportActivity extends AppCompatActivity } } Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); - intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getId()); + intent.putExtra(Constants.VPN_PROFILES_SINGLE, mProfile.getUUID().toString()); 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())); + setResult(RESULT_OK, new Intent().putExtra(VpnProfileDataSource.KEY_UUID, mProfile.getUUID().toString())); finish(); } } /** * Verify the user input and display error messages. + * * @return true if the input is valid */ private boolean verifyInput() @@ -899,14 +901,15 @@ public class VpnProfileImportActivity extends AppCompatActivity public void alias(final String alias) { /* alias() is not called from our main thread */ - runOnUiThread(new Runnable() { + runOnUiThread(new Runnable() + { @Override public void run() { mUserCertLoading = alias; updateUserCertView(); if (alias != null) - { /* otherwise the dialog was canceled, the request denied */ + { /* otherwise the dialog was canceled, the request denied */ LoaderManager.getInstance(VpnProfileImportActivity.this).restartLoader(USER_CERT_LOADER, null, mUserCertificateLoaderCallbacks); } } 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 291a4fcea..6a22fdf5b 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 @@ -1,4 +1,5 @@ /* + * Copyright (C) 2023 Relution GmbH * Copyright (C) 2012-2019 Tobias Brunner * Copyright (C) 2012 Giuliano Grassi * Copyright (C) 2012 Ralf Sager @@ -39,8 +40,12 @@ import android.widget.ListView; import android.widget.Toast; import org.strongswan.android.R; +import org.strongswan.android.data.ManagedConfiguration; +import org.strongswan.android.data.ManagedConfigurationService; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.data.VpnProfileSource; +import org.strongswan.android.logic.StrongSwanApplication; import org.strongswan.android.ui.adapter.VpnProfileAdapter; import org.strongswan.android.utils.Constants; @@ -48,6 +53,8 @@ import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Objects; +import java.util.Set; import java.util.UUID; import androidx.fragment.app.Fragment; @@ -62,18 +69,22 @@ public class VpnProfileListFragment extends Fragment private VpnProfileAdapter mListAdapter; private ListView mListView; private OnVpnProfileSelectedListener mListener; - private HashSet mSelected; + private Set mSelected; private boolean mReadOnly; - private BroadcastReceiver mProfilesChanged = new BroadcastReceiver() + private ManagedConfigurationService mManagedConfigurationService; + + private final 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) + String uuid; + String[] uuids; + + if ((uuid = intent.getStringExtra(Constants.VPN_PROFILES_SINGLE)) != null) { - VpnProfile profile = mDataSource.getVpnProfile(id); + VpnProfile profile = mDataSource.getVpnProfile(uuid); if (profile != null) { /* in case this was an edit, we remove it first */ mVpnProfiles.remove(profile); @@ -81,20 +92,26 @@ public class VpnProfileListFragment extends Fragment mListAdapter.notifyDataSetChanged(); } } - else if ((ids = intent.getLongArrayExtra(Constants.VPN_PROFILES_MULTIPLE)) != null) + else if ((uuids = intent.getStringArrayExtra(Constants.VPN_PROFILES_MULTIPLE)) != null) { - for (long i : ids) + for (final String id : uuids) { - Iterator profiles = mVpnProfiles.iterator(); + final Iterator profiles = mVpnProfiles.iterator(); while (profiles.hasNext()) { - VpnProfile profile = profiles.next(); - if (profile.getId() == i) - { + final VpnProfile profile = profiles.next(); + if (Objects.equals(profile.getUUID().toString(), id)) + { /* in case this was an edit, we remove it first */ profiles.remove(); break; } } + + VpnProfile profile = mDataSource.getVpnProfile(id); + if (profile != null) + { + mVpnProfiles.add(profile); + } } mListAdapter.notifyDataSetChanged(); } @@ -104,7 +121,8 @@ public class VpnProfileListFragment extends Fragment /** * The activity containing this fragment should implement this interface */ - public interface OnVpnProfileSelectedListener { + public interface OnVpnProfileSelectedListener + { void onVpnProfileSelected(VpnProfile profile); } @@ -159,9 +177,11 @@ public class VpnProfileListFragment extends Fragment mSelected = selected != null ? new HashSet<>(selected) : new HashSet<>(); } - mDataSource = new VpnProfileDataSource(this.getActivity()); + mDataSource = new VpnProfileSource(this.getActivity()); mDataSource.open(); + mManagedConfigurationService = StrongSwanApplication.getInstance().getManagedConfigurationService(); + /* cached list of profiles used as backend for the ListView */ mVpnProfiles = mDataSource.getAllVpnProfiles(); @@ -204,21 +224,32 @@ public class VpnProfileListFragment extends Fragment } @Override - public boolean onOptionsItemSelected(MenuItem item) + public void onPrepareOptionsMenu(Menu menu) { - switch (item.getItemId()) + final MenuItem addProfile = menu.findItem(R.id.add_profile); + if (addProfile != null) { - case R.id.add_profile: - Intent connectionIntent = new Intent(getActivity(), - VpnProfileDetailActivity.class); - startActivity(connectionIntent); - return true; - default: - return super.onOptionsItemSelected(item); + final ManagedConfiguration managedConfiguration = mManagedConfigurationService.getManagedConfiguration(); + addProfile.setVisible(managedConfiguration.isAllowProfileCreation()); + addProfile.setEnabled(managedConfiguration.isAllowProfileCreation()); } } - private final OnItemClickListener mVpnProfileClicked = new OnItemClickListener() { + @Override + public boolean onOptionsItemSelected(MenuItem item) + { + if (item.getItemId() == R.id.add_profile) + { + Intent connectionIntent = new Intent(getActivity(), + VpnProfileDetailActivity.class); + startActivity(connectionIntent); + return true; + } + return super.onOptionsItemSelected(item); + } + + private final OnItemClickListener mVpnProfileClicked = new OnItemClickListener() + { @Override public void onItemClick(AdapterView a, View v, int position, long id) { @@ -229,21 +260,31 @@ public class VpnProfileListFragment extends Fragment } }; - private final MultiChoiceModeListener mVpnProfileSelected = new MultiChoiceModeListener() { + private final MultiChoiceModeListener mVpnProfileSelected = new MultiChoiceModeListener() + { private MenuItem mEditProfile; private MenuItem mCopyProfile; + private MenuItem mDeleteProfile; + + private boolean mCanEdit; + private boolean mCanCopy; + private boolean mCanDelete; + + private int mReadOnlyCount; @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { - mEditProfile.setEnabled(mSelected.size() == 1); - mCopyProfile.setEnabled(mEditProfile.isEnabled()); + mEditProfile.setEnabled(mCanEdit); + mCopyProfile.setEnabled(mCanCopy); + mDeleteProfile.setEnabled(mCanDelete); return true; } @Override public void onDestroyActionMode(ActionMode mode) { + mReadOnlyCount = 0; mSelected.clear(); } @@ -254,6 +295,7 @@ public class VpnProfileListFragment extends Fragment inflater.inflate(R.menu.profile_list_context, menu); mEditProfile = menu.findItem(R.id.edit_profile); mCopyProfile = menu.findItem(R.id.copy_profile); + mDeleteProfile = menu.findItem(R.id.delete_profile); mode.setTitle(R.string.select_profiles); return true; } @@ -268,7 +310,7 @@ public class VpnProfileListFragment extends Fragment int position = mSelected.iterator().next(); VpnProfile profile = (VpnProfile)mListView.getItemAtPosition(position); Intent connectionIntent = new Intent(getActivity(), VpnProfileDetailActivity.class); - connectionIntent.putExtra(VpnProfileDataSource.KEY_ID, profile.getId()); + connectionIntent.putExtra(VpnProfileDataSource.KEY_UUID, profile.getUUID().toString()); startActivity(connectionIntent); break; } @@ -282,11 +324,11 @@ public class VpnProfileListFragment extends Fragment mDataSource.insertProfile(profile); Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); - intent.putExtra(Constants.VPN_PROFILES_SINGLE, profile.getId()); + intent.putExtra(Constants.VPN_PROFILES_SINGLE, profile.getUUID().toString()); LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); Intent connectionIntent = new Intent(getActivity(), VpnProfileDetailActivity.class); - connectionIntent.putExtra(VpnProfileDataSource.KEY_ID, profile.getId()); + connectionIntent.putExtra(VpnProfileDataSource.KEY_UUID, profile.getUUID().toString()); startActivity(connectionIntent); break; } @@ -297,15 +339,15 @@ public class VpnProfileListFragment extends Fragment { profiles.add((VpnProfile)mListView.getItemAtPosition(position)); } - long ids[] = new long[profiles.size()]; + String[] uuids = new String[profiles.size()]; for (int i = 0; i < profiles.size(); i++) { VpnProfile profile = profiles.get(i); - ids[i] = profile.getId(); + uuids[i] = profile.getUUID().toString(); mDataSource.deleteVpnProfile(profile); } Intent intent = new Intent(Constants.VPN_PROFILES_CHANGED); - intent.putExtra(Constants.VPN_PROFILES_MULTIPLE, ids); + intent.putExtra(Constants.VPN_PROFILES_MULTIPLE, uuids); LocalBroadcastManager.getInstance(getActivity()).sendBroadcast(intent); Toast.makeText(VpnProfileListFragment.this.getActivity(), R.string.profiles_deleted, Toast.LENGTH_SHORT).show(); @@ -322,13 +364,17 @@ public class VpnProfileListFragment extends Fragment public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { + VpnProfile profile = (VpnProfile)mListView.getItemAtPosition(position); + if (checked) { mSelected.add(position); + mReadOnlyCount += profile.isReadOnly() ? 1 : 0; } else { mSelected.remove(position); + mReadOnlyCount -= profile.isReadOnly() ? 1 : 0; } final int checkedCount = mSelected.size(); switch (checkedCount) @@ -343,6 +389,11 @@ public class VpnProfileListFragment extends Fragment mode.setSubtitle(String.format(getString(R.string.x_profiles_selected), checkedCount)); break; } + + mCanEdit = checkedCount == 1; + mCanCopy = checkedCount == 1 && mReadOnlyCount == 0; + mCanDelete = checkedCount > 0 && mReadOnlyCount == 0; + mode.invalidate(); } }; diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java index 78d8a92db..dd64d0c75 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnProfileSelectActivity.java @@ -45,7 +45,7 @@ public class VpnProfileSelectActivity extends AppCompatActivity implements OnVpn public void onVpnProfileSelected(VpnProfile profile) { Intent shortcut = new Intent(VpnProfileControlActivity.START_PROFILE); - shortcut.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_ID, profile.getUUID().toString()); + shortcut.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, profile.getUUID().toString()); ShortcutInfoCompat.Builder builder = new ShortcutInfoCompat.Builder(this, profile.getUUID().toString()); builder.setIntent(shortcut); diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnTileService.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnTileService.java index 2e128962d..c49f4ba75 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnTileService.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/VpnTileService.java @@ -32,6 +32,7 @@ import android.service.quicksettings.TileService; import org.strongswan.android.R; import org.strongswan.android.data.VpnProfile; import org.strongswan.android.data.VpnProfileDataSource; +import org.strongswan.android.data.VpnProfileSource; import org.strongswan.android.data.VpnType; import org.strongswan.android.logic.VpnStateService; import org.strongswan.android.utils.Constants; @@ -73,7 +74,7 @@ public class VpnTileService extends TileService implements VpnStateService.VpnSt context.bindService(new Intent(context, VpnStateService.class), mServiceConnection, Service.BIND_AUTO_CREATE); - mDataSource = new VpnProfileDataSource(this); + mDataSource = new VpnProfileSource(this); mDataSource.open(); } @@ -139,7 +140,7 @@ public class VpnTileService extends TileService implements VpnStateService.VpnSt } else if (mDataSource != null) { /* always get the plain profile without cached password */ - profile = mDataSource.getVpnProfile(profile.getId()); + profile = mDataSource.getVpnProfile(profile.getUUID()); } /* reconnect the profile in case of an error */ if (mService.getErrorState() == VpnStateService.ErrorState.NO_ERROR) @@ -172,7 +173,7 @@ public class VpnTileService extends TileService implements VpnStateService.VpnSt Intent intent = new Intent(this, VpnProfileControlActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.setAction(VpnProfileControlActivity.START_PROFILE); - intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_ID, profile.getUUID().toString()); + intent.putExtra(VpnProfileControlActivity.EXTRA_VPN_PROFILE_UUID, profile.getUUID().toString()); if (profile.getVpnType().has(VpnType.VpnTypeFeature.USER_PASS) && profile.getPassword() == null) { /* the user will have to enter the password, so collapse the drawer */ diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/SelectedApplicationsAdapter.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/SelectedApplicationsAdapter.java index c98e4ab83..9ac720d3b 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/SelectedApplicationsAdapter.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/SelectedApplicationsAdapter.java @@ -35,11 +35,12 @@ import java.util.List; public class SelectedApplicationsAdapter extends BaseAdapter implements Filterable { - private Context mContext; + private final Context mContext; private final Object mLock = new Object(); - private List mData; + private final List mData; private List mDataFiltered; private SelectedApplicationsFilter mFilter; + private boolean mReadOnly; public SelectedApplicationsAdapter(Context context) { @@ -100,9 +101,10 @@ public class SelectedApplicationsAdapter extends BaseAdapter implements Filterab SelectedApplicationEntry item = getItem(position); CheckableLinearLayout checkable = (CheckableLinearLayout)view; checkable.setChecked(item.isSelected()); - ImageView icon = (ImageView)view.findViewById(R.id.app_icon); + checkable.setEnabled(!mReadOnly); + ImageView icon = view.findViewById(R.id.app_icon); icon.setImageDrawable(item.getIcon()); - TextView text = (TextView)view.findViewById(R.id.app_name); + TextView text = view.findViewById(R.id.app_name); text.setText(item.toString()); return view; } @@ -117,6 +119,16 @@ public class SelectedApplicationsAdapter extends BaseAdapter implements Filterab return mFilter; } + public boolean isReadOnly() + { + return mReadOnly; + } + + public void setReadOnly(final boolean readOnly) + { + this.mReadOnly = readOnly; + } + private class SelectedApplicationsFilter extends Filter { diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/VpnProfileAdapter.java b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/VpnProfileAdapter.java index 05a15464d..c2942b8a1 100644 --- a/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/VpnProfileAdapter.java +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/ui/adapter/VpnProfileAdapter.java @@ -61,11 +61,13 @@ public class VpnProfileAdapter extends ArrayAdapter vpnProfileView = inflater.inflate(resource, null); } VpnProfile profile = getItem(position); - TextView tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_name); + TextView tv = vpnProfileView.findViewById(R.id.profile_item_name); tv.setText(profile.getName()); - tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_gateway); + tv = vpnProfileView.findViewById(R.id.profile_item_managed); + tv.setVisibility(profile.isReadOnly() ? View.VISIBLE : View.GONE); + tv = vpnProfileView.findViewById(R.id.profile_item_gateway); tv.setText(getContext().getString(R.string.profile_gateway_label) + ": " + profile.getGateway()); - tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_username); + tv = vpnProfileView.findViewById(R.id.profile_item_username); if (profile.getVpnType().has(VpnTypeFeature.USER_PASS)) { /* if the view is reused we make sure it is visible */ tv.setVisibility(View.VISIBLE); @@ -81,7 +83,7 @@ public class VpnProfileAdapter extends ArrayAdapter { tv.setVisibility(View.GONE); } - tv = (TextView)vpnProfileView.findViewById(R.id.profile_item_certificate); + tv = vpnProfileView.findViewById(R.id.profile_item_certificate); if (profile.getVpnType().has(VpnTypeFeature.CERTIFICATE)) { tv.setText(getContext().getString(R.string.profile_user_certificate_label) + ": " + profile.getUserCertificateAlias()); @@ -103,7 +105,8 @@ public class VpnProfileAdapter extends ArrayAdapter private void sortItems() { - Collections.sort(this.items, new Comparator() { + Collections.sort(this.items, new Comparator() + { @Override public int compare(VpnProfile lhs, VpnProfile rhs) { diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Certificates.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Certificates.java new file mode 100644 index 000000000..acadfd56f --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Certificates.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; + +import androidx.annotation.NonNull; + +public class Certificates +{ + @NonNull + public static X509Certificate from(@NonNull final String certificateData) throws IOException, CertificateException + { + final byte[] bytes = android.util.Base64.decode(certificateData, android.util.Base64.DEFAULT); + + try (final ByteArrayInputStream stream = new ByteArrayInputStream(bytes)) + { + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + return (X509Certificate)certificateFactory.generateCertificate(stream); + } + } + + private Certificates() {} +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Difference.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Difference.java new file mode 100644 index 000000000..e3d2fe990 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/Difference.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import androidx.annotation.NonNull; +import androidx.arch.core.util.Function; +import androidx.core.util.Pair; + +public class Difference +{ + @NonNull + private final List inserts; + @NonNull + private final List> updates; + @NonNull + private final List unchanged; + @NonNull + private final List deletes; + + @NonNull + public static Difference between( + @NonNull final List existing, + @NonNull final List modified, + @NonNull final Function getKey) + { + final Map existingMap = mapOf(existing, getKey); + final Map modifiedMap = mapOf(modified, getKey); + + final List inserts = notIn(existingMap, getKey, modified); + final List deletes = notIn(modifiedMap, getKey, existing); + final List> updates = new ArrayList<>(modifiedMap.size()); + final List unchanged = new ArrayList<>(existingMap.size()); + changeBetween(existingMap, modifiedMap, updates, unchanged); + + return new Difference<>(inserts, updates, unchanged, deletes); + } + + @NonNull + private static Map mapOf( + @NonNull final List list, + @NonNull final Function getKey) + { + final Map map = new HashMap<>(list.size()); + + for (final V entry : list) + { + final K key = getKey.apply(entry); + map.put(key, entry); + } + + return map; + } + + @NonNull + private static List notIn( + @NonNull final Map map, + @NonNull final Function getKey, + @NonNull final List list) + { + final List filtered = new ArrayList<>(list.size()); + + for (final V value : list) + { + final K key = getKey.apply(value); + if (!map.containsKey(key)) + { + filtered.add(value); + } + } + + return filtered; + } + + @NonNull + private static void changeBetween( + @NonNull final Map existingMap, + @NonNull final Map modifiedMap, + @NonNull List> updates, + @NonNull List unchanged) + { + for (final Map.Entry entry : modifiedMap.entrySet()) + { + final V existingValue = existingMap.get(entry.getKey()); + final V modifiedValue = entry.getValue(); + + if (existingValue != null && !Objects.equals(existingValue, modifiedValue)) + { + final Pair change = Pair.create(existingValue, modifiedValue); + updates.add(change); + } + else if (existingValue != null) + { + unchanged.add(existingValue); + } + } + + } + + public Difference( + @NonNull List inserts, + @NonNull List> updates, + @NonNull List unchanged, + @NonNull List deletes) + { + this.inserts = inserts; + this.updates = updates; + this.unchanged = unchanged; + this.deletes = deletes; + } + + @NonNull + public List getInserts() + { + return inserts; + } + + @NonNull + public List> getUpdates() + { + return updates; + } + + @NonNull + public List getUnchanged() + { + return unchanged; + } + + @NonNull + public List getDeletes() + { + return deletes; + } + + public boolean isEmpty() + { + return inserts.isEmpty() && updates.isEmpty() && deletes.isEmpty(); + } + + @NonNull + @Override + public String toString() + { + return "Difference {" + inserts + ", " + updates + ", " + deletes + "}"; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/KeyPair.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/KeyPair.java new file mode 100644 index 000000000..52d4d1dcd --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/KeyPair.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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; + +import java.security.PrivateKey; +import java.security.cert.Certificate; +import java.util.Objects; + +import androidx.annotation.NonNull; + +/** + * Represents a key pair, which consists of a certificate (i.e. public key) and its corresponding + * private key. + */ +public class KeyPair +{ + @NonNull + public final Certificate certificate; + @NonNull + public final PrivateKey privateKey; + + /** + * Constructor for a {@link KeyPair}. + * + * @param certificate the certificate of the key pair. + * @param privateKey the private key of the key pair. + */ + public KeyPair(@NonNull Certificate certificate, @NonNull PrivateKey privateKey) + { + this.certificate = certificate; + this.privateKey = privateKey; + } + + @Override + public boolean equals(Object o) + { + if (this == o) + { + return true; + } + if (o == null || getClass() != o.getClass()) + { + return false; + } + final KeyPair that = (KeyPair)o; + return Objects.equals(certificate, that.certificate) && + Objects.equals(privateKey, that.privateKey); + } + + @Override + public int hashCode() + { + return Objects.hash(certificate, privateKey); + } + + @NonNull + @Override + public String toString() + { + return "KeyPair{" + certificate + ", " + privateKey + "}"; + } +} diff --git a/src/frontends/android/app/src/main/java/org/strongswan/android/utils/KeyPairs.java b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/KeyPairs.java new file mode 100644 index 000000000..43c896683 --- /dev/null +++ b/src/frontends/android/app/src/main/java/org/strongswan/android/utils/KeyPairs.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2023 Relution GmbH + * + * Copyright (C) secunet Security Networks AG + * + * 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; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.Key; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.UnrecoverableKeyException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Enumeration; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class KeyPairs +{ + private static final String KEYSTORE_INSTANCE = "PKCS12"; + + @NonNull + private static KeyStore toKeyStore(@NonNull byte[] bytes, @NonNull char[] password) + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException + { + try (final ByteArrayInputStream stream = new ByteArrayInputStream(bytes)) + { + final KeyStore keyStore = KeyStore.getInstance(KEYSTORE_INSTANCE); + keyStore.load(stream, password); + return keyStore; + } + } + + @Nullable + private static KeyPair getKeyPair( + @NonNull final KeyStore keyStore, + @NonNull final String alias, + @NonNull final char[] passwordChars) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException + { + final Certificate certificate = keyStore.getCertificate(alias); + if (!(certificate instanceof X509Certificate)) + { + return null; + } + + final Key key = keyStore.getKey(alias, passwordChars); + if (key == null) + { + return null; + } + return new KeyPair(certificate, (PrivateKey)key); + } + + @Nullable + private static KeyPair getKeyPair(@NonNull KeyStore keyStore, @NonNull char[] passwordChars) + throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException + { + final Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements()) + { + final String alias = aliases.nextElement(); + final KeyPair keyPair = getKeyPair(keyStore, alias, passwordChars); + if (keyPair != null) + { + return keyPair; + } + } + return null; + } + + @Nullable + public static KeyPair from(@NonNull final String userCertificate, @NonNull final String password) + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException + { + final byte[] bytes = android.util.Base64.decode(userCertificate, android.util.Base64.DEFAULT); + final char[] passwordChars = password.toCharArray(); + + final KeyStore keyStore = toKeyStore(bytes, passwordChars); + return getKeyPair(keyStore, passwordChars); + } + + private KeyPairs() {} +} diff --git a/src/frontends/android/app/src/main/play/listings/de-DE/full-description.txt b/src/frontends/android/app/src/main/play/listings/de-DE/full-description.txt index e4c741d85..3922313c3 100644 --- a/src/frontends/android/app/src/main/play/listings/de-DE/full-description.txt +++ b/src/frontends/android/app/src/main/play/listings/de-DE/full-description.txt @@ -16,6 +16,7 @@ Dies ist die offizielle Android-Portierung der populären strongSwan VPN-Lösung
  • Die IPsec-Implementierung unterstützt derzeit die AES-CBC, AES-GCM, ChaCha20/Poly1305 und SHA1/SHA2-Algorithmen
  • Passwörter werden zurzeit als Klartext in der Datenbank gespeichert (nur wenn diese mit einem Profil gespeichert werden)
  • VPN Profile können von Dateien importiert werden
  • +
  • Unterstützt verwaltete Konfigurationen via Enterprise Mobility Management (EMM)
  • Details und ein Changelog sind in unserer Dokumentation zu finden: https://docs.strongswan.org/docs/5.9/os/androidVpnClient.html diff --git a/src/frontends/android/app/src/main/play/listings/en-US/full-description.txt b/src/frontends/android/app/src/main/play/listings/en-US/full-description.txt index 1966148e6..57c2fcfa6 100644 --- a/src/frontends/android/app/src/main/play/listings/en-US/full-description.txt +++ b/src/frontends/android/app/src/main/play/listings/en-US/full-description.txt @@ -16,6 +16,7 @@ Official Android port of the popular strongSwan VPN solution.
  • The IPsec implementation currently supports the AES-CBC, AES-GCM, ChaCha20/Poly1305 and SHA1/SHA2 algorithms
  • Passwords are currently stored as cleartext in the database (only if stored with a profile)
  • VPN profiles may be imported from files
  • +
  • Supports managed configurations via enterprise mobility management (EMM)
  • Details and a changelog can be found in our documentation: https://docs.strongswan.org/docs/5.9/os/androidVpnClient.html diff --git a/src/frontends/android/app/src/main/play/release-notes/de-DE/default.txt b/src/frontends/android/app/src/main/play/release-notes/de-DE/default.txt index 55437a9cf..0aa929312 100644 --- a/src/frontends/android/app/src/main/play/release-notes/de-DE/default.txt +++ b/src/frontends/android/app/src/main/play/release-notes/de-DE/default.txt @@ -1,5 +1,3 @@ -# 2.4.2 # +# 2.5.0 # -- Ziel-SDK auf Android 13 erhöht und frage um Erlaubnis, um Status-Mitteilung anzuzeigen -- Hardwarebeschleunigung in OpenSSL aktiviert -- Verwendet einen stabileren Ansatz, um die Quell-IP zu ermitteln +- Unterstützung für verwaltete Konfigurationen via Enterprise Mobility Management (EMM) diff --git a/src/frontends/android/app/src/main/play/release-notes/en-US/default.txt b/src/frontends/android/app/src/main/play/release-notes/en-US/default.txt index e57010a8e..c0fe425c7 100644 --- a/src/frontends/android/app/src/main/play/release-notes/en-US/default.txt +++ b/src/frontends/android/app/src/main/play/release-notes/en-US/default.txt @@ -1,5 +1,3 @@ -# 2.4.2 # +# 2.5.0 # -- Increased target SDK to Android 13 and ask for permission to show status notification -- Enable hardware acceleration in OpenSSL -- Use a more stable approach to determine source IP +- Support for managed configurations via enterprise mobility management (EMM) diff --git a/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml b/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml index bdded4337..5197c63f7 100644 --- a/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml +++ b/src/frontends/android/app/src/main/res/layout/profile_detail_view.xml @@ -16,541 +16,567 @@ or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. --> - + - + android:background="@drawable/state_background" + android:drawableStart="@android:drawable/ic_dialog_alert" + android:drawablePadding="8dp" + android:padding="8dp" + android:text="@string/alert_text_vpn_profile_read_only" + android:textColor="?android:attr/textColorPrimary" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textStyle="bold" + android:visibility="gone" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> - - - - - - - - - - - + + android:animateLayoutChanges="true" + android:orientation="vertical" + android:padding="10dp"> + android:layout_marginTop="6dp" + android:hint="@string/profile_gateway_label" + app:helper_text="@string/profile_gateway_hint"> - - - - - - - - - - - - - - - - - -