From 09c5cb0a0d33c58897026b7b4b1901c1ddc088f1 Mon Sep 17 00:00:00 2001 From: Arne Schwabe Date: Tue, 24 Aug 2021 19:25:59 +0200 Subject: Implement support of openvpn://import-profile/ support For details about the protocol see https://github.com/OpenVPN/openvpn3/blob/master/doc/webauth.md --- .../de/blinkt/openvpn/fragments/ImportASConfig.kt | 421 ------------------- .../blinkt/openvpn/fragments/ImportRemoteConfig.kt | 465 +++++++++++++++++++++ .../blinkt/openvpn/fragments/VPNProfileList.java | 14 +- 3 files changed, 469 insertions(+), 431 deletions(-) delete mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/ImportASConfig.kt create mode 100644 main/src/ui/java/de/blinkt/openvpn/fragments/ImportRemoteConfig.kt (limited to 'main/src/ui/java/de/blinkt/openvpn/fragments') diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ImportASConfig.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/ImportASConfig.kt deleted file mode 100644 index 71d1ee7c..00000000 --- a/main/src/ui/java/de/blinkt/openvpn/fragments/ImportASConfig.kt +++ /dev/null @@ -1,421 +0,0 @@ -/* - * Copyright (c) 2012-2019 Arne Schwabe - * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt - */ - -package de.blinkt.openvpn.fragments - -import android.app.AlertDialog -import android.app.Dialog -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.net.TrafficStats -import android.os.Bundle -import android.text.InputType -import android.util.Base64 -import android.util.Base64.NO_WRAP -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.CheckBox -import android.widget.EditText -import android.widget.Toast -import androidx.fragment.app.DialogFragment -import androidx.lifecycle.lifecycleScope -import de.blinkt.openvpn.R -import de.blinkt.openvpn.activities.ConfigConverter -import de.blinkt.openvpn.core.Preferences -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import okhttp3.* -import okhttp3.internal.tls.OkHostnameVerifier -import java.io.IOException -import java.security.MessageDigest -import java.security.cert.CertPathValidatorException -import java.security.cert.CertificateException -import java.security.cert.X509Certificate -import java.util.concurrent.TimeUnit -import java.util.regex.Pattern -import javax.net.ssl.* - -class BasicAuthInterceptor(user: String, password: String) : Interceptor { - - private val credentials: String - - init { - this.credentials = Credentials.basic(user, password) - } - - @Throws(IOException::class) - override fun intercept(chain: Interceptor.Chain): Response { - val request = chain.request() - val authenticatedRequest = request.newBuilder() - .header("Authorization", credentials).build() - return chain.proceed(authenticatedRequest) - } - -} - - -fun getCompositeSSLSocketFactory(certPin: CertificatePinner, hostname: String): SSLSocketFactory { - val trustPinnedCerts = arrayOf(object : X509TrustManager { - override fun getAcceptedIssuers(): Array { - return emptyArray() - } - - @Throws(CertificateException::class) - override fun checkClientTrusted(chain: Array, authType: String) { - throw CertificateException("Why would we check client certificates?!") - } - - @Throws(CertificateException::class) - override fun checkServerTrusted(chain: Array, authType: String) { - certPin.check(hostname, chain.toList()) - } - }) - - // Install the all-trusting trust manager - val sslContext = SSLContext.getInstance("TLS") - sslContext.init(null, trustPinnedCerts, java.security.SecureRandom()) - // Create an ssl socket factory with our all-trusting manager - - return sslContext.socketFactory - -} - -class ImportASConfig : DialogFragment() { - private lateinit var asUseAutlogin: CheckBox - private lateinit var asServername: EditText - private lateinit var asUsername: EditText - private lateinit var asPassword: EditText - private lateinit var dialogView: View - - - - internal fun getHostNameVerifier(prefs: SharedPreferences): HostnameVerifier { - val pinnedHostnames: Set = prefs.getStringSet("pinnedHosts", emptySet())!! - - val mapping = mutableMapOf() - - pinnedHostnames.forEach { ph -> - mapping[ph] = prefs.getString("pin-${ph}", "") - } - - val defaultVerifier = OkHostnameVerifier.INSTANCE; - val pinHostVerifier = object : HostnameVerifier { - override fun verify(hostname: String?, session: SSLSession?): Boolean { - val unverifiedHandshake = Handshake.get(session) - val cert = unverifiedHandshake.peerCertificates()[0] as X509Certificate - val hostPin = CertificatePinner.pin(cert) - - if (mapping.containsKey(hostname) && mapping[hostname] == hostPin) - return true - else - return defaultVerifier.verify(hostname, session) - } - - } - return pinHostVerifier - } - - internal fun buildHttpClient(c: Context, user: String, password: String, hostname: String): OkHttpClient { - - // TODO: HACK - val THREAD_ID = 10000; - TrafficStats.setThreadStatsTag(THREAD_ID); - - val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) - val pinnedHosts: Set = prefs.getStringSet("pinnedHosts", emptySet())!! - - val okHttpClient = OkHttpClient.Builder() - .addInterceptor(BasicAuthInterceptor(user, password)) - .connectTimeout(15, TimeUnit.SECONDS) - - /* Rely on system certificates if we do not have the host pinned */ - if (pinnedHosts.contains(hostname)) { - val cpb = CertificatePinner.Builder() - - pinnedHosts.forEach { ph -> - cpb.add(ph, prefs.getString("pin-${ph}", "")) - } - - - val certPinner = cpb.build() - getCompositeSSLSocketFactory(certPinner, hostname).let { - okHttpClient.sslSocketFactory(it) - } - //okHttpClient.certificatePinner(certPinner) - } - - okHttpClient.hostnameVerifier(getHostNameVerifier(prefs)) - - val client = okHttpClient.build() - return client - - } - - /** - * @param fp Fingerprint in sha 256 format - */ - internal fun addPinnedCert(c: Context, host: String, fp: String) { - val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) - val pedit = prefs.edit() - val pinnedHosts: MutableSet = prefs.getStringSet("pinnedHosts", mutableSetOf())!! - - pinnedHosts.add(host) - - pedit.putString("pin-${host}", "sha256/${fp}") - - pedit.putStringSet("pinnedHosts", pinnedHosts) - - pedit.apply() - } - - internal fun removedPinnedCert(c: Context, host: String) { - val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) - val pedit = prefs.edit() - val pinnedHosts: MutableSet = prefs.getStringSet("pinnedHosts", mutableSetOf())!! - - pinnedHosts.remove(host) - - pedit.remove("pin-${host}") - - pedit.putStringSet("pinnedHosts", pinnedHosts) - - pedit.apply() - } - - fun fetchProfile(c: Context, asUri: HttpUrl, user: String, password: String): Response? { - - - val httpClient = buildHttpClient(c, user, password, asUri.host() ?: "") - - val request = Request.Builder() - .url(asUri) - .build() - - val response = httpClient.newCall(request).execute() - - return response - - } - - private fun getAsUrl(url: String, autologin: Boolean): HttpUrl { - var asurl = url - if (!asurl.startsWith("http")) - asurl = "https://" + asurl - - if (autologin) - asurl += "/rest/GetAutologin?tls-cryptv2=1" - else - asurl += "/rest/GetUserlogin?tls-cryptv2=1" - - val asUri = HttpUrl.parse(asurl) - return asUri - } - - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - return dialogView - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val inflater = requireActivity().layoutInflater - dialogView = inflater.inflate(R.layout.import_as_config, null); - - val builder = AlertDialog.Builder(requireContext()) - - builder.setView(dialogView) - builder.setTitle(R.string.import_from_as) - - asServername = dialogView.findViewById(R.id.as_servername) - asUsername = dialogView.findViewById(R.id.username) - asPassword = dialogView.findViewById(R.id.password) - asUseAutlogin = dialogView.findViewById(R.id.request_autologin) - - builder.setPositiveButton(R.string.import_config, null) - builder.setNegativeButton(android.R.string.cancel) { _, _ -> } - - val dialog = builder.create() - - return dialog - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - dialog!!.setOnShowListener() { d2 -> - val d: AlertDialog = d2 as AlertDialog - - d.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener() - - { _ -> - viewLifecycleOwner.lifecycleScope.launch { - doAsImport(asUsername.text.toString(), asPassword.text.toString()) - } - } - } - } - - val crvMessage = Pattern.compile(".*CRV1:R,E:(.*):(.*):(.*).*", Pattern.DOTALL) - - suspend internal fun doAsImport(user: String, password: String) { - var pleaseWait:AlertDialog? - withContext(Dispatchers.IO) - { - - withContext(Dispatchers.Main) - { - val ab = AlertDialog.Builder(requireContext()) - ab.setTitle("Downloading profile") - ab.setMessage("Please wait") - pleaseWait = ab.show() - - Toast.makeText(context, "Downloading profile", Toast.LENGTH_LONG).show() - } - - - val asProfileUri = getAsUrl(asServername.text.toString(), asUseAutlogin.isChecked) - - var e: Exception? = null - try { - val response = fetchProfile(requireContext(), asProfileUri, user, password) - - if (response == null) { - throw Exception("No Response from Server") - } - - val profile = response.body().string() - if (response.code() == 401 && crvMessage.matcher(profile).matches()) { - withContext(Dispatchers.Main) { - pleaseWait?.dismiss() - showCRDialog(profile) - } - } else if (response.isSuccessful) { - - withContext(Dispatchers.Main) { - pleaseWait?.dismiss() - val startImport = Intent(activity, ConfigConverter::class.java) - startImport.action = ConfigConverter.IMPORT_PROFILE_DATA - startImport.putExtra(Intent.EXTRA_TEXT, profile) - startActivity(startImport) - dismiss() - } - } else { - throw Exception("Invalid Response from server: \n${response.code()} ${response.message()} \n\n ${profile}") - } - - } catch (ce: SSLHandshakeException) { - e = ce - // Find out if we are in the non trust path - if (ce.cause is CertificateException && ce.cause != null) { - val certExp: CertificateException = (ce.cause as CertificateException) - if (certExp.cause is CertPathValidatorException && certExp.cause != null) { - val caPathExp: CertPathValidatorException = certExp.cause as CertPathValidatorException - if (caPathExp.certPath.type.equals("X.509") && caPathExp.certPath.certificates.size > 0) { - val firstCert: X509Certificate = (caPathExp.certPath.certificates[0] as X509Certificate) - - val fpBytes = MessageDigest.getInstance("SHA-256").digest(firstCert.publicKey.encoded) - val fp = Base64.encodeToString(fpBytes, NO_WRAP) - - - - Log.i("OpenVPN", "Found cert with FP ${fp}: ${firstCert.subjectDN}") - withContext(Dispatchers.Main) { - - pleaseWait?.dismiss() - - AlertDialog.Builder(requireContext()) - .setTitle("Untrusted certificate found") - .setMessage(firstCert.toString()) - .setPositiveButton("Trust") { _, _ -> addPinnedCert(requireContext(), asProfileUri.host(), fp) } - .setNegativeButton("Do not trust", null) - .show() - } - e = null - } - } else if (ce.message != null && ce.message!!.contains("Certificate pinning failure")) { - withContext(Dispatchers.Main) { - pleaseWait?.dismiss() - - AlertDialog.Builder(requireContext()) - .setTitle("Different certificate than trusted certificate from server") - .setMessage(ce.message) - .setNegativeButton(android.R.string.ok, null) - .setPositiveButton("Forget pinned certificate", { _, _ -> removedPinnedCert(requireContext(), asProfileUri.host()) }) - .show(); - } - e = null - - } - } - } catch (ge: Exception) { - e = ge - } - if (e != null) { - withContext(Dispatchers.Main) { - pleaseWait?.dismiss() - AlertDialog.Builder(requireContext()) - .setTitle("Import failed") - .setMessage("Error: " + e.localizedMessage) - .setPositiveButton(android.R.string.ok, null) - .show() - } - } - } - } - - private fun showCRDialog(response: String) { - // This is a dirty hack instead of properly parsing the response - val m = crvMessage.matcher(response) - // We already know that it matches - m.matches() - val challenge = m.group(1) - var username = m.group(2) - val message = m.group(3) - - username = String(Base64.decode(username, Base64.DEFAULT)) - - val pwprefix = "CRV1::${challenge}::" - - val entry = EditText(context) - entry.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD) - - AlertDialog.Builder(requireContext()) - .setTitle("Server request challenge/response authentication") - .setMessage("Challenge: " + message) - .setView(entry) - .setNegativeButton(android.R.string.cancel, null) - .setPositiveButton(R.string.import_config) { _,_ -> - viewLifecycleOwner.lifecycleScope.launch { - doAsImport(username, pwprefix + entry.text.toString()) - } - } - .show() - - } - - - override fun onResume() { - super.onResume() - asServername.setText(Preferences.getDefaultSharedPreferences(activity).getString("as-hostname", "")) - asUsername.setText(Preferences.getDefaultSharedPreferences(activity).getString("as-username", "")) - } - - override fun onPause() { - super.onPause() - val prefs = Preferences.getDefaultSharedPreferences(activity) - prefs.edit().putString("as-hostname", asServername.text.toString()).apply() - prefs.edit().putString("as-username", asUsername.text.toString()).apply() - } - - companion object { - @JvmStatic - fun newInstance(): ImportASConfig { - return ImportASConfig(); - } - } - -} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/ImportRemoteConfig.kt b/main/src/ui/java/de/blinkt/openvpn/fragments/ImportRemoteConfig.kt new file mode 100644 index 00000000..6cd322ca --- /dev/null +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/ImportRemoteConfig.kt @@ -0,0 +1,465 @@ +/* + * Copyright (c) 2012-2019 Arne Schwabe + * Distributed under the GNU GPL v2 with additional terms. For full terms see the file doc/LICENSE.txt + */ + +package de.blinkt.openvpn.fragments + +import android.app.AlertDialog +import android.app.Dialog +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.TrafficStats +import android.os.Bundle +import android.text.InputType +import android.util.Base64 +import android.util.Base64.NO_WRAP +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.* +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import de.blinkt.openvpn.R +import de.blinkt.openvpn.activities.ConfigConverter +import de.blinkt.openvpn.core.Preferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.* +import okhttp3.internal.tls.OkHostnameVerifier +import java.io.IOException +import java.security.MessageDigest +import java.security.cert.CertPathValidatorException +import java.security.cert.CertificateException +import java.security.cert.X509Certificate +import java.util.concurrent.TimeUnit +import java.util.regex.Pattern +import javax.net.ssl.* + +class BasicAuthInterceptor(user: String, password: String) : Interceptor { + + private val credentials: String + + init { + this.credentials = Credentials.basic(user, password) + } + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val authenticatedRequest = request.newBuilder() + .header("Authorization", credentials).build() + return chain.proceed(authenticatedRequest) + } + +} + + +fun getCompositeSSLSocketFactory(certPin: CertificatePinner, hostname: String): SSLSocketFactory { + val trustPinnedCerts = arrayOf(object : X509TrustManager { + override fun getAcceptedIssuers(): Array { + return emptyArray() + } + + @Throws(CertificateException::class) + override fun checkClientTrusted(chain: Array, authType: String) { + throw CertificateException("Why would we check client certificates?!") + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + certPin.check(hostname, chain.toList()) + } + }) + + // Install the all-trusting trust manager + val sslContext = SSLContext.getInstance("TLS") + sslContext.init(null, trustPinnedCerts, java.security.SecureRandom()) + // Create an ssl socket factory with our all-trusting manager + + return sslContext.socketFactory + +} + +class ImportRemoteConfig : DialogFragment() { + private lateinit var asUseAutologin: CheckBox + private lateinit var asServername: EditText + private lateinit var asUsername: EditText + private lateinit var asPassword: EditText + private lateinit var dialogView: View + + private lateinit var importChoiceGroup: RadioGroup + private lateinit var importChoiceAS: RadioButton + + + + internal fun getHostNameVerifier(prefs: SharedPreferences): HostnameVerifier { + val pinnedHostnames: Set = prefs.getStringSet("pinnedHosts", emptySet())!! + + val mapping = mutableMapOf() + + pinnedHostnames.forEach { ph -> + mapping[ph] = prefs.getString("pin-${ph}", "") + } + + val defaultVerifier = OkHostnameVerifier.INSTANCE; + val pinHostVerifier = object : HostnameVerifier { + override fun verify(hostname: String?, session: SSLSession?): Boolean { + val unverifiedHandshake = Handshake.get(session) + val cert = unverifiedHandshake.peerCertificates()[0] as X509Certificate + val hostPin = CertificatePinner.pin(cert) + + if (mapping.containsKey(hostname) && mapping[hostname] == hostPin) + return true + else + return defaultVerifier.verify(hostname, session) + } + + } + return pinHostVerifier + } + + internal fun buildHttpClient(c: Context, user: String, password: String, hostname: String): OkHttpClient { + + // TODO: HACK + val THREAD_ID = 10000; + TrafficStats.setThreadStatsTag(THREAD_ID); + + val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) + val pinnedHosts: Set = prefs.getStringSet("pinnedHosts", emptySet())!! + + val okHttpClient = OkHttpClient.Builder() + if (user.isNotBlank() && password.isNotBlank()) { + okHttpClient.addInterceptor(BasicAuthInterceptor(user, password)) + } + okHttpClient.connectTimeout(15, TimeUnit.SECONDS) + + /* Rely on system certificates if we do not have the host pinned */ + if (pinnedHosts.contains(hostname)) { + val cpb = CertificatePinner.Builder() + + pinnedHosts.forEach { ph -> + cpb.add(ph, prefs.getString("pin-${ph}", "")) + } + + + val certPinner = cpb.build() + getCompositeSSLSocketFactory(certPinner, hostname).let { + okHttpClient.sslSocketFactory(it) + } + //okHttpClient.certificatePinner(certPinner) + } + + okHttpClient.hostnameVerifier(getHostNameVerifier(prefs)) + + val client = okHttpClient.build() + return client + + } + + /** + * @param fp Fingerprint in sha 256 format + */ + internal fun addPinnedCert(c: Context, host: String, fp: String) { + val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) + val pedit = prefs.edit() + val pinnedHosts: MutableSet = prefs.getStringSet("pinnedHosts", mutableSetOf())!! + .toMutableSet() + + pinnedHosts.add(host) + + pedit.putString("pin-${host}", "sha256/${fp}") + + pedit.putStringSet("pinnedHosts", pinnedHosts) + + pedit.apply() + } + + internal fun removedPinnedCert(c: Context, host: String) { + val prefs = c.getSharedPreferences("pinnedCerts", Context.MODE_PRIVATE) + val pedit = prefs.edit() + val pinnedHosts: MutableSet = prefs.getStringSet("pinnedHosts", mutableSetOf())!!.toMutableSet() + + pinnedHosts.remove(host) + + pedit.remove("pin-${host}") + + pedit.putStringSet("pinnedHosts", pinnedHosts) + + pedit.apply() + } + + fun fetchProfile(c: Context, asUri: HttpUrl, user: String, password: String): Response? { + + + val httpClient = buildHttpClient(c, user, password, asUri.host() ?: "") + + val request = Request.Builder() + .url(asUri) + .build() + + val response = httpClient.newCall(request).execute() + + return response + + } + + private fun getAsUrl(url: String, autologin: Boolean): HttpUrl { + var asurl = url + if (!asurl.startsWith("http")) + asurl = "https://" + asurl + + if (autologin) + asurl += "/rest/GetAutologin?tls-cryptv2=1" + else + asurl += "/rest/GetUserlogin?tls-cryptv2=1" + + val asUri = HttpUrl.parse(asurl) + return asUri + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return dialogView + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val inflater = requireActivity().layoutInflater + dialogView = inflater.inflate(R.layout.import_remote_config, null); + + val builder = AlertDialog.Builder(requireContext()) + + builder.setView(dialogView) + builder.setTitle(R.string.import_from_as) + + asServername = dialogView.findViewById(R.id.as_servername) + asUsername = dialogView.findViewById(R.id.username) + asPassword = dialogView.findViewById(R.id.password) + asUseAutologin = dialogView.findViewById(R.id.request_autologin) + + importChoiceGroup = dialogView.findViewById(R.id.import_source_group) + importChoiceAS = dialogView.findViewById(R.id.import_choice_as) + + importChoiceGroup.setOnCheckedChangeListener { group, checkedId -> + if (checkedId == R.id.import_choice_as) + asServername.setHint(R.string.as_servername) + else + asServername.setHint(R.string.server_url) + } + + builder.setPositiveButton(R.string.import_config, null) + builder.setNegativeButton(android.R.string.cancel) { _, _ -> } + + if (arguments?.getString("url") != null) + { + asServername.setText(arguments?.getString("url")) + importChoiceGroup.check(R.id.import_choice_url) + } + + val dialog = builder.create() + + return dialog + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + dialog!!.setOnShowListener() { d2 -> + val d: AlertDialog = d2 as AlertDialog + + d.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener() + + { _ -> + viewLifecycleOwner.lifecycleScope.launch { + doAsImport(asUsername.text.toString(), asPassword.text.toString()) + } + } + } + } + + val crvMessage = Pattern.compile(".*CRV1:R,E:(.*):(.*):(.*).*", Pattern.DOTALL) + + suspend internal fun doAsImport(user: String, password: String) { + var pleaseWait:AlertDialog? + withContext(Dispatchers.IO) + { + + withContext(Dispatchers.Main) + { + val ab = AlertDialog.Builder(requireContext()) + ab.setTitle("Downloading profile") + ab.setMessage("Please wait") + pleaseWait = ab.show() + + Toast.makeText(context, "Downloading profile", Toast.LENGTH_LONG).show() + } + + val asProfileUri:HttpUrl + if (importChoiceAS.isChecked) + asProfileUri = getAsUrl(asServername.text.toString(), asUseAutologin.isChecked) + else + asProfileUri = HttpUrl.parse(asServername.text.toString()) + + var e: Exception? = null + try { + val response = fetchProfile(requireContext(), asProfileUri, user, password) + + if (response == null) { + throw Exception("No Response from Server") + } + + val profile = response.body().string() + if (response.code() == 401 && crvMessage.matcher(profile).matches()) { + withContext(Dispatchers.Main) { + pleaseWait?.dismiss() + showCRDialog(profile) + } + } else if (response.isSuccessful) { + + withContext(Dispatchers.Main) { + pleaseWait?.dismiss() + val startImport = Intent(activity, ConfigConverter::class.java) + startImport.action = ConfigConverter.IMPORT_PROFILE_DATA + startImport.putExtra(Intent.EXTRA_TEXT, profile) + startActivity(startImport) + dismiss() + } + } else { + throw Exception("Invalid Response from server: \n${response.code()} ${response.message()} \n\n ${profile}") + } + + } catch (ce: SSLHandshakeException) { + e = ce + // Find out if we are in the non trust path + if (ce.cause is CertificateException && ce.cause != null) { + val certExp: CertificateException = (ce.cause as CertificateException) + if (certExp.cause is CertPathValidatorException && certExp.cause != null) { + val caPathExp: CertPathValidatorException = certExp.cause as CertPathValidatorException + if (caPathExp.certPath.type.equals("X.509") && caPathExp.certPath.certificates.size > 0) { + val firstCert: X509Certificate = (caPathExp.certPath.certificates[0] as X509Certificate) + + val fpBytes = MessageDigest.getInstance("SHA-256").digest(firstCert.publicKey.encoded) + val fp = Base64.encodeToString(fpBytes, NO_WRAP) + + + + Log.i("OpenVPN", "Found cert with FP ${fp}: ${firstCert.subjectDN}") + withContext(Dispatchers.Main) { + + pleaseWait?.dismiss() + + AlertDialog.Builder(requireContext()) + .setTitle("Untrusted certificate found") + .setMessage(firstCert.toString()) + .setPositiveButton("Trust") { _, _ -> addPinnedCert(requireContext(), asProfileUri.host(), fp) } + .setNegativeButton("Do not trust", null) + .show() + } + e = null + } + } else if (ce.message != null && ce.message!!.contains("Certificate pinning failure")) { + withContext(Dispatchers.Main) { + pleaseWait?.dismiss() + + AlertDialog.Builder(requireContext()) + .setTitle("Different certificate than trusted certificate from server") + .setMessage(ce.message) + .setNegativeButton(android.R.string.ok, null) + .setPositiveButton("Forget pinned certificate", { _, _ -> removedPinnedCert(requireContext(), asProfileUri.host()) }) + .show(); + } + e = null + + } + } + } catch (ge: Exception) { + e = ge + } + if (e != null) { + withContext(Dispatchers.Main) { + pleaseWait?.dismiss() + AlertDialog.Builder(requireContext()) + .setTitle("Import failed") + .setMessage("Error: " + e.localizedMessage) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + } + } + + private fun showCRDialog(response: String) { + // This is a dirty hack instead of properly parsing the response + val m = crvMessage.matcher(response) + // We already know that it matches + m.matches() + val challenge = m.group(1) + var username = m.group(2) + val message = m.group(3) + + username = String(Base64.decode(username, Base64.DEFAULT)) + + val pwprefix = "CRV1::${challenge}::" + + val entry = EditText(context) + entry.setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD) + + AlertDialog.Builder(requireContext()) + .setTitle("Server request challenge/response authentication") + .setMessage("Challenge: " + message) + .setView(entry) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.import_config) { _,_ -> + viewLifecycleOwner.lifecycleScope.launch { + doAsImport(username, pwprefix + entry.text.toString()) + } + } + .show() + + } + + + override fun onResume() { + super.onResume() + if (arguments == null) { + asServername.setText( + Preferences.getDefaultSharedPreferences(activity).getString("as-hostname", "") + ) + asUsername.setText( + Preferences.getDefaultSharedPreferences(activity).getString("as-username", "") + ) + if (Preferences.getDefaultSharedPreferences(activity).getBoolean("as-selected", true)) { + importChoiceGroup.check(R.id.import_choice_as) + } else { + importChoiceGroup.check(R.id.import_choice_url) + } + + } + } + + override fun onPause() { + super.onPause() + val prefs = Preferences.getDefaultSharedPreferences(activity) + val editor = prefs.edit() + editor.putString("as-hostname", asServername.text.toString()) + editor.putString("as-username", asUsername.text.toString()) + editor.putBoolean("as-selected", importChoiceAS.isChecked) + editor.apply() + } + + companion object { + @JvmStatic + fun newInstance(url:String? = null): ImportRemoteConfig { + val frag = ImportRemoteConfig() + if (url != null) + { + val extras = Bundle() + extras.putString("url", url) + frag.arguments = extras + } + return frag + } + } +} diff --git a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java index 3804926f..ce6fa7f1 100644 --- a/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java +++ b/main/src/ui/java/de/blinkt/openvpn/fragments/VPNProfileList.java @@ -21,7 +21,6 @@ import android.os.Bundle; import android.os.PersistableBundle; import androidx.annotation.RequiresApi; -import androidx.fragment.app.DialogFragment; import androidx.fragment.app.ListFragment; import android.text.Html; @@ -95,7 +94,7 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn private boolean showUserRequestDialogIfNeeded(ConnectionStatus level, Intent intent) { if (level == LEVEL_WAITING_FOR_USER_INPUT) { - if (intent.getStringExtra(EXTRA_CHALLENGE_TXT) != null) { + if (intent != null && intent.getStringExtra(EXTRA_CHALLENGE_TXT) != null) { PasswordDialogFragment pwInputFrag = PasswordDialogFragment.Companion.newInstance(intent, false); pwInputFrag.show(getParentFragmentManager(), "dialog"); @@ -126,6 +125,7 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); + setListAdapter(); } @RequiresApi(api = Build.VERSION_CODES.N_MR1) @@ -277,12 +277,6 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn } - @Override - public void onActivityCreated(Bundle savedInstanceState) { - super.onActivityCreated(savedInstanceState); - setListAdapter(); - } - private void setListAdapter() { if (mArrayadapter == null) { mArrayadapter = new VPNArrayAdapter(getActivity(), R.layout.vpn_list_item, R.id.vpn_item_title); @@ -354,8 +348,8 @@ public class VPNProfileList extends ListFragment implements OnClickListener, Vpn } private boolean startASProfileImport() { - ImportASConfig asImportFrag = ImportASConfig.newInstance(); - asImportFrag.show(requireFragmentManager(), "dialog"); + ImportRemoteConfig asImportFrag = ImportRemoteConfig.newInstance(null); + asImportFrag.show(getParentFragmentManager(), "dialog"); return true; } -- cgit v1.2.3