Added CRAM-MD5 authentication

This commit is contained in:
M66B 2020-01-23 12:38:31 +01:00
parent e715772ac5
commit 581d02d1ec
6 changed files with 246 additions and 2 deletions

4
FAQ.md
View File

@ -1522,11 +1522,11 @@ The following authentication methods are supported and used in this order:
* LOGIN
* PLAIN
* CRAM-MD5
* NTLM (untested)
SASL authentication methods, like CRAM-MD5, are not supported
SASL authentication methods, besides CRAM-MD5, are not supported
because [JavaMail for Android](https://javaee.github.io/javamail/Android) does not support SASL authentication.
If using secure connections, a must today, there is little value in using CRAM-MD5 anyway.
If your provider requires an unsupported authentication method, you'll likely get the error message *authentication failed*.

View File

@ -100,3 +100,6 @@
-dontnote com.bugsnag.android.**
-dontnote biweekly.io.**
#SASL
-keep class com.sun.mail.imap.protocol.IMAPSaslAuthenticator
-keep class com.sun.mail.smtp.SMTPSaslAuthenticator

View File

@ -0,0 +1,103 @@
package com.sun.mail.imap.protocol;
/*
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 <http://www.gnu.org/licenses/>.
Copyright 2018-2020 by Marcel Bokhorst (M66B)
*/
import android.util.Base64;
import com.sun.mail.iap.Argument;
import com.sun.mail.iap.ProtocolException;
import com.sun.mail.iap.Response;
import com.sun.mail.util.MailLogger;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import eu.faircode.email.Helper;
// https://github.com/javaee/javamail/blob/master/mail/src/main/java/com/sun/mail/imap/protocol/IMAPSaslAuthenticator.java
public class IMAPSaslAuthenticator implements SaslAuthenticator {
private IMAPProtocol pr;
private String name;
private Properties props;
private MailLogger logger;
private String host;
public IMAPSaslAuthenticator(
IMAPProtocol pr, String name, Properties props,
MailLogger logger, String host) {
this.pr = pr;
this.name = name;
this.props = props;
this.logger = logger;
this.host = host;
}
@Override
public boolean authenticate(
String[] mechs, final String realm,
final String authzid, final String u,
final String p) throws ProtocolException {
if (!pr.hasCapability("AUTH=CRAM-MD5"))
throw new UnsupportedOperationException();
List<Response> v = new ArrayList<>();
String tag = null;
Response r = null;
boolean done = false;
try {
Argument args = new Argument();
args.writeAtom("CRAM-MD5");
tag = pr.writeCommand("AUTHENTICATE", args);
} catch (Exception ex) {
r = Response.byeResponse(ex);
done = true;
}
while (!done) {
try {
r = pr.readResponse();
if (r.isContinuation()) {
byte[] nonce = Base64.decode(r.getRest(), Base64.NO_WRAP);
String hmac = Helper.HMAC("MD5", 64, p.getBytes(), nonce);
String hash = Base64.encodeToString((u + " " + hmac).getBytes(), Base64.NO_WRAP) + "\r\n";
pr.getIMAPOutputStream().write(hash.getBytes());
pr.getIMAPOutputStream().flush();
} else if (r.isTagged() && r.getTag().equals(tag))
done = true;
else if (r.isBYE())
done = true;
} catch (Exception ex) {
r = Response.byeResponse(ex);
done = true;
}
v.add(r);
}
Response[] responses = v.toArray(new Response[0]);
pr.handleCapabilityResponse(responses);
pr.notifyResponseHandlers(responses);
pr.handleLoginResult(r);
pr.setCapabilities(r);
return true;
}
}

View File

@ -0,0 +1,101 @@
package com.sun.mail.smtp;
/*
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 <http://www.gnu.org/licenses/>.
Copyright 2018-2020 by Marcel Bokhorst (M66B)
*/
import android.util.Base64;
import com.sun.mail.util.MailLogger;
import java.security.NoSuchAlgorithmException;
import java.util.Properties;
import javax.mail.MessagingException;
import eu.faircode.email.Helper;
import eu.faircode.email.Log;
// https://github.com/javaee/javamail/blob/master/mail/src/main/java/com/sun/mail/smtp/SMTPSaslAuthenticator.java
public class SMTPSaslAuthenticator implements SaslAuthenticator {
private SMTPTransport pr;
private String name;
private Properties props;
private MailLogger logger;
private String host;
public SMTPSaslAuthenticator(
SMTPTransport pr, String name,
Properties props, MailLogger logger, String host) {
this.pr = pr;
this.name = name;
this.props = props;
this.logger = logger;
this.host = host;
}
@Override
public boolean authenticate(
String[] mechs, final String realm,
final String authzid, final String u,
final String p) throws MessagingException {
if (!pr.supportsAuthentication("CRAM-MD5"))
throw new UnsupportedOperationException();
// https://tools.ietf.org/html/rfc4954
int resp = simpleCommand(pr, "AUTH CRAM-MD5");
if (resp == 530) { // Authentication is required
pr.startTLS();
resp = simpleCommand(pr, "AUTH CRAM-MD5");
}
if (resp == 235) // Authentication Succeeded
return true;
if (resp != 334) // server challenge
return false;
try {
String t = responseText(pr);
byte[] nonce = Base64.decode(t, Base64.NO_WRAP);
String hmac = Helper.HMAC("MD5", 64, p.getBytes(), nonce);
String hash = Base64.encodeToString((u + " " + hmac).getBytes(), Base64.NO_WRAP);
resp = simpleCommand(pr, hash);
} catch (NoSuchAlgorithmException ex) {
throw new MessagingException("CRAM-MD5", ex);
}
return (resp == 235);
}
private static int simpleCommand(SMTPTransport pr, String command) throws MessagingException {
Log.i("SMTP SASL command=" + command);
int resp = pr.simpleCommand(command);
Log.i("SMTP SASL response=" + pr.getLastServerResponse());
return resp;
}
private static String responseText(SMTPTransport pr) {
String resp = pr.getLastServerResponse().trim();
if (resp.length() > 4)
return resp.substring(4);
else
return "";
}
}

View File

@ -978,6 +978,41 @@ public class Helper {
void onNothingSelected();
}
public static String HMAC(String algo, int blocksize, byte[] key, byte[] text) throws NoSuchAlgorithmException {
MessageDigest md = MessageDigest.getInstance(algo);
if (key.length > blocksize)
key = md.digest(key);
byte[] ipad = new byte[blocksize];
byte[] opad = new byte[blocksize];
for (int i = 0; i < key.length; i++) {
ipad[i] = key[i];
opad[i] = key[i];
}
for (int i = 0; i < blocksize; i++) {
ipad[i] ^= 0x36;
opad[i] ^= 0x5c;
}
byte[] digest;
md.update(ipad);
md.update(text);
digest = md.digest();
md.update(opad);
md.update(digest);
digest = md.digest();
StringBuilder sb = new StringBuilder();
for (byte b : digest)
sb.append(String.format("%02x", b));
return sb.toString();
}
// Miscellaneous
static <T> List<List<T>> chunkList(List<T> list, int size) {

View File

@ -121,6 +121,8 @@ public class MailService implements AutoCloseable {
properties.put("mail.event.scope", "folder");
properties.put("mail.event.executor", executor);
properties.put("mail." + protocol + ".sasl.enable", "true");
properties.put("mail." + protocol + ".sasl.mechanisms", "CRAM-MD5");
properties.put("mail." + protocol + ".sasl.realm", realm == null ? "" : realm);
properties.put("mail." + protocol + ".auth.ntlm.domain", realm == null ? "" : realm);