1
0
mirror of https://github.com/moparisthebest/k-9 synced 2024-12-26 01:28:50 -05:00

IMAP authentication improvements

Changes:

Implement the PLAIN SASL mechanism.  IMAPv4rev1 assures its availability
so long as the connection is encrypted.  The big advantage of PLAIN over
IMAP "LOGIN" is that PLAIN uses UTF-8 encoding for the user name and
password, whereas "LOGIN" is only safe for 7-bit US-ASCII -- the encoding
of 8-bit data is undefined.

(Note that RFC 6855 says that IMAP "LOGIN" does not support UTF-8, and
clients must use IMAP "AUTHENTICATE" to pass UTF-8 user names and
passwords.)

Honor the "LOGINDISABLED" CAPABILITY (RFC 2595) when the server declares
it.  There's no sense transmitting a password in the clear when it is
known that it will be rejected.

No attempt is made to try CRAM-MD5 if the server doesn't profess to
support it in its CAPABILITY response. (This is the same behavior as
Thunderbird.)

Extract code from ImapConnection.open into new method
ImapConnection.login.

Extract code from ImapConnection.executeSimpleCommand into new method
ImapConnection.readStatusResponse.

Related issues:  6015, 6016
This commit is contained in:
Joe Steele 2014-02-09 22:02:23 -05:00
parent 1091e7af99
commit 871ee1cc6c

View File

