/*
 * simplesasl.cpp - Simple SASL implementation
 * Copyright (C) 2003  Justin Karneges
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 */

#include "simplesasl.h"

#include <QObject>
#include <qhostaddress.h>
#include <qstringlist.h>
#include <QList>
#include <qca.h>
#include <QByteArray>
#include <stdlib.h>
#include <QtCrypto>
#include <QDebug>

#include "xmpp/sasl/plainmessage.h"
#include "xmpp/sasl/digestmd5response.h"
#include "xmpp/sasl/scramsha1response.h"
#include "xmpp/sasl/scramsha1message.h"
#include "xmpp/sasl/scramsha1signature.h"
#include "xmpp/base/randrandomnumbergenerator.h"

namespace XMPP {
class SimpleSASLContext : public QCA::SASLContext
{
	Q_OBJECT
public:
		class ParamsMutable
		{
		public:
			/**
			   User is held
			*/
			bool user;

			/**
			   Authorization ID is held
			*/
			bool authzid;

			/**
			   Password is held
			*/
			bool pass;

			/**
			   Realm is held
			*/
			bool realm;
		};
	// core props
	QString service, host;

	// state
	int step;
	bool capable;
	bool allow_plain;
	QByteArray out_buf, in_buf;
	QString mechanism_;
	QString out_mech;

	ParamsMutable need;
	ParamsMutable have;
	QString user, authz, realm;
	QCA::SecureArray pass;
	Result result_;
	QCA::SASL::AuthCondition authCondition_;
	QByteArray result_to_net_, result_to_app_;
	int encoded_;

	// scram specific stuff
	QByteArray client_first_message;
	QCA::SecureArray server_signature;

	SimpleSASLContext(QCA::Provider* p) : QCA::SASLContext(p)
	{
		reset();
	}

	~SimpleSASLContext()
	{
		reset();
	}

	void reset()
	{
		resetState();

		capable = true;
		allow_plain = false;
		need.user = false;
		need.authzid = false;
		need.pass = false;
		need.realm = false;
		have.user = false;
		have.authzid = false;
		have.pass = false;
		have.realm = false;
		user = QString();
		authz = QString();
		pass = QCA::SecureArray();
		realm = QString();
	}

	void resetState()
	{
		out_mech = QString();
		out_buf.resize(0);
		authCondition_ = QCA::SASL::AuthFail;
	}

	virtual void setConstraints(QCA::SASL::AuthFlags flags, int ssfMin, int) {
		if(flags & (QCA::SASL::RequireForwardSecrecy | QCA::SASL::RequirePassCredentials | QCA::SASL::RequireMutualAuth) || ssfMin > 0)
			capable = false;
		else
			capable = true;
		allow_plain = flags & QCA::SASL::AllowPlain;
	}

	virtual void setup(const QString& _service, const QString& _host, const QCA::SASLContext::HostPort*, const QCA::SASLContext::HostPort*, const QString&, int) {
		service = _service;
		host = _host;
	}

	virtual void startClient(const QStringList &mechlist, bool allowClientSendFirst) {
		Q_UNUSED(allowClientSendFirst);

		mechanism_ = QString();
		foreach(QString mech, mechlist) {
			if (mech == "SCRAM-SHA-1") {
				mechanism_ = "SCRAM-SHA-1";
				break;
			}
			if (mech == "DIGEST-MD5") {
				mechanism_ = "DIGEST-MD5";
				break;
			}
			if (mech == "PLAIN" && allow_plain)
				mechanism_ = "PLAIN";
		}

		if(!capable || mechanism_.isEmpty()) {
			result_ = Error;
			authCondition_ = QCA::SASL::NoMechanism;
			if (!capable)
				qWarning("simplesasl.cpp: Not enough capabilities");
			if (mechanism_.isEmpty())
				qWarning("simplesasl.cpp: No mechanism available");
			QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
			return;
		}

		resetState();
		result_ = Continue;
		step = 0;
		tryAgain();
	}

	virtual void nextStep(const QByteArray &from_net) {
		in_buf = from_net;
		tryAgain();
	}

