summaryrefslogtreecommitdiff
path: root/app/src/main/java/se/leap/bitmaskclient/base
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/java/se/leap/bitmaskclient/base')
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java11
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java7
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/CensorshipCircumventionFragment.java166
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java5
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java8
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java166
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java3
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java25
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java41
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java126
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java14
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java1
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java132
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java522
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java175
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/BitmaskCoreProvider.java28
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java23
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java57
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/CredentialsParser.java58
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java2
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java242
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelper.java124
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/utils/RSAHelper.java72
-rw-r--r--app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java6
25 files changed, 1704 insertions, 313 deletions
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
index c7e12491..6b3ba348 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/BitmaskApp.java
@@ -35,8 +35,10 @@ import androidx.lifecycle.ProcessLifecycleOwner;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.multidex.MultiDexApplication;
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.conscrypt.Conscrypt;
+import java.security.Provider;
import java.security.Security;
import se.leap.bitmaskclient.BuildConfig;
@@ -70,7 +72,14 @@ public class BitmaskApp extends MultiDexApplication implements DefaultLifecycleO
super.onCreate();
// Normal app init code...*/
PRNGFixes.apply();
- Security.insertProviderAt(Conscrypt.newProvider(), 1);
+ final Provider provider = Security.getProvider(BouncyCastleProvider.PROVIDER_NAME);
+ // Replace Android's own BC provider
+ if (!provider.getClass().equals(BouncyCastleProvider.class)) {
+ Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME);
+ Security.insertProviderAt(new BouncyCastleProvider(), 1);
+ }
+ Security.insertProviderAt(Conscrypt.newProvider(), 2);
+
preferenceHelper = new PreferenceHelper(this);
providerObservable = ProviderObservable.getInstance();
providerObservable.updateProvider(getSavedProviderFromSharedPreferences());
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
index 21fc81e4..5363f16e 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/StartActivity.java
@@ -51,6 +51,7 @@ import se.leap.bitmaskclient.base.models.ProviderObservable;
import se.leap.bitmaskclient.base.utils.DateHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
import se.leap.bitmaskclient.eip.EipCommand;
+import se.leap.bitmaskclient.providersetup.ProviderSetupObservable;
import se.leap.bitmaskclient.providersetup.activities.SetupActivity;
/**
@@ -178,9 +179,10 @@ public class StartActivity extends Activity{
if ((hasNewFeature(FeatureVersionCode.CALYX_PROVIDER_LILYPAD_UPDATE) && (
getPackageName().equals("org.calyxinstitute.vpn") ||
ProviderObservable.getInstance().getCurrentProvider().getDomain().equals("calyx.net"))) ||
- hasNewFeature(FeatureVersionCode.RISEUP_PROVIDER_LILYPAD_UPDATE) && (
+ (hasNewFeature(FeatureVersionCode.RISEUP_PROVIDER_LILYPAD_UPDATE) || hasNewFeature(FeatureVersionCode.RISEUP_PROVIDER_LILYPAD_UPDATE_v2)
+ && (
getPackageName().equals("se.leap.riseupvpn") ||
- ProviderObservable.getInstance().getCurrentProvider().getDomain().equals("riseup.net"))) {
+ ProviderObservable.getInstance().getCurrentProvider().getDomain().equals("riseup.net")))) {
// deletion of current configured provider so that a new provider setup is triggered
Provider provider = ProviderObservable.getInstance().getCurrentProvider();
if (provider != null && !provider.isDefault()) {
@@ -267,6 +269,7 @@ public class StartActivity extends Activity{
}
private void showNextActivity(Provider provider) {
+ ProviderSetupObservable.cancel();
if (provider.shouldShowMotdSeen()) {
try {
IMessages messages = Motd.newMessages(provider.getMotdJsonString());
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/CensorshipCircumventionFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/CensorshipCircumventionFragment.java
new file mode 100644
index 00000000..fc561d48
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/CensorshipCircumventionFragment.java
@@ -0,0 +1,166 @@
+package se.leap.bitmaskclient.base.fragments;
+
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseObfs4;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseObfs4Kcp;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUsePortHopping;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseObfs4Quic;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseSnowflake;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.hasSnowflakePrefs;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.resetSnowflakeSettings;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUsePortHopping;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUseTunnel;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useBridges;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useSnowflake;
+import static se.leap.bitmaskclient.base.utils.ViewHelper.setActionBarSubtitle;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RadioButton;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import de.blinkt.openvpn.core.VpnStatus;
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.models.ProviderObservable;
+import se.leap.bitmaskclient.databinding.FCensorshipCircumventionBinding;
+import se.leap.bitmaskclient.eip.EipCommand;
+
+public class CensorshipCircumventionFragment extends Fragment {
+ public static int DISCOVERY_AUTOMATICALLY = 100200000;
+ public static int DISCOVERY_SNOWFLAKE = 100200001;
+ public static int DISCOVERY_INVITE_PROXY = 100200002;
+
+ public static int TUNNELING_AUTOMATICALLY = 100300000;
+ public static int TUNNELING_OBFS4 = 100300001;
+ public static int TUNNELING_OBFS4_KCP = 100300002;
+ public static int TUNNELING_QUIC = 100300003;
+
+ private @NonNull FCensorshipCircumventionBinding binding;
+
+ public static CensorshipCircumventionFragment newInstance() {
+ CensorshipCircumventionFragment fragment = new CensorshipCircumventionFragment();
+ Bundle args = new Bundle();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ binding = FCensorshipCircumventionBinding.inflate(getLayoutInflater(), container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ setActionBarSubtitle(this, R.string.censorship_circumvention);
+ initDiscovery();
+ initTunneling();
+ initPortHopping();
+ }
+
+
+ private void initDiscovery() {
+ boolean hasIntroducer = ProviderObservable.getInstance().getCurrentProvider().hasIntroducer();
+ RadioButton automaticallyRadioButton = new RadioButton(binding.getRoot().getContext());
+ automaticallyRadioButton.setText(getText(R.string.automatically_select));
+ automaticallyRadioButton.setId(DISCOVERY_AUTOMATICALLY);
+ automaticallyRadioButton.setChecked(!hasSnowflakePrefs() && !hasIntroducer);
+ binding.discoveryRadioGroup.addView(automaticallyRadioButton);
+
+ RadioButton snowflakeRadioButton = new RadioButton(binding.getRoot().getContext());
+ snowflakeRadioButton.setText(getText(R.string.snowflake));
+ snowflakeRadioButton.setId(DISCOVERY_SNOWFLAKE);
+ snowflakeRadioButton.setChecked(!hasIntroducer && hasSnowflakePrefs() && getUseSnowflake());
+ binding.discoveryRadioGroup.addView(snowflakeRadioButton);
+
+ if (hasIntroducer) {
+ RadioButton inviteProxyRadioButton = new RadioButton(binding.getRoot().getContext());
+ inviteProxyRadioButton.setText(getText(R.string.invite_proxy));
+ inviteProxyRadioButton.setId(DISCOVERY_INVITE_PROXY);
+ inviteProxyRadioButton.setChecked(true);
+ binding.discoveryRadioGroup.addView(inviteProxyRadioButton);
+ }
+
+ binding.discoveryRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ useBridges(true);
+ if (checkedId == DISCOVERY_AUTOMATICALLY) {
+ resetSnowflakeSettings();
+ } else if (checkedId == DISCOVERY_SNOWFLAKE) {
+ useSnowflake(true);
+ } else if (checkedId == DISCOVERY_INVITE_PROXY) {
+ useSnowflake(false);
+ }
+ });
+ }
+
+ private void tryReconnectVpn() {
+ if (VpnStatus.isVPNActive()) {
+ EipCommand.startVPN(getContext(), false);
+ Toast.makeText(getContext(), R.string.reconnecting, Toast.LENGTH_LONG).show();
+ }
+ }
+
+
+ private void initTunneling() {
+ RadioButton noneRadioButton = new RadioButton(binding.getRoot().getContext());
+ noneRadioButton.setText(getText(R.string.automatically_select));
+ noneRadioButton.setChecked(!getUseObfs4() && !getUseObfs4Kcp() && !getUseObfs4Quic());
+ noneRadioButton.setId(TUNNELING_AUTOMATICALLY);
+ binding.tunnelingRadioGroup.addView(noneRadioButton);
+
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsObfs4()) {
+ RadioButton obfs4RadioButton = new RadioButton(binding.getRoot().getContext());
+ obfs4RadioButton.setText(getText(R.string.tunnelling_obfs4));
+ obfs4RadioButton.setId(TUNNELING_OBFS4);
+ obfs4RadioButton.setChecked(getUseObfs4());
+ binding.tunnelingRadioGroup.addView(obfs4RadioButton);
+ }
+
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsObfs4Kcp()) {
+ RadioButton obfs4KcpRadioButton = new RadioButton(binding.getRoot().getContext());
+ obfs4KcpRadioButton.setText(getText(R.string.tunnelling_obfs4_kcp));
+ obfs4KcpRadioButton.setId(TUNNELING_OBFS4_KCP);
+ obfs4KcpRadioButton.setChecked(getUseObfs4Kcp());
+ binding.tunnelingRadioGroup.addView(obfs4KcpRadioButton);
+ }
+
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsObfs4Quic()) {
+ RadioButton obfs4QuicRadioButton = new RadioButton(binding.getRoot().getContext());
+ obfs4QuicRadioButton.setText(getText(R.string.tunnelling_quic));
+ obfs4QuicRadioButton.setId(TUNNELING_QUIC);
+ obfs4QuicRadioButton.setChecked(getUseObfs4Quic());
+ binding.tunnelingRadioGroup.addView(obfs4QuicRadioButton);
+ }
+
+ binding.tunnelingRadioGroup.setOnCheckedChangeListener((group, checkedId) -> {
+ useBridges(true);
+ setUseTunnel(checkedId);
+ tryReconnectVpn();
+ });
+ }
+
+ private void initPortHopping() {
+ binding.portHoppingSwitch.setVisibility(ProviderObservable.getInstance().getCurrentProvider().supportsObfs4Hop() ? View.VISIBLE : View.GONE);
+ binding.portHoppingSwitch.findViewById(R.id.material_icon).setVisibility(View.GONE);
+ binding.portHoppingSwitch.setChecked(getUsePortHopping());
+ binding.portHoppingSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (!buttonView.isPressed()) {
+ return;
+ }
+ useBridges(true);
+ setUsePortHopping(isChecked);
+ tryReconnectVpn();
+ });
+ }
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java
index fb93796e..76834332 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/EipFragment.java
@@ -50,6 +50,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.AppCompatImageView;
import androidx.appcompat.widget.AppCompatTextView;
import androidx.core.content.ContextCompat;
+import androidx.core.os.BundleCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
@@ -116,7 +117,7 @@ public class EipFragment extends Fragment implements PropertyChangeListener {
Activity activity = getActivity();
if (activity != null) {
if (arguments != null) {
- provider = arguments.getParcelable(PROVIDER_KEY);
+ provider = BundleCompat.getParcelable(arguments, PROVIDER_KEY, Provider.class);
if (provider == null) {
handleNoProvider(activity);
} else {
@@ -307,7 +308,7 @@ public class EipFragment extends Fragment implements PropertyChangeListener {
Log.e(TAG, "context is null when trying to start VPN");
return;
}
- if (!provider.getGeoipUrl().isDefault() && provider.shouldUpdateGeoIpJson()) {
+ if (!provider.getGeoipUrl().isEmpty() && provider.shouldUpdateGeoIpJson()) {
Bundle bundle = new Bundle();
bundle.putBoolean(EIP_ACTION_START, true);
bundle.putBoolean(EIP_EARLY_ROUTES, false);
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java
index bb5a06c4..5cd6c2a0 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/GatewaySelectionFragment.java
@@ -48,8 +48,6 @@ import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.lang.ref.WeakReference;
import java.util.List;
-import java.util.Observable;
-import java.util.Observer;
import de.blinkt.openvpn.core.VpnStatus;
import de.blinkt.openvpn.core.connection.Connection;
@@ -144,10 +142,11 @@ public class GatewaySelectionFragment extends Fragment implements PropertyChange
}
private void initBridgesHint(@NonNull View view) {
+ boolean allowResettingBridges = getUseBridges() && gatewaysManager.hasLocationsForOpenVPN();
bridgesHint = view.findViewById(R.id.manual_subtitle);
- bridgesHint.setVisibility(getUseBridges() ? VISIBLE : GONE);
+ bridgesHint.setVisibility(allowResettingBridges ? VISIBLE : GONE);
disableBridges = view.findViewById(R.id.disable_bridges);
- disableBridges.setVisibility(getUseBridges() ? VISIBLE : GONE);
+ disableBridges.setVisibility(allowResettingBridges ? VISIBLE : GONE);
disableBridges.setOnClickListener(v -> {
useBridges(false);
});
@@ -218,6 +217,7 @@ public class GatewaySelectionFragment extends Fragment implements PropertyChange
locationListAdapter.updateTransport(selectedTransport, gatewaysManager);
bridgesHint.setVisibility(showBridges ? VISIBLE : GONE);
disableBridges.setVisibility(showBridges ? VISIBLE : GONE);
+ updateRecommendedLocation();
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java
new file mode 100644
index 00000000..263a8d46
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LanguageSelectionFragment.java
@@ -0,0 +1,166 @@
+package se.leap.bitmaskclient.base.fragments;
+
+import static se.leap.bitmaskclient.base.utils.ViewHelper.setActionBarSubtitle;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatDelegate;
+import androidx.core.os.LocaleListCompat;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Locale;
+
+import se.leap.bitmaskclient.R;
+import se.leap.bitmaskclient.base.views.SimpleCheckBox;
+import se.leap.bitmaskclient.databinding.FLanguageSelectionBinding;
+import se.leap.bitmaskclient.databinding.VSelectTextListItemBinding;
+
+public class LanguageSelectionFragment extends BottomSheetDialogFragment {
+ static final String TAG = LanguageSelectionFragment.class.getSimpleName();
+ static final String SYSTEM_LOCALE = "systemLocale";
+ private FLanguageSelectionBinding binding;
+
+ public static LanguageSelectionFragment newInstance(Locale defaultLocale) {
+ LanguageSelectionFragment fragment = new LanguageSelectionFragment();
+ Bundle args = new Bundle();
+ args.putString(SYSTEM_LOCALE, defaultLocale.toLanguageTag());
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ binding = FLanguageSelectionBinding.inflate(inflater, container, false);
+ return binding.getRoot();
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ setActionBarSubtitle(this, R.string.select_language);
+
+ initRecyclerView(Arrays.asList(getResources().getStringArray(R.array.supported_languages)));
+ }
+
+ private static void customizeSelectionItem(VSelectTextListItemBinding binding) {
+ binding.title.setVisibility(View.GONE);
+ binding.bridgeImage.setVisibility(View.GONE);
+ binding.quality.setVisibility(View.GONE);
+ }
+
+ private void initRecyclerView(List<String> supportedLanguages) {
+ Locale defaultLocale = AppCompatDelegate.getApplicationLocales().get(0);
+ if (defaultLocale == null) {
+ defaultLocale = LocaleListCompat.getDefault().get(0);
+ }
+ // NOTE: Sort the supported languages by their display names.
+ // This would make updating supported languages easier as we don't have to tip toe around the order
+ Collections.sort(supportedLanguages, (lang1, lang2) -> {
+ String displayName1 = Locale.forLanguageTag(lang1).getDisplayName(Locale.ENGLISH);
+ String displayName2 = Locale.forLanguageTag(lang2).getDisplayName(Locale.ENGLISH);
+ return displayName1.compareTo(displayName2);
+ });
+ binding.languages.setAdapter(
+ new LanguageSelectionAdapter(supportedLanguages, this::updateLocale, defaultLocale)
+ );
+ binding.languages.setLayoutManager(new LinearLayoutManager(getContext()));
+ }
+
+ public static Locale getCurrentLocale() {
+ Locale defaultLocale = AppCompatDelegate.getApplicationLocales().get(0);
+ if (defaultLocale == null) {
+ defaultLocale = LocaleListCompat.getDefault().get(0);
+ }
+ return defaultLocale;
+ }
+
+ /**
+ * Update the locale of the application
+ *
+ * @param languageTag the language tag to set the locale to
+ */
+ private void updateLocale(String languageTag) {
+ if (languageTag.isEmpty()) {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.getEmptyLocaleList());
+ } else {
+ AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(languageTag));
+ }
+ }
+
+ /**
+ * Adapter for the language selection recycler view.
+ */
+ static class LanguageSelectionAdapter extends RecyclerView.Adapter<LanguageSelectionAdapter.LanguageViewHolder> {
+
+ private final List<String> languages;
+ private final LanguageClickListener clickListener;
+ private final Locale selectedLocale;
+
+ public LanguageSelectionAdapter(List<String> languages, LanguageClickListener clickListener, Locale defaultLocale) {
+ this.languages = languages;
+ this.clickListener = clickListener;
+ this.selectedLocale = defaultLocale;
+ }
+
+ @NonNull
+ @Override
+ public LanguageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ VSelectTextListItemBinding binding = VSelectTextListItemBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
+ return new LanguageViewHolder(binding);
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull LanguageViewHolder holder, int position) {
+ String languageTag = languages.get(position);
+ holder.languageName.setText(Locale.forLanguageTag(languageTag).getDisplayName(Locale.ENGLISH));
+ if (languages.contains(selectedLocale.toLanguageTag())) {
+ holder.selected.setChecked(selectedLocale.toLanguageTag().equals(languageTag));
+ } else {
+ holder.selected.setChecked(selectedLocale.getLanguage().equals(languageTag));
+ }
+ holder.itemView.setOnClickListener(v -> clickListener.onLanguageClick(languageTag));
+ }
+
+ @Override
+ public int getItemCount() {
+ return languages.size();
+ }
+
+ /**
+ * View holder for the language item
+ */
+ static class LanguageViewHolder extends RecyclerView.ViewHolder {
+ TextView languageName;
+ SimpleCheckBox selected;
+
+ public LanguageViewHolder(@NonNull VSelectTextListItemBinding binding) {
+ super(binding.getRoot());
+ languageName = binding.location;
+ selected = binding.selected;
+ customizeSelectionItem(binding);
+ }
+ }
+ }
+
+
+ /**
+ * Interface for the language click listener
+ */
+ interface LanguageClickListener {
+ void onLanguageClick(String languageTag);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java
index 56b7259e..c165d19b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/LogFragment.java
@@ -43,6 +43,7 @@ import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatTextView;
+import androidx.core.os.BundleCompat;
import androidx.fragment.app.ListFragment;
import java.text.SimpleDateFormat;
@@ -300,7 +301,7 @@ public class LogFragment extends ListFragment implements StateListener, SeekBar.
// We have been called
if (msg.what == MESSAGE_NEWLOG) {
- LogItem logMessage = msg.getData().getParcelable("logmessage");
+ LogItem logMessage = BundleCompat.getParcelable(msg.getData(), "logmessage", LogItem.class);
if (addLogMessage(logMessage))
for (DataSetObserver observer : observers) {
observer.onChanged();
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
index 3dbdbe64..ca84d330 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/MainActivityErrorDialog.java
@@ -37,6 +37,7 @@ import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
+import androidx.core.os.BundleCompat;
import androidx.fragment.app.DialogFragment;
import org.json.JSONObject;
@@ -186,7 +187,7 @@ public class MainActivityErrorDialog extends DialogFragment {
return;
}
if (savedInstanceState.containsKey(KEY_PROVIDER)) {
- this.provider = savedInstanceState.getParcelable(KEY_PROVIDER);
+ this.provider = BundleCompat.getParcelable(savedInstanceState, KEY_PROVIDER, Provider.class);
}
if (savedInstanceState.containsKey(KEY_REASON_TO_FAIL)) {
this.reasonToFail = savedInstanceState.getString(KEY_REASON_TO_FAIL);
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
index cbab1d32..41e102bb 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/NavigationDrawerFragment.java
@@ -46,15 +46,19 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBarDrawerToggle;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.app.AppCompatDelegate;
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable;
import androidx.appcompat.widget.Toolbar;
import androidx.core.content.ContextCompat;
+import androidx.core.os.LocaleListCompat;
import androidx.core.view.GravityCompat;
import androidx.drawerlayout.widget.DrawerLayout;
import androidx.fragment.app.Fragment;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
+import java.util.Locale;
+import java.util.Map;
import se.leap.bitmaskclient.BuildConfig;
import se.leap.bitmaskclient.R;
@@ -211,6 +215,7 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
initSwitchProviderEntry();
initSaveBatteryEntry();
initManualGatewayEntry();
+ initLanguageSettingsEntry();
initAdvancedSettingsEntry();
initDonateEntry();
initLogEntry();
@@ -314,6 +319,26 @@ public class NavigationDrawerFragment extends Fragment implements SharedPreferen
});
}
+
+ private void initLanguageSettingsEntry() {
+ IconTextEntry languageSwitcher = drawerView.findViewById(R.id.language_switcher);
+
+ Locale currentLocale = LanguageSelectionFragment.getCurrentLocale();
+ languageSwitcher.setSubtitle(currentLocale.getDisplayName(Locale.ENGLISH));
+
+ languageSwitcher.setOnClickListener(v -> {
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ closeDrawer();
+ Fragment current = fragmentManager.findFragmentByTag(MainActivity.TAG);
+ if (current instanceof LanguageSelectionFragment) {
+ return;
+ }
+ Fragment fragment = LanguageSelectionFragment.newInstance(Locale.getDefault());
+ setDrawerToggleColor(drawerView.getContext(), ContextCompat.getColor(drawerView.getContext(), R.color.colorActionBarTitleFont));
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ });
+ }
+
private void initDonateEntry() {
if (ENABLE_DONATION) {
IconTextEntry donate = drawerView.findViewById(R.id.donate);
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java
index 7d12ca70..2e4eec8a 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/ObfuscationProxyDialog.java
@@ -2,11 +2,16 @@ package se.leap.bitmaskclient.base.fragments;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.KCP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.QUIC;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.TCP;
import android.app.Dialog;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -14,10 +19,10 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatDialogFragment;
import androidx.appcompat.widget.AppCompatButton;
import androidx.appcompat.widget.AppCompatEditText;
+import androidx.appcompat.widget.AppCompatSpinner;
import se.leap.bitmaskclient.base.utils.BuildConfigHelper;
import se.leap.bitmaskclient.base.utils.PreferenceHelper;
-import se.leap.bitmaskclient.base.views.IconSwitchEntry;
import se.leap.bitmaskclient.databinding.DObfuscationProxyBinding;
import se.leap.bitmaskclient.eip.GatewaysManager;
@@ -30,7 +35,9 @@ public class ObfuscationProxyDialog extends AppCompatDialogFragment {
AppCompatButton saveButton;
AppCompatButton useDefaultsButton;
AppCompatButton cancelButton;
- IconSwitchEntry kcpSwitch;
+ AppCompatSpinner protocolSpinner;
+ private final String[] protocols = { TCP.toString(), KCP.toString(), QUIC.toString() };
+
@NonNull
@Override
@@ -46,15 +53,29 @@ public class ObfuscationProxyDialog extends AppCompatDialogFragment {
saveButton = binding.buttonSave;
useDefaultsButton = binding.buttonDefaults;
cancelButton = binding.buttonCancel;
- kcpSwitch = binding.kcpSwitch;
+ protocolSpinner = binding.protocolSpinner;
ipField.setText(PreferenceHelper.getObfuscationPinningIP());
portField.setText(PreferenceHelper.getObfuscationPinningPort());
certificateField.setText(PreferenceHelper.getObfuscationPinningCert());
- kcpSwitch.setChecked(PreferenceHelper.getObfuscationPinningKCP());
GatewaysManager gatewaysManager = new GatewaysManager(getContext());
+
+ ArrayAdapter<String> adapter = new ArrayAdapter<>(binding.getRoot().getContext(), android.R.layout.simple_spinner_item, protocols);
+ adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ protocolSpinner.setAdapter(adapter);
+
+ protocolSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ @Override
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ PreferenceHelper.setObfuscationPinningProtocol(protocols[position]);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView<?> parent) {}
+ });
+
saveButton.setOnClickListener(v -> {
String ip = TextUtils.isEmpty(ipField.getText()) ? null : ipField.getText().toString();
PreferenceHelper.setObfuscationPinningIP(ip);
@@ -62,7 +83,6 @@ public class ObfuscationProxyDialog extends AppCompatDialogFragment {
PreferenceHelper.setObfuscationPinningPort(port);
String cert = TextUtils.isEmpty(certificateField.getText()) ? null : certificateField.getText().toString();
PreferenceHelper.setObfuscationPinningCert(cert);
- PreferenceHelper.setObfuscationPinningKCP(kcpSwitch.isChecked());
PreferenceHelper.setUseObfuscationPinning(ip != null && port != null && cert != null);
PreferenceHelper.setObfuscationPinningGatewayLocation(gatewaysManager.getLocationNameForIP(ip, v.getContext()));
dismiss();
@@ -73,7 +93,7 @@ public class ObfuscationProxyDialog extends AppCompatDialogFragment {
ipField.setText(BuildConfigHelper.obfsvpnIP());
portField.setText(BuildConfigHelper.obfsvpnPort());
certificateField.setText(BuildConfigHelper.obfsvpnCert());
- kcpSwitch.setChecked(BuildConfigHelper.useKcp());
+ protocolSpinner.setSelection(getIndexForProtocol(BuildConfigHelper.obfsvpnTransportProtocol()));
});
cancelButton.setOnClickListener(v -> {
@@ -85,6 +105,15 @@ public class ObfuscationProxyDialog extends AppCompatDialogFragment {
return builder.create();
}
+ private int getIndexForProtocol(String protocol) {
+ for (int i = 0; i < protocols.length; i++) {
+ if (protocols[i].equals(protocol)) {
+ return i;
+ }
+ }
+ return 0;
+ }
+
@Override
public void onDestroyView() {
super.onDestroyView();
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java b/app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java
index d7b62de2..d4d72812 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/fragments/SettingsFragment.java
@@ -3,12 +3,12 @@ package se.leap.bitmaskclient.base.fragments;
import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static se.leap.bitmaskclient.R.string.advanced_settings;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_AUTOMATICALLY;
import static se.leap.bitmaskclient.base.models.Constants.GATEWAY_PINNING;
import static se.leap.bitmaskclient.base.models.Constants.PREFER_UDP;
import static se.leap.bitmaskclient.base.models.Constants.USE_BRIDGES;
import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL;
import static se.leap.bitmaskclient.base.models.Constants.USE_OBFUSCATION_PINNING;
-import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.useObfsVpn;
import static se.leap.bitmaskclient.base.utils.ConfigHelper.isCalyxOSWithTetheringSupport;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.allowExperimentalTransports;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getExcludedApps;
@@ -18,11 +18,16 @@ import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseBridges;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.getUseSnowflake;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.hasSnowflakePrefs;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.preferUDP;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.resetSnowflakeSettings;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setAllowExperimentalTransports;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUseObfuscationPinning;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUsePortHopping;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.setUseTunnel;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useBridges;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useManualDiscoverySettings;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useObfuscationPinning;
import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useSnowflake;
+import static se.leap.bitmaskclient.base.utils.PreferenceHelper.useManualBridgeSettings;
import static se.leap.bitmaskclient.base.utils.ViewHelper.setActionBarSubtitle;
import android.app.AlertDialog;
@@ -36,10 +41,13 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.EditText;
+import android.widget.LinearLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.appcompat.widget.AppCompatTextView;
+import androidx.appcompat.widget.SwitchCompat;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentTransaction;
@@ -79,8 +87,8 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
initAlwaysOnVpnEntry(view);
initExcludeAppsEntry(view);
initPreferUDPEntry(view);
- initUseBridgesEntry(view);
- initUseSnowflakeEntry(view);
+ initAutomaticCircumventionEntry(view);
+ initManualCircumventionEntry(view);
initFirewallEntry(view);
initTetheringEntry(view);
initGatewayPinningEntry(view);
@@ -90,48 +98,91 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
return view;
}
- @Override
- public void onDestroy() {
- PreferenceHelper.unregisterOnSharedPreferenceChangeListener(this);
- super.onDestroy();
- }
+ private void initAutomaticCircumventionEntry(View rootView) {
+ IconSwitchEntry automaticCircumvention = rootView.findViewById(R.id.bridge_automatic_switch);
+ automaticCircumvention.setChecked(getUseBridges() && !useManualBridgeSettings());
+ automaticCircumvention.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ if (!buttonView.isPressed()) {
+ return;
+ }
- private void initUseBridgesEntry(View rootView) {
- IconSwitchEntry useBridges = rootView.findViewById(R.id.bridges_switch);
- if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
- useBridges.setVisibility(VISIBLE);
- useBridges.setChecked(getUseBridges());
- useBridges.setOnCheckedChangeListener((buttonView, isChecked) -> {
- if (!buttonView.isPressed()) {
- return;
- }
+ if (isChecked) {
+ resetSnowflakeSettings();
+ setUseTunnel(TUNNELING_AUTOMATICALLY);
+ setUsePortHopping(false);
+ } else {
+ useSnowflake(false);
+ }
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports()) {
useBridges(isChecked);
if (VpnStatus.isVPNActive()) {
EipCommand.startVPN(getContext(), false);
Toast.makeText(getContext(), R.string.reconnecting, Toast.LENGTH_LONG).show();
}
- });
- //We check the UI state of the useUdpEntry here as well, in order to avoid a situation
- //where both entries are disabled, because both preferences are enabled.
- //bridges can be enabled not only from here but also from error handling
- boolean useUDP = getPreferUDP() && useUdpEntry.isEnabled();
- useBridges.setEnabled(!useUDP);
- useBridges.setSubtitle(getString(useUDP ? R.string.disabled_while_udp_on : R.string.nav_drawer_subtitle_obfuscated_connection));
- } else {
- useBridges.setVisibility(GONE);
- }
+ }
+ });
+
+ //We check the UI state of the useUdpEntry here as well, in order to avoid a situation
+ //where both entries are disabled, because both preferences are enabled.
+ //bridges can be enabled not only from here but also from error handling
+ boolean useUDP = getPreferUDP() && useUdpEntry.isEnabled();
+ automaticCircumvention.setEnabled(!useUDP);
+ automaticCircumvention.setSubtitle(getString(useUDP ? R.string.disabled_while_udp_on : R.string.automatic_bridge_description));
}
- private void initUseSnowflakeEntry(View rootView) {
- IconSwitchEntry useSnowflake = rootView.findViewById(R.id.snowflake_switch);
- useSnowflake.setVisibility(VISIBLE);
- useSnowflake.setChecked(hasSnowflakePrefs() && getUseSnowflake());
- useSnowflake.setOnCheckedChangeListener((buttonView, isChecked) -> {
+ private void initManualCircumventionEntry(View rootView) {
+ LinearLayout manualConfigRoot = rootView.findViewById(R.id.bridge_manual_switch_entry);
+ manualConfigRoot.setVisibility(ProviderObservable.getInstance().getCurrentProvider().supportsPluggableTransports() ? VISIBLE : GONE);
+ IconTextEntry manualConfiguration = rootView.findViewById(R.id.bridge_manual_switch);
+ SwitchCompat manualConfigurationSwitch = rootView.findViewById(R.id.bridge_manual_switch_control);
+ boolean useManualCircumventionSettings = useManualBridgeSettings() || useManualDiscoverySettings();
+ manualConfigurationSwitch.setChecked(useManualCircumventionSettings);
+ manualConfigurationSwitch.setOnCheckedChangeListener((buttonView, isChecked) -> {
if (!buttonView.isPressed()) {
return;
}
- useSnowflake(isChecked);
+ resetManualConfig();
+ if (!useManualCircumventionSettings){
+ openManualConfigurationFragment();
+ }
});
+ manualConfiguration.setOnClickListener((buttonView) -> openManualConfigurationFragment());
+
+ //We check the UI state of the useUdpEntry here as well, in order to avoid a situation
+ //where both entries are disabled, because both preferences are enabled.
+ //bridges can be enabled not only from here but also from error handling
+ boolean useUDP = getPreferUDP() && useUdpEntry.isEnabled();
+ manualConfiguration.setEnabled(!useUDP);
+ manualConfigurationSwitch.setVisibility(useUDP ? GONE : VISIBLE);
+ manualConfiguration.setSubtitle(getString(useUDP ? R.string.disabled_while_udp_on : R.string.manual_bridge_description));
+ }
+
+ private void resetManualConfig() {
+ useSnowflake(false);
+ setUseTunnel(TUNNELING_AUTOMATICALLY);
+ setUsePortHopping(false);
+ useBridges(false);
+ if (VpnStatus.isVPNActive()) {
+ EipCommand.startVPN(getContext(), false);
+ Toast.makeText(getContext(), R.string.reconnecting, Toast.LENGTH_LONG).show();
+ }
+ View rootView = getView();
+ if (rootView == null) {
+ return;
+ }
+ initAutomaticCircumventionEntry(rootView);
+ }
+
+ private void openManualConfigurationFragment() {
+ FragmentManagerEnhanced fragmentManager = new FragmentManagerEnhanced(getActivity().getSupportFragmentManager());
+ Fragment fragment = CensorshipCircumventionFragment.newInstance();
+ fragmentManager.replace(R.id.main_container, fragment, MainActivity.TAG);
+ }
+
+ @Override
+ public void onDestroy() {
+ PreferenceHelper.unregisterOnSharedPreferenceChangeListener(this);
+ super.onDestroy();
}
private void initAlwaysOnVpnEntry(View rootView) {
@@ -225,7 +276,7 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
private void initGatewayPinningEntry(View rootView) {
IconTextEntry gatewayPinning = rootView.findViewById(R.id.gateway_pinning);
- if (!BuildConfig.BUILD_TYPE.equals("debug")) {
+ if (!BuildConfig.DEBUG_MODE) {
gatewayPinning.setVisibility(GONE);
return;
}
@@ -261,7 +312,7 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
public void initObfuscationPinningEntry(View rootView) {
IconSwitchEntry obfuscationPinning = rootView.findViewById(R.id.obfuscation_proxy_pinning);
- if (!BuildConfig.BUILD_TYPE.equals("debug") || !useObfsVpn()) {
+ if (!BuildConfig.DEBUG_MODE) {
obfuscationPinning.setVisibility(GONE);
return;
}
@@ -302,7 +353,7 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
public void initExperimentalTransportsEntry(View rootView) {
IconSwitchEntry experimentalTransports = rootView.findViewById(R.id.experimental_transports);
- if (useObfsVpn() && ProviderObservable.getInstance().getCurrentProvider().supportsExperimentalPluggableTransports()) {
+ if (ProviderObservable.getInstance().getCurrentProvider().supportsExperimentalPluggableTransports()) {
experimentalTransports.setVisibility(VISIBLE);
experimentalTransports.setChecked(allowExperimentalTransports());
experimentalTransports.setOnCheckedChangeListener((buttonView, isChecked) -> {
@@ -351,8 +402,9 @@ public class SettingsFragment extends Fragment implements SharedPreferences.OnSh
return;
}
if (key.equals(USE_BRIDGES) || key.equals(PREFER_UDP)) {
- initUseBridgesEntry(rootView);
initPreferUDPEntry(rootView);
+ initManualCircumventionEntry(rootView);
+ initAutomaticCircumventionEntry(rootView);
} else if (key.equals(USE_IPv6_FIREWALL)) {
initFirewallEntry(getView());
} else if (key.equals(GATEWAY_PINNING)) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
index 754491f8..b8849c4d 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Constants.java
@@ -42,6 +42,8 @@ public interface Constants {
String RESTART_ON_UPDATE = "restart_on_update";
String LAST_UPDATE_CHECK = "last_update_check";
String PREFERRED_CITY = "preferred_city";
+ // ATTENTION: this key is also used in bitmask-core for persistence
+ String COUNTRYCODE = "COUNTRYCODE";
String USE_SNOWFLAKE = "use_snowflake";
String PREFER_UDP = "prefer_UDP";
String GATEWAY_PINNING = "gateway_pinning";
@@ -51,9 +53,12 @@ public interface Constants {
String OBFUSCATION_PINNING_PORT = "obfuscation_pinning_port";
String OBFUSCATION_PINNING_CERT = "obfuscation_pinning_cert";
String OBFUSCATION_PINNING_KCP = "obfuscation_pinning_udp";
+ String OBFUSCATION_PINNING_PROTOCOL = "obfuscation_pinning_protocol";
String OBFUSCATION_PINNING_LOCATION = "obfuscation_pinning_location";
String USE_SYSTEM_PROXY = "usesystemproxy";
String CUSTOM_PROVIDER_DOMAINS = "custom_provider_domains";
+ String USE_PORT_HOPPING = "use_port_hopping";
+ String USE_TUNNEL = "tunnel";
//////////////////////////////////////////////
@@ -122,6 +127,10 @@ public interface Constants {
String PROVIDER_MOTD_HASHES = "Constants.PROVIDER_MOTD_HASHES";
String PROVIDER_MOTD_LAST_SEEN = "Constants.PROVIDER_MOTD_LAST_SEEN";
String PROVIDER_MOTD_LAST_UPDATED = "Constants.PROVIDER_MOTD_LAST_UPDATED";
+ String PROVIDER_MODELS_PROVIDER = "Constants.PROVIDER_MODELS_PROVIDER";
+ String PROVIDER_MODELS_EIPSERVICE = "Constants.PROVIDER_MDOELS_EIPSERVICE";
+ String PROVIDER_MODELS_GATEWAYS = "Constants.PROVIDER_MODELS_GATEWAYS";
+ String PROVIDER_MODELS_BRIDGES = "Constants.PROVIDER_MODELS_BRIDGES";
////////////////////////////////////////////////
// PRESHIPPED PROVIDER CONFIG
@@ -184,6 +193,7 @@ public interface Constants {
String UDP = "udp";
String TCP = "tcp";
String KCP = "kcp";
+ String QUIC = "quic";
String CAPABILITIES = "capabilities";
String TRANSPORT = "transport";
String TYPE = "type";
@@ -194,6 +204,10 @@ public interface Constants {
String ENDPOINTS = "endpoints";
String PORT_SEED = "port_seed";
String PORT_COUNT = "port_count";
+ String HOP_JITTER = "hop_jitter";
+ String MIN_HOP_PORT = "min_hop_port";
+ String MAX_HOP_PORT = "max_hop_port";
+ String MIN_HOP_SECONDS = "min_hop_seconds";
String EXPERIMENTAL = "experimental";
String VERSION = "version";
String NAME = "name";
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java b/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java
index 8b78c966..1771f0b0 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/FeatureVersionCode.java
@@ -5,6 +5,7 @@ public interface FeatureVersionCode {
int GEOIP_SERVICE = 148;
int CALYX_PROVIDER_LILYPAD_UPDATE = 165000;
int RISEUP_PROVIDER_LILYPAD_UPDATE = 165000;
+ int RISEUP_PROVIDER_LILYPAD_UPDATE_v2 = 172000;
int ENCRYPTED_SHARED_PREFS = 170000;
int NOTIFICATION_PREMISSION_API_UPDATE = 170000;
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java
new file mode 100644
index 00000000..e3175010
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Introducer.java
@@ -0,0 +1,132 @@
+package se.leap.bitmaskclient.base.models;
+
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.UnsupportedEncodingException;
+import java.net.IDN;
+import java.net.URISyntaxException;
+import java.net.URLEncoder;
+import java.util.Locale;
+
+public class Introducer implements Parcelable {
+ private final String type;
+ private final String address;
+ private final String certificate;
+ private final String fullyQualifiedDomainName;
+ private final boolean kcpEnabled;
+ private final String auth;
+
+ public Introducer(String type, String address, String certificate, String fullyQualifiedDomainName, boolean kcpEnabled, String auth) {
+ this.type = type;
+ this.address = address;
+ this.certificate = certificate;
+ this.fullyQualifiedDomainName = fullyQualifiedDomainName;
+ this.kcpEnabled = kcpEnabled;
+ this.auth = auth;
+ }
+
+ protected Introducer(Parcel in) {
+ type = in.readString();
+ address = in.readString();
+ certificate = in.readString();
+ fullyQualifiedDomainName = in.readString();
+ kcpEnabled = in.readByte() != 0;
+ auth = in.readString();
+ }
+
+ public String getFullyQualifiedDomainName() {
+ return fullyQualifiedDomainName;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(type);
+ dest.writeString(address);
+ dest.writeString(certificate);
+ dest.writeString(fullyQualifiedDomainName);
+ dest.writeByte((byte) (kcpEnabled ? 1 : 0));
+ dest.writeString(auth);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ public static final Creator<Introducer> CREATOR = new Creator<>() {
+ @Override
+ public Introducer createFromParcel(Parcel in) {
+ return new Introducer(in);
+ }
+
+ @Override
+ public Introducer[] newArray(int size) {
+ return new Introducer[size];
+ }
+ };
+
+ public boolean validate() {
+ if (!"obfsvpnintro".equals(type)) {
+ throw new IllegalArgumentException("Unknown type: " + type);
+ }
+ if (!address.contains(":") || address.split(":").length != 2) {
+ throw new IllegalArgumentException("Expected address in format ipaddr:port");
+ }
+ if (certificate.length() != 70) {
+ throw new IllegalArgumentException("Wrong certificate length: " + certificate.length());
+ }
+ if (!"localhost".equals(fullyQualifiedDomainName) && fullyQualifiedDomainName.split("\\.").length < 2) {
+ throw new IllegalArgumentException("Expected a FQDN, got: " + fullyQualifiedDomainName);
+ }
+
+ if (auth == null || auth.isEmpty()) {
+ throw new IllegalArgumentException("Auth token is missing");
+ }
+ return true;
+ }
+
+ public static Introducer fromUrl(String introducerUrl) throws URISyntaxException, IllegalArgumentException {
+ Uri uri = Uri.parse(introducerUrl);
+ String fqdn = uri.getQueryParameter("fqdn");
+ if (fqdn == null || fqdn.isEmpty()) {
+ throw new IllegalArgumentException("FQDN not found in the introducer URL");
+ }
+
+ if (!isAscii(fqdn)) {
+ throw new IllegalArgumentException("FQDN is not ASCII: " + fqdn);
+ }
+
+ boolean kcp = "1".equals(uri.getQueryParameter( "kcp"));
+
+ String cert = uri.getQueryParameter( "cert");
+ if (cert == null || cert.isEmpty()) {
+ throw new IllegalArgumentException("Cert not found in the introducer URL");
+ }
+
+ String auth = uri.getQueryParameter( "auth");
+ if (auth == null || auth.isEmpty()) {
+ throw new IllegalArgumentException("Authentication token not found in the introducer URL");
+ }
+ return new Introducer(uri.getScheme(), uri.getAuthority(), cert, fqdn, kcp, auth);
+ }
+
+ public String getAuthToken() {
+ return auth;
+ }
+
+ private static boolean isAscii(String fqdn) {
+ try {
+ String asciiFQDN = IDN.toASCII(fqdn, IDN.USE_STD3_ASCII_RULES);
+ return fqdn.equals(asciiFQDN);
+ } catch (IllegalArgumentException e) {
+ return false;
+ }
+ }
+
+ public String toUrl() throws UnsupportedEncodingException {
+ return String.format(Locale.US, "%s://%s?fqdn=%s&kcp=%d&cert=%s&auth=%s", type, address, URLEncoder.encode(fullyQualifiedDomainName, "UTF-8"), kcpEnabled ? 1 : 0, URLEncoder.encode(certificate, "UTF-8"), URLEncoder.encode(auth, "UTF-8"));
+ }
+
+} \ No newline at end of file
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
index cb9bd520..b4ec23e6 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Provider.java
@@ -17,6 +17,7 @@
package se.leap.bitmaskclient.base.models;
import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.KCP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.QUIC;
import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.TCP;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4;
import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
@@ -28,8 +29,9 @@ import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOWED_REGIS
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_ALLOW_ANONYMOUS;
import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
import static se.leap.bitmaskclient.base.models.Constants.TYPE;
-import static se.leap.bitmaskclient.base.utils.BuildConfigHelper.useObfsVpn;
-import static se.leap.bitmaskclient.base.utils.RSAHelper.parseRsaKeyFromString;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.isDomainName;
+import static se.leap.bitmaskclient.base.utils.ConfigHelper.isNetworkUrl;
+import static se.leap.bitmaskclient.base.utils.PrivateKeyHelper.parsePrivateKeyFromString;
import static se.leap.bitmaskclient.providersetup.ProviderAPI.ERRORS;
import android.os.Parcel;
@@ -38,23 +40,33 @@ import android.os.Parcelable;
import androidx.annotation.NonNull;
import com.google.gson.Gson;
+import com.google.gson.JsonSyntaxException;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.MalformedURLException;
+import java.net.URISyntaxException;
import java.net.URL;
-import java.security.interfaces.RSAPrivateKey;
+import java.security.PrivateKey;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
+import java.util.Objects;
import java.util.Set;
import de.blinkt.openvpn.core.connection.Connection.TransportProtocol;
import de.blinkt.openvpn.core.connection.Connection.TransportType;
+import io.swagger.client.JSON;
+import io.swagger.client.model.ModelsBridge;
+import io.swagger.client.model.ModelsEIPService;
+import io.swagger.client.model.ModelsGateway;
+import io.swagger.client.model.ModelsProvider;
import motd.IStringCollection;
import motd.Motd;
+import se.leap.bitmaskclient.BuildConfig;
/**
* @author Sean Leonard <meanderingcode@aetherislands.net>
@@ -69,25 +81,30 @@ public final class Provider implements Parcelable {
private JSONObject eipServiceJson = new JSONObject();
private JSONObject geoIpJson = new JSONObject();
private JSONObject motdJson = new JSONObject();
- private DefaultedURL mainUrl = new DefaultedURL();
- private DefaultedURL apiUrl = new DefaultedURL();
- private DefaultedURL geoipUrl = new DefaultedURL();
- private DefaultedURL motdUrl = new DefaultedURL();
+ private String mainUrl = "";
+ private String apiUrl = "";
+ private String geoipUrl = "";
+ private String motdUrl = "";
+ private ModelsEIPService modelsEIPService = null;
+ private ModelsProvider modelsProvider = null;
+ private ModelsGateway[] modelsGateways = null;
+ private ModelsBridge[] modelsBridges = null;
private String domain = "";
private String providerIp = ""; // ip of the provider main url
private String providerApiIp = ""; // ip of the provider api url
private String certificatePin = "";
private String certificatePinEncoding = "";
private String caCert = "";
- private String apiVersion = "";
- private String privateKey = "";
-
- private transient RSAPrivateKey rsaPrivateKey = null;
+ private int apiVersion = 3;
+ private int[] apiVersions = new int[0];
+ private String privateKeyString = "";
+ private transient PrivateKey privateKey = null;
private String vpnCertificate = "";
private long lastEipServiceUpdate = 0L;
private long lastGeoIpUpdate = 0L;
private long lastMotdUpdate = 0L;
private long lastMotdSeen = 0L;
+ private Introducer introducer = null;
private Set<String> lastMotdSeenHashes = new HashSet<>();
private boolean shouldUpdateVpnCertificate;
@@ -97,6 +114,7 @@ public final class Provider implements Parcelable {
final public static String
API_URL = "api_uri",
API_VERSION = "api_version",
+ API_VERSIONS = "api_versions",
ALLOW_REGISTRATION = "allow_registration",
API_RETURN_SERIAL = "serial",
SERVICE = "service",
@@ -117,37 +135,36 @@ public final class Provider implements Parcelable {
public Provider() { }
+ public Provider(Introducer introducer) {
+ this("https://" + introducer.getFullyQualifiedDomainName());
+ this.introducer = introducer;
+ }
+
public Provider(String mainUrl) {
- this(mainUrl, null);
+ this(mainUrl, null);
+ domain = getHostFromUrl(mainUrl);
}
public Provider(String mainUrl, String geoipUrl) {
- try {
- this.mainUrl.setUrl(new URL(mainUrl));
- } catch (MalformedURLException e) {
- this.mainUrl = new DefaultedURL();
- }
+ setMainUrl(mainUrl);
setGeoipUrl(geoipUrl);
+ domain = getHostFromUrl(mainUrl);
}
- public static Provider createCustomProvider(String mainUrl, String domain) {
+ public static Provider createCustomProvider(String mainUrl, String domain, Introducer introducer) {
Provider p = new Provider(mainUrl);
p.domain = domain;
+ p.introducer = introducer;
return p;
}
public Provider(String mainUrl, String geoipUrl, String motdUrl, String providerIp, String providerApiIp) {
- try {
- this.mainUrl.setUrl(new URL(mainUrl));
- if (providerIp != null) {
- this.providerIp = providerIp;
- }
- if (providerApiIp != null) {
- this.providerApiIp = providerApiIp;
- }
- } catch (MalformedURLException e) {
- e.printStackTrace();
- return;
+ setMainUrl(mainUrl);
+ if (providerIp != null) {
+ this.providerIp = providerIp;
+ }
+ if (providerApiIp != null) {
+ this.providerApiIp = providerApiIp;
}
setGeoipUrl(geoipUrl);
setMotdUrl(motdUrl);
@@ -179,64 +196,195 @@ public final class Provider implements Parcelable {
}
};
+ public void setBridges(String bridgesJson) {
+ if (bridgesJson == null) {
+ this.modelsBridges = null;
+ return;
+ }
+ try {
+ this.modelsBridges = JSON.createGson().create().fromJson(bridgesJson, ModelsBridge[].class);
+ } catch (JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public ModelsBridge[] getBridges() {
+ return this.modelsBridges;
+ }
+
+ public String getBridgesJson() {
+ return getJsonString(modelsBridges);
+ }
+
+ public void setGateways(String gatewaysJson) {
+ if (gatewaysJson == null) {
+ this.modelsGateways = null;
+ return;
+ }
+ try {
+ this.modelsGateways = JSON.createGson().create().fromJson(gatewaysJson, ModelsGateway[].class);
+ } catch (JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public ModelsGateway[] getGateways() {
+ return modelsGateways;
+ }
+
+ public String getGatewaysJson() {
+ return getJsonString(modelsGateways);
+ }
+
+ public void setService(String serviceJson) {
+ if (serviceJson == null) {
+ this.modelsEIPService = null;
+ return;
+ }
+ try {
+ this.modelsEIPService = JSON.createGson().create().fromJson(serviceJson, ModelsEIPService.class);
+ } catch (JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+ public ModelsEIPService getService() {
+ return this.modelsEIPService;
+ }
+
+ public String getServiceJson() {
+ return getJsonString(modelsEIPService);
+ }
+
+ public void setModelsProvider(String json) {
+ if (json == null) {
+ this.modelsProvider = null;
+ return;
+ }
+ try {
+ this.modelsProvider = JSON.createGson().create().fromJson(json, ModelsProvider.class);
+ } catch (JsonSyntaxException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public String getModelsProviderJson() {
+ return getJsonString(modelsProvider);
+ }
+
+ private String getJsonString(Object model) {
+ if (model == null) {
+ return null;
+ }
+ try {
+ return JSON.createGson().create().toJson(model);
+ } catch (JsonSyntaxException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
public boolean isConfigured() {
- return !mainUrl.isDefault() &&
- !apiUrl.isDefault() &&
- hasCaCert() &&
- hasDefinition() &&
- hasVpnCertificate() &&
- hasEIP() &&
- hasPrivateKey();
+ if (apiVersion < 5) {
+ return !mainUrl.isEmpty() &&
+ !apiUrl.isEmpty() &&
+ hasCaCert() &&
+ hasDefinition() &&
+ hasVpnCertificate() &&
+ hasEIP() &&
+ hasPrivateKey();
+ } else {
+ return !mainUrl.isEmpty() &&
+ modelsProvider != null &&
+ modelsEIPService != null &&
+ modelsGateways != null &&
+ hasVpnCertificate() &&
+ hasPrivateKey();
+ }
}
public boolean supportsPluggableTransports() {
- if (useObfsVpn()) {
- return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP), new Pair<>(OBFS4, KCP), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP)});
- }
- return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP)});
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP), new Pair<>(OBFS4, KCP), new Pair<>(OBFS4, QUIC), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP), new Pair<>(OBFS4_HOP, QUIC)});
}
public boolean supportsExperimentalPluggableTransports() {
- return supportsTransports(new Pair[]{new Pair<>(OBFS4, KCP), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP)});
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4, KCP), new Pair<>(OBFS4_HOP, TCP), new Pair<>(OBFS4_HOP, KCP), new Pair<>(OBFS4, QUIC), new Pair<>(OBFS4_HOP, QUIC)});
+ }
+
+
+ public boolean supportsObfs4() {
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4, TCP)});
+ }
+
+ public boolean supportsObfs4Kcp() {
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4, KCP)});
+ }
+
+ public boolean supportsObfs4Quic() {
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4, QUIC)});
+ }
+
+ public boolean supportsObfs4Hop() {
+ return supportsTransports(new Pair[]{new Pair<>(OBFS4_HOP, KCP), new Pair<>(OBFS4_HOP, QUIC), new Pair<>(OBFS4_HOP, TCP)});
}
private boolean supportsTransports(Pair<TransportType, TransportProtocol>[] transportTypes) {
- try {
- JSONArray gatewayJsons = eipServiceJson.getJSONArray(GATEWAYS);
- for (int i = 0; i < gatewayJsons.length(); i++) {
- JSONArray transports = gatewayJsons.getJSONObject(i).
- getJSONObject(CAPABILITIES).
- getJSONArray(TRANSPORT);
- for (int j = 0; j < transports.length(); j++) {
- String supportedTransportType = transports.getJSONObject(j).getString(TYPE);
- JSONArray transportProtocols = transports.getJSONObject(j).getJSONArray(PROTOCOLS);
- for (Pair<TransportType, TransportProtocol> transportPair : transportTypes) {
- for (int k = 0; k < transportProtocols.length(); k++) {
- if (transportPair.first.toString().equals(supportedTransportType) &&
- transportPair.second.toString().equals(transportProtocols.getString(k))) {
- return true;
+ if (apiVersion < 5) {
+ try {
+ JSONArray gatewayJsons = eipServiceJson.getJSONArray(GATEWAYS);
+ for (int i = 0; i < gatewayJsons.length(); i++) {
+ JSONArray transports = gatewayJsons.getJSONObject(i).
+ getJSONObject(CAPABILITIES).
+ getJSONArray(TRANSPORT);
+ for (int j = 0; j < transports.length(); j++) {
+ String supportedTransportType = transports.getJSONObject(j).getString(TYPE);
+ JSONArray transportProtocols = transports.getJSONObject(j).getJSONArray(PROTOCOLS);
+ for (Pair<TransportType, TransportProtocol> transportPair : transportTypes) {
+ for (int k = 0; k < transportProtocols.length(); k++) {
+ if (transportPair.first.toString().equals(supportedTransportType) &&
+ transportPair.second.toString().equals(transportProtocols.getString(k))) {
+ return true;
+ }
}
}
}
}
+ } catch (Exception e) {
+ // ignore
+ }
+ } else {
+ if (modelsBridges == null) return false;
+ for (ModelsBridge bridge : modelsBridges) {
+ for (Pair<TransportType, TransportProtocol> transportPair : transportTypes) {
+ if (transportPair.first.toString().equals(bridge.getType()) &&
+ transportPair.second.toString().equals(bridge.getTransport())) {
+ return true;
+ }
+ }
}
- } catch (Exception e) {
- // ignore
}
+
return false;
}
public String getIpForHostname(String host) {
if (host != null) {
- if (host.equals(mainUrl.getUrl().getHost())) {
+ if (host.equals(getHostFromUrl(mainUrl))) {
return providerIp;
- } else if (host.equals(apiUrl.getUrl().getHost())) {
+ } else if (host.equals(getHostFromUrl(apiUrl))) {
return providerApiIp;
}
}
return "";
}
+ private String getHostFromUrl(String url) {
+ try {
+ return new URL(url).getHost();
+ } catch (MalformedURLException e) {
+ return "";
+ }
+ }
+
public String getProviderApiIp() {
return this.providerApiIp;
}
@@ -256,14 +404,21 @@ public final class Provider implements Parcelable {
}
public void setMainUrl(URL url) {
- mainUrl.setUrl(url);
+ mainUrl = url.toString();
}
public void setMainUrl(String url) {
try {
- mainUrl.setUrl(new URL(url));
+ if (isNetworkUrl(url)) {
+ this.mainUrl = new URL(url).toString();
+ } else if (isDomainName(url)){
+ this.mainUrl = new URL("https://" + url).toString();
+ } else {
+ this.mainUrl = "";
+ }
} catch (MalformedURLException e) {
e.printStackTrace();
+ this.mainUrl = "";
}
}
@@ -281,55 +436,54 @@ public final class Provider implements Parcelable {
}
public String getDomain() {
- return domain;
- }
-
- public String getMainUrlString() {
- return getMainUrl().toString();
+ if ((apiVersion < 5 && (domain == null || domain.isEmpty())) ||
+ (modelsProvider == null)) {
+ return getHostFromUrl(mainUrl);
+ }
+ if (apiVersion < 5) {
+ return domain;
+ }
+ return modelsProvider.getDomain();
}
- public DefaultedURL getMainUrl() {
+ public String getMainUrl() {
return mainUrl;
}
- protected DefaultedURL getApiUrl() {
- return apiUrl;
- }
-
- public DefaultedURL getGeoipUrl() {
+ public String getGeoipUrl() {
return geoipUrl;
}
public void setGeoipUrl(String url) {
try {
- this.geoipUrl.setUrl(new URL(url));
+ this.geoipUrl = new URL(url).toString();
} catch (MalformedURLException e) {
- this.geoipUrl = new DefaultedURL();
+ this.geoipUrl = "";
}
}
- public DefaultedURL getMotdUrl() {
+ public String getMotdUrl() {
return this.motdUrl;
}
public void setMotdUrl(String url) {
try {
- this.motdUrl.setUrl(new URL(url));
+ this.motdUrl = new URL(url).toString();
} catch (MalformedURLException e) {
- this.motdUrl = new DefaultedURL();
+ this.motdUrl = "";
}
}
public String getApiUrlWithVersion() {
- return getApiUrlString() + "/" + getApiVersion();
+ return getApiUrl() + "/" + getApiVersion();
}
- public String getApiUrlString() {
- return getApiUrl().toString();
+ public String getApiUrl() {
+ return apiUrl;
}
- public String getApiVersion() {
+ public int getApiVersion() {
return apiVersion;
}
@@ -338,7 +492,7 @@ public final class Provider implements Parcelable {
}
public boolean hasDefinition() {
- return definition != null && definition.length() > 0;
+ return (definition != null && definition.length() > 0) || (modelsProvider != null);
}
public boolean hasGeoIpJson() {
@@ -363,7 +517,7 @@ public final class Provider implements Parcelable {
name = definition.getJSONObject(API_TERM_NAME).getString("en");
} catch (JSONException e2) {
if (mainUrl != null) {
- String host = mainUrl.getDomain();
+ String host = getHostFromUrl(mainUrl);
name = host.substring(0, host.indexOf("."));
}
}
@@ -394,12 +548,25 @@ public final class Provider implements Parcelable {
&& !getEipServiceJson().has(ERRORS);
}
+ public boolean hasServiceInfo() {
+ return modelsEIPService != null;
+ }
+
public boolean hasGatewaysInDifferentLocations() {
- try {
- return getEipServiceJson().getJSONObject(LOCATIONS).length() > 1;
- } catch (NullPointerException | JSONException e) {
- return false;
+ if (apiVersion >= 5) {
+ try {
+ return getService().getLocations().size() > 1;
+ } catch (NullPointerException e) {
+ return false;
+ }
+ } else {
+ try {
+ return getEipServiceJson().getJSONObject(LOCATIONS).length() > 1;
+ } catch (NullPointerException | JSONException e) {
+ return false;
+ }
}
+
}
@Override
@@ -410,17 +577,17 @@ public final class Provider implements Parcelable {
@Override
public void writeToParcel(Parcel parcel, int i) {
parcel.writeString(getDomain());
- parcel.writeString(getMainUrlString());
+ parcel.writeString(getMainUrl());
parcel.writeString(getProviderIp());
parcel.writeString(getProviderApiIp());
- parcel.writeString(getGeoipUrl().toString());
- parcel.writeString(getMotdUrl().toString());
+ parcel.writeString(getGeoipUrl());
+ parcel.writeString(getMotdUrl());
parcel.writeString(getDefinitionString());
parcel.writeString(getCaCert());
parcel.writeString(getEipServiceJsonString());
parcel.writeString(getGeoIpJsonString());
parcel.writeString(getMotdJsonString());
- parcel.writeString(getPrivateKey());
+ parcel.writeString(getPrivateKeyString());
parcel.writeString(getVpnCertificate());
parcel.writeLong(lastEipServiceUpdate);
parcel.writeLong(lastGeoIpUpdate);
@@ -428,6 +595,14 @@ public final class Provider implements Parcelable {
parcel.writeLong(lastMotdSeen);
parcel.writeStringList(new ArrayList<>(lastMotdSeenHashes));
parcel.writeInt(shouldUpdateVpnCertificate ? 0 : 1);
+ parcel.writeParcelable(introducer, 0);
+ if (apiVersion == 5) {
+ Gson gson = JSON.createGson().create();
+ parcel.writeString(modelsProvider != null ? gson.toJson(modelsProvider) : "");
+ parcel.writeString(modelsEIPService != null ? gson.toJson(modelsEIPService) : "");
+ parcel.writeString(modelsBridges != null ? gson.toJson(modelsBridges) : "");
+ parcel.writeString(modelsGateways != null ? gson.toJson(modelsGateways) : "");
+ }
}
@@ -435,7 +610,7 @@ public final class Provider implements Parcelable {
private Provider(Parcel in) {
try {
domain = in.readString();
- mainUrl.setUrl(new URL(in.readString()));
+ setMainUrl(in.readString());
String tmpString = in.readString();
if (!tmpString.isEmpty()) {
providerIp = tmpString;
@@ -446,11 +621,11 @@ public final class Provider implements Parcelable {
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
- geoipUrl.setUrl(new URL(tmpString));
+ geoipUrl = new URL(tmpString).toString();
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
- motdUrl.setUrl(new URL(tmpString));
+ motdUrl = new URL(tmpString).toString();
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
@@ -475,7 +650,7 @@ public final class Provider implements Parcelable {
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
- this.setPrivateKey(tmpString);
+ this.setPrivateKeyString(tmpString);
}
tmpString = in.readString();
if (!tmpString.isEmpty()) {
@@ -489,6 +664,13 @@ public final class Provider implements Parcelable {
in.readStringList(lastMotdSeenHashes);
this.lastMotdSeenHashes = new HashSet<>(lastMotdSeenHashes);
this.shouldUpdateVpnCertificate = in.readInt() == 0;
+ this.introducer = in.readParcelable(Introducer.class.getClassLoader());
+ if (this.apiVersion == 5) {
+ this.setModelsProvider(in.readString());
+ this.setService(in.readString());
+ this.setBridges(in.readString());
+ this.setGateways(in.readString());
+ }
} catch (MalformedURLException | JSONException e) {
e.printStackTrace();
}
@@ -500,24 +682,28 @@ public final class Provider implements Parcelable {
if (o instanceof Provider) {
Provider p = (Provider) o;
return getDomain().equals(p.getDomain()) &&
- mainUrl.getDomain().equals(p.mainUrl.getDomain()) &&
- definition.toString().equals(p.getDefinition().toString()) &&
- eipServiceJson.toString().equals(p.getEipServiceJsonString()) &&
- geoIpJson.toString().equals(p.getGeoIpJsonString()) &&
- motdJson.toString().equals(p.getMotdJsonString()) &&
- providerIp.equals(p.getProviderIp()) &&
- providerApiIp.equals(p.getProviderApiIp()) &&
- apiUrl.equals(p.getApiUrl()) &&
- geoipUrl.equals(p.getGeoipUrl()) &&
- motdUrl.equals(p.getMotdUrl()) &&
- certificatePin.equals(p.getCertificatePin()) &&
- certificatePinEncoding.equals(p.getCertificatePinEncoding()) &&
- caCert.equals(p.getCaCert()) &&
- apiVersion.equals(p.getApiVersion()) &&
- privateKey.equals(p.getPrivateKey()) &&
- vpnCertificate.equals(p.getVpnCertificate()) &&
- allowAnonymous == p.allowsAnonymous() &&
- allowRegistered == p.allowsRegistered();
+ getHostFromUrl(mainUrl).equals(getHostFromUrl(p.getMainUrl())) &&
+ definition.toString().equals(p.getDefinition().toString()) &&
+ eipServiceJson.toString().equals(p.getEipServiceJsonString()) &&
+ geoIpJson.toString().equals(p.getGeoIpJsonString()) &&
+ motdJson.toString().equals(p.getMotdJsonString()) &&
+ providerIp.equals(p.getProviderIp()) &&
+ providerApiIp.equals(p.getProviderApiIp()) &&
+ apiUrl.equals(p.getApiUrl()) &&
+ geoipUrl.equals(p.getGeoipUrl()) &&
+ motdUrl.equals(p.getMotdUrl()) &&
+ certificatePin.equals(p.getCertificatePin()) &&
+ certificatePinEncoding.equals(p.getCertificatePinEncoding()) &&
+ caCert.equals(p.getCaCert()) &&
+ apiVersion == p.getApiVersion() &&
+ privateKeyString.equals(p.getPrivateKeyString()) &&
+ vpnCertificate.equals(p.getVpnCertificate()) &&
+ allowAnonymous == p.allowsAnonymous() &&
+ allowRegistered == p.allowsRegistered() &&
+ Objects.equals(modelsProvider, p.modelsProvider) &&
+ Objects.equals(modelsEIPService, p.modelsEIPService) &&
+ Arrays.equals(modelsBridges, p.modelsBridges) &&
+ Arrays.equals(modelsGateways, p.modelsGateways);
} else return false;
}
@@ -535,7 +721,7 @@ public final class Provider implements Parcelable {
@Override
public int hashCode() {
- return getMainUrlString().hashCode();
+ return getMainUrl().hashCode();
}
@Override
@@ -545,20 +731,65 @@ public final class Provider implements Parcelable {
private boolean parseDefinition(JSONObject definition) {
try {
+ this.apiVersions = parseApiVersionsArray();
+ this.apiVersion = selectPreferredApiVersion();
+ this.domain = getDefinition().getString(Provider.DOMAIN);
String pin = definition.getString(CA_CERT_FINGERPRINT);
this.certificatePin = pin.split(":")[1].trim();
this.certificatePinEncoding = pin.split(":")[0].trim();
- this.apiUrl.setUrl(new URL(definition.getString(API_URL)));
- this.allowAnonymous = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOW_ANONYMOUS);
- this.allowRegistered = definition.getJSONObject(Provider.SERVICE).getBoolean(PROVIDER_ALLOWED_REGISTERED);
- this.apiVersion = getDefinition().getString(Provider.API_VERSION);
- this.domain = getDefinition().getString(Provider.DOMAIN);
+ this.apiUrl = new URL(definition.getString(API_URL)).toString();
+ JSONObject serviceJSONObject = definition.getJSONObject(Provider.SERVICE);
+ if (serviceJSONObject.has(PROVIDER_ALLOW_ANONYMOUS)) {
+ this.allowAnonymous = serviceJSONObject.getBoolean(PROVIDER_ALLOW_ANONYMOUS);
+ }
+ if (serviceJSONObject.has(PROVIDER_ALLOWED_REGISTERED)) {
+ this.allowRegistered = serviceJSONObject.getBoolean(PROVIDER_ALLOWED_REGISTERED);
+ }
return true;
- } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException e) {
+ } catch (JSONException | ArrayIndexOutOfBoundsException | MalformedURLException | NullPointerException | NumberFormatException e) {
return false;
}
}
+ /**
+ @returns latest api version supported by client and server or the version set in 'api_version'
+ in case there's not a common supported version
+ */
+ private int selectPreferredApiVersion() throws JSONException, NumberFormatException {
+ if (apiVersions.length == 0) {
+ return Integer.parseInt(getDefinition().getString(Provider.API_VERSION));
+ }
+
+ // apiVersion is a sorted Array
+ for (int i = apiVersions.length -1; i >= 0; i--) {
+ if (apiVersions[i] == BuildConfig.preferred_client_api_version ||
+ apiVersions[i] < BuildConfig.preferred_client_api_version) {
+ return apiVersions[i];
+ }
+ }
+
+ return Integer.parseInt(getDefinition().getString(Provider.API_VERSION));
+ }
+
+ private int[] parseApiVersionsArray() {
+ int[] versionArray = new int[0];
+ try {
+ JSONArray versions = getDefinition().getJSONArray(Provider.API_VERSIONS);
+ versionArray = new int[versions.length()];
+ for (int i = 0; i < versions.length(); i++) {
+ try {
+ versionArray[i] = Integer.parseInt(versions.getString(i));
+ } catch (NumberFormatException e) {
+ e.printStackTrace();
+ }
+ }
+ } catch (JSONException ignore) {
+ // this backend doesn't support api_versions yet
+ }
+ Arrays.sort(versionArray);
+ return versionArray;
+ }
+
public void setCaCert(String cert) {
this.caCert = cert;
}
@@ -600,7 +831,7 @@ public final class Provider implements Parcelable {
* @return true if last message of the day was shown more than 24h ago
*/
public boolean shouldShowMotdSeen() {
- return !motdUrl.isDefault() && System.currentTimeMillis() - lastMotdSeen >= MOTD_TIMEOUT;
+ return !motdUrl.isEmpty() && System.currentTimeMillis() - lastMotdSeen >= MOTD_TIMEOUT;
}
/**
@@ -636,7 +867,7 @@ public final class Provider implements Parcelable {
}
public boolean shouldUpdateMotdJson() {
- return !motdUrl.isDefault() && System.currentTimeMillis() - lastMotdUpdate >= MOTD_TIMEOUT;
+ return !motdUrl.isEmpty() && System.currentTimeMillis() - lastMotdUpdate >= MOTD_TIMEOUT;
}
public void setMotdJson(@NonNull JSONObject motdJson) {
@@ -693,31 +924,31 @@ public final class Provider implements Parcelable {
}
public boolean isDefault() {
- return getMainUrl().isDefault() &&
- getApiUrl().isDefault() &&
- getGeoipUrl().isDefault() &&
+ return getMainUrl().isEmpty() &&
+ getApiUrl().isEmpty() &&
+ getGeoipUrl().isEmpty() &&
certificatePin.isEmpty() &&
certificatePinEncoding.isEmpty() &&
caCert.isEmpty();
}
- public String getPrivateKey() {
- return privateKey;
+ public String getPrivateKeyString() {
+ return privateKeyString;
}
- public RSAPrivateKey getRSAPrivateKey() {
- if (rsaPrivateKey == null) {
- rsaPrivateKey = parseRsaKeyFromString(privateKey);
+ public PrivateKey getPrivateKey() {
+ if (privateKey == null) {
+ privateKey = parsePrivateKeyFromString(privateKeyString);
}
- return rsaPrivateKey;
+ return privateKey;
}
- public void setPrivateKey(String privateKey) {
- this.privateKey = privateKey;
+ public void setPrivateKeyString(String privateKeyString) {
+ this.privateKeyString = privateKeyString;
}
public boolean hasPrivateKey() {
- return privateKey != null && privateKey.length() > 0;
+ return privateKeyString != null && privateKeyString.length() > 0;
}
public String getVpnCertificate() {
@@ -744,6 +975,18 @@ public final class Provider implements Parcelable {
return getCertificatePinEncoding() + ":" + getCertificatePin();
}
+ public boolean hasIntroducer() {
+ return introducer != null;
+ }
+
+ public Introducer getIntroducer() {
+ return introducer;
+ }
+
+ public void setIntroducer(String introducerUrl) throws URISyntaxException, IllegalArgumentException {
+ this.introducer = Introducer.fromUrl(introducerUrl);
+ }
+
/**
* resets everything except the main url, the providerIp and the geoip
* service url (currently preseeded)
@@ -753,16 +996,21 @@ public final class Provider implements Parcelable {
eipServiceJson = new JSONObject();
geoIpJson = new JSONObject();
motdJson = new JSONObject();
- apiUrl = new DefaultedURL();
+ apiUrl = "";
certificatePin = "";
certificatePinEncoding = "";
caCert = "";
- apiVersion = "";
- privateKey = "";
+ apiVersion = BuildConfig.preferred_client_api_version;
+ privateKeyString = "";
vpnCertificate = "";
allowRegistered = false;
allowAnonymous = false;
lastGeoIpUpdate = 0L;
lastEipServiceUpdate = 0L;
+ modelsProvider = null;
+ modelsGateways = null;
+ modelsBridges = null;
+ modelsEIPService = null;
}
+
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java b/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java
index 33fbbf7a..d0149a3f 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/models/Transport.java
@@ -1,5 +1,21 @@
package se.leap.bitmaskclient.base.models;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OBFS4_HOP;
+import static de.blinkt.openvpn.core.connection.Connection.TransportType.OPENVPN;
+import static se.leap.bitmaskclient.base.models.Constants.CAPABILITIES;
+import static se.leap.bitmaskclient.base.models.Constants.CERT;
+import static se.leap.bitmaskclient.base.models.Constants.HOP_JITTER;
+import static se.leap.bitmaskclient.base.models.Constants.IAT_MODE;
+import static se.leap.bitmaskclient.base.models.Constants.MAX_HOP_PORT;
+import static se.leap.bitmaskclient.base.models.Constants.MIN_HOP_PORT;
+import static se.leap.bitmaskclient.base.models.Constants.MIN_HOP_SECONDS;
+import static se.leap.bitmaskclient.base.models.Constants.PORTS;
+import static se.leap.bitmaskclient.base.models.Constants.PORT_COUNT;
+import static se.leap.bitmaskclient.base.models.Constants.PORT_SEED;
+import static se.leap.bitmaskclient.base.models.Constants.PROTOCOLS;
+import static se.leap.bitmaskclient.base.models.Constants.TRANSPORT;
+
+import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.gson.FieldNamingPolicy;
@@ -7,31 +23,44 @@ import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.annotations.SerializedName;
+import org.json.JSONArray;
import org.json.JSONObject;
import java.io.Serializable;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Vector;
import de.blinkt.openvpn.core.connection.Connection;
+import io.swagger.client.model.ModelsBridge;
+import io.swagger.client.model.ModelsGateway;
public class Transport implements Serializable {
- private String type;
- private String[] protocols;
+ private final String type;
+ private final String[] protocols;
@Nullable
- private String[] ports;
+ private final String[] ports;
@Nullable
- private Options options;
+ private final Options options;
public Transport(String type, String[] protocols, String[] ports, String cert) {
this(type, protocols, ports, new Options(cert, "0"));
}
- public Transport(String type, String[] protocols, String[] ports, Options options) {
+ public Transport(String type, String[] protocols, @Nullable String[] ports, @Nullable Options options) {
this.type = type;
this.protocols = protocols;
this.ports = ports;
this.options = options;
}
+ public Transport(String type, String[] protocols, @Nullable String[] ports) {
+ this.type = type;
+ this.protocols = protocols;
+ this.ports = ports;
+ this.options = null;
+ }
+
public String getType() {
return type;
}
@@ -67,12 +96,107 @@ public class Transport implements Serializable {
fromJson(json.toString(), Transport.class);
}
+ public static Transport createTransportFrom(ModelsBridge modelsBridge) {
+ if (modelsBridge == null) {
+ return null;
+ }
+ Map<String, Object> options = modelsBridge.getOptions();
+ Transport.Options transportOptions = new Transport.Options((String) options.get(CERT), (String) options.get(IAT_MODE));
+ if (OBFS4_HOP.toString().equals(modelsBridge.getType())) {
+ transportOptions.minHopSeconds = getIntOption(options, MIN_HOP_SECONDS, 5);
+ transportOptions.minHopPort = getIntOption(options, MIN_HOP_PORT, 49152);
+ transportOptions.maxHopPort = getIntOption(options, MAX_HOP_PORT, 65535);
+ transportOptions.hopJitter = getIntOption(options, HOP_JITTER, 10);
+ transportOptions.portCount = getIntOption(options, PORT_COUNT, 100);
+ transportOptions.portSeed = getIntOption(options, PORT_SEED, 1);
+ }
+ Transport transport = new Transport(
+ modelsBridge.getType(),
+ new String[]{modelsBridge.getTransport()},
+ new String[]{String.valueOf(modelsBridge.getPort())},
+ transportOptions
+ );
+ return transport;
+ }
+
+ private static int getIntOption(Map<String, Object> options, String key, int defaultValue) {
+ try {
+ Object o = options.get(key);
+ if (o == null) {
+ return defaultValue;
+ }
+ if (o instanceof String) {
+ return Integer.parseInt((String) o);
+ }
+ return (int) o;
+ } catch (NullPointerException | ClassCastException | NumberFormatException e){
+ e.printStackTrace();
+ return defaultValue;
+ }
+ }
+
+ public static Transport createTransportFrom(ModelsGateway modelsGateway) {
+ if (modelsGateway == null) {
+ return null;
+ }
+ Transport transport = new Transport(
+ modelsGateway.getType(),
+ new String[]{modelsGateway.getTransport()},
+ new String[]{String.valueOf(modelsGateway.getPort())}
+ );
+ return transport;
+ }
+
+
+ @NonNull
+ public static Vector<Transport> createTransportsFrom(JSONObject gateway, int apiVersion) throws IllegalArgumentException {
+ Vector<Transport> transports = new Vector<>();
+ try {
+ if (apiVersion >= 3) {
+ JSONArray supportedTransports = gateway.getJSONObject(CAPABILITIES).getJSONArray(TRANSPORT);
+ for (int i = 0; i < supportedTransports.length(); i++) {
+ Transport transport = Transport.fromJson(supportedTransports.getJSONObject(i));
+ transports.add(transport);
+ }
+ } else {
+ JSONObject capabilities = gateway.getJSONObject(CAPABILITIES);
+ JSONArray ports = capabilities.getJSONArray(PORTS);
+ JSONArray protocols = capabilities.getJSONArray(PROTOCOLS);
+ String[] portArray = new String[ports.length()];
+ String[] protocolArray = new String[protocols.length()];
+ for (int i = 0; i < ports.length(); i++) {
+ portArray[i] = String.valueOf(ports.get(i));
+ }
+ for (int i = 0; i < protocols.length(); i++) {
+ protocolArray[i] = protocols.optString(i);
+ }
+ Transport transport = new Transport(OPENVPN.toString(), protocolArray, portArray);
+ transports.add(transport);
+ }
+ } catch (Exception e) {
+ throw new IllegalArgumentException();
+ //throw new ConfigParser.ConfigParseError("Api version ("+ apiVersion +") did not match required JSON fields");
+ }
+ return transports;
+ }
+
+ public static Vector<Transport> createTransportsFrom(ModelsBridge modelsBridge) {
+ Vector<Transport> transports = new Vector<>();
+ transports.add(Transport.createTransportFrom(modelsBridge));
+ return transports;
+ }
+
+ public static Vector<Transport> createTransportsFrom(ModelsGateway modelsGateway) {
+ Vector<Transport> transports = new Vector<>();
+ transports.add(Transport.createTransportFrom(modelsGateway));
+ return transports;
+ }
+
public static class Options implements Serializable {
@Nullable
- private String cert;
+ private final String cert;
@SerializedName("iatMode")
- private String iatMode;
-
+ private final String iatMode;
@Nullable
private Endpoint[] endpoints;
@@ -81,23 +205,30 @@ public class Transport implements Serializable {
private int portSeed;
private int portCount;
-
+ private int minHopPort;
+ private int maxHopPort;
+ private int minHopSeconds;
+ private int hopJitter;
public Options(String cert, String iatMode) {
this.cert = cert;
this.iatMode = iatMode;
}
- public Options(String iatMode, Endpoint[] endpoints, int portSeed, int portCount, boolean experimental) {
- this(iatMode, endpoints, null, portSeed, portCount, experimental);
+ public Options(String iatMode, Endpoint[] endpoints, int portSeed, int portCount, int minHopPort, int maxHopPort, int minHopSeconds, int hopJitter, boolean experimental) {
+ this(iatMode, endpoints, null, portSeed, portCount, minHopPort, maxHopPort, minHopSeconds, hopJitter, experimental);
}
- public Options(String iatMode, Endpoint[] endpoints, String cert, int portSeed, int portCount, boolean experimental) {
+ public Options(String iatMode, Endpoint[] endpoints, String cert, int portSeed, int portCount, int minHopPort, int maxHopPort, int minHopSeconds, int hopJitter, boolean experimental) {
this.iatMode = iatMode;
this.endpoints = endpoints;
this.portSeed = portSeed;
this.portCount = portCount;
this.experimental = experimental;
+ this.minHopPort = minHopPort;
+ this.maxHopPort = maxHopPort;
+ this.minHopSeconds = minHopSeconds;
+ this.hopJitter = hopJitter;
this.cert = cert;
}
@@ -128,6 +259,22 @@ public class Transport implements Serializable {
return portCount;
}
+ public int getMinHopPort() {
+ return minHopPort;
+ }
+
+ public int getMaxHopPort() {
+ return maxHopPort;
+ }
+
+ public int getMinHopSeconds() {
+ return minHopSeconds;
+ }
+
+ public int getHopJitter() {
+ return hopJitter;
+ }
+
@Override
public String toString() {
return new Gson().toJson(this);
@@ -136,8 +283,8 @@ public class Transport implements Serializable {
public static class Endpoint implements Serializable {
- private String ip;
- private String cert;
+ private final String ip;
+ private final String cert;
public Endpoint(String ip, String cert) {
this.ip = ip;
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/BitmaskCoreProvider.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/BitmaskCoreProvider.java
new file mode 100644
index 00000000..77cf9cf0
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/BitmaskCoreProvider.java
@@ -0,0 +1,28 @@
+package se.leap.bitmaskclient.base.utils;
+
+import de.blinkt.openvpn.core.NativeUtils;
+import mobile.BitmaskMobile;
+import mobilemodels.BitmaskMobileCore;
+
+public class BitmaskCoreProvider {
+ private static BitmaskMobileCore customMobileCore;
+
+ /**
+ * Returns an empty BitmaskMobile instance, which can be currently only used to access
+ * bitmask-core's persistence layer API
+ * @return BitmaskMobileCore interface
+ */
+ public static BitmaskMobileCore getBitmaskMobile() {
+ if (customMobileCore == null) {
+ return new BitmaskMobile(new PreferenceHelper.SharedPreferenceStore());
+ }
+ return customMobileCore;
+ }
+
+ public static void initBitmaskMobile(BitmaskMobileCore bitmaskMobileCore) {
+ if (!NativeUtils.isUnitTest()) {
+ throw new IllegalStateException("Initializing custom BitmaskMobileCore implementation outside of an unit test is not allowed");
+ }
+ BitmaskCoreProvider.customMobileCore = bitmaskMobileCore;
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java
index e1f65b5e..22939611 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/BuildConfigHelper.java
@@ -11,29 +11,26 @@ import se.leap.bitmaskclient.BuildConfig;
public class BuildConfigHelper {
public interface BuildConfigHelperInterface {
- boolean useObfsVpn();
boolean hasObfuscationPinningDefaults();
String obfsvpnIP();
String obfsvpnPort();
String obfsvpnCert();
- boolean useKcp();
+ String obfsvpnTransportProtocol();
boolean isDefaultBitmask();
}
public static class DefaultBuildConfigHelper implements BuildConfigHelperInterface {
- @Override
- public boolean useObfsVpn() {
- return BuildConfig.use_obfsvpn;
- }
@Override
public boolean hasObfuscationPinningDefaults() {
return BuildConfig.obfsvpn_ip != null &&
BuildConfig.obfsvpn_port != null &&
BuildConfig.obfsvpn_cert != null &&
+ BuildConfig.obfsvpn_transport_protocol != null &&
!BuildConfig.obfsvpn_ip.isEmpty() &&
!BuildConfig.obfsvpn_port.isEmpty() &&
- !BuildConfig.obfsvpn_cert.isEmpty();
+ !BuildConfig.obfsvpn_cert.isEmpty() &&
+ !BuildConfig.obfsvpn_transport_protocol.isEmpty();
}
@Override
@@ -52,8 +49,8 @@ public class BuildConfigHelper {
}
@Override
- public boolean useKcp() {
- return BuildConfig.obfsvpn_use_kcp;
+ public String obfsvpnTransportProtocol() {
+ return BuildConfig.obfsvpn_transport_protocol;
}
@Override
@@ -72,10 +69,6 @@ public class BuildConfigHelper {
instance = helperInterface;
}
- public static boolean useObfsVpn() {
- return instance.useObfsVpn();
- }
-
public static boolean hasObfuscationPinningDefaults() {
return instance.hasObfuscationPinningDefaults();
}
@@ -88,8 +81,8 @@ public class BuildConfigHelper {
public static String obfsvpnCert() {
return instance.obfsvpnCert();
}
- public static boolean useKcp() {
- return instance.useKcp();
+ public static String obfsvpnTransportProtocol() {
+ return instance.obfsvpnTransportProtocol();
}
public static boolean isDefaultBitmask() {
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java
index cd5d1fca..74328f45 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/ConfigHelper.java
@@ -21,6 +21,8 @@ import android.content.Context;
import android.content.res.Resources;
import android.os.Build;
import android.os.Looper;
+import android.util.Patterns;
+import android.webkit.URLUtil;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -34,6 +36,7 @@ import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
+import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
@@ -119,6 +122,36 @@ public class ConfigHelper {
return null;
}
+ public static String parseX509CertificatesToString(ArrayList<X509Certificate> certs) {
+ StringBuilder sb = new StringBuilder();
+ for (X509Certificate certificate : certs) {
+
+ byte[] derCert = new byte[0];
+ try {
+ derCert = certificate.getEncoded();
+ byte[] encodedCert = Base64.encode(derCert);
+ String base64Cert = new String(encodedCert);
+
+ // add cert header
+ sb.append("-----BEGIN CERTIFICATE-----\n");
+
+ // split base64 string into lines of 64 characters
+ int index = 0;
+ while (index < base64Cert.length()) {
+ sb.append(base64Cert.substring(index, Math.min(index + 64, base64Cert.length())))
+ .append("\n");
+ index += 64;
+ }
+
+ // add cert footer
+ sb.append("-----END CERTIFICATE-----\n");
+ } catch (CertificateEncodingException e) {
+ e.printStackTrace();
+ }
+ }
+ return sb.toString().trim();
+ }
+
public static void ensureNotOnMainThread(@NonNull Context context) throws IllegalStateException{
Looper looper = Looper.myLooper();
if (looper != null && looper == context.getMainLooper()) {
@@ -190,8 +223,28 @@ public class ConfigHelper {
return matcher.matches();
}
- public static String getDomainFromMainURL(@NonNull String mainUrl) throws NullPointerException {
- return PublicSuffixDatabase.Companion.get().getEffectiveTldPlusOne(mainUrl).replaceFirst("http[s]?://", "").replaceFirst("/.*", "");
+ public static boolean isNetworkUrl(String url) {
+ return url != null && URLUtil.isNetworkUrl(url)
+ && URLUtil.isHttpsUrl(url)
+ && Patterns.WEB_URL.matcher(url).matches();
+ }
+
+ public static boolean isDomainName(String url) {
+ return url != null && Patterns.DOMAIN_NAME.matcher(url).matches();
+ }
+
+ /**
+ * Extracts a domain from a given URL
+ * @param mainUrl URL as String
+ * @return Domain as String, null if mainUrl is an invalid URL
+ */
+ public static String getDomainFromMainURL(String mainUrl) {
+ try {
+ String topLevelDomain = PublicSuffixDatabase.Companion.get().getEffectiveTldPlusOne(mainUrl);
+ return topLevelDomain.replaceFirst("http[s]?://", "").replaceFirst("/.*", "");
+ } catch (NullPointerException | IllegalArgumentException e) {
+ return null;
+ }
}
public static boolean isCalyxOSWithTetheringSupport(Context context) {
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/CredentialsParser.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/CredentialsParser.java
new file mode 100644
index 00000000..a62d548a
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/CredentialsParser.java
@@ -0,0 +1,58 @@
+package se.leap.bitmaskclient.base.utils;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlPullParserFactory;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import se.leap.bitmaskclient.base.models.Provider;
+
+public class CredentialsParser {
+
+ public static void parseXml(String xmlString, Provider provider) throws XmlPullParserException, IOException {
+ XmlPullParserFactory factory = XmlPullParserFactory.newInstance();
+ XmlPullParser parser = factory.newPullParser();
+ parser.setInput(new StringReader(xmlString));
+
+ String currentTag = null;
+ String ca = null;
+ String key = null;
+ String cert = null;
+
+ int eventType = parser.getEventType();
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ switch (eventType) {
+ case XmlPullParser.START_TAG -> currentTag = parser.getName();
+ case XmlPullParser.TEXT -> {
+ if (currentTag != null) {
+ switch (currentTag) {
+ case "ca" -> {
+ ca = parser.getText();
+ ca = ca.trim();
+ }
+ case "key" -> {
+ key = parser.getText();
+ key = key.trim();
+ }
+ case "cert" -> {
+ cert = parser.getText();
+ cert = cert.trim();
+ }
+ }
+ }
+ }
+ case XmlPullParser.END_TAG -> currentTag = null;
+ }
+ eventType = parser.next();
+ }
+
+ provider.setCaCert(ca);
+ provider.setPrivateKeyString(key);
+ provider.setVpnCertificate(cert);
+
+ }
+}
+
+
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java
index f1d86876..9d29911b 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/FileHelper.java
@@ -70,7 +70,7 @@ public class FileHelper {
}
reader.close();
return sb.toString();
- } catch (IOException errabi) {
+ } catch (NullPointerException | IOException errabi) {
return null;
}
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
index eee3bfb2..ba644b91 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PreferenceHelper.java
@@ -1,12 +1,18 @@
package se.leap.bitmaskclient.base.utils;
import static android.content.Context.MODE_PRIVATE;
+import static de.blinkt.openvpn.core.connection.Connection.TransportProtocol.TCP;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_AUTOMATICALLY;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_OBFS4;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_OBFS4_KCP;
+import static se.leap.bitmaskclient.base.fragments.CensorshipCircumventionFragment.TUNNELING_QUIC;
import static se.leap.bitmaskclient.base.models.Constants.ALLOW_EXPERIMENTAL_TRANSPORTS;
import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_BLUETOOTH;
import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_USB;
import static se.leap.bitmaskclient.base.models.Constants.ALLOW_TETHERING_WIFI;
import static se.leap.bitmaskclient.base.models.Constants.ALWAYS_ON_SHOW_DIALOG;
import static se.leap.bitmaskclient.base.models.Constants.CLEARLOG;
+import static se.leap.bitmaskclient.base.models.Constants.COUNTRYCODE;
import static se.leap.bitmaskclient.base.models.Constants.CUSTOM_PROVIDER_DOMAINS;
import static se.leap.bitmaskclient.base.models.Constants.DEFAULT_SHARED_PREFS_BATTERY_SAVER;
import static se.leap.bitmaskclient.base.models.Constants.EIP_IS_ALWAYS_ON;
@@ -19,14 +25,18 @@ import static se.leap.bitmaskclient.base.models.Constants.LAST_UPDATE_CHECK;
import static se.leap.bitmaskclient.base.models.Constants.LAST_USED_PROFILE;
import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_CERT;
import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_IP;
-import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_KCP;
import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_LOCATION;
import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_PORT;
+import static se.leap.bitmaskclient.base.models.Constants.OBFUSCATION_PINNING_PROTOCOL;
import static se.leap.bitmaskclient.base.models.Constants.PREFERENCES_APP_VERSION;
import static se.leap.bitmaskclient.base.models.Constants.PREFERRED_CITY;
import static se.leap.bitmaskclient.base.models.Constants.PREFER_UDP;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_CONFIGURED;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_EIP_DEFINITION;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_BRIDGES;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_EIPSERVICE;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_GATEWAYS;
+import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MODELS_PROVIDER;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_HASHES;
import static se.leap.bitmaskclient.base.models.Constants.PROVIDER_MOTD_LAST_SEEN;
@@ -40,12 +50,16 @@ import static se.leap.bitmaskclient.base.models.Constants.SHOW_EXPERIMENTAL;
import static se.leap.bitmaskclient.base.models.Constants.USE_BRIDGES;
import static se.leap.bitmaskclient.base.models.Constants.USE_IPv6_FIREWALL;
import static se.leap.bitmaskclient.base.models.Constants.USE_OBFUSCATION_PINNING;
+import static se.leap.bitmaskclient.base.models.Constants.USE_PORT_HOPPING;
import static se.leap.bitmaskclient.base.models.Constants.USE_SNOWFLAKE;
import static se.leap.bitmaskclient.base.models.Constants.USE_SYSTEM_PROXY;
+import static se.leap.bitmaskclient.base.models.Constants.USE_TUNNEL;
+import static se.leap.bitmaskclient.base.utils.BitmaskCoreProvider.getBitmaskMobile;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
+import android.util.Base64;
import android.util.Log;
import androidx.annotation.VisibleForTesting;
@@ -57,9 +71,9 @@ import org.json.JSONException;
import org.json.JSONObject;
import java.io.IOException;
-import java.net.MalformedURLException;
import java.net.URL;
import java.security.GeneralSecurityException;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@@ -68,6 +82,7 @@ import java.util.Set;
import de.blinkt.openvpn.VpnProfile;
import de.blinkt.openvpn.core.NativeUtils;
import se.leap.bitmaskclient.BuildConfig;
+import se.leap.bitmaskclient.base.models.Introducer;
import se.leap.bitmaskclient.base.models.Provider;
import se.leap.bitmaskclient.tor.TorStatusObservable;
@@ -143,13 +158,19 @@ public class PreferenceHelper {
provider.define(new JSONObject(preferences.getString(Provider.KEY, "")));
provider.setCaCert(preferences.getString(Provider.CA_CERT, ""));
provider.setVpnCertificate(preferences.getString(PROVIDER_VPN_CERTIFICATE, ""));
- provider.setPrivateKey(preferences.getString(PROVIDER_PRIVATE_KEY, ""));
+ provider.setPrivateKeyString(preferences.getString(PROVIDER_PRIVATE_KEY, ""));
provider.setEipServiceJson(new JSONObject(preferences.getString(PROVIDER_EIP_DEFINITION, "")));
provider.setMotdJson(new JSONObject(preferences.getString(PROVIDER_MOTD, "")));
provider.setLastMotdSeen(preferences.getLong(PROVIDER_MOTD_LAST_SEEN, 0L));
provider.setLastMotdUpdate(preferences.getLong(PROVIDER_MOTD_LAST_UPDATED, 0L));
provider.setMotdLastSeenHashes(preferences.getStringSet(PROVIDER_MOTD_HASHES, new HashSet<>()));
- } catch (MalformedURLException | JSONException e) {
+ provider.setModelsProvider(preferences.getString(PROVIDER_MODELS_PROVIDER, null));
+ provider.setService(preferences.getString(PROVIDER_MODELS_EIPSERVICE, null));
+ provider.setBridges(preferences.getString(PROVIDER_MODELS_BRIDGES, null));
+ provider.setGateways(preferences.getString(PROVIDER_MODELS_GATEWAYS, null));
+ provider.setIntroducer(getBitmaskMobile().getIntroducerURLByDomain(provider.getDomain()));
+
+ } catch (Exception e) {
e.printStackTrace();
}
}
@@ -197,9 +218,16 @@ public class PreferenceHelper {
for (String domain : providerDomains) {
String mainURL = preferences.getString(Provider.MAIN_URL + "." + domain, null);
if (mainURL != null) {
- customProviders.put(mainURL, Provider.createCustomProvider(mainURL, domain));
+ Introducer introducer = null;
+ try {
+ introducer = Introducer.fromUrl(BitmaskCoreProvider.getBitmaskMobile().getIntroducerURLByDomain(domain));
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ customProviders.put(mainURL, Provider.createCustomProvider(mainURL, domain, introducer));
}
}
+
return customProviders;
}
@@ -210,7 +238,7 @@ public class PreferenceHelper {
SharedPreferences.Editor editor = preferences.edit();
for (Provider provider : providers) {
String providerDomain = provider.getDomain();
- editor.putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrlString());
+ editor.putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrl());
newProviderDomains.add(providerDomain);
}
@@ -238,16 +266,20 @@ public class PreferenceHelper {
putString(Provider.GEOIP_URL, provider.getGeoipUrl().toString()).
putString(Provider.MOTD_URL, provider.getMotdUrl().toString()).
putString(Provider.PROVIDER_API_IP, provider.getProviderApiIp()).
- putString(Provider.MAIN_URL, provider.getMainUrlString()).
+ putString(Provider.MAIN_URL, provider.getMainUrl()).
putString(Provider.KEY, provider.getDefinitionString()).
putString(Provider.CA_CERT, provider.getCaCert()).
putString(PROVIDER_EIP_DEFINITION, provider.getEipServiceJsonString()).
- putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKey()).
+ putString(PROVIDER_PRIVATE_KEY, provider.getPrivateKeyString()).
putString(PROVIDER_VPN_CERTIFICATE, provider.getVpnCertificate()).
putString(PROVIDER_MOTD, provider.getMotdJsonString()).
putStringSet(PROVIDER_MOTD_HASHES, provider.getMotdLastSeenHashes()).
putLong(PROVIDER_MOTD_LAST_SEEN, provider.getLastMotdSeen()).
- putLong(PROVIDER_MOTD_LAST_UPDATED, provider.getLastMotdUpdate());
+ putLong(PROVIDER_MOTD_LAST_UPDATED, provider.getLastMotdUpdate()).
+ putString(PROVIDER_MODELS_GATEWAYS, provider.getGatewaysJson()).
+ putString(PROVIDER_MODELS_BRIDGES, provider.getBridgesJson()).
+ putString(PROVIDER_MODELS_EIPSERVICE, provider.getServiceJson()).
+ putString(PROVIDER_MODELS_PROVIDER, provider.getModelsProviderJson());
if (async) {
editor.apply();
} else {
@@ -258,9 +290,9 @@ public class PreferenceHelper {
preferences.edit().putBoolean(PROVIDER_CONFIGURED, true).
putString(Provider.PROVIDER_IP + "." + providerDomain, provider.getProviderIp()).
putString(Provider.PROVIDER_API_IP + "." + providerDomain, provider.getProviderApiIp()).
- putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrlString()).
- putString(Provider.GEOIP_URL + "." + providerDomain, provider.getGeoipUrl().toString()).
- putString(Provider.MOTD_URL + "." + providerDomain, provider.getMotdUrl().toString()).
+ putString(Provider.MAIN_URL + "." + providerDomain, provider.getMainUrl()).
+ putString(Provider.GEOIP_URL + "." + providerDomain, provider.getGeoipUrl()).
+ putString(Provider.MOTD_URL + "." + providerDomain, provider.getMotdUrl()).
putString(Provider.KEY + "." + providerDomain, provider.getDefinitionString()).
putString(Provider.CA_CERT + "." + providerDomain, provider.getCaCert()).
putString(PROVIDER_EIP_DEFINITION + "." + providerDomain, provider.getEipServiceJsonString()).
@@ -445,6 +477,7 @@ public class PreferenceHelper {
putBoolean(USE_SNOWFLAKE, isEnabled);
if (!isEnabled) {
TorStatusObservable.setProxyPort(-1);
+ TorStatusObservable.setSocksProxyPort(-1);
}
}
@@ -452,6 +485,10 @@ public class PreferenceHelper {
return hasKey(USE_SNOWFLAKE);
}
+ public static void resetSnowflakeSettings() {
+ removeKey(USE_SNOWFLAKE);
+ }
+
public static Boolean getUseSnowflake() {
return getBoolean(USE_SNOWFLAKE, true);
}
@@ -513,8 +550,7 @@ public class PreferenceHelper {
}
public static boolean useObfuscationPinning() {
- return BuildConfigHelper.useObfsVpn() &&
- getUseBridges() &&
+ return getUseBridges() &&
getBoolean(USE_OBFUSCATION_PINNING, false) &&
!TextUtils.isEmpty(getObfuscationPinningIP()) &&
!TextUtils.isEmpty(getObfuscationPinningCert()) &&
@@ -553,18 +589,54 @@ public class PreferenceHelper {
return getString(OBFUSCATION_PINNING_LOCATION, null);
}
- public static Boolean getObfuscationPinningKCP() {
- return getBoolean(OBFUSCATION_PINNING_KCP, false);
+ public static String getObfuscationPinningProtocol() {
+ return getString(OBFUSCATION_PINNING_PROTOCOL, TCP.toString());
}
- public static void setObfuscationPinningKCP(boolean isKCP) {
- putBoolean(OBFUSCATION_PINNING_KCP, isKCP);
+ public static void setObfuscationPinningProtocol(String protocol) {
+ putString(OBFUSCATION_PINNING_PROTOCOL, protocol);
}
public static void setUseIPv6Firewall(boolean useFirewall) {
putBoolean(USE_IPv6_FIREWALL, useFirewall);
}
+ public static boolean getUsePortHopping() {
+ return getBoolean(USE_PORT_HOPPING, false);
+ }
+
+ public static void setUsePortHopping(boolean usePortHopping) {
+ putBoolean(USE_PORT_HOPPING, usePortHopping);
+ }
+
+ public static boolean getUseObfs4() {
+ return getUseTunnel() == TUNNELING_OBFS4;
+ }
+
+ public static boolean getUseObfs4Kcp() {
+ return getUseTunnel() == TUNNELING_OBFS4_KCP;
+ }
+
+ public static boolean getUseObfs4Quic() {
+ return getUseTunnel() == TUNNELING_QUIC;
+ }
+
+ public static boolean useManualBridgeSettings() {
+ return getUseObfs4() || getUseObfs4Kcp() || getUseObfs4Quic() || getUsePortHopping();
+ }
+
+ public static boolean useManualDiscoverySettings() {
+ return hasSnowflakePrefs() && getUseSnowflake();
+ }
+
+ public static void setUseTunnel(int tunnel) {
+ putInt(USE_TUNNEL, tunnel);
+ }
+
+ public static int getUseTunnel() {
+ return getInt(USE_TUNNEL, TUNNELING_AUTOMATICALLY);
+ }
+
public static boolean useIpv6Firewall() {
return getBoolean(USE_IPv6_FIREWALL, false);
}
@@ -577,6 +649,14 @@ public class PreferenceHelper {
return getBoolean(ALWAYS_ON_SHOW_DIALOG, true);
}
+ public static String getBaseCountry() {
+ return getString(COUNTRYCODE, null);
+ }
+
+ public static void setBaseCountry(String countryCode) {
+ putString(COUNTRYCODE, countryCode);
+ }
+
public static String getPreferredCity() {
return useObfuscationPinning() ? null : getString(PREFERRED_CITY, null);
}
@@ -631,6 +711,12 @@ public class PreferenceHelper {
}
}
+ public static void removeKey(String key) {
+ synchronized (LOCK) {
+ preferences.edit().remove(key).apply();
+ }
+ }
+
public static long getLong(String key, long defValue) {
synchronized (LOCK) {
return preferences.getLong(key, defValue);
@@ -738,4 +824,124 @@ public class PreferenceHelper {
preferences.edit().clear().apply();
}
}
+
+ public static class SharedPreferenceStore implements mobilemodels.Store {
+
+ public SharedPreferenceStore() throws IllegalArgumentException {
+ if (preferences == null) {
+ throw new IllegalStateException("Preferences not initialized.");
+ }
+ }
+ @Override
+ public void clear() throws Exception {
+ preferences.edit().clear().apply();
+ }
+
+ @Override
+ public void close() throws Exception {
+
+ }
+
+ @Override
+ public boolean contains(String s) throws Exception {
+ return preferences.contains(s);
+ }
+
+ @Override
+ public boolean getBoolean(String s) {
+ return preferences.getBoolean(s, false);
+ }
+
+ @Override
+ public boolean getBooleanWithDefault(String s, boolean b) {
+ return preferences.getBoolean(s, b);
+ }
+
+ @Override
+ public byte[] getByteArray(String s) {
+ String encodedString = preferences.getString(s, Arrays.toString(Base64.encode(new byte[0], Base64.DEFAULT)));
+ try {
+ return Base64.decode(encodedString, Base64.DEFAULT);
+ } catch (IllegalArgumentException e) {
+ return new byte[0];
+ }
+ }
+
+ @Override
+ public byte[] getByteArrayWithDefault(String s, byte[] bytes) {
+ String encodedString = preferences.getString(s, "");
+ try {
+ return Base64.decode(encodedString, Base64.DEFAULT);
+ } catch (IllegalArgumentException e) {
+ return bytes;
+ }
+ }
+
+ @Override
+ public long getInt(String s) {
+ return preferences.getInt(s, 0);
+ }
+
+ @Override
+ public long getIntWithDefault(String s, long l) {
+ return preferences.getInt(s, (int) l);
+ }
+
+ @Override
+ public long getLong(String s) {
+ return preferences.getLong(s, 0L);
+ }
+
+ @Override
+ public long getLongWithDefault(String s, long l) {
+ return preferences.getLong(s, l);
+ }
+
+ @Override
+ public String getString(String s) {
+ return preferences.getString(s, "");
+ }
+
+ @Override
+ public String getStringWithDefault(String s, String s1) {
+ return preferences.getString(s, s1);
+ }
+
+ @Override
+ public void open() throws Exception {
+
+ }
+
+ @Override
+ public void remove(String s) throws Exception {
+ preferences.edit().remove(s).apply();
+ }
+
+ @Override
+ public void setBoolean(String s, boolean b) {
+ preferences.edit().putBoolean(s, b).apply();
+ }
+
+ @Override
+ public void setByteArray(String s, byte[] bytes) {
+ String encodedString = Base64.encodeToString(bytes, Base64.DEFAULT);
+ preferences.edit().putString(s, encodedString).apply();
+ }
+
+ @Override
+ public void setInt(String s, long l) {
+ preferences.edit().putInt(s, (int) l).apply();
+ }
+
+ @Override
+ public void setLong(String s, long l) {
+ preferences.edit().putLong(s, l).apply();
+ }
+
+ @Override
+ public void setString(String s, String s1) {
+ preferences.edit().putString(s, s1).apply();
+ }
+ }
+
}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelper.java
new file mode 100644
index 00000000..43af5200
--- /dev/null
+++ b/app/src/main/java/se/leap/bitmaskclient/base/utils/PrivateKeyHelper.java
@@ -0,0 +1,124 @@
+package se.leap.bitmaskclient.base.utils;
+
+import static android.util.Base64.encodeToString;
+
+import android.util.Log;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+
+import org.spongycastle.util.encoders.Base64;
+
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.interfaces.RSAPrivateKey;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+import de.blinkt.openvpn.core.NativeUtils;
+
+public class PrivateKeyHelper {
+
+ public static final String TAG = PrivateKeyHelper.class.getSimpleName();
+
+ public static final String RSA = "RSA";
+ public static final String ED_25519 = "Ed25519";
+ public static final String ECDSA = "ECDSA";
+
+ public static final String RSA_KEY_BEGIN = "-----BEGIN RSA PRIVATE KEY-----\n";
+ public static final String RSA_KEY_END = "-----END RSA PRIVATE KEY-----";
+ public static final String EC_KEY_BEGIN = "-----BEGIN PRIVATE KEY-----\n";
+ public static final String EC_KEY_END = "-----END PRIVATE KEY-----";
+
+
+ public interface PrivateKeyHelperInterface {
+
+
+ @Nullable PrivateKey parsePrivateKeyFromString(String privateKeyString);
+ }
+
+ public static class DefaultPrivateKeyHelper implements PrivateKeyHelperInterface {
+
+ public PrivateKey parsePrivateKeyFromString(String privateKeyString) {
+ if (privateKeyString == null || privateKeyString.isBlank()) {
+ return null;
+ }
+ if (privateKeyString.contains(RSA_KEY_BEGIN)) {
+ return parseRsaKeyFromString(privateKeyString);
+ } else if (privateKeyString.contains(EC_KEY_BEGIN)) {
+ return parseECPrivateKey(privateKeyString);
+ } else {
+ return null;
+ }
+ }
+
+ private RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
+ RSAPrivateKey key;
+ try {
+ KeyFactory kf;
+ kf = KeyFactory.getInstance(RSA, "BC");
+ rsaKeyString = rsaKeyString.replaceFirst(RSA_KEY_BEGIN, "").replaceFirst(RSA_KEY_END, "");
+
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString));
+ key = (RSAPrivateKey) kf.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException | NullPointerException |
+ NoSuchProviderException e) {
+ e.printStackTrace();
+ return null;
+ }
+
+ return key;
+ }
+
+ private PrivateKey parseECPrivateKey(String ecKeyString) {
+ String base64 = ecKeyString.replace(EC_KEY_BEGIN, "").replace(EC_KEY_END, "");
+ byte[] keyBytes = Base64.decode(base64);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
+ String errMsg;
+ try {
+ KeyFactory keyFactory = KeyFactory.getInstance(ED_25519, "BC");
+ return keyFactory.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchProviderException e) {
+ errMsg = e.toString();
+ }
+
+ try {
+ KeyFactory keyFactory = KeyFactory.getInstance(ECDSA, "BC");
+ return keyFactory.generatePrivate(keySpec);
+ } catch (InvalidKeySpecException | NoSuchAlgorithmException | NoSuchProviderException e) {
+ errMsg += "\n" + e.toString();
+ Log.e(TAG, errMsg);
+ }
+ return null;
+ }
+ }
+
+ public static String getPEMFormattedPrivateKey(PrivateKey key) throws NullPointerException {
+ if (key == null) {
+ throw new NullPointerException("Private key was null.");
+ }
+ String keyString = encodeToString(key.getEncoded(), android.util.Base64.DEFAULT);
+
+ if (key instanceof RSAPrivateKey) {
+ return (RSA_KEY_BEGIN + keyString + RSA_KEY_END);
+ } else {
+ return EC_KEY_BEGIN + keyString + EC_KEY_END;
+ }
+ }
+
+ private static PrivateKeyHelperInterface instance = new DefaultPrivateKeyHelper();
+
+ @VisibleForTesting
+ public PrivateKeyHelper(PrivateKeyHelperInterface helperInterface) {
+ if (!NativeUtils.isUnitTest()) {
+ throw new IllegalStateException("PrivateKeyHelper injected with PrivateKeyHelperInterface outside of an unit test");
+ }
+ instance = helperInterface;
+ }
+
+ public static @Nullable PrivateKey parsePrivateKeyFromString(String rsaKeyString) {
+ return instance.parsePrivateKeyFromString(rsaKeyString);
+ }
+}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/utils/RSAHelper.java b/app/src/main/java/se/leap/bitmaskclient/base/utils/RSAHelper.java
deleted file mode 100644
index 2872139a..00000000
--- a/app/src/main/java/se/leap/bitmaskclient/base/utils/RSAHelper.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package se.leap.bitmaskclient.base.utils;
-
-import android.os.Build;
-
-import androidx.annotation.VisibleForTesting;
-
-import org.spongycastle.util.encoders.Base64;
-
-import java.security.KeyFactory;
-import java.security.NoSuchAlgorithmException;
-import java.security.NoSuchProviderException;
-import java.security.interfaces.RSAPrivateKey;
-import java.security.spec.InvalidKeySpecException;
-import java.security.spec.PKCS8EncodedKeySpec;
-
-import de.blinkt.openvpn.core.NativeUtils;
-
-public class RSAHelper {
-
- public interface RSAHelperInterface {
- RSAPrivateKey parseRsaKeyFromString(String rsaKeyString);
- }
-
- public static class DefaultRSAHelper implements RSAHelperInterface {
-
- @Override
- public RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
- RSAPrivateKey key;
- try {
- KeyFactory kf;
- if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
- kf = KeyFactory.getInstance("RSA", "BC");
- } else {
- kf = KeyFactory.getInstance("RSA");
- }
- rsaKeyString = rsaKeyString.replaceFirst("-----BEGIN RSA PRIVATE KEY-----", "").replaceFirst("-----END RSA PRIVATE KEY-----", "");
- PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.decode(rsaKeyString));
- key = (RSAPrivateKey) kf.generatePrivate(keySpec);
- } catch (InvalidKeySpecException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- return null;
- } catch (NoSuchAlgorithmException e) {
- // TODO Auto-generated catch block
- e.printStackTrace();
- return null;
- } catch (NullPointerException e) {
- e.printStackTrace();
- return null;
- } catch (NoSuchProviderException e) {
- e.printStackTrace();
- return null;
- }
-
- return key;
- }
- }
-
- private static RSAHelperInterface instance = new DefaultRSAHelper();
-
- @VisibleForTesting
- public RSAHelper(RSAHelperInterface helperInterface) {
- if (!NativeUtils.isUnitTest()) {
- throw new IllegalStateException("RSAHelper injected with RSAHelperInterface outside of an unit test");
- }
- instance = helperInterface;
- }
-
- public static RSAPrivateKey parseRsaKeyFromString(String rsaKeyString) {
- return instance.parseRsaKeyFromString(rsaKeyString);
- }
-}
diff --git a/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java
index 7aefd089..57a33bf4 100644
--- a/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java
+++ b/app/src/main/java/se/leap/bitmaskclient/base/views/IconTextEntry.java
@@ -108,4 +108,10 @@ public class IconTextEntry extends LinearLayout {
iconView.setImageResource(id);
}
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ textView.setTextColor(getResources().getColor(enabled ? android.R.color.black : R.color.colorDisabled));
+ iconView.setImageAlpha(enabled ? 255 : 128);
+ }
}