/* This file is part of the KDE project
   SPDX-FileCopyrightText: 2006 Thomas Schaap <thomas.schaap@kdemail.net>
   SPDX-FileCopyrightText: 2010 C. Boemann <cbo@boemann.dk>

   SPDX-License-Identifier: LGPL-2.0-or-later
*/
#ifdef QCA2

// clazy:excludeall=qstring-arg
#include "KoEncryptedStore.h"
#include "KoEncryptionChecker.h"
#include "KoStore_p.h"
#include "KoXmlReader.h"
#include <KoXmlNS.h>

#include <QString>
#include <QByteArray>
#include <QIODevice>
#include <QWidget>
#include <QBuffer>
#include <KPasswordDialog>
#include <KNewPasswordDialog>
#include <KWallet>
#include <KLocalizedString>
#include <KCompressionDevice>
#include <KMessageBox>
#include <KZip>
#include <KoNetAccess.h>
#include <QTemporaryFile>
#include <StoreDebug.h>

#include <QApplication>

struct KoEncryptedStore_EncryptionData {
    // Needed for Key Derivation
    QCA::SecureArray salt;
    unsigned int iterationCount;

    // Needed for enc/decryption
    QCA::SecureArray initVector;

    // Needed for (optional) password-checking
    QCA::SecureArray checksum;
    // checksumShort is set to true if the checksum-algorithm is SHA1/1K, which basically means we only use the first 1024 bytes of the unencrypted file to check against (see also http://www.openoffice.org/servlets/ReadMsg?list=dev&msgNo=17498)
    bool checksumShort;

    // The size of the uncompressed file
    qint64 filesize;
};

// TODO: Discuss naming of this filer in saving-dialogues
// TODO: Discuss possibility of allowing programs to remember the password after opening to enable them to supply it when saving
// TODO: Discuss autosaving and password/leakage-problem (currently: hardcoded no autosave)
namespace
{
const char MANIFEST_FILE[] = "META-INF/manifest.xml";
const char META_FILE[] = "meta.xml";
const char THUMBNAIL_FILE[] = "Thumbnails/thumbnail.png";
}

KoEncryptedStore::KoEncryptedStore(const QString & filename, Mode mode,
                                   const QByteArray & appIdentification, bool writeMimetype)
  : KoStore(mode, writeMimetype)
  , m_filename(filename)
  , m_tempFile(nullptr)
  , m_bPasswordUsed(false)
  , m_bPasswordDeclined(false)
  , m_currentDir(nullptr)
{
    Q_D(KoStore);

    m_pZip = new KZip(filename);
    d->good = true;
    d->localFileName = filename;

    init(appIdentification);
}

KoEncryptedStore::KoEncryptedStore(QIODevice *dev, Mode mode, const QByteArray & appIdentification,
                                   bool writeMimetype)
    : KoStore(mode, writeMimetype)
    , m_tempFile(nullptr)
    , m_bPasswordUsed(false)
    , m_bPasswordDeclined(false)
    , m_currentDir(nullptr)
{
    Q_D(KoStore);

    m_pZip = new KZip(dev);
    d->good = true;

    init(appIdentification);
}

KoEncryptedStore::KoEncryptedStore(QWidget* window, const QUrl &url, const QString & filename,
                                   Mode mode,
                                   const QByteArray & appIdentification, bool writeMimetype)
    : KoStore(mode, writeMimetype)
    , m_filename(url.url())
    , m_tempFile(nullptr)
    , m_bPasswordUsed(false)
    , m_bPasswordDeclined(false)
    , m_currentDir(nullptr)
{
    Q_D(KoStore);

    d->window = window;
    d->good = true;

    if (mode == Read) {
        d->fileMode = KoStorePrivate::RemoteRead;
        d->localFileName = filename;
        m_pZip = new KZip(d->localFileName);
    } else {
        d->fileMode = KoStorePrivate::RemoteWrite;
        m_tempFile = new QTemporaryFile();
        if (!m_tempFile->open()) {
            d->good = false;
        } else {
            d->localFileName = m_tempFile->fileName();
            m_pZip = new KZip(m_tempFile);
        }
    }
    d->url = url;

    init(appIdentification);
}

