diff options
Diffstat (limited to 'kwallet/backend/kwalletbackend.cc')
-rw-r--r-- | kwallet/backend/kwalletbackend.cc | 838 |
1 files changed, 838 insertions, 0 deletions
diff --git a/kwallet/backend/kwalletbackend.cc b/kwallet/backend/kwalletbackend.cc new file mode 100644 index 000000000..5b9c29299 --- /dev/null +++ b/kwallet/backend/kwalletbackend.cc @@ -0,0 +1,838 @@ +/* This file is part of the KDE project + * + * Copyright (C) 2001-2004 George Staikos <staikos@kde.org> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public License + * along with this library; see the file COPYING.LIB. If not, write to + * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +#include "kwalletbackend.h" + +#include <stdlib.h> + +#include <kdebug.h> +#include <kglobal.h> +#include <klocale.h> +#include <kmdcodec.h> +#include <ksavefile.h> +#include <kstandarddirs.h> + +#include <qfile.h> +#include <qfileinfo.h> +#include <qregexp.h> + +#include "blowfish.h" +#include "sha1.h" +#include "cbc.h" + +#include <assert.h> + +#define KWALLET_VERSION_MAJOR 0 +#define KWALLET_VERSION_MINOR 0 + +#define KWALLET_CIPHER_BLOWFISH_CBC 0 +#define KWALLET_CIPHER_3DES_CBC 1 // unsupported + +#define KWALLET_HASH_SHA1 0 +#define KWALLET_HASH_MD5 1 // unsupported + + +using namespace KWallet; + +#define KWMAGIC "KWALLET\n\r\0\r\n" +#define KWMAGIC_LEN 12 + +static void initKWalletDir() +{ + KGlobal::dirs()->addResourceType("kwallet", "share/apps/kwallet"); +} + +Backend::Backend(const QString& name, bool isPath) : _name(name), _ref(0) { + initKWalletDir(); + if (isPath) { + _path = name; + } else { + _path = KGlobal::dirs()->saveLocation("kwallet") + "/" + _name + ".kwl"; + } + + _open = false; +} + + +Backend::~Backend() { + if (_open) { + close(); + } +} + + +int Backend::close() { + for (FolderMap::ConstIterator i = _entries.begin(); i != _entries.end(); ++i) { + for (EntryMap::ConstIterator j = i.data().begin(); j != i.data().end(); ++j) { + delete j.data(); + } + } + _entries.clear(); + +return 0; +} + + +static int getRandomBlock(QByteArray& randBlock) { + // First try /dev/urandom + if (QFile::exists("/dev/urandom")) { + QFile devrand("/dev/urandom"); + if (devrand.open(IO_ReadOnly)) { + unsigned int rc = devrand.readBlock(randBlock.data(), randBlock.size()); + + if (rc != randBlock.size()) { + return -3; // not enough data read + } + + return 0; + } + } + + // If that failed, try /dev/random + // FIXME: open in noblocking mode! + if (QFile::exists("/dev/random")) { + QFile devrand("/dev/random"); + if (devrand.open(IO_ReadOnly)) { + unsigned int rc = 0; + unsigned int cnt = 0; + + do { + int rc2 = devrand.readBlock(randBlock.data() + rc, randBlock.size()); + + if (rc2 < 0) { + return -3; // read error + } + + rc += rc2; + cnt++; + if (cnt > randBlock.size()) { + return -4; // reading forever?! + } + } while(rc < randBlock.size()); + + return 0; + } + } + + // EGD method + char *randFilename; + if ((randFilename = getenv("RANDFILE"))) { + if (QFile::exists(randFilename)) { + QFile devrand(randFilename); + if (devrand.open(IO_ReadOnly)) { + unsigned int rc = devrand.readBlock(randBlock.data(), randBlock.size()); + if (rc != randBlock.size()) { + return -3; // not enough data read + } + return 0; + } + } + } + + // Couldn't get any random data!! + + return -1; +} + + +// this should be SHA-512 for release probably +static int password2hash(const QByteArray& password, QByteArray& hash) { + SHA1 sha; + int shasz = sha.size() / 8; + + assert(shasz >= 20); + + QByteArray block1(shasz); + + sha.process(password.data(), QMIN(password.size(), 16)); + + // To make brute force take longer + for (int i = 0; i < 2000; i++) { + memcpy(block1.data(), sha.hash(), shasz); + sha.reset(); + sha.process(block1.data(), shasz); + } + + sha.reset(); + + if (password.size() > 16) { + sha.process(password.data() + 16, QMIN(password.size() - 16, 16)); + QByteArray block2(shasz); + // To make brute force take longer + for (int i = 0; i < 2000; i++) { + memcpy(block2.data(), sha.hash(), shasz); + sha.reset(); + sha.process(block2.data(), shasz); + } + + sha.reset(); + + if (password.size() > 32) { + sha.process(password.data() + 32, QMIN(password.size() - 32, 16)); + + QByteArray block3(shasz); + // To make brute force take longer + for (int i = 0; i < 2000; i++) { + memcpy(block3.data(), sha.hash(), shasz); + sha.reset(); + sha.process(block3.data(), shasz); + } + + sha.reset(); + + if (password.size() > 48) { + sha.process(password.data() + 48, password.size() - 48); + + QByteArray block4(shasz); + // To make brute force take longer + for (int i = 0; i < 2000; i++) { + memcpy(block4.data(), sha.hash(), shasz); + sha.reset(); + sha.process(block4.data(), shasz); + } + + sha.reset(); + // split 14/14/14/14 + hash.resize(56); + memcpy(hash.data(), block1.data(), 14); + memcpy(hash.data() + 14, block2.data(), 14); + memcpy(hash.data() + 28, block3.data(), 14); + memcpy(hash.data() + 42, block4.data(), 14); + block4.fill(0); + } else { + // split 20/20/16 + hash.resize(56); + memcpy(hash.data(), block1.data(), 20); + memcpy(hash.data() + 20, block2.data(), 20); + memcpy(hash.data() + 40, block3.data(), 16); + } + block3.fill(0); + } else { + // split 20/20 + hash.resize(40); + memcpy(hash.data(), block1.data(), 20); + memcpy(hash.data() + 20, block2.data(), 20); + } + block2.fill(0); + } else { + // entirely block1 + hash.resize(20); + memcpy(hash.data(), block1.data(), 20); + } + + block1.fill(0); + + return 0; +} + + +bool Backend::exists(const QString& wallet) { + initKWalletDir(); + QString path = KGlobal::dirs()->saveLocation("kwallet") + "/" + wallet + ".kwl"; + // Note: 60 bytes is presently the minimum size of a wallet file. + // Anything smaller is junk. +return QFile::exists(path) && QFileInfo(path).size() >= 60; +} + + +QString Backend::openRCToString(int rc) { + switch (rc) { + case -255: + return i18n("Already open."); + case -2: + return i18n("Error opening file."); + case -3: + return i18n("Not a wallet file."); + case -4: + return i18n("Unsupported file format revision."); + case -42: + return i18n("Unknown encryption scheme."); + case -43: + return i18n("Corrupt file?"); + case -8: + return i18n("Error validating wallet integrity. Possibly corrupted."); + case -5: + case -7: + case -9: + return i18n("Read error - possibly incorrect password."); + case -6: + return i18n("Decryption error."); + default: + return QString::null; + } +} + + +int Backend::open(const QByteArray& password) { + + if (_open) { + return -255; // already open + } + + QByteArray passhash; + + // No wallet existed. Let's create it. + // Note: 60 bytes is presently the minimum size of a wallet file. + // Anything smaller is junk and should be deleted. + if (!QFile::exists(_path) || QFileInfo(_path).size() < 60) { + QFile newfile(_path); + if (!newfile.open(IO_ReadWrite)) { + return -2; // error opening file + } + newfile.close(); + _open = true; + sync(password); + return 1; // new file opened, but OK + } + + QFile db(_path); + + if (!db.open(IO_ReadOnly)) { + return -2; // error opening file + } + + char magicBuf[KWMAGIC_LEN]; + db.readBlock(magicBuf, KWMAGIC_LEN); + if (memcmp(magicBuf, KWMAGIC, KWMAGIC_LEN) != 0) { + return -3; // bad magic + } + + db.readBlock(magicBuf, 4); + + // First byte is major version, second byte is minor version + if (magicBuf[0] != KWALLET_VERSION_MAJOR) { + return -4; // unknown version + } + + if (magicBuf[1] != KWALLET_VERSION_MINOR) { + return -4; // unknown version + } + + if (magicBuf[2] != KWALLET_CIPHER_BLOWFISH_CBC) { + return -42; // unknown cipher + } + + if (magicBuf[3] != KWALLET_HASH_SHA1) { + return -42; // unknown hash + } + + _hashes.clear(); + // Read in the hashes + QDataStream hds(&db); + Q_UINT32 n; + hds >> n; + if (n > 0xffff) { // sanity check + return -43; + } + + for (size_t i = 0; i < n; ++i) { + KMD5::Digest d, d2; // judgment day + MD5Digest ba; + QMap<MD5Digest,QValueList<MD5Digest> >::iterator it; + Q_UINT32 fsz; + if (hds.atEnd()) return -43; + hds.readRawBytes(reinterpret_cast<char *>(d), 16); + hds >> fsz; + ba.duplicate(reinterpret_cast<char *>(d), 16); + it = _hashes.insert(ba, QValueList<MD5Digest>()); + for (size_t j = 0; j < fsz; ++j) { + hds.readRawBytes(reinterpret_cast<char *>(d2), 16); + ba.duplicate(reinterpret_cast<char *>(d2), 16); + (*it).append(ba); + } + } + + // Read in the rest of the file. + QByteArray encrypted = db.readAll(); + assert(encrypted.size() < db.size()); + + BlowFish _bf; + CipherBlockChain bf(&_bf); + int blksz = bf.blockSize(); + if ((encrypted.size() % blksz) != 0) { + return -5; // invalid file structure + } + + // Decrypt the encrypted data + passhash.resize(bf.keyLen()/8); + password2hash(password, passhash); + + bf.setKey((void *)passhash.data(), passhash.size()*8); + + if (!encrypted.data()) { + passhash.fill(0); + encrypted.fill(0); + return -7; // file structure error + } + + int rc = bf.decrypt(encrypted.data(), encrypted.size()); + if (rc < 0) { + passhash.fill(0); + encrypted.fill(0); + return -6; // decrypt error + } + + passhash.fill(0); // passhash is UNUSABLE NOW + + const char *t = encrypted.data(); + + // strip the leading data + t += blksz; // one block of random data + + // strip the file size off + long fsize = 0; + + fsize |= (long(*t) << 24) & 0xff000000; + t++; + fsize |= (long(*t) << 16) & 0x00ff0000; + t++; + fsize |= (long(*t) << 8) & 0x0000ff00; + t++; + fsize |= long(*t) & 0x000000ff; + t++; + + if (fsize < 0 || fsize > long(encrypted.size()) - blksz - 4) { + //kdDebug() << "fsize: " << fsize << " encrypted.size(): " << encrypted.size() << " blksz: " << blksz << endl; + encrypted.fill(0); + return -9; // file structure error. + } + + // compute the hash ourself + SHA1 sha; + sha.process(t, fsize); + const char *testhash = (const char *)sha.hash(); + + // compare hashes + int sz = encrypted.size(); + for (int i = 0; i < 20; i++) { + if (testhash[i] != encrypted[sz - 20 + i]) { + encrypted.fill(0); + sha.reset(); + return -8; // hash error. + } + } + + sha.reset(); + + // chop off the leading blksz+4 bytes + QByteArray tmpenc; + tmpenc.duplicate(encrypted.data()+blksz+4, fsize); + encrypted.fill(0); + encrypted.duplicate(tmpenc.data(), tmpenc.size()); + tmpenc.fill(0); + + // Load the data structures up + QDataStream eStream(encrypted, IO_ReadOnly); + + while (!eStream.atEnd()) { + QString folder; + Q_UINT32 n; + + eStream >> folder; + eStream >> n; + + // Force initialisation + _entries[folder].clear(); + + for (size_t i = 0; i < n; i++) { + QString key; + KWallet::Wallet::EntryType et = KWallet::Wallet::Unknown; + Entry *e = new Entry; + eStream >> key; + Q_INT32 x = 0; // necessary to read properly + eStream >> x; + et = static_cast<KWallet::Wallet::EntryType>(x); + + switch (et) { + case KWallet::Wallet::Password: + case KWallet::Wallet::Stream: + case KWallet::Wallet::Map: + break; + default: // Unknown entry + delete e; + continue; + } + + QByteArray a; + eStream >> a; + e->setValue(a); + e->setType(et); + e->setKey(key); + _entries[folder][key] = e; + } + } + + _open = true; + return 0; +} + + +int Backend::sync(const QByteArray& password) { + if (!_open) { + return -255; // not open yet + } + + KSaveFile sf(_path, 0600); + QFile *qf = sf.file(); + + if (!qf) { + sf.abort(); + return -1; // error opening file + } + + qf->writeBlock(KWMAGIC, KWMAGIC_LEN); + + // Write the version number + QByteArray version(4); + version[0] = KWALLET_VERSION_MAJOR; + version[1] = KWALLET_VERSION_MINOR; + version[2] = KWALLET_CIPHER_BLOWFISH_CBC; + version[3] = KWALLET_HASH_SHA1; + qf->writeBlock(version, 4); + + // Holds the hashes we write out + QByteArray hashes; + QDataStream hashStream(hashes, IO_WriteOnly); + KMD5 md5; + hashStream << static_cast<Q_UINT32>(_entries.count()); + + // Holds decrypted data prior to encryption + QByteArray decrypted; + + // FIXME: we should estimate the amount of data we will write in each + // buffer and resize them approximately in order to avoid extra + // resizes. + + // populate decrypted + QDataStream dStream(decrypted, IO_WriteOnly); + for (FolderMap::ConstIterator i = _entries.begin(); i != _entries.end(); ++i) { + dStream << i.key(); + dStream << static_cast<Q_UINT32>(i.data().count()); + + md5.reset(); + md5.update(i.key().utf8()); + hashStream.writeRawBytes(reinterpret_cast<const char*>(&(md5.rawDigest()[0])), 16); + hashStream << static_cast<Q_UINT32>(i.data().count()); + + for (EntryMap::ConstIterator j = i.data().begin(); j != i.data().end(); ++j) { + dStream << j.key(); + dStream << static_cast<Q_INT32>(j.data()->type()); + dStream << j.data()->value(); + + md5.reset(); + md5.update(j.key().utf8()); + hashStream.writeRawBytes(reinterpret_cast<const char*>(&(md5.rawDigest()[0])), 16); + } + } + + qf->writeBlock(hashes, hashes.size()); + + // calculate the hash of the file + SHA1 sha; + BlowFish _bf; + CipherBlockChain bf(&_bf); + + sha.process(decrypted.data(), decrypted.size()); + + // prepend and append the random data + QByteArray wholeFile; + long blksz = bf.blockSize(); + long newsize = decrypted.size() + + blksz + // encrypted block + 4 + // file size + 20; // size of the SHA hash + + int delta = (blksz - (newsize % blksz)); + newsize += delta; + wholeFile.resize(newsize); + + QByteArray randBlock; + randBlock.resize(blksz+delta); + if (getRandomBlock(randBlock) < 0) { + sha.reset(); + decrypted.fill(0); + sf.abort(); + return -3; // Fatal error: can't get random + } + + for (int i = 0; i < blksz; i++) { + wholeFile[i] = randBlock[i]; + } + + for (int i = 0; i < 4; i++) { + wholeFile[(int)(i+blksz)] = (decrypted.size() >> 8*(3-i))&0xff; + } + + for (unsigned int i = 0; i < decrypted.size(); i++) { + wholeFile[(int)(i+blksz+4)] = decrypted[i]; + } + + for (int i = 0; i < delta; i++) { + wholeFile[(int)(i+blksz+4+decrypted.size())] = randBlock[(int)(i+blksz)]; + } + + const char *hash = (const char *)sha.hash(); + for (int i = 0; i < 20; i++) { + wholeFile[(int)(newsize - 20 + i)] = hash[i]; + } + + sha.reset(); + decrypted.fill(0); + + // hash the passphrase + QByteArray passhash; + password2hash(password, passhash); + + // encrypt the data + if (!bf.setKey(passhash.data(), passhash.size() * 8)) { + passhash.fill(0); + wholeFile.fill(0); + sf.abort(); + return -2; + } + + int rc = bf.encrypt(wholeFile.data(), wholeFile.size()); + if (rc < 0) { + passhash.fill(0); + wholeFile.fill(0); + sf.abort(); + return -2; // encrypt error + } + + passhash.fill(0); // passhash is UNUSABLE NOW + + // write the file + qf->writeBlock(wholeFile, wholeFile.size()); + if (!sf.close()) { + wholeFile.fill(0); + sf.abort(); + return -4; // write error + } + + wholeFile.fill(0); + +return 0; +} + + +int Backend::close(const QByteArray& password) { + int rc = sync(password); + _open = false; + if (rc != 0) { + return rc; + } + return close(); +} + + +const QString& Backend::walletName() const { + return _name; +} + + +bool Backend::isOpen() const { + return _open; +} + + +QStringList Backend::folderList() const { + return _entries.keys(); +} + + +QStringList Backend::entryList() const { + return _entries[_folder].keys(); +} + + +Entry *Backend::readEntry(const QString& key) { +Entry *rc = 0L; + + if (_open && hasEntry(key)) { + rc = _entries[_folder][key]; + } + +return rc; +} + + +QPtrList<Entry> Backend::readEntryList(const QString& key) { + QPtrList<Entry> rc; + + if (!_open) { + return rc; + } + + QRegExp re(key, true, true); + + const EntryMap& map = _entries[_folder]; + for (EntryMap::ConstIterator i = map.begin(); i != map.end(); ++i) { + if (re.exactMatch(i.key())) { + rc.append(i.data()); + } + } + return rc; +} + + +bool Backend::createFolder(const QString& f) { + if (_entries.contains(f)) { + return false; + } + + _entries.insert(f, EntryMap()); + + KMD5 folderMd5; + folderMd5.update(f.utf8()); + _hashes.insert(MD5Digest(folderMd5.rawDigest()), QValueList<MD5Digest>()); + +return true; +} + + +int Backend::renameEntry(const QString& oldName, const QString& newName) { +EntryMap& emap = _entries[_folder]; +EntryMap::Iterator oi = emap.find(oldName); +EntryMap::Iterator ni = emap.find(newName); + + if (oi != emap.end() && ni == emap.end()) { + Entry *e = oi.data(); + emap.remove(oi); + emap[newName] = e; + + KMD5 folderMd5; + folderMd5.update(_folder.utf8()); + + HashMap::iterator i = _hashes.find(MD5Digest(folderMd5.rawDigest())); + if (i != _hashes.end()) { + KMD5 oldMd5, newMd5; + oldMd5.update(oldName.utf8()); + newMd5.update(newName.utf8()); + i.data().remove(MD5Digest(oldMd5.rawDigest())); + i.data().append(MD5Digest(newMd5.rawDigest())); + } + return 0; + } + +return -1; +} + + +void Backend::writeEntry(Entry *e) { + if (!_open) + return; + + if (!hasEntry(e->key())) { + _entries[_folder][e->key()] = new Entry; + } + _entries[_folder][e->key()]->copy(e); + + KMD5 folderMd5; + folderMd5.update(_folder.utf8()); + + HashMap::iterator i = _hashes.find(MD5Digest(folderMd5.rawDigest())); + if (i != _hashes.end()) { + KMD5 md5; + md5.update(e->key().utf8()); + i.data().append(MD5Digest(md5.rawDigest())); + } +} + + +bool Backend::hasEntry(const QString& key) const { + return _entries.contains(_folder) && _entries[_folder].contains(key); +} + + +bool Backend::removeEntry(const QString& key) { + if (!_open) { + return false; + } + + FolderMap::Iterator fi = _entries.find(_folder); + EntryMap::Iterator ei = fi.data().find(key); + + if (fi != _entries.end() && ei != fi.data().end()) { + delete ei.data(); + fi.data().remove(ei); + KMD5 folderMd5; + folderMd5.update(_folder.utf8()); + + HashMap::iterator i = _hashes.find(MD5Digest(folderMd5.rawDigest())); + if (i != _hashes.end()) { + KMD5 md5; + md5.update(key.utf8()); + i.data().remove(MD5Digest(md5.rawDigest())); + } + return true; + } + +return false; +} + + +bool Backend::removeFolder(const QString& f) { + if (!_open) { + return false; + } + + FolderMap::Iterator fi = _entries.find(f); + + if (fi != _entries.end()) { + if (_folder == f) { + _folder = QString::null; + } + + for (EntryMap::Iterator ei = fi.data().begin(); ei != fi.data().end(); ++ei) { + delete ei.data(); + } + + _entries.remove(fi); + + KMD5 folderMd5; + folderMd5.update(f.utf8()); + _hashes.erase(MD5Digest(folderMd5.rawDigest())); + return true; + } + +return false; +} + + +bool Backend::folderDoesNotExist(const QString& folder) const { + KMD5 md5; + md5.update(folder.utf8()); + return !_hashes.contains(MD5Digest(md5.rawDigest())); +} + + +bool Backend::entryDoesNotExist(const QString& folder, const QString& entry) const { + KMD5 md5; + md5.update(folder.utf8()); + HashMap::const_iterator i = _hashes.find(MD5Digest(md5.rawDigest())); + if (i != _hashes.end()) { + md5.reset(); + md5.update(entry.utf8()); + return !i.data().contains(MD5Digest(md5.rawDigest())); + } + return true; +} + + |