@ -84,6 +84,7 @@ import com.fsck.k9.mail.PushReceiver;
import com.fsck.k9.mail.Pusher; import com.fsck.k9.mail.Pusher;
import com.fsck.k9.mail.ServerSettings; import com.fsck.k9.mail.ServerSettings;
import com.fsck.k9.mail.Store; import com.fsck.k9.mail.Store;
import com.fsck.k9.mail.filter.Base64;
import com.fsck.k9.mail.filter.EOLConvertingOutputStream; import com.fsck.k9.mail.filter.EOLConvertingOutputStream;
import com.fsck.k9.mail.filter.FixedLengthInputStream; import com.fsck.k9.mail.filter.FixedLengthInputStream;
import com.fsck.k9.mail.filter.PeekableInputStream; import com.fsck.k9.mail.filter.PeekableInputStream;
@ -128,6 +129,9 @@ public class ImapStore extends Store {
private Set<Flag> mPermanentFlagsIndex = new HashSet<Flag>(); private Set<Flag> mPermanentFlagsIndex = new HashSet<Flag>();
private static final String CAPABILITY_IDLE = "IDLE"; private static final String CAPABILITY_IDLE = "IDLE";
private static final String CAPABILITY_AUTH_CRAM_MD5 = "AUTH=CRAM-MD5";
private static final String CAPABILITY_AUTH_PLAIN = "AUTH=PLAIN";
private static final String CAPABILITY_LOGINDISABLED = "LOGINDISABLED";
private static final String COMMAND_IDLE = "IDLE"; private static final String COMMAND_IDLE = "IDLE";
private static final String CAPABILITY_NAMESPACE = "NAMESPACE"; private static final String CAPABILITY_NAMESPACE = "NAMESPACE";
private static final String COMMAND_NAMESPACE = "NAMESPACE"; private static final String COMMAND_NAMESPACE = "NAMESPACE";
@ -2517,6 +2521,14 @@ public class ImapStore extends Store {
.getInputStream(), 1024)); .getInputStream(), 1024));
mParser = new ImapResponseParser(mIn); mParser = new ImapResponseParser(mIn);
mOut = mSocket.getOutputStream(); mOut = mSocket.getOutputStream();
// Per RFC 2595 (3.1): Once TLS has been started, reissue CAPABILITY command
if (K9.DEBUG)
Log.i(K9.LOG_TAG, "Updating capabilities after STARTTLS for " + getLogId());
capabilities.clear();
List<ImapResponse> responses = receiveCapabilities(executeSimpleCommand(COMMAND_CAPABILITY));
if (responses.size() != 2) {
throw new MessagingException("Invalid CAPABILITY response received");
}
} else if (mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_REQUIRED) { } else if (mSettings.getConnectionSecurity() == CONNECTION_SECURITY_TLS_REQUIRED) {
throw new MessagingException("TLS not supported but required"); throw new MessagingException("TLS not supported but required");
} }
@ -2525,20 +2537,30 @@ public class ImapStore extends Store {
mOut = new BufferedOutputStream(mOut, 1024); mOut = new BufferedOutputStream(mOut, 1024);
try { try {
if (mSettings.getAuthType() == AuthType.CRAM_MD5) { switch (mSettings.getAuthType()) {
authCramMD5(); case CRAM_MD5:
// The authCramMD5 method called on the previous line does not allow for handling updated capabilities if (hasCapability(CAPABILITY_AUTH_CRAM_MD5)) {
// sent by the server. So, to make sure we update to the post-authentication capability list authCramMD5();
// we fetch the capabilities here. } else {
if (K9.DEBUG) throw new MessagingException(
Log.i(K9.LOG_TAG, "Updating capabilities after CRAM-MD5 authentication for " + getLogId()); "Server doesn't support encrypted passwords using CRAM-MD5.");
List<ImapResponse> responses = receiveCapabilities(executeSimpleCommand(COMMAND_CAPABILITY));
if (responses.size() != 2) {
throw new MessagingException("Invalid CAPABILITY response received");
} }
break;
} else if (mSettings.getAuthType() == AuthType.PLAIN) { case PLAIN:
receiveCapabilities(executeSimpleCommand(String.format("LOGIN %s %s", ImapStore.encodeString(mSettings.getUsername()), ImapStore.encodeString(mSettings.getPassword())), true)); if (hasCapability(CAPABILITY_AUTH_PLAIN)) {
saslAuthPlain();
} else if (!hasCapability(CAPABILITY_LOGINDISABLED)) {
login();
} else {
throw new MessagingException(
"Server doesn't support unencrypted passwords using AUTH=PLAIN and LOGIN is disabled.");
}
break;
default:
throw new MessagingException(
"Unhandled authentication method found in the server settings (bug).");
} }
authSuccess = true; authSuccess = true;
} catch (ImapException ie) { } catch (ImapException ie) {
@ -2664,6 +2686,13 @@ public class ImapStore extends Store {
} }
} }
protected void login() throws IOException, MessagingException {
receiveCapabilities(executeSimpleCommand(String.format(
"LOGIN %s %s",
ImapStore.encodeString(mSettings.getUsername()),
ImapStore.encodeString(mSettings.getPassword())), true));
}
protected void authCramMD5() throws AuthenticationFailedException, MessagingException { protected void authCramMD5() throws AuthenticationFailedException, MessagingException {
try { try {
String tag = sendCommand("AUTHENTICATE CRAM-MD5", false); String tag = sendCommand("AUTHENTICATE CRAM-MD5", false);
@ -2708,6 +2737,75 @@ public class ImapStore extends Store {
} }
} }
protected void saslAuthPlain() throws IOException, MessagingException {
String command = "AUTHENTICATE PLAIN";
String tag = sendCommand(command, false);
readContinuationResponse(tag);
mOut.write(Base64.encodeBase64(("\000" + mSettings.getUsername()
+ "\000" + mSettings.getPassword()).getBytes()));
mOut.write('\r');
mOut.write('\n');
mOut.flush();
try {
receiveCapabilities(readStatusResponse(tag, command, null));
} catch (MessagingException e) {
throw new AuthenticationFailedException(e.getMessage());
}
}
protected ImapResponse readContinuationResponse(String tag)
throws IOException, MessagingException {
ImapResponse response;
do {
response = readResponse();
if (response.mTag != null) {
if (response.mTag.equalsIgnoreCase(tag)) {
throw new MessagingException(
"Command continuation aborted: " + response);
} else {
Log.w(K9.LOG_TAG, "After sending tag " + tag
+ ", got tag response from previous command "
+ response + " for " + getLogId());
}
}
} while (!response.mCommandContinuationRequested);
return response;
}
protected ArrayList<ImapResponse> readStatusResponse(String tag,
String commandToLog, UntaggedHandler untaggedHandler)
throws IOException, MessagingException {
ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>();
ImapResponse response;
do {
response = mParser.readResponse();
if (K9.DEBUG && K9.DEBUG_PROTOCOL_IMAP)
Log.v(K9.LOG_TAG, getLogId() + "<<<" + response);
if (response.mTag != null && !response.mTag.equalsIgnoreCase(tag)) {
Log.w(K9.LOG_TAG, "After sending tag " + tag + ", got tag response from previous command " + response + " for " + getLogId());
Iterator<ImapResponse> iter = responses.iterator();
while (iter.hasNext()) {
ImapResponse delResponse = iter.next();
if (delResponse.mTag != null || delResponse.size() < 2
|| (!ImapResponseParser.equalsIgnoreCase(delResponse.get(1), "EXISTS") && !ImapResponseParser.equalsIgnoreCase(delResponse.get(1), "EXPUNGE"))) {
iter.remove();
}
}
response.mTag = null;
continue;
}
if (untaggedHandler != null) {
untaggedHandler.handleAsyncUntaggedResponse(response);
}
responses.add(response);
} while (response.mTag == null);
if (response.size() < 1 || !ImapResponseParser.equalsIgnoreCase(response.get(0), "OK")) {
throw new ImapException("Command: " + commandToLog + "; response: " + response.toString(), response.getAlertText());
}
return responses;
}
protected void setReadTimeout(int millis) throws SocketException { protected void setReadTimeout(int millis) throws SocketException {
Socket sock = mSocket; Socket sock = mSocket;
if (sock != null) { if (sock != null) {
@ -2832,35 +2930,7 @@ public class ImapStore extends Store {
//if (K9.DEBUG) //if (K9.DEBUG)
// Log.v(K9.LOG_TAG, "Sent IMAP command " + commandToLog + " with tag " + tag + " for " + getLogId()); // Log.v(K9.LOG_TAG, "Sent IMAP command " + commandToLog + " with tag " + tag + " for " + getLogId());
ArrayList<ImapResponse> responses = new ArrayList<ImapResponse>(); return readStatusResponse(tag, commandToLog, untaggedHandler);
ImapResponse response;
do {
response = mParser.readResponse();
if (K9.DEBUG && K9.DEBUG_PROTOCOL_IMAP)
Log.v(K9.LOG_TAG, getLogId() + "<<<" + response);
if (response.mTag != null && !response.mTag.equalsIgnoreCase(tag)) {
Log.w(K9.LOG_TAG, "After sending tag " + tag + ", got tag response from previous command " + response + " for " + getLogId());
Iterator<ImapResponse> iter = responses.iterator();
while (iter.hasNext()) {
ImapResponse delResponse = iter.next();
if (delResponse.mTag != null || delResponse.size() < 2
|| (!ImapResponseParser.equalsIgnoreCase(delResponse.get(1), "EXISTS") && !ImapResponseParser.equalsIgnoreCase(delResponse.get(1), "EXPUNGE"))) {
iter.remove();
}
}
response.mTag = null;
continue;
}
if (untaggedHandler != null) {
untaggedHandler.handleAsyncUntaggedResponse(response);
}
responses.add(response);
} while (response.mTag == null);
if (response.size() < 1 || !ImapResponseParser.equalsIgnoreCase(response.get(0), "OK")) {
throw new ImapException("Command: " + commandToLog + "; response: " + response.toString(), response.getAlertText());
}
return responses;
} }
} }