void KoEncryptedStore::init(const QByteArray & appIdentification)
{
    Q_D(KoStore);
    bool checksumErrorShown = false;
    bool unreadableErrorShown = false;
    if (d->mode == Write) {
        d->good = KoEncryptionChecker::isEncryptionSupported();
        if (d->good) {
            if (!m_pZip->open(QIODevice::WriteOnly)) {
                d->good = false;
                return;
            }
            m_pZip->setExtraField(KZip::NoExtraField);
            // Write identification
            if (d->writeMimetype) {
                m_pZip->setCompression(KZip::NoCompression);
                (void)m_pZip->writeFile(QStringLiteral("mimetype"), appIdentification);
            }
            // FIXME: Hmm, seems to be a bug here since this is
            //        inconsistent with the code in openWrite():
            m_pZip->setCompression(KZip::DeflateCompression);
            // We don't need the extra field in Calligra - so we leave it as "no extra field".
        }
    } else {
        d->good = m_pZip->open(QIODevice::ReadOnly);
        d->good &= m_pZip->directory() != nullptr;
        if (!d->good) {
            return;
        }

        // Read the manifest-file, so we can get the data we'll need to decrypt the other files in the store
        const KArchiveEntry* manifestArchiveEntry = m_pZip->directory()->entry(QLatin1String(MANIFEST_FILE));
        if (!manifestArchiveEntry || !manifestArchiveEntry->isFile()) {
            // No manifest file? OK, *I* won't complain
            return;
        }
        QIODevice *dev = (static_cast< const KArchiveFile* >(manifestArchiveEntry))->createDevice();

        KoXmlDocument xmldoc;
        bool namespaceProcessing = true; // for the manifest ignore the namespace (bug #260515)
        if (!xmldoc.setContent(dev, namespaceProcessing) || xmldoc.documentElement().localName() != QStringLiteral("manifest") || xmldoc.documentElement().namespaceURI() != KoXmlNS::manifest) {
            //setErrorMessage(i18n("The manifest file seems to be corrupted. The document could not be opened."));
            // FIXME this message is not something we actually want to not mention, but it makes thumbnails noisy at times, so... let's not
            dev->close();
            delete dev;
            m_pZip->close();
            d->good = false;
            return;
        }
        KoXmlElement xmlroot = xmldoc.documentElement();
        if (xmlroot.hasChildNodes()) {
            QCA::Base64 base64decoder(QCA::Decode);
            KoXmlNode xmlnode = xmlroot.firstChild();
            while (!xmlnode.isNull()) {
                // Search for files
                if (!xmlnode.isElement() || xmlroot.namespaceURI() != KoXmlNS::manifest || xmlnode.toElement().localName() != QStringLiteral("file-entry") || !xmlnode.toElement().hasAttribute(QStringLiteral("full-path")) || !xmlnode.hasChildNodes()) {
                    xmlnode = xmlnode.nextSibling();
                    continue;
                }

                // Build a structure to hold the data and fill it with defaults
                KoEncryptedStore_EncryptionData encData;
                encData.filesize = 0;
                encData.checksum = QCA::SecureArray();
                encData.checksumShort = false;
                encData.salt = QCA::SecureArray();
                encData.iterationCount = 0;
                encData.initVector = QCA::SecureArray();

                // Get some info about the file
                QString fullpath = xmlnode.toElement().attribute(QStringLiteral("full-path"));

                if (xmlnode.toElement().hasAttribute(QStringLiteral("size"))) {
                    encData.filesize = xmlnode.toElement().attribute(QStringLiteral("size")).toUInt();
                }

                // Find the embedded encryption-data block
                KoXmlNode xmlencnode = xmlnode.firstChild();
                while (!xmlencnode.isNull() && (!xmlencnode.isElement() || xmlencnode.toElement().localName() != QStringLiteral("encryption-data") || !xmlencnode.hasChildNodes())) {
                    xmlencnode = xmlencnode.nextSibling();
                }
                if (xmlencnode.isNull()) {
                    xmlnode = xmlnode.nextSibling();
                    continue;
                }

                // Find some things about the checksum
                if (xmlencnode.toElement().hasAttribute(QStringLiteral("checksum"))) {
                    base64decoder.clear();
                    encData.checksum = base64decoder.decode(QCA::SecureArray(xmlencnode.toElement().attribute(QStringLiteral("checksum")).toLatin1()));
                    if (xmlencnode.toElement().hasAttribute(QStringLiteral("checksum-type"))) {
                        QString checksumType = xmlencnode.toElement().attribute(QStringLiteral("checksum-type"));
                        if (checksumType == QStringLiteral("SHA1")) {
                            encData.checksumShort = false;
                        }
                        // For this particular hash-type: check KoEncryptedStore_encryptionData.checksumShort
                        else if (checksumType == QStringLiteral("SHA1/1K")) {
                            encData.checksumShort = true;
                        } else {
                            // Checksum type unknown
                            if (!checksumErrorShown) {
                                setErrorMessage(i18n("This document contains an unknown checksum. When you give a password it might not be verified."));
                                checksumErrorShown = true;
                            }
                            encData.checksum = QCA::SecureArray();
                        }
                    } else {
                        encData.checksumShort = false;
                    }
                }

                KoXmlNode xmlencattr = xmlencnode.firstChild();
                bool algorithmFound = false;
                bool keyDerivationFound = false;
                // Search all data about encryption
                while (!xmlencattr.isNull()) {
                    if (!xmlencattr.isElement()) {
                        xmlencattr = xmlencattr.nextSibling();
                        continue;
                    }

                    // Find some things about the encryption algorithm
                    if (xmlencattr.toElement().localName() == QStringLiteral("algorithm") && xmlencattr.toElement().hasAttribute(QStringLiteral("initialisation-vector"))) {
                        algorithmFound = true;
                        encData.initVector = base64decoder.decode(QCA::SecureArray(xmlencattr.toElement().attribute(QStringLiteral("initialisation-vector")).toLatin1()));
                        if (xmlencattr.toElement().hasAttribute(QStringLiteral("algorithm-name")) && xmlencattr.toElement().attribute(QStringLiteral("algorithm-name")) != QStringLiteral("Blowfish CFB")) {
                            if (!unreadableErrorShown) {
                                setErrorMessage(i18n("This document contains an unknown encryption method. Some parts may be unreadable."));
                                unreadableErrorShown = true;
                            }
                            encData.initVector = QCA::SecureArray();
                        }
                    }

                    // Find some things about the key derivation
                    if (xmlencattr.toElement().localName() == QStringLiteral("key-derivation") && xmlencattr.toElement().hasAttribute(QStringLiteral("salt"))) {
                        keyDerivationFound = true;
                        encData.salt = base64decoder.decode(QCA::SecureArray(xmlencattr.toElement().attribute(QStringLiteral("salt")).toLatin1()));
                        encData.iterationCount = 1024;
                        if (xmlencattr.toElement().hasAttribute(QStringLiteral("iteration-count"))) {
                            encData.iterationCount = xmlencattr.toElement().attribute(QStringLiteral("iteration-count")).toUInt();
                        }
                        if (xmlencattr.toElement().hasAttribute(QStringLiteral("key-derivation-name")) && xmlencattr.toElement().attribute(QStringLiteral("key-derivation-name")) != QStringLiteral("PBKDF2")) {
                            if (!unreadableErrorShown) {
                                setErrorMessage(i18n("This document contains an unknown encryption method. Some parts may be unreadable."));
                                unreadableErrorShown = true;
                            }
                            encData.salt = QCA::SecureArray();
                        }
                    }

                    xmlencattr = xmlencattr.nextSibling();
                }

                // Only use this encryption data if it makes sense to use it
                if (!(encData.salt.isEmpty() || encData.initVector.isEmpty())) {
                    m_encryptionData.insert(fullpath, encData);
                    if (!(algorithmFound && keyDerivationFound)) {
                        if (!unreadableErrorShown) {
                            setErrorMessage(i18n("This document contains incomplete encryption data. Some parts may be unreadable."));
                            unreadableErrorShown = true;
                        }
                    }
                }

                xmlnode = xmlnode.nextSibling();
            }
        }
        dev->close();
        delete dev;

        if (isEncrypted() && !(QCA::isSupported("sha1") && QCA::isSupported("pbkdf2(sha1)") && QCA::isSupported("blowfish-cfb"))) {
            d->good = false;
            setErrorMessage(i18n("QCA has currently no support for SHA1 or PBKDF2 using SHA1. The document can not be opened."));
        }
    }
}