	virtual void tryAgain() {
		// All exits of the method must emit the ready signal
		// so all exits go through a goto ready;
		if(step == 0) {
			out_mech = mechanism_;

			// PLAIN
			if (out_mech == "PLAIN" or out_mech == "SCRAM-SHA-1") {
				// First, check if we have everything
				if(need.user || need.pass) {
					qWarning("simplesasl.cpp: Did not receive necessary auth parameters");
					result_ = Error;
					goto ready;
				}
				if(!have.user)
					need.user = true;
				if(!have.pass)
					need.pass = true;
				if(need.user || need.pass) {
					result_ = Params;
					goto ready;
				}
			}
			if (out_mech == "PLAIN") {
				out_buf = PLAINMessage(authz, user, pass.toByteArray()).getValue();
			} else if (out_mech == "SCRAM-SHA-1") {
				// send client-first-message
				SCRAMSHA1Message msg(authz, user, QByteArray(0, ' '), RandRandomNumberGenerator());
				if (msg.isValid()) {
					out_buf = msg.getValue();
					client_first_message = out_buf;
				} else {
					qWarning("simplesasl.cpp: SASLprep failed.");
					result_ = Error;
					goto ready;
				}
			}
			++step;
			if (out_mech == "PLAIN")
				result_ = Success;
			else
				result_ = Continue;
		} else if(step == 1) {
			Q_ASSERT(out_mech != "PLAIN");
			if (out_mech == "DIGEST-MD5") {
				// if we still need params, then the app has failed us!
				if(need.user || need.authzid || need.pass || need.realm) {
					qWarning("simplesasl.cpp: Did not receive necessary auth parameters");
					result_ = Error;
					goto ready;
				}

				// see if some params are needed
				if(!have.user)
					need.user = true;
				//if(!have.authzid)
				//	need.authzid = true;
				if(!have.pass)
					need.pass = true;
				if(need.user || need.authzid || need.pass) {
					result_ = Params;
					goto ready;
				}

				DIGESTMD5Response response(in_buf, service, host, realm, user, authz, pass.toByteArray(), RandRandomNumberGenerator());
				if (!response.isValid()) {
					authCondition_ = QCA::SASL::BadProtocol;
					result_ = Error;
					goto ready;
				}
				out_buf = response.getValue();
				++step;
				result_ = Continue;
			} else if (out_mech == "SCRAM-SHA-1") {
				// if we still need params, then the app has failed us!
				if(need.user || need.pass) {
					qWarning("simplesasl.cpp: Did not receive necessary auth parameters");
					result_ = Error;
					goto ready;
				}

				// see if some params are needed
				if(!have.user)
					need.user = true;
				//if(!have.authzid)
				//	need.authzid = true;
				if(!have.pass)
					need.pass = true;
				if(need.user || need.pass) {
					result_ = Params;
					goto ready;
				}
				// parse server-first-message, send client-final-message
				QVariant prop = property("scram-salted-password-base64");
				QString salted_password_base64;
				if (prop.isValid()) {
					salted_password_base64 = prop.toString();
				}
				SCRAMSHA1Response response(in_buf, pass.toByteArray(), client_first_message, salted_password_base64, RandRandomNumberGenerator());
				if (!response.isValid()) {
					authCondition_ = QCA::SASL::BadProtocol;
					result_ = Error;
					goto ready;
				}
				setProperty("scram-salted-password-base64", QVariant(response.getSaltedPassword()));
				server_signature = response.getServerSignature();
				out_buf = response.getValue();
				++step;
				result_ = Continue;
			}
		} else if (step == 2 && out_mech == "SCRAM-SHA-1") {
			// verify the server's response on success, for SCRAM-SHA-1
			SCRAMSHA1Signature sig(in_buf, server_signature);
			if (sig.isValid()) {
				result_ = Success;
			} else {
				qWarning() << "ServerSignature doesn't match the one we've calculated.";
				authCondition_ = QCA::SASL::AuthFail;
				result_ = Error;
				goto ready;
			}
		}
		/*else if (step == 2) {
			//Commenting this out is Justin's fix for updated QCA.
			out_buf.resize(0);
			result_ = Continue;
			++step;
		}*/
		else {
			out_buf.resize(0);
			result_ = Success;
		}
ready:
		QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
	}

	virtual void update(const QByteArray &from_net, const QByteArray &from_app) {
		result_to_app_ = from_net;
		result_to_net_ = from_app;
		encoded_ = from_app.size();
		result_ = Success;
		QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
	}

	virtual bool waitForResultsReady(int msecs) {

		// TODO: for now, all operations block anyway
		Q_UNUSED(msecs);
		return true;
	}

	virtual Result result() const {
		return result_;
	}

	virtual QStringList mechlist() const {
		return QStringList();
	}

	virtual QString mech() const {
		return out_mech;
	}

	virtual bool haveClientInit() const {
		return out_mech == "PLAIN";
	}

	virtual QByteArray stepData() const {
		return out_buf;
	}

	virtual QByteArray to_net() {
		return result_to_net_;
	}

	virtual int encoded() const {
		return encoded_;
	}

	virtual QByteArray to_app() {
		return result_to_app_;
	}

	virtual int ssf() const {
		return 0;
	}

	virtual QCA::SASL::AuthCondition authCondition() const {
		return authCondition_;
	}

	virtual QCA::SASL::Params clientParams() const {
		return QCA::SASL::Params(need.user, need.authzid, need.pass, need.realm);
	}

	virtual void setClientParams(const QString *_user, const QString *_authzid, const QCA::SecureArray *_pass, const QString *_realm) {
		if(_user) {
			user = *_user;
			need.user = false;
			have.user = true;
		}
		if(_authzid) {
			authz = *_authzid;
			need.authzid = false;
			have.authzid = true;
		}
		if(_pass) {
			pass = *_pass;
			need.pass = false;
			have.pass = true;
		}
		if(_realm) {
			realm = *_realm;
			need.realm = false;
			have.realm = true;
		}
	}

	virtual QStringList realmlist() const
	{
		// TODO
		return QStringList();
	}

	virtual QString username() const {
		return QString();
	}

	virtual QString authzid() const {
		return QString();
	}

	virtual QCA::Provider::Context* clone() const {
		SimpleSASLContext* s = new SimpleSASLContext(provider());
		// TODO: Copy all the members
		return s;
	}

	virtual void startServer(const QString &, bool) {
		result_ =  QCA::SASLContext::Error;
		QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
	}
	virtual void serverFirstStep(const QString &, const QByteArray *) {
		result_ =  QCA::SASLContext::Error;
		QMetaObject::invokeMethod(this, "resultsReady", Qt::QueuedConnection);
	}

};

class QCASimpleSASL : public QCA::Provider
{
public:
	QCASimpleSASL() {}
	~QCASimpleSASL() {}

	void init()
	{
	}

	QString name() const {
		return "simplesasl";
	}

	QStringList features() const {
		return QStringList("sasl");
	}

	QCA::Provider::Context* createContext(const QString& cap)
	{
		if(cap == "sasl")
			return new SimpleSASLContext(this);
		return 0;
	}
	int qcaVersion() const
	{
		return QCA_VERSION;
	}
};

QCA::Provider *createProviderSimpleSASL()
{
	return (new QCASimpleSASL);
}

}

#include "simplesasl.moc"
