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:
parent
1091e7af99
commit
871ee1cc6c
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user