diff options
Diffstat (limited to 'tdeioslave/sftp/tdeio_sftp.cpp')
| -rw-r--r-- | tdeioslave/sftp/tdeio_sftp.cpp | 941 |
1 files changed, 663 insertions, 278 deletions
diff --git a/tdeioslave/sftp/tdeio_sftp.cpp b/tdeioslave/sftp/tdeio_sftp.cpp index d62e7e062..3e88b8e65 100644 --- a/tdeioslave/sftp/tdeio_sftp.cpp +++ b/tdeioslave/sftp/tdeio_sftp.cpp @@ -33,6 +33,10 @@ #include <tqfile.h> #include <tqdir.h> +#include <numeric> +#include <functional> +#include <vector> + #include <stdlib.h> #include <unistd.h> #include <errno.h> @@ -50,7 +54,7 @@ #include <kdebug.h> #include <tdemessagebox.h> #include <tdeglobal.h> -#include <kstandarddirs.h> +#include <tdestandarddirs.h> #include <tdelocale.h> #include <kurl.h> #include <tdeio/ioslave_defaults.h> @@ -68,7 +72,7 @@ using namespace TDEIO; extern "C" { - int KDE_EXPORT kdemain( int argc, char **argv ) + int TDE_EXPORT kdemain( int argc, char **argv ) { TDEInstance instance( "tdeio_sftp" ); @@ -92,6 +96,54 @@ extern "C" } } +// Some helper functions/classes +namespace { + +// A quick and dirty scope guard implementation +class ExitGuard { +public: + template<class Callable> + ExitGuard(Callable && undo_func) : f(std::forward<Callable>(undo_func)) {} + ExitGuard(ExitGuard && other) : f(std::move(other.f)) { + other.f = nullptr; + } + + ~ExitGuard() { + run(); + } + + void run() noexcept { + if(f) { f(); f = nullptr; } + } + + void abort() { + f = nullptr; + } + + ExitGuard(const ExitGuard&) = delete; + void operator= (const ExitGuard&) = delete; + +private: + std::function<void()> f; +}; + +// A small helper to purge passwords. Paranoiac's note: this is not enough to guarantee the +// complete purge of the password and all its copy from memory (ioslaves are sending the passwords +// via dcop, so it's far beyond calling it "secure" in any way), but it's still better than nothing. +void purgeString(TQString &s) { + s.fill('\0'); + s.setLength(0); + s = TQString::null; +} + +// A helper class to cleanup password when it goes out of the scope +class PasswordPurger: public ExitGuard { +public: + PasswordPurger(TQString &pw) : ExitGuard( [&pw](){purgeString(pw);} ) {} +}; + +} /* namespace */ + // The callback function for libssh int auth_callback(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata) @@ -120,6 +172,68 @@ void log_callback(ssh_session session, int priority, const char *message, slave->log_callback(session, priority, message, userdata); } +class PublicKeyAuth: public SSHAuthMethod { +public: + unsigned flag() override {return SSH_AUTH_METHOD_PUBLICKEY;}; + int authenticate(sftpProtocol *ioslave) const override { + return ioslave->authenticatePublicKey(); + } + SSHAuthMethod* clone() override {return new PublicKeyAuth; } +}; + +class KeyboardInteractiveAuth: public SSHAuthMethod { +public: + KeyboardInteractiveAuth(bool noPaswordQuery = false): mNoPaswordQuery(noPaswordQuery) {} + + unsigned flag() override {return SSH_AUTH_METHOD_INTERACTIVE;}; + int authenticate(sftpProtocol *ioslave) const override { + return ioslave->authenticateKeyboardInteractive(mNoPaswordQuery); + } + SSHAuthMethod* clone() override {return new KeyboardInteractiveAuth(mNoPaswordQuery); } + +private: + const bool mNoPaswordQuery; +}; + +class PasswordAuth: public SSHAuthMethod { +public: + PasswordAuth(bool noPaswordQuery = false): mNoPaswordQuery(noPaswordQuery) {} + + unsigned flag() override {return SSH_AUTH_METHOD_PASSWORD;}; + int authenticate(sftpProtocol *ioslave) const override { + return ioslave->authenticatePassword(mNoPaswordQuery); + } + SSHAuthMethod* clone() override {return new PasswordAuth(mNoPaswordQuery); } + +private: + const bool mNoPaswordQuery; +}; + +TQString SSHAuthMethod::flagToStr (unsigned m) { + switch (m) { + case SSH_AUTH_METHOD_NONE : return TQString::fromLatin1 ( "none" ); + case SSH_AUTH_METHOD_PASSWORD : return TQString::fromLatin1 ( "password" ); + case SSH_AUTH_METHOD_PUBLICKEY : return TQString::fromLatin1 ( "publickey" ); + case SSH_AUTH_METHOD_HOSTBASED : return TQString::fromLatin1 ( "hostbased" ); + case SSH_AUTH_METHOD_INTERACTIVE : return TQString::fromLatin1 ( "keyboard-interactive" ); + case SSH_AUTH_METHOD_GSSAPI_MIC : return TQString::fromLatin1 ( "gssapi-with-mic" ); + default : return TQString::fromLatin1 ( "unknown" ); + } +} + +TQStringList SSHAuthMethod::bitsetToStr (unsigned m) { + TQStringList rv; + + for (int i=0; m>>i; i++) { + unsigned flag = m & (1 << i); + if (flag) { + rv.append(flagToStr(flag)); + } + } + return rv; +} + + // Public key authentication int sftpProtocol::auth_callback(const char *prompt, char *buf, size_t len, int echo, int verify, void *userdata) @@ -128,42 +242,82 @@ int sftpProtocol::auth_callback(const char *prompt, char *buf, size_t len, (void) echo; (void) verify; (void) userdata; + (void) prompt; + + Q_ASSERT(len>0); kdDebug(TDEIO_SFTP_DB) << "Entering public key authentication callback" << endl; - if(!pubKeyInfo) - { - pubKeyInfo = new TDEIO::AuthInfo; - } - else - { - // TODO: inform user about incorrect password - } + int rc=0; + bool firstTimeCalled = !mPubKeyAuthData.wasCalled; + mPubKeyAuthData.wasCalled = true; - pubKeyInfo->url.setProtocol("sftp"); - pubKeyInfo->url.setHost(mHost); - pubKeyInfo->url.setPort(mPort); - pubKeyInfo->url.setUser(mUsername); + AuthInfo pubKeyInfo = authInfo(); - pubKeyInfo->caption = i18n("SFTP Login"); - pubKeyInfo->comment = "sftp://" + mUsername + "@" + mHost; - pubKeyInfo->username = mUsername; - pubKeyInfo->readOnly = false; - pubKeyInfo->prompt = TQString::fromUtf8(prompt); - pubKeyInfo->keepPassword = false; // don't save passwords for public key, + pubKeyInfo.keepPassword = false; // don't save passwords for public key, // that's the task of ssh-agent. + pubKeyInfo.readOnly = true; // We don't want to handle user name change when authing with a key - if (!openPassDlg(*pubKeyInfo)) { - kdDebug(TDEIO_SFTP_DB) << "User canceled entry of public key password." << endl; - return -1; + TQString errMsg; + TQString keyFile; +#if LIBSSH_VERSION_INT < SSH_VERSION_INT(0, 10, 0) + // no way to determine keyfile name on older libssh +#else + char *ssh_key_file = 0; + rc = ssh_userauth_publickey_auto_get_current_identity(mSession, &ssh_key_file); + + if (rc == 0 && ssh_key_file && ssh_key_file[0]) { + keyFile = ssh_key_file; } + ssh_string_free_char(ssh_key_file); +#endif - strncpy(buf, pubKeyInfo->password.utf8().data(), len - 1); + bool firstTry = !mPubKeyAuthData.attemptedKeys.contains(keyFile); - pubKeyInfo->password.fill('x'); - pubKeyInfo->password = ""; + if (firstTry) { + SlaveBase::s_seqNr = mPubKeyAuthData.current_seqNr; + } else { + errMsg = i18n("Incorrect or invalid passphrase.").append('\n'); + } - return 0; + // libssh prompt is trash and we know we use this function only for publickey auth, so we'll give + // the user a descent prompt + if (!keyFile.isEmpty()) { + pubKeyInfo.prompt = i18n("Please enter the passphrase for next public key:\n%1").arg(keyFile); + } else { // Generally shouldn't happend but on older libssh + pubKeyInfo.prompt = i18n("Please enter the passphrase for your public key."); + } + + // We don't want to clobber with normal passwords in kpasswdserver's cache + pubKeyInfo.realmValue = "keyfile passphrase:" + keyFile; + + if (openPassDlg(pubKeyInfo, errMsg)) { + if (len < pubKeyInfo.password.utf8().length()+1) { + kdDebug(TDEIO_SFTP_DB) << "Insufficient buffer size for password: " << len + << " (" << pubKeyInfo.password.utf8().length()+1 << "needed)" << endl; + } + + strncpy(buf, pubKeyInfo.password.utf8().data(), len-1); + buf[len-1]=0; // Just to be on the safe side + + purgeString(pubKeyInfo.password); + + // take a note that we already tried unlocking this keyfile + if(firstTry) { + mPubKeyAuthData.attemptedKeys.append(keyFile); + } + + // we consider publickey auth canceled only if we cancel all the key dialogs + mPubKeyAuthData.wasCanceled = false; + } else { + kdDebug(TDEIO_SFTP_DB) << "User canceled entry of public key passphrase" << endl; + rc = -1; + if (firstTimeCalled) { + mPubKeyAuthData.wasCanceled = true; + } + } + + return rc; } void sftpProtocol::log_callback(ssh_session session, int priority, @@ -174,91 +328,323 @@ void sftpProtocol::log_callback(ssh_session session, int priority, kdDebug(TDEIO_SFTP_DB) << "[" << priority << "] " << message << endl; } -int sftpProtocol::authenticateKeyboardInteractive(AuthInfo &info) { - TQString name, instruction, prompt; - int err = SSH_AUTH_ERROR; +int sftpProtocol::authenticatePublicKey(){ + kdDebug(TDEIO_SFTP_DB) << "Trying to authenticate with public key" << endl; + + // First let's do some cleanup + mPubKeyAuthData.attemptedKeys.clear(); + mPubKeyAuthData.current_seqNr = SlaveBase::s_seqNr; + mPubKeyAuthData.wasCalled = 0; + mPubKeyAuthData.wasCanceled = 0; + + int rc; + + while (1) { + mPubKeyAuthData.wasCalled = 0; + rc = ssh_userauth_publickey_auto(mSession, nullptr, nullptr); + + kdDebug(TDEIO_SFTP_DB) << "ssh_userauth_publickey_auto returned rc=" << rc + << " ssh_err=" << ssh_get_error_code(mSession) + << " (" << ssh_get_error(mSession) << ")" << endl; + if (rc == SSH_AUTH_DENIED) { + if (!mPubKeyAuthData.wasCalled) { + kdDebug(TDEIO_SFTP_DB) << "Passkey auth denied because it has no matching key" << endl; + break; /* rc == SSH_AUTH_DENIED */ + } else if (mPubKeyAuthData.wasCanceled) { + kdDebug(TDEIO_SFTP_DB) << "Passkey auth denied because user canceled" << endl; + rc = sftpProtocol::SSH_AUTH_CANCELED; + break; + } else { + kdDebug(TDEIO_SFTP_DB) << "User entered wrong passphrase for the key" << endl; + mPubKeyAuthData.current_seqNr = SlaveBase::s_seqNr; + // Try it again + } + } else { + // every other rc is either error or success + break; + } + } + + return rc; +} +int sftpProtocol::authenticateKeyboardInteractive(bool noPaswordQuery) { kdDebug(TDEIO_SFTP_DB) << "Entering keyboard interactive function" << endl; - err = ssh_userauth_kbdint(mSession, mUsername.utf8().data(), NULL); - while (err == SSH_AUTH_INFO) { + int rc = SSH_AUTH_ERROR; + + bool retryDenied = false; // a flag to avoid infinite looping + + TQString cachablePassword; + PasswordPurger cachePurger(cachablePassword); + + // Different prompts during a single pass should be queried with the same s_seqNr value + long current_seqNr = SlaveBase::s_seqNr; + + while (1) { int n = 0; int i = 0; + rc = ssh_userauth_kbdint(mSession, NULL, NULL); + + if (rc == SSH_AUTH_DENIED) { // do nothing + kdDebug(TDEIO_SFTP_DB) << "kb-interactive auth was denied; retrying again" << endl; + if (retryDenied) { + // If we were denied update the s_seqNr + current_seqNr = SlaveBase::s_seqNr; + continue; + } else { + break; + } + } else if (rc != SSH_AUTH_INFO) { + kdDebug(TDEIO_SFTP_DB) << "Finishing kb-interactive auth rc=" << rc + << " ssh_err=" << ssh_get_error_code(mSession) + << " (" << ssh_get_error(mSession) << ")" << endl; + break; + } + + // See "RFC4256 Section 3.3 User Interface" for meaning of the values + TQString name, instruction, prompt; name = TQString::fromUtf8(ssh_userauth_kbdint_getname(mSession)); instruction = TQString::fromUtf8(ssh_userauth_kbdint_getinstruction(mSession)); n = ssh_userauth_kbdint_getnprompts(mSession); + if (n>0) { + // If there is at least one prompt we will want to retry auth if we fail + retryDenied = true; + } + kdDebug(TDEIO_SFTP_DB) << "name=" << name << " instruction=" << instruction - << " prompts" << n << endl; + << " prompts:" << n << endl; for (i = 0; i < n; ++i) { char echo; - const char *answer = ""; + bool isPassword=false; + TQString answer; + TQString errMsg; + + // restore the s_seqNr so it would be the same for all the prompts + SlaveBase::s_seqNr = current_seqNr; prompt = TQString::fromUtf8(ssh_userauth_kbdint_getprompt(mSession, i, &echo)); kdDebug(TDEIO_SFTP_DB) << "prompt=" << prompt << " echo=" << TQString::number(echo) << endl; - if (echo) { - // See RFC4256 Section 3.3 User Interface - TQString newPrompt; - TDEIO::AuthInfo infoKbdInt; - infoKbdInt.url.setProtocol("sftp"); - infoKbdInt.url.setHost(mHost); - infoKbdInt.url.setPort(mPort); + TDEIO::AuthInfo infoKbdInt = authInfo(); + infoKbdInt.realmValue = prompt; // each prompt will be treated on its own by kpasswdserver + infoKbdInt.keepPassword = false; + + if (!name.isEmpty()) { + infoKbdInt.caption = TQString(i18n("SFTP Login") + " - " + name); + } + + // Those strings might or might not contain some sensitive information + PasswordPurger answerPurger{answer}; + PasswordPurger infoPurger{infoKbdInt.password}; - infoKbdInt.caption = i18n("SFTP Login"); - infoKbdInt.comment = "sftp://" + mUsername + "@" + mHost; + if (!echo) { + // ssh server requests us to ask user a question without displaying an answer. In normal + // circumstances this is probably a password, but it might be something else depending + // on the server configuration. + if (prompt.lower().startsWith("password")) { + // We can assume that the ssh server asks for a password and we will handle that case + // with more care since it's what most users will see + isPassword = true; + if (noPaswordQuery) { // if we have a cached password we might use it + kdDebug(TDEIO_SFTP_DB) << "Using cached password" << endl; + answer = mPassword; + cachablePassword = mPassword; + purgeString(mPassword); // if we used up password purge it + } else { + infoKbdInt.prompt = i18n("Please enter your password."); + infoKbdInt.realmValue = TQString(); // passwords use generic realm + infoKbdInt.keepPassword = true; + + if (mPasswordWasPrompted) { + errMsg = i18n("Login failed: incorrect password or username.").append('\n'); + } + mPasswordWasPrompted = true; + } + } else { + // If the server's request doesn't look like a password, keep the servers prompt but + // don't prompt for saving the answer + infoKbdInt.prompt = i18n("Please enter answer for the next request:"); + if (!instruction.isEmpty()) { + infoKbdInt.prompt.append("\n\n").append(instruction); + } + infoKbdInt.prompt.append("\n\n").append(prompt); + infoKbdInt.readOnly = true; // set username readonly (enable changing it only with password) + } + + if (answer.isNull()) { + if (openPassDlg(infoKbdInt, errMsg)) { + answer = infoKbdInt.password; + kdDebug(TDEIO_SFTP_DB) << "Got the answer from the password dialog" << endl; - if (!name.isEmpty()) { - infoKbdInt.caption = TQString(i18n("SFTP Login") + " - " + name); + if (isPassword) { + TQString sshUser=sshUsername(); + if (infoKbdInt.username != sshUser) { + kdDebug(TDEIO_SFTP_DB) << "Username changed from " << sshUser + << " to " << infoKbdInt.username << endl; + mCachedUsername = infoKbdInt.username; + mPassword = infoKbdInt.password; + + return sftpProtocol::SSH_AUTH_NEED_RECONNECT; + } + } + } else { + return sftpProtocol::SSH_AUTH_CANCELED; + } } + } else { + // ssh server asks for some clear-text information from a user (e.g. a one-time + // identification code) which should be echoed while user enters it. As for now tdeio has + // no means to handle that correctly, so we will have to be creative with the password + // dialog. + TQString newPrompt; if (!instruction.isEmpty()) { newPrompt = instruction + "\n\n"; } + newPrompt.append(prompt).append("\n\n"); + newPrompt.append(i18n("Use the username input field to answer this question.")); + infoKbdInt.prompt = newPrompt; - newPrompt.append(prompt + "\n\n"); - infoKbdInt.readOnly = false; - infoKbdInt.keepPassword = false; - infoKbdInt.prompt = i18n("Use the username input field to answer this question."); + infoKbdInt.url.setUser(infoKbdInt.username); + infoKbdInt.username = TQString::null; + infoKbdInt.readOnly = false; if (openPassDlg(infoKbdInt)) { - kdDebug(TDEIO_SFTP_DB) << "Got the answer from the password dialog" << endl; - answer = info.username.utf8().data(); + answer = infoKbdInt.username; + kdDebug(TDEIO_SFTP_DB) << "Got the answer from the password dialog: " << answer << endl; + } else { + return sftpProtocol::SSH_AUTH_CANCELED; } + } - if (ssh_userauth_kbdint_setanswer(mSession, i, answer) < 0) { - kdDebug(TDEIO_SFTP_DB) << "An error occurred setting the answer: " - << ssh_get_error(mSession) << endl; - return SSH_AUTH_ERROR; - } - break; - } else { - if (prompt.lower().startsWith("password")) { - answer = mPassword.utf8().data(); - } else { - info.readOnly = true; // set username readonly - info.prompt = prompt; + if (ssh_userauth_kbdint_setanswer(mSession, i, answer.utf8().data()) < 0) { + kdDebug(TDEIO_SFTP_DB) << "An error occurred setting the answer: " + << ssh_get_error(mSession) << endl; + return SSH_AUTH_ERROR; + } + } // for each ssh_userauth_kbdint_getprompt() + } // while (1) - if (openPassDlg(info)) { - kdDebug(TDEIO_SFTP_DB) << "Got the answer from the password dialog" << endl; - answer = info.password.utf8().data(); - } - } + if (!mPasswordWasPrompted && !cachablePassword.isEmpty() && (rc == SSH_AUTH_SUCCESS || rc == SSH_AUTH_PARTIAL)) { + // if the password was never prompted, it was never cached, so we should cache it manually + TDEIO::AuthInfo info = authInfo(); + info.password = cachablePassword; + info.keepPassword = false; + cacheAuthentication(info); + purgeString(info.password); + } - if (ssh_userauth_kbdint_setanswer(mSession, i, answer) < 0) { - kdDebug(TDEIO_SFTP_DB) << "An error occurred setting the answer: " - << ssh_get_error(mSession) << endl; - return SSH_AUTH_ERROR; - } + return rc; +} + +int sftpProtocol::authenticatePassword(bool noPaswordQuery) { + kdDebug(TDEIO_SFTP_DB) << "Trying to authenticate with password" << endl; + + AuthInfo info = authInfo(); + info.keepPassword = true; + info.prompt = i18n("Please enter your username and password."); + + PasswordPurger pPurger(info.password); + + int rc; + do { + TQString errMsg; + + if(noPaswordQuery) { // on the first try use cached password + info.password = mPassword; + purgeString(mPassword); + } else { + if (mPasswordWasPrompted) { + errMsg = i18n("Login failed: incorrect password or username.").append('\n'); + } + + mPasswordWasPrompted = true; + + // Handle user canceled or dialog failed to open... + if (!openPassDlg(info, errMsg)) { + kdDebug(TDEIO_SFTP_DB) << "User canceled password dialog" << endl; + return sftpProtocol::SSH_AUTH_CANCELED; + } + + TQString sshUser=sshUsername(); + if (info.username != sshUser) { + kdDebug(TDEIO_SFTP_DB) << "Username changed from " << sshUser + << " to " << info.username << endl; + mCachedUsername = info.username; + mPassword = info.password; + // libssh doc says that most servers don't permit changing the username during + // authentication, so we should reinitialize the session here + return sftpProtocol::SSH_AUTH_NEED_RECONNECT; } } - err = ssh_userauth_kbdint(mSession, mUsername.utf8().data(), NULL); + + rc = ssh_userauth_password(mSession, NULL, info.password.utf8().data()); + + } while (rc == SSH_AUTH_DENIED && !noPaswordQuery); + + if (!mPasswordWasPrompted && (rc == SSH_AUTH_SUCCESS || rc == SSH_AUTH_PARTIAL)) { + // if the password was never prompted, it was never cached, so we should cache it manually + info.keepPassword = false; + cacheAuthentication(info); + } + return rc; +} + + +TQString sftpProtocol::sshUsername() { + int rc; + TQString rv; + + char *ssh_username = NULL; + rc = ssh_options_get(mSession, SSH_OPTIONS_USER, &ssh_username); + if (rc == 0 && ssh_username && ssh_username[0]) { + rv = TQString::fromUtf8(ssh_username); } + ssh_string_free_char(ssh_username); - return err; + return rv; +} + + +TQString sftpProtocol::sshError(TQString errMsg) { + if (ssh_get_error_code(mSession)) { + errMsg.append("\n\n").append(i18n("SSH error: \"%1\" (%2)") + .arg(TQString::fromUtf8(ssh_get_error(mSession))).arg(ssh_get_error_code(mSession))); + } + return errMsg; +} + +TDEIO::AuthInfo sftpProtocol::authInfo() { + TDEIO::AuthInfo rv; + + rv.url.setProtocol("sftp"); + rv.url.setHost(mHost); + rv.url.setPort(mPort); + rv.url.setUser(mUsername); + + rv.caption = i18n("SFTP Login"); + rv.comment = "sftp://" + mHost + ':' + TQString::number(mPort); + rv.commentLabel = i18n("site:"); + + if(!mUsername.isEmpty()) { + rv.username = mUsername; + } if(!mCachedUsername.isEmpty()) { + rv.username = mCachedUsername; + } else if (mSession) { + rv.username = sshUsername(); + } + + // if username was specified in the address string it shouldn't be changed + if (!mUsername.isEmpty()) { + rv.readOnly = true; + } + + return rv; } void sftpProtocol::reportError(const KURL &url, const int err) { @@ -447,7 +833,7 @@ TQString sftpProtocol::canonicalizePath(const TQString &path) { sftpProtocol::sftpProtocol(const TQCString &pool_socket, const TQCString &app_socket) : SlaveBase("tdeio_sftp", pool_socket, app_socket), mConnected(false), mPort(-1), mSession(NULL), mSftp(NULL) { -#ifndef Q_WS_WIN +#ifndef TQ_WS_WIN kdDebug(TDEIO_SFTP_DB) << "pid = " << getpid() << endl; kdDebug(TDEIO_SFTP_DB) << "debug = " << getenv("TDEIO_SFTP_LOG_VERBOSITY") << endl; @@ -470,15 +856,17 @@ sftpProtocol::sftpProtocol(const TQCString &pool_socket, const TQCString &app_so } sftpProtocol::~sftpProtocol() { -#ifndef Q_WS_WIN +#ifndef TQ_WS_WIN kdDebug(TDEIO_SFTP_DB) << "pid = " << getpid() << endl; #endif closeConnection(); - delete mCallbacks; + free(mCallbacks); /* cleanup and shut down cryto stuff */ ssh_finalize(); + + purgeString(mPassword); } void sftpProtocol::setHost(const TQString& h, int port, const TQString& user, const TQString& pass) { @@ -505,53 +893,11 @@ void sftpProtocol::setHost(const TQString& h, int port, const TQString& user, co mUsername = user; mPassword = pass; + mCachedUsername = TQString::null; } -void sftpProtocol::openConnection() { - - if (mConnected) { - return; - } - - kdDebug(TDEIO_SFTP_DB) << "username=" << mUsername << ", host=" << mHost << ", port=" << mPort << endl; - - infoMessage(i18n("Opening SFTP connection to host %1:%2").arg(mHost).arg(mPort)); - - if (mHost.isEmpty()) { - kdDebug(TDEIO_SFTP_DB) << "openConnection(): Need hostname..." << endl; - error(TDEIO::ERR_UNKNOWN_HOST, i18n("No hostname specified.")); - return; - } - - // Setup AuthInfo for use with password caching and the - // password dialog box. - AuthInfo info; - - info.url.setProtocol("sftp"); - info.url.setHost(mHost); - info.url.setPort(mPort); - info.url.setUser(mUsername); - info.caption = i18n("SFTP Login"); - info.comment = "sftp://" + mHost + ':' + TQString::number(mPort); - info.commentLabel = i18n("site:"); - info.username = mUsername; - info.keepPassword = true; // make the "keep Password" check box visible to the user. - - // Check for cached authentication info if no password is specified... - if (mPassword.isEmpty()) { - kdDebug(TDEIO_SFTP_DB) << "checking cache: info.username = " << info.username - << ", info.url = " << info.url.prettyURL() << endl; - - if (checkCachedAuthentication(info)) { - kdDebug() << "using cached" << endl; - mUsername = info.username; - mPassword = info.password; - } - } - // Start the ssh connection. - TQString msg; // msg for dialog box - TQString caption; // dialog box caption +int sftpProtocol::initializeConnection() { unsigned char *hash = NULL; // the server hash char *hexa; char *verbosity; @@ -561,7 +907,7 @@ void sftpProtocol::openConnection() { mSession = ssh_new(); if (mSession == NULL) { error(TDEIO::ERR_INTERNAL, i18n("Could not create a new SSH session.")); - return; + return SSH_ERROR; } kdDebug(TDEIO_SFTP_DB) << "Creating the SSH session and setting options" << endl; @@ -591,23 +937,24 @@ void sftpProtocol::openConnection() { rc = ssh_options_set(mSession, SSH_OPTIONS_HOST, mHost.utf8().data()); if (rc < 0) { error(TDEIO::ERR_OUT_OF_MEMORY, i18n("Could not set host.")); - return; + return SSH_ERROR; } if (mPort > 0) { rc = ssh_options_set(mSession, SSH_OPTIONS_PORT, &mPort); if (rc < 0) { - error(TDEIO::ERR_OUT_OF_MEMORY, i18n("Could not set port.")); - return; + error(TDEIO::ERR_OUT_OF_MEMORY, i18n("Could not set port.")); + return SSH_ERROR; } } // Set the username - if (!mUsername.isEmpty()) { - rc = ssh_options_set(mSession, SSH_OPTIONS_USER, mUsername.utf8().data()); + if (!mCachedUsername.isEmpty() || !mUsername.isEmpty()) { + TQString username = !mCachedUsername.isEmpty() ? mCachedUsername : mUsername; + rc = ssh_options_set(mSession, SSH_OPTIONS_USER, username.utf8().data()); if (rc < 0) { error(TDEIO::ERR_OUT_OF_MEMORY, i18n("Could not set username.")); - return; + return rc; } } @@ -616,7 +963,7 @@ void sftpProtocol::openConnection() { rc = ssh_options_set(mSession, SSH_OPTIONS_LOG_VERBOSITY_STR, verbosity); if (rc < 0) { error(TDEIO::ERR_OUT_OF_MEMORY, i18n("Could not set log verbosity.")); - return; + return rc; } } @@ -624,7 +971,7 @@ void sftpProtocol::openConnection() { rc = ssh_options_parse_config(mSession, NULL); if (rc < 0) { error(TDEIO::ERR_INTERNAL, i18n("Could not parse the config file.")); - return; + return rc; } ssh_set_callbacks(mSession, mCallbacks); @@ -634,9 +981,8 @@ void sftpProtocol::openConnection() { /* try to connect */ rc = ssh_connect(mSession); if (rc < 0) { - error(TDEIO::ERR_COULD_NOT_CONNECT, TQString::fromUtf8(ssh_get_error(mSession))); - closeConnection(); - return; + error(TDEIO::ERR_COULD_NOT_CONNECT, sshError()); + return rc; } kdDebug(TDEIO_SFTP_DB) << "Getting the SSH server hash" << endl; @@ -644,24 +990,24 @@ void sftpProtocol::openConnection() { /* get the hash */ ssh_key serverKey; #if LIBSSH_VERSION_INT < SSH_VERSION_INT(0, 7, 90) - if (ssh_get_publickey(mSession, &serverKey) < 0) { + rc = ssh_get_publickey(mSession, &serverKey); #else - if (ssh_get_server_publickey(mSession, &serverKey) < 0) { + rc = ssh_get_server_publickey(mSession, &serverKey); #endif - error(TDEIO::ERR_COULD_NOT_CONNECT, TQString::fromUtf8(ssh_get_error(mSession))); - closeConnection(); - return; + if (rc<0) { + error(TDEIO::ERR_COULD_NOT_CONNECT, sshError()); + return rc; } size_t hlen; #if LIBSSH_VERSION_INT < SSH_VERSION_INT(0, 8, 90) - if (ssh_get_publickey_hash(serverKey, SSH_PUBLICKEY_HASH_MD5, &hash, &hlen) < 0) { + rc = ssh_get_publickey_hash(serverKey, SSH_PUBLICKEY_HASH_MD5, &hash, &hlen); #else - if (ssh_get_publickey_hash(serverKey, SSH_PUBLICKEY_HASH_SHA256, &hash, &hlen) < 0) { + rc = ssh_get_publickey_hash(serverKey, SSH_PUBLICKEY_HASH_SHA256, &hash, &hlen); #endif - error(TDEIO::ERR_COULD_NOT_CONNECT, TQString::fromUtf8(ssh_get_error(mSession))); - closeConnection(); - return; + if (rc<0) { + error(TDEIO::ERR_COULD_NOT_CONNECT, sshError()); + return rc; } kdDebug(TDEIO_SFTP_DB) << "Checking if the SSH server is known" << endl; @@ -682,8 +1028,7 @@ void sftpProtocol::openConnection() { "An attacker might change the default server key to confuse your " "client into thinking the key does not exist.\n" "Please contact your system administrator.\n%1").arg(TQString::fromUtf8(ssh_get_error(mSession)))); - closeConnection(); - return; + return SSH_ERROR; case TDEIO_SSH_KNOWN_HOSTS_CHANGED: hexa = ssh_get_hexa(hash, hlen); delete hash; @@ -695,10 +1040,11 @@ void sftpProtocol::openConnection() { "Please contact your system administrator.\n%3").arg( mHost).arg(TQString::fromUtf8(hexa)).arg(TQString::fromUtf8(ssh_get_error(mSession)))); delete hexa; - closeConnection(); - return; + return SSH_ERROR; case TDEIO_SSH_KNOWN_HOSTS_NOT_FOUND: - case TDEIO_SSH_KNOWN_HOSTS_UNKNOWN: + case TDEIO_SSH_KNOWN_HOSTS_UNKNOWN: { + TQString msg; // msg for dialog box + TQString caption; // dialog box caption hexa = ssh_get_hexa(hash, hlen); delete hash; caption = i18n("Warning: Cannot verify host's identity."); @@ -708,9 +1054,8 @@ void sftpProtocol::openConnection() { delete hexa; if (KMessageBox::Yes != messageBox(WarningYesNo, msg, caption)) { - closeConnection(); error(TDEIO::ERR_USER_CANCELED, TQString()); - return; + return SSH_ERROR; } /* write the known_hosts file */ @@ -720,136 +1065,200 @@ void sftpProtocol::openConnection() { #else if (ssh_session_update_known_hosts(mSession) != SSH_OK) { #endif - error(TDEIO::ERR_USER_CANCELED, TQString::fromUtf8(ssh_get_error(mSession))); - closeConnection(); - return; + error(TDEIO::ERR_USER_CANCELED, sshError()); + return SSH_ERROR; } break; + } case TDEIO_SSH_KNOWN_HOSTS_ERROR: delete hash; - error(TDEIO::ERR_COULD_NOT_CONNECT, TQString::fromUtf8(ssh_get_error(mSession))); - return; + error(TDEIO::ERR_COULD_NOT_CONNECT, sshError()); + return SSH_ERROR; } kdDebug(TDEIO_SFTP_DB) << "Trying to authenticate with the server" << endl; - // Try to authenticate - rc = ssh_userauth_none(mSession, NULL); - if (rc == SSH_AUTH_ERROR) { - closeConnection(); - error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed (method: %1).") - .arg(i18n("none"))); + return SSH_OK; +} + + +void sftpProtocol::openConnection() { + + if (mConnected) { return; } - int method = ssh_auth_list(mSession); - if (!method && rc != SSH_AUTH_SUCCESS) - { - error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed." - " The server did not send any authentication methods!")); + kdDebug(TDEIO_SFTP_DB) << "username=" << mUsername << ", host=" << mHost << ", port=" << mPort << endl; + + infoMessage(i18n("Opening SFTP connection to host %1:%2").arg(mHost).arg(mPort)); + + if (mHost.isEmpty()) { + kdDebug(TDEIO_SFTP_DB) << "openConnection(): Need hostname..." << endl; + error(TDEIO::ERR_UNKNOWN_HOST, i18n("No hostname specified.")); return; } - bool firstTime = true; - bool dlgResult; - while (rc != SSH_AUTH_SUCCESS) { - // Try to authenticate with public key first - if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_PUBLICKEY) && !mPassword) - { - kdDebug(TDEIO_SFTP_DB) << "Trying to authenticate with public key" << endl; - for(;;) - { - rc = ssh_userauth_publickey_auto(mSession, nullptr, nullptr); - if (rc == SSH_AUTH_ERROR) - { - clearPubKeyAuthInfo(); - closeConnection(); - error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed (method: %1).") - .arg(i18n("public key"))); - return; - } - if (rc == SSH_AUTH_DENIED || !pubKeyInfo || !pubKeyInfo->isModified()) - { - clearPubKeyAuthInfo(); - break; - } - } + // Check for cached authentication info if no password is specified... + if (mPassword.isEmpty()) { + AuthInfo info = authInfo(); + + kdDebug(TDEIO_SFTP_DB) << "checking cache: info.username = " << info.username + << ", info.url = " << info.url.prettyURL() << endl; + + if (checkCachedAuthentication(info)) { + kdDebug() << "using cached" << endl; + mCachedUsername = info.username; + mPassword = info.password; + + purgeString(info.password); //< not really necessary because of Qt's implicit data sharing } + } - // Try to authenticate with keyboard interactive - if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_INTERACTIVE)) - { - kdDebug(TDEIO_SFTP_DB) << "Trying to authenticate with keyboard interactive" << endl; + mPasswordWasPrompted = false; + PasswordPurger pwPurger{mPassword}; - TDEIO::AuthInfo tmpInfo(info); - rc = authenticateKeyboardInteractive(tmpInfo); - if (rc == SSH_AUTH_SUCCESS) - { - info = tmpInfo; - } - else if (rc == SSH_AUTH_ERROR) - { - closeConnection(); - error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed (method: %1).") - .arg(i18n("keyboard interactive"))); - return; - } - } + int rc; + ExitGuard connectionCloser([this](){ closeConnection(); }); - // Try to authenticate with password - if (rc != SSH_AUTH_SUCCESS && (method & SSH_AUTH_METHOD_PASSWORD)) - { - kdDebug(TDEIO_SFTP_DB) << "Trying to authenticate with password" << endl; - - info.keepPassword = true; - for(;;) - { - if(!firstTime || mPassword.isEmpty()) - { - if (firstTime) { - info.prompt = i18n("Please enter your username and password."); - } else { - info.prompt = i18n("Login failed.\nPlease confirm your username and password, and enter them again."); - } - dlgResult = openPassDlg(info); - - // Handle user canceled or dialog failed to open... - if (!dlgResult) { - kdDebug(TDEIO_SFTP_DB) << "User canceled, dlgResult = " << dlgResult << endl; - closeConnection(); - error(TDEIO::ERR_USER_CANCELED, TQString()); - return; - } + do { // A loop to restart connection when needed + // Start the ssh connection. + if (initializeConnection() < 0) { + return; + } - firstTime = false; - } + // Try to authenticate (this required before calling ssh_auth_list()) + rc = ssh_userauth_none(mSession, NULL); + if (rc == SSH_AUTH_ERROR) { + error(TDEIO::ERR_COULD_NOT_LOGIN, sshError(i18n("Authentication failed (method: %1).") + .arg(i18n("none")))); + return; + } - if (mUsername != info.username) { - kdDebug(TDEIO_SFTP_DB) << "Username changed from " << mUsername - << " to " << info.username << endl; - } - mUsername = info.username; - mPassword = info.password; + // Preinit the list of supported auth methods + static const auto authMethodsNormal = [](){ + std::vector<std::unique_ptr<SSHAuthMethod>> rv; + rv.emplace_back(std::unique_ptr<PublicKeyAuth>(new PublicKeyAuth)); + rv.emplace_back(std::unique_ptr<KeyboardInteractiveAuth>(new KeyboardInteractiveAuth)); + rv.emplace_back(std::unique_ptr<PasswordAuth>(new PasswordAuth)); + return rv; + }(); + + const static int supportedMethods = std::accumulate( + authMethodsNormal.begin(), authMethodsNormal.end(), + SSH_AUTH_METHOD_NONE, //< none is supported by default + [](int acc, const std::unique_ptr<SSHAuthMethod> &m){ return acc |= m->flag(); }); + + unsigned attemptedMethods = 0; + + // Backup of the value of the SlaveBase::s_seqNr. This is used to query different data values + // with openPassDlg() with the same seqNr. Otherwise it will result in the prompting of the pass + // dialog to the user in cases the values should be recovered from the cache. + // This is a bit hacky but necessary + long current_seqNr = SlaveBase::s_seqNr; + + while (rc != SSH_AUTH_SUCCESS) { + // Note this loop can rerun in case of multistage ssh authentication e.g. "password,publickey" + // which will require user to provide a valid password at first and then a valid public key. + // see AuthenticationMethods in man 5 sshd_config for more info + bool wasCanceled = false; + unsigned availableMethodes = ssh_auth_list(mSession); + + SlaveBase::s_seqNr = current_seqNr; + + if (!availableMethodes) { + // Technically libssh docs suggest that the server merely MAY send auth methods, but it's + // highly unclear what we should do in such case and it looks like openssh doesn't have an + // option for that, so let's just consider this server a jerk and don't talk to him anymore. + error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed.\n" + "The server did not send any authentication methods!")); + return; + } else if (!(availableMethodes & supportedMethods)) { + error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed.\n" + "The server sent only unsupported authentication methods (%1)!") + .arg(SSHAuthMethod::bitsetToStr(availableMethodes).join(", "))); + return; + } - rc = ssh_userauth_password(mSession, mUsername.utf8().data(), - mPassword.utf8().data()); - if (rc == SSH_AUTH_ERROR) { - closeConnection(); - error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Authentication failed (method: %1).") - .arg(i18n("password"))); + const auto *authMethods = &authMethodsNormal; + + // If we have cached password we want try to use it before public key + if(!mPassword.isEmpty()) { + static const auto authMethodsWithPassword = []() { + std::vector<std::unique_ptr<SSHAuthMethod>> rv; + rv.emplace_back(std::unique_ptr<KeyboardInteractiveAuth>( + new KeyboardInteractiveAuth(/* noPasswordQuery = */true) ) ); + rv.emplace_back(std::unique_ptr<PasswordAuth>( + new PasswordAuth(/* noPasswordQuery = */true) ) ); + for (const auto &m: authMethodsNormal) { rv.emplace_back(m->clone()); } + return rv; + }(); + + authMethods = &authMethodsWithPassword; + } + + // Actually iterate over the list of methods and try them out + for (const auto &method: *authMethods) { + if (!(availableMethodes & method->flag())) { continue; } + + rc = method->authenticate( this ); + attemptedMethods |= method->flag(); + if (rc == SSH_AUTH_SUCCESS || rc == SSH_AUTH_PARTIAL) { + kdDebug(TDEIO_SFTP_DB) << "method=" << method->name() << ": auth " + << (rc == SSH_AUTH_SUCCESS ? "success" : "partial") << endl; + break; // either next auth method or continue on with the connect + } else if (rc == SSH_AUTH_ERROR || rc == SSH_AUTH_AGAIN) { + TQString errMsg = i18n("Authentication failed (method: %1).").arg(method->name()); + // SSH_AUTH_AGAIN returned in case of some errors when server hangs up unexpectedly like + // in case there were too many failed authentication attempts + if (rc == SSH_AUTH_AGAIN) { + errMsg.append("\n").append(i18n("Server is slow to respond or hung up unexpectedly.")); + } + error(TDEIO::ERR_COULD_NOT_LOGIN, sshError(errMsg)); return; - } else if (rc == SSH_AUTH_SUCCESS) { + } else if (rc == SSH_AUTH_CANCELED) { + kdDebug(TDEIO_SFTP_DB) << "method=" << method->name() << " was canceled by user" << endl; + // don't quit immediately due to that the user might have canceled one method to use another + wasCanceled = true; + } else if (rc == SSH_AUTH_NEED_RECONNECT) { + kdDebug(TDEIO_SFTP_DB) << "method=" << method->name() << " requested reconnection" << endl; break; + } else if (rc == SSH_AUTH_DENIED) { + kdDebug(TDEIO_SFTP_DB) << "Auth for method=" << method->name() << " was denied" << endl; + // do nothing, just proceed with next auth method + } else { + // Shouldn't happen, but to be on the safe side better handle it + error(TDEIO::ERR_UNKNOWN, sshError(i18n("Authentication failed unexpectedly"))); + return; } } - } - } + + // At this point rc values should be one of: + // SSH_AUTH_SUCCESS, SSH_AUTH_PARTIAL, SSH_AUTH_DENIED, SSH_AUTH_CANCELED or SSH_AUTH_NEED_RECONNECT + if(rc == SSH_AUTH_NEED_RECONNECT) { + closeConnection(); //< have to do it manually + break; + } else if (wasCanceled && (rc == SSH_AUTH_CANCELED || rc == SSH_AUTH_DENIED)) { + error(TDEIO::ERR_USER_CANCELED, TQString::null); + return; + } else if (rc != SSH_AUTH_SUCCESS && rc != SSH_AUTH_PARTIAL) { + TQString errMsg = i18n("Authentication denied (attempted methods: %1).") + .arg(SSHAuthMethod::bitsetToStr(attemptedMethods).join(", ")); + if (availableMethodes & ~supportedMethods) { + errMsg.append("\n") + .append(i18n("Note: server also declares some unsupported authentication methods (%1)") + .arg(SSHAuthMethod::bitsetToStr(availableMethodes & ~supportedMethods).join(", "))); + } + error(TDEIO::ERR_COULD_NOT_LOGIN, errMsg); + return; + } + } // while (rc != SSH_AUTH_SUCCESS) + } while(rc == SSH_AUTH_NEED_RECONNECT); + // start sftp session kdDebug(TDEIO_SFTP_DB) << "Trying to request the sftp session" << endl; mSftp = sftp_new(mSession); if (mSftp == NULL) { - closeConnection(); error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Unable to request the SFTP subsystem. " "Make sure SFTP is enabled on the server.")); return; @@ -857,34 +1266,19 @@ void sftpProtocol::openConnection() { kdDebug(TDEIO_SFTP_DB) << "Trying to initialize the sftp session" << endl; if (sftp_init(mSftp) < 0) { - closeConnection(); error(TDEIO::ERR_COULD_NOT_LOGIN, i18n("Could not initialize the SFTP session.")); return; } // Login succeeded! infoMessage(i18n("Successfully connected to %1").arg(mHost)); - info.url.setProtocol("sftp"); - info.url.setHost(mHost); - info.url.setPort(mPort); - info.url.setUser(mUsername); - info.username = mUsername; - info.password = mPassword; - - kdDebug(TDEIO_SFTP_DB) << "Caching info.username = " << info.username - << ", info.url = " << info.url.prettyURL() << endl; - - cacheAuthentication(info); //setTimeoutSpecialCommand(TDEIO_SFTP_SPECIAL_TIMEOUT); mConnected = true; - connected(); + connectionCloser.abort(); - mPassword.fill('x'); - mPassword = ""; - info.password.fill('x'); - info.password = ""; + connected(); return; } @@ -925,7 +1319,7 @@ void sftpProtocol::special(const TQByteArray &data) { } if (rc < 0) { - kdDebug(TDEIO_SFTP_DB) << "channel_poll failed: " << ssh_get_error(mSession); + kdDebug(TDEIO_SFTP_DB) << "channel_poll failed: " << ssh_get_error(mSession) << endl; } setTimeoutSpecialCommand(TDEIO_SFTP_SPECIAL_TIMEOUT); @@ -1833,12 +2227,3 @@ void sftpProtocol::slave_status() { kdDebug(TDEIO_SFTP_DB) << "connected to " << mHost << "?: " << mConnected << endl; slaveStatus((mConnected ? mHost : TQString()), mConnected); } - -void sftpProtocol::clearPubKeyAuthInfo() -{ - if (!pubKeyInfo) - { - delete pubKeyInfo; - pubKeyInfo = nullptr; - } -} |