bool KoEncryptedStore::doFinalize()
{
    Q_D(KoStore);
    if (d->good) {
        if (isOpen()) {
            close();
        }
        if (d->mode == Write) {
            // First change the manifest file and write it
            // We'll use the QDom classes here, since KoXmlReader and KoXmlWriter have no way of copying a complete xml-file
            // other than parsing it completely and rebuilding it.
            // Errorhandling here is done to prevent data from being lost whatever happens
            // TODO: Convert this to KoXML when KoXML is extended enough
            // Note: right now this is impossible due to lack of possibilities to copy an element as-is
            QDomDocument document;
            if (m_manifestBuffer.isEmpty()) {
                // No manifest? Better create one
                document = QDomDocument();
                QDomElement rootElement = document.createElement(QStringLiteral("manifest:manifest"));
                rootElement.setAttribute(QStringLiteral("xmlns:manifest"), QStringLiteral("urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"));
                document.appendChild(rootElement);
            }
            if (!m_manifestBuffer.isEmpty() && !document.setContent(m_manifestBuffer)) {
                // Oi! That's fresh XML we should have here!
                // This is the only case we can't fix
                setErrorMessage(i18n("The manifest file seems to be corrupted. It cannot be modified and the document will remain unreadable. Please try and save the document again to prevent losing your work."));
                m_pZip->close();
                return false;
            }
            QDomElement documentElement = document.documentElement();
            QDomNodeList fileElements = documentElement.elementsByTagName(QStringLiteral("manifest:file-entry"));
            // Search all files in the manifest
            QStringList foundFiles;
            for (int i = 0; i < fileElements.size(); i++) {
                QDomElement fileElement = fileElements.item(i).toElement();
                QString fullpath = fileElement.toElement().attribute(QStringLiteral("manifest:full-path"));
                // See if it's encrypted
                if (fullpath.isEmpty() || !m_encryptionData.contains(fullpath)) {
                    continue;
                }
                foundFiles += fullpath;
                KoEncryptedStore_EncryptionData encData = m_encryptionData.value(fullpath);
                // Set the unencrypted size of the file
                fileElement.setAttribute(QStringLiteral("manifest:size"), encData.filesize);
                // See if the user of this store has already provided (old) encryption data
                QDomNodeList childElements = fileElement.elementsByTagName(QStringLiteral("manifest:encryption-data"));
                QDomElement encryptionElement;
                QDomElement algorithmElement;
                QDomElement keyDerivationElement;
                if (childElements.isEmpty()) {
                    encryptionElement = document.createElement(QStringLiteral("manifest:encryption-data"));
                    fileElement.appendChild(encryptionElement);
                } else {
                    encryptionElement = childElements.item(0).toElement();
                }
                childElements = encryptionElement.elementsByTagName(QStringLiteral("manifest:algorithm"));
                if (childElements.isEmpty()) {
                    algorithmElement = document.createElement(QStringLiteral("manifest:algorithm"));
                    encryptionElement.appendChild(algorithmElement);
                } else {
                    algorithmElement = childElements.item(0).toElement();
                }
                childElements = encryptionElement.elementsByTagName(QStringLiteral("manifest:key-derivation"));
                if (childElements.isEmpty()) {
                    keyDerivationElement = document.createElement(QStringLiteral("manifest:key-derivation"));
                    encryptionElement.appendChild(keyDerivationElement);
                } else {
                    keyDerivationElement = childElements.item(0).toElement();
                }
                // Set the right encryption data
                QCA::Base64 encoder;
                QCA::SecureArray checksum = encoder.encode(encData.checksum);
                if (encData.checksumShort) {
                    encryptionElement.setAttribute(QStringLiteral("manifest:checksum-type"), QStringLiteral("SHA1/1K"));
                } else {
                    encryptionElement.setAttribute(QStringLiteral("manifest:checksum-type"), QStringLiteral("SHA1"));
                }
                encryptionElement.setAttribute(QStringLiteral("manifest:checksum"), QString::fromUtf8(checksum.toByteArray()));
                QCA::SecureArray initVector = encoder.encode(encData.initVector);
                algorithmElement.setAttribute(QStringLiteral("manifest:algorithm-name"), QStringLiteral("Blowfish CFB"));
                algorithmElement.setAttribute(QStringLiteral("manifest:initialisation-vector"), QString::fromUtf8(initVector.toByteArray()));
                QCA::SecureArray salt = encoder.encode(encData.salt);
                keyDerivationElement.setAttribute(QStringLiteral("manifest:key-derivation-name"), QStringLiteral("PBKDF2"));
                keyDerivationElement.setAttribute(QStringLiteral("manifest:iteration-count"), QString::number(encData.iterationCount));
                keyDerivationElement.setAttribute(QStringLiteral("manifest:salt"), QString::fromUtf8(salt.toByteArray()));
            }
            if (foundFiles.size() < m_encryptionData.size()) {
                QList<QString> keys = m_encryptionData.keys();
                for (int i = 0; i < keys.size(); i++) {
                    if (!foundFiles.contains(keys.value(i))) {
                        KoEncryptedStore_EncryptionData encData = m_encryptionData.value(keys.value(i));
                        QDomElement fileElement = document.createElement(QStringLiteral("manifest:file-entry"));
                        fileElement.setAttribute(QStringLiteral("manifest:full-path"), keys.value(i));
                        fileElement.setAttribute(QStringLiteral("manifest:size"), encData.filesize);
                        fileElement.setAttribute(QStringLiteral("manifest:media-type"), QString());
                        documentElement.appendChild(fileElement);
                        QDomElement encryptionElement = document.createElement(QStringLiteral("manifest:encryption-data"));
                        QCA::Base64 encoder;
                        QCA::SecureArray checksum = encoder.encode(encData.checksum);
                        QCA::SecureArray initVector = encoder.encode(encData.initVector);
                        QCA::SecureArray salt = encoder.encode(encData.salt);
                        if (encData.checksumShort) {
                            encryptionElement.setAttribute(QStringLiteral("manifest:checksum-type"), QStringLiteral("SHA1/1K"));
                        } else {
                            encryptionElement.setAttribute(QStringLiteral("manifest:checksum-type"), QStringLiteral("SHA1"));
                        }
                        encryptionElement.setAttribute(QStringLiteral("manifest:checksum"), QString::fromUtf8(checksum.toByteArray()));
                        fileElement.appendChild(encryptionElement);
                        QDomElement algorithmElement = document.createElement(QStringLiteral("manifest:algorithm"));
                        algorithmElement.setAttribute(QStringLiteral("manifest:algorithm-name"), QStringLiteral("Blowfish CFB"));
                        algorithmElement.setAttribute(QStringLiteral("manifest:initialisation-vector"), QString::fromUtf8(initVector.toByteArray()));
                        encryptionElement.appendChild(algorithmElement);
                        QDomElement keyDerivationElement = document.createElement(QStringLiteral("manifest:key-derivation"));
                        keyDerivationElement.setAttribute(QStringLiteral("manifest:key-derivation-name"), QStringLiteral("PBKDF2"));
                        keyDerivationElement.setAttribute(QStringLiteral("manifest:iteration-count"), QString::number(encData.iterationCount));
                        keyDerivationElement.setAttribute(QStringLiteral("manifest:salt"), QString::fromUtf8(salt.toByteArray()));
                        encryptionElement.appendChild(keyDerivationElement);
                    }
                }
            }
            m_manifestBuffer = document.toByteArray();
            m_pZip->setCompression(KZip::DeflateCompression);
            if (!m_pZip->writeFile(QLatin1String(MANIFEST_FILE), m_manifestBuffer)) {
                setErrorMessage(i18n("The manifest file cannot be written. The document will remain unreadable. Please try and save the document again to prevent losing your work."));
                m_pZip->close();
                return false;
            }
        }
    }
    if (m_pZip)
        return m_pZip->close();
    else
        return true;
}

KoEncryptedStore::~KoEncryptedStore()
{
    Q_D(KoStore);
    /* Finalization of an encrypted store must happen earlier than deleting the zip. This rule normally is executed by KoStore, but too late to do any good.*/
    if (!d->finalized) {
        finalize();
    }

    delete m_pZip;

    if (d->fileMode == KoStorePrivate::RemoteWrite) {
        KIO::NetAccess::upload(d->localFileName, d->url, d->window);
        delete m_tempFile;
    } else if (d->fileMode == KoStorePrivate::RemoteRead) {
        KIO::NetAccess::removeTempFile(d->localFileName);
    }

    delete d->stream;
}

bool KoEncryptedStore::isEncrypted()
{
    Q_D(KoStore);
    if (d->mode == Read) {
        return !m_encryptionData.isEmpty();
    }
    return true;
}

QStringList KoEncryptedStore::directoryList() const
{
    QStringList retval;
    const KArchiveDirectory *directory = m_pZip->directory();
    const QStringList entries = directory->entries();
    for (const QString &name : entries) {
        const KArchiveEntry* fileArchiveEntry = m_pZip->directory()->entry(name);
        if (fileArchiveEntry->isDirectory()) {
            retval << name;
        }
    }
    return retval;
}

bool KoEncryptedStore::isToBeEncrypted(const QString& name)
{
    return !(name == QLatin1String(META_FILE) || name == QLatin1String(MANIFEST_FILE) || name == QLatin1String(THUMBNAIL_FILE));
}

bool KoEncryptedStore::openRead(const QString& name)
{
    Q_D(KoStore);
    if (bad())
        return false;

    const KArchiveEntry* fileArchiveEntry = m_pZip->directory()->entry(name);
    if (!fileArchiveEntry) {
        return false;
    }
    if (fileArchiveEntry->isDirectory()) {
        warnStore << name << " is a directory!";
        return false;
    }
    const KZipFileEntry* fileZipEntry = static_cast<const KZipFileEntry*>(fileArchiveEntry);

    delete d->stream;
    d->stream = fileZipEntry->createDevice();
    d->size = fileZipEntry->size();
    if (m_encryptionData.contains(name)) {
        // This file is encrypted, do some decryption first
        if (m_bPasswordDeclined) {
            // The user has already declined to give a password
            // Open the file as empty
            d->stream->close();
            delete d->stream;
            d->stream = new QBuffer();
            d->stream->open(QIODevice::ReadOnly);
            d->size = 0;
            setErrorMessage(KOSTORE_CANCELED_MESSAGE);
            return true;
        }
        QCA::SecureArray encryptedFile(d->stream->readAll());
        if (encryptedFile.size() != d->size) {
            // Read error detected
            d->stream->close();
            delete d->stream;
            d->stream = nullptr;
            warnStore << "read error";
            setErrorMessage(i18n("Read error"));
            return false;
        }
        d->stream->close();
        delete d->stream;
        d->stream = nullptr;
        KoEncryptedStore_EncryptionData encData = m_encryptionData.value(name);
        QCA::SecureArray decrypted;

        // If we don't have a password yet, try and find one
        if (m_password.isEmpty()) {
            findPasswordInKWallet();
        }

        while (true) {
            QCA::SecureArray password;
            bool keepPass = false;
            // I already have a password! Let's test it. If it's not good, we can dump it, anyway.
            if (!m_password.isEmpty()) {
                password = m_password;
                m_password = QCA::SecureArray();
            } else {
                if (!m_filename.isNull())
                    keepPass = false;
                KPasswordDialog dlg(d->window , keepPass ? KPasswordDialog::ShowKeepPassword : KPasswordDialog::NoFlags);
                dlg.setPrompt(xi18nc("@info", "Please enter the password to open this file:<nl/>%1", m_filename));
                QApplication::setOverrideCursor(Qt::ArrowCursor);
                if (! dlg.exec()) {
                    m_bPasswordDeclined = true;
                    QApplication::restoreOverrideCursor();
                    setErrorMessage(KOSTORE_CANCELED_MESSAGE);
                    return false;
                }
                QApplication::restoreOverrideCursor();
                password = QCA::SecureArray(dlg.password().toUtf8());
                if (keepPass)
                    keepPass = dlg.keepPassword();
                if (password.isEmpty()) {
                    continue;
                }
            }

            decrypted = decryptFile(encryptedFile, encData, password);
            if (decrypted.isEmpty()) {
                errorStore << "empty decrypted file" << '\n';
                setErrorMessage(i18n("Empty decrypted file"));
                return false;
            }

            if (!encData.checksum.isEmpty()) {
                QCA::SecureArray checksum;
                if (encData.checksumShort && decrypted.size() > 1024) {
                    // TODO: Eww!!!! I don't want to convert via insecure arrays to get the first 1K characters of a secure array <- fix QCA?
                    checksum = QCA::Hash(QStringLiteral("sha1")).hash(QCA::SecureArray(decrypted.toByteArray().left(1024)));
                } else {
                    checksum = QCA::Hash(QStringLiteral("sha1")).hash(decrypted);
                }
                if (checksum != encData.checksum) {
                    continue;
                }
            }

            // The password passed all possible tests, so let's accept it
            m_password = password;
            m_bPasswordUsed = true;

            if (keepPass) {
                savePasswordInKWallet();
            }

            break;
        }

        QByteArray *resultArray = new QByteArray(decrypted.toByteArray());
        KCompressionDevice::CompressionType type = KCompressionDevice::compressionTypeForMimeType(QStringLiteral("application/x-gzip"));
        KCompressionDevice *resultDevice = new KCompressionDevice(new QBuffer(resultArray, nullptr), false, type);

        if (!resultDevice) {
            delete resultArray;
            return false;
        }
        resultDevice->setSkipHeaders();
        d->stream = resultDevice;
        d->size = encData.filesize;
    }
    if (!d->stream->isOpen()) {
        d->stream->open(QIODevice::ReadOnly);
    }
    return true;
}

bool KoEncryptedStore::closeRead()
{
    Q_D(KoStore);
    delete d->stream;
    d->stream = nullptr;
    return true;
}

void KoEncryptedStore::findPasswordInKWallet()
{
    Q_D(KoStore);
    /* About KWallet access
     *
     * The choice has been made to postfix every entry in a kwallet concerning passwords for opendocument files with /opendocument
     * This choice has been made since, at the time of this writing, the author could not find any reference to standardized
     * naming schemes for entries in the wallet. Since collision of passwords in entries should be avoided and is at least possible,
     * considering remote files might be both protected by a secured web-area (konqueror makes an entry) and a password (we make an
     * entry), it seems a good thing to make sure it won't happen.
     */
    if (!m_filename.isNull() && !KWallet::Wallet::folderDoesNotExist(KWallet::Wallet::LocalWallet(), KWallet::Wallet::PasswordFolder()) && !KWallet::Wallet::keyDoesNotExist(KWallet::Wallet::LocalWallet(), KWallet::Wallet::PasswordFolder(), m_filename + QStringLiteral("/opendocument"))) {
        KWallet::Wallet *wallet = KWallet::Wallet::openWallet(KWallet::Wallet::LocalWallet(), d->window ? d->window->winId() : 0);
        if (wallet) {
            if (wallet->setFolder(KWallet::Wallet::PasswordFolder())) {
                QString pass;
                wallet->readPassword(m_filename + QStringLiteral("/opendocument"), pass);
                m_password = QCA::SecureArray(pass.toUtf8());
            }
            delete wallet;
        }
    }
}

void KoEncryptedStore::savePasswordInKWallet()
{
    Q_D(KoStore);
    KWallet::Wallet *wallet = KWallet::Wallet::openWallet(KWallet::Wallet::LocalWallet(), d->window ? d->window->winId() : 0);
    if (wallet) {
        if (!wallet->hasFolder(KWallet::Wallet::PasswordFolder())) {
            wallet->createFolder(KWallet::Wallet::PasswordFolder());
        }
        if (wallet->setFolder(KWallet::Wallet::PasswordFolder())) {
            if (wallet->hasEntry(m_filename + QStringLiteral("/opendocument"))) {
                wallet->removeEntry(m_filename + QStringLiteral("/opendocument"));
            }
            wallet->writePassword(m_filename + QStringLiteral("/opendocument"), QString::fromUtf8(m_password.toByteArray()));
        }
        delete wallet;
    }
}

QCA::SecureArray KoEncryptedStore::decryptFile(QCA::SecureArray & encryptedFile, KoEncryptedStore_EncryptionData & encData, QCA::SecureArray & password)
{
    QCA::SecureArray keyhash = QCA::Hash(QStringLiteral("sha1")).hash(password);
    QCA::SymmetricKey key = QCA::PBKDF2(QStringLiteral("sha1")).makeKey(keyhash, QCA::InitializationVector(encData.salt), 16, encData.iterationCount);
    QCA::Cipher decrypter(QStringLiteral("blowfish"), QCA::Cipher::CFB, QCA::Cipher::DefaultPadding, QCA::Decode, key, QCA::InitializationVector(encData.initVector));
    QCA::SecureArray result = decrypter.update(encryptedFile);
    result += decrypter.final();
    return result;
}

bool KoEncryptedStore::setPassword(const QString& password)
{
    if (m_bPasswordUsed || password.isEmpty()) {
        return false;
    }
    m_password = QCA::SecureArray(password.toUtf8());
    return true;
}

QString KoEncryptedStore::password()
{
    if (m_password.isEmpty()) {
        return QString();
    }
    return QString::fromUtf8(m_password.toByteArray());
}

bool KoEncryptedStore::passwordDeclined() const
{
    return m_bPasswordDeclined;
}

bool KoEncryptedStore::openWrite(const QString& name)
{
    Q_D(KoStore);
    if (bad())
        return false;
    if (isToBeEncrypted(name)) {
        // Encrypted files will be compressed by this class and should be stored in the zip as not compressed
        m_pZip->setCompression(KZip::NoCompression);
    } else {
        m_pZip->setCompression(KZip::DeflateCompression);
    }
    d->stream = new QBuffer();
    (static_cast< QBuffer* >(d->stream))->open(QIODevice::WriteOnly);
    if (name == QLatin1String(MANIFEST_FILE))
        return true;
    return m_pZip->prepareWriting(name, QString(), QString(), 0);
}

