diff options
Diffstat (limited to 'app/src/main/java/se/leap/bitmaskclient/base')
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); + } } |
