KWallet

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

KDE's Doxygen guidelines are available online.