diff --git a/libs/store/KoEncryptedStore.cpp b/libs/store/KoEncryptedStore.cpp index ab07691f04d..222dad73cdd 100644 --- a/libs/store/KoEncryptedStore.cpp +++ b/libs/store/KoEncryptedStore.cpp @@ -1,851 +1,850 @@ /* This file is part of the KDE project Copyright (C) 2006 Thomas Schaap Copyright (C) 2010 C. Boemann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Library General Public License as published by the Free Software Foundation; either version 2 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 Library General Public License for more details. You should have received a copy of the GNU Library General Public License along with this library; see the file COPYING.LIB. If not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifdef QCA2 #include "KoEncryptedStore.h" #include "KoEncryptionChecker.h" #include "KoStore_p.h" #include "KoXmlReader.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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(0) , m_bPasswordUsed(false) , m_bPasswordDeclined(false) , m_currentDir(0) { 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(0) , m_bPasswordUsed(false) , m_bPasswordDeclined(false) , m_currentDir(0) { 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(0) , m_bPasswordUsed(false) , m_bPasswordDeclined(false) , m_currentDir(0) { 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(QLatin1String("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() != 0; 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(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() != "manifest" || xmldoc.documentElement().namespaceURI() != KoXmlNS::manifest) { //KMessage::message(KMessage::Warning, 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() != "file-entry" || !xmlnode.toElement().hasAttribute("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("full-path"); if (xmlnode.toElement().hasAttribute("size")) { encData.filesize = xmlnode.toElement().attribute("size").toUInt(); } // Find the embedded encryption-data block KoXmlNode xmlencnode = xmlnode.firstChild(); while (!xmlencnode.isNull() && (!xmlencnode.isElement() || xmlencnode.toElement().localName() != "encryption-data" || !xmlencnode.hasChildNodes())) { xmlencnode = xmlencnode.nextSibling(); } if (xmlencnode.isNull()) { xmlnode = xmlnode.nextSibling(); continue; } // Find some things about the checksum if (xmlencnode.toElement().hasAttribute("checksum")) { base64decoder.clear(); encData.checksum = base64decoder.decode(QCA::SecureArray(xmlencnode.toElement().attribute("checksum").toLatin1())); if (xmlencnode.toElement().hasAttribute("checksum-type")) { QString checksumType = xmlencnode.toElement().attribute("checksum-type"); if (checksumType == "SHA1") { encData.checksumShort = false; } // For this particual hash-type: check KoEncryptedStore_encryptionData.checksumShort else if (checksumType == "SHA1/1K") { encData.checksumShort = true; } else { // Checksum type unknown if (!checksumErrorShown) { KMessage::message(KMessage::Warning, 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 encrption while (!xmlencattr.isNull()) { if (!xmlencattr.isElement()) { xmlencattr = xmlencattr.nextSibling(); continue; } // Find some things about the encryption algorithm if (xmlencattr.toElement().localName() == "algorithm" && xmlencattr.toElement().hasAttribute("initialisation-vector")) { algorithmFound = true; encData.initVector = base64decoder.decode(QCA::SecureArray(xmlencattr.toElement().attribute("initialisation-vector").toLatin1())); if (xmlencattr.toElement().hasAttribute("algorithm-name") && xmlencattr.toElement().attribute("algorithm-name") != "Blowfish CFB") { if (!unreadableErrorShown) { KMessage::message(KMessage::Warning, 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() == "key-derivation" && xmlencattr.toElement().hasAttribute("salt")) { keyDerivationFound = true; encData.salt = base64decoder.decode(QCA::SecureArray(xmlencattr.toElement().attribute("salt").toLatin1())); encData.iterationCount = 1024; if (xmlencattr.toElement().hasAttribute("iteration-count")) { encData.iterationCount = xmlencattr.toElement().attribute("iteration-count").toUInt(); } if (xmlencattr.toElement().hasAttribute("key-derivation-name") && xmlencattr.toElement().attribute("key-derivation-name") != "PBKDF2") { if (!unreadableErrorShown) { KMessage::message(KMessage::Warning, 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) { KMessage::message(KMessage::Warning, 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; KMessage::message(KMessage::Error, 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("manifest:manifest"); rootElement.setAttribute("xmlns:manifest", "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 KMessage::message(KMessage::Error, 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("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("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("manifest:size", encData.filesize); // See if the user of this store has already provided (old) encryption data QDomNodeList childElements = fileElement.elementsByTagName("manifest:encryption-data"); QDomElement encryptionElement; QDomElement algorithmElement; QDomElement keyDerivationElement; if (childElements.isEmpty()) { encryptionElement = document.createElement("manifest:encryption-data"); fileElement.appendChild(encryptionElement); } else { encryptionElement = childElements.item(0).toElement(); } childElements = encryptionElement.elementsByTagName("manifest:algorithm"); if (childElements.isEmpty()) { algorithmElement = document.createElement("manifest:algorithm"); encryptionElement.appendChild(algorithmElement); } else { algorithmElement = childElements.item(0).toElement(); } childElements = encryptionElement.elementsByTagName("manifest:key-derivation"); if (childElements.isEmpty()) { keyDerivationElement = document.createElement("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("manifest:checksum-type", "SHA1/1K"); } else { encryptionElement.setAttribute("manifest:checksum-type", "SHA1"); } encryptionElement.setAttribute("manifest:checksum", QString(checksum.toByteArray())); QCA::SecureArray initVector = encoder.encode(encData.initVector); algorithmElement.setAttribute("manifest:algorithm-name", "Blowfish CFB"); algorithmElement.setAttribute("manifest:initialisation-vector", QString(initVector.toByteArray())); QCA::SecureArray salt = encoder.encode(encData.salt); keyDerivationElement.setAttribute("manifest:key-derivation-name", "PBKDF2"); keyDerivationElement.setAttribute("manifest:iteration-count", QString::number(encData.iterationCount)); keyDerivationElement.setAttribute("manifest:salt", QString(salt.toByteArray())); } if (foundFiles.size() < m_encryptionData.size()) { QList 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("manifest:file-entry"); fileElement.setAttribute("manifest:full-path", keys.value(i)); fileElement.setAttribute("manifest:size", encData.filesize); fileElement.setAttribute("manifest:media-type", ""); documentElement.appendChild(fileElement); QDomElement encryptionElement = document.createElement("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("manifest:checksum-type", "SHA1/1K"); } else { encryptionElement.setAttribute("manifest:checksum-type", "SHA1"); } encryptionElement.setAttribute("manifest:checksum", QString(checksum.toByteArray())); fileElement.appendChild(encryptionElement); QDomElement algorithmElement = document.createElement("manifest:algorithm"); algorithmElement.setAttribute("manifest:algorithm-name", "Blowfish CFB"); algorithmElement.setAttribute("manifest:initialisation-vector", QString(initVector.toByteArray())); encryptionElement.appendChild(algorithmElement); QDomElement keyDerivationElement = document.createElement("manifest:key-derivation"); keyDerivationElement.setAttribute("manifest:key-derivation-name", "PBKDF2"); keyDerivationElement.setAttribute("manifest:iteration-count", QString::number(encData.iterationCount)); keyDerivationElement.setAttribute("manifest:salt", QString(salt.toByteArray())); encryptionElement.appendChild(keyDerivationElement); } } } m_manifestBuffer = document.toByteArray(); m_pZip->setCompression(KZip::DeflateCompression); if (!m_pZip->writeFile(QLatin1String(MANIFEST_FILE), m_manifestBuffer)) { KMessage::message(KMessage::Error, 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(); foreach(const QString &name, directory->entries()) { const KArchiveEntry* fileArchiveEntry = m_pZip->directory()->entry(name); if (fileArchiveEntry->isDirectory()) { retval << name; } } return retval; } bool KoEncryptedStore::isToBeEncrypted(const QString& name) { return !(name == META_FILE || name == MANIFEST_FILE || name == 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(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; 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"; 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) { - QByteArray pass; 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 : static_cast(0)); dlg.setPrompt(i18n("Please enter the password to open this file.")); if (! dlg.exec()) { m_bPasswordDeclined = true; d->stream = new QBuffer(); d->stream->open(QIODevice::ReadOnly); d->size = 0; return true; } 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" << endl; 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("sha1").hash(QCA::SecureArray(decrypted.toByteArray().left(1024))); } else { checksum = QCA::Hash("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 = KFilterDev::compressionTypeForMimeType("application/x-gzip"); QIODevice *resultDevice = new KCompressionDevice(new QBuffer(resultArray, nullptr), false, type); if (!resultDevice) { delete resultArray; return false; } static_cast(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 + "/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 + "/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 + "/opendocument")) { wallet->removeEntry(m_filename + "/opendocument"); } - wallet->writePassword(m_filename + "/opendocument", m_password.toByteArray().data()); + wallet->writePassword(m_filename + "/opendocument", m_password.toByteArray().constData()); } delete wallet; } } QCA::SecureArray KoEncryptedStore::decryptFile(QCA::SecureArray & encryptedFile, KoEncryptedStore_EncryptionData & encData, QCA::SecureArray & password) { QCA::SecureArray keyhash = QCA::Hash("sha1").hash(password); QCA::SymmetricKey key = QCA::PBKDF2("sha1").makeKey(keyhash, QCA::InitializationVector(encData.salt), 16, encData.iterationCount); QCA::Cipher decrypter("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(m_password.toByteArray()); } 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 == MANIFEST_FILE) return true; return m_pZip->prepareWriting(name, "", "", 0); } bool KoEncryptedStore::closeWrite() { Q_D(KoStore); bool passWasAsked = false; if (d->fileName == MANIFEST_FILE) { m_manifestBuffer = static_cast(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.")); 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 = 0; d->good = false; return false; } m_password = QCA::SecureArray(dlg.password().toUtf8()); passWasAsked = true; } // Ask the user to save the password if (passWasAsked && KMessageBox::questionYesNo(d->window, i18n("Do you want to save the password?")) == KMessageBox::Yes) { savePasswordInKWallet(); } QByteArray resultData; if (d->fileName == THUMBNAIL_FILE) { // TODO: Replace with a generic 'encrypted'-thumbnail resultData = static_cast(d->stream)->buffer(); } else if (!isToBeEncrypted(d->fileName)) { resultData = static_cast(d->stream)->buffer(); } else { m_bPasswordUsed = true; // Build all cryptographic data QCA::SecureArray passwordHash = QCA::Hash("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("sha1").makeKey(passwordHash, QCA::InitializationVector(encData.salt), 16, encData.iterationCount); QCA::Cipher encrypter("blowfish", QCA::Cipher::CFB, QCA::Cipher::DefaultPadding, QCA::Encode, key, QCA::InitializationVector(encData.initVector)); // Get the written data QByteArray data = static_cast(d->stream)->buffer(); encData.filesize = data.size(); // Compress the data QBuffer compressedData; KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType("application/x-gzip"); QIODevice *compressDevice = new KCompressionDevice(&compressedData, false, type); if (!compressDevice) { return false; } static_cast(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("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(entry); return m_currentDir != 0; } return false; } else { // Write, no checking here return true; } } bool KoEncryptedStore::enterAbsoluteDirectory(const QString& path) { if (path.isEmpty()) { m_currentDir = 0; return true; } m_currentDir = dynamic_cast(m_pZip->directory()->entry(path)); return m_currentDir != 0; } bool KoEncryptedStore::fileExists(const QString& absPath) const { const KArchiveEntry *entry = m_pZip->directory()->entry(absPath); return (entry && entry->isFile()) || (absPath == MANIFEST_FILE && !m_manifestBuffer.isNull()); } #endif // QCA2 diff --git a/libs/store/tests/storedroptest.cpp b/libs/store/tests/storedroptest.cpp index 88f73c3e84e..e7a33fb4b07 100644 --- a/libs/store/tests/storedroptest.cpp +++ b/libs/store/tests/storedroptest.cpp @@ -1,170 +1,172 @@ /* This file is part of the KDE project * Copyright (C) 2004 David Faure * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License as published by the Free Software Foundation; either * version 2 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 * Library General Public License for more details. * * You should have received a copy of the GNU Library General Public License * along with this library; see the file COPYING.LIB. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. */ #include #include #include #include #include #include #include #include #include #include #include #include class StoreDropTest : public QTextBrowser { +Q_OBJECT public: StoreDropTest(QWidget* parent); protected: virtual void contentsDragEnterEvent(QDragEnterEvent * e); virtual void contentsDragMoveEvent(QDragMoveEvent * e); virtual void contentsDropEvent(QDropEvent * e); virtual void keyPressEvent(QKeyEvent * e); virtual void paste(); private: bool processMimeData(const QMimeData* mimeData); void showZipContents(QByteArray data, const QString &mimeType, bool oasis); QString loadTextFile(KoStore* store, const QString& fileName); }; int main(int argc, char** argv) { QApplication app(argc, argv); StoreDropTest* window = new StoreDropTest(0); window->resize(500, 500); window->show(); QObject::connect(qApp, SIGNAL(lastWindowClosed()), qApp, SLOT(quit())); return app.exec(); } StoreDropTest::StoreDropTest(QWidget* parent) : QTextBrowser(parent) { setText("KoStore drop/paste test\nDrop or paste a selection from a Calligra application into this widget to see the ZIP contents"); setAcceptDrops(true); } void StoreDropTest::contentsDragEnterEvent(QDragEnterEvent * ev) { ev->acceptProposedAction(); } void StoreDropTest::contentsDragMoveEvent(QDragMoveEvent * ev) { ev->acceptProposedAction(); } void StoreDropTest::keyPressEvent(QKeyEvent * e) { if (((e->modifiers() & Qt::ShiftModifier) && e->key() == Qt::Key_Insert) || ((e->modifiers() & Qt::ControlModifier) && e->key() == Qt::Key_V)) paste(); //else // QTextBrowser::keyPressEvent( e ); } void StoreDropTest::paste() { qDebug("paste"); const QMimeData* m = QApplication::clipboard()->mimeData(); if (!m) return; const QString acceptMimeType("application/vnd.oasis.opendocument."); QStringList formats = m->formats(); foreach(QString fmt, formats) { bool oasis = fmt.startsWith(acceptMimeType); if (oasis || fmt == "application/x-kpresenter") { QByteArray data = m->data(fmt); showZipContents(data, fmt.toLatin1(), oasis); return; } } setText("No acceptable format found. All I got was:\n" + formats.join("\n")); } void StoreDropTest::contentsDropEvent(QDropEvent *ev) { if (processMimeData(ev->mimeData())) ev->acceptProposedAction(); else ev->ignore(); } bool StoreDropTest::processMimeData(const QMimeData* mimeData) { const QString acceptMimeType("application/vnd.oasis.opendocument."); foreach (const QString &format, mimeData->formats()) { bool oasis = format.startsWith(acceptMimeType); if (oasis || format == "application/x-kpresenter") { const QByteArray data = mimeData->data(format); showZipContents(data, format, oasis); return true; } } setText("No acceptable format found. All I got was:\n" + mimeData->formats().join("\n")); return false; } void StoreDropTest::showZipContents(QByteArray data, const QString &mimeType, bool oasis) { if (data.isEmpty()) { setText("No data!"); return; } QBuffer buffer(&data); KoStore * store = KoStore::createStore(&buffer, KoStore::Read); if (store->bad()) { setText("Invalid ZIP!"); delete store; return; } QString txt = QString("Valid ZIP file found for format ") + mimeType + "\n"; if (oasis) { txt += loadTextFile(store, "content.xml"); txt += loadTextFile(store, "styles.xml"); txt += loadTextFile(store, "settings.xml"); txt += loadTextFile(store, "META-INF/manifest.xml"); } else { txt += loadTextFile(store, "maindoc.xml"); } setText(txt); delete store; } QString StoreDropTest::loadTextFile(KoStore* store, const QString& fileName) { if (!store->open(fileName)) return QString("%1 not found\n").arg(fileName); QByteArray data = store->device()->readAll(); store->close(); QString txt = QString("Found %1: \n").arg(fileName); txt += QString::fromUtf8(data.data(), data.size()); txt += "\n"; return txt; } +#include "storedroptest.moc"