From 74711292fa92beca29a05aa5d4cb13267d512402 Mon Sep 17 00:00:00 2001 From: M66B Date: Wed, 6 Apr 2022 18:21:01 +0200 Subject: [PATCH] jDKIM --- ATTRIBUTION.md | 1 + app/build.gradle | 10 +- app/src/main/assets/ATTRIBUTION.md | 1 + .../java/eu/faircode/email/MessageHelper.java | 265 ++++-------------- 4 files changed, 70 insertions(+), 207 deletions(-) diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 59a6bd3681..dc47111dc4 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -42,3 +42,4 @@ FairEmail uses: * [Lato font](https://fonts.google.com/specimen/Lato). By Łukasz Dziedzic. [Apache License 2.0](https://fonts.google.com/specimen/Lato#license). * [Caladea font](https://fonts.google.com/specimen/Caladea). By Andrés Torresi, Carolina Giovanolli. [Apache License 2.0](https://fonts.google.com/specimen/Caladea#license). * [Apache Commons Compress](https://commons.apache.org/proper/commons-compress/). Copyright © 2002-2021 The Apache Software Foundation. All Rights Reserved. [Apache License 2.0](https://www.apache.org/licenses/). +* [Apache JAMES jDKIM](https://github.com/Bedework/apache-jdkim). Copyright 2009-2010 The Apache Software Foundation. [Apache License 2.0](https://github.com/Bedework/apache-jdkim/blob/master/LICENSE) diff --git a/app/build.gradle b/app/build.gradle index d88df8fdb1..fcbbe2b0f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -99,13 +99,15 @@ android { 'META-INF/COPYRIGHT.html', 'META-INF/LICENSE.gpl.txt', 'META-INF/LICENSE.commercial.txt', + 'META-INF/README', + 'META-INF/DEPENDENCIES', + 'META-INF/INDEX.LIST', 'LICENSE-2.0.txt', 'RELEASE.txt' ] } } - // https://google.github.io/android-gradle-dsl/current/com.android.build.gradle.internal.dsl.SigningConfig.html signingConfigs { release { @@ -582,4 +584,10 @@ dependencies { // https://commons.apache.org/proper/commons-compress/ // https://mvnrepository.com/artifact/org.apache.commons/commons-compress implementation "org.apache.commons:commons-compress:$compress_version" + + implementation("org.bedework:apache-jdkim:0.6") { + exclude group: "org.apache.geronimo.specs", module: "geronimo-activation_1.1_spec" + exclude group: "org.apache.geronimo.javamail", module: "geronimo-javamail_1.4_mail" + exclude group: "dnsjava", module: "dnsjava" + } } diff --git a/app/src/main/assets/ATTRIBUTION.md b/app/src/main/assets/ATTRIBUTION.md index 59a6bd3681..dc47111dc4 100644 --- a/app/src/main/assets/ATTRIBUTION.md +++ b/app/src/main/assets/ATTRIBUTION.md @@ -42,3 +42,4 @@ FairEmail uses: * [Lato font](https://fonts.google.com/specimen/Lato). By Łukasz Dziedzic. [Apache License 2.0](https://fonts.google.com/specimen/Lato#license). * [Caladea font](https://fonts.google.com/specimen/Caladea). By Andrés Torresi, Carolina Giovanolli. [Apache License 2.0](https://fonts.google.com/specimen/Caladea#license). * [Apache Commons Compress](https://commons.apache.org/proper/commons-compress/). Copyright © 2002-2021 The Apache Software Foundation. All Rights Reserved. [Apache License 2.0](https://www.apache.org/licenses/). +* [Apache JAMES jDKIM](https://github.com/Bedework/apache-jdkim). Copyright 2009-2010 The Apache Software Foundation. [Apache License 2.0](https://github.com/Bedework/apache-jdkim/blob/master/LICENSE) diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index 16a9c65191..61c4d97f58 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -30,7 +30,6 @@ import android.text.TextUtils; import android.util.Base64; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.core.net.MailTo; import androidx.core.util.PatternsCompat; import androidx.documentfile.provider.DocumentFile; @@ -56,6 +55,12 @@ import org.apache.commons.compress.archivers.ArchiveInputStream; import org.apache.commons.compress.archivers.ArchiveStreamFactory; import org.apache.commons.compress.archivers.zip.UnsupportedZipFeatureException; import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream; +import org.apache.james.jdkim.DKIMVerifier; +import org.apache.james.jdkim.api.Headers; +import org.apache.james.jdkim.api.PublicKeyRecordRetriever; +import org.apache.james.jdkim.api.SignatureRecord; +import org.apache.james.jdkim.exceptions.PermFailException; +import org.apache.james.jdkim.exceptions.TempFailException; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; import org.jsoup.nodes.Node; @@ -83,12 +88,7 @@ import java.net.URLDecoder; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; -import java.security.KeyFactory; -import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.PublicKey; -import java.security.Signature; -import java.security.spec.X509EncodedKeySpec; import java.text.Normalizer; import java.text.ParsePosition; import java.util.ArrayList; @@ -1879,214 +1879,67 @@ public class MessageHelper { boolean verifyDKIM(Context context) throws MessagingException { ensureHeaders(); - // https://datatracker.ietf.org/doc/html/rfc6376/ - String[] dkims = imessage.getHeader("DKIM-Signature"); - if (dkims == null || dkims.length < 1) - return false; - - Address[] from = getFrom(); - if (from == null || from.length != 1 || !(from[0] instanceof InternetAddress)) - return false; - - String sender = ((InternetAddress) from[0]).getAddress(); - String dsender = UriHelper.getEmailDomain(sender); - if (TextUtils.isEmpty(dsender)) - return false; - - boolean verified = false; - for (String dkim : dkims) - try { - String udkim = MimeUtility.unfold(dkim); - //String udkim = dkim.replaceAll("\\r?\\n[ \\t]", " "); - Log.i("DKIM " + udkim.replaceAll("\\r?\\n", "|")); - Map kv = getKeyValues(udkim); - - String v = kv.get("v"); // version - if (TextUtils.isEmpty(v)) - throw new IllegalArgumentException("DKIM v missing"); - if (!"1".equals(v)) - throw new IllegalArgumentException("DKIM v invalid=" + v); - - String a = kv.get("a"); // algorithm - if (TextUtils.isEmpty(a)) - throw new IllegalArgumentException("DKIM a missing"); - - String algo; - String hash; - String sign; - if ("rsa-sha1".equalsIgnoreCase(a)) { - algo = "RSA"; - hash = "SHA-1"; - sign = "SHA1withRSA"; - } else if ("rsa-sha256".equalsIgnoreCase(a)) { - algo = "RSA"; - hash = "SHA-256"; - sign = "SHA256withRSA"; - } else - throw new IllegalArgumentException("DKIM a unsupported=" + a); - - Log.i("DKIM algo=" + algo + " hash=" + hash + " sign=" + sign); - - String q = kv.get("q"); - if (!TextUtils.isEmpty(q) && !"dns/txt".equals(q)) - throw new IllegalArgumentException("DKIM q invalid=" + q); - - String c = kv.get("c"); // canonicalization - if (TextUtils.isEmpty(c)) - throw new IllegalArgumentException("DKIM c missing"); - - String[] canon = c.split("/"); - if (canon.length != 2) - throw new IllegalArgumentException("DKIM c invalid=" + c); - - String s = kv.get("s"); // selector - if (TextUtils.isEmpty(s)) - throw new IllegalArgumentException("DKIM s missing"); - - // TODO: check - // TODO: i - String d = kv.get("d"); // domain - if (TextUtils.isEmpty(d)) - throw new IllegalArgumentException("DKIM d missing"); - - if (!dsender.equalsIgnoreCase(d) && false) { - Log.w("DKIM domain=" + dsender + "/" + d); - continue; - } - - // TODO: t - - String h = kv.get("h"); // signed headers - if (TextUtils.isEmpty(h)) - throw new IllegalArgumentException("DKIM h missing"); - h = h.replaceAll("\\s+", ""); - Log.i("DKIM headers=" + h); - - String bh = kv.get("bh"); // Body hash - if (TextUtils.isEmpty(bh)) - throw new IllegalArgumentException("DKIM bh missing"); - bh = bh.replaceAll("\\s+", ""); - - String b = kv.get("b"); // signature - if (TextUtils.isEmpty(b)) - throw new IllegalArgumentException("DKIM b missing"); - - b = b.replaceAll("\\s+", ""); - Log.i("DKIM signature=" + b); - - // Lookup public key - String dns = s + "._domainkey." + d; - Log.i("DKIM lookup " + dns); - DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, dns, "txt"); - if (records.length == 0) - throw new IllegalArgumentException("DKIM domain key missing"); - - Log.i("DKIM got " + records[0].name); - Map dk = getKeyValues(records[0].name); - - String p = dk.get("p"); - if (TextUtils.isEmpty(p)) - throw new IllegalArgumentException("DKIM public key missing"); - - p = p.replaceAll("\\s+", ""); - Log.i("DKIM pubkey=" + p); - - // Build headers - List names = new ArrayList<>(); - for (String name : h.split(":")) - if (!names.contains(name) && !"DKIM-Signature".equalsIgnoreCase(name)) - names.add(name); - names.add("DKIM-Signature"); - - StringBuilder headers = new StringBuilder(); - for (String name : names) { - String[] mh = ("DKIM-Signature".equals(name) - ? new String[]{udkim} - : imessage.getHeader(name)); - - if (mh == null || mh.length == 0) { - Log.w("DKIM header missing='" + name + "'"); - continue; + try { + // https://datatracker.ietf.org/doc/html/rfc6376/ + DKIMVerifier jdkim = new DKIMVerifier(new PublicKeyRecordRetriever() { + @Override + public List getRecords(CharSequence methodAndOptions, CharSequence selector, CharSequence token) + throws TempFailException, PermFailException { + if (methodAndOptions == null || + !"dns/txt".equalsIgnoreCase(methodAndOptions.toString())) + throw new PermFailException("Query method=" + methodAndOptions); + try { + String query = selector + "._domainkey." + token; + DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, query, "txt"); + List result = new ArrayList<>(); + for (DnsHelper.DnsRecord record : records) + result.add(record.name); + return result; + } catch (Exception ex) { + Log.w(ex); + throw new PermFailException("dns/lookup", ex); } + } + }); + List records = jdkim.verify(new Headers() { + @Override + public List getFields() { + Log.e("DKIM getFields"); + throw new IllegalArgumentException("getFields"); + } - for (int i = mh.length - 1; i >= 0; i--) { - String value = mh[i]; - if ("DKIM-Signature".equals(name)) { - int idx = value.lastIndexOf("b="); - if (idx < 0) - throw new IllegalArgumentException("DKIM b missing"); - value = value.substring(0, idx + 2); - } - - if ("simple".equals(canon[0])) - headers.append(name).append(": ") - .append(value); - else if ("relaxed".equals(canon[0])) { - value = MimeUtility.unfold(value); - headers.append(name.trim().toLowerCase()).append(':') - .append(value.replaceAll("\\s+", " ").trim()); - } else - throw new IllegalArgumentException("DKIM header/c invalid=" + canon[0]); - - if (!"DKIM-Signature".equals(name)) - headers.append("\r\n"); + @Override + public List getFields(String name) { + try { + List result = new ArrayList<>(); + String[] headers = imessage.getHeader(name); + if (headers != null) + for (String header : headers) + result.add(name + ": " + header); + return result; + } catch (MessagingException ex) { + Log.e(ex); + return new ArrayList<>(); } } - Log.i("DKIM hash=" + headers.toString().replaceAll("\\r?\\n", "|")); + }, imessage.getRawInputStream()); - // Get body - // TODO l - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - Helper.copy(imessage.getRawInputStream(), bos); - String body = bos.toString(StandardCharsets.UTF_8.name()); - - if ("simple".equals(canon[1])) { - if (TextUtils.isEmpty(body)) - body = "\r\n"; - else if (!body.endsWith("\r\n")) - body += "\r\n"; - else { - while (body.endsWith("\r\n\r\n")) - body = body.substring(0, body.length() - 2); - } - } else if ("relaxed".equals(canon[1])) { - if (TextUtils.isEmpty(body)) - body = ""; - else { - body = body.replaceAll("[ \\t]+\r\n", "\r\n"); - body = body.replaceAll("[ \\t]+", " "); - while (body.endsWith("\r\n\r\n")) - body = body.substring(0, body.length() - 2); - if ("\r\n".equals(body)) - body = ""; - } - } else - throw new IllegalArgumentException("DKIM header/c invalid=" + canon[1]); - - byte[] _lbh = MessageDigest.getInstance(hash).digest(body.getBytes(StandardCharsets.UTF_8.name())); - String lbh = Base64.encodeToString(_lbh, Base64.NO_WRAP); - if (!bh.equals(lbh)) - throw new IllegalArgumentException("DKIM bh invalid " + lbh + "/" + bh); - - X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.decode(p, Base64.DEFAULT)); - KeyFactory keyFactory = KeyFactory.getInstance(algo); - PublicKey pubKey = keyFactory.generatePublic(pubKeySpec); - Signature sig = Signature.getInstance(sign); - sig.initVerify(pubKey); - sig.update(headers.toString().getBytes()); - boolean valid = sig.verify(Base64.decode(b, Base64.DEFAULT)); - Log.i("DKIM valid=" + valid); - if (valid) - verified = true; - else - throw new IllegalArgumentException("DKIM invalid"); - } catch (Throwable ex) { - Log.e("DKIM", ex); + if (records == null) return false; + + for (SignatureRecord record : records) { + String hash = record.getHashAlgo().toString(); + if ("sha-1".equalsIgnoreCase(hash)) + throw new IllegalArgumentException("hash=" + hash); + if (!"sha-256".equalsIgnoreCase(hash)) + Log.w("DKIM hash=" + hash); } - Log.i("DKIM verified=" + verified); - return verified; + return (records.size() > 0); + } catch (Throwable ex) { + Log.e("DKIM", ex); + return false; + } } Address[] getMailFrom(String[] headers) {