/*
 * Copyright (C) 2014-2022 CZ.NIC
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * In addition, as a special exception, the copyright holders give
 * permission to link the code of portions of this program with the
 * OpenSSL library under certain conditions as described in each
 * individual source file, and distribute linked combinations including
 * the two.
 */

#include <QDir>
#include <QSettings>

#include "src/datovka_shared/crypto/crypto_pwd.h"
#include "src/datovka_shared/crypto/crypto_wrapped.h"
#include "src/datovka_shared/log/log.h"
#include "src/datovka_shared/settings/account.h"
#include "src/datovka_shared/settings/account_p.h"

/* Null objects - for convenience. */
static const QByteArray nullByteArray;
static const QString nullString;

namespace CredNames {
	const QString regularCreds("credentials");
	const QString shadowCreds("shadow_credentials");

	const QString acntName("name");
	const QString userName("username");
	const QString lMethod("login_method");
	const QString pwd("password");
	const QString pwdAlg("password_alg");
	const QString pwdSalt("password_salt");
	const QString pwdIv("password_iv");
	const QString pwdCode("password_code");
	const QString mepToken("mep_token");
	const QString testAcnt("test_account");
	const QString rememberPwd("remember_password");
	const QString dbDir("database_dir");
	const QString syncWithAll("sync_with_all");
	const QString p12File("p12file");
}

/*!
 * @brief Login method names as stored in configuration file.
 */
namespace MethodNames {
	static const QString uNamePwd("username");
	static const QString uNameCrt("certificate");
	static const QString uNamePwdCrt("user_certificate");
	static const QString uNamePwdHotp("hotp");
	static const QString uNamePwdTotp("totp");
	static const QString uNameMep("mep");
}

/*!
 * @brief Converts login method string to identifier.
 *
 * @param[in] str Identifier string as used in configuration file.
 * @return Identifier value.
 */
static
enum AcntSettings::LoginMethod methodStrToEnum(const QString &str)
{
	if (str == MethodNames::uNamePwd) {
		return AcntSettings::LIM_UNAME_PWD;
	} else if (str == MethodNames::uNameCrt) {
		return AcntSettings::LIM_UNAME_CRT;
	} else if (str == MethodNames::uNamePwdCrt) {
		return AcntSettings::LIM_UNAME_PWD_CRT;
	} else if (str == MethodNames::uNamePwdHotp) {
		return AcntSettings::LIM_UNAME_PWD_HOTP;
	} else if (str == MethodNames::uNamePwdTotp) {
		return AcntSettings::LIM_UNAME_PWD_TOTP;
	} else if (str == MethodNames::uNameMep) {
		return AcntSettings::LIM_UNAME_MEP;
	} else {
		return AcntSettings::LIM_UNKNOWN;
	}
}

/*!
 * @brief Converts login method identifier to string.
 *
 * @param[in] val Identifier value as used in the programme.
 * @return Identifier string as used in configuration file.
 */
static
const QString &methodEnumToStr(enum AcntSettings::LoginMethod val)
{
	static const QString nullStr;

	switch (val) {
	case AcntSettings::LIM_UNAME_PWD:
		return MethodNames::uNamePwd;
		break;
	case AcntSettings::LIM_UNAME_CRT:
		return MethodNames::uNameCrt;
		break;
	case AcntSettings::LIM_UNAME_PWD_CRT:
		return MethodNames::uNamePwdCrt;
		break;
	case AcntSettings::LIM_UNAME_PWD_HOTP:
		return MethodNames::uNamePwdHotp;
		break;
	case AcntSettings::LIM_UNAME_PWD_TOTP:
		return MethodNames::uNamePwdTotp;
		break;
	case AcntSettings::LIM_UNAME_MEP:
		return MethodNames::uNameMep;
		break;
	case AcntSettings::LIM_UNKNOWN:
	default:
		Q_ASSERT(0);
		return nullStr;
		break;
	}
}

AcntSettings::AcntSettings(void)
    : d_ptr(Q_NULLPTR)
{
}

