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