1 /*
2     This file is part of the KDE project
3     SPDX-FileCopyrightText: 2001-2004 George Staikos <staikos@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "kwalletbackend.h"
9 #include "kwalletbackend_debug.h"
10 
11 #include <stdlib.h>
12 
13 #include <QSaveFile>
14 #ifdef HAVE_GPGMEPP
15 #include <gpgme++/key.h>
16 #endif
17 #include <gcrypt.h>
18 #include <KNotification>
19 #include <KLocalizedString>
20 
21 #include <QDir>
22 #include <QFile>
23 #include <QFileInfo>
24 #include <QSaveFile>
25 #include <QCryptographicHash>
26 #include <QRegularExpression>
27 #include <QStandardPaths>
28 
29 #include "blowfish.h"
30 #include "sha1.h"
31 #include "cbc.h"
32 
33 #include <assert.h>
34 
35 // quick fix to get random numbers on win32
36 #ifdef Q_OS_WIN //krazy:exclude=cpp
37 #include <windows.h>
38 #include <wincrypt.h>
39 #endif
40 
41 #define KWALLET_VERSION_MAJOR       0
42 #define KWALLET_VERSION_MINOR       1
43 
44 using namespace KWallet;
45 
46 #define KWMAGIC "KWALLET\n\r\0\r\n"
47 
48 class Backend::BackendPrivate
49 {
50 };
51 
52 // static void initKWalletDir()
53 // {
54 //     KGlobal::dirs()->addResourceType("kwallet", 0, "share/apps/kwallet");
55 // }
56 
Backend(const QString & name,bool isPath)57 Backend::Backend(const QString &name, bool isPath)
58     : d(nullptr),
59       _name(name),
60       _cipherType(KWallet::BACKEND_CIPHER_UNKNOWN)
61 {
62 //  initKWalletDir();
63     if (isPath) {
64         _path = name;
65     } else {
66         _path = getSaveLocation() + QDir::separator() + _name + ".kwl";
67     }
68 
69     _open = false;
70 }
71 
~Backend()72 Backend::~Backend()
73 {
74     if (_open) {
75         close();
76     }
77     delete d;
78 }
79 
getSaveLocation()80 QString Backend::getSaveLocation()
81 {
82     QString writeLocation = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
83     if (writeLocation.right(1) == QLatin1String("5")) {
84       // HACK
85       // setApplicationName("kwalletd5") yields the path ~/.local/share/kwalletd5 for the location where to store wallets
86       // that is not desirable, as the 5 is present in the data folder's name
87       // this workaround getts the right ~/.local/share/kwalletd location
88       writeLocation = writeLocation.left(writeLocation.length() -1);
89     }
90     QDir writeDir(writeLocation);
91     if (!writeDir.exists()) {
92         if (!writeDir.mkpath(writeLocation)) {
93             qFatal("Cannot create wallet save location!");
94         }
95     }
96 
97     // qCDebug(KWALLETBACKEND_LOG) << "Using saveLocation " + writeLocation;
98     return writeLocation;
99 }
100 
setCipherType(BackendCipherType ct)101 void Backend::setCipherType(BackendCipherType ct)
102 {
103     // changing cipher type on already initialed wallets is not permitted
104     assert(_cipherType == KWallet::BACKEND_CIPHER_UNKNOWN);
105     _cipherType = ct;
106 }
107 
password2PBKDF2_SHA512(const QByteArray & password,QByteArray & hash,const QByteArray & salt)108 static int password2PBKDF2_SHA512(const QByteArray &password, QByteArray &hash, const QByteArray &salt)
109 {
110     if (!gcry_check_version("1.5.0")) {
111         qCWarning(KWALLETBACKEND_LOG) << "libcrypt version is too old";
112         return GPG_ERR_USER_2;
113     }
114 
115     gcry_error_t error;
116     bool static gcry_secmem_init = false;
117     if (!gcry_secmem_init) {
118         error = gcry_control(GCRYCTL_INIT_SECMEM, 32768, 0);
119         if (error != 0) {
120             qCWarning(KWALLETBACKEND_LOG) << "Can't get secure memory:" << error;
121             return error;
122         }
123         gcry_secmem_init = true;
124     }
125 
126     gcry_control(GCRYCTL_INITIALIZATION_FINISHED, 0);
127 
128     error = gcry_kdf_derive(password.constData(), password.size(),
129                             GCRY_KDF_PBKDF2, GCRY_MD_SHA512,
130                             salt.data(), salt.size(),
131                             PBKDF2_SHA512_ITERATIONS, PBKDF2_SHA512_KEYSIZE, hash.data());
132 
133     return error;
134 }
135 
136 // this should be SHA-512 for release probably
password2hash(const QByteArray & password,QByteArray & hash)137 static int password2hash(const QByteArray &password, QByteArray &hash)
138 {
139     SHA1 sha;
140     int shasz = sha.size() / 8;
141 
142     assert(shasz >= 20);
143 
144     QByteArray block1(shasz, 0);
145 
146     sha.process(password.data(), qMin(password.size(), 16));
147 
148     // To make brute force take longer
149     for (int i = 0; i < 2000; i++) {
150         memcpy(block1.data(), sha.hash(), shasz);
151         sha.reset();
152         sha.process(block1.data(), shasz);
153     }
154 
155     sha.reset();
156 
157     if (password.size() > 16) {
158         sha.process(password.data() + 16, qMin(password.size() - 16, 16));
159         QByteArray block2(shasz, 0);
160         // To make brute force take longer
161         for (int i = 0; i < 2000; i++) {
162             memcpy(block2.data(), sha.hash(), shasz);
163             sha.reset();
164             sha.process(block2.data(), shasz);
165         }
166 
167         sha.reset();
168 
169         if (password.size() > 32) {
170             sha.process(password.data() + 32, qMin(password.size() - 32, 16));
171 
172             QByteArray block3(shasz, 0);
173             // To make brute force take longer
174             for (int i = 0; i < 2000; i++) {
175                 memcpy(block3.data(), sha.hash(), shasz);
176                 sha.reset();
177                 sha.process(block3.data(), shasz);
178             }
179 
180             sha.reset();
181 
182             if (password.size() > 48) {
183                 sha.process(password.data() + 48, password.size() - 48);
184 
185                 QByteArray block4(shasz, 0);
186                 // To make brute force take longer
187                 for (int i = 0; i < 2000; i++) {
188                     memcpy(block4.data(), sha.hash(), shasz);
189                     sha.reset();
190                     sha.process(block4.data(), shasz);
191                 }
192 
193                 sha.reset();
194                 // split 14/14/14/14
195                 hash.resize(56);
196                 memcpy(hash.data(),      block1.data(), 14);
197                 memcpy(hash.data() + 14, block2.data(), 14);
198                 memcpy(hash.data() + 28, block3.data(), 14);
199                 memcpy(hash.data() + 42, block4.data(), 14);
200                 block4.fill(0);
201             } else {
202                 // split 20/20/16
203                 hash.resize(56);
204                 memcpy(hash.data(),      block1.data(), 20);
205                 memcpy(hash.data() + 20, block2.data(), 20);
206                 memcpy(hash.data() + 40, block3.data(), 16);
207             }
208             block3.fill(0);
209         } else {
210             // split 20/20
211             hash.resize(40);
212             memcpy(hash.data(),      block1.data(), 20);
213             memcpy(hash.data() + 20, block2.data(), 20);
214         }
215         block2.fill(0);
216     } else {
217         // entirely block1
218         hash.resize(20);
219         memcpy(hash.data(), block1.data(), 20);
220     }
221 
222     block1.fill(0);
223 
224     return 0;
225 }
226 
deref()227 int Backend::deref()
228 {
229     if (--_ref < 0) {
230         qCDebug(KWALLETBACKEND_LOG) << "refCount negative!";
231         _ref = 0;
232     }
233     return _ref;
234 }
235 
exists(const QString & wallet)236 bool Backend::exists(const QString &wallet)
237 {
238     QString saveLocation = getSaveLocation();
239     QString path = saveLocation + '/' + wallet + QLatin1String(".kwl");
240     // Note: 60 bytes is presently the minimum size of a wallet file.
241     //       Anything smaller is junk.
242     return QFile::exists(path) && QFileInfo(path).size() >= 60;
243 }
244 
openRCToString(int rc)245 QString Backend::openRCToString(int rc)
246 {
247     switch (rc) {
248     case -255:
249         return i18n("Already open.");
250     case -2:
251         return i18n("Error opening file.");
252     case -3:
253         return i18n("Not a wallet file.");
254     case -4:
255         return i18n("Unsupported file format revision.");
256     case -41:
257         return QStringLiteral("Unknown cipher or hash"); //FIXME: use i18n after string freeze
258     case -42:
259         return i18n("Unknown encryption scheme.");
260     case -43:
261         return i18n("Corrupt file?");
262     case -8:
263         return i18n("Error validating wallet integrity. Possibly corrupted.");
264     case -5:
265     case -7:
266     case -9:
267         return i18n("Read error - possibly incorrect password.");
268     case -6:
269         return i18n("Decryption error.");
270     default:
271         return QString();
272     }
273 }
274 
open(const QByteArray & password,WId w)275 int Backend::open(const QByteArray &password, WId w)
276 {
277     if (_open) {
278         return -255;  // already open
279     }
280 
281     setPassword(password);
282     return openInternal(w);
283 }
284 
285 #ifdef HAVE_GPGMEPP
open(const GpgME::Key & key)286 int Backend::open(const GpgME::Key &key)
287 {
288     if (_open) {
289         return -255;  // already open
290     }
291     _gpgKey = key;
292     return openInternal();
293 }
294 #endif // HAVE_GPGMEPP
295 
openPreHashed(const QByteArray & passwordHash)296 int Backend::openPreHashed(const QByteArray &passwordHash)
297 {
298     if (_open) {
299         return -255;  // already open
300     }
301 
302     // check the password hash for correct size (currently fixed)
303     if (passwordHash.size() != 20 && passwordHash.size() != 40 &&
304             passwordHash.size() != 56) {
305         return -42; // unsupported encryption scheme
306     }
307 
308     _passhash = passwordHash;
309     _newPassHash = passwordHash;
310     _useNewHash = true;//Only new hash is supported
311 
312     return openInternal();
313 }
314 
openInternal(WId w)315 int Backend::openInternal(WId w)
316 {
317     // No wallet existed.  Let's create it.
318     // Note: 60 bytes is presently the minimum size of a wallet file.
319     //       Anything smaller is junk and should be deleted.
320     if (!QFile::exists(_path) || QFileInfo(_path).size() < 60) {
321         QFile newfile(_path);
322         if (!newfile.open(QIODevice::ReadWrite)) {
323             return -2;   // error opening file
324         }
325         newfile.close();
326         _open = true;
327         if (sync(w) != 0) {
328             return -2;
329         }
330     }
331 
332     QFile db(_path);
333 
334     if (!db.open(QIODevice::ReadOnly)) {
335         return -2;         // error opening file
336     }
337 
338     char magicBuf[KWMAGIC_LEN];
339     db.read(magicBuf, KWMAGIC_LEN);
340     if (memcmp(magicBuf, KWMAGIC, KWMAGIC_LEN) != 0) {
341         return -3;         // bad magic
342     }
343 
344     db.read(magicBuf, 4);
345 
346     // First byte is major version, second byte is minor version
347     if (magicBuf[0] != KWALLET_VERSION_MAJOR) {
348         return -4;         // unknown version
349     }
350 
351     //0 has been the MINOR version until 4.13, from that point we use it to upgrade the hash
352     if (magicBuf[1] == 1) {
353         qCDebug(KWALLETBACKEND_LOG) << "Wallet new enough, using new hash";
354         swapToNewHash();
355     } else if (magicBuf[1] != 0) {
356         qCDebug(KWALLETBACKEND_LOG) << "Wallet is old, sad panda :(";
357         return -4;  // unknown version
358     }
359 
360     BackendPersistHandler *phandler = BackendPersistHandler::getPersistHandler(magicBuf);
361     if (nullptr == phandler) {
362         return -41; // unknown cipher or hash
363     }
364     int result = phandler->read(this, db, w);
365     delete phandler;
366     return result;
367 }
368 
swapToNewHash()369 void Backend::swapToNewHash()
370 {
371     //Runtime error happened and we can't use the new hash
372     if (!_useNewHash) {
373         qCDebug(KWALLETBACKEND_LOG) << "Runtime error on the new hash";
374         return;
375     }
376     _passhash.fill(0);//Making sure the old passhash is not around in memory
377     _passhash = _newPassHash;//Use the new hash, means the wallet is modern enough
378 }
379 
createAndSaveSalt(const QString & path) const380 QByteArray Backend::createAndSaveSalt(const QString &path) const
381 {
382     QFile saltFile(path);
383     saltFile.remove();
384 
385     if (!saltFile.open(QIODevice::WriteOnly)) {
386         return QByteArray();
387     }
388     saltFile.setPermissions(QFile::ReadUser | QFile::WriteUser);
389 
390     char *randomData = (char *) gcry_random_bytes(PBKDF2_SHA512_SALTSIZE, GCRY_STRONG_RANDOM);
391     QByteArray salt(randomData, PBKDF2_SHA512_SALTSIZE);
392     free(randomData);
393 
394     if (saltFile.write(salt) != PBKDF2_SHA512_SALTSIZE) {
395         return QByteArray();
396     }
397 
398     saltFile.close();
399 
400     return salt;
401 }
402 
sync(WId w)403 int Backend::sync(WId w)
404 {
405     if (!_open) {
406         return -255;  // not open yet
407     }
408 
409     if (!QFile::exists(_path)) {
410         return -3; // File does not exist
411     }
412 
413     QSaveFile sf(_path);
414 
415     if (!sf.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) {
416         return -1;      // error opening file
417     }
418     sf.setPermissions(QFile::ReadUser | QFile::WriteUser);
419 
420     if (sf.write(KWMAGIC, KWMAGIC_LEN) != KWMAGIC_LEN) {
421         sf.cancelWriting();
422         return -4; // write error
423     }
424 
425     // Write the version number
426     QByteArray version(4, 0);
427     version[0] = KWALLET_VERSION_MAJOR;
428     if (_useNewHash) {
429         version[1] = KWALLET_VERSION_MINOR;
430         //Use the sync to update the hash to PBKDF2_SHA512
431         swapToNewHash();
432     } else {
433         version[1] = 0; //was KWALLET_VERSION_MINOR before the new hash
434     }
435 
436     BackendPersistHandler *phandler = BackendPersistHandler::getPersistHandler(_cipherType);
437     if (nullptr == phandler) {
438         return -4; // write error
439     }
440     int rc = phandler->write(this, sf, version, w);
441     if (rc < 0) {
442         // Oops! wallet file sync filed! Display a notification about that
443         // TODO: change kwalletd status flags, when status flags will be implemented
444         KNotification *notification = new KNotification(QStringLiteral("syncFailed"));
445         notification->setText(i18n("Failed to sync wallet <b>%1</b> to disk. Error codes are:\nRC <b>%2</b>\nSF <b>%3</b>. Please file a BUG report using this information to bugs.kde.org", _name, rc, sf.errorString()));
446         notification->sendEvent();
447     }
448     delete phandler;
449     return rc;
450 }
451 
close(bool save)452 int Backend::close(bool save)
453 {
454     // save if requested
455     if (save) {
456         int rc = sync(0);
457         if (rc != 0) {
458             return rc;
459         }
460     }
461 
462     // do the actual close
463     for (FolderMap::ConstIterator i = _entries.constBegin(); i != _entries.constEnd(); ++i) {
464         for (EntryMap::ConstIterator j = i.value().constBegin(); j != i.value().constEnd(); ++j) {
465             delete j.value();
466         }
467     }
468     _entries.clear();
469 
470     // empty the password hash
471     _passhash.fill(0);
472     _newPassHash.fill(0);
473 
474     _open = false;
475 
476     return 0;
477 }
478 
walletName() const479 const QString &Backend::walletName() const
480 {
481     return _name;
482 }
483 
isOpen() const484 bool Backend::isOpen() const
485 {
486     return _open;
487 }
488 
folderList() const489 QStringList Backend::folderList() const
490 {
491     return _entries.keys();
492 }
493 
entryList() const494 QStringList Backend::entryList() const
495 {
496     return _entries[_folder].keys();
497 }
498 
readEntry(const QString & key)499 Entry *Backend::readEntry(const QString &key)
500 {
501     Entry *rc = nullptr;
502 
503     if (_open && hasEntry(key)) {
504         rc = _entries[_folder][key];
505     }
506 
507     return rc;
508 }
509 
510 #if KWALLET_BUILD_DEPRECATED_SINCE(5, 72)
readEntryList(const QString & key)511 QList<Entry *> Backend::readEntryList(const QString &key)
512 {
513     QList<Entry *> rc;
514 
515     if (!_open) {
516         return rc;
517     }
518 
519     // HACK: see Wallet::WalletPrivate::forEachItemThatMatches()
520     const QString pattern = QRegularExpression::wildcardToRegularExpression(key).replace(
521                                                          QLatin1String("[^/]"), QLatin1String("."));
522     const QRegularExpression re(pattern);
523 
524     const EntryMap &map = _entries[_folder];
525     for (EntryMap::ConstIterator i = map.begin(); i != map.end(); ++i) {
526         if (re.match(i.key()).hasMatch()) {
527             rc.append(i.value());
528         }
529     }
530     return rc;
531 }
532 #endif
533 
entriesList() const534 QList<Entry *> Backend::entriesList() const
535 {
536     if (!_open) {
537         return QList<Entry *>();
538     }
539     const EntryMap &map = _entries[_folder];
540 
541     return map.values();
542 }
543 
544 
createFolder(const QString & f)545 bool Backend::createFolder(const QString &f)
546 {
547     if (_entries.contains(f)) {
548         return false;
549     }
550 
551     _entries.insert(f, EntryMap());
552 
553     QCryptographicHash folderMd5(QCryptographicHash::Md5);
554     folderMd5.addData(f.toUtf8());
555     _hashes.insert(MD5Digest(folderMd5.result()), QList<MD5Digest>());
556 
557     return true;
558 }
559 
renameEntry(const QString & oldName,const QString & newName)560 int Backend::renameEntry(const QString &oldName, const QString &newName)
561 {
562     EntryMap &emap = _entries[_folder];
563     EntryMap::Iterator oi = emap.find(oldName);
564     EntryMap::Iterator ni = emap.find(newName);
565 
566     if (oi != emap.end() && ni == emap.end()) {
567         Entry *e = oi.value();
568         emap.erase(oi);
569         emap[newName] = e;
570 
571         QCryptographicHash folderMd5(QCryptographicHash::Md5);
572         folderMd5.addData(_folder.toUtf8());
573 
574         HashMap::iterator i = _hashes.find(MD5Digest(folderMd5.result()));
575         if (i != _hashes.end()) {
576             QCryptographicHash oldMd5(QCryptographicHash::Md5);
577             QCryptographicHash newMd5(QCryptographicHash::Md5);
578             oldMd5.addData(oldName.toUtf8());
579             newMd5.addData(newName.toUtf8());
580             i.value().removeAll(MD5Digest(oldMd5.result()));
581             i.value().append(MD5Digest(newMd5.result()));
582         }
583         return 0;
584     }
585 
586     return -1;
587 }
588 
writeEntry(Entry * e)589 void Backend::writeEntry(Entry *e)
590 {
591     if (!_open) {
592         return;
593     }
594 
595     if (!hasEntry(e->key())) {
596         _entries[_folder][e->key()] = new Entry;
597     }
598     _entries[_folder][e->key()]->copy(e);
599 
600     QCryptographicHash folderMd5(QCryptographicHash::Md5);
601     folderMd5.addData(_folder.toUtf8());
602 
603     HashMap::iterator i = _hashes.find(MD5Digest(folderMd5.result()));
604     if (i != _hashes.end()) {
605         QCryptographicHash md5(QCryptographicHash::Md5);
606         md5.addData(e->key().toUtf8());
607         i.value().append(MD5Digest(md5.result()));
608     }
609 }
610 
hasEntry(const QString & key) const611 bool Backend::hasEntry(const QString &key) const
612 {
613     return _entries.contains(_folder) && _entries[_folder].contains(key);
614 }
615 
removeEntry(const QString & key)616 bool Backend::removeEntry(const QString &key)
617 {
618     if (!_open) {
619         return false;
620     }
621 
622     FolderMap::Iterator fi = _entries.find(_folder);
623     EntryMap::Iterator ei = fi.value().find(key);
624 
625     if (fi != _entries.end() && ei != fi.value().end()) {
626         delete ei.value();
627         fi.value().erase(ei);
628         QCryptographicHash folderMd5(QCryptographicHash::Md5);
629         folderMd5.addData(_folder.toUtf8());
630 
631         HashMap::iterator i = _hashes.find(MD5Digest(folderMd5.result()));
632         if (i != _hashes.end()) {
633             QCryptographicHash md5(QCryptographicHash::Md5);
634             md5.addData(key.toUtf8());
635             i.value().removeAll(MD5Digest(md5.result()));
636         }
637         return true;
638     }
639 
640     return false;
641 }
642 
removeFolder(const QString & f)643 bool Backend::removeFolder(const QString &f)
644 {
645     if (!_open) {
646         return false;
647     }
648 
649     FolderMap::Iterator fi = _entries.find(f);
650 
651     if (fi != _entries.end()) {
652         if (_folder == f) {
653             _folder.clear();
654         }
655 
656         for (EntryMap::Iterator ei = fi.value().begin(); ei != fi.value().end(); ++ei) {
657             delete ei.value();
658         }
659 
660         _entries.erase(fi);
661 
662         QCryptographicHash folderMd5(QCryptographicHash::Md5);
663         folderMd5.addData(f.toUtf8());
664         _hashes.remove(MD5Digest(folderMd5.result()));
665         return true;
666     }
667 
668     return false;
669 }
670 
folderDoesNotExist(const QString & folder) const671 bool Backend::folderDoesNotExist(const QString &folder) const
672 {
673     QCryptographicHash md5(QCryptographicHash::Md5);
674     md5.addData(folder.toUtf8());
675     return !_hashes.contains(MD5Digest(md5.result()));
676 }
677 
entryDoesNotExist(const QString & folder,const QString & entry) const678 bool Backend::entryDoesNotExist(const QString &folder, const QString &entry) const
679 {
680     QCryptographicHash md5(QCryptographicHash::Md5);
681     md5.addData(folder.toUtf8());
682     HashMap::const_iterator i = _hashes.find(MD5Digest(md5.result()));
683     if (i != _hashes.end()) {
684         md5.reset();
685         md5.addData(entry.toUtf8());
686         return !i.value().contains(MD5Digest(md5.result()));
687     }
688     return true;
689 }
690 
setPassword(const QByteArray & password)691 void Backend::setPassword(const QByteArray &password)
692 {
693     _passhash.fill(0); // empty just in case
694     BlowFish _bf;
695     CipherBlockChain bf(&_bf);
696     _passhash.resize(bf.keyLen() / 8);
697     _newPassHash.resize(bf.keyLen() / 8);
698     _newPassHash.fill(0);
699 
700     password2hash(password, _passhash);
701 
702     QByteArray salt;
703     QFile saltFile(getSaveLocation() + QDir::separator() + _name + ".salt");
704     if (!saltFile.exists() || saltFile.size() == 0) {
705         salt = createAndSaveSalt(saltFile.fileName());
706     } else {
707         if (!saltFile.open(QIODevice::ReadOnly)) {
708             salt = createAndSaveSalt(saltFile.fileName());
709         } else {
710             salt = saltFile.readAll();
711         }
712     }
713 
714     if (!salt.isEmpty() && password2PBKDF2_SHA512(password, _newPassHash,  salt) == 0) {
715         qCDebug(KWALLETBACKEND_LOG) << "Setting useNewHash to true";
716         _useNewHash = true;
717     }
718 }
719 
720 #ifdef HAVE_GPGMEPP
gpgKey() const721 const GpgME::Key &Backend::gpgKey() const
722 {
723     return _gpgKey;
724 }
725 #endif
726