diff --git a/app/src/main/java/eu/faircode/email/Bimi.java b/app/src/main/java/eu/faircode/email/Bimi.java
new file mode 100644
index 0000000000..b0653ffb5f
--- /dev/null
+++ b/app/src/main/java/eu/faircode/email/Bimi.java
@@ -0,0 +1,235 @@
+package eu.faircode.email;
+
+/*
+ This file is part of FairEmail.
+
+ FairEmail is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ FairEmail is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with FairEmail. If not, see .
+
+ Copyright 2018-2021 by Marcel Bokhorst (M66B)
+*/
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.graphics.Bitmap;
+import android.graphics.Color;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import androidx.preference.PreferenceManager;
+
+import org.bouncycastle.asn1.x509.Extension;
+import org.bouncycastle.util.io.pem.PemObject;
+import org.bouncycastle.util.io.pem.PemReader;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.URL;
+import java.security.KeyStore;
+import java.security.cert.CertPathBuilder;
+import java.security.cert.CertPathBuilderResult;
+import java.security.cert.CertPathValidator;
+import java.security.cert.CertStore;
+import java.security.cert.CertStoreParameters;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateFactory;
+import java.security.cert.CollectionCertStoreParameters;
+import java.security.cert.PKIXBuilderParameters;
+import java.security.cert.TrustAnchor;
+import java.security.cert.X509CertSelector;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import javax.net.ssl.HttpsURLConnection;
+
+public class Bimi {
+ // Beam me up, Scotty
+ private static final int CONNECT_TIMEOUT = 10 * 1000; // milliseconds
+ private static final int READ_TIMEOUT = 15 * 1000; // milliseconds
+ private static final String OID_BrandIndicatorforMessageIdentification = "1.3.6.1.5.5.7.3.31";
+
+ static Pair get(Context context, String domain, String selector, int scaleToPixels)
+ throws IOException {
+ SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+ boolean bimi_vmc = prefs.getBoolean("bimi_vmc", false);
+
+ final String txt = selector + "._bimi." + domain;
+ Log.i("BIMI fetch TXT=" + txt);
+ DnsHelper.DnsRecord[] bimi = DnsHelper.lookup(context, txt, "txt");
+ if (bimi.length == 0)
+ return null;
+ Log.i("BIMI got TXT=" + bimi[0].name);
+
+ Bitmap bitmap = null;
+ boolean verified = !bimi_vmc;
+ String[] params = bimi[0].name.split(";");
+ for (String param : params) {
+ String[] kv = param.split("=");
+ if (kv.length != 2)
+ continue;
+
+ switch (kv[0].trim().toLowerCase()) {
+ case "v": { // Version
+ String version = kv[1].trim();
+ if (!"BIMI1".equalsIgnoreCase(version))
+ Log.w("BIMI unsupported version=" + version);
+ break;
+ }
+
+ case "l": { // Image link
+ if (!bimi_vmc)
+ continue;
+
+ String svg = kv[1].trim();
+ if (TextUtils.isEmpty(svg))
+ continue;
+
+ URL url = new URL(svg);
+
+ Log.i("BIMI favicon " + url);
+ HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setReadTimeout(READ_TIMEOUT);
+ connection.setConnectTimeout(CONNECT_TIMEOUT);
+ connection.setInstanceFollowRedirects(true);
+ connection.setRequestProperty("User-Agent", WebViewEx.getUserAgent(context));
+ connection.connect();
+
+ try {
+ bitmap = ImageHelper.renderSvg(
+ connection.getInputStream(),
+ Color.WHITE, scaleToPixels);
+ } finally {
+ connection.disconnect();
+ }
+
+ break;
+ }
+
+ case "a": { // Certificate link
+ String a = kv[1].trim();
+ if (TextUtils.isEmpty(a))
+ continue;
+
+ URL url = new URL(a);
+
+ try {
+ Log.i("BIMI PEM " + url);
+ HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
+ connection.setRequestMethod("GET");
+ connection.setReadTimeout(READ_TIMEOUT);
+ connection.setConnectTimeout(CONNECT_TIMEOUT);
+ connection.setInstanceFollowRedirects(true);
+ connection.setRequestProperty("User-Agent", WebViewEx.getUserAgent(context));
+ connection.connect();
+
+ // Fetch PEM objects
+ List pems = new ArrayList<>();
+ try {
+ InputStreamReader isr = new InputStreamReader(connection.getInputStream());
+ PemReader reader = new PemReader(isr);
+ while (true) {
+ PemObject pem = reader.readPemObject();
+ if (pem == null)
+ break;
+ else
+ pems.add(pem);
+ }
+ } finally {
+ connection.disconnect();
+ }
+
+ if (pems.size() == 0)
+ throw new IllegalArgumentException("No PEM objects");
+
+ // Convert to X.509 certificates
+ List certs = new ArrayList<>();
+ CertificateFactory fact = CertificateFactory.getInstance("X.509");
+ for (PemObject pem : pems) {
+ ByteArrayInputStream bis = new ByteArrayInputStream(pem.getContent());
+ certs.add((X509Certificate) fact.generateCertificate(bis));
+ }
+
+ // Get first certificate
+ // https://datatracker.ietf.org/doc/draft-fetch-validation-vmc-wchuang/
+ X509Certificate cert = certs.remove(0);
+
+ // Check certificate type
+ List ku = cert.getExtendedKeyUsage();
+ if (!ku.contains(OID_BrandIndicatorforMessageIdentification))
+ throw new IllegalArgumentException("Invalid certificate type");
+
+ // Check subject
+ if (!EntityCertificate.getDnsNames(cert).contains(domain))
+ throw new IllegalArgumentException("Invalid certificate domain");
+
+ // Get trust anchors
+ Set trustAnchors = new HashSet<>();
+ for (String ca : context.getAssets().list(""))
+ if (ca.endsWith(".pem")) {
+ Log.i("Reading ca=" + ca);
+ try (InputStream is = context.getAssets().open(ca)) {
+ X509Certificate c = (X509Certificate) fact.generateCertificate(is);
+ trustAnchors.add(new TrustAnchor(c, null));
+ }
+ }
+
+ KeyStore ks = KeyStore.getInstance("AndroidCAStore");
+ ks.load(null, null);
+ Enumeration aliases = ks.aliases();
+ while (aliases.hasMoreElements()) {
+ String alias = aliases.nextElement();
+ Certificate c = ks.getCertificate(alias);
+ if (c instanceof X509Certificate)
+ trustAnchors.add(new TrustAnchor((X509Certificate) c, null));
+ }
+
+ // https://datatracker.ietf.org/doc/html/rfc3709#page-6
+ // TODO: logoType
+ byte[] logoType = cert.getExtensionValue(Extension.logoType.getId());
+
+ // Validate certificate
+ X509CertSelector target = new X509CertSelector();
+ target.setCertificate(cert);
+
+ PKIXBuilderParameters pparams = new PKIXBuilderParameters(trustAnchors, target);
+ CertStoreParameters intermediates = new CollectionCertStoreParameters(certs);
+ pparams.addCertStore(CertStore.getInstance("Collection", intermediates));
+ pparams.setRevocationEnabled(false);
+ pparams.setDate(null);
+
+ CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
+ CertPathBuilderResult path = builder.build(pparams);
+
+ CertPathValidator cpv = CertPathValidator.getInstance("PKIX");
+ cpv.validate(path.getCertPath(), pparams);
+
+ Log.i("BIMI valid domain=" + domain);
+ verified = true;
+ } catch (Throwable ex) {
+ Log.w(new Throwable("BIMI", ex));
+ }
+ break;
+ }
+ }
+ }
+
+ return (bitmap == null ? null : new Pair<>(bitmap, verified));
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/ContactInfo.java b/app/src/main/java/eu/faircode/email/ContactInfo.java
index 29cb34976b..0d14921a95 100644
--- a/app/src/main/java/eu/faircode/email/ContactInfo.java
+++ b/app/src/main/java/eu/faircode/email/ContactInfo.java
@@ -34,13 +34,11 @@ import android.net.NetworkInfo;
import android.net.Uri;
import android.provider.ContactsContract;
import android.text.TextUtils;
+import android.util.Pair;
import androidx.annotation.NonNull;
import androidx.preference.PreferenceManager;
-import org.bouncycastle.asn1.x509.Extension;
-import org.bouncycastle.util.io.pem.PemObject;
-import org.bouncycastle.util.io.pem.PemReader;
import org.json.JSONArray;
import org.json.JSONObject;
import org.jsoup.nodes.Document;
@@ -48,13 +46,11 @@ import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import java.io.BufferedOutputStream;
-import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
-import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.ConnectException;
@@ -63,33 +59,17 @@ import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
-import java.security.KeyStore;
-import java.security.cert.CertPathBuilder;
-import java.security.cert.CertPathBuilderResult;
-import java.security.cert.CertPathValidator;
import java.security.cert.CertPathValidatorException;
-import java.security.cert.CertStore;
-import java.security.cert.CertStoreParameters;
-import java.security.cert.Certificate;
import java.security.cert.CertificateException;
-import java.security.cert.CertificateFactory;
-import java.security.cert.CollectionCertStoreParameters;
-import java.security.cert.PKIXBuilderParameters;
-import java.security.cert.TrustAnchor;
-import java.security.cert.X509CertSelector;
-import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
-import java.util.Enumeration;
import java.util.HashMap;
-import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
-import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
@@ -273,7 +253,6 @@ public class ContactInfo {
boolean avatars = prefs.getBoolean("avatars", true);
boolean gravatars = prefs.getBoolean("gravatars", false);
boolean bimi = prefs.getBoolean("bimi", false);
- boolean bimi_vmc = prefs.getBoolean("bimi_vmc", false);
boolean favicons = prefs.getBoolean("favicons", false);
boolean generated = prefs.getBoolean("generated_icons", true);
boolean identicons = prefs.getBoolean("identicons", false);
@@ -412,168 +391,9 @@ public class ContactInfo {
@Override
public Favicon call() throws Exception {
// TODO: BIMI selector
- final String txt = "default._bimi." + _domain;
- Log.i("BIMI fetch TXT=" + txt);
- DnsHelper.DnsRecord[] bimi = DnsHelper.lookup(context, txt, "txt");
- if (bimi.length == 0)
- return null;
- Log.i("BIMI got TXT=" + bimi[0].name);
-
- Bitmap bitmap = null;
- boolean verified = !bimi_vmc;
- String[] params = bimi[0].name.split(";");
- for (String param : params) {
- String[] kv = param.split("=");
- if (kv.length != 2)
- continue;
-
- switch (kv[0].trim().toLowerCase()) {
- case "v": { // Version
- String version = kv[1].trim();
- if (!"BIMI1".equalsIgnoreCase(version))
- Log.w("BIMI unsupported version=" + version);
- break;
- }
-
- case "l": { // Image link
- if (!bimi_vmc)
- continue;
-
- String svg = kv[1].trim();
- if (TextUtils.isEmpty(svg))
- continue;
-
- URL url = new URL(svg);
-
- Log.i("BIMI favicon " + url);
- HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
- connection.setRequestMethod("GET");
- connection.setReadTimeout(FAVICON_READ_TIMEOUT);
- connection.setConnectTimeout(FAVICON_CONNECT_TIMEOUT);
- connection.setInstanceFollowRedirects(true);
- connection.setRequestProperty("User-Agent", WebViewEx.getUserAgent(context));
- connection.connect();
-
- try {
- bitmap = ImageHelper.renderSvg(
- connection.getInputStream(),
- Color.WHITE, scaleToPixels);
- } finally {
- connection.disconnect();
- }
-
- break;
- }
-
- case "a": { // Certificate link
- String a = kv[1].trim();
- if (TextUtils.isEmpty(a))
- continue;
-
- URL url = new URL(a);
-
- try {
- Log.i("BIMI PEM " + url);
- HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();
- connection.setRequestMethod("GET");
- connection.setReadTimeout(FAVICON_READ_TIMEOUT);
- connection.setConnectTimeout(FAVICON_CONNECT_TIMEOUT);
- connection.setInstanceFollowRedirects(true);
- connection.setRequestProperty("User-Agent", WebViewEx.getUserAgent(context));
- connection.connect();
-
- // Fetch PEM objects
- List pems = new ArrayList<>();
- try {
- InputStreamReader isr = new InputStreamReader(connection.getInputStream());
- PemReader reader = new PemReader(isr);
- while (true) {
- PemObject pem = reader.readPemObject();
- if (pem == null)
- break;
- else
- pems.add(pem);
- }
- } finally {
- connection.disconnect();
- }
-
- if (pems.size() == 0)
- throw new IllegalArgumentException("No PEM objects");
-
- // Convert to X.509 certificates
- List certs = new ArrayList<>();
- CertificateFactory fact = CertificateFactory.getInstance("X.509");
- for (PemObject pem : pems) {
- ByteArrayInputStream bis = new ByteArrayInputStream(pem.getContent());
- certs.add((X509Certificate) fact.generateCertificate(bis));
- }
-
- // Get first certificate
- // https://datatracker.ietf.org/doc/draft-fetch-validation-vmc-wchuang/
- X509Certificate cert = certs.remove(0);
-
- // Check certificate type
- List ku = cert.getExtendedKeyUsage();
- if (!ku.contains(EntityCertificate.OID_BrandIndicatorforMessageIdentification))
- throw new IllegalArgumentException("Invalid certificate type");
-
- // Check subject
- if (!EntityCertificate.getDnsNames(cert).contains(_domain))
- throw new IllegalArgumentException("Invalid certificate domain");
-
- // Get trust anchors
- Set trustAnchors = new HashSet<>();
- for (String ca : context.getAssets().list(""))
- if (ca.endsWith(".pem")) {
- Log.i("Reading ca=" + ca);
- try (InputStream is = context.getAssets().open(ca)) {
- X509Certificate c = (X509Certificate) fact.generateCertificate(is);
- trustAnchors.add(new TrustAnchor(c, null));
- }
- }
-
- KeyStore ks = KeyStore.getInstance("AndroidCAStore");
- ks.load(null, null);
- Enumeration aliases = ks.aliases();
- while (aliases.hasMoreElements()) {
- String alias = aliases.nextElement();
- Certificate c = ks.getCertificate(alias);
- if (c instanceof X509Certificate)
- trustAnchors.add(new TrustAnchor((X509Certificate) c, null));
- }
-
- // https://datatracker.ietf.org/doc/html/rfc3709#page-6
- byte[] logoType = cert.getExtensionValue(Extension.logoType.getId());
- // TODO: decode
-
- // Validate certificate
- X509CertSelector target = new X509CertSelector();
- target.setCertificate(cert);
-
- PKIXBuilderParameters pparams = new PKIXBuilderParameters(trustAnchors, target);
- CertStoreParameters intermediates = new CollectionCertStoreParameters(certs);
- pparams.addCertStore(CertStore.getInstance("Collection", intermediates));
- pparams.setRevocationEnabled(false);
- pparams.setDate(null);
-
- CertPathBuilder builder = CertPathBuilder.getInstance("PKIX");
- CertPathBuilderResult path = builder.build(pparams);
-
- CertPathValidator cpv = CertPathValidator.getInstance("PKIX");
- cpv.validate(path.getCertPath(), pparams);
-
- Log.i("BIMI valid domain=" + _domain);
- verified = true;
- } catch (Throwable ex) {
- Log.w(new Throwable("BIMI", ex));
- }
- break;
- }
- }
- }
-
- return (bitmap == null ? null : new Favicon(bitmap, verified));
+ Pair bimi =
+ Bimi.get(context, _domain, "default", scaleToPixels);
+ return (bimi == null ? null : new Favicon(bimi.first, bimi.second));
}
}));
}
diff --git a/app/src/main/java/eu/faircode/email/EntityCertificate.java b/app/src/main/java/eu/faircode/email/EntityCertificate.java
index dd42be9b6f..726ae9f9db 100644
--- a/app/src/main/java/eu/faircode/email/EntityCertificate.java
+++ b/app/src/main/java/eu/faircode/email/EntityCertificate.java
@@ -79,8 +79,6 @@ public class EntityCertificate {
@NonNull
public String data;
- static final String OID_BrandIndicatorforMessageIdentification = "1.3.6.1.5.5.7.3.31";
-
static EntityCertificate from(X509Certificate certificate, String email) throws CertificateEncodingException, NoSuchAlgorithmException {
return from(certificate, false, email);
}