/** * @file operator-authenticate.cc * @brief Implementation of the AUTHENTICATE command, incl. CRAM-MD5. * @author Andreas Aardal Hanssen, Erwin Hoffmann * @date 2002-2005, 2023 */ #include "authenticate.h" #include "base64.h" #include "convert.h" #include "depot.h" #include "globals.h" #include "iodevice.h" #include "iofactory.h" #include "operators.h" #include "recursivedescent.h" #include "session.h" #include #include using namespace Binc; using std::endl; using std::string; AuthenticateOperator::AuthenticateOperator(void) {} AuthenticateOperator::~AuthenticateOperator(void) {} const string AuthenticateOperator::getName(void) const { return "AUTHENTICATE"; } int AuthenticateOperator::getState(void) const { return Session::NONAUTHENTICATED; } Operator::ProcessResult AuthenticateOperator::Login(string &username, string &password) { Session &session = Session::getInstance(); bincClient << "+ " << base64encode("User Name") << endl; bincClient.flush(); // Read user name string b64usr; for (;;) { char c; if (!bincClient.readChar(&c)) { session.setLastError("unexpected EOF"); return BAD; } if (c == '\n') break; b64usr += c; } if (b64usr != "" && b64usr[0] == '*') { session.setLastError("Authentication cancelled by user"); return NO; } bincClient << "+ " << base64encode("Password") << endl; bincClient.flush(); // Read password string b64pwd; for (;;) { char c; if (!bincClient.readChar(&c)) { session.setLastError("unexpected EOF"); return BAD; } if (c == '\n') break; b64pwd += c; } if (b64pwd != "" && b64pwd[0] == '*') { session.setLastError("Authentication cancelled by user"); return NO; } username = base64decode(b64usr); password = base64decode(b64pwd); return OK; } Operator::ProcessResult AuthenticateOperator::Plain(string &username, string &password) { Session &session = Session::getInstance(); bincClient << "+ " << endl; bincClient.flush(); string b64; for (;;) { char c; if (!bincClient.readChar(&c)) { session.setLastError("unexpected EOF"); return BAD; } if (c == '\n') break; b64 += c; } if (b64.size() >= 1 && b64[0] == '*') { session.setLastError("Authentication cancelled by user"); return NO; } string plain = base64decode(b64); string::size_type pos = 0; if ((pos = plain.find('\0')) == string::npos) { session.setLastError("Authentication failed. In PLAIN mode, " "there must be at least two null characters " "in the input string, but none were found"); return NO; } plain = plain.substr(pos + 1); if ((pos = plain.find('\0')) == string::npos) { session.setLastError("Authentication failed. In PLAIN mode, " "there must be at least two null characters " "in the input string, but only one was found"); return NO; } username = plain.substr(0, pos); password = plain.substr(pos + 1); return OK; } Operator::ProcessResult AuthenticateOperator::Cram(string &username, string &password, string &challenge) { Session &session = Session::getInstance(); // generate challenge first: and deploy it to authenticator time_t timer; struct tm y2k = {}; int timestamp; y2k.tm_hour = 0; y2k.tm_min = 0; y2k.tm_sec = 0; y2k.tm_year = 100; y2k.tm_mon = 0; y2k.tm_mday = 1; time(&timer); /* get current time; same as: timer = time(NULL) */ timestamp = difftime(timer, mktime(&y2k)); challenge += "<"; challenge += std::to_string(session.getPid()); challenge += "."; challenge += std::to_string(timestamp); challenge += "@"; challenge += session.getEnv("TCPLOCALHOST"); challenge += ">"; bincClient << "+ " << base64encode(challenge) << endl; bincClient.flush(); // Read response string b64; for (;;) { char c; if (!bincClient.readChar(&c)) return BAD; if (c == '\n') break; b64 += c; } // Disentangle response string response = base64decode(b64); string::size_type pos = 0; if ((pos = response.find(' ')) == string::npos) { session.setLastError("Authentication failed. In CRAM-MD5 mode, " "there must be a white space in the " "input string between username and digest"); return NO; } username = response.substr(0, pos); password = response.substr(pos + 1); return OK; } Operator::ProcessResult AuthenticateOperator::process(Depot &depot, Request &command) { Session &session = Session::getInstance(); string authtype = command.getAuthType(); uppercase(authtype); string username; string password; string challenge; ProcessResult r = NOTHING; if (authtype == "LOGIN") { // we only allow this type of authentication over an unencryted connection // if it is explicitely commanded if (!session.command.ssl && !session.hasEnv("ALLOW_NONSSL_PLAINTEXT_LOGINS")) { session.setLastError("Plain text password authentication is disallowd. " "Please enable StartTLS or TLS in your mail client."); return NO; } if ((r = Login(username, password)) != OK) return r; } else if (authtype == "PLAIN") { // we only allow this type of authentication over an TLS encrypted connection. if (!session.command.ssl && !session.hasEnv("ALLOW_NONSSL_PLAINTEXT_LOGINS")) { session.setLastError("Plain text password authentication is disallowd. " "Please enable StartTLS or TLS in your mail client."); return NO; } if ((r = Plain(username, password)) != OK) return r; } else if (authtype == "CRAM-MD5") { // this type can be used even over unencrypted connections if ((r = Cram(username, password, challenge)) != OK) return r; } else { // Any other disallowed session.setLastError("The authentication method " + toImapString(authtype) + " is not supported. " "Please try again with a different method. " "There is built in support for \"PLAIN\" " "and \"LOGIN\"."); return NO; } putenv(strdup(("BINCIMAP_LOGIN=AUTHENTICATE+" + command.getTag()).c_str())); // put the username in the environment for logging purpose session.setEnv("USER", username.c_str()); // the authenticate function calls a stub which does the actual // authentication. the function returns 0 (success), 1 (internal // error) or 2 (failed) switch (authenticate(depot, username, password, challenge)) { case 1: session.setLastError("An internal error occurred when you attempted " "to log in to the IMAP server. Please contact " "your system administrator."); return NO; case 2: session.setLastError("Login failed. Either your user name " "or your password was wrong. Please try again, " "and if the problem persists, please contact " "your system administrator."); return NO; case 3: bincClient << "* BYE Timeout after " << IDLE_TIMEOUT << " seconds of inactivity." << endl; break; case -1: bincClient << "* BYE The server died unexpectedly. Please contact " "your system administrator for more information." << endl; break; default: // bincLog << "<" << username.c_str() << "> authenticated" << endl; break; } // auth was ok. go to logout state session.setState(Session::LOGOUT); return NOTHING; } Operator::ParseResult AuthenticateOperator::parse(Request &c_in) const { Session &session = Session::getInstance(); if (c_in.getUidMode()) return REJECT; Operator::ParseResult res; if ((res = expectSPACE()) != ACCEPT) { session.setLastError("Expected single SPACE after AUTHENTICATE"); return res; } string authtype; if ((res = expectAtom(authtype)) != ACCEPT) { session.setLastError("Expected auth_type after AUTHENTICATE SPACE"); return ERROR; } if ((res = expectCRLF()) != ACCEPT) { session.setLastError("Expected CRLF after AUTHENTICATE SPACE auth_type"); return res; } c_in.setAuthType(authtype); c_in.setName("AUTHENTICATE"); return ACCEPT; }