Remove CA support, make trusted certs pinned to hostname
This commit is contained in:
parent
eb11eec984
commit
4304004a17
11 changed files with 498 additions and 365 deletions
|
|
@ -2,7 +2,7 @@
|
|||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 16,
|
||||
"identityHash": "1e2fa6a6cd2ed5146905f38f71f3c904",
|
||||
"identityHash": "af6e656e277e4390d3ebbbca1c4bb845",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "Subscription",
|
||||
|
|
@ -362,8 +362,14 @@
|
|||
},
|
||||
{
|
||||
"tableName": "TrustedCertificate",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`fingerprint` TEXT NOT NULL, `pem` TEXT NOT NULL, PRIMARY KEY(`fingerprint`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`baseUrl` TEXT NOT NULL, `fingerprint` TEXT NOT NULL, `pem` TEXT NOT NULL, PRIMARY KEY(`baseUrl`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "baseUrl",
|
||||
"columnName": "baseUrl",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fingerprint",
|
||||
"columnName": "fingerprint",
|
||||
|
|
@ -380,7 +386,7 @@
|
|||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"fingerprint"
|
||||
"baseUrl"
|
||||
]
|
||||
}
|
||||
},
|
||||
|
|
@ -417,7 +423,7 @@
|
|||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1e2fa6a6cd2ed5146905f38f71f3c904')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'af6e656e277e4390d3ebbbca1c4bb845')"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -231,9 +231,9 @@ class Backuper(val context: Context) {
|
|||
try {
|
||||
val cert = CertUtil.parsePemCertificate(c.pem)
|
||||
val fingerprint = CertUtil.calculateFingerprint(cert)
|
||||
repository.addTrustedCertificate(fingerprint, c.pem)
|
||||
repository.addTrustedCertificate(c.baseUrl, fingerprint, c.pem)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Unable to restore trusted certificate: ${e.message}. Ignoring.", e)
|
||||
Log.w(TAG, "Unable to restore trusted certificate for ${c.baseUrl}: ${e.message}. Ignoring.", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -375,7 +375,10 @@ class Backuper(val context: Context) {
|
|||
|
||||
private suspend fun createTrustedCertificateList(): List<TrustedCertificateBackup> {
|
||||
return repository.getTrustedCertificates().map { trustedCert ->
|
||||
TrustedCertificateBackup(pem = trustedCert.pem)
|
||||
TrustedCertificateBackup(
|
||||
baseUrl = trustedCert.baseUrl,
|
||||
pem = trustedCert.pem
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -492,6 +495,7 @@ data class User(
|
|||
)
|
||||
|
||||
data class TrustedCertificateBackup(
|
||||
val baseUrl: String,
|
||||
val pem: String
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -190,7 +190,8 @@ data class User(
|
|||
|
||||
@Entity(tableName = "TrustedCertificate")
|
||||
data class TrustedCertificate(
|
||||
@PrimaryKey @ColumnInfo(name = "fingerprint") val fingerprint: String,
|
||||
@PrimaryKey @ColumnInfo(name = "baseUrl") val baseUrl: String,
|
||||
@ColumnInfo(name = "fingerprint") val fingerprint: String,
|
||||
@ColumnInfo(name = "pem") val pem: String
|
||||
)
|
||||
|
||||
|
|
@ -386,7 +387,7 @@ abstract class Database : RoomDatabase() {
|
|||
|
||||
private val MIGRATION_15_16 = object : Migration(15, 16) {
|
||||
override fun migrate(db: SupportSQLiteDatabase) {
|
||||
db.execSQL("CREATE TABLE TrustedCertificate (fingerprint TEXT NOT NULL, pem TEXT NOT NULL, PRIMARY KEY(fingerprint))")
|
||||
db.execSQL("CREATE TABLE TrustedCertificate (baseUrl TEXT NOT NULL, fingerprint TEXT NOT NULL, pem TEXT NOT NULL, PRIMARY KEY(baseUrl))")
|
||||
db.execSQL("CREATE TABLE ClientCertificate (baseUrl TEXT NOT NULL, p12Base64 TEXT NOT NULL, password TEXT NOT NULL, PRIMARY KEY(baseUrl))")
|
||||
}
|
||||
}
|
||||
|
|
@ -565,11 +566,14 @@ interface TrustedCertificateDao {
|
|||
@Query("SELECT * FROM TrustedCertificate")
|
||||
suspend fun list(): List<TrustedCertificate>
|
||||
|
||||
@Query("SELECT * FROM TrustedCertificate WHERE baseUrl = :baseUrl")
|
||||
suspend fun get(baseUrl: String): TrustedCertificate?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insert(cert: TrustedCertificate)
|
||||
|
||||
@Query("DELETE FROM TrustedCertificate WHERE fingerprint = :fingerprint")
|
||||
suspend fun delete(fingerprint: String)
|
||||
@Query("DELETE FROM TrustedCertificate WHERE baseUrl = :baseUrl")
|
||||
suspend fun delete(baseUrl: String)
|
||||
}
|
||||
|
||||
@Dao
|
||||
|
|
|
|||
|
|
@ -199,12 +199,16 @@ class Repository(private val sharedPrefs: SharedPreferences, database: Database)
|
|||
return trustedCertificateDao.list()
|
||||
}
|
||||
|
||||
suspend fun addTrustedCertificate(fingerprint: String, pem: String) {
|
||||
trustedCertificateDao.insert(TrustedCertificate(fingerprint, pem))
|
||||
suspend fun getTrustedCertificate(baseUrl: String): TrustedCertificate? {
|
||||
return trustedCertificateDao.get(baseUrl)
|
||||
}
|
||||
|
||||
suspend fun removeTrustedCertificate(fingerprint: String) {
|
||||
trustedCertificateDao.delete(fingerprint)
|
||||
suspend fun addTrustedCertificate(baseUrl: String, fingerprint: String, pem: String) {
|
||||
trustedCertificateDao.insert(TrustedCertificate(baseUrl, fingerprint, pem))
|
||||
}
|
||||
|
||||
suspend fun removeTrustedCertificate(baseUrl: String) {
|
||||
trustedCertificateDao.delete(baseUrl)
|
||||
}
|
||||
|
||||
// Client certificates
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ class AddFragment : DialogFragment(), TrustedCertificateFragment.TrustedCertific
|
|||
enableSubscribeView(true)
|
||||
|
||||
TrustedCertificateFragment
|
||||
.newInstanceUnknown(certificate)
|
||||
.newInstanceUnknown(certificate, baseUrl)
|
||||
.show(childFragmentManager, TrustedCertificateFragment.TAG)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -70,20 +70,19 @@ class CertificateSettingsFragment : BasePreferenceFragment(),
|
|||
try {
|
||||
val cert = CertUtil.parsePemCertificate(trustedCert.pem)
|
||||
val subject = parseCommonName(cert.subjectX500Principal.name)
|
||||
val issuer = parseCommonName(cert.issuerX500Principal.name)
|
||||
val isSelfSigned = cert.subjectX500Principal == cert.issuerX500Principal
|
||||
val isExpired = !isValid(cert)
|
||||
|
||||
val pref = Preference(preferenceScreen.context)
|
||||
pref.title = subject
|
||||
pref.summary = when {
|
||||
isSelfSigned && isExpired -> getString(R.string.settings_advanced_certificates_trusted_item_summary_ca_expired)
|
||||
isSelfSigned -> getString(R.string.settings_advanced_certificates_trusted_item_summary_ca, dateFormat.format(cert.notAfter))
|
||||
isExpired -> getString(R.string.settings_advanced_certificates_trusted_item_summary_leaf_expired, issuer)
|
||||
else -> getString(R.string.settings_advanced_certificates_trusted_item_summary_leaf, issuer, dateFormat.format(cert.notAfter))
|
||||
// Title is the short URL (baseUrl)
|
||||
pref.title = shortUrl(trustedCert.baseUrl)
|
||||
// Summary shows certificate subject and expiry
|
||||
pref.summary = if (isExpired) {
|
||||
getString(R.string.settings_advanced_certificates_trusted_item_summary_expired, subject)
|
||||
} else {
|
||||
getString(R.string.settings_advanced_certificates_trusted_item_summary, subject, dateFormat.format(cert.notAfter))
|
||||
}
|
||||
pref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
TrustedCertificateFragment.newInstanceView(trustedCert.fingerprint)
|
||||
TrustedCertificateFragment.newInstanceView(trustedCert.baseUrl)
|
||||
.show(childFragmentManager, TrustedCertificateFragment.TAG)
|
||||
true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,22 @@ import android.app.Dialog
|
|||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import io.heckel.ntfy.R
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import io.heckel.ntfy.util.AfterChangedTextWatcher
|
||||
import io.heckel.ntfy.util.CertUtil
|
||||
import io.heckel.ntfy.util.validUrl
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
@ -27,30 +33,44 @@ import java.util.Locale
|
|||
* Full-screen dialog fragment for viewing and trusting/deleting server certificates.
|
||||
*
|
||||
* Modes:
|
||||
* - UNKNOWN: Shows certificate from SSL error with "Trust" action (from AddFragment)
|
||||
* - ADD: Shows certificate from file picker with "Trust" action (from CertificateSettingsFragment)
|
||||
* - VIEW: Shows certificate details with "Delete" action (from CertificateSettingsFragment)
|
||||
* - UNKNOWN: Shows certificate from SSL error with "Trust" action (from AddFragment).
|
||||
* baseUrl is passed as argument, goes directly to page 2.
|
||||
* - ADD: Two-page flow - first enter Service URL, then view details and trust.
|
||||
* Certificate is passed as argument.
|
||||
* - VIEW: Shows certificate details with "Delete" action (from CertificateSettingsFragment).
|
||||
* baseUrl is passed as argument.
|
||||
*/
|
||||
class TrustedCertificateFragment : DialogFragment() {
|
||||
private lateinit var repository: Repository
|
||||
private var listener: TrustedCertificateListener? = null
|
||||
|
||||
private var mode: Mode = Mode.ADD
|
||||
private var currentPage: Int = 1
|
||||
private var cert: X509Certificate? = null
|
||||
private var fingerprint: String? = null
|
||||
private var baseUrl: String? = null
|
||||
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
private lateinit var nextMenuItem: MenuItem
|
||||
private lateinit var trustMenuItem: MenuItem
|
||||
private lateinit var deleteMenuItem: MenuItem
|
||||
|
||||
// Page 1 views
|
||||
private lateinit var page1Layout: LinearLayout
|
||||
private lateinit var baseUrlLayout: TextInputLayout
|
||||
private lateinit var baseUrlText: TextInputEditText
|
||||
private lateinit var errorText: TextView
|
||||
|
||||
// Page 2 views
|
||||
private lateinit var page2Layout: LinearLayout
|
||||
private lateinit var descriptionText: TextView
|
||||
private lateinit var warningText: TextView
|
||||
private lateinit var baseUrlValueLabel: TextView
|
||||
private lateinit var baseUrlValueText: TextView
|
||||
private lateinit var subjectText: TextView
|
||||
private lateinit var issuerText: TextView
|
||||
private lateinit var fingerprintText: TextView
|
||||
private lateinit var validFromText: TextView
|
||||
private lateinit var validUntilText: TextView
|
||||
private lateinit var caText: TextView
|
||||
private lateinit var caInfoText: TextView
|
||||
|
||||
interface TrustedCertificateListener {
|
||||
fun onCertificateTrusted(certificate: X509Certificate)
|
||||
|
|
@ -77,18 +97,25 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
// Determine mode from arguments
|
||||
mode = Mode.valueOf(arguments?.getString(ARG_MODE) ?: Mode.ADD.name)
|
||||
|
||||
// Get certificate data based on mode
|
||||
// Get data based on mode
|
||||
when (mode) {
|
||||
Mode.UNKNOWN, Mode.ADD -> {
|
||||
Mode.UNKNOWN -> {
|
||||
val certBytes = arguments?.getByteArray(ARG_CERTIFICATE)
|
||||
?: throw IllegalArgumentException("Certificate bytes required for ADD/UNKNOWN mode")
|
||||
?: throw IllegalArgumentException("Certificate bytes required for UNKNOWN mode")
|
||||
baseUrl = arguments?.getString(ARG_BASE_URL)
|
||||
?: throw IllegalArgumentException("Base URL required for UNKNOWN mode")
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
cert = certFactory.generateCertificate(java.io.ByteArrayInputStream(certBytes)) as X509Certificate
|
||||
}
|
||||
Mode.ADD -> {
|
||||
val certBytes = arguments?.getByteArray(ARG_CERTIFICATE)
|
||||
?: throw IllegalArgumentException("Certificate bytes required for ADD mode")
|
||||
val certFactory = CertificateFactory.getInstance("X.509")
|
||||
cert = certFactory.generateCertificate(java.io.ByteArrayInputStream(certBytes)) as X509Certificate
|
||||
fingerprint = CertUtil.calculateFingerprint(cert!!)
|
||||
}
|
||||
Mode.VIEW -> {
|
||||
fingerprint = arguments?.getString(ARG_FINGERPRINT)
|
||||
?: throw IllegalArgumentException("Fingerprint required for VIEW mode")
|
||||
baseUrl = arguments?.getString(ARG_BASE_URL)
|
||||
?: throw IllegalArgumentException("Base URL required for VIEW mode")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -114,17 +141,16 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun setupView(view: android.view.View) {
|
||||
private fun setupView(view: View) {
|
||||
// Setup toolbar
|
||||
toolbar = view.findViewById(R.id.trusted_certificate_toolbar)
|
||||
toolbar.setNavigationOnClickListener {
|
||||
if (mode == Mode.ADD || mode == Mode.UNKNOWN) {
|
||||
listener?.onCertificateRejected()
|
||||
}
|
||||
dismiss()
|
||||
}
|
||||
toolbar.setNavigationOnClickListener { handleBack() }
|
||||
toolbar.setOnMenuItemClickListener { menuItem ->
|
||||
when (menuItem.itemId) {
|
||||
R.id.trusted_certificate_action_next -> {
|
||||
nextClicked()
|
||||
true
|
||||
}
|
||||
R.id.trusted_certificate_action_trust -> {
|
||||
trustCertificate()
|
||||
true
|
||||
|
|
@ -136,19 +162,31 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
else -> false
|
||||
}
|
||||
}
|
||||
nextMenuItem = toolbar.menu.findItem(R.id.trusted_certificate_action_next)
|
||||
trustMenuItem = toolbar.menu.findItem(R.id.trusted_certificate_action_trust)
|
||||
deleteMenuItem = toolbar.menu.findItem(R.id.trusted_certificate_action_delete)
|
||||
|
||||
// Setup views
|
||||
// Page 1 views
|
||||
page1Layout = view.findViewById(R.id.trusted_certificate_page1)
|
||||
baseUrlLayout = view.findViewById(R.id.trusted_certificate_base_url_layout)
|
||||
baseUrlText = view.findViewById(R.id.trusted_certificate_base_url_text)
|
||||
errorText = view.findViewById(R.id.trusted_certificate_error_text)
|
||||
|
||||
// Page 2 views
|
||||
page2Layout = view.findViewById(R.id.trusted_certificate_page2)
|
||||
descriptionText = view.findViewById(R.id.trusted_certificate_description)
|
||||
warningText = view.findViewById(R.id.trusted_certificate_warning)
|
||||
baseUrlValueLabel = view.findViewById(R.id.trusted_certificate_base_url_value_label)
|
||||
baseUrlValueText = view.findViewById(R.id.trusted_certificate_base_url_value)
|
||||
subjectText = view.findViewById(R.id.trusted_certificate_subject)
|
||||
issuerText = view.findViewById(R.id.trusted_certificate_issuer)
|
||||
fingerprintText = view.findViewById(R.id.trusted_certificate_fingerprint)
|
||||
validFromText = view.findViewById(R.id.trusted_certificate_valid_from)
|
||||
validUntilText = view.findViewById(R.id.trusted_certificate_valid_until)
|
||||
caText = view.findViewById(R.id.trusted_certificate_ca)
|
||||
caInfoText = view.findViewById(R.id.trusted_certificate_ca_info)
|
||||
|
||||
// Validate input when typing
|
||||
val textWatcher = AfterChangedTextWatcher { validatePage1() }
|
||||
baseUrlText.addTextChangedListener(textWatcher)
|
||||
|
||||
when (mode) {
|
||||
Mode.UNKNOWN -> setupUnknownMode()
|
||||
|
|
@ -158,34 +196,47 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
}
|
||||
|
||||
private fun setupUnknownMode() {
|
||||
// Go directly to page 2 with details
|
||||
toolbar.setTitle(R.string.trusted_certificate_dialog_title_unknown)
|
||||
descriptionText.setText(R.string.trusted_certificate_dialog_description_unknown)
|
||||
descriptionText.isVisible = true
|
||||
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
|
||||
page1Layout.isVisible = false
|
||||
page2Layout.isVisible = true
|
||||
nextMenuItem.isVisible = false
|
||||
trustMenuItem.isVisible = true
|
||||
deleteMenuItem.isVisible = false
|
||||
|
||||
descriptionText.setText(R.string.trusted_certificate_dialog_description_unknown)
|
||||
descriptionText.isVisible = true
|
||||
baseUrlValueLabel.isVisible = true
|
||||
baseUrlValueText.isVisible = true
|
||||
baseUrlValueText.text = baseUrl
|
||||
|
||||
displayCertificateDetails(cert!!)
|
||||
}
|
||||
|
||||
private fun setupAddMode() {
|
||||
toolbar.setTitle(R.string.trusted_certificate_dialog_title_add)
|
||||
descriptionText.setText(R.string.trusted_certificate_dialog_description_add)
|
||||
descriptionText.isVisible = true
|
||||
trustMenuItem.isVisible = true
|
||||
deleteMenuItem.isVisible = false
|
||||
|
||||
displayCertificateDetails(cert!!)
|
||||
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
|
||||
showPage1()
|
||||
}
|
||||
|
||||
private fun setupViewMode() {
|
||||
toolbar.setTitle(R.string.trusted_certificate_dialog_title)
|
||||
descriptionText.isVisible = false
|
||||
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
|
||||
page1Layout.isVisible = false
|
||||
page2Layout.isVisible = true
|
||||
nextMenuItem.isVisible = false
|
||||
trustMenuItem.isVisible = false
|
||||
deleteMenuItem.isVisible = true
|
||||
|
||||
descriptionText.isVisible = false
|
||||
baseUrlValueLabel.isVisible = true
|
||||
baseUrlValueText.isVisible = true
|
||||
baseUrlValueText.text = baseUrl
|
||||
|
||||
// Load certificate from repository
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val trustedCert = repository.getTrustedCertificates().find { it.fingerprint == fingerprint }
|
||||
val trustedCert = repository.getTrustedCertificate(baseUrl!!)
|
||||
if (trustedCert != null) {
|
||||
try {
|
||||
val x509Cert = CertUtil.parsePemCertificate(trustedCert.pem)
|
||||
|
|
@ -195,7 +246,7 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
}
|
||||
} catch (e: Exception) {
|
||||
withContext(Dispatchers.Main) {
|
||||
fingerprintText.text = fingerprint
|
||||
fingerprintText.text = trustedCert.fingerprint
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -206,6 +257,65 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showPage1() {
|
||||
currentPage = 1
|
||||
page1Layout.isVisible = true
|
||||
page2Layout.isVisible = false
|
||||
toolbar.setNavigationIcon(R.drawable.ic_close_white_24dp)
|
||||
nextMenuItem.isVisible = true
|
||||
trustMenuItem.isVisible = false
|
||||
deleteMenuItem.isVisible = false
|
||||
validatePage1()
|
||||
}
|
||||
|
||||
private fun showPage2() {
|
||||
currentPage = 2
|
||||
page1Layout.isVisible = false
|
||||
page2Layout.isVisible = true
|
||||
toolbar.setNavigationIcon(R.drawable.ic_arrow_back_white_24dp)
|
||||
nextMenuItem.isVisible = false
|
||||
trustMenuItem.isVisible = true
|
||||
deleteMenuItem.isVisible = false
|
||||
|
||||
descriptionText.setText(R.string.trusted_certificate_dialog_description_add)
|
||||
descriptionText.isVisible = true
|
||||
baseUrlValueLabel.isVisible = true
|
||||
baseUrlValueText.isVisible = true
|
||||
baseUrlValueText.text = baseUrl
|
||||
|
||||
displayCertificateDetails(cert!!)
|
||||
}
|
||||
|
||||
private fun validatePage1() {
|
||||
val url = baseUrlText.text?.toString()?.trim() ?: ""
|
||||
nextMenuItem.isEnabled = validUrl(url)
|
||||
}
|
||||
|
||||
private fun nextClicked() {
|
||||
val url = baseUrlText.text?.toString()?.trim() ?: ""
|
||||
|
||||
if (!validUrl(url)) {
|
||||
showError(getString(R.string.trusted_certificate_dialog_error_invalid_url))
|
||||
return
|
||||
}
|
||||
|
||||
baseUrl = url
|
||||
errorText.isVisible = false
|
||||
showPage2()
|
||||
}
|
||||
|
||||
private fun handleBack() {
|
||||
when {
|
||||
mode == Mode.VIEW -> dismiss()
|
||||
mode == Mode.UNKNOWN -> {
|
||||
listener?.onCertificateRejected()
|
||||
dismiss()
|
||||
}
|
||||
currentPage == 2 -> showPage1()
|
||||
else -> dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun displayCertificateDetails(certificate: X509Certificate) {
|
||||
val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault())
|
||||
|
||||
|
|
@ -215,11 +325,6 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
validFromText.text = dateFormat.format(certificate.notBefore)
|
||||
validUntilText.text = dateFormat.format(certificate.notAfter)
|
||||
|
||||
// Determine if this is a CA certificate (self-signed)
|
||||
val isCa = certificate.subjectX500Principal == certificate.issuerX500Principal
|
||||
caText.text = if (isCa) getString(R.string.common_yes) else getString(R.string.common_no)
|
||||
caInfoText.isVisible = isCa
|
||||
|
||||
// Show warning if certificate is expired or not yet valid
|
||||
val now = Date()
|
||||
when {
|
||||
|
|
@ -237,12 +342,18 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun showError(message: String) {
|
||||
errorText.text = message
|
||||
errorText.isVisible = true
|
||||
}
|
||||
|
||||
private fun trustCertificate() {
|
||||
val certificate = cert ?: return
|
||||
val url = baseUrl ?: return
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val fingerprint = CertUtil.calculateFingerprint(certificate)
|
||||
val pem = CertUtil.encodeCertificateToPem(certificate)
|
||||
repository.addTrustedCertificate(fingerprint, pem)
|
||||
repository.addTrustedCertificate(url, fingerprint, pem)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, R.string.trusted_certificate_dialog_added_toast, Toast.LENGTH_SHORT).show()
|
||||
listener?.onCertificateTrusted(certificate)
|
||||
|
|
@ -252,9 +363,9 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
}
|
||||
|
||||
private fun deleteCertificate() {
|
||||
val fp = fingerprint ?: return
|
||||
val url = baseUrl ?: return
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
repository.removeTrustedCertificate(fp)
|
||||
repository.removeTrustedCertificate(url)
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(context, R.string.trusted_certificate_dialog_deleted_toast, Toast.LENGTH_SHORT).show()
|
||||
listener?.onCertificateDeleted()
|
||||
|
|
@ -273,24 +384,28 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
const val TAG = "NtfyTrustedCertFragment"
|
||||
private const val ARG_MODE = "mode"
|
||||
private const val ARG_CERTIFICATE = "certificate"
|
||||
private const val ARG_FINGERPRINT = "fingerprint"
|
||||
private const val ARG_BASE_URL = "base_url"
|
||||
|
||||
/**
|
||||
* Create fragment for UNKNOWN mode - showing unknown server certificate with Trust action
|
||||
* Used when connecting to a server with an untrusted certificate (from AddFragment)
|
||||
* Create fragment for UNKNOWN mode - showing unknown server certificate with Trust action.
|
||||
* Used when connecting to a server with an untrusted certificate (from AddFragment).
|
||||
* The baseUrl is provided so it goes directly to the details page.
|
||||
*/
|
||||
fun newInstanceUnknown(certificate: X509Certificate): TrustedCertificateFragment {
|
||||
fun newInstanceUnknown(certificate: X509Certificate, baseUrl: String): TrustedCertificateFragment {
|
||||
return TrustedCertificateFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_MODE, Mode.UNKNOWN.name)
|
||||
putByteArray(ARG_CERTIFICATE, certificate.encoded)
|
||||
putString(ARG_BASE_URL, baseUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create fragment for ADD mode - showing certificate details with Trust action
|
||||
* Used when adding a certificate from file picker (from CertificateSettingsFragment)
|
||||
* Create fragment for ADD mode - two-page flow to add a trusted certificate.
|
||||
* Page 1: Enter Service URL
|
||||
* Page 2: View certificate details and trust
|
||||
* Used when adding a certificate from file picker (from CertificateSettingsFragment).
|
||||
*/
|
||||
fun newInstanceAdd(certificate: X509Certificate): TrustedCertificateFragment {
|
||||
return TrustedCertificateFragment().apply {
|
||||
|
|
@ -302,13 +417,14 @@ class TrustedCertificateFragment : DialogFragment() {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create fragment for VIEW mode - showing certificate details with Delete action
|
||||
* Create fragment for VIEW mode - showing certificate details with Delete action.
|
||||
* baseUrl is used to look up the certificate from the repository.
|
||||
*/
|
||||
fun newInstanceView(fingerprint: String): TrustedCertificateFragment {
|
||||
fun newInstanceView(baseUrl: String): TrustedCertificateFragment {
|
||||
return TrustedCertificateFragment().apply {
|
||||
arguments = Bundle().apply {
|
||||
putString(ARG_MODE, Mode.VIEW.name)
|
||||
putString(ARG_FINGERPRINT, fingerprint)
|
||||
putString(ARG_BASE_URL, baseUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,15 +6,14 @@ import android.util.Base64
|
|||
import io.heckel.ntfy.db.ClientCertificate
|
||||
import io.heckel.ntfy.db.Repository
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.internal.tls.OkHostnameVerifier
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.net.URL
|
||||
import java.security.KeyStore
|
||||
import java.security.MessageDigest
|
||||
import java.security.SecureRandom
|
||||
import java.security.cert.CertificateException
|
||||
import java.security.cert.CertificateFactory
|
||||
import java.security.cert.X509Certificate
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.KeyManager
|
||||
import javax.net.ssl.KeyManagerFactory
|
||||
import javax.net.ssl.SSLContext
|
||||
|
|
@ -22,13 +21,14 @@ import javax.net.ssl.SSLException
|
|||
import javax.net.ssl.SSLSocket
|
||||
import javax.net.ssl.TrustManager
|
||||
import javax.net.ssl.TrustManagerFactory
|
||||
import javax.net.ssl.HostnameVerifier
|
||||
import javax.net.ssl.X509TrustManager
|
||||
|
||||
/**
|
||||
* TLS config:
|
||||
* - Trust system roots
|
||||
* - Also trust user-added certs (leaf and/or CA; chains to user-added CAs)
|
||||
* - Hostname verify ONLY when chain is system-trusted; skip when only user-trusted
|
||||
* - For each baseUrl, either use the pinned certificate (if one exists) OR system trust
|
||||
* - Pinned cert = ONLY that exact certificate is trusted (strict pinning)
|
||||
* - Hostname verification is bypassed for pinned certificates (the fingerprint match is the trust anchor)
|
||||
* - Optional mTLS via per-baseUrl PKCS#12 client cert
|
||||
*/
|
||||
class CertUtil private constructor(context: Context) {
|
||||
|
|
@ -37,32 +37,35 @@ class CertUtil private constructor(context: Context) {
|
|||
|
||||
suspend fun withTLSConfig(builder: OkHttpClient.Builder, baseUrl: String): OkHttpClient.Builder {
|
||||
try {
|
||||
val trustedCerts = repository.getTrustedCertificates().mapNotNull {
|
||||
try {
|
||||
parsePemCertificate(it.pem)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to parse trusted certificate: ${it.fingerprint}", e)
|
||||
null
|
||||
}
|
||||
}
|
||||
val pinnedCert = repository.getTrustedCertificate(baseUrl)
|
||||
val clientCert = repository.getClientCertificate(baseUrl)
|
||||
val clientCertKeyManagers = clientCert?.let { clientCertKeyManagers(it) }
|
||||
|
||||
// Always include system trust; add user trust if present.
|
||||
val systemTm = systemTrustManager()
|
||||
val userTm = if (trustedCerts.isNotEmpty()) trustManagerForUserCerts(trustedCerts) else null
|
||||
val compositeTm = if (userTm != null) compositeTrustManager(systemTm, userTm) else systemTm
|
||||
|
||||
// Only override SSL config if we actually have something to add (user trust or mTLS).
|
||||
if (userTm != null || clientCertKeyManagers != null) {
|
||||
val sslContext = SSLContext.getInstance("TLS").apply {
|
||||
init(clientCertKeyManagers, arrayOf<TrustManager>(compositeTm), SecureRandom())
|
||||
// Determine which trust manager to use
|
||||
val trustManager: X509TrustManager = if (pinnedCert != null) {
|
||||
// Strict pinning: only trust the exact pinned certificate
|
||||
try {
|
||||
val cert = parsePemCertificate(pinnedCert.pem)
|
||||
pinnedCertTrustManager(cert)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to parse pinned certificate for $baseUrl, falling back to system trust", e)
|
||||
systemTrustManager()
|
||||
}
|
||||
builder.sslSocketFactory(sslContext.socketFactory, compositeTm)
|
||||
} else {
|
||||
systemTrustManager()
|
||||
}
|
||||
|
||||
// Hostname rules only matter if we have custom trust. If not, keep default verifier.
|
||||
if (userTm != null) {
|
||||
builder.hostnameVerifier(selectiveHostnameVerifier(systemTm, userTm))
|
||||
// Only override SSL config if we have a pinned cert or client cert
|
||||
if (pinnedCert != null || clientCertKeyManagers != null) {
|
||||
val sslContext = SSLContext.getInstance("TLS").apply {
|
||||
init(clientCertKeyManagers, arrayOf<TrustManager>(trustManager), SecureRandom())
|
||||
}
|
||||
builder.sslSocketFactory(sslContext.socketFactory, trustManager)
|
||||
|
||||
// Bypass hostname verification for pinned certificates
|
||||
// The certificate fingerprint match is the trust anchor, not the hostname
|
||||
if (pinnedCert != null) {
|
||||
builder.hostnameVerifier(trustAllHostnameVerifier())
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
|
@ -144,81 +147,48 @@ class CertUtil private constructor(context: Context) {
|
|||
return tmf.trustManagers.first { it is X509TrustManager } as X509TrustManager
|
||||
}
|
||||
|
||||
private fun trustManagerForUserCerts(trustedCerts: List<X509Certificate>): X509TrustManager {
|
||||
val ks = KeyStore.getInstance(KeyStore.getDefaultType()).apply { load(null, null) }
|
||||
trustedCerts.forEachIndexed { idx, cert ->
|
||||
ks.setCertificateEntry("added-$idx", cert)
|
||||
}
|
||||
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply {
|
||||
init(ks)
|
||||
}
|
||||
return tmf.trustManagers.first { it is X509TrustManager } as X509TrustManager
|
||||
/**
|
||||
* Hostname verifier that accepts any hostname.
|
||||
* Used for pinned certificates where the fingerprint match is the trust anchor.
|
||||
*/
|
||||
@SuppressLint("BadHostnameVerifier")
|
||||
private fun trustAllHostnameVerifier(): HostnameVerifier {
|
||||
return HostnameVerifier { _, _ -> true }
|
||||
}
|
||||
|
||||
/**
|
||||
* Trust if either system OR custom accepts the chain.
|
||||
* Create a trust manager that ONLY trusts the exact pinned certificate.
|
||||
* This implements strict certificate pinning - system CAs are not trusted.
|
||||
*/
|
||||
private fun compositeTrustManager(systemTm: X509TrustManager, userTm: X509TrustManager): X509TrustManager =
|
||||
object : X509TrustManager {
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> =
|
||||
(systemTm.acceptedIssuers + userTm.acceptedIssuers)
|
||||
.distinctBy { it.subjectX500Principal.name }
|
||||
.toTypedArray()
|
||||
@SuppressLint("CustomX509TrustManager")
|
||||
private fun pinnedCertTrustManager(pinnedCert: X509Certificate): X509TrustManager {
|
||||
val pinnedFingerprint = calculateFingerprint(pinnedCert)
|
||||
return object : X509TrustManager {
|
||||
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf(pinnedCert)
|
||||
|
||||
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||
try {
|
||||
systemTm.checkClientTrusted(chain, authType)
|
||||
} catch (_: Exception) {
|
||||
userTm.checkClientTrusted(chain, authType)
|
||||
}
|
||||
throw CertificateException("Client authentication not supported with pinned certificate")
|
||||
}
|
||||
|
||||
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
|
||||
try {
|
||||
systemTm.checkServerTrusted(chain, authType)
|
||||
} catch (_: Exception) {
|
||||
userTm.checkServerTrusted(chain, authType)
|
||||
if (chain.isNullOrEmpty()) {
|
||||
throw CertificateException("Empty certificate chain")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hostname verification:
|
||||
* - if system-trusted => enforce hostname verification
|
||||
* - else if custom-trusted => skip hostname verification
|
||||
* - else => fail
|
||||
*/
|
||||
private fun selectiveHostnameVerifier(systemTm: X509TrustManager, userTm: X509TrustManager) =
|
||||
HostnameVerifier { hostname, session ->
|
||||
val chain = try {
|
||||
session.peerCertificates.map { it as X509Certificate }.toTypedArray()
|
||||
} catch (_: Exception) {
|
||||
return@HostnameVerifier false
|
||||
}
|
||||
if (isTrustedBy(systemTm, chain)) {
|
||||
OkHostnameVerifier.verify(hostname, session)
|
||||
} else {
|
||||
isTrustedBy(userTm, chain)
|
||||
}
|
||||
}
|
||||
|
||||
private fun isTrustedBy(tm: X509TrustManager, chain: Array<X509Certificate>): Boolean {
|
||||
// authType not reliably available here; try common ones.
|
||||
return try {
|
||||
tm.checkServerTrusted(chain, "RSA")
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
try {
|
||||
tm.checkServerTrusted(chain, "EC")
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
val serverCert = chain[0]
|
||||
val serverFingerprint = calculateFingerprint(serverCert)
|
||||
if (serverFingerprint != pinnedFingerprint) {
|
||||
throw CertificateException(
|
||||
"Certificate fingerprint mismatch. Expected: $pinnedFingerprint, Got: $serverFingerprint"
|
||||
)
|
||||
}
|
||||
// Optionally verify certificate validity
|
||||
serverCert.checkValidity()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "NtfySSLManager"
|
||||
private const val TAG = "NtfyCertUtil"
|
||||
|
||||
@Volatile
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
|
|
|
|||
|
|
@ -36,223 +36,247 @@
|
|||
android:paddingHorizontal="?dialogPreferredPadding"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Description (only shown in ADD mode) -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_description"
|
||||
<!-- Page 1: Service URL input -->
|
||||
<LinearLayout
|
||||
android:id="@+id/trusted_certificate_page1"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_description"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingTop="16dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- Warning for expired/not-yet-valid certs -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_warning"
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_page1_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_description_page1"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingTop="16dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/trusted_certificate_base_url_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/trusted_certificate_dialog_base_url_hint">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/trusted_certificate_base_url_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri"
|
||||
android:text="@string/trusted_certificate_dialog_base_url_placeholder" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_error_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/holo_red_dark"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<!-- Page 2: Certificate details -->
|
||||
<LinearLayout
|
||||
android:id="@+id/trusted_certificate_page2"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/holo_red_dark"
|
||||
android:textStyle="bold"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_description"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<!-- Subject -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_subject_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_subject"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_warning"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<!-- Description (shown in ADD and UNKNOWN modes) -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_description"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:paddingTop="16dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_subject"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_subject_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<!-- Warning for expired/not-yet-valid certs -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_warning"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@android:color/holo_red_dark"
|
||||
android:textStyle="bold"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Issuer -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_issuer_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_issuer"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_subject"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<!-- Service URL (shown in VIEW and UNKNOWN modes) -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_base_url_value_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_service_url"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_issuer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_issuer_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_base_url_value"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textIsSelectable="true"
|
||||
android:visibility="gone" />
|
||||
|
||||
<!-- Fingerprint -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_fingerprint_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_fingerprint"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_issuer"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<!-- Subject -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_subject_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_subject"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_fingerprint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textIsSelectable="true"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_fingerprint_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_subject"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textIsSelectable="true" />
|
||||
|
||||
<!-- Valid From -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_valid_from_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_valid_from"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_fingerprint"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/trusted_certificate_valid_until_label"
|
||||
app:layout_constraintWidth_percent="0.5" />
|
||||
<!-- Issuer -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_issuer_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_issuer"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_valid_from"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_valid_from_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/trusted_certificate_valid_until"
|
||||
app:layout_constraintWidth_percent="0.5" />
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_issuer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textIsSelectable="true" />
|
||||
|
||||
<!-- Valid Until -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_valid_until_label"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_valid_until"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_fingerprint"
|
||||
app:layout_constraintStart_toEndOf="@id/trusted_certificate_valid_from_label"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.5" />
|
||||
<!-- Fingerprint -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_fingerprint_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_fingerprint"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_valid_until"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_valid_until_label"
|
||||
app:layout_constraintStart_toEndOf="@id/trusted_certificate_valid_from"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintWidth_percent="0.5" />
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_fingerprint"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="12sp"
|
||||
android:fontFamily="monospace"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:textIsSelectable="true" />
|
||||
|
||||
<!-- Certificate Authority -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_ca_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_ca"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_valid_from"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<!-- Valid From / Valid Until -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginTop="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_ca"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_ca_label"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- CA Info (only shown for CA certificates) -->
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_ca_info"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_ca_info"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toBottomOf="@id/trusted_certificate_ca"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent" />
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_valid_from_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_valid_from"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_valid_from"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_valid_until_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/trusted_certificate_dialog_valid_until"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?android:textColorSecondary"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/trusted_certificate_valid_until"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="14sp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="16dp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<item
|
||||
android:id="@+id/trusted_certificate_action_next"
|
||||
android:title="@string/trusted_certificate_dialog_button_next"
|
||||
android:visible="false"
|
||||
app:showAsAction="always" />
|
||||
<item
|
||||
android:id="@+id/trusted_certificate_action_trust"
|
||||
android:title="@string/trusted_certificate_dialog_button_trust"
|
||||
|
|
@ -12,4 +17,3 @@
|
|||
android:visible="false"
|
||||
app:showAsAction="always" />
|
||||
</menu>
|
||||
|
||||
|
|
|
|||
|
|
@ -428,10 +428,8 @@
|
|||
<string name="settings_advanced_certificates_title">Manage certificates</string>
|
||||
<string name="settings_advanced_certificates_summary">Add certificates to the trust store and manage client certificates for mTLS</string>
|
||||
<string name="settings_advanced_certificates_trusted_header">Trusted certificates</string>
|
||||
<string name="settings_advanced_certificates_trusted_item_summary_leaf">Leaf certificate, issued by %1$s\nExpires %2$s</string>
|
||||
<string name="settings_advanced_certificates_trusted_item_summary_leaf_expired">Leaf certificate, issued by %1$s\nExpired</string>
|
||||
<string name="settings_advanced_certificates_trusted_item_summary_ca">CA certificate, self-signed\nExpires %1$s</string>
|
||||
<string name="settings_advanced_certificates_trusted_item_summary_ca_expired">CA certificate, self-signed\nExpired</string>
|
||||
<string name="settings_advanced_certificates_trusted_item_summary">%1$s, expires %2$s</string>
|
||||
<string name="settings_advanced_certificates_trusted_item_summary_expired">%1$s, expired</string>
|
||||
<string name="settings_advanced_certificates_trusted_add_title">Add a trusted certificate</string>
|
||||
<string name="settings_advanced_certificates_trusted_add_summary">Import a certificate into the trust store (PEM). HTTPS and WebSocket connections will trust this certificate.</string>
|
||||
<string name="settings_advanced_certificates_client_header">Client certificates (mTLS)</string>
|
||||
|
|
@ -505,18 +503,22 @@
|
|||
<string name="trusted_certificate_dialog_title">Certificate details</string>
|
||||
<string name="trusted_certificate_dialog_title_unknown">Unknown certificate</string>
|
||||
<string name="trusted_certificate_dialog_title_add">Add trusted certificate</string>
|
||||
<string name="trusted_certificate_dialog_description">The server\'s certificate could not be verified. This may happen with self-signed certificates or custom Certificate Authorities. Review the details below before trusting.</string>
|
||||
<string name="trusted_certificate_dialog_description_unknown">The server\'s certificate could not be verified. This may happen with self-signed certificates or custom Certificate Authorities. Review the details below before trusting.</string>
|
||||
<string name="trusted_certificate_dialog_description">The server\'s certificate could not be verified. Review the details below before trusting.</string>
|
||||
<string name="trusted_certificate_dialog_description_unknown">The server\'s certificate could not be verified. Review the details below before trusting.</string>
|
||||
<string name="trusted_certificate_dialog_description_add">You\'ve selected a certificate file. Please review the details below before adding it to your trusted certificates.</string>
|
||||
<string name="trusted_certificate_dialog_description_page1">Enter the service URL that this certificate should be pinned to. The certificate will only be trusted for this URL.</string>
|
||||
<string name="trusted_certificate_dialog_base_url_hint">Service URL</string>
|
||||
<string name="trusted_certificate_dialog_base_url_placeholder">https://ntfy.example.com</string>
|
||||
<string name="trusted_certificate_dialog_service_url">Service URL</string>
|
||||
<string name="trusted_certificate_dialog_subject">Subject</string>
|
||||
<string name="trusted_certificate_dialog_issuer">Issuer</string>
|
||||
<string name="trusted_certificate_dialog_fingerprint">SHA-256 fingerprint</string>
|
||||
<string name="trusted_certificate_dialog_valid_from">Valid from</string>
|
||||
<string name="trusted_certificate_dialog_valid_until">Valid until</string>
|
||||
<string name="trusted_certificate_dialog_ca">Certificate Authority (CA)</string>
|
||||
<string name="trusted_certificate_dialog_ca_info">All certificates signed by this Certificate Authority will be trusted.</string>
|
||||
<string name="trusted_certificate_dialog_expired_warning">Warning: This certificate has expired.</string>
|
||||
<string name="trusted_certificate_dialog_not_yet_valid_warning">Warning: This certificate is not yet valid.</string>
|
||||
<string name="trusted_certificate_dialog_error_invalid_url">Invalid URL</string>
|
||||
<string name="trusted_certificate_dialog_button_next">Next</string>
|
||||
<string name="trusted_certificate_dialog_button_trust">Trust</string>
|
||||
<string name="trusted_certificate_dialog_button_delete">Delete</string>
|
||||
<string name="trusted_certificate_dialog_added_toast">Certificate added</string>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue