PoC: DKIM verification

This commit is contained in:
M66B 2022-04-05 16:18:22 +02:00
parent db0267e3e0
commit 2ac38a3e3d
5 changed files with 261 additions and 3 deletions

View File

@ -3818,6 +3818,7 @@ class Core {
boolean download_plain = prefs.getBoolean("download_plain", false);
boolean notify_known = prefs.getBoolean("notify_known", false);
boolean experiments = prefs.getBoolean("experiments", false);
boolean dkim_verify = prefs.getBoolean("dkim_verify", false);
boolean pro = ActivityBilling.isPro(context);
long uid = ifolder.getUID(imessage);
@ -3971,7 +3972,10 @@ class Core {
message.tls = helper.getTLS();
message.dkim = MessageHelper.getAuthentication("dkim", authentication);
if (Boolean.TRUE.equals(message.dkim))
message.dkim = helper.checkDKIMRequirements();
if (!BuildConfig.PLAY_STORE_RELEASE && dkim_verify)
message.dkim = helper.verifyDKIM(context);
else
message.dkim = helper.checkDKIMRequirements();
message.spf = MessageHelper.getAuthentication("spf", authentication);
if (message.spf == null && helper.getSPF())
message.spf = true;

View File

@ -171,6 +171,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
private SwitchCompat swLogarithmicBackoff;
private SwitchCompat swExactAlarms;
private SwitchCompat swInfra;
private SwitchCompat swDkimVerify;
private SwitchCompat swDupMsgId;
private SwitchCompat swTestIab;
private Button btnImportProviders;
@ -211,7 +212,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
"use_modseq", "uid_command", "perform_expunge", "uid_expunge",
"auth_plain", "auth_login", "auth_ntlm", "auth_sasl", "auth_apop",
"keep_alive_poll", "empty_pool", "idle_done", "logarithmic_backoff",
"exact_alarms", "infra", "dup_msgids", "test_iab"
"exact_alarms", "infra", "dkim_verify", "dup_msgids", "test_iab"
};
private final static String[] RESET_QUESTIONS = new String[]{
@ -336,6 +337,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
swLogarithmicBackoff = view.findViewById(R.id.swLogarithmicBackoff);
swExactAlarms = view.findViewById(R.id.swExactAlarms);
swInfra = view.findViewById(R.id.swInfra);
swDkimVerify = view.findViewById(R.id.swDkimVerify);
swDupMsgId = view.findViewById(R.id.swDupMsgId);
swTestIab = view.findViewById(R.id.swTestIab);
btnImportProviders = view.findViewById(R.id.btnImportProviders);
@ -1136,6 +1138,14 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
}
});
swDkimVerify.setVisibility(BuildConfig.PLAY_STORE_RELEASE ? View.GONE : View.VISIBLE);
swDkimVerify.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
prefs.edit().putBoolean("dkim_verify", checked).apply();
}
});
swDupMsgId.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
@Override
public void onCheckedChanged(CompoundButton compoundButton, boolean checked) {
@ -1727,6 +1737,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc
swLogarithmicBackoff.setChecked(prefs.getBoolean("logarithmic_backoff", true));
swExactAlarms.setChecked(prefs.getBoolean("exact_alarms", true));
swInfra.setChecked(prefs.getBoolean("infra", false));
swDkimVerify.setChecked(prefs.getBoolean("dkim_verify", false));
swDupMsgId.setChecked(prefs.getBoolean("dup_msgids", false));
swTestIab.setChecked(prefs.getBoolean("test_iab", false));

View File

@ -83,7 +83,12 @@ 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;
@ -1871,6 +1876,219 @@ public class MessageHelper {
return true;
}
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<String, String> 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<String, String> 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<String> 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;
}
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");
}
}
Log.i("DKIM hash=" + headers.toString().replaceAll("\\r?\\n", "|"));
// 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);
return false;
}
Log.i("DKIM verified=" + verified);
return verified;
}
Address[] getMailFrom(String[] headers) {
if (headers == null)
return null;

View File

@ -1356,6 +1356,30 @@
app:srcCompat="@drawable/infra_zoho"
tools:ignore="MissingConstraints" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/swDkimVerify"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:text="@string/title_advanced_dkim_verify"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/flowInfra"
app:switchPadding="12dp" />
<eu.faircode.email.FixedTextView
android:id="@+id/tvDkimVerifyHint"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="48dp"
android:text="@string/title_advanced_usage_hint"
android:textAppearance="@style/TextAppearance.AppCompat.Small"
android:textColor="?attr/colorWarning"
android:textStyle="italic"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/swDkimVerify" />
<androidx.appcompat.widget.SwitchCompat
android:id="@+id/swDupMsgId"
android:layout_width="0dp"
@ -1364,7 +1388,7 @@
android:text="@string/title_advanced_dup_msgid"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/flowInfra"
app:layout_constraintTop_toBottomOf="@id/tvDkimVerifyHint"
app:switchPadding="12dp" />
<androidx.appcompat.widget.SwitchCompat

View File

@ -742,6 +742,7 @@
<string name="title_advanced_empty_pool" translatable="false">Empty connection pool</string>
<string name="title_advanced_exact_alarms" translatable="false">Use exact timers</string>
<string name="title_advanced_infra" translatable="false">Show infrastructure</string>
<string name="title_advanced_dkim_verify" translatable="false">DKIM verification</string>
<string name="title_advanced_dup_msgid" translatable="false">Duplicates by message ID</string>
<string name="title_advanced_test_iab" translatable="false">Test IAB</string>
<string name="title_advanced_import_providers" translatable="false">Import providers</string>