From 459a545a400c23afb44da2732a3f5ed0b4e291ff Mon Sep 17 00:00:00 2001 From: M66B Date: Sat, 30 Dec 2023 14:22:24 +0100 Subject: [PATCH] Added certificate transparency --- app/build.gradle | 4 +++ .../java/eu/faircode/email/DebugHelper.java | 2 ++ .../java/eu/faircode/email/EmailService.java | 10 +++---- .../email/FragmentOptionsConnection.java | 21 ++++++++++++++- .../main/java/eu/faircode/email/Helper.java | 1 + .../java/eu/faircode/email/SSLHelper.java | 17 ++++++++++-- .../eu/faircode/email/ServiceSynchronize.java | 2 +- .../layout/fragment_options_connection.xml | 26 ++++++++++++++++++- app/src/main/res/values/strings.xml | 1 + 9 files changed, 74 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 49547b49bd..a88a1f0764 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -542,6 +542,7 @@ dependencies { def biometric_version = "1.2.0-alpha05" def billingclient_version = "6.0.1" def playservicesbase_version = "18.2.0"; + def transparency_version = "2.5.19" def javamail_version = "1.6.7" def jsoup_version = "1.16.2" def jsonpath_version = "2.8.0" @@ -689,6 +690,9 @@ dependencies { largeImplementation "com.google.android.gms:play-services-basement:$playservicesbase_version" playImplementation "com.google.android.gms:play-services-basement:$playservicesbase_version" + // https://github.com/appmattus/certificatetransparency + implementation "com.appmattus.certificatetransparency:certificatetransparency-android:$transparency_version" + // https://developer.amazon.com/docs/in-app-purchasing/iap-get-started.html amazonImplementation files('lib/in-app-purchasing-2.0.76.jar') diff --git a/app/src/main/java/eu/faircode/email/DebugHelper.java b/app/src/main/java/eu/faircode/email/DebugHelper.java index de957339b5..36638b5ec5 100644 --- a/app/src/main/java/eu/faircode/email/DebugHelper.java +++ b/app/src/main/java/eu/faircode/email/DebugHelper.java @@ -1117,6 +1117,7 @@ public class DebugHelper { boolean ssl_harden = prefs.getBoolean("ssl_harden", false); boolean ssl_harden_strict = (ssl_harden && prefs.getBoolean("ssl_harden_strict", false)); boolean cert_strict = prefs.getBoolean("cert_strict", true); + boolean cert_transparency = prefs.getBoolean("cert_transparency", false); boolean open_safe = prefs.getBoolean("open_safe", false); size += write(os, "timeout=" + timeout + "s" + (timeout == EmailService.DEFAULT_CONNECT_TIMEOUT ? "" : " !!!") + "\r\n"); @@ -1144,6 +1145,7 @@ public class DebugHelper { size += write(os, "ssl_harden=" + ssl_harden + (ssl_harden ? " !!!" : "") + "\r\n"); size += write(os, "ssl_harden_strict=" + ssl_harden_strict + (ssl_harden_strict ? " !!!" : "") + "\r\n"); size += write(os, "cert_strict=" + cert_strict + (cert_strict ? " !!!" : "") + "\r\n"); + size += write(os, "cert_transparency=" + cert_transparency + (cert_transparency ? " !!!" : "") + "\r\n"); size += write(os, "open_safe=" + open_safe + "\r\n"); size += write(os, "\r\n"); diff --git a/app/src/main/java/eu/faircode/email/EmailService.java b/app/src/main/java/eu/faircode/email/EmailService.java index c01ff78e39..0fcc419514 100644 --- a/app/src/main/java/eu/faircode/email/EmailService.java +++ b/app/src/main/java/eu/faircode/email/EmailService.java @@ -103,6 +103,7 @@ public class EmailService implements AutoCloseable { private boolean ssl_harden; private boolean ssl_harden_strict; private boolean cert_strict; + private boolean cert_transparency; private boolean check_names; private boolean useip; private String ehlo; @@ -191,6 +192,7 @@ public class EmailService implements AutoCloseable { this.ssl_harden = prefs.getBoolean("ssl_harden", false); this.ssl_harden_strict = prefs.getBoolean("ssl_harden_strict", false); this.cert_strict = prefs.getBoolean("cert_strict", true); + this.cert_transparency = prefs.getBoolean("cert_transparency", false); this.check_names = prefs.getBoolean("check_names", !BuildConfig.PLAY_STORE_RELEASE); boolean auth_plain = prefs.getBoolean("auth_plain", true); @@ -452,7 +454,7 @@ public class EmailService implements AutoCloseable { boolean bc = prefs.getBoolean("bouncy_castle", false); boolean fips = prefs.getBoolean("bc_fips", false); factory = new SSLSocketFactoryService( - host, insecure, ssl_harden, strict, cert_strict, check_names, bc, fips, key, chain, fingerprint); + host, insecure, ssl_harden, strict, cert_strict, cert_transparency, check_names, bc, fips, key, chain, fingerprint); properties.put("mail." + protocol + ".ssl.socketFactory", factory); properties.put("mail." + protocol + ".socketFactory.fallback", "false"); properties.put("mail." + protocol + ".ssl.checkserveridentity", "false"); @@ -1034,23 +1036,21 @@ public class EmailService implements AutoCloseable { private boolean secure; private boolean ssl_harden; private boolean ssl_harden_strict; - private boolean cert_strict; private String trustedFingerprint; private SSLSocketFactory factory; private X509Certificate certificate; SSLSocketFactoryService(String host, boolean insecure, - boolean ssl_harden, boolean ssl_harden_strict, boolean cert_strict, boolean check_names, + boolean ssl_harden, boolean ssl_harden_strict, boolean cert_strict, boolean cert_transparency, boolean check_names, boolean bc, boolean fips, PrivateKey key, X509Certificate[] chain, String fingerprint) throws GeneralSecurityException { this.server = host; this.secure = !insecure; this.ssl_harden = ssl_harden; this.ssl_harden_strict = ssl_harden_strict; - this.cert_strict = cert_strict; this.trustedFingerprint = fingerprint; - TrustManager[] tms = SSLHelper.getTrustManagers(server, secure, cert_strict, check_names, trustedFingerprint, + TrustManager[] tms = SSLHelper.getTrustManagers(server, secure, cert_strict, cert_transparency, check_names, trustedFingerprint, new SSLHelper.ITrust() { @Override public void checkServerTrusted(X509Certificate[] chain) { diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java b/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java index 9af093e4db..d624d28835 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsConnection.java @@ -100,6 +100,8 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre private SwitchCompat swSslHarden; private SwitchCompat swSslHardenStrict; private SwitchCompat swCertStrict; + private SwitchCompat swCertTransparency; + private ImageButton ibCertTransparency; private SwitchCompat swCheckNames; private SwitchCompat swOpenSafe; private SwitchCompat swHttpRedirect; @@ -125,7 +127,7 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre "download_headers", "download_eml", "download_plain", "require_validated", "require_validated_captive", "vpn_only", "timeout", "prefer_ip4", "bind_socket", "standalone_vpn", "tcp_keep_alive", - "ssl_update", "ssl_harden", "ssl_harden_strict", "cert_strict", "check_names", + "ssl_update", "ssl_harden", "ssl_harden_strict", "cert_strict", "cert_transparency", "check_names", "open_safe", "http_redirect", "bouncy_castle", "bc_fips" }; @@ -162,6 +164,8 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre swSslHarden = view.findViewById(R.id.swSslHarden); swSslHardenStrict = view.findViewById(R.id.swSslHardenStrict); swCertStrict = view.findViewById(R.id.swCertStrict); + swCertTransparency = view.findViewById(R.id.swCertTransparency); + ibCertTransparency = view.findViewById(R.id.ibCertTransparency); swCheckNames = view.findViewById(R.id.swCheckNames); swOpenSafe = view.findViewById(R.id.swOpenSafe); swHttpRedirect = view.findViewById(R.id.swHttpRedirect); @@ -381,6 +385,20 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre } }); + swCertTransparency.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + prefs.edit().putBoolean("cert_transparency", checked).apply(); + } + }); + + ibCertTransparency.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Helper.view(v.getContext(), Uri.parse(Helper.URI_CERT_TRANSPARENCY), true); + } + }); + swCheckNames.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { @@ -704,6 +722,7 @@ public class FragmentOptionsConnection extends FragmentBase implements SharedPre swSslHardenStrict.setChecked(prefs.getBoolean("ssl_harden_strict", false)); swSslHardenStrict.setEnabled(swSslHarden.isChecked()); swCertStrict.setChecked(prefs.getBoolean("cert_strict", true)); + swCertTransparency.setChecked(prefs.getBoolean("cert_transparency", false)); swCheckNames.setChecked(prefs.getBoolean("check_names", !BuildConfig.PLAY_STORE_RELEASE)); swOpenSafe.setChecked(prefs.getBoolean("open_safe", false)); swHttpRedirect.setChecked(prefs.getBoolean("http_redirect", true)); diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java index b3743a8717..82efb93256 100644 --- a/app/src/main/java/eu/faircode/email/Helper.java +++ b/app/src/main/java/eu/faircode/email/Helper.java @@ -225,6 +225,7 @@ public class Helper { static final String DONTKILL_URI = "https://dontkillmyapp.com/"; static final String URI_SUPPORT_RESET_OPEN = "https://support.google.com/pixelphone/answer/6271667"; static final String URI_SUPPORT_CONTACT_GROUP = "https://support.google.com/contacts/answer/30970"; + static final String URI_CERT_TRANSPARENCY = "https://github.com/appmattus/certificatetransparency/blob/main/docs/what-is-certificate-transparency.md"; // https://developer.android.com/distribute/marketing-tools/linking-to-google-play#PerformingSearch private static final String PLAY_STORE_SEARCH = "https://play.google.com/store/search"; diff --git a/app/src/main/java/eu/faircode/email/SSLHelper.java b/app/src/main/java/eu/faircode/email/SSLHelper.java index d4c0b27d1e..6721153c6d 100644 --- a/app/src/main/java/eu/faircode/email/SSLHelper.java +++ b/app/src/main/java/eu/faircode/email/SSLHelper.java @@ -4,6 +4,10 @@ import android.text.TextUtils; import androidx.annotation.NonNull; +import com.appmattus.certificatetransparency.CTLogger; +import com.appmattus.certificatetransparency.CTTrustManagerBuilder; +import com.appmattus.certificatetransparency.VerificationResult; + import java.net.InetAddress; import java.net.UnknownHostException; import java.security.KeyStore; @@ -21,7 +25,7 @@ import javax.net.ssl.X509TrustManager; public class SSLHelper { static TrustManager[] getTrustManagers( - String server, boolean secure, boolean cert_strict, boolean check_names, String trustedFingerprint, ITrust intf) { + String server, boolean secure, boolean cert_strict, boolean transparency, boolean check_names, String trustedFingerprint, ITrust intf) { TrustManagerFactory tmf; try { tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); @@ -43,7 +47,16 @@ public class SSLHelper { for (TrustManager tm : tms) Log.e("Trust manager " + tm.getClass()); - final X509TrustManager rtm = (X509TrustManager) tms[0]; + CTLogger logger = new CTLogger() { + @Override + public void log(@NonNull String host, @NonNull VerificationResult result) { + Log.w("Transparency: host=" + host + " result=" + result); + } + }; + + final X509TrustManager rtm = (transparency + ? new CTTrustManagerBuilder((X509TrustManager) tms[0]).setLogger(logger).build() + : (X509TrustManager) tms[0]); return new TrustManager[]{new X509TrustManager() { // openssl s_client -connect diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index c281a53032..8b15a09da5 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -172,7 +172,7 @@ public class ServiceSynchronize extends ServiceBase implements SharedPreferences "sync_shared_folders", "download_headers", "download_eml", "prefer_ip4", "bind_socket", "standalone_vpn", "tcp_keep_alive", // force reconnect - "ssl_harden", "ssl_harden_strict", "cert_strict", "check_names", "bouncy_castle", "bc_fips", // force reconnect + "ssl_harden", "ssl_harden_strict", "cert_strict", "cert_transparency", "check_names", "bouncy_castle", "bc_fips", // force reconnect "experiments", "debug", "protocol", // force reconnect "auth_plain", "auth_login", "auth_ntlm", "auth_sasl", "auth_apop", // force reconnect "keep_alive_poll", "empty_pool", "idle_done", // force reconnect diff --git a/app/src/main/res/layout/fragment_options_connection.xml b/app/src/main/res/layout/fragment_options_connection.xml index 1a8d732574..7c84d3972b 100644 --- a/app/src/main/res/layout/fragment_options_connection.xml +++ b/app/src/main/res/layout/fragment_options_connection.xml @@ -533,6 +533,30 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/swCertStrict" /> + + + + Harden SSL connections Require TLS 1.3 Strict certificate checking + Certificate transparency Check host names against server certificates Open secure connections only Allow connection redirection