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">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:singleLine="true" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:text="@string/profile_vpn_type_label"
+ android:textSize="12sp" />
+ android:entries="@array/vpn_types"
+ android:spinnerMode="dropdown" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+ android:layout_marginTop="8dp"
+ android:hint="@string/profile_name_label"
+ app:helper_text="@string/profile_name_hint">
-
+ android:completionThreshold="1"
+ android:inputType="textNoSuggestions"
+ android:singleLine="true" />
-
+ android:text="@string/profile_show_advanced_label" />
-
+
+
+ android:layout_marginLeft="4dp"
+ android:layout_marginTop="10dp"
+ android:text="@string/profile_advanced_label"
+ android:textSize="20sp" />
-
+
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
diff --git a/src/frontends/android/app/src/main/res/layout/profile_list_item.xml b/src/frontends/android/app/src/main/res/layout/profile_list_item.xml
index caf548858..d4d9b651e 100644
--- a/src/frontends/android/app/src/main/res/layout/profile_list_item.xml
+++ b/src/frontends/android/app/src/main/res/layout/profile_list_item.xml
@@ -17,44 +17,60 @@
for more details.
-->
+ android:paddingTop="4dip">
+ android:layout_marginStart="15dp"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ tools:text="Profile name" />
+
+
+ android:textColor="?android:textColorSecondary"
+ tools:text="Server: vpn.example.com" />
+ android:textColor="?android:textColorSecondary"
+ tools:text="Username" />
+ android:singleLine="true"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:textColorSecondary"
+ tools:text="Certificate" />
diff --git a/src/frontends/android/app/src/main/res/values-de/strings.xml b/src/frontends/android/app/src/main/res/values-de/strings.xml
index 0b21fc747..4e16f5d8d 100644
--- a/src/frontends/android/app/src/main/res/values-de/strings.xml
+++ b/src/frontends/android/app/src/main/res/values-de/strings.xml
@@ -139,6 +139,7 @@
Zertifikat aus VPN Profil importieren
Zertifikat für \"%1$s\"
Profil-ID
+ Verwaltetes Profil
Ein Wert wird benötigt, um die Verbindung aufbauen zu können
Bitte geben Sie Ihren Benutzernamen ein
@@ -148,6 +149,7 @@
Bitte geben Sie mit Leerzeichen getrennte, gültige Subnetzte und/oder IP-Adressen ein
Bitte geben Sie mit Leerzeichen getrennte, gültige IP-Adressen ein
Bitte geben Sie eine mit Bindestrichen getrennte, gültige Liste von Algorithmen ein
+ Dieses Profil wird von Ihrem Administrator verwaltet und kann nicht bearbeitet werden. Nur das Passwort kann geändert werden
EAP-TNC kann Ihre Privatsphäre beeinträchtigen
Gerätedaten werden an den Server-Betreiber gesendet
Trusted Network Connect (TNC) erlaubt Server-Betreibern den Gesundheitszustand von Endgeräten zu prüfen.
Dazu kann der Betreiber Daten verlangen, wie etwa eine eindeutige Identifikationsnummer, eine Liste der installierten Pakete, Systemeinstellungen oder kryptografische Prüfsummen von Dateien.
Solche Daten werden nur übermittelt nachdem die Identität des Servers geprüft wurde. ]]>
diff --git a/src/frontends/android/app/src/main/res/values-de/strings_managed_configuration.xml b/src/frontends/android/app/src/main/res/values-de/strings_managed_configuration.xml
new file mode 100644
index 000000000..6a58f62ad
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/values-de/strings_managed_configuration.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+ Allow profile creation
+ Specifies whether users are allowed to add their own profiles
+ Allow profile import
+ Specifies whether users are allowed to import their own profiles
+ Show existing profiles
+ Specifies whether users can continue to see and use their previously created profiles
+ Allow certificate import
+ Specifies whether users are allowed to import certificates
+ Allow modifying settings
+ Specifies whether users are allowed change global app settings
+ @string/pref_default_vpn_profile
+ Unique identifier of the VPN profile to use by default, use the value \"mru\" for the most recently used profile
+ @string/pref_power_whitelist_title
+ @string/pref_power_whitelist_summary
+ VPN profiles
+ Collection of managed VPN profiles
+ VPN profile
+ A managed VPN profile
+
+
+ Unique identifier
+ Unique identifier of the VPN profile. Version 4 UUIDs (random-generated) are recommended
+ @string/profile_name_label
+ @string/profile_name_hint
+ @string/profile_vpn_type_label
+ The type of client authentication used by the VPN profile
+ Apps allowed to use the VPN (Optional)
+ Space-separated list of package names; all other apps will not see/use the VPN
+ Apps excluded from using the VPN (Optional)
+ Space-separated list of package names of apps excluded from using the VPN; only used of allow list is empty
+ @string/profile_proposals_ike_label
+ @string/profile_proposals_ike_hint
+ @string/profile_proposals_esp_label
+ @string/profile_proposals_esp_hint
+ @string/profile_mtu_label
+ @string/profile_mtu_hint
+ @string/profile_nat_keepalive_label
+ @string/profile_nat_keepalive_hint
+ @string/profile_dns_servers_label
+ @string/profile_dns_servers_hint
+ @string/profile_ipv6_transport_label
+ @string/profile_ipv6_transport_hint
+
+
+ Remote
+ Specifies information about the server
+ @string/profile_gateway_label
+ @string/profile_gateway_hint
+ @string/profile_port_label
+ @string/profile_port_hint
+ @string/profile_remote_id_label
+ @string/profile_remote_id_hint
+ CA or server certificate (Optional)
+ Base64-encoded CA or server certificate. Is imported into the app, not the system keystore. If not set, automatic CA certificate selection is enabled
+ Send certificate requests
+ Specifies whether to send certificate requests for all installed or selected CA certificates. Disabling this may reduce the size of the IKE_AUTH message if the server does not support fragmentation. But it only works if the server doesn\'t require certificate requests to send back the server certificate
+ @string/profile_use_ocsp_label
+ @string/profile_use_ocsp_hint
+ @string/profile_use_crl_label
+ @string/profile_use_crl_hint
+ @string/profile_strict_revocation_label
+ @string/profile_strict_revocation_hint
+
+
+ Local
+ Specifies information about the client
+ Identity/username for EAP authentication (Optional)
+ If this is required (for username/password-based EAP authentication) but not configured here, the user is prompted for it. If it is set, the user is not able to change it. In both cases the user may optionally enter the password
+ @string/profile_local_id_label
+ @string/profile_local_id_hint_user
+ @string/profile_user_certificate_label
+ Base64-encoded PKCS#12-container with the client certificate and private key and optional certificate chain (the latter might cause warnings on older Android releases, see Android VPN client configuration for details). Not necessary for username/password-based EAP authentication or if the user already has the certificate/key installed as it may be selected while importing the profile
+ User certificate password (Optional)
+ Password required to extract the private key of the PKCS#12-container for installation
+ @string/profile_rsa_pss_label
+ @string/profile_rsa_pss_hint
+
+
+ @string/profile_split_tunneling_label
+ @string/profile_split_tunneling_intro
+ @string/profile_included_subnets_label
+ @string/profile_included_subnets_hint
+ @string/profile_excluded_subnets_label
+ @string/profile_excluded_subnets_hint
+ @string/profile_split_tunnelingv4_title
+ Specifies whether to block IPv4 traffic that\'s not destined for the VPN. Forces all IPv4 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including 0.0.0.0/0 in subnets
+ @string/profile_split_tunnelingv6_title
+ Specifies whether to block IPv6 traffic that\'s not destined for the VPN. Forces all IPv6 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including ::/0 in subnets
+
+
diff --git a/src/frontends/android/app/src/main/res/values-pl/strings.xml b/src/frontends/android/app/src/main/res/values-pl/strings.xml
index ec6ba216b..68089da93 100644
--- a/src/frontends/android/app/src/main/res/values-pl/strings.xml
+++ b/src/frontends/android/app/src/main/res/values-pl/strings.xml
@@ -141,6 +141,7 @@
Import certificate from VPN profile
Certificate for \"%1$s\"
Profile ID
+ Managed profile
A value is required to initiate the connection
Wprowadź swoją nazwę użytkownika
@@ -150,6 +151,7 @@
Please enter valid subnets and/or IP addresses, separated by spaces
Please enter valid IP addresses, separated by spaces
Please enter a valid list of algorithms, separated by hyphens
+ This VPN profile is managed by your administrator and can\'t be modified. You can only change the password
EAP-TNC may affect your privacy
Device data is sent to the server operator
Trusted Network Connect (TNC) allows server operators to assess the health of a client device.For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.
Any data will be sent only after verifying the server\'s identity. ]]>
diff --git a/src/frontends/android/app/src/main/res/values-pl/strings_managed_configuration.xml b/src/frontends/android/app/src/main/res/values-pl/strings_managed_configuration.xml
new file mode 100644
index 000000000..6a58f62ad
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/values-pl/strings_managed_configuration.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+ Allow profile creation
+ Specifies whether users are allowed to add their own profiles
+ Allow profile import
+ Specifies whether users are allowed to import their own profiles
+ Show existing profiles
+ Specifies whether users can continue to see and use their previously created profiles
+ Allow certificate import
+ Specifies whether users are allowed to import certificates
+ Allow modifying settings
+ Specifies whether users are allowed change global app settings
+ @string/pref_default_vpn_profile
+ Unique identifier of the VPN profile to use by default, use the value \"mru\" for the most recently used profile
+ @string/pref_power_whitelist_title
+ @string/pref_power_whitelist_summary
+ VPN profiles
+ Collection of managed VPN profiles
+ VPN profile
+ A managed VPN profile
+
+
+ Unique identifier
+ Unique identifier of the VPN profile. Version 4 UUIDs (random-generated) are recommended
+ @string/profile_name_label
+ @string/profile_name_hint
+ @string/profile_vpn_type_label
+ The type of client authentication used by the VPN profile
+ Apps allowed to use the VPN (Optional)
+ Space-separated list of package names; all other apps will not see/use the VPN
+ Apps excluded from using the VPN (Optional)
+ Space-separated list of package names of apps excluded from using the VPN; only used of allow list is empty
+ @string/profile_proposals_ike_label
+ @string/profile_proposals_ike_hint
+ @string/profile_proposals_esp_label
+ @string/profile_proposals_esp_hint
+ @string/profile_mtu_label
+ @string/profile_mtu_hint
+ @string/profile_nat_keepalive_label
+ @string/profile_nat_keepalive_hint
+ @string/profile_dns_servers_label
+ @string/profile_dns_servers_hint
+ @string/profile_ipv6_transport_label
+ @string/profile_ipv6_transport_hint
+
+
+ Remote
+ Specifies information about the server
+ @string/profile_gateway_label
+ @string/profile_gateway_hint
+ @string/profile_port_label
+ @string/profile_port_hint
+ @string/profile_remote_id_label
+ @string/profile_remote_id_hint
+ CA or server certificate (Optional)
+ Base64-encoded CA or server certificate. Is imported into the app, not the system keystore. If not set, automatic CA certificate selection is enabled
+ Send certificate requests
+ Specifies whether to send certificate requests for all installed or selected CA certificates. Disabling this may reduce the size of the IKE_AUTH message if the server does not support fragmentation. But it only works if the server doesn\'t require certificate requests to send back the server certificate
+ @string/profile_use_ocsp_label
+ @string/profile_use_ocsp_hint
+ @string/profile_use_crl_label
+ @string/profile_use_crl_hint
+ @string/profile_strict_revocation_label
+ @string/profile_strict_revocation_hint
+
+
+ Local
+ Specifies information about the client
+ Identity/username for EAP authentication (Optional)
+ If this is required (for username/password-based EAP authentication) but not configured here, the user is prompted for it. If it is set, the user is not able to change it. In both cases the user may optionally enter the password
+ @string/profile_local_id_label
+ @string/profile_local_id_hint_user
+ @string/profile_user_certificate_label
+ Base64-encoded PKCS#12-container with the client certificate and private key and optional certificate chain (the latter might cause warnings on older Android releases, see Android VPN client configuration for details). Not necessary for username/password-based EAP authentication or if the user already has the certificate/key installed as it may be selected while importing the profile
+ User certificate password (Optional)
+ Password required to extract the private key of the PKCS#12-container for installation
+ @string/profile_rsa_pss_label
+ @string/profile_rsa_pss_hint
+
+
+ @string/profile_split_tunneling_label
+ @string/profile_split_tunneling_intro
+ @string/profile_included_subnets_label
+ @string/profile_included_subnets_hint
+ @string/profile_excluded_subnets_label
+ @string/profile_excluded_subnets_hint
+ @string/profile_split_tunnelingv4_title
+ Specifies whether to block IPv4 traffic that\'s not destined for the VPN. Forces all IPv4 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including 0.0.0.0/0 in subnets
+ @string/profile_split_tunnelingv6_title
+ Specifies whether to block IPv6 traffic that\'s not destined for the VPN. Forces all IPv6 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including ::/0 in subnets
+
+
diff --git a/src/frontends/android/app/src/main/res/values-ru/strings.xml b/src/frontends/android/app/src/main/res/values-ru/strings.xml
index 0150016b7..ab35f29e7 100644
--- a/src/frontends/android/app/src/main/res/values-ru/strings.xml
+++ b/src/frontends/android/app/src/main/res/values-ru/strings.xml
@@ -135,6 +135,7 @@
Import certificate from VPN profile
Certificate for \"%1$s\"
Profile ID
+ Managed profile
A value is required to initiate the connection
Пожалуйста введите имя пользователя
@@ -144,6 +145,7 @@
Please enter valid subnets and/or IP addresses, separated by spaces
Please enter valid IP addresses, separated by spaces
Please enter a valid list of algorithms, separated by hyphens
+ This VPN profile is managed by your administrator and can\'t be modified. You can only change the password
EAP-TNC may affect your privacy
Device data is sent to the server operator
Trusted Network Connect (TNC) allows server operators to assess the health of a client device.For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.
Any data will be sent only after verifying the server\'s identity. ]]>
diff --git a/src/frontends/android/app/src/main/res/values-ru/strings_managed_configuration.xml b/src/frontends/android/app/src/main/res/values-ru/strings_managed_configuration.xml
new file mode 100644
index 000000000..6a58f62ad
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/values-ru/strings_managed_configuration.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+ Allow profile creation
+ Specifies whether users are allowed to add their own profiles
+ Allow profile import
+ Specifies whether users are allowed to import their own profiles
+ Show existing profiles
+ Specifies whether users can continue to see and use their previously created profiles
+ Allow certificate import
+ Specifies whether users are allowed to import certificates
+ Allow modifying settings
+ Specifies whether users are allowed change global app settings
+ @string/pref_default_vpn_profile
+ Unique identifier of the VPN profile to use by default, use the value \"mru\" for the most recently used profile
+ @string/pref_power_whitelist_title
+ @string/pref_power_whitelist_summary
+ VPN profiles
+ Collection of managed VPN profiles
+ VPN profile
+ A managed VPN profile
+
+
+ Unique identifier
+ Unique identifier of the VPN profile. Version 4 UUIDs (random-generated) are recommended
+ @string/profile_name_label
+ @string/profile_name_hint
+ @string/profile_vpn_type_label
+ The type of client authentication used by the VPN profile
+ Apps allowed to use the VPN (Optional)
+ Space-separated list of package names; all other apps will not see/use the VPN
+ Apps excluded from using the VPN (Optional)
+ Space-separated list of package names of apps excluded from using the VPN; only used of allow list is empty
+ @string/profile_proposals_ike_label
+ @string/profile_proposals_ike_hint
+ @string/profile_proposals_esp_label
+ @string/profile_proposals_esp_hint
+ @string/profile_mtu_label
+ @string/profile_mtu_hint
+ @string/profile_nat_keepalive_label
+ @string/profile_nat_keepalive_hint
+ @string/profile_dns_servers_label
+ @string/profile_dns_servers_hint
+ @string/profile_ipv6_transport_label
+ @string/profile_ipv6_transport_hint
+
+
+ Remote
+ Specifies information about the server
+ @string/profile_gateway_label
+ @string/profile_gateway_hint
+ @string/profile_port_label
+ @string/profile_port_hint
+ @string/profile_remote_id_label
+ @string/profile_remote_id_hint
+ CA or server certificate (Optional)
+ Base64-encoded CA or server certificate. Is imported into the app, not the system keystore. If not set, automatic CA certificate selection is enabled
+ Send certificate requests
+ Specifies whether to send certificate requests for all installed or selected CA certificates. Disabling this may reduce the size of the IKE_AUTH message if the server does not support fragmentation. But it only works if the server doesn\'t require certificate requests to send back the server certificate
+ @string/profile_use_ocsp_label
+ @string/profile_use_ocsp_hint
+ @string/profile_use_crl_label
+ @string/profile_use_crl_hint
+ @string/profile_strict_revocation_label
+ @string/profile_strict_revocation_hint
+
+
+ Local
+ Specifies information about the client
+ Identity/username for EAP authentication (Optional)
+ If this is required (for username/password-based EAP authentication) but not configured here, the user is prompted for it. If it is set, the user is not able to change it. In both cases the user may optionally enter the password
+ @string/profile_local_id_label
+ @string/profile_local_id_hint_user
+ @string/profile_user_certificate_label
+ Base64-encoded PKCS#12-container with the client certificate and private key and optional certificate chain (the latter might cause warnings on older Android releases, see Android VPN client configuration for details). Not necessary for username/password-based EAP authentication or if the user already has the certificate/key installed as it may be selected while importing the profile
+ User certificate password (Optional)
+ Password required to extract the private key of the PKCS#12-container for installation
+ @string/profile_rsa_pss_label
+ @string/profile_rsa_pss_hint
+
+
+ @string/profile_split_tunneling_label
+ @string/profile_split_tunneling_intro
+ @string/profile_included_subnets_label
+ @string/profile_included_subnets_hint
+ @string/profile_excluded_subnets_label
+ @string/profile_excluded_subnets_hint
+ @string/profile_split_tunnelingv4_title
+ Specifies whether to block IPv4 traffic that\'s not destined for the VPN. Forces all IPv4 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including 0.0.0.0/0 in subnets
+ @string/profile_split_tunnelingv6_title
+ Specifies whether to block IPv6 traffic that\'s not destined for the VPN. Forces all IPv6 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including ::/0 in subnets
+
+
diff --git a/src/frontends/android/app/src/main/res/values-uk/strings.xml b/src/frontends/android/app/src/main/res/values-uk/strings.xml
index e02a640a3..677d7c95d 100644
--- a/src/frontends/android/app/src/main/res/values-uk/strings.xml
+++ b/src/frontends/android/app/src/main/res/values-uk/strings.xml
@@ -136,6 +136,7 @@
Import certificate from VPN profile
Certificate for \"%1$s\"
Profile ID
+ Managed profile
A value is required to initiate the connection
Введіть ім\'я користувача
@@ -145,6 +146,7 @@
Please enter valid subnets and/or IP addresses, separated by spaces
Please enter valid IP addresses, separated by spaces
Please enter a valid list of algorithms, separated by hyphens
+ This VPN profile is managed by your administrator and can\'t be modified. You can only change the password
EAP-TNC may affect your privacy
Device data is sent to the server operator
Trusted Network Connect (TNC) allows server operators to assess the health of a client device.For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.
Any data will be sent only after verifying the server\'s identity. ]]>
diff --git a/src/frontends/android/app/src/main/res/values-uk/strings_managed_configuration.xml b/src/frontends/android/app/src/main/res/values-uk/strings_managed_configuration.xml
new file mode 100644
index 000000000..6a58f62ad
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/values-uk/strings_managed_configuration.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+ Allow profile creation
+ Specifies whether users are allowed to add their own profiles
+ Allow profile import
+ Specifies whether users are allowed to import their own profiles
+ Show existing profiles
+ Specifies whether users can continue to see and use their previously created profiles
+ Allow certificate import
+ Specifies whether users are allowed to import certificates
+ Allow modifying settings
+ Specifies whether users are allowed change global app settings
+ @string/pref_default_vpn_profile
+ Unique identifier of the VPN profile to use by default, use the value \"mru\" for the most recently used profile
+ @string/pref_power_whitelist_title
+ @string/pref_power_whitelist_summary
+ VPN profiles
+ Collection of managed VPN profiles
+ VPN profile
+ A managed VPN profile
+
+
+ Unique identifier
+ Unique identifier of the VPN profile. Version 4 UUIDs (random-generated) are recommended
+ @string/profile_name_label
+ @string/profile_name_hint
+ @string/profile_vpn_type_label
+ The type of client authentication used by the VPN profile
+ Apps allowed to use the VPN (Optional)
+ Space-separated list of package names; all other apps will not see/use the VPN
+ Apps excluded from using the VPN (Optional)
+ Space-separated list of package names of apps excluded from using the VPN; only used of allow list is empty
+ @string/profile_proposals_ike_label
+ @string/profile_proposals_ike_hint
+ @string/profile_proposals_esp_label
+ @string/profile_proposals_esp_hint
+ @string/profile_mtu_label
+ @string/profile_mtu_hint
+ @string/profile_nat_keepalive_label
+ @string/profile_nat_keepalive_hint
+ @string/profile_dns_servers_label
+ @string/profile_dns_servers_hint
+ @string/profile_ipv6_transport_label
+ @string/profile_ipv6_transport_hint
+
+
+ Remote
+ Specifies information about the server
+ @string/profile_gateway_label
+ @string/profile_gateway_hint
+ @string/profile_port_label
+ @string/profile_port_hint
+ @string/profile_remote_id_label
+ @string/profile_remote_id_hint
+ CA or server certificate (Optional)
+ Base64-encoded CA or server certificate. Is imported into the app, not the system keystore. If not set, automatic CA certificate selection is enabled
+ Send certificate requests
+ Specifies whether to send certificate requests for all installed or selected CA certificates. Disabling this may reduce the size of the IKE_AUTH message if the server does not support fragmentation. But it only works if the server doesn\'t require certificate requests to send back the server certificate
+ @string/profile_use_ocsp_label
+ @string/profile_use_ocsp_hint
+ @string/profile_use_crl_label
+ @string/profile_use_crl_hint
+ @string/profile_strict_revocation_label
+ @string/profile_strict_revocation_hint
+
+
+ Local
+ Specifies information about the client
+ Identity/username for EAP authentication (Optional)
+ If this is required (for username/password-based EAP authentication) but not configured here, the user is prompted for it. If it is set, the user is not able to change it. In both cases the user may optionally enter the password
+ @string/profile_local_id_label
+ @string/profile_local_id_hint_user
+ @string/profile_user_certificate_label
+ Base64-encoded PKCS#12-container with the client certificate and private key and optional certificate chain (the latter might cause warnings on older Android releases, see Android VPN client configuration for details). Not necessary for username/password-based EAP authentication or if the user already has the certificate/key installed as it may be selected while importing the profile
+ User certificate password (Optional)
+ Password required to extract the private key of the PKCS#12-container for installation
+ @string/profile_rsa_pss_label
+ @string/profile_rsa_pss_hint
+
+
+ @string/profile_split_tunneling_label
+ @string/profile_split_tunneling_intro
+ @string/profile_included_subnets_label
+ @string/profile_included_subnets_hint
+ @string/profile_excluded_subnets_label
+ @string/profile_excluded_subnets_hint
+ @string/profile_split_tunnelingv4_title
+ Specifies whether to block IPv4 traffic that\'s not destined for the VPN. Forces all IPv4 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including 0.0.0.0/0 in subnets
+ @string/profile_split_tunnelingv6_title
+ Specifies whether to block IPv6 traffic that\'s not destined for the VPN. Forces all IPv6 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including ::/0 in subnets
+
+
diff --git a/src/frontends/android/app/src/main/res/values-zh-rCN/strings.xml b/src/frontends/android/app/src/main/res/values-zh-rCN/strings.xml
index 99693c509..43d3134d9 100644
--- a/src/frontends/android/app/src/main/res/values-zh-rCN/strings.xml
+++ b/src/frontends/android/app/src/main/res/values-zh-rCN/strings.xml
@@ -135,6 +135,7 @@
从VPN配置导入证书
\"%1$s\" 所对应的证书
配置文件ID
+ Managed profile
必填信息以初始化连接
请输入您的用户名
@@ -144,6 +145,7 @@
请输入有效的子网和/或IP地址,用空格分隔
请输入有效的IP地址,以空格分隔
请输入用连字符分隔的有效算法列表
+ This VPN profile is managed by your administrator and can\'t be modified. You can only change the password
EAP-TNC可能会影响您的隐私
设备数据已被发送至服务器管理员
可信网络连接t (TNC) 允许服务器管理员评定一个用户设备的状况。出于此目的,服务器管理员可能要求以下数据如独立ID、已安装软件列表、系统设置、或加密过的文件校验值。
任何数据都仅将在验证过服务器的身份ID之后被发出。 ]]>
diff --git a/src/frontends/android/app/src/main/res/values-zh-rCN/strings_managed_configuration.xml b/src/frontends/android/app/src/main/res/values-zh-rCN/strings_managed_configuration.xml
new file mode 100644
index 000000000..6a58f62ad
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/values-zh-rCN/strings_managed_configuration.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+ Allow profile creation
+ Specifies whether users are allowed to add their own profiles
+ Allow profile import
+ Specifies whether users are allowed to import their own profiles
+ Show existing profiles
+ Specifies whether users can continue to see and use their previously created profiles
+ Allow certificate import
+ Specifies whether users are allowed to import certificates
+ Allow modifying settings
+ Specifies whether users are allowed change global app settings
+ @string/pref_default_vpn_profile
+ Unique identifier of the VPN profile to use by default, use the value \"mru\" for the most recently used profile
+ @string/pref_power_whitelist_title
+ @string/pref_power_whitelist_summary
+ VPN profiles
+ Collection of managed VPN profiles
+ VPN profile
+ A managed VPN profile
+
+
+ Unique identifier
+ Unique identifier of the VPN profile. Version 4 UUIDs (random-generated) are recommended
+ @string/profile_name_label
+ @string/profile_name_hint
+ @string/profile_vpn_type_label
+ The type of client authentication used by the VPN profile
+ Apps allowed to use the VPN (Optional)
+ Space-separated list of package names; all other apps will not see/use the VPN
+ Apps excluded from using the VPN (Optional)
+ Space-separated list of package names of apps excluded from using the VPN; only used of allow list is empty
+ @string/profile_proposals_ike_label
+ @string/profile_proposals_ike_hint
+ @string/profile_proposals_esp_label
+ @string/profile_proposals_esp_hint
+ @string/profile_mtu_label
+ @string/profile_mtu_hint
+ @string/profile_nat_keepalive_label
+ @string/profile_nat_keepalive_hint
+ @string/profile_dns_servers_label
+ @string/profile_dns_servers_hint
+ @string/profile_ipv6_transport_label
+ @string/profile_ipv6_transport_hint
+
+
+ Remote
+ Specifies information about the server
+ @string/profile_gateway_label
+ @string/profile_gateway_hint
+ @string/profile_port_label
+ @string/profile_port_hint
+ @string/profile_remote_id_label
+ @string/profile_remote_id_hint
+ CA or server certificate (Optional)
+ Base64-encoded CA or server certificate. Is imported into the app, not the system keystore. If not set, automatic CA certificate selection is enabled
+ Send certificate requests
+ Specifies whether to send certificate requests for all installed or selected CA certificates. Disabling this may reduce the size of the IKE_AUTH message if the server does not support fragmentation. But it only works if the server doesn\'t require certificate requests to send back the server certificate
+ @string/profile_use_ocsp_label
+ @string/profile_use_ocsp_hint
+ @string/profile_use_crl_label
+ @string/profile_use_crl_hint
+ @string/profile_strict_revocation_label
+ @string/profile_strict_revocation_hint
+
+
+ Local
+ Specifies information about the client
+ Identity/username for EAP authentication (Optional)
+ If this is required (for username/password-based EAP authentication) but not configured here, the user is prompted for it. If it is set, the user is not able to change it. In both cases the user may optionally enter the password
+ @string/profile_local_id_label
+ @string/profile_local_id_hint_user
+ @string/profile_user_certificate_label
+ Base64-encoded PKCS#12-container with the client certificate and private key and optional certificate chain (the latter might cause warnings on older Android releases, see Android VPN client configuration for details). Not necessary for username/password-based EAP authentication or if the user already has the certificate/key installed as it may be selected while importing the profile
+ User certificate password (Optional)
+ Password required to extract the private key of the PKCS#12-container for installation
+ @string/profile_rsa_pss_label
+ @string/profile_rsa_pss_hint
+
+
+ @string/profile_split_tunneling_label
+ @string/profile_split_tunneling_intro
+ @string/profile_included_subnets_label
+ @string/profile_included_subnets_hint
+ @string/profile_excluded_subnets_label
+ @string/profile_excluded_subnets_hint
+ @string/profile_split_tunnelingv4_title
+ Specifies whether to block IPv4 traffic that\'s not destined for the VPN. Forces all IPv4 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including 0.0.0.0/0 in subnets
+ @string/profile_split_tunnelingv6_title
+ Specifies whether to block IPv6 traffic that\'s not destined for the VPN. Forces all IPv6 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including ::/0 in subnets
+
+
diff --git a/src/frontends/android/app/src/main/res/values-zh-rTW/strings.xml b/src/frontends/android/app/src/main/res/values-zh-rTW/strings.xml
index 79d3e41ba..731682f66 100644
--- a/src/frontends/android/app/src/main/res/values-zh-rTW/strings.xml
+++ b/src/frontends/android/app/src/main/res/values-zh-rTW/strings.xml
@@ -135,6 +135,7 @@
從VPN設定檔匯入憑證
\"%1$s\" 對應的憑證
Profile ID
+ Managed profile
請填寫必要訊息才能初始化連線
請輸入您的用戶名稱
@@ -144,6 +145,7 @@
Please enter valid subnets and/or IP addresses, separated by spaces
Please enter valid IP addresses, separated by spaces
Please enter a valid list of algorithms, separated by hyphens
+ This VPN profile is managed by your administrator and can\'t be modified. You can only change the password
EAP-TNC可能會影響您的隱私安全
裝置資料已經發送給伺服器管理者
Trusted Network Connect (TNC) 可以讓伺服器管理者評估用戶裝置的狀況。在這個目的下,伺服器管理者可能會要求以下資料,例如ID、已安裝的App項目、系統設定、或加密檔案驗證值。
任何資料都只有在驗證伺服器的身分ID之後才會被送出。 ]]>
diff --git a/src/frontends/android/app/src/main/res/values-zh-rTW/strings_managed_configuration.xml b/src/frontends/android/app/src/main/res/values-zh-rTW/strings_managed_configuration.xml
new file mode 100644
index 000000000..6a58f62ad
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/values-zh-rTW/strings_managed_configuration.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+ Allow profile creation
+ Specifies whether users are allowed to add their own profiles
+ Allow profile import
+ Specifies whether users are allowed to import their own profiles
+ Show existing profiles
+ Specifies whether users can continue to see and use their previously created profiles
+ Allow certificate import
+ Specifies whether users are allowed to import certificates
+ Allow modifying settings
+ Specifies whether users are allowed change global app settings
+ @string/pref_default_vpn_profile
+ Unique identifier of the VPN profile to use by default, use the value \"mru\" for the most recently used profile
+ @string/pref_power_whitelist_title
+ @string/pref_power_whitelist_summary
+ VPN profiles
+ Collection of managed VPN profiles
+ VPN profile
+ A managed VPN profile
+
+
+ Unique identifier
+ Unique identifier of the VPN profile. Version 4 UUIDs (random-generated) are recommended
+ @string/profile_name_label
+ @string/profile_name_hint
+ @string/profile_vpn_type_label
+ The type of client authentication used by the VPN profile
+ Apps allowed to use the VPN (Optional)
+ Space-separated list of package names; all other apps will not see/use the VPN
+ Apps excluded from using the VPN (Optional)
+ Space-separated list of package names of apps excluded from using the VPN; only used of allow list is empty
+ @string/profile_proposals_ike_label
+ @string/profile_proposals_ike_hint
+ @string/profile_proposals_esp_label
+ @string/profile_proposals_esp_hint
+ @string/profile_mtu_label
+ @string/profile_mtu_hint
+ @string/profile_nat_keepalive_label
+ @string/profile_nat_keepalive_hint
+ @string/profile_dns_servers_label
+ @string/profile_dns_servers_hint
+ @string/profile_ipv6_transport_label
+ @string/profile_ipv6_transport_hint
+
+
+ Remote
+ Specifies information about the server
+ @string/profile_gateway_label
+ @string/profile_gateway_hint
+ @string/profile_port_label
+ @string/profile_port_hint
+ @string/profile_remote_id_label
+ @string/profile_remote_id_hint
+ CA or server certificate (Optional)
+ Base64-encoded CA or server certificate. Is imported into the app, not the system keystore. If not set, automatic CA certificate selection is enabled
+ Send certificate requests
+ Specifies whether to send certificate requests for all installed or selected CA certificates. Disabling this may reduce the size of the IKE_AUTH message if the server does not support fragmentation. But it only works if the server doesn\'t require certificate requests to send back the server certificate
+ @string/profile_use_ocsp_label
+ @string/profile_use_ocsp_hint
+ @string/profile_use_crl_label
+ @string/profile_use_crl_hint
+ @string/profile_strict_revocation_label
+ @string/profile_strict_revocation_hint
+
+
+ Local
+ Specifies information about the client
+ Identity/username for EAP authentication (Optional)
+ If this is required (for username/password-based EAP authentication) but not configured here, the user is prompted for it. If it is set, the user is not able to change it. In both cases the user may optionally enter the password
+ @string/profile_local_id_label
+ @string/profile_local_id_hint_user
+ @string/profile_user_certificate_label
+ Base64-encoded PKCS#12-container with the client certificate and private key and optional certificate chain (the latter might cause warnings on older Android releases, see Android VPN client configuration for details). Not necessary for username/password-based EAP authentication or if the user already has the certificate/key installed as it may be selected while importing the profile
+ User certificate password (Optional)
+ Password required to extract the private key of the PKCS#12-container for installation
+ @string/profile_rsa_pss_label
+ @string/profile_rsa_pss_hint
+
+
+ @string/profile_split_tunneling_label
+ @string/profile_split_tunneling_intro
+ @string/profile_included_subnets_label
+ @string/profile_included_subnets_hint
+ @string/profile_excluded_subnets_label
+ @string/profile_excluded_subnets_hint
+ @string/profile_split_tunnelingv4_title
+ Specifies whether to block IPv4 traffic that\'s not destined for the VPN. Forces all IPv4 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including 0.0.0.0/0 in subnets
+ @string/profile_split_tunnelingv6_title
+ Specifies whether to block IPv6 traffic that\'s not destined for the VPN. Forces all IPv6 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including ::/0 in subnets
+
+
diff --git a/src/frontends/android/app/src/main/res/values/arrays.xml b/src/frontends/android/app/src/main/res/values/arrays.xml
index 5a74b7f3f..717deb30a 100644
--- a/src/frontends/android/app/src/main/res/values/arrays.xml
+++ b/src/frontends/android/app/src/main/res/values/arrays.xml
@@ -24,6 +24,15 @@
- IKEv2 EAP-TNC (Username/Password)
+
+ - ikev2-eap
+ - ikev2-cert
+ - ikev2-cert-eap
+ - ikev2-eap-tls
+ - ikev2-byod-eap
+
+ ikev2-eap
+
- All applications use the VPN
diff --git a/src/frontends/android/app/src/main/res/values/strings.xml b/src/frontends/android/app/src/main/res/values/strings.xml
index 59f24e7e2..71dc6e851 100644
--- a/src/frontends/android/app/src/main/res/values/strings.xml
+++ b/src/frontends/android/app/src/main/res/values/strings.xml
@@ -139,6 +139,7 @@
Import certificate from VPN profile
Certificate for \"%1$s\"
Profile ID
+ Managed profile
A value is required to initiate the connection
Please enter your username
@@ -148,6 +149,7 @@
Please enter valid subnets and/or IP addresses, separated by spaces
Please enter valid IP addresses, separated by spaces
Please enter a valid list of algorithms, separated by hyphens
+ This VPN profile is managed by your administrator and can\'t be modified. You can only change the password
EAP-TNC may affect your privacy
Device data is sent to the server operator
Trusted Network Connect (TNC) allows server operators to assess the health of a client device.For that purpose the server operator may request data such as a unique identifier, a list of installed packages, system settings, or cryptographic checksums of files.
Any data will be sent only after verifying the server\'s identity. ]]>
diff --git a/src/frontends/android/app/src/main/res/values/strings_managed_configuration.xml b/src/frontends/android/app/src/main/res/values/strings_managed_configuration.xml
new file mode 100644
index 000000000..6a58f62ad
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/values/strings_managed_configuration.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+ Allow profile creation
+ Specifies whether users are allowed to add their own profiles
+ Allow profile import
+ Specifies whether users are allowed to import their own profiles
+ Show existing profiles
+ Specifies whether users can continue to see and use their previously created profiles
+ Allow certificate import
+ Specifies whether users are allowed to import certificates
+ Allow modifying settings
+ Specifies whether users are allowed change global app settings
+ @string/pref_default_vpn_profile
+ Unique identifier of the VPN profile to use by default, use the value \"mru\" for the most recently used profile
+ @string/pref_power_whitelist_title
+ @string/pref_power_whitelist_summary
+ VPN profiles
+ Collection of managed VPN profiles
+ VPN profile
+ A managed VPN profile
+
+
+ Unique identifier
+ Unique identifier of the VPN profile. Version 4 UUIDs (random-generated) are recommended
+ @string/profile_name_label
+ @string/profile_name_hint
+ @string/profile_vpn_type_label
+ The type of client authentication used by the VPN profile
+ Apps allowed to use the VPN (Optional)
+ Space-separated list of package names; all other apps will not see/use the VPN
+ Apps excluded from using the VPN (Optional)
+ Space-separated list of package names of apps excluded from using the VPN; only used of allow list is empty
+ @string/profile_proposals_ike_label
+ @string/profile_proposals_ike_hint
+ @string/profile_proposals_esp_label
+ @string/profile_proposals_esp_hint
+ @string/profile_mtu_label
+ @string/profile_mtu_hint
+ @string/profile_nat_keepalive_label
+ @string/profile_nat_keepalive_hint
+ @string/profile_dns_servers_label
+ @string/profile_dns_servers_hint
+ @string/profile_ipv6_transport_label
+ @string/profile_ipv6_transport_hint
+
+
+ Remote
+ Specifies information about the server
+ @string/profile_gateway_label
+ @string/profile_gateway_hint
+ @string/profile_port_label
+ @string/profile_port_hint
+ @string/profile_remote_id_label
+ @string/profile_remote_id_hint
+ CA or server certificate (Optional)
+ Base64-encoded CA or server certificate. Is imported into the app, not the system keystore. If not set, automatic CA certificate selection is enabled
+ Send certificate requests
+ Specifies whether to send certificate requests for all installed or selected CA certificates. Disabling this may reduce the size of the IKE_AUTH message if the server does not support fragmentation. But it only works if the server doesn\'t require certificate requests to send back the server certificate
+ @string/profile_use_ocsp_label
+ @string/profile_use_ocsp_hint
+ @string/profile_use_crl_label
+ @string/profile_use_crl_hint
+ @string/profile_strict_revocation_label
+ @string/profile_strict_revocation_hint
+
+
+ Local
+ Specifies information about the client
+ Identity/username for EAP authentication (Optional)
+ If this is required (for username/password-based EAP authentication) but not configured here, the user is prompted for it. If it is set, the user is not able to change it. In both cases the user may optionally enter the password
+ @string/profile_local_id_label
+ @string/profile_local_id_hint_user
+ @string/profile_user_certificate_label
+ Base64-encoded PKCS#12-container with the client certificate and private key and optional certificate chain (the latter might cause warnings on older Android releases, see Android VPN client configuration for details). Not necessary for username/password-based EAP authentication or if the user already has the certificate/key installed as it may be selected while importing the profile
+ User certificate password (Optional)
+ Password required to extract the private key of the PKCS#12-container for installation
+ @string/profile_rsa_pss_label
+ @string/profile_rsa_pss_hint
+
+
+ @string/profile_split_tunneling_label
+ @string/profile_split_tunneling_intro
+ @string/profile_included_subnets_label
+ @string/profile_included_subnets_hint
+ @string/profile_excluded_subnets_label
+ @string/profile_excluded_subnets_hint
+ @string/profile_split_tunnelingv4_title
+ Specifies whether to block IPv4 traffic that\'s not destined for the VPN. Forces all IPv4 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including 0.0.0.0/0 in subnets
+ @string/profile_split_tunnelingv6_title
+ Specifies whether to block IPv6 traffic that\'s not destined for the VPN. Forces all IPv6 traffic via VPN (traffic that does not match the negotiated traffic selector is then just dropped). Thus this is basically equivalent to including ::/0 in subnets
+
+
diff --git a/src/frontends/android/app/src/main/res/xml/managed_configuration.xml b/src/frontends/android/app/src/main/res/xml/managed_configuration.xml
new file mode 100644
index 000000000..105b3f839
--- /dev/null
+++ b/src/frontends/android/app/src/main/res/xml/managed_configuration.xml
@@ -0,0 +1,303 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/frontends/android/app/src/test/java/org/strongswan/android/utils/DifferenceTest.java b/src/frontends/android/app/src/test/java/org/strongswan/android/utils/DifferenceTest.java
new file mode 100644
index 000000000..01fcd719e
--- /dev/null
+++ b/src/frontends/android/app/src/test/java/org/strongswan/android/utils/DifferenceTest.java
@@ -0,0 +1,151 @@
+/*
+ * 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 static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.Test;
+
+import java.util.List;
+import java.util.Objects;
+
+import androidx.core.util.Pair;
+
+public class DifferenceTest
+{
+ @Test
+ public void testElementAdded()
+ {
+ final Element element = new Element("a", 0);
+ final List existing = List.of();
+ final List modified = List.of(element);
+
+ final Difference diff = Difference.between(existing, modified, Element::getKey);
+
+ assertThat(diff.getInserts()).containsExactly(element);
+ assertThat(diff.getUpdates()).isEmpty();
+ assertThat(diff.getUnchanged()).isEmpty();
+ assertThat(diff.getDeletes()).isEmpty();
+ }
+
+ @Test
+ public void testElementRemoved()
+ {
+ final Element element = new Element("a", 0);
+ final List existing = List.of(element);
+ final List modified = List.of();
+
+ final Difference diff = Difference.between(existing, modified, Element::getKey);
+
+ assertThat(diff.getInserts()).isEmpty();
+ assertThat(diff.getUpdates()).isEmpty();
+ assertThat(diff.getUnchanged()).isEmpty();
+ assertThat(diff.getDeletes()).containsExactly(element);
+ }
+
+ @Test
+ public void testElementIdentical()
+ {
+ final Element element0 = new Element("a", 0);
+ final Element element1 = new Element("a", 0);
+ final List existing = List.of(element0);
+ final List modified = List.of(element1);
+
+ final Difference diff = Difference.between(existing, modified, Element::getKey);
+
+ assertThat(diff.getInserts()).isEmpty();
+ assertThat(diff.getUpdates()).isEmpty();
+ assertThat(diff.getUnchanged()).containsExactly(element0);
+ assertThat(diff.getDeletes()).isEmpty();
+ }
+
+ @Test
+ public void testElementSwap()
+ {
+ final Element elementA = new Element("a", 0);
+ final Element elementB = new Element("b", 0);
+ final List existing = List.of(elementA);
+ final List modified = List.of(elementB);
+
+ final Difference diff = Difference.between(existing, modified, Element::getKey);
+
+ assertThat(diff.getInserts()).containsExactly(elementB);
+ assertThat(diff.getUpdates()).isEmpty();
+ assertThat(diff.getUnchanged()).isEmpty();
+ assertThat(diff.getDeletes()).containsExactly(elementA);
+ }
+
+ @Test
+ public void testElementUpdate()
+ {
+ final Element elementA0 = new Element("a", 0);
+ final Element elementA1 = new Element("a", 1);
+ final List existing = List.of(elementA0);
+ final List modified = List.of(elementA1);
+
+ final Difference diff = Difference.between(existing, modified, Element::getKey);
+
+ assertThat(diff.getInserts()).isEmpty();
+ assertThat(diff.getUpdates()).containsExactly(Pair.create(elementA0, elementA1));
+ assertThat(diff.getUnchanged()).isEmpty();
+ assertThat(diff.getDeletes()).isEmpty();
+ }
+
+ private static class Element
+ {
+ private final String key;
+ private final int value;
+
+ public Element(final String key, final int value)
+ {
+ this.key = key;
+ this.value = value;
+ }
+
+ public String getKey()
+ {
+ return key;
+ }
+
+ public int getValue()
+ {
+ return value;
+ }
+
+ @Override
+ public boolean equals(Object o)
+ {
+ if (this == o)
+ {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass())
+ {
+ return false;
+ }
+ Element element = (Element)o;
+ return value == element.value && Objects.equals(key, element.key);
+ }
+
+ @Override
+ public int hashCode()
+ {
+ return Objects.hash(key, value);
+ }
+ }
+}