bool KoEncryptedStore::closeWrite()
{
    Q_D(KoStore);
    bool passWasAsked = false;
    if (d->fileName == QLatin1String(MANIFEST_FILE)) {
        m_manifestBuffer = static_cast<QBuffer*>(d->stream)->buffer();
        return true;
    }

    // Find a password
    // Do not accept empty passwords for compatibility with OOo
    if (m_password.isEmpty()) {
        findPasswordInKWallet();
    }
    while (m_password.isEmpty()) {
        KNewPasswordDialog dlg(d->window);
        dlg.setPrompt(i18n("Please enter the password to encrypt the document with."));
        QApplication::setOverrideCursor(Qt::ArrowCursor);
        if (! dlg.exec()) {
            // Without the first password, prevent asking again by deadsimply refusing to continue functioning
            // TODO: This feels rather hackish. There should be a better way to do this.
            delete m_pZip;
            m_pZip = nullptr;
            d->good = false;
            QApplication::restoreOverrideCursor();
            return false;
        }
        QApplication::restoreOverrideCursor();
        m_password = QCA::SecureArray(dlg.password().toUtf8());
        passWasAsked = true;
    }

    // Ask the user to save the password
    if (passWasAsked) {
        QApplication::setOverrideCursor(Qt::ArrowCursor);
        if (KMessageBox::questionTwoActions(d->window,
                                            i18n("Do you want to save the password?"),
                                            i18nc("@title:window", "Save Password"),
                                            KStandardGuiItem::save(),
                                            KStandardGuiItem::dontSave()) == KMessageBox::PrimaryAction) {
            savePasswordInKWallet();
        }
        QApplication::restoreOverrideCursor();
    }

    QByteArray resultData;
    if (d->fileName == QLatin1String(THUMBNAIL_FILE)) {
        // TODO: Replace with a generic 'encrypted'-thumbnail
        resultData = static_cast<QBuffer*>(d->stream)->buffer();
    } else if (!isToBeEncrypted(d->fileName)) {
        resultData = static_cast<QBuffer*>(d->stream)->buffer();
    } else {
        m_bPasswordUsed = true;
        // Build all cryptographic data
        QCA::SecureArray passwordHash = QCA::Hash(QStringLiteral("sha1")).hash(m_password);
        QCA::Random random;
        KoEncryptedStore_EncryptionData encData;
        encData.initVector = random.randomArray(8);
        encData.salt = random.randomArray(16);
        encData.iterationCount = 1024;
        QCA::SymmetricKey key = QCA::PBKDF2(QStringLiteral("sha1")).makeKey(passwordHash, QCA::InitializationVector(encData.salt), 16, encData.iterationCount);
        QCA::Cipher encrypter(QStringLiteral("blowfish"), QCA::Cipher::CFB, QCA::Cipher::DefaultPadding, QCA::Encode, key, QCA::InitializationVector(encData.initVector));

        // Get the written data
        QByteArray data = static_cast<QBuffer*>(d->stream)->buffer();
        encData.filesize = data.size();

        // Compress the data
        QBuffer compressedData;
        KCompressionDevice::CompressionType type = KCompressionDevice::compressionTypeForMimeType(QStringLiteral("application/x-gzip"));
        KCompressionDevice *compressDevice = new KCompressionDevice(&compressedData, false, type);

        if (!compressDevice) {
            return false;
        }
        compressDevice->setSkipHeaders();
        if (!compressDevice->open(QIODevice::WriteOnly)) {
            delete compressDevice;
            return false;
        }
        if (compressDevice->write(data) != data.size()) {
            delete compressDevice;
            return false;
        }
        compressDevice->close();
        delete compressDevice;

        encData.checksum = QCA::Hash(QStringLiteral("sha1")).hash(QCA::SecureArray(compressedData.buffer()));
        encData.checksumShort = false;

        // Encrypt the data
        QCA::SecureArray result = encrypter.update(QCA::SecureArray(compressedData.buffer()));
        result += encrypter.final();
        resultData = result.toByteArray();

        m_encryptionData.insert(d->fileName, encData);
    }

    if (!m_pZip->writeData(resultData.data(), resultData.size())) {
        m_pZip->finishWriting(resultData.size());
        return false;
    }

    return m_pZip->finishWriting(resultData.size());
}

bool KoEncryptedStore::enterRelativeDirectory(const QString& dirName)
{
    Q_D(KoStore);
    if (d->mode == Read) {
        if (!m_currentDir) {
            m_currentDir = m_pZip->directory(); // initialize
        }
        const KArchiveEntry *entry = m_currentDir->entry(dirName);
        if (entry && entry->isDirectory()) {
            m_currentDir = dynamic_cast<const KArchiveDirectory*>(entry);
            return m_currentDir != nullptr;
        }
        return false;
    } else { // Write, no checking here
        return true;
    }
}

bool KoEncryptedStore::enterAbsoluteDirectory(const QString& path)
{
    if (path.isEmpty()) {
        m_currentDir = nullptr;
        return true;
    }
    m_currentDir = dynamic_cast<const KArchiveDirectory*>(m_pZip->directory()->entry(path));
    return m_currentDir != nullptr;
}

bool KoEncryptedStore::fileExists(const QString& absPath) const
{
    const KArchiveEntry *entry = m_pZip->directory()->entry(absPath);
    return (entry && entry->isFile()) || (absPath == QLatin1String(MANIFEST_FILE) && !m_manifestBuffer.isNull());
}

#endif // QCA2
