Implement optional mTLS via client certificate option
This commit is contained in:
@@ -1,3 +1,8 @@
|
||||
package com.github.gotify
|
||||
|
||||
internal class SSLSettings(val validateSSL: Boolean, val cert: String?)
|
||||
internal class SSLSettings(
|
||||
val validateSSL: Boolean,
|
||||
val caCertPath: String?,
|
||||
val clientCertPath: String?,
|
||||
val clientCertPassword: String?
|
||||
)
|
||||
|
||||
@@ -26,12 +26,21 @@ internal class Settings(context: Context) {
|
||||
var serverVersion: String
|
||||
get() = sharedPreferences.getString("version", "UNKNOWN")!!
|
||||
set(value) = sharedPreferences.edit().putString("version", value).apply()
|
||||
var cert: String?
|
||||
get() = sharedPreferences.getString("cert", null)
|
||||
set(value) = sharedPreferences.edit().putString("cert", value).apply()
|
||||
var caCertPath: String?
|
||||
get() = sharedPreferences.getString("caCertPath", null)
|
||||
set(value) = sharedPreferences.edit().putString("caCertPath", value).apply()
|
||||
var caCertCN: String?
|
||||
get() = sharedPreferences.getString("caCertCN", null)
|
||||
set(value) = sharedPreferences.edit().putString("caCertCN", value).apply()
|
||||
var validateSSL: Boolean
|
||||
get() = sharedPreferences.getBoolean("validateSSL", true)
|
||||
set(value) = sharedPreferences.edit().putBoolean("validateSSL", value).apply()
|
||||
var clientCertPath: String?
|
||||
get() = sharedPreferences.getString("clientCertPath", null)
|
||||
set(value) = sharedPreferences.edit().putString("clientCertPath", value).apply()
|
||||
var clientCertPassword: String?
|
||||
get() = sharedPreferences.getString("clientCertPass", null)
|
||||
set(value) = sharedPreferences.edit().putString("clientCertPass", value).apply()
|
||||
|
||||
init {
|
||||
sharedPreferences = context.getSharedPreferences("gotify", Context.MODE_PRIVATE)
|
||||
@@ -43,7 +52,10 @@ internal class Settings(context: Context) {
|
||||
url = ""
|
||||
token = null
|
||||
validateSSL = true
|
||||
cert = null
|
||||
caCertPath = null
|
||||
caCertCN = null
|
||||
clientCertPath = null
|
||||
clientCertPassword = null
|
||||
}
|
||||
|
||||
fun setUser(name: String?, admin: Boolean) {
|
||||
@@ -51,6 +63,11 @@ internal class Settings(context: Context) {
|
||||
}
|
||||
|
||||
fun sslSettings(): SSLSettings {
|
||||
return SSLSettings(validateSSL, cert)
|
||||
return SSLSettings(
|
||||
validateSSL,
|
||||
caCertPath,
|
||||
clientCertPath,
|
||||
clientCertPassword
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,17 @@ package com.github.gotify.api
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import com.github.gotify.SSLSettings
|
||||
import com.github.gotify.Utils
|
||||
import java.io.IOException
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.InputStream
|
||||
import java.security.GeneralSecurityException
|
||||
import java.security.KeyStore
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.Certificate
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.KeyManager
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
import javax.net.ssl.SSLContext
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
@@ -18,6 +21,9 @@ import okhttp3.OkHttpClient
|
||||
import org.tinylog.kotlin.Logger
|
||||
|
||||
internal object CertUtils {
|
||||
const val CA_CERT_NAME = "ca-cert.crt"
|
||||
const val CLIENT_CERT_NAME = "client-cert.p12"
|
||||
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
private val trustAll = object : X509TrustManager {
|
||||
@SuppressLint("TrustAllX509TrustManager")
|
||||
@@ -31,10 +37,10 @@ internal object CertUtils {
|
||||
override fun getAcceptedIssuers() = arrayOf<X509Certificate>()
|
||||
}
|
||||
|
||||
fun parseCertificate(cert: String): Certificate {
|
||||
fun parseCertificate(inputStream: InputStream): Certificate {
|
||||
try {
|
||||
val certificateFactory = CertificateFactory.getInstance("X509")
|
||||
return certificateFactory.generateCertificate(Utils.stringToInputStream(cert))
|
||||
return certificateFactory.generateCertificate(inputStream)
|
||||
} catch (e: Exception) {
|
||||
throw IllegalArgumentException("certificate is invalid")
|
||||
}
|
||||
@@ -43,25 +49,37 @@ internal object CertUtils {
|
||||
fun applySslSettings(builder: OkHttpClient.Builder, settings: SSLSettings) {
|
||||
// Modified from ApiClient.applySslSettings in the client package.
|
||||
try {
|
||||
if (!settings.validateSSL) {
|
||||
val context = SSLContext.getInstance("TLS")
|
||||
context.init(arrayOf(), arrayOf<TrustManager>(trustAll), SecureRandom())
|
||||
builder.sslSocketFactory(context.socketFactory, trustAll)
|
||||
builder.hostnameVerifier { _, _ -> true }
|
||||
return
|
||||
var customManagers = false
|
||||
var trustManagers: Array<TrustManager>? = null
|
||||
var keyManagers: Array<KeyManager>? = null
|
||||
if (settings.caCertPath != null) {
|
||||
val tempTrustManagers = certToTrustManager(settings.caCertPath)
|
||||
if (tempTrustManagers.isNotEmpty()) {
|
||||
trustManagers = tempTrustManagers
|
||||
customManagers = true
|
||||
}
|
||||
val cert = settings.cert
|
||||
if (cert != null) {
|
||||
val trustManagers = certToTrustManager(cert)
|
||||
if (trustManagers.isNotEmpty()) {
|
||||
}
|
||||
if (settings.clientCertPath != null) {
|
||||
val tempKeyManagers = certToKeyManager(
|
||||
settings.clientCertPath,
|
||||
settings.clientCertPassword
|
||||
)
|
||||
if (tempKeyManagers.isNotEmpty()) {
|
||||
keyManagers = tempKeyManagers
|
||||
}
|
||||
}
|
||||
if (!settings.validateSSL) {
|
||||
trustManagers = arrayOf(trustAll)
|
||||
builder.hostnameVerifier { _, _ -> true }
|
||||
}
|
||||
if (customManagers || !settings.validateSSL) {
|
||||
val context = SSLContext.getInstance("TLS")
|
||||
context.init(arrayOf(), trustManagers, SecureRandom())
|
||||
context.init(keyManagers, trustManagers, SecureRandom())
|
||||
builder.sslSocketFactory(
|
||||
context.socketFactory,
|
||||
trustManagers[0] as X509TrustManager
|
||||
trustManagers!![0] as X509TrustManager
|
||||
)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// We shouldn't have issues since the cert is verified on login.
|
||||
Logger.error(e, "Failed to apply SSL settings")
|
||||
@@ -69,12 +87,13 @@ internal object CertUtils {
|
||||
}
|
||||
|
||||
@Throws(GeneralSecurityException::class)
|
||||
private fun certToTrustManager(cert: String): Array<TrustManager> {
|
||||
private fun certToTrustManager(certPath: String): Array<TrustManager> {
|
||||
val certificateFactory = CertificateFactory.getInstance("X.509")
|
||||
val certificates = certificateFactory.generateCertificates(Utils.stringToInputStream(cert))
|
||||
val inputStream = FileInputStream(File(certPath))
|
||||
val certificates = certificateFactory.generateCertificates(inputStream)
|
||||
require(certificates.isNotEmpty()) { "expected non-empty set of trusted certificates" }
|
||||
|
||||
val caKeyStore = newEmptyKeyStore()
|
||||
val caKeyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply { load(null) }
|
||||
certificates.forEachIndexed { index, certificate ->
|
||||
val certificateAlias = "ca$index"
|
||||
caKeyStore.setCertificateEntry(certificateAlias, certificate)
|
||||
@@ -86,13 +105,15 @@ internal object CertUtils {
|
||||
}
|
||||
|
||||
@Throws(GeneralSecurityException::class)
|
||||
private fun newEmptyKeyStore(): KeyStore {
|
||||
return try {
|
||||
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
|
||||
keyStore.load(null, null)
|
||||
keyStore
|
||||
} catch (e: IOException) {
|
||||
throw AssertionError(e)
|
||||
}
|
||||
private fun certToKeyManager(certPath: String, certPassword: String?): Array<KeyManager> {
|
||||
require(certPassword != null) { "empty client certificate password" }
|
||||
|
||||
val keyStore = KeyStore.getInstance("PKCS12")
|
||||
val inputStream = FileInputStream(File(certPath))
|
||||
keyStore.load(inputStream, certPassword.toCharArray())
|
||||
val keyManagerFactory =
|
||||
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
|
||||
keyManagerFactory.init(keyStore, certPassword.toCharArray())
|
||||
return keyManagerFactory.keyManagers
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ internal class AdvancedDialog(
|
||||
private var onCheckedChangeListener: CompoundButton.OnCheckedChangeListener? = null
|
||||
private lateinit var onClickSelectCaCertificate: Runnable
|
||||
private lateinit var onClickRemoveCaCertificate: Runnable
|
||||
private lateinit var onClickSelectClientCertificate: Runnable
|
||||
private lateinit var onClickRemoveClientCertificate: Runnable
|
||||
private lateinit var onClose: (password: String) -> Unit
|
||||
|
||||
fun onDisableSSLChanged(
|
||||
onCheckedChangeListener: CompoundButton.OnCheckedChangeListener?
|
||||
@@ -33,35 +36,82 @@ internal class AdvancedDialog(
|
||||
return this
|
||||
}
|
||||
|
||||
fun show(disableSSL: Boolean, selectedCertificate: String?): AdvancedDialog {
|
||||
fun onClickSelectClientCertificate(onClickSelectClientCertificate: Runnable): AdvancedDialog {
|
||||
this.onClickSelectClientCertificate = onClickSelectClientCertificate
|
||||
return this
|
||||
}
|
||||
|
||||
fun onClickRemoveClientCertificate(onClickRemoveClientCertificate: Runnable): AdvancedDialog {
|
||||
this.onClickRemoveClientCertificate = onClickRemoveClientCertificate
|
||||
return this
|
||||
}
|
||||
|
||||
fun onClose(onClose: (passworrd: String) -> Unit): AdvancedDialog {
|
||||
this.onClose = onClose
|
||||
return this
|
||||
}
|
||||
|
||||
fun show(
|
||||
disableSSL: Boolean,
|
||||
caCertPath: String? = null,
|
||||
caCertCN: String?,
|
||||
clientCertPath: String? = null,
|
||||
clientCertPassword: String?
|
||||
): AdvancedDialog {
|
||||
binding = AdvancedSettingsDialogBinding.inflate(layoutInflater)
|
||||
binding.disableSSL.isChecked = disableSSL
|
||||
binding.disableSSL.setOnCheckedChangeListener(onCheckedChangeListener)
|
||||
if (selectedCertificate == null) {
|
||||
showSelectCACertificate()
|
||||
if (caCertPath == null) {
|
||||
showSelectCaCertificate()
|
||||
} else {
|
||||
showRemoveCACertificate(selectedCertificate)
|
||||
showRemoveCaCertificate(caCertCN!!)
|
||||
}
|
||||
if (clientCertPath == null) {
|
||||
showSelectClientCertificate()
|
||||
} else {
|
||||
showRemoveClientCertificate()
|
||||
}
|
||||
if (!clientCertPassword.isNullOrEmpty()) {
|
||||
binding.clientCertPasswordEdittext.setText(clientCertPassword)
|
||||
}
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setView(binding.root)
|
||||
.setTitle(R.string.advanced_settings)
|
||||
.setPositiveButton(context.getString(R.string.done), null)
|
||||
.setOnDismissListener {
|
||||
onClose(binding.clientCertPasswordEdittext.text.toString())
|
||||
}
|
||||
.show()
|
||||
return this
|
||||
}
|
||||
|
||||
private fun showSelectCACertificate() {
|
||||
private fun showSelectCaCertificate() {
|
||||
binding.toggleCaCert.setText(R.string.select_ca_certificate)
|
||||
binding.toggleCaCert.setOnClickListener { onClickSelectCaCertificate.run() }
|
||||
binding.selecetedCaCert.setText(R.string.no_certificate_selected)
|
||||
binding.selectedCaCert.setText(R.string.no_certificate_selected)
|
||||
}
|
||||
|
||||
fun showRemoveCACertificate(certificate: String) {
|
||||
fun showRemoveCaCertificate(certificateCN: String) {
|
||||
binding.toggleCaCert.setText(R.string.remove_ca_certificate)
|
||||
binding.toggleCaCert.setOnClickListener {
|
||||
showSelectCACertificate()
|
||||
showSelectCaCertificate()
|
||||
onClickRemoveCaCertificate.run()
|
||||
}
|
||||
binding.selecetedCaCert.text = certificate
|
||||
binding.selectedCaCert.text = certificateCN
|
||||
}
|
||||
|
||||
private fun showSelectClientCertificate() {
|
||||
binding.toggleClientCert.setText(R.string.select_client_certificate)
|
||||
binding.toggleClientCert.setOnClickListener { onClickSelectClientCertificate.run() }
|
||||
binding.selectedClientCert.setText(R.string.no_certificate_selected)
|
||||
}
|
||||
|
||||
fun showRemoveClientCertificate() {
|
||||
binding.toggleClientCert.setText(R.string.remove_client_certificate)
|
||||
binding.toggleClientCert.setOnClickListener {
|
||||
showSelectClientCertificate()
|
||||
onClickRemoveClientCertificate.run()
|
||||
}
|
||||
binding.selectedClientCert.setText(R.string.certificate_found)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.github.gotify.R
|
||||
import com.github.gotify.SSLSettings
|
||||
@@ -31,6 +33,10 @@ import com.github.gotify.log.LogsActivity
|
||||
import com.github.gotify.log.UncaughtExceptionHandler
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import java.security.cert.X509Certificate
|
||||
import okhttp3.HttpUrl
|
||||
import org.tinylog.kotlin.Logger
|
||||
@@ -40,10 +46,13 @@ internal class LoginActivity : AppCompatActivity() {
|
||||
private lateinit var settings: Settings
|
||||
|
||||
private var disableSslValidation = false
|
||||
private var caCertContents: String? = null
|
||||
private var caCertCN: String? = null
|
||||
private var caCertPath: String? = null
|
||||
private var clientCertPath: String? = null
|
||||
private var clientCertPassword: String? = null
|
||||
private lateinit var advancedDialog: AdvancedDialog
|
||||
|
||||
private val certificateDialogResultLauncher =
|
||||
private val caDialogResultLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
try {
|
||||
require(result.resultCode == RESULT_OK) { "result was ${result.resultCode}" }
|
||||
@@ -52,18 +61,38 @@ internal class LoginActivity : AppCompatActivity() {
|
||||
val uri = result.data!!.data ?: throw IllegalArgumentException("file path was null")
|
||||
val fileStream = contentResolver.openInputStream(uri)
|
||||
?: throw IllegalArgumentException("file path was invalid")
|
||||
val destinationFile = File(filesDir, CertUtils.CA_CERT_NAME)
|
||||
copyStreamToFile(fileStream, destinationFile)
|
||||
|
||||
val content = Utils.readFileFromStream(fileStream)
|
||||
val name = getNameOfCertContent(content)
|
||||
|
||||
// temporarily set the contents (don't store to settings until they decide to login)
|
||||
caCertContents = content
|
||||
advancedDialog.showRemoveCACertificate(name)
|
||||
// temporarily store it (don't store to settings until they decide to login)
|
||||
caCertCN = getNameOfCertContent(destinationFile)!!
|
||||
caCertPath = destinationFile.absolutePath
|
||||
advancedDialog.showRemoveCaCertificate(caCertCN!!)
|
||||
} catch (e: Exception) {
|
||||
Utils.showSnackBar(this, getString(R.string.select_ca_failed, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
private val clientCertDialogResultLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
try {
|
||||
require(result.resultCode == RESULT_OK) { "result was ${result.resultCode}" }
|
||||
requireNotNull(result.data) { "file path was null" }
|
||||
|
||||
val uri = result.data!!.data ?: throw IllegalArgumentException("file path was null")
|
||||
val fileStream = contentResolver.openInputStream(uri)
|
||||
?: throw IllegalArgumentException("file path was invalid")
|
||||
val destinationFile = File(filesDir, CertUtils.CLIENT_CERT_NAME)
|
||||
copyStreamToFile(fileStream, destinationFile)
|
||||
|
||||
// temporarily store it (don't store to settings until they decide to login)
|
||||
clientCertPath = destinationFile.absolutePath
|
||||
advancedDialog.showRemoveClientCertificate()
|
||||
} catch (e: Exception) {
|
||||
Utils.showSnackBar(this, getString(R.string.select_client_failed, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
UncaughtExceptionHandler.registerCurrentThread()
|
||||
@@ -140,12 +169,6 @@ internal class LoginActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun toggleShowAdvanced() {
|
||||
val selectedCertName = if (caCertContents != null) {
|
||||
getNameOfCertContent(caCertContents!!)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
advancedDialog = AdvancedDialog(this, layoutInflater)
|
||||
.onDisableSSLChanged { _, disable ->
|
||||
invalidateUrl()
|
||||
@@ -153,33 +176,51 @@ internal class LoginActivity : AppCompatActivity() {
|
||||
}
|
||||
.onClickSelectCaCertificate {
|
||||
invalidateUrl()
|
||||
doSelectCACertificate()
|
||||
doSelectCertificate(caDialogResultLauncher, R.string.select_ca_file)
|
||||
}
|
||||
.onClickRemoveCaCertificate {
|
||||
invalidateUrl()
|
||||
caCertContents = null
|
||||
caCertPath = null
|
||||
}
|
||||
.show(disableSslValidation, selectedCertName)
|
||||
.onClickSelectClientCertificate {
|
||||
invalidateUrl()
|
||||
doSelectCertificate(clientCertDialogResultLauncher, R.string.select_client_file)
|
||||
}
|
||||
.onClickRemoveClientCertificate {
|
||||
invalidateUrl()
|
||||
clientCertPath = null
|
||||
}
|
||||
.onClose { newPassword ->
|
||||
clientCertPassword = newPassword
|
||||
}
|
||||
.show(
|
||||
disableSslValidation,
|
||||
caCertPath,
|
||||
caCertCN,
|
||||
clientCertPath,
|
||||
clientCertPassword
|
||||
)
|
||||
}
|
||||
|
||||
private fun doSelectCACertificate() {
|
||||
private fun doSelectCertificate(
|
||||
resultLauncher: ActivityResultLauncher<Intent>,
|
||||
@StringRes descriptionId: Int
|
||||
) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
// we don't really care what kind of file it is as long as we can parse it
|
||||
intent.type = "*/*"
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
|
||||
try {
|
||||
certificateDialogResultLauncher.launch(
|
||||
Intent.createChooser(intent, getString(R.string.select_ca_file))
|
||||
)
|
||||
resultLauncher.launch(Intent.createChooser(intent, getString(descriptionId)))
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
// case for user not having a file browser installed
|
||||
Utils.showSnackBar(this, getString(R.string.please_install_file_browser))
|
||||
}
|
||||
}
|
||||
|
||||
private fun getNameOfCertContent(content: String): String {
|
||||
val ca = CertUtils.parseCertificate(content)
|
||||
private fun getNameOfCertContent(file: File): String? {
|
||||
val ca = CertUtils.parseCertificate(FileInputStream(file))
|
||||
return (ca as X509Certificate).subjectDN.name
|
||||
}
|
||||
|
||||
@@ -265,7 +306,10 @@ internal class LoginActivity : AppCompatActivity() {
|
||||
private fun onCreatedClient(client: Client) {
|
||||
settings.token = client.token
|
||||
settings.validateSSL = !disableSslValidation
|
||||
settings.cert = caCertContents
|
||||
settings.caCertCN = caCertCN
|
||||
settings.caCertPath = caCertPath
|
||||
settings.clientCertPath = clientCertPath
|
||||
settings.clientCertPassword = clientCertPassword
|
||||
|
||||
Utils.showSnackBar(this, getString(R.string.created_client))
|
||||
startActivity(Intent(this, InitializationActivity::class.java))
|
||||
@@ -288,6 +332,17 @@ internal class LoginActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private fun tempSslSettings(): SSLSettings {
|
||||
return SSLSettings(!disableSslValidation, caCertContents)
|
||||
return SSLSettings(
|
||||
!disableSslValidation,
|
||||
caCertPath,
|
||||
clientCertPath,
|
||||
clientCertPassword
|
||||
)
|
||||
}
|
||||
|
||||
private fun copyStreamToFile(inputStream: InputStream, file: File) {
|
||||
FileOutputStream(file).use {
|
||||
inputStream.copyTo(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical" android:layout_width="match_parent"
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="20dp">
|
||||
|
||||
@@ -18,8 +20,47 @@
|
||||
android:text="@string/select_ca_certificate" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/seleceted_ca_cert"
|
||||
android:id="@+id/selected_ca_cert"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_certificate_selected" />
|
||||
|
||||
<com.google.android.material.divider.MaterialDivider
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/toggle_client_cert"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:textColor="@android:color/white"
|
||||
android:text="@string/select_client_certificate" />
|
||||
|
||||
<com.google.android.material.textview.MaterialTextView
|
||||
android:id="@+id/selected_client_cert"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/no_certificate_selected" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/client_cert_password"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
android:maxWidth="280dp"
|
||||
android:ems="10">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/client_cert_password_edittext"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:singleLine="true"
|
||||
android:inputType="textPassword"
|
||||
android:importantForAutofill="no"
|
||||
android:hint="@string/client_cert_password" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -40,9 +40,14 @@
|
||||
<string name="password">Password</string>
|
||||
<string name="disabled_validate_ssl">Disable SSL Validation</string>
|
||||
<string name="select_ca_certificate">Select CA Certificate</string>
|
||||
<string name="select_ca_file">Select a Certificate File</string>
|
||||
<string name="select_client_certificate">Select Client Certificate (PKCS#12)</string>
|
||||
<string name="select_ca_file">Select a CA Certificate File</string>
|
||||
<string name="select_client_file">Select a Client Certificate File</string>
|
||||
<string name="client_cert_password">Certificate Password</string>
|
||||
<string name="please_install_file_browser">Please install a file browser</string>
|
||||
<string name="select_ca_failed">Failed to read CA: %s</string>
|
||||
<string name="select_ca_failed">Failed to read CA cert: %s</string>
|
||||
<string name="select_client_failed">Failed to read client cert: %s</string>
|
||||
<string name="certificate_found">Certificate found</string>
|
||||
<string name="login">Login</string>
|
||||
<string name="check_url">Check URL</string>
|
||||
<string name="permissions_dialog_grant">Grant</string>
|
||||
@@ -65,6 +70,7 @@
|
||||
<string name="done">Done</string>
|
||||
<string name="no_certificate_selected">No certificate selected</string>
|
||||
<string name="remove_ca_certificate">Remove CA Certificate</string>
|
||||
<string name="remove_client_certificate">Remove Client Certificate</string>
|
||||
<string name="warning">Warning</string>
|
||||
<string name="http_warning">Using HTTP is insecure and it\'s recommend to use HTTPS instead. Use your favorite search engine to get more information about this topic.</string>
|
||||
<string name="i_understand">I Understand</string>
|
||||
|
||||
Reference in New Issue
Block a user