diff --git a/app/src/main/java/io/heckel/ntfy/ui/CertificateSettingsFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/CertificateSettingsFragment.kt index a34c05f9..d2a3ef67 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/CertificateSettingsFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/CertificateSettingsFragment.kt @@ -63,19 +63,19 @@ class CertificateSettingsFragment : BasePreferenceFragment(), // Trusted certificates header val trustedCategory = PreferenceCategory(preferenceScreen.context) - trustedCategory.title = getString(R.string.settings_certificates_prefs_trusted_header) + trustedCategory.title = getString(R.string.settings_advanced_certificates_trusted_header) preferenceScreen.addPreference(trustedCategory) certs.forEach { trustedCert -> try { val cert = CertUtil.parseCertificate(trustedCert.pem) + val issuer = parseCommonName(cert.issuerX500Principal.name) val pref = Preference(preferenceScreen.context) - pref.title = getDisplaySubject(cert) + pref.title = parseCommonName(cert.subjectX500Principal.name) pref.summary = if (isValid(cert)) { - getString(R.string.settings_certificates_prefs_expires_after, - dateFormat.format(cert.notAfter)) + getString(R.string.settings_advanced_certificates_trusted_item_summary_not_expired, issuer, dateFormat.format(cert.notAfter)) } else { - getString(R.string.settings_certificates_prefs_expired) + getString(R.string.settings_advanced_certificates_trusted_item_summary_expired, issuer) } pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { TrustedCertificateFragment.newInstanceView(trustedCert.fingerprint) @@ -90,8 +90,8 @@ class CertificateSettingsFragment : BasePreferenceFragment(), // Add trusted certificate - launches file picker directly val addTrustedPref = Preference(preferenceScreen.context) - addTrustedPref.title = getString(R.string.settings_certificates_prefs_trusted_add_title) - addTrustedPref.summary = getString(R.string.settings_certificates_prefs_trusted_add_summary) + addTrustedPref.title = getString(R.string.settings_advanced_certificates_trusted_add_title) + addTrustedPref.summary = getString(R.string.settings_advanced_certificates_trusted_add_summary) addTrustedPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { trustedCertFilePicker.launch("*/*") true @@ -104,25 +104,22 @@ class CertificateSettingsFragment : BasePreferenceFragment(), // Client certificates header val clientCategory = PreferenceCategory(preferenceScreen.context) - clientCategory.title = getString(R.string.settings_certificates_prefs_client_header) + clientCategory.title = getString(R.string.settings_advanced_certificates_client_header) preferenceScreen.addPreference(clientCategory) certs.forEach { clientCert -> val pref = Preference(preferenceScreen.context) try { - val x509Cert = CertUtil.parsePkcs12Certificate(clientCert.p12Base64, clientCert.password) - pref.title = getDisplaySubject(x509Cert) - val expires = if (isValid(x509Cert)) { - getString(R.string.settings_certificates_prefs_expires_after, - dateFormat.format(x509Cert.notAfter)) + val cert = CertUtil.parsePkcs12Certificate(clientCert.p12Base64, clientCert.password) + val issuer = parseCommonName(cert.issuerX500Principal.name) + pref.title = parseCommonName(cert.subjectX500Principal.name) + pref.summary = if (isValid(cert)) { + getString(R.string.settings_advanced_certificates_client_item_summary_not_expired, issuer, dateFormat.format(cert.notAfter), shortUrl(clientCert.baseUrl)) } else { - getString(R.string.settings_certificates_prefs_expired) + getString(R.string.settings_advanced_certificates_client_item_summary_expired, issuer, shortUrl(clientCert.baseUrl)) } - pref.summary = getString(R.string.settings_certificates_prefs_client_summary, - shortUrl(clientCert.baseUrl), expires) - } catch (e: Exception) { + } catch (_: Exception) { pref.title = shortUrl(clientCert.baseUrl) - pref.summary = getString(R.string.settings_certificates_prefs_client_configured) } pref.onPreferenceClickListener = Preference.OnPreferenceClickListener { ClientCertificateFragment.newInstanceView(clientCert.baseUrl) @@ -134,8 +131,8 @@ class CertificateSettingsFragment : BasePreferenceFragment(), // Add client certificate - launches file picker directly val addClientPref = Preference(preferenceScreen.context) - addClientPref.title = getString(R.string.settings_certificates_prefs_client_add_title) - addClientPref.summary = getString(R.string.settings_certificates_prefs_client_add_summary) + addClientPref.title = getString(R.string.settings_advanced_certificates_client_add_title) + addClientPref.summary = getString(R.string.settings_advanced_certificates_client_add_summary) addClientPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { clientCertFilePicker.launch("*/*") true @@ -153,11 +150,11 @@ class CertificateSettingsFragment : BasePreferenceFragment(), TrustedCertificateFragment.newInstanceAdd(cert) .show(childFragmentManager, TrustedCertificateFragment.TAG) } else { - Toast.makeText(context, R.string.certificate_dialog_error_invalid_cert, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.settings_advanced_certificates_error_invalid_cert, Toast.LENGTH_SHORT).show() } } catch (e: Exception) { Log.w(TAG, "Failed to read certificate file", e) - Toast.makeText(context, R.string.certificate_dialog_error_invalid_cert, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.settings_advanced_certificates_error_invalid_cert, Toast.LENGTH_SHORT).show() } } @@ -170,18 +167,17 @@ class CertificateSettingsFragment : BasePreferenceFragment(), ClientCertificateFragment.newInstance(data) .show(childFragmentManager, ClientCertificateFragment.TAG) } else { - Toast.makeText(context, R.string.certificate_dialog_error_invalid_p12, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.settings_advanced_certificates_error_invalid_p12, Toast.LENGTH_SHORT).show() } } catch (e: Exception) { Log.w(TAG, "Failed to read PKCS#12 file", e) - Toast.makeText(context, R.string.certificate_dialog_error_invalid_p12, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.settings_advanced_certificates_error_invalid_p12, Toast.LENGTH_SHORT).show() } } - private fun getDisplaySubject(cert: X509Certificate): String { - val subject = cert.subjectX500Principal.name - val cnMatch = Regex("CN=([^,]+)").find(subject) - return cnMatch?.groupValues?.get(1) ?: subject + private fun parseCommonName(name: String): String { + val cnMatch = Regex("CN=([^,]+)").find(name) + return cnMatch?.groupValues?.get(1) ?: name } private fun isValid(cert: X509Certificate): Boolean { diff --git a/app/src/main/java/io/heckel/ntfy/ui/ClientCertificateFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/ClientCertificateFragment.kt index 864a0f89..de685a35 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/ClientCertificateFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/ClientCertificateFragment.kt @@ -260,7 +260,7 @@ class ClientCertificateFragment : DialogFragment() { val pwd = passwordText.text?.toString() ?: "" if (!validUrl(url)) { - showError(getString(R.string.certificate_dialog_error_invalid_url)) + showError(getString(R.string.client_certificate_dialog_error_invalid_url)) return } @@ -289,13 +289,13 @@ class ClientCertificateFragment : DialogFragment() { val p12Base64 = Base64.encodeToString(data, Base64.NO_WRAP) repository.addClientCertificate(url, p12Base64, pwd) withContext(Dispatchers.Main) { - Toast.makeText(context, R.string.certificate_dialog_added_toast, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.client_certificate_dialog_added_toast, Toast.LENGTH_SHORT).show() listener?.onCertificateAdded() dismiss() } } catch (e: Exception) { withContext(Dispatchers.Main) { - showError(getString(R.string.certificate_dialog_error_invalid_p12_password)) + showError(getString(R.string.client_certificate_dialog_error_invalid_p12_password)) } } } @@ -304,18 +304,18 @@ class ClientCertificateFragment : DialogFragment() { private fun confirmDeleteCertificate() { val url = baseUrl ?: return MaterialAlertDialogBuilder(requireContext()) - .setMessage(R.string.certificate_dialog_delete_confirm) - .setPositiveButton(R.string.certificate_dialog_button_delete) { _, _ -> + .setMessage(R.string.client_certificate_dialog_delete_confirm) + .setPositiveButton(R.string.client_certificate_dialog_button_delete) { _, _ -> lifecycleScope.launch(Dispatchers.IO) { repository.removeClientCertificate(url) withContext(Dispatchers.Main) { - Toast.makeText(context, R.string.certificate_dialog_deleted_toast, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.client_certificate_dialog_deleted_toast, Toast.LENGTH_SHORT).show() listener?.onCertificateDeleted() dismiss() } } } - .setNegativeButton(R.string.certificate_dialog_button_cancel, null) + .setNegativeButton(R.string.client_certificate_dialog_button_cancel, null) .show() } diff --git a/app/src/main/java/io/heckel/ntfy/ui/TrustedCertificateFragment.kt b/app/src/main/java/io/heckel/ntfy/ui/TrustedCertificateFragment.kt index b038eca7..c3f395fe 100644 --- a/app/src/main/java/io/heckel/ntfy/ui/TrustedCertificateFragment.kt +++ b/app/src/main/java/io/heckel/ntfy/ui/TrustedCertificateFragment.kt @@ -215,11 +215,11 @@ class TrustedCertificateFragment : DialogFragment() { val now = Date() when { now.after(certificate.notAfter) -> { - warningText.text = getString(R.string.certificate_trust_dialog_expired_warning) + warningText.text = getString(R.string.trusted_certificate_dialog_expired_warning) warningText.isVisible = true } now.before(certificate.notBefore) -> { - warningText.text = getString(R.string.certificate_trust_dialog_not_yet_valid_warning) + warningText.text = getString(R.string.trusted_certificate_dialog_not_yet_valid_warning) warningText.isVisible = true } else -> { @@ -235,7 +235,7 @@ class TrustedCertificateFragment : DialogFragment() { val pem = CertUtil.encodeToPem(certificate) repository.addTrustedCertificate(fp, pem) withContext(Dispatchers.Main) { - Toast.makeText(context, R.string.certificate_dialog_added_toast, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.trusted_certificate_dialog_added_toast, Toast.LENGTH_SHORT).show() listener?.onCertificateTrusted(certificate) dismiss() } @@ -247,7 +247,7 @@ class TrustedCertificateFragment : DialogFragment() { lifecycleScope.launch(Dispatchers.IO) { repository.removeTrustedCertificate(fp) withContext(Dispatchers.Main) { - Toast.makeText(context, R.string.certificate_dialog_deleted_toast, Toast.LENGTH_SHORT).show() + Toast.makeText(context, R.string.trusted_certificate_dialog_deleted_toast, Toast.LENGTH_SHORT).show() listener?.onCertificateDeleted() dismiss() } diff --git a/app/src/main/res/layout/fragment_client_certificate_dialog.xml b/app/src/main/res/layout/fragment_client_certificate_dialog.xml index aa12cb82..30d4f1be 100644 --- a/app/src/main/res/layout/fragment_client_certificate_dialog.xml +++ b/app/src/main/res/layout/fragment_client_certificate_dialog.xml @@ -154,7 +154,7 @@ android:id="@+id/client_certificate_subject_label" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/certificate_trust_dialog_subject" + android:text="@string/client_certificate_dialog_subject" android:textSize="12sp" android:textColor="?android:textColorSecondary" android:paddingStart="4dp" @@ -175,7 +175,7 @@ android:id="@+id/client_certificate_issuer_label" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/certificate_trust_dialog_issuer" + android:text="@string/client_certificate_dialog_issuer" android:textSize="12sp" android:textColor="?android:textColorSecondary" android:paddingStart="4dp" @@ -196,7 +196,7 @@ android:id="@+id/client_certificate_fingerprint_label" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/certificate_trust_dialog_fingerprint" + android:text="@string/client_certificate_dialog_fingerprint" android:textSize="12sp" android:textColor="?android:textColorSecondary" android:paddingStart="4dp" @@ -229,7 +229,7 @@ diff --git a/app/src/main/res/menu/menu_trusted_certificate_dialog.xml b/app/src/main/res/menu/menu_trusted_certificate_dialog.xml index 27e3d9ae..0eed4986 100644 --- a/app/src/main/res/menu/menu_trusted_certificate_dialog.xml +++ b/app/src/main/res/menu/menu_trusted_certificate_dialog.xml @@ -3,12 +3,12 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d24af1a1..f4df699c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -423,6 +423,20 @@ Exact alarms ntfy can schedule exact alarms. Exact alarms are required to reconnect WebSockets in the background. Click to revoke the permission. ntfy cannot schedule exact alarms. Exact alarms are required to reconnect WebSockets in the background. Click to grant the permission. + Manage certificates + Trust self-signed server certificates and manage client certificates for mTLS + Trusted server certificates + Issued by %1$s, expires %2$s + Issued by %1$s, expired + Add a trusted certificate + Import a server or CA certificate into the trust store (PEM). HTTPS and WebSocket connections will trust this certificate. + Client certificates (mTLS) + Issued by %1$s, expires %2$s, used by %3$s + Issued by %1$s, expired, used by %2$s + Add a client certificate + Import certificate for mutual TLS authentication (PKCS#12). This certificate will be used when connecting to the server. + Invalid certificate file + Invalid PKCS#12 file About Version ntfy %1$s (%2$s) @@ -452,7 +466,7 @@ About Topic URL - + Add user Edit user You can add a user here. All topics for the given server will use this user. @@ -467,7 +481,7 @@ Delete user Save - + Add custom header Edit custom header Service URL @@ -483,73 +497,45 @@ Save Delete - + Certificate details Unknown certificate Add trusted certificate + 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. 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. You\'ve selected a certificate file. Please review the details below before adding it to your trusted certificates. - 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. - Subject - Issuer - SHA-256 Fingerprint - Valid from - Valid until - Warning: This certificate has expired! - Warning: This certificate is not yet valid! - Trust - Cancel + Subject + Issuer + SHA-256 fingerprint + Valid from + Valid until + Warning: This certificate has expired. + Warning: This certificate is not yet valid. + Trust + Delete + Certificate added + Certificate deleted - - Manage certificates - Trust self-signed server certificates and manage client certificates for mTLS - Certificates - Trusted server certificates - Add certificate - Add a trusted certificate - Import a server or CA certificate into the trust store (PEM format). HTTPS and WebSocket connections will trust this certificate. - Client certificates (mTLS) - Add client certificate - Add a client certificate - Import certificate for mutual TLS authentication (PKCS#12 format). ntfy will use this certificate when connecting to the server. - Client certificate configured - Expires %1$s - Expired - Expires %1$s - Used by %1$s, %2$s - - - Certificate details - Add trusted certificate - Add client certificate - Add a trusted server or CA certificate. Connections to the service URL will trust this certificate. - Add a client certificate for mTLS authentication (PEM). The certificate will be used when connecting to the service. - Service URL (e.g. https://ntfy.example.com) - Select certificate file (.pem, .crt) - Select client certificate (.p12) - Certificate password - No file selected - Add - Delete - Cancel + Client certificate Add client certificate - Next - Save Enter the password for the PKCS#12 file and the server URL this certificate should be used for. Review the certificate details and save to add this client certificate. Password Server URL (e.g. https://example.com) + Subject + Issuer + SHA-256 Fingerprint + Valid from + Valid until + Next + Save + Delete + Cancel + Delete this certificate? + Certificate added + Certificate deleted Wrong password or invalid PKCS#12 file - Invalid certificate file - Invalid PKCS#12 file - Invalid password or corrupted PKCS#12 file - Please enter a service URL - Invalid service URL - Please select a certificate file - Please select a PKCS#12 file - Please enter the certificate password - Delete this certificate? - Certificate added - Certificate deleted + Invalid password or corrupted PKCS#12 file + Invalid service URL diff --git a/app/src/main/res/xml/certificate_preferences.xml b/app/src/main/res/xml/certificate_preferences.xml index d4384091..654e6076 100644 --- a/app/src/main/res/xml/certificate_preferences.xml +++ b/app/src/main/res/xml/certificate_preferences.xml @@ -1,6 +1,6 @@ + app:title="@string/settings_advanced_certificates_title">