1 /* This file is part of the KDE project
3 * Copyright (C) 2001-2004 George Staikos <staikos@kde.org>
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Library General Public License for more details.
15 * You should have received a copy of the GNU Library General Public License
16 * along with this library; see the file COPYING.LIB. If not, write to
17 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
21 #include "kwalletbackend.h"
29 #include <ksavefile.h>
30 #include <kstandarddirs.h>
32 #include <QtCore/QFile>
33 #include <QtCore/QFileInfo>
34 #include <QtCore/QRegExp>
42 // quick fix to get random numbers on win32
43 #ifdef Q_OS_WIN //krazy:exclude=cpp
48 #define KWALLET_VERSION_MAJOR 0
49 #define KWALLET_VERSION_MINOR 0
51 #define KWALLET_CIPHER_BLOWFISH_CBC 0
52 #define KWALLET_CIPHER_3DES_CBC 1 // unsupported
54 #define KWALLET_HASH_SHA1 0
55 #define KWALLET_HASH_MD5 1 // unsupported
58 using namespace KWallet
;
60 #define KWMAGIC "KWALLET\n\r\0\r\n"
61 #define KWMAGIC_LEN 12
63 class Backend::BackendPrivate
67 static void initKWalletDir()
69 KGlobal::dirs()->addResourceType("kwallet", 0, "share/apps/kwallet");
72 Backend::Backend(const QString
& name
, bool isPath
) : d(0), _name(name
), _ref(0) {
77 _path
= KGlobal::dirs()->saveLocation("kwallet") + _name
+ ".kwl";
91 static int getRandomBlock(QByteArray
& randBlock
) {
93 #ifdef Q_OS_WIN //krazy:exclude=cpp
95 // Use windows crypto API to get randomness on win32
96 // HACK: this should be done using qca
99 if (!CryptAcquireContext(&hProv
, NULL
, NULL
, PROV_RSA_FULL
,
100 CRYPT_VERIFYCONTEXT
| CRYPT_SILENT
)) return -1; // couldn't get random data
102 if (!CryptGenRandom(hProv
, static_cast<DWORD
>(randBlock
.size()),
103 (BYTE
*)randBlock
.data())) {
104 return -3; // read error
107 // release the crypto context
108 CryptReleaseContext(hProv
, 0);
110 return randBlock
.size();
114 // First try /dev/urandom
115 if (QFile::exists("/dev/urandom")) {
116 QFile
devrand("/dev/urandom");
117 if (devrand
.open(QIODevice::ReadOnly
)) {
118 int rc
= devrand
.read(randBlock
.data(), randBlock
.size());
120 if (rc
!= randBlock
.size()) {
121 return -3; // not enough data read
128 // If that failed, try /dev/random
129 // FIXME: open in noblocking mode!
130 if (QFile::exists("/dev/random")) {
131 QFile
devrand("/dev/random");
132 if (devrand
.open(QIODevice::ReadOnly
)) {
137 int rc2
= devrand
.read(randBlock
.data() + rc
, randBlock
.size());
140 return -3; // read error
145 if (cnt
> randBlock
.size()) {
146 return -4; // reading forever?!
148 } while(rc
< randBlock
.size());
155 QString randFilename
= QString::fromLocal8Bit(qgetenv("RANDFILE"));
156 if (!randFilename
.isEmpty()) {
157 if (QFile::exists(randFilename
)) {
158 QFile
devrand(randFilename
);
159 if (devrand
.open(QIODevice::ReadOnly
)) {
160 int rc
= devrand
.read(randBlock
.data(), randBlock
.size());
161 if (rc
!= randBlock
.size()) {
162 return -3; // not enough data read
169 // Couldn't get any random data!!
176 // this should be SHA-512 for release probably
177 static int password2hash(const QByteArray
& password
, QByteArray
& hash
) {
179 int shasz
= sha
.size() / 8;
183 QByteArray
block1(shasz
, 0);
185 sha
.process(password
.data(), qMin(password
.size(), 16));
187 // To make brute force take longer
188 for (int i
= 0; i
< 2000; i
++) {
189 memcpy(block1
.data(), sha
.hash(), shasz
);
191 sha
.process(block1
.data(), shasz
);
196 if (password
.size() > 16) {
197 sha
.process(password
.data() + 16, qMin(password
.size() - 16, 16));
198 QByteArray
block2(shasz
, 0);
199 // To make brute force take longer
200 for (int i
= 0; i
< 2000; i
++) {
201 memcpy(block2
.data(), sha
.hash(), shasz
);
203 sha
.process(block2
.data(), shasz
);
208 if (password
.size() > 32) {
209 sha
.process(password
.data() + 32, qMin(password
.size() - 32, 16));
211 QByteArray
block3(shasz
, 0);
212 // To make brute force take longer
213 for (int i
= 0; i
< 2000; i
++) {
214 memcpy(block3
.data(), sha
.hash(), shasz
);
216 sha
.process(block3
.data(), shasz
);
221 if (password
.size() > 48) {
222 sha
.process(password
.data() + 48, password
.size() - 48);
224 QByteArray
block4(shasz
, 0);
225 // To make brute force take longer
226 for (int i
= 0; i
< 2000; i
++) {
227 memcpy(block4
.data(), sha
.hash(), shasz
);
229 sha
.process(block4
.data(), shasz
);
235 memcpy(hash
.data(), block1
.data(), 14);
236 memcpy(hash
.data() + 14, block2
.data(), 14);
237 memcpy(hash
.data() + 28, block3
.data(), 14);
238 memcpy(hash
.data() + 42, block4
.data(), 14);
243 memcpy(hash
.data(), block1
.data(), 20);
244 memcpy(hash
.data() + 20, block2
.data(), 20);
245 memcpy(hash
.data() + 40, block3
.data(), 16);
251 memcpy(hash
.data(), block1
.data(), 20);
252 memcpy(hash
.data() + 20, block2
.data(), 20);
258 memcpy(hash
.data(), block1
.data(), 20);
267 int Backend::deref() {
269 kDebug() << "refCount negative!";
275 bool Backend::exists(const QString
& wallet
) {
277 QString path
= KGlobal::dirs()->saveLocation("kwallet") + '/' + wallet
+ ".kwl";
278 // Note: 60 bytes is presently the minimum size of a wallet file.
279 // Anything smaller is junk.
280 return QFile::exists(path
) && QFileInfo(path
).size() >= 60;
284 QString
Backend::openRCToString(int rc
) {
287 return i18n("Already open.");
289 return i18n("Error opening file.");
291 return i18n("Not a wallet file.");
293 return i18n("Unsupported file format revision.");
295 return i18n("Unknown encryption scheme.");
297 return i18n("Corrupt file?");
299 return i18n("Error validating wallet integrity. Possibly corrupted.");
303 return i18n("Read error - possibly incorrect password.");
305 return i18n("Decryption error.");
312 int Backend::open(const QByteArray
& password
) {
315 return -255; // already open
318 setPassword(password
);
320 // No wallet existed. Let's create it.
321 // Note: 60 bytes is presently the minimum size of a wallet file.
322 // Anything smaller is junk and should be deleted.
323 if (!QFile::exists(_path
) || QFileInfo(_path
).size() < 60) {
324 QFile
newfile(_path
);
325 if (!newfile
.open(QIODevice::ReadWrite
)) {
326 return -2; // error opening file
331 return 1; // new file opened, but OK
336 if (!db
.open(QIODevice::ReadOnly
)) {
337 return -2; // error opening file
340 char magicBuf
[KWMAGIC_LEN
];
341 db
.read(magicBuf
, KWMAGIC_LEN
);
342 if (memcmp(magicBuf
, KWMAGIC
, KWMAGIC_LEN
) != 0) {
343 return -3; // bad magic
346 db
.read(magicBuf
, 4);
348 // First byte is major version, second byte is minor version
349 if (magicBuf
[0] != KWALLET_VERSION_MAJOR
) {
350 return -4; // unknown version
353 if (magicBuf
[1] != KWALLET_VERSION_MINOR
) {
354 return -4; // unknown version
357 if (magicBuf
[2] != KWALLET_CIPHER_BLOWFISH_CBC
) {
358 return -42; // unknown cipher
361 if (magicBuf
[3] != KWALLET_HASH_SHA1
) {
362 return -42; // unknown hash
366 // Read in the hashes
367 QDataStream
hds(&db
);
370 if (n
> 0xffff) { // sanity check
374 for (size_t i
= 0; i
< n
; ++i
) {
375 KMD5::Digest d
, d2
; // judgment day
377 QMap
<MD5Digest
,QList
<MD5Digest
> >::iterator it
;
379 if (hds
.atEnd()) return -43;
380 hds
.readRawData(reinterpret_cast<char *>(d
), 16);
382 ba
= MD5Digest(reinterpret_cast<char *>(d
));
383 it
= _hashes
.insert(ba
, QList
<MD5Digest
>());
384 for (size_t j
= 0; j
< fsz
; ++j
) {
385 hds
.readRawData(reinterpret_cast<char *>(d2
), 16);
386 ba
= MD5Digest(reinterpret_cast<char *>(d2
));
391 // Read in the rest of the file.
392 QByteArray encrypted
= db
.readAll();
393 assert(encrypted
.size() < db
.size());
396 CipherBlockChain
bf(&_bf
);
397 int blksz
= bf
.blockSize();
398 if ((encrypted
.size() % blksz
) != 0) {
399 return -5; // invalid file structure
402 bf
.setKey((void *)_passhash
.data(), _passhash
.size()*8);
404 if (!encrypted
.data()) {
407 return -7; // file structure error
410 int rc
= bf
.decrypt(encrypted
.data(), encrypted
.size());
414 return -6; // decrypt error
417 const char *t
= encrypted
.data();
419 // strip the leading data
420 t
+= blksz
; // one block of random data
422 // strip the file size off
425 fsize
|= (long(*t
) << 24) & 0xff000000;
427 fsize
|= (long(*t
) << 16) & 0x00ff0000;
429 fsize
|= (long(*t
) << 8) & 0x0000ff00;
431 fsize
|= long(*t
) & 0x000000ff;
434 if (fsize
< 0 || fsize
> long(encrypted
.size()) - blksz
- 4) {
435 //kDebug() << "fsize: " << fsize << " encrypted.size(): " << encrypted.size() << " blksz: " << blksz;
437 return -9; // file structure error.
440 // compute the hash ourself
442 sha
.process(t
, fsize
);
443 const char *testhash
= (const char *)sha
.hash();
446 int sz
= encrypted
.size();
447 for (int i
= 0; i
< 20; i
++) {
448 if (testhash
[i
] != encrypted
[sz
- 20 + i
]) {
451 return -8; // hash error.
457 // chop off the leading blksz+4 bytes
458 QByteArray
tmpenc(encrypted
.data()+blksz
+4, fsize
);
462 // Load the data structures up
463 QDataStream
eStream(encrypted
);
465 while (!eStream
.atEnd()) {
472 // Force initialisation
473 _entries
[folder
].clear();
475 for (size_t i
= 0; i
< n
; ++i
) {
477 KWallet::Wallet::EntryType et
= KWallet::Wallet::Unknown
;
478 Entry
*e
= new Entry
;
480 qint32 x
= 0; // necessary to read properly
482 et
= static_cast<KWallet::Wallet::EntryType
>(x
);
485 case KWallet::Wallet::Password
:
486 case KWallet::Wallet::Stream
:
487 case KWallet::Wallet::Map
:
489 default: // Unknown entry
499 _entries
[folder
][key
] = e
;
509 int Backend::sync() {
511 return -255; // not open yet
516 if (!sf
.open(QIODevice::WriteOnly
| QIODevice::Unbuffered
)) {
517 return -1; // error opening file
519 sf
.setPermissions(QFile::ReadUser
|QFile::WriteUser
);
521 if (sf
.write(KWMAGIC
, KWMAGIC_LEN
) != KWMAGIC_LEN
) {
523 return -4; // write error
526 // Write the version number
527 QByteArray
version(4, 0);
528 version
[0] = KWALLET_VERSION_MAJOR
;
529 version
[1] = KWALLET_VERSION_MINOR
;
530 version
[2] = KWALLET_CIPHER_BLOWFISH_CBC
;
531 version
[3] = KWALLET_HASH_SHA1
;
532 if (sf
.write(version
, 4) != 4) {
534 return -4; // write error
537 // Holds the hashes we write out
539 QDataStream
hashStream(&hashes
, QIODevice::WriteOnly
);
541 hashStream
<< static_cast<quint32
>(_entries
.count());
543 // Holds decrypted data prior to encryption
544 QByteArray decrypted
;
546 // FIXME: we should estimate the amount of data we will write in each
547 // buffer and resize them approximately in order to avoid extra
550 // populate decrypted
551 QDataStream
dStream(&decrypted
, QIODevice::WriteOnly
);
552 for (FolderMap::ConstIterator i
= _entries
.constBegin(); i
!= _entries
.constEnd(); ++i
) {
554 dStream
<< static_cast<quint32
>(i
.value().count());
557 md5
.update(i
.key().toUtf8());
558 hashStream
.writeRawData(reinterpret_cast<const char*>(&(md5
.rawDigest()[0])), 16);
559 hashStream
<< static_cast<quint32
>(i
.value().count());
561 for (EntryMap::ConstIterator j
= i
.value().constBegin(); j
!= i
.value().constEnd(); ++j
) {
563 dStream
<< static_cast<qint32
>(j
.value()->type());
564 dStream
<< j
.value()->value();
567 md5
.update(j
.key().toUtf8());
568 hashStream
.writeRawData(reinterpret_cast<const char*>(&(md5
.rawDigest()[0])), 16);
572 if (sf
.write(hashes
, hashes
.size()) != hashes
.size()) {
574 return -4; // write error
577 // calculate the hash of the file
580 CipherBlockChain
bf(&_bf
);
582 sha
.process(decrypted
.data(), decrypted
.size());
584 // prepend and append the random data
585 QByteArray wholeFile
;
586 long blksz
= bf
.blockSize();
587 long newsize
= decrypted
.size() +
588 blksz
+ // encrypted block
590 20; // size of the SHA hash
592 int delta
= (blksz
- (newsize
% blksz
));
594 wholeFile
.resize(newsize
);
596 QByteArray randBlock
;
597 randBlock
.resize(blksz
+delta
);
598 if (getRandomBlock(randBlock
) < 0) {
602 return -3; // Fatal error: can't get random
605 for (int i
= 0; i
< blksz
; i
++) {
606 wholeFile
[i
] = randBlock
[i
];
609 for (int i
= 0; i
< 4; i
++) {
610 wholeFile
[(int)(i
+blksz
)] = (decrypted
.size() >> 8*(3-i
))&0xff;
613 for (int i
= 0; i
< decrypted
.size(); i
++) {
614 wholeFile
[(int)(i
+blksz
+4)] = decrypted
[i
];
617 for (int i
= 0; i
< delta
; i
++) {
618 wholeFile
[(int)(i
+blksz
+4+decrypted
.size())] = randBlock
[(int)(i
+blksz
)];
621 const char *hash
= (const char *)sha
.hash();
622 for (int i
= 0; i
< 20; i
++) {
623 wholeFile
[(int)(newsize
- 20 + i
)] = hash
[i
];
630 if (!bf
.setKey(_passhash
.data(), _passhash
.size() * 8)) {
633 return -2; // encrypt error
636 int rc
= bf
.encrypt(wholeFile
.data(), wholeFile
.size());
640 return -2; // encrypt error
644 if (sf
.write(wholeFile
, wholeFile
.size()) != wholeFile
.size()) {
647 return -4; // write error
649 if (!sf
.finalize()) {
651 return -4; // write error
660 int Backend::close(bool save
) {
669 // do the actual close
670 for (FolderMap::ConstIterator i
= _entries
.constBegin(); i
!= _entries
.constEnd(); ++i
) {
671 for (EntryMap::ConstIterator j
= i
.value().constBegin(); j
!= i
.value().constEnd(); ++j
) {
677 // empty the password hash
685 const QString
& Backend::walletName() const {
690 bool Backend::isOpen() const {
695 QStringList
Backend::folderList() const {
696 return _entries
.keys();
700 QStringList
Backend::entryList() const {
701 return _entries
[_folder
].keys();
705 Entry
*Backend::readEntry(const QString
& key
) {
708 if (_open
&& hasEntry(key
)) {
709 rc
= _entries
[_folder
][key
];
716 QList
<Entry
*> Backend::readEntryList(const QString
& key
) {
723 QRegExp
re(key
, Qt::CaseSensitive
, QRegExp::Wildcard
);
725 const EntryMap
& map
= _entries
[_folder
];
726 for (EntryMap::ConstIterator i
= map
.begin(); i
!= map
.end(); ++i
) {
727 if (re
.exactMatch(i
.key())) {
728 rc
.append(i
.value());
735 bool Backend::createFolder(const QString
& f
) {
736 if (_entries
.contains(f
)) {
740 _entries
.insert(f
, EntryMap());
743 folderMd5
.update(f
.toUtf8());
744 _hashes
.insert(MD5Digest(folderMd5
.rawDigest()), QList
<MD5Digest
>());
750 int Backend::renameEntry(const QString
& oldName
, const QString
& newName
) {
751 EntryMap
& emap
= _entries
[_folder
];
752 EntryMap::Iterator oi
= emap
.find(oldName
);
753 EntryMap::Iterator ni
= emap
.find(newName
);
755 if (oi
!= emap
.end() && ni
== emap
.end()) {
756 Entry
*e
= oi
.value();
761 folderMd5
.update(_folder
.toUtf8());
763 HashMap::iterator i
= _hashes
.find(MD5Digest(folderMd5
.rawDigest()));
764 if (i
!= _hashes
.end()) {
766 oldMd5
.update(oldName
.toUtf8());
767 newMd5
.update(newName
.toUtf8());
768 i
.value().removeAll(MD5Digest(oldMd5
.rawDigest()));
769 i
.value().append(MD5Digest(newMd5
.rawDigest()));
778 void Backend::writeEntry(Entry
*e
) {
782 if (!hasEntry(e
->key())) {
783 _entries
[_folder
][e
->key()] = new Entry
;
785 _entries
[_folder
][e
->key()]->copy(e
);
788 folderMd5
.update(_folder
.toUtf8());
790 HashMap::iterator i
= _hashes
.find(MD5Digest(folderMd5
.rawDigest()));
791 if (i
!= _hashes
.end()) {
793 md5
.update(e
->key().toUtf8());
794 i
.value().append(MD5Digest(md5
.rawDigest()));
799 bool Backend::hasEntry(const QString
& key
) const {
800 return _entries
.contains(_folder
) && _entries
[_folder
].contains(key
);
804 bool Backend::removeEntry(const QString
& key
) {
809 FolderMap::Iterator fi
= _entries
.find(_folder
);
810 EntryMap::Iterator ei
= fi
.value().find(key
);
812 if (fi
!= _entries
.end() && ei
!= fi
.value().end()) {
814 fi
.value().erase(ei
);
816 folderMd5
.update(_folder
.toUtf8());
818 HashMap::iterator i
= _hashes
.find(MD5Digest(folderMd5
.rawDigest()));
819 if (i
!= _hashes
.end()) {
821 md5
.update(key
.toUtf8());
822 i
.value().removeAll(MD5Digest(md5
.rawDigest()));
831 bool Backend::removeFolder(const QString
& f
) {
836 FolderMap::Iterator fi
= _entries
.find(f
);
838 if (fi
!= _entries
.end()) {
843 for (EntryMap::Iterator ei
= fi
.value().begin(); ei
!= fi
.value().end(); ++ei
) {
850 folderMd5
.update(f
.toUtf8());
851 _hashes
.remove(MD5Digest(folderMd5
.rawDigest()));
859 bool Backend::folderDoesNotExist(const QString
& folder
) const {
861 md5
.update(folder
.toUtf8());
862 return !_hashes
.contains(MD5Digest(md5
.rawDigest()));
866 bool Backend::entryDoesNotExist(const QString
& folder
, const QString
& entry
) const {
868 md5
.update(folder
.toUtf8());
869 HashMap::const_iterator i
= _hashes
.find(MD5Digest(md5
.rawDigest()));
870 if (i
!= _hashes
.end()) {
872 md5
.update(entry
.toUtf8());
873 return !i
.value().contains(MD5Digest(md5
.rawDigest()));
878 void Backend::setPassword(const QByteArray
&password
) {
879 _passhash
.fill(0); // empty just in case
881 CipherBlockChain
bf(&_bf
);
882 _passhash
.resize(bf
.keyLen()/8);
883 password2hash(password
, _passhash
);