/** * This file is part of the KDE project * Copyright (C) 2013 Valentin Rusu * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Library General Public * License version 2 as published by the Free Software Foundation. * * 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 #include #include #include #include #include #include #include #ifdef HAVE_QGPGME #include #include #include #include #include #include #include #endif #include "backendpersisthandler.h" #include "kwalletbackend.h" #include "blowfish.h" #include "sha1.h" #include "cbc.h" #define KWALLET_CIPHER_BLOWFISH_ECB 0 // this was the old KWALLET_CIPHER_BLOWFISH_CBC #define KWALLET_CIPHER_3DES_CBC 1 // unsupported #define KWALLET_CIPHER_GPG 2 #define KWALLET_CIPHER_BLOWFISH_CBC 3 #define KWALLET_HASH_SHA1 0 #define KWALLET_HASH_MD5 1 // unsupported #define KWALLET_HASH_PBKDF2_SHA512 2 // used when using kwallet with pam or since 4.13 version namespace KWallet { static int getRandomBlock(QByteArray& randBlock) { // First try /dev/urandom if (QFile::exists("/dev/urandom")) { QFile devrand("/dev/urandom"); if (devrand.open(QIODevice::ReadOnly)) { int rc = devrand.read(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(QIODevice::ReadOnly)) { int rc = 0; int cnt = 0; do { int rc2 = devrand.read(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 QString randFilename = QString::fromLocal8Bit(qgetenv("RANDFILE")); if (!randFilename.isEmpty()) { if (QFile::exists(randFilename)) { QFile devrand(randFilename); if (devrand.open(QIODevice::ReadOnly)) { int rc = devrand.read(randBlock.data(), randBlock.size()); if (rc != randBlock.size()) { return -3; // not enough data read } return 0; } } } // Couldn't get any random data!! return -1; } static BlowfishPersistHandler *blowfishHandler =0; #ifdef HAVE_QGPGME static GpgPersistHandler *gpgHandler =0; #endif // HAVE_QGPGME BackendPersistHandler *BackendPersistHandler::getPersistHandler(BackendCipherType cipherType) { switch (cipherType){ case BACKEND_CIPHER_BLOWFISH: if (0 == blowfishHandler) blowfishHandler = new BlowfishPersistHandler; return blowfishHandler; #ifdef HAVE_QGPGME case BACKEND_CIPHER_GPG: if (0 == gpgHandler) gpgHandler = new GpgPersistHandler; return gpgHandler; #endif // HAVE_QGPGME default: Q_ASSERT(0); return 0; } } BackendPersistHandler *BackendPersistHandler::getPersistHandler(char magicBuf[12]) { if ((magicBuf[2] == KWALLET_CIPHER_BLOWFISH_ECB || magicBuf[2] == KWALLET_CIPHER_BLOWFISH_CBC) && (magicBuf[3] == KWALLET_HASH_SHA1 || magicBuf[3] == KWALLET_HASH_PBKDF2_SHA512)) { if (0 == blowfishHandler) { bool useECBforReading = magicBuf[2] == KWALLET_CIPHER_BLOWFISH_ECB; if (useECBforReading) { qDebug() << "this wallet uses ECB encryption. It'll be converted to CBC on next save."; } blowfishHandler = new BlowfishPersistHandler(useECBforReading); } return blowfishHandler; } #ifdef HAVE_QGPGME if (magicBuf[2] == KWALLET_CIPHER_GPG && magicBuf[3] == 0) { if (0 == gpgHandler) gpgHandler = new GpgPersistHandler; return gpgHandler; } #endif // HAVE_QGPGME return 0; // unknown cipher or hash } int BlowfishPersistHandler::write(Backend* wb, KSaveFile& sf, QByteArray& version, WId) { assert(wb->_cipherType == BACKEND_CIPHER_BLOWFISH); if (_useECBforReading) { qDebug() << "This wallet used ECB and is now saved using CBC"; _useECBforReading = false; } version[2] = KWALLET_CIPHER_BLOWFISH_CBC; if(!wb->_useNewHash) { version[3] = KWALLET_HASH_SHA1; } else { version[3] = KWALLET_HASH_PBKDF2_SHA512;//Since 4.13 we always use PBKDF2_SHA512 } if (sf.write(version, 4) != 4) { sf.abort(); return -4; // write error } // Holds the hashes we write out QByteArray hashes; QDataStream hashStream(&hashes, QIODevice::WriteOnly); KMD5 md5; hashStream << static_cast(wb->_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, QIODevice::WriteOnly); for (Backend::FolderMap::ConstIterator i = wb->_entries.constBegin(); i != wb->_entries.constEnd(); ++i) { dStream << i.key(); dStream << static_cast(i.value().count()); md5.reset(); md5.update(i.key().toUtf8()); hashStream.writeRawData(reinterpret_cast(&(md5.rawDigest()[0])), 16); hashStream << static_cast(i.value().count()); for (Backend::EntryMap::ConstIterator j = i.value().constBegin(); j != i.value().constEnd(); ++j) { dStream << j.key(); dStream << static_cast(j.value()->type()); dStream << j.value()->value(); md5.reset(); md5.update(j.key().toUtf8()); hashStream.writeRawData(reinterpret_cast(&(md5.rawDigest()[0])), 16); } } if (sf.write(hashes, hashes.size()) != hashes.size()) { sf.abort(); return -4; // write error } // 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 (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); // encrypt the data if (!bf.setKey(wb->_passhash.data(), wb->_passhash.size() * 8)) { wholeFile.fill(0); sf.abort(); return -2; // encrypt error } int rc = bf.encrypt(wholeFile.data(), wholeFile.size()); if (rc < 0) { wholeFile.fill(0); sf.abort(); return -2; // encrypt error } // write the file if (sf.write(wholeFile, wholeFile.size()) != wholeFile.size()) { wholeFile.fill(0); sf.abort(); return -4; // write error } if (!sf.finalize()) { kDebug() << "WARNING: wallet sync to disk failed! KSaveFile status was " << sf.errorString(); wholeFile.fill(0); return -4; // write error } wholeFile.fill(0); return 0; } int BlowfishPersistHandler::read(Backend* wb, QFile& db, WId) { wb->_cipherType = BACKEND_CIPHER_BLOWFISH; wb->_hashes.clear(); // Read in the hashes QDataStream hds(&db); quint32 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 >::iterator it; quint32 fsz; if (hds.atEnd()) return -43; hds.readRawData(reinterpret_cast(d), 16); hds >> fsz; ba = MD5Digest(reinterpret_cast(d)); it = wb->_hashes.insert(ba, QList()); for (size_t j = 0; j < fsz; ++j) { hds.readRawData(reinterpret_cast(d2), 16); ba = MD5Digest(reinterpret_cast(d2)); (*it).append(ba); } } // Read in the rest of the file. QByteArray encrypted = db.readAll(); assert(encrypted.size() < db.size()); BlowFish _bf; CipherBlockChain bf(&_bf, _useECBforReading); int blksz = bf.blockSize(); if ((encrypted.size() % blksz) != 0) { return -5; // invalid file structure } bf.setKey((void *)wb->_passhash.data(), wb->_passhash.size()*8); if (!encrypted.data()) { wb->_passhash.fill(0); encrypted.fill(0); return -7; // file structure error } int rc = bf.decrypt(encrypted.data(), encrypted.size()); if (rc < 0) { wb->_passhash.fill(0); encrypted.fill(0); return -6; // decrypt error } 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) { //kDebug() << "fsize: " << fsize << " encrypted.size(): " << encrypted.size() << " blksz: " << blksz; 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(encrypted.data()+blksz+4, fsize); encrypted = tmpenc; tmpenc.fill(0); // Load the data structures up QDataStream eStream(encrypted); while (!eStream.atEnd()) { QString folder; quint32 n; eStream >> folder; eStream >> n; // Force initialisation wb->_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; qint32 x = 0; // necessary to read properly eStream >> x; et = static_cast(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); wb->_entries[folder][key] = e; } } wb->_open = true; encrypted.fill(0); return 0; } #ifdef HAVE_QGPGME GpgME::Error initGpgME() { GpgME::Error err; static bool alreadyInitialized = false; if (!alreadyInitialized) { GpgME::initializeLibrary(); err = GpgME::checkEngine(GpgME::OpenPGP); if (err){ kDebug() << "OpenPGP not supported!"; } alreadyInitialized = true; } return err; } int GpgPersistHandler::write(Backend* wb, KSaveFile& sf, QByteArray& version, WId w) { version[2] = KWALLET_CIPHER_GPG; version[3] = 0; if (sf.write(version, 4) != 4) { sf.abort(); return -4; // write error } GpgME::Error err = initGpgME(); if (err) { kDebug() << "initGpgME returned " << err.code(); KMessageBox::errorWId( w, i18n("Error when attempting to initialize OpenPGP while attempting to save the wallet %1. Error code is %2. Please fix your system configuration, then try again!", Qt::escape(wb->_name), err.code())); sf.abort(); return -5; } boost::shared_ptr< GpgME::Context > ctx( GpgME::Context::createForProtocol(GpgME::OpenPGP) ); if (0 == ctx) { kDebug() << "Cannot setup OpenPGP context!"; KMessageBox::errorWId(w, i18n("Error when attempting to initialize OpenPGP while attempting to save the wallet %1. Please fix your system configuration, then try again!"), Qt::escape(wb->_name)); return -6; } assert(wb->_cipherType == BACKEND_CIPHER_GPG); QByteArray hashes; QDataStream hashStream(&hashes, QIODevice::WriteOnly); KMD5 md5; hashStream << static_cast(wb->_entries.count()); QByteArray values; QDataStream valueStream(&values, QIODevice::WriteOnly); Backend::FolderMap::ConstIterator i = wb->_entries.constBegin(); Backend::FolderMap::ConstIterator ie = wb->_entries.constEnd(); for ( ; i != ie; ++i) { valueStream << i.key(); valueStream << static_cast(i.value().count()); md5.reset(); md5.update(i.key().toUtf8()); hashStream.writeRawData(reinterpret_cast(&(md5.rawDigest()[0])), 16); hashStream << static_cast(i.value().count()); Backend::EntryMap::ConstIterator j = i.value().constBegin(); Backend::EntryMap::ConstIterator je = i.value().constEnd(); for (; j != je; ++j) { valueStream << j.key(); valueStream << static_cast(j.value()->type()); valueStream << j.value()->value(); md5.reset(); md5.update(j.key().toUtf8()); hashStream.writeRawData(reinterpret_cast(&(md5.rawDigest()[0])), 16); } } QByteArray dataBuffer; QDataStream dataStream(&dataBuffer, QIODevice::WriteOnly); QString keyID(wb->_gpgKey.keyID()); dataStream << keyID; dataStream << hashes; dataStream << values; GpgME::Data decryptedData(dataBuffer.data(), dataBuffer.size(), false); GpgME::Data encryptedData; std::vector< GpgME::Key > keys; keys.push_back(wb->_gpgKey); GpgME::EncryptionResult res = ctx->encrypt(keys, decryptedData, encryptedData, GpgME::Context::None); if (res.error()){ int gpgerr = res.error().code(); KMessageBox::errorWId( w, i18n("Encryption error while attempting to save the wallet %1. Error code is %2 (%3). Please fix your system configuration, then try again!", Qt::escape(wb->_name), gpgerr, gpgme_strerror(gpgerr))); kDebug() << "GpgME encryption error: " << res.error().code(); sf.abort(); return -7; } char buffer[4096]; ssize_t bytes = 0; encryptedData.seek(0, SEEK_SET); while (bytes == encryptedData.read(buffer, sizeof(buffer)/sizeof(buffer[0]))){ if (sf.write(buffer, bytes) != bytes){ KMessageBox::errorWId( w, i18n("File handling error while attempting to save the wallet %1. Error was %2. Please fix your system configuration, then try again!", Qt::escape(wb->_name), sf.errorString())); sf.abort(); return -4; // write error } } return 0; } int GpgPersistHandler::read(Backend* wb, QFile& sf, WId w) { GpgME::Error err = initGpgME(); if (err){ KMessageBox::errorWId( w, i18n("Error when attempting to initialize OpenPGP while attempting to open the wallet %1. Error code is %2. Please fix your system configuration, then try again!", Qt::escape(wb->_name), err.code())); return -1; } wb->_cipherType = BACKEND_CIPHER_GPG; wb->_hashes.clear(); // the remainder of the file is GPG encrypted. Let's decrypt it GpgME::Data encryptedData; char buffer[4096]; ssize_t bytes = 0; while (bytes == sf.read(buffer, sizeof(buffer)/sizeof(buffer[0]))){ encryptedData.write(buffer, bytes); } retry_label: boost::shared_ptr< GpgME::Context > ctx( GpgME::Context::createForProtocol(GpgME::OpenPGP) ); if (0 == ctx) { KMessageBox::errorWId(w, i18n("Error when attempting to initialize OpenPGP while attempting to open the wallet %1. Please fix your system configuration, then try again!", Qt::escape(wb->_name))); kDebug() << "Cannot setup OpenPGP context!"; return -1; } GpgME::Data decryptedData; encryptedData.seek(0, SEEK_SET); GpgME::DecryptionResult res = ctx->decrypt(encryptedData, decryptedData); if (res.error()){ kDebug() << "Error decrypting message: " << res.error().asString() << ", code " << res.error().code() << ", source " << res.error().source(); KGuiItem btnRetry(i18n("Retry")); // FIXME the logic here should be a little more elaborate; a dialog box should be used with "retry", "cancel", but also "troubleshoot" with options to show card status and to kill scdaemon int userChoice = KMessageBox::warningYesNoWId(w, i18n("Error when attempting to decrypt the wallet %1 using GPG. If you're using a SmartCard, please ensure it's inserted then try again.

GPG error was %2
", Qt::escape(wb->_name), res.error().asString()), i18n("kwalletd GPG backend"), btnRetry, KStandardGuiItem::cancel()); if (userChoice == KMessageBox::Yes) { decryptedData.seek(0, SEEK_SET); goto retry_label; } return -1; } decryptedData.seek(0, SEEK_SET); QByteArray dataBuffer; while (bytes == decryptedData.read(buffer, sizeof(buffer)/sizeof(buffer[0]))){ dataBuffer.append(buffer, bytes); } // load the wallet from the decrypted data QDataStream dataStream(dataBuffer); QString keyID; QByteArray hashes; QByteArray values; dataStream >> keyID; dataStream >> hashes; dataStream >> values; // locate the GPG key having the ID found inside the file. This will be needed later, when writing changes to disk. QDataStream fileStream(&sf); fileStream.unsetDevice(); kDebug() << "This wallet was encrypted using GPG key with ID " << keyID; ctx->setKeyListMode(GPGME_KEYLIST_MODE_LOCAL); std::vector< GpgME::Key > keys; int row =0; err = ctx->startKeyListing(); while (!err) { GpgME::Key k = ctx->nextKey(err); if (err) break; if (keyID == k.keyID()){ kDebug() << "The key was found."; wb->_gpgKey = k; break; } } ctx->endKeyListing(); if (wb->_gpgKey.isNull()){ KMessageBox::errorWId(w, i18n("Error when attempting to open the wallet %1. The wallet was encrypted using the GPG Key ID %2 but this key was not found on your system.", Qt::escape(wb->_name), keyID)); return -1; } QDataStream hashStream(hashes); QDataStream valueStream(values); quint32 hashCount; hashStream >> hashCount; if (hashCount > 0xFFFF) { return -43; } quint32 folderCount = hashCount; while (hashCount--){ KMD5::Digest d; hashStream.readRawData(reinterpret_cast(d), 16); quint32 folderSize; hashStream >> folderSize; MD5Digest ba = MD5Digest(reinterpret_cast(d)); QMap >::iterator it = wb->_hashes.insert(ba, QList()); while (folderSize--){ KMD5::Digest d2; hashStream.readRawData(reinterpret_cast(d2), 16); ba = MD5Digest(reinterpret_cast(d2)); (*it).append(ba); } } while (folderCount--){ QString folder; valueStream >> folder; quint32 entryCount; valueStream >> entryCount; wb->_entries[folder].clear(); while (entryCount--){ KWallet::Wallet::EntryType et = KWallet::Wallet::Unknown; Entry *e = new Entry; QString key; valueStream >> key; qint32 x =0; // necessary to read properly valueStream >> x; et = static_cast(x); switch (et) { case KWallet::Wallet::Password: case KWallet::Wallet::Stream: case KWallet::Wallet::Map: break; default: // Unknown entry delete e; continue; } QByteArray a; valueStream >> a; e->setValue(a); e->setType(et); e->setKey(key); wb->_entries[folder][key] = e; } } wb->_open = true; return 0; } #endif // HAVE_QGPGME } // namespace