Warning, file /office/calligra/libs/store/KoEncryptedStore.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).
0001 /* This file is part of the KDE project 0002 Copyright (C) 2006 Thomas Schaap <thomas.schaap@kdemail.net> 0003 Copyright (C) 2010 C. Boemann <cbo@boemann.dk> 0004 0005 This library is free software; you can redistribute it and/or 0006 modify it under the terms of the GNU Library General Public 0007 License as published by the Free Software Foundation; either 0008 version 2 of the License, or (at your option) any later version. 0009 0010 This library is distributed in the hope that it will be useful, 0011 but WITHOUT ANY WARRANTY; without even the implied warranty of 0012 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 0013 Library General Public License for more details. 0014 0015 You should have received a copy of the GNU Library General Public License 0016 along with this library; see the file COPYING.LIB. If not, write to 0017 the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, 0018 Boston, MA 02110-1301, USA. 0019 */ 0020 #ifdef QCA2 0021 0022 #include "KoEncryptedStore.h" 0023 #include "KoEncryptionChecker.h" 0024 #include "KoStore_p.h" 0025 #include "KoXmlReader.h" 0026 #include <KoXmlNS.h> 0027 0028 #include <QString> 0029 #include <QByteArray> 0030 #include <QIODevice> 0031 #include <QWidget> 0032 #include <QBuffer> 0033 #include <kpassworddialog.h> 0034 #include <knewpassworddialog.h> 0035 #include <kwallet.h> 0036 #include <klocalizedstring.h> 0037 #include <kfilterdev.h> 0038 #include <kmessage.h> 0039 #include <kmessagebox.h> 0040 #include <kzip.h> 0041 #include <KoNetAccess.h> 0042 #include <QTemporaryFile> 0043 #include <StoreDebug.h> 0044 0045 struct KoEncryptedStore_EncryptionData { 0046 // Needed for Key Derivation 0047 QCA::SecureArray salt; 0048 unsigned int iterationCount; 0049 0050 // Needed for enc/decryption 0051 QCA::SecureArray initVector; 0052 0053 // Needed for (optional) password-checking 0054 QCA::SecureArray checksum; 0055 // 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) 0056 bool checksumShort; 0057 0058 // The size of the uncompressed file 0059 qint64 filesize; 0060 }; 0061 0062 // TODO: Discuss naming of this filer in saving-dialogues 0063 // TODO: Discuss possibility of allowing programs to remember the password after opening to enable them to supply it when saving 0064 // TODO: Discuss autosaving and password/leakage-problem (currently: hardcoded no autosave) 0065 namespace 0066 { 0067 const char MANIFEST_FILE[] = "META-INF/manifest.xml"; 0068 const char META_FILE[] = "meta.xml"; 0069 const char THUMBNAIL_FILE[] = "Thumbnails/thumbnail.png"; 0070 } 0071 0072 KoEncryptedStore::KoEncryptedStore(const QString & filename, Mode mode, 0073 const QByteArray & appIdentification, bool writeMimetype) 0074 : KoStore(mode, writeMimetype) 0075 , m_filename(filename) 0076 , m_tempFile(0) 0077 , m_bPasswordUsed(false) 0078 , m_bPasswordDeclined(false) 0079 , m_currentDir(0) 0080 { 0081 Q_D(KoStore); 0082 0083 m_pZip = new KZip(filename); 0084 d->good = true; 0085 d->localFileName = filename; 0086 0087 init(appIdentification); 0088 } 0089 0090 KoEncryptedStore::KoEncryptedStore(QIODevice *dev, Mode mode, const QByteArray & appIdentification, 0091 bool writeMimetype) 0092 : KoStore(mode, writeMimetype) 0093 , m_tempFile(0) 0094 , m_bPasswordUsed(false) 0095 , m_bPasswordDeclined(false) 0096 , m_currentDir(0) 0097 { 0098 Q_D(KoStore); 0099 0100 m_pZip = new KZip(dev); 0101 d->good = true; 0102 0103 init(appIdentification); 0104 } 0105 0106 KoEncryptedStore::KoEncryptedStore(QWidget* window, const QUrl &url, const QString & filename, 0107 Mode mode, 0108 const QByteArray & appIdentification, bool writeMimetype) 0109 : KoStore(mode, writeMimetype) 0110 , m_filename(url.url()) 0111 , m_tempFile(0) 0112 , m_bPasswordUsed(false) 0113 , m_bPasswordDeclined(false) 0114 , m_currentDir(0) 0115 { 0116 Q_D(KoStore); 0117 0118 d->window = window; 0119 d->good = true; 0120 0121 if (mode == Read) { 0122 d->fileMode = KoStorePrivate::RemoteRead; 0123 d->localFileName = filename; 0124 m_pZip = new KZip(d->localFileName); 0125 } else { 0126 d->fileMode = KoStorePrivate::RemoteWrite; 0127 m_tempFile = new QTemporaryFile(); 0128 if (!m_tempFile->open()) { 0129 d->good = false; 0130 } else { 0131 d->localFileName = m_tempFile->fileName(); 0132 m_pZip = new KZip(m_tempFile); 0133 } 0134 } 0135 d->url = url; 0136 0137 init(appIdentification); 0138 } 0139 0140 void KoEncryptedStore::init(const QByteArray & appIdentification) 0141 { 0142 Q_D(KoStore); 0143 bool checksumErrorShown = false; 0144 bool unreadableErrorShown = false; 0145 if (d->mode == Write) { 0146 d->good = KoEncryptionChecker::isEncryptionSupported(); 0147 if (d->good) { 0148 if (!m_pZip->open(QIODevice::WriteOnly)) { 0149 d->good = false; 0150 return; 0151 } 0152 m_pZip->setExtraField(KZip::NoExtraField); 0153 // Write identification 0154 if (d->writeMimetype) { 0155 m_pZip->setCompression(KZip::NoCompression); 0156 (void)m_pZip->writeFile(QLatin1String("mimetype"), appIdentification); 0157 } 0158 // FIXME: Hmm, seems to be a bug here since this is 0159 // inconsistent with the code in openWrite(): 0160 m_pZip->setCompression(KZip::DeflateCompression); 0161 // We don't need the extra field in Calligra - so we leave it as "no extra field". 0162 } 0163 } else { 0164 d->good = m_pZip->open(QIODevice::ReadOnly); 0165 d->good &= m_pZip->directory() != 0; 0166 if (!d->good) { 0167 return; 0168 } 0169 0170 // Read the manifest-file, so we can get the data we'll need to decrypt the other files in the store 0171 const KArchiveEntry* manifestArchiveEntry = m_pZip->directory()->entry(MANIFEST_FILE); 0172 if (!manifestArchiveEntry || !manifestArchiveEntry->isFile()) { 0173 // No manifest file? OK, *I* won't complain 0174 return; 0175 } 0176 QIODevice *dev = (static_cast< const KArchiveFile* >(manifestArchiveEntry))->createDevice(); 0177 0178 KoXmlDocument xmldoc; 0179 bool namespaceProcessing = true; // for the manifest ignore the namespace (bug #260515) 0180 if (!xmldoc.setContent(dev, namespaceProcessing) || xmldoc.documentElement().localName() != "manifest" || xmldoc.documentElement().namespaceURI() != KoXmlNS::manifest) { 0181 //KMessage::message(KMessage::Warning, i18n("The manifest file seems to be corrupted. The document could not be opened.")); 0182 /// FIXME this message is not something we actually want to not mention, but it makes thumbnails noisy at times, so... let's not 0183 dev->close(); 0184 delete dev; 0185 m_pZip->close(); 0186 d->good = false; 0187 return; 0188 } 0189 KoXmlElement xmlroot = xmldoc.documentElement(); 0190 if (xmlroot.hasChildNodes()) { 0191 QCA::Base64 base64decoder(QCA::Decode); 0192 KoXmlNode xmlnode = xmlroot.firstChild(); 0193 while (!xmlnode.isNull()) { 0194 // Search for files 0195 if (!xmlnode.isElement() || xmlroot.namespaceURI() != KoXmlNS::manifest || xmlnode.toElement().localName() != "file-entry" || !xmlnode.toElement().hasAttribute("full-path") || !xmlnode.hasChildNodes()) { 0196 xmlnode = xmlnode.nextSibling(); 0197 continue; 0198 } 0199 0200 // Build a structure to hold the data and fill it with defaults 0201 KoEncryptedStore_EncryptionData encData; 0202 encData.filesize = 0; 0203 encData.checksum = QCA::SecureArray(); 0204 encData.checksumShort = false; 0205 encData.salt = QCA::SecureArray(); 0206 encData.iterationCount = 0; 0207 encData.initVector = QCA::SecureArray(); 0208 0209 // Get some info about the file 0210 QString fullpath = xmlnode.toElement().attribute("full-path"); 0211 0212 if (xmlnode.toElement().hasAttribute("size")) { 0213 encData.filesize = xmlnode.toElement().attribute("size").toUInt(); 0214 } 0215 0216 // Find the embedded encryption-data block 0217 KoXmlNode xmlencnode = xmlnode.firstChild(); 0218 while (!xmlencnode.isNull() && (!xmlencnode.isElement() || xmlencnode.toElement().localName() != "encryption-data" || !xmlencnode.hasChildNodes())) { 0219 xmlencnode = xmlencnode.nextSibling(); 0220 } 0221 if (xmlencnode.isNull()) { 0222 xmlnode = xmlnode.nextSibling(); 0223 continue; 0224 } 0225 0226 // Find some things about the checksum 0227 if (xmlencnode.toElement().hasAttribute("checksum")) { 0228 base64decoder.clear(); 0229 encData.checksum = base64decoder.decode(QCA::SecureArray(xmlencnode.toElement().attribute("checksum").toLatin1())); 0230 if (xmlencnode.toElement().hasAttribute("checksum-type")) { 0231 QString checksumType = xmlencnode.toElement().attribute("checksum-type"); 0232 if (checksumType == "SHA1") { 0233 encData.checksumShort = false; 0234 } 0235 // For this particual hash-type: check KoEncryptedStore_encryptionData.checksumShort 0236 else if (checksumType == "SHA1/1K") { 0237 encData.checksumShort = true; 0238 } else { 0239 // Checksum type unknown 0240 if (!checksumErrorShown) { 0241 KMessage::message(KMessage::Warning, i18n("This document contains an unknown checksum. When you give a password it might not be verified.")); 0242 checksumErrorShown = true; 0243 } 0244 encData.checksum = QCA::SecureArray(); 0245 } 0246 } else { 0247 encData.checksumShort = false; 0248 } 0249 } 0250 0251 KoXmlNode xmlencattr = xmlencnode.firstChild(); 0252 bool algorithmFound = false; 0253 bool keyDerivationFound = false; 0254 // Search all data about encryption 0255 while (!xmlencattr.isNull()) { 0256 if (!xmlencattr.isElement()) { 0257 xmlencattr = xmlencattr.nextSibling(); 0258 continue; 0259 } 0260 0261 // Find some things about the encryption algorithm 0262 if (xmlencattr.toElement().localName() == "algorithm" && xmlencattr.toElement().hasAttribute("initialisation-vector")) { 0263 algorithmFound = true; 0264 encData.initVector = base64decoder.decode(QCA::SecureArray(xmlencattr.toElement().attribute("initialisation-vector").toLatin1())); 0265 if (xmlencattr.toElement().hasAttribute("algorithm-name") && xmlencattr.toElement().attribute("algorithm-name") != "Blowfish CFB") { 0266 if (!unreadableErrorShown) { 0267 KMessage::message(KMessage::Warning, i18n("This document contains an unknown encryption method. Some parts may be unreadable.")); 0268 unreadableErrorShown = true; 0269 } 0270 encData.initVector = QCA::SecureArray(); 0271 } 0272 } 0273 0274 // Find some things about the key derivation 0275 if (xmlencattr.toElement().localName() == "key-derivation" && xmlencattr.toElement().hasAttribute("salt")) { 0276 keyDerivationFound = true; 0277 encData.salt = base64decoder.decode(QCA::SecureArray(xmlencattr.toElement().attribute("salt").toLatin1())); 0278 encData.iterationCount = 1024; 0279 if (xmlencattr.toElement().hasAttribute("iteration-count")) { 0280 encData.iterationCount = xmlencattr.toElement().attribute("iteration-count").toUInt(); 0281 } 0282 if (xmlencattr.toElement().hasAttribute("key-derivation-name") && xmlencattr.toElement().attribute("key-derivation-name") != "PBKDF2") { 0283 if (!unreadableErrorShown) { 0284 KMessage::message(KMessage::Warning, i18n("This document contains an unknown encryption method. Some parts may be unreadable.")); 0285 unreadableErrorShown = true; 0286 } 0287 encData.salt = QCA::SecureArray(); 0288 } 0289 } 0290 0291 xmlencattr = xmlencattr.nextSibling(); 0292 } 0293 0294 // Only use this encryption data if it makes sense to use it 0295 if (!(encData.salt.isEmpty() || encData.initVector.isEmpty())) { 0296 m_encryptionData.insert(fullpath, encData); 0297 if (!(algorithmFound && keyDerivationFound)) { 0298 if (!unreadableErrorShown) { 0299 KMessage::message(KMessage::Warning, i18n("This document contains incomplete encryption data. Some parts may be unreadable.")); 0300 unreadableErrorShown = true; 0301 } 0302 } 0303 } 0304 0305 xmlnode = xmlnode.nextSibling(); 0306 } 0307 } 0308 dev->close(); 0309 delete dev; 0310 0311 if (isEncrypted() && !(QCA::isSupported("sha1") && QCA::isSupported("pbkdf2(sha1)") && QCA::isSupported("blowfish-cfb"))) { 0312 d->good = false; 0313 KMessage::message(KMessage::Error, i18n("QCA has currently no support for SHA1 or PBKDF2 using SHA1. The document can not be opened.")); 0314 } 0315 } 0316 } 0317 0318 bool KoEncryptedStore::doFinalize() 0319 { 0320 Q_D(KoStore); 0321 if (d->good) { 0322 if (isOpen()) { 0323 close(); 0324 } 0325 if (d->mode == Write) { 0326 // First change the manifest file and write it 0327 // We'll use the QDom classes here, since KoXmlReader and KoXmlWriter have no way of copying a complete xml-file 0328 // other than parsing it completely and rebuilding it. 0329 // Errorhandling here is done to prevent data from being lost whatever happens 0330 // TODO: Convert this to KoXML when KoXML is extended enough 0331 // Note: right now this is impossible due to lack of possibilities to copy an element as-is 0332 QDomDocument document; 0333 if (m_manifestBuffer.isEmpty()) { 0334 // No manifest? Better create one 0335 document = QDomDocument(); 0336 QDomElement rootElement = document.createElement("manifest:manifest"); 0337 rootElement.setAttribute("xmlns:manifest", "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0"); 0338 document.appendChild(rootElement); 0339 } 0340 if (!m_manifestBuffer.isEmpty() && !document.setContent(m_manifestBuffer)) { 0341 // Oi! That's fresh XML we should have here! 0342 // This is the only case we can't fix 0343 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.")); 0344 m_pZip->close(); 0345 return false; 0346 } 0347 QDomElement documentElement = document.documentElement(); 0348 QDomNodeList fileElements = documentElement.elementsByTagName("manifest:file-entry"); 0349 // Search all files in the manifest 0350 QStringList foundFiles; 0351 for (int i = 0; i < fileElements.size(); i++) { 0352 QDomElement fileElement = fileElements.item(i).toElement(); 0353 QString fullpath = fileElement.toElement().attribute("manifest:full-path"); 0354 // See if it's encrypted 0355 if (fullpath.isEmpty() || !m_encryptionData.contains(fullpath)) { 0356 continue; 0357 } 0358 foundFiles += fullpath; 0359 KoEncryptedStore_EncryptionData encData = m_encryptionData.value(fullpath); 0360 // Set the unencrypted size of the file 0361 fileElement.setAttribute("manifest:size", encData.filesize); 0362 // See if the user of this store has already provided (old) encryption data 0363 QDomNodeList childElements = fileElement.elementsByTagName("manifest:encryption-data"); 0364 QDomElement encryptionElement; 0365 QDomElement algorithmElement; 0366 QDomElement keyDerivationElement; 0367 if (childElements.isEmpty()) { 0368 encryptionElement = document.createElement("manifest:encryption-data"); 0369 fileElement.appendChild(encryptionElement); 0370 } else { 0371 encryptionElement = childElements.item(0).toElement(); 0372 } 0373 childElements = encryptionElement.elementsByTagName("manifest:algorithm"); 0374 if (childElements.isEmpty()) { 0375 algorithmElement = document.createElement("manifest:algorithm"); 0376 encryptionElement.appendChild(algorithmElement); 0377 } else { 0378 algorithmElement = childElements.item(0).toElement(); 0379 } 0380 childElements = encryptionElement.elementsByTagName("manifest:key-derivation"); 0381 if (childElements.isEmpty()) { 0382 keyDerivationElement = document.createElement("manifest:key-derivation"); 0383 encryptionElement.appendChild(keyDerivationElement); 0384 } else { 0385 keyDerivationElement = childElements.item(0).toElement(); 0386 } 0387 // Set the right encryption data 0388 QCA::Base64 encoder; 0389 QCA::SecureArray checksum = encoder.encode(encData.checksum); 0390 if (encData.checksumShort) { 0391 encryptionElement.setAttribute("manifest:checksum-type", "SHA1/1K"); 0392 } else { 0393 encryptionElement.setAttribute("manifest:checksum-type", "SHA1"); 0394 } 0395 encryptionElement.setAttribute("manifest:checksum", QString(checksum.toByteArray())); 0396 QCA::SecureArray initVector = encoder.encode(encData.initVector); 0397 algorithmElement.setAttribute("manifest:algorithm-name", "Blowfish CFB"); 0398 algorithmElement.setAttribute("manifest:initialisation-vector", QString(initVector.toByteArray())); 0399 QCA::SecureArray salt = encoder.encode(encData.salt); 0400 keyDerivationElement.setAttribute("manifest:key-derivation-name", "PBKDF2"); 0401 keyDerivationElement.setAttribute("manifest:iteration-count", QString::number(encData.iterationCount)); 0402 keyDerivationElement.setAttribute("manifest:salt", QString(salt.toByteArray())); 0403 } 0404 if (foundFiles.size() < m_encryptionData.size()) { 0405 QList<QString> keys = m_encryptionData.keys(); 0406 for (int i = 0; i < keys.size(); i++) { 0407 if (!foundFiles.contains(keys.value(i))) { 0408 KoEncryptedStore_EncryptionData encData = m_encryptionData.value(keys.value(i)); 0409 QDomElement fileElement = document.createElement("manifest:file-entry"); 0410 fileElement.setAttribute("manifest:full-path", keys.value(i)); 0411 fileElement.setAttribute("manifest:size", encData.filesize); 0412 fileElement.setAttribute("manifest:media-type", ""); 0413 documentElement.appendChild(fileElement); 0414 QDomElement encryptionElement = document.createElement("manifest:encryption-data"); 0415 QCA::Base64 encoder; 0416 QCA::SecureArray checksum = encoder.encode(encData.checksum); 0417 QCA::SecureArray initVector = encoder.encode(encData.initVector); 0418 QCA::SecureArray salt = encoder.encode(encData.salt); 0419 if (encData.checksumShort) { 0420 encryptionElement.setAttribute("manifest:checksum-type", "SHA1/1K"); 0421 } else { 0422 encryptionElement.setAttribute("manifest:checksum-type", "SHA1"); 0423 } 0424 encryptionElement.setAttribute("manifest:checksum", QString(checksum.toByteArray())); 0425 fileElement.appendChild(encryptionElement); 0426 QDomElement algorithmElement = document.createElement("manifest:algorithm"); 0427 algorithmElement.setAttribute("manifest:algorithm-name", "Blowfish CFB"); 0428 algorithmElement.setAttribute("manifest:initialisation-vector", QString(initVector.toByteArray())); 0429 encryptionElement.appendChild(algorithmElement); 0430 QDomElement keyDerivationElement = document.createElement("manifest:key-derivation"); 0431 keyDerivationElement.setAttribute("manifest:key-derivation-name", "PBKDF2"); 0432 keyDerivationElement.setAttribute("manifest:iteration-count", QString::number(encData.iterationCount)); 0433 keyDerivationElement.setAttribute("manifest:salt", QString(salt.toByteArray())); 0434 encryptionElement.appendChild(keyDerivationElement); 0435 } 0436 } 0437 } 0438 m_manifestBuffer = document.toByteArray(); 0439 m_pZip->setCompression(KZip::DeflateCompression); 0440 if (!m_pZip->writeFile(QLatin1String(MANIFEST_FILE), m_manifestBuffer)) { 0441 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.")); 0442 m_pZip->close(); 0443 return false; 0444 } 0445 } 0446 } 0447 if (m_pZip) 0448 return m_pZip->close(); 0449 else 0450 return true; 0451 } 0452 0453 KoEncryptedStore::~KoEncryptedStore() 0454 { 0455 Q_D(KoStore); 0456 /* 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.*/ 0457 if (!d->finalized) { 0458 finalize(); 0459 } 0460 0461 delete m_pZip; 0462 0463 if (d->fileMode == KoStorePrivate::RemoteWrite) { 0464 KIO::NetAccess::upload(d->localFileName, d->url, d->window); 0465 delete m_tempFile; 0466 } else if (d->fileMode == KoStorePrivate::RemoteRead) { 0467 KIO::NetAccess::removeTempFile(d->localFileName); 0468 } 0469 0470 delete d->stream; 0471 } 0472 0473 bool KoEncryptedStore::isEncrypted() 0474 { 0475 Q_D(KoStore); 0476 if (d->mode == Read) { 0477 return !m_encryptionData.isEmpty(); 0478 } 0479 return true; 0480 } 0481 0482 QStringList KoEncryptedStore::directoryList() const 0483 { 0484 QStringList retval; 0485 const KArchiveDirectory *directory = m_pZip->directory(); 0486 foreach(const QString &name, directory->entries()) { 0487 const KArchiveEntry* fileArchiveEntry = m_pZip->directory()->entry(name); 0488 if (fileArchiveEntry->isDirectory()) { 0489 retval << name; 0490 } 0491 } 0492 return retval; 0493 } 0494 0495 bool KoEncryptedStore::isToBeEncrypted(const QString& name) 0496 { 0497 return !(name == META_FILE || name == MANIFEST_FILE || name == THUMBNAIL_FILE); 0498 } 0499 0500 bool KoEncryptedStore::openRead(const QString& name) 0501 { 0502 Q_D(KoStore); 0503 if (bad()) 0504 return false; 0505 0506 const KArchiveEntry* fileArchiveEntry = m_pZip->directory()->entry(name); 0507 if (!fileArchiveEntry) { 0508 return false; 0509 } 0510 if (fileArchiveEntry->isDirectory()) { 0511 warnStore << name << " is a directory!"; 0512 return false; 0513 } 0514 const KZipFileEntry* fileZipEntry = static_cast<const KZipFileEntry*>(fileArchiveEntry); 0515 0516 delete d->stream; 0517 d->stream = fileZipEntry->createDevice(); 0518 d->size = fileZipEntry->size(); 0519 if (m_encryptionData.contains(name)) { 0520 // This file is encrypted, do some decryption first 0521 if (m_bPasswordDeclined) { 0522 // The user has already declined to give a password 0523 // Open the file as empty 0524 d->stream->close(); 0525 delete d->stream; 0526 d->stream = new QBuffer(); 0527 d->stream->open(QIODevice::ReadOnly); 0528 d->size = 0; 0529 return true; 0530 } 0531 QCA::SecureArray encryptedFile(d->stream->readAll()); 0532 if (encryptedFile.size() != d->size) { 0533 // Read error detected 0534 d->stream->close(); 0535 delete d->stream; 0536 d->stream = nullptr; 0537 warnStore << "read error"; 0538 return false; 0539 } 0540 d->stream->close(); 0541 delete d->stream; 0542 d->stream = nullptr; 0543 KoEncryptedStore_EncryptionData encData = m_encryptionData.value(name); 0544 QCA::SecureArray decrypted; 0545 0546 // If we don't have a password yet, try and find one 0547 if (m_password.isEmpty()) { 0548 findPasswordInKWallet(); 0549 } 0550 0551 while (true) { 0552 QCA::SecureArray password; 0553 bool keepPass = false; 0554 // I already have a password! Let's test it. If it's not good, we can dump it, anyway. 0555 if (!m_password.isEmpty()) { 0556 password = m_password; 0557 m_password = QCA::SecureArray(); 0558 } else { 0559 if (!m_filename.isNull()) 0560 keepPass = false; 0561 KPasswordDialog dlg(d->window , keepPass ? KPasswordDialog::ShowKeepPassword : static_cast<KPasswordDialog::KPasswordDialogFlags>(0)); 0562 dlg.setPrompt(i18n("Please enter the password to open this file.")); 0563 if (! dlg.exec()) { 0564 m_bPasswordDeclined = true; 0565 d->stream = new QBuffer(); 0566 d->stream->open(QIODevice::ReadOnly); 0567 d->size = 0; 0568 return true; 0569 } 0570 password = QCA::SecureArray(dlg.password().toUtf8()); 0571 if (keepPass) 0572 keepPass = dlg.keepPassword(); 0573 if (password.isEmpty()) { 0574 continue; 0575 } 0576 } 0577 0578 decrypted = decryptFile(encryptedFile, encData, password); 0579 if (decrypted.isEmpty()) { 0580 errorStore << "empty decrypted file" << endl; 0581 return false; 0582 } 0583 0584 if (!encData.checksum.isEmpty()) { 0585 QCA::SecureArray checksum; 0586 if (encData.checksumShort && decrypted.size() > 1024) { 0587 // TODO: Eww!!!! I don't want to convert via insecure arrays to get the first 1K characters of a secure array <- fix QCA? 0588 checksum = QCA::Hash("sha1").hash(QCA::SecureArray(decrypted.toByteArray().left(1024))); 0589 } else { 0590 checksum = QCA::Hash("sha1").hash(decrypted); 0591 } 0592 if (checksum != encData.checksum) { 0593 continue; 0594 } 0595 } 0596 0597 // The password passed all possible tests, so let's accept it 0598 m_password = password; 0599 m_bPasswordUsed = true; 0600 0601 if (keepPass) { 0602 savePasswordInKWallet(); 0603 } 0604 0605 break; 0606 } 0607 0608 QByteArray *resultArray = new QByteArray(decrypted.toByteArray()); 0609 KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType("application/x-gzip"); 0610 QIODevice *resultDevice = new KCompressionDevice(new QBuffer(resultArray, nullptr), false, type); 0611 0612 if (!resultDevice) { 0613 delete resultArray; 0614 return false; 0615 } 0616 static_cast<KFilterDev*>(resultDevice)->setSkipHeaders(); 0617 d->stream = resultDevice; 0618 d->size = encData.filesize; 0619 } 0620 if (!d->stream->isOpen()) { 0621 d->stream->open(QIODevice::ReadOnly); 0622 } 0623 return true; 0624 } 0625 0626 bool KoEncryptedStore::closeRead() 0627 { 0628 Q_D(KoStore); 0629 delete d->stream; 0630 d->stream = nullptr; 0631 return true; 0632 } 0633 0634 void KoEncryptedStore::findPasswordInKWallet() 0635 { 0636 Q_D(KoStore); 0637 /* About KWallet access 0638 * 0639 * The choice has been made to postfix every entry in a kwallet concerning passwords for opendocument files with /opendocument 0640 * This choice has been made since, at the time of this writing, the author could not find any reference to standardized 0641 * naming schemes for entries in the wallet. Since collision of passwords in entries should be avoided and is at least possible, 0642 * considering remote files might be both protected by a secured web-area (konqueror makes an entry) and a password (we make an 0643 * entry), it seems a good thing to make sure it won't happen. 0644 */ 0645 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")) { 0646 KWallet::Wallet *wallet = KWallet::Wallet::openWallet(KWallet::Wallet::LocalWallet(), d->window ? d->window->winId() : 0); 0647 if (wallet) { 0648 if (wallet->setFolder(KWallet::Wallet::PasswordFolder())) { 0649 QString pass; 0650 wallet->readPassword(m_filename + "/opendocument", pass); 0651 m_password = QCA::SecureArray(pass.toUtf8()); 0652 } 0653 delete wallet; 0654 } 0655 } 0656 } 0657 0658 void KoEncryptedStore::savePasswordInKWallet() 0659 { 0660 Q_D(KoStore); 0661 KWallet::Wallet *wallet = KWallet::Wallet::openWallet(KWallet::Wallet::LocalWallet(), d->window ? d->window->winId() : 0); 0662 if (wallet) { 0663 if (!wallet->hasFolder(KWallet::Wallet::PasswordFolder())) { 0664 wallet->createFolder(KWallet::Wallet::PasswordFolder()); 0665 } 0666 if (wallet->setFolder(KWallet::Wallet::PasswordFolder())) { 0667 if (wallet->hasEntry(m_filename + "/opendocument")) { 0668 wallet->removeEntry(m_filename + "/opendocument"); 0669 } 0670 wallet->writePassword(m_filename + "/opendocument", m_password.toByteArray().constData()); 0671 } 0672 delete wallet; 0673 } 0674 } 0675 0676 QCA::SecureArray KoEncryptedStore::decryptFile(QCA::SecureArray & encryptedFile, KoEncryptedStore_EncryptionData & encData, QCA::SecureArray & password) 0677 { 0678 QCA::SecureArray keyhash = QCA::Hash("sha1").hash(password); 0679 QCA::SymmetricKey key = QCA::PBKDF2("sha1").makeKey(keyhash, QCA::InitializationVector(encData.salt), 16, encData.iterationCount); 0680 QCA::Cipher decrypter("blowfish", QCA::Cipher::CFB, QCA::Cipher::DefaultPadding, QCA::Decode, key, QCA::InitializationVector(encData.initVector)); 0681 QCA::SecureArray result = decrypter.update(encryptedFile); 0682 result += decrypter.final(); 0683 return result; 0684 } 0685 0686 bool KoEncryptedStore::setPassword(const QString& password) 0687 { 0688 if (m_bPasswordUsed || password.isEmpty()) { 0689 return false; 0690 } 0691 m_password = QCA::SecureArray(password.toUtf8()); 0692 return true; 0693 } 0694 0695 QString KoEncryptedStore::password() 0696 { 0697 if (m_password.isEmpty()) { 0698 return QString(); 0699 } 0700 return QString(m_password.toByteArray()); 0701 } 0702 0703 bool KoEncryptedStore::openWrite(const QString& name) 0704 { 0705 Q_D(KoStore); 0706 if (bad()) 0707 return false; 0708 if (isToBeEncrypted(name)) { 0709 // Encrypted files will be compressed by this class and should be stored in the zip as not compressed 0710 m_pZip->setCompression(KZip::NoCompression); 0711 } else { 0712 m_pZip->setCompression(KZip::DeflateCompression); 0713 } 0714 d->stream = new QBuffer(); 0715 (static_cast< QBuffer* >(d->stream))->open(QIODevice::WriteOnly); 0716 if (name == MANIFEST_FILE) 0717 return true; 0718 return m_pZip->prepareWriting(name, "", "", 0); 0719 } 0720 0721 bool KoEncryptedStore::closeWrite() 0722 { 0723 Q_D(KoStore); 0724 bool passWasAsked = false; 0725 if (d->fileName == MANIFEST_FILE) { 0726 m_manifestBuffer = static_cast<QBuffer*>(d->stream)->buffer(); 0727 return true; 0728 } 0729 0730 // Find a password 0731 // Do not accept empty passwords for compatibility with OOo 0732 if (m_password.isEmpty()) { 0733 findPasswordInKWallet(); 0734 } 0735 while (m_password.isEmpty()) { 0736 KNewPasswordDialog dlg(d->window); 0737 dlg.setPrompt(i18n("Please enter the password to encrypt the document with.")); 0738 if (! dlg.exec()) { 0739 // Without the first password, prevent asking again by deadsimply refusing to continue functioning 0740 // TODO: This feels rather hackish. There should be a better way to do this. 0741 delete m_pZip; 0742 m_pZip = 0; 0743 d->good = false; 0744 return false; 0745 } 0746 m_password = QCA::SecureArray(dlg.password().toUtf8()); 0747 passWasAsked = true; 0748 } 0749 0750 // Ask the user to save the password 0751 if (passWasAsked && KMessageBox::questionYesNo(d->window, i18n("Do you want to save the password?")) == KMessageBox::Yes) { 0752 savePasswordInKWallet(); 0753 } 0754 0755 QByteArray resultData; 0756 if (d->fileName == THUMBNAIL_FILE) { 0757 // TODO: Replace with a generic 'encrypted'-thumbnail 0758 resultData = static_cast<QBuffer*>(d->stream)->buffer(); 0759 } else if (!isToBeEncrypted(d->fileName)) { 0760 resultData = static_cast<QBuffer*>(d->stream)->buffer(); 0761 } else { 0762 m_bPasswordUsed = true; 0763 // Build all cryptographic data 0764 QCA::SecureArray passwordHash = QCA::Hash("sha1").hash(m_password); 0765 QCA::Random random; 0766 KoEncryptedStore_EncryptionData encData; 0767 encData.initVector = random.randomArray(8); 0768 encData.salt = random.randomArray(16); 0769 encData.iterationCount = 1024; 0770 QCA::SymmetricKey key = QCA::PBKDF2("sha1").makeKey(passwordHash, QCA::InitializationVector(encData.salt), 16, encData.iterationCount); 0771 QCA::Cipher encrypter("blowfish", QCA::Cipher::CFB, QCA::Cipher::DefaultPadding, QCA::Encode, key, QCA::InitializationVector(encData.initVector)); 0772 0773 // Get the written data 0774 QByteArray data = static_cast<QBuffer*>(d->stream)->buffer(); 0775 encData.filesize = data.size(); 0776 0777 // Compress the data 0778 QBuffer compressedData; 0779 KCompressionDevice::CompressionType type = KFilterDev::compressionTypeForMimeType("application/x-gzip"); 0780 QIODevice *compressDevice = new KCompressionDevice(&compressedData, false, type); 0781 0782 if (!compressDevice) { 0783 return false; 0784 } 0785 static_cast<KFilterDev*>(compressDevice)->setSkipHeaders(); 0786 if (!compressDevice->open(QIODevice::WriteOnly)) { 0787 delete compressDevice; 0788 return false; 0789 } 0790 if (compressDevice->write(data) != data.size()) { 0791 delete compressDevice; 0792 return false; 0793 } 0794 compressDevice->close(); 0795 delete compressDevice; 0796 0797 encData.checksum = QCA::Hash("sha1").hash(QCA::SecureArray(compressedData.buffer())); 0798 encData.checksumShort = false; 0799 0800 // Encrypt the data 0801 QCA::SecureArray result = encrypter.update(QCA::SecureArray(compressedData.buffer())); 0802 result += encrypter.final(); 0803 resultData = result.toByteArray(); 0804 0805 m_encryptionData.insert(d->fileName, encData); 0806 } 0807 0808 if (!m_pZip->writeData(resultData.data(), resultData.size())) { 0809 m_pZip->finishWriting(resultData.size()); 0810 return false; 0811 } 0812 0813 return m_pZip->finishWriting(resultData.size()); 0814 } 0815 0816 bool KoEncryptedStore::enterRelativeDirectory(const QString& dirName) 0817 { 0818 Q_D(KoStore); 0819 if (d->mode == Read) { 0820 if (!m_currentDir) { 0821 m_currentDir = m_pZip->directory(); // initialize 0822 } 0823 const KArchiveEntry *entry = m_currentDir->entry(dirName); 0824 if (entry && entry->isDirectory()) { 0825 m_currentDir = dynamic_cast<const KArchiveDirectory*>(entry); 0826 return m_currentDir != 0; 0827 } 0828 return false; 0829 } else { // Write, no checking here 0830 return true; 0831 } 0832 } 0833 0834 bool KoEncryptedStore::enterAbsoluteDirectory(const QString& path) 0835 { 0836 if (path.isEmpty()) { 0837 m_currentDir = 0; 0838 return true; 0839 } 0840 m_currentDir = dynamic_cast<const KArchiveDirectory*>(m_pZip->directory()->entry(path)); 0841 return m_currentDir != 0; 0842 } 0843 0844 bool KoEncryptedStore::fileExists(const QString& absPath) const 0845 { 0846 const KArchiveEntry *entry = m_pZip->directory()->entry(absPath); 0847 return (entry && entry->isFile()) || (absPath == MANIFEST_FILE && !m_manifestBuffer.isNull()); 0848 } 0849 0850 #endif // QCA2