Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/src/main/kotlin/com/github/gotify/SSLSettings.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
package com.github.gotify
Comment thread
cyb3rko marked this conversation as resolved.

internal class SSLSettings(val validateSSL: Boolean, val cert: String?)
internal class SSLSettings(
val validateSSL: Boolean,
val caCertPath: String?,
val clientCertPath: String?,
val clientCertPassword: String?
)
27 changes: 22 additions & 5 deletions app/src/main/kotlin/com/github/gotify/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,21 @@ internal class Settings(context: Context) {
var serverVersion: String
Comment thread
cyb3rko marked this conversation as resolved.
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()
Comment thread
jmattheis marked this conversation as resolved.
Outdated
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)
Expand All @@ -43,14 +52,22 @@ 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) {
sharedPreferences.edit().putString("username", name).putBoolean("admin", admin).apply()
}

fun sslSettings(): SSLSettings {
return SSLSettings(validateSSL, cert)
return SSLSettings(
validateSSL,
caCertPath,
clientCertPath,
clientCertPassword
)
}
}
24 changes: 0 additions & 24 deletions app/src/main/kotlin/com/github/gotify/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson
import com.squareup.picasso.Picasso.LoadedFrom
import com.squareup.picasso.Target
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.net.MalformedURLException
import java.net.URI
import java.net.URISyntaxException
Expand All @@ -28,7 +24,6 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import okio.Buffer
import org.threeten.bp.OffsetDateTime
import org.tinylog.kotlin.Logger

Expand Down Expand Up @@ -86,25 +81,6 @@ internal object Utils {
}
}

fun readFileFromStream(inputStream: InputStream): String {
val sb = StringBuilder()
var currentLine: String?
try {
BufferedReader(InputStreamReader(inputStream)).use { reader ->
while (reader.readLine().also { currentLine = it } != null) {
sb.append(currentLine).append("\n")
}
}
} catch (e: IOException) {
throw IllegalArgumentException("failed to read input")
}
return sb.toString()
}

fun stringToInputStream(str: String?): InputStream? {
return if (str == null) null else Buffer().writeUtf8(str).inputStream()
}

fun AppCompatActivity.launchCoroutine(
dispatcher: CoroutineDispatcher = Dispatchers.IO,
action: suspend (coroutineScope: CoroutineScope) -> Unit
Expand Down
81 changes: 51 additions & 30 deletions app/src/main/kotlin/com/github/gotify/api/CertUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
Expand All @@ -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")
}
Expand All @@ -43,24 +49,36 @@ internal object CertUtils {
fun applySslSettings(builder: OkHttpClient.Builder, settings: SSLSettings) {
// Modified from ApiClient.applySslSettings in the client package.
try {
var customManagers = false
Comment thread
cyb3rko marked this conversation as resolved.
Outdated
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
}
}
if (settings.clientCertPath != null) {
val tempKeyManagers = certToKeyManager(
settings.clientCertPath,
settings.clientCertPassword
)
if (tempKeyManagers.isNotEmpty()) {
keyManagers = tempKeyManagers
}
}
if (!settings.validateSSL) {
val context = SSLContext.getInstance("TLS")
context.init(arrayOf(), arrayOf<TrustManager>(trustAll), SecureRandom())
builder.sslSocketFactory(context.socketFactory, trustAll)
trustManagers = arrayOf(trustAll)
builder.hostnameVerifier { _, _ -> true }
return
}
val cert = settings.cert
if (cert != null) {
val trustManagers = certToTrustManager(cert)
if (trustManagers.isNotEmpty()) {
val context = SSLContext.getInstance("TLS")
context.init(arrayOf(), trustManagers, SecureRandom())
builder.sslSocketFactory(
context.socketFactory,
trustManagers[0] as X509TrustManager
)
}
if (customManagers || !settings.validateSSL) {
Comment thread
cyb3rko marked this conversation as resolved.
Outdated
val context = SSLContext.getInstance("TLS")
context.init(keyManagers, trustManagers, SecureRandom())
builder.sslSocketFactory(
context.socketFactory,
trustManagers!![0] as X509TrustManager
)
}
} catch (e: Exception) {
// We shouldn't have issues since the cert is verified on login.
Expand All @@ -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)
Comment thread
cyb3rko marked this conversation as resolved.
Outdated
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)
Expand All @@ -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")
Comment thread
cyb3rko marked this conversation as resolved.
val inputStream = FileInputStream(File(certPath))
keyStore.load(inputStream, certPassword.toCharArray())
val keyManagerFactory =
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, certPassword.toCharArray())
return keyManagerFactory.keyManagers
}
}
68 changes: 59 additions & 9 deletions app/src/main/kotlin/com/github/gotify/login/AdvancedDialog.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand All @@ -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 {
Comment thread
cyb3rko marked this conversation as resolved.
Outdated
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(caCertCN!!)
}
if (clientCertPath == null) {
showSelectClientCertificate()
} else {
showRemoveCACertificate(selectedCertificate)
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)
}
}
Loading