Add service that provides access to managed configurations.
--- /dev/null
+/*
+ * 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 <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.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<String, ManagedVpnProfile> 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<Bundle> 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<Bundle> 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<Bundle> 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<String, ManagedVpnProfile> getVpnProfiles()
+ {
+ return mManagedVpnProfiles;
+ }
+}
--- /dev/null
+/*
+ * 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 <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.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<String, ManagedVpnProfile> 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<String, ManagedVpnProfile> getManagedProfiles()
+ {
+ return mManagedVpnProfiles;
+ }
+}
--- /dev/null
+/*
+ * 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 <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+ * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * for more details.
+ */
+
+package org.strongswan.android.data;
+
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import org.strongswan.android.utils.Constants;
+
+import java.util.UUID;
+
+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";
+
+ 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);
+ if (remote != null)
+ {
+ setGateway(remote.getString(VpnProfileDataSource.KEY_GATEWAY));
+ setPort(getInt(remote, VpnProfileDataSource.KEY_PORT, 1, 65535));
+ setRemoteId(remote.getString(VpnProfileDataSource.KEY_REMOTE_ID));
+ setCertificateAlias(remote.getString(VpnProfileDataSource.KEY_CERTIFICATE));
+
+ 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);
+ }
+
+ final Bundle local = bundle.getBundle(KEY_LOCAL);
+ if (local != null)
+ {
+ setLocalId(local.getString(VpnProfileDataSource.KEY_LOCAL_ID));
+ setUsername(local.getString(VpnProfileDataSource.KEY_USERNAME));
+
+ flags = addPositiveFlag(flags, local, KEY_LOCAL_RSA_PSS_FLAG, VpnProfile.FLAGS_RSA_PSS);
+ }
+
+ final String includedPackageNames = bundle.getString(KEY_INCLUDED_APPS);
+ final String excludedPackageNames = bundle.getString(KEY_EXCLUDED_APPS);
+
+ if (!TextUtils.isEmpty(includedPackageNames))
+ {
+ setSelectedAppsHandling(VpnProfile.SelectedAppsHandling.SELECTED_APPS_ONLY);
+ setSelectedApps(includedPackageNames);
+ }
+ else if (!TextUtils.isEmpty(excludedPackageNames))
+ {
+ setSelectedAppsHandling(VpnProfile.SelectedAppsHandling.SELECTED_APPS_EXCLUDE);
+ setSelectedApps(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 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;
+ }
+}