Show message signers

This commit is contained in:
M66B
2023-02-13 21:54:33 +01:00
parent bf91c80b7e
commit 1bd7b4cfdb
10 changed files with 3195 additions and 175 deletions

View File

@@ -1989,177 +1989,182 @@ public class MessageHelper {
return true;
}
Boolean verifyDKIM(Context context) throws MessagingException, IOException {
if (!(imessage instanceof IMAPMessage))
return null;
@NonNull
List<String> verifyDKIM(Context context) {
List<String> signers = new ArrayList<>();
// Workaround reformatted headers
Properties props = MessageHelper.getSessionProperties(true);
Session isession = Session.getInstance(props, null);
MimeMessage amessage = new MimeMessage(isession, ((IMAPMessage) imessage).getMimeStream());
try {
// Workaround reformatted headers
Properties props = MessageHelper.getSessionProperties(true);
Session isession = Session.getInstance(props, null);
MimeMessage amessage = new MimeMessage(isession, ((IMAPMessage) imessage).getMimeStream());
// https://datatracker.ietf.org/doc/html/rfc6376/
String[] headers = amessage.getHeader("DKIM-Signature");
if (headers == null || headers.length < 1)
return null;
// https://datatracker.ietf.org/doc/html/rfc6376/
String[] headers = amessage.getHeader("DKIM-Signature");
if (headers == null || headers.length < 1)
return signers;
boolean valid = false;
for (String header : headers) {
Map<String, String> kv = getKeyValues(MimeUtility.unfold(header));
for (String header : headers) {
Map<String, String> kv = getKeyValues(MimeUtility.unfold(header));
String a = kv.get("a");
String halgo;
String salgo;
if ("rsa-sha1".equals(a)) {
halgo = "SHA-1";
salgo = "SHA1withRSA";
} else if ("rsa-sha256".equals(a)) {
halgo = "SHA-256";
salgo = "SHA256withRSA";
} else {
Log.i("DKIM a=" + a);
return false;
}
try {
String dns = kv.get("s") + "._domainkey." + kv.get("d");
Log.i("DKIM lookup " + dns);
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, dns, "txt");
if (records.length > 0) {
Log.i("DKIM got " + records[0].name);
Map<String, String> dk = getKeyValues(records[0].name);
Log.i("DKIM canonicalization=" + kv.get("c"));
String[] c = kv.get("c").split("/");
StringBuilder head = new StringBuilder();
String hs = kv.get("h");
Log.i("DKIM headers=" + hs);
boolean from = false;
List<String> _h = new ArrayList<>();
for (String key : hs.split(":")) {
_h.add(key.trim());
from = (from || "from".equalsIgnoreCase(key.trim()));
}
if (!from)
throw new IllegalArgumentException("from missing: " + hs);
_h.add("DKIM-Signature");
Map<String, Integer> index = new Hashtable<>();
for (String n : _h) {
// https://datatracker.ietf.org/doc/html/rfc6376/#section-5.4.2
String _n = n.toLowerCase(Locale.ROOT);
Integer idx = index.get(_n);
idx = (idx == null ? 1 : idx + 1);
index.put(_n, idx);
String[] h = ("DKIM-Signature".equals(n) ? new String[]{header} : amessage.getHeader(n));
if (h == null || idx > h.length) {
// https://datatracker.ietf.org/doc/html/rfc6376/#section-5.4
Log.i("DKIM missing header=" + n + "[" + idx + "/" + (h == null ? null : h.length) + "]");
continue;
}
String v = h[h.length - idx];
if ("DKIM-Signature".equals(n)) {
int b = v.lastIndexOf("b=");
int s = v.indexOf(";", b + 2);
v = v.substring(0, b + 2) + (s < 0 ? "" : v.substring(s));
} else
Log.i("DKIM " + n + "=" + v.replaceAll("\\r?\\n", "|"));
if ("simple".equals(c[0])) {
if ("DKIM-Signature".equals(n))
head.append(n).append(": ").append(v);
else {
// Find original header/name
Enumeration<Header> oheaders = amessage.getAllHeaders();
while (oheaders.hasMoreElements()) {
Header oheader = oheaders.nextElement();
if (n.equalsIgnoreCase(oheader.getName()))
head.append(oheader.getName()).append(": ")
.append(oheader.getValue());
}
}
} else if ("relaxed".equals(c[0])) {
v = MimeUtility.unfold(v);
head.append(_n).append(':')
.append(v.replaceAll("\\s+", " ").trim());
} else
throw new IllegalArgumentException(c[0]);
if (!"DKIM-Signature".equals(n))
head.append("\r\n");
}
Log.i("DKIM head=" + head.toString().replace("\r\n", "|"));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Helper.copy(amessage.getRawInputStream(), bos);
String body = bos.toString(); // TODO: charset
if ("simple".equals(c[c.length > 1 ? 1 : 0])) {
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(c[c.length > 1 ? 1 : 0])) {
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(c[1]);
Log.i("DKIM body=" + body.replace("\r\n", "|"));
byte[] bh = MessageDigest.getInstance(halgo).digest(body.getBytes()); // TODO: charset
Log.i("DKIM bh=" + Base64.encodeToString(bh, Base64.NO_WRAP) + "/" + kv.get("bh"));
String p = dk.get("p").replaceAll("\\s+", "");
Log.i("DKIM pubkey=" + p);
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.decode(p, Base64.DEFAULT));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(pubKeySpec);
Signature sig = Signature.getInstance(salgo); // a=
String s = kv.get("b").replaceAll("\\s+", "");
Log.i("DKIM signature=" + s);
byte[] signature = Base64.decode(s, Base64.DEFAULT);
// TODO: check signature length
sig.initVerify(pubKey);
sig.update(head.toString().getBytes());
boolean verified = sig.verify(signature);
Log.i("DKIM valid=" + verified +
" dns=" + dns +
" from=" + formatAddresses(getFrom()));
if (verified)
valid = true;
String a = kv.get("a");
String halgo;
String salgo;
if ("rsa-sha1".equals(a)) {
halgo = "SHA-1";
salgo = "SHA1withRSA";
} else if ("rsa-sha256".equals(a)) {
halgo = "SHA-256";
salgo = "SHA256withRSA";
} else {
Log.i("DKIM a=" + a);
continue;
}
try {
String signer = kv.get("d");
String dns = kv.get("s") + "._domainkey." + signer;
Log.i("DKIM lookup " + dns);
DnsHelper.DnsRecord[] records = DnsHelper.lookup(context, dns, "txt");
if (records.length > 0) {
Log.i("DKIM got " + records[0].name);
Map<String, String> dk = getKeyValues(records[0].name);
Log.i("DKIM canonicalization=" + kv.get("c"));
String[] c = kv.get("c").split("/");
StringBuilder head = new StringBuilder();
String hs = kv.get("h");
Log.i("DKIM headers=" + hs);
boolean from = false;
List<String> _h = new ArrayList<>();
for (String key : hs.split(":")) {
_h.add(key.trim());
from = (from || "from".equalsIgnoreCase(key.trim()));
}
if (!from)
throw new IllegalArgumentException("from missing: " + hs);
_h.add("DKIM-Signature");
Map<String, Integer> index = new Hashtable<>();
for (String n : _h) {
// https://datatracker.ietf.org/doc/html/rfc6376/#section-5.4.2
String _n = n.toLowerCase(Locale.ROOT);
Integer idx = index.get(_n);
idx = (idx == null ? 1 : idx + 1);
index.put(_n, idx);
String[] h = ("DKIM-Signature".equals(n) ? new String[]{header} : amessage.getHeader(n));
if (h == null || idx > h.length) {
// https://datatracker.ietf.org/doc/html/rfc6376/#section-5.4
Log.i("DKIM missing header=" + n + "[" + idx + "/" + (h == null ? null : h.length) + "]");
continue;
}
String v = h[h.length - idx];
if ("DKIM-Signature".equals(n)) {
int b = v.lastIndexOf("b=");
int s = v.indexOf(";", b + 2);
v = v.substring(0, b + 2) + (s < 0 ? "" : v.substring(s));
} else
Log.i("DKIM " + n + "=" + v.replaceAll("\\r?\\n", "|"));
if ("simple".equals(c[0])) {
if ("DKIM-Signature".equals(n))
head.append(n).append(": ").append(v);
else {
// Find original header/name
Enumeration<Header> oheaders = amessage.getAllHeaders();
while (oheaders.hasMoreElements()) {
Header oheader = oheaders.nextElement();
if (n.equalsIgnoreCase(oheader.getName()))
head.append(oheader.getName()).append(": ")
.append(oheader.getValue());
}
}
} else if ("relaxed".equals(c[0])) {
v = MimeUtility.unfold(v);
head.append(_n).append(':')
.append(v.replaceAll("\\s+", " ").trim());
} else
throw new IllegalArgumentException(c[0]);
if (!"DKIM-Signature".equals(n))
head.append("\r\n");
}
Log.i("DKIM head=" + head.toString().replace("\r\n", "|"));
ByteArrayOutputStream bos = new ByteArrayOutputStream();
Helper.copy(amessage.getRawInputStream(), bos);
String body = bos.toString(); // TODO: charset
if ("simple".equals(c[c.length > 1 ? 1 : 0])) {
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(c[c.length > 1 ? 1 : 0])) {
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(c[1]);
Log.i("DKIM body=" + body.replace("\r\n", "|"));
byte[] bh = MessageDigest.getInstance(halgo).digest(body.getBytes()); // TODO: charset
Log.i("DKIM bh=" + Base64.encodeToString(bh, Base64.NO_WRAP) + "/" + kv.get("bh"));
String p = dk.get("p").replaceAll("\\s+", "");
Log.i("DKIM pubkey=" + p);
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(Base64.decode(p, Base64.DEFAULT));
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(pubKeySpec);
Signature sig = Signature.getInstance(salgo); // a=
String s = kv.get("b").replaceAll("\\s+", "");
Log.i("DKIM signature=" + s);
byte[] signature = Base64.decode(s, Base64.DEFAULT);
// TODO: check signature length
sig.initVerify(pubKey);
sig.update(head.toString().getBytes());
boolean verified = sig.verify(signature);
Log.i("DKIM valid=" + verified +
" dns=" + dns +
" from=" + formatAddresses(getFrom()));
if (verified &&
!signers.contains(signer))
signers.add(signer);
}
} catch (Throwable ex) {
Log.e("DKIM", ex);
}
} catch (Throwable ex) {
Log.i("DKIM error=" + ex);
Log.e(ex);
}
Log.i("DKIM signers=" + TextUtils.join(",", signers));
} catch (Throwable ex) {
Log.e("DKIM", ex);
}
Log.i("DKIM passed=" + valid);
return valid;
return signers;
}
Address[] getMailFrom(String[] headers) {