AcntSettings::AcntSettings(const AcntSettings &other)
    : d_ptr((other.d_func() != Q_NULLPTR) ? (new (::std::nothrow) AcntSettingsPrivate) : Q_NULLPTR)
{
	Q_D(AcntSettings);
	if (d == Q_NULLPTR) {
		return;
	}

	*d = *other.d_func();
}

#ifdef Q_COMPILER_RVALUE_REFS
AcntSettings::AcntSettings(AcntSettings &&other) Q_DECL_NOEXCEPT
#if (QT_VERSION >= QT_VERSION_CHECK(5, 12, 0))
    : d_ptr(other.d_ptr.release()) //d_ptr(::std::move(other.d_ptr))
#else /* < Qt-5.12 */
    : d_ptr(other.d_ptr.take())
#endif /* >= Qt-5.12 */
{
}
#endif /* Q_COMPILER_RVALUE_REFS */

AcntSettings::~AcntSettings(void)
{
}

/*!
 * @brief Ensures private account settings presence.
 *
 * @note Returns if private account settings could not be allocated.
 */
#define ensureAcntSettingsPrivate(_x_) \
	do { \
		if (Q_UNLIKELY(!ensurePrivate())) { \
			return _x_; \
		} \
	} while (0)

AcntSettings &AcntSettings::operator=(const AcntSettings &other) Q_DECL_NOTHROW
{
	if (other.d_func() == Q_NULLPTR) {
		d_ptr.reset(Q_NULLPTR);
		return *this;
	}
	ensureAcntSettingsPrivate(*this);
	Q_D(AcntSettings);

	*d = *other.d_func();

	return *this;
}

#ifdef Q_COMPILER_RVALUE_REFS
AcntSettings &AcntSettings::operator=(AcntSettings &&other) Q_DECL_NOTHROW
{
	swap(*this, other);
	return *this;
}
#endif /* Q_COMPILER_RVALUE_REFS */

bool AcntSettings::operator==(const AcntSettings &other) const
{
	Q_D(const AcntSettings);
	if ((d == Q_NULLPTR) && ((other.d_func() == Q_NULLPTR))) {
		return true;
	} else if ((d == Q_NULLPTR) || ((other.d_func() == Q_NULLPTR))) {
		return false;
	}

	return *d == *other.d_func();
}

bool AcntSettings::operator!=(const AcntSettings &other) const
{
	return !operator==(other);
}

bool AcntSettings::isNull(void) const
{
	Q_D(const AcntSettings);
	return d == Q_NULLPTR;
}

void AcntSettings::clear(void)
{
	d_ptr.reset(Q_NULLPTR);
}

bool AcntSettings::isValid(void) const
{
	return !isNull() &&
	    !accountName().isEmpty() && !userName().isEmpty();
}

const QString &AcntSettings::accountName(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_accountName;
}

void AcntSettings::setAccountName(const QString &an)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_accountName = an;
}

#ifdef Q_COMPILER_RVALUE_REFS
void  AcntSettings::setAccountName(QString &&an)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_accountName = ::std::move(an);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QString &AcntSettings::userName(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_userName;
}

void AcntSettings::setUserName(const QString &un)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_userName = un;
}

#ifdef Q_COMPILER_RVALUE_REFS
void AcntSettings::setUserName(QString &&un)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_userName = ::std::move(un);
}
#endif /* Q_COMPILER_RVALUE_REFS */

enum AcntSettings::LoginMethod AcntSettings::loginMethod(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return LIM_UNKNOWN;
	}

	return d->m_loginMethod;
}

void AcntSettings::setLoginMethod(enum LoginMethod method)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_loginMethod = method;
}

const QString &AcntSettings::password(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_password;
}

void AcntSettings::setPassword(const QString &pwd)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_password = pwd;
}

#ifdef Q_COMPILER_RVALUE_REFS
void AcntSettings::setPassword(QString &&pwd)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_password = ::std::move(pwd);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QString &AcntSettings::pwdAlg(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_pwdAlg;
}

void AcntSettings::setPwdAlg(const QString &pwdAlg)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_pwdAlg = pwdAlg;
}

#ifdef Q_COMPILER_RVALUE_REFS
void AcntSettings::setPwdAlg(QString &&pwdAlg)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_pwdAlg = ::std::move(pwdAlg);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QByteArray &AcntSettings::pwdSalt(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullByteArray;
	}

	return d->m_pwdSalt;
}

void AcntSettings::setPwdSalt(const QByteArray &pwdSalt)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_pwdSalt = pwdSalt;
}

#ifdef Q_COMPILER_RVALUE_REFS
void AcntSettings::setPwdSalt(QByteArray &&pwdSalt)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_pwdSalt = ::std::move(pwdSalt);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QByteArray &AcntSettings::pwdIv(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullByteArray;
	}

	return d->m_pwdIv;
}

void AcntSettings::setPwdIv(const QByteArray &pwdIv)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_pwdIv = pwdIv;
}

#ifdef Q_COMPILER_RVALUE_REFS
void AcntSettings::setPwdIv(QByteArray &&pwdIv)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_pwdIv = ::std::move(pwdIv);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QByteArray &AcntSettings::pwdCode(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullByteArray;
	}

	return d->m_pwdCode;
}

void AcntSettings::setPwdCode(const QByteArray &pwdCode)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_pwdCode = pwdCode;
}

#ifdef Q_COMPILER_RVALUE_REFS
void AcntSettings::setPwdCode(QByteArray &&pwdCode)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_pwdCode = ::std::move(pwdCode);
}
#endif /* Q_COMPILER_RVALUE_REFS */

const QString &AcntSettings::mepToken(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_mepToken;
}

void AcntSettings::setMepToken(const QString &mepToken)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_mepToken = mepToken;
}

#ifdef Q_COMPILER_RVALUE_REFS
void AcntSettings::setMepToken(QString &&mepToken)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_mepToken = ::std::move(mepToken);
}
#endif /* Q_COMPILER_RVALUE_REFS */

bool AcntSettings::isTestAccount(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return false;
	}

	return d->m_isTestAccount;
}

void AcntSettings::setTestAccount(bool isTesting)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_isTestAccount = isTesting;
}

bool AcntSettings::rememberPwd(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return false;
	}

	return d->m_rememberPwd;
}

void AcntSettings::setRememberPwd(bool remember)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_rememberPwd = remember;
}

const QString &AcntSettings::dbDir(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_dbDir;
}

void AcntSettings::setDbDir(const QString &path, const QString &confDir)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_dbDir = (path == confDir) ? QString() : path; /* Default path is empty. */
}

#ifdef Q_COMPILER_RVALUE_REFS
void AcntSettings::setDbDir(QString &&path, const QString &confDir)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_dbDir = ::std::move((path == confDir) ? QString() : path); /* Default path is empty. */
}
#endif /* Q_COMPILER_RVALUE_REFS */

bool AcntSettings::syncWithAll(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return false;
	}

	return d->m_syncWithAll;
}

void AcntSettings::setSyncWithAll(bool sync)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_syncWithAll = sync;
}

const QString &AcntSettings::p12File(void) const
{
	Q_D(const AcntSettings);
	if (Q_UNLIKELY(d == Q_NULLPTR)) {
		return nullString;
	}

	return d->m_p12File;
}

void AcntSettings::setP12File(const QString &p12)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_p12File = p12;
}

#ifdef Q_COMPILER_RVALUE_REFS
void AcntSettings::setP12File(QString &&p12)
{
	ensureAcntSettingsPrivate();
	Q_D(AcntSettings);
	d->m_p12File = ::std::move(p12);
}
#endif /* Q_COMPILER_RVALUE_REFS */

void AcntSettings::decryptPassword(const QString &oldPin)
{
	if (!password().isEmpty()) {
		/* Password already stored in decrypted form. */
		logDebugLv0NL(
		    "Password for username '%s' already held in decrypted form.",
		    userName().toUtf8().constData());
		return;
	}

	if (oldPin.isEmpty()) {
		/*
		 * Old PIN not given, password should already be in plain
		 * format.
		 */
		logDebugLv0NL(
		    "No PIN supplied to decrypt password for username '%s'.",
		    userName().toUtf8().constData());
		return;
	}

	if (!pwdAlg().isEmpty() && !pwdSalt().isEmpty() &&
	    !pwdCode().isEmpty()) {
		logDebugLv0NL("Decrypting password for username '%s'.",
		    userName().toUtf8().constData());
		QString decrypted(decryptPwd(pwdCode(), oldPin, pwdAlg(),
		    pwdSalt(), pwdIv()));
		/* Store password. */
		if (!decrypted.isEmpty()) {
			setPassword(decrypted);
		} else {
			logWarningNL(
			    "Failed decrypting password for username '%s'.",
			    userName().toUtf8().constData());
		}
	}
}

/*!
 * @brief Read password data.
 *
 * @note The PIN value may not be known when the settings are read. Therefore
 *     password decryption is performed somewhere else.
 * @todo Modify the programme to ensure that PIN is known at the time when
 *     account data is read.
 *
 * @param[in,out] aData Account data to store password data into.
 * @param[in]     settings Settings structure.
 * @param[in]     groupName Settings group name.
 */
static
void readPwdData(AcntSettings &aData, const QSettings &settings,
    const QString &groupName)
{
	QString prefix;
	if (!groupName.isEmpty()) {
		prefix = groupName + QLatin1String("/");
	}

	{
		QString pwd(settings.value(prefix + CredNames::pwd,
		    QString()).toString());
		aData.setPassword(
		    QString::fromUtf8(QByteArray::fromBase64(pwd.toUtf8())));
	}

	aData.setPwdAlg(settings.value(prefix + CredNames::pwdAlg,
	    QString()).toString());

	aData.setPwdSalt(QByteArray::fromBase64(
	    settings.value(prefix + CredNames::pwdSalt,
	        QString()).toString().toUtf8()));

	aData.setPwdIv(QByteArray::fromBase64(
	    settings.value(prefix + CredNames::pwdIv,
	        QString()).toString().toUtf8()));

	aData.setPwdCode(QByteArray::fromBase64(
	    settings.value(prefix + CredNames::pwdCode,
	        QString()).toString().toUtf8()));

	if (!aData.password().isEmpty() && !aData.pwdCode().isEmpty()) {
		logWarningNL(
		    "Account with username '%s' has both encrypted and unencrypted password set.",
		    aData.userName().toUtf8().constData());
	}
}

/*!
 * @brief Read MEP token data.
 *
 * @param[in,out] aData Account data to store MEP token data into.
 * @param[in]     settings Settings structure.
 * @param[in]     groupName Settings group name.
 */
static
void readMepTokenData(AcntSettings &aData, const QSettings &settings,
    const QString &groupName)
{
	QString prefix;
	if (!groupName.isEmpty()) {
		prefix = groupName + QLatin1String("/");
	}

	{
		QString mepToken(settings.value(prefix + CredNames::mepToken,
		    QString()).toString());
		aData.setMepToken(
		    QString::fromUtf8(QByteArray::fromBase64(mepToken.toUtf8())));
	}
}

void AcntSettings::loadFromSettings(const QString &confDir,
    const QSettings &settings, const QString &group)
{
	QString prefix;
	if (!group.isEmpty()) {
		prefix = group + "/";
	}

	/*
	 * String containing comma character are loaded as a string list.
	 *
	 * FIXME -- Any white-space characters trailing the comma are lost.
	 */
	setAccountName(settings.value(prefix + CredNames::acntName,
	    QString()).toStringList().join(", "));
	setUserName(settings.value(prefix + CredNames::userName,
	    QString()).toString());
	setLoginMethod(methodStrToEnum(
	    settings.value(prefix + CredNames::lMethod, QString()).toString()));
	readPwdData(*this, settings, group);
	readMepTokenData(*this, settings, group);
	setTestAccount(settings.value(prefix + CredNames::testAcnt,
	    QString()).toBool());
	setRememberPwd(settings.value(prefix + CredNames::rememberPwd,
	    QString()).toBool());
	setDbDir(
	    settings.value(prefix + CredNames::dbDir, QString()).toString(),
	    confDir);
	setSyncWithAll(settings.value(prefix + CredNames::syncWithAll,
	    QString()).toBool());
	setP12File(settings.value(prefix + CredNames::p12File,
	    QString()).toString());
}

/*!
 * @brief Stores encrypted password into settings.
 *
 * @param[in]     pinVal PIN value to be used for encryption.
 * @param[in,out] settings Settings structure.
 * @param[in]     aData Account data to be stored.
 * @param[in]     password Password to be stored.
 */
static
bool storeEncryptedPwd(const QString &pinVal, QSettings &settings,
    const AcntSettings &aData, const QString &password)
{
	/* Currently only one cryptographic algorithm is supported. */
	const struct pwd_alg *pwdAlgDesc = &aes256_cbc;

	/* Ignore the algorithm settings. */
	const QString pwdAlg(aes256_cbc.name);
	QByteArray pwdSalt(aData.pwdSalt());
	QByteArray pwdIV(aData.pwdIv());

	if (pwdSalt.size() < pwdAlgDesc->key_len) {
		pwdSalt = randomSalt(pwdAlgDesc->key_len);
	}

	if (pwdIV.size() < pwdAlgDesc->iv_len) {
		pwdIV = randomSalt(pwdAlgDesc->iv_len);
	}

	QByteArray pwdCode = encryptPwd(password, pinVal, pwdAlgDesc->name,
	    pwdSalt, pwdIV);
	if (Q_UNLIKELY(pwdCode.isEmpty())) {
		return false;
	}

	settings.setValue(CredNames::pwdAlg, pwdAlg);
	settings.setValue(CredNames::pwdSalt,
	    QString::fromUtf8(pwdSalt.toBase64()));
	settings.setValue(CredNames::pwdIv,
	    QString::fromUtf8(pwdIV.toBase64()));
	settings.setValue(CredNames::pwdCode,
	    QString::fromUtf8(pwdCode.toBase64()));

	return true;
}

void AcntSettings::saveToSettings(const QString &pinVal, const QString &confDir,
    QSettings &settings, const QString &group) const
{
	if (!group.isEmpty()) {
		settings.beginGroup(group);
	}

	settings.setValue(CredNames::acntName, accountName());
	settings.setValue(CredNames::userName, userName());
	settings.setValue(CredNames::lMethod, methodEnumToStr(loginMethod()));
	settings.setValue(CredNames::testAcnt, isTestAccount());
	settings.setValue(CredNames::rememberPwd, rememberPwd());
	if (rememberPwd() && !password().isEmpty()) {
		bool writePlainPwd = pinVal.isEmpty();
		if (!writePlainPwd) {
			writePlainPwd = !storeEncryptedPwd(pinVal, settings,
			    *this, password());
		}
		if (writePlainPwd) { /* Only when plain or encryption fails. */
			/* Store unencrypted password. */
			settings.setValue(CredNames::pwd,
			    QString::fromUtf8(password().toUtf8().toBase64()));
		}
	}
	if (!mepToken().isEmpty()) {
		/* Store unencrypted MEP token. */
		settings.setValue(CredNames::mepToken,
		    QString::fromUtf8(mepToken().toUtf8().toBase64()));
	}

	if (!dbDir().isEmpty()) {
		if (QDir(dbDir()) != QDir(confDir)) {
			settings.setValue(CredNames::dbDir, dbDir());
		}
	}
	if (!p12File().isEmpty()) {
		settings.setValue(CredNames::p12File, p12File());
	}

	settings.setValue(CredNames::syncWithAll, syncWithAll());

	if (!group.isEmpty()) {
		settings.endGroup();
	}
}

AcntSettings::AcntSettings(AcntSettingsPrivate *d)
    : d_ptr(d)
{
}

bool AcntSettings::ensurePrivate(void)
{
	if (Q_UNLIKELY(d_ptr == Q_NULLPTR)) {
		AcntSettingsPrivate *p = new (::std::nothrow) AcntSettingsPrivate;
		if (Q_UNLIKELY(p == Q_NULLPTR)) {
			Q_ASSERT(0);
			return false;
		}
		d_ptr.reset(p);
	}
	return true;
}

void swap(AcntSettings &first, AcntSettings &second) Q_DECL_NOTHROW
{
	using ::std::swap;
	swap(first.d_ptr, second.d_ptr);
}
