kde-workspace/kwalletd/backend/kwalletbackend.cc
Ivailo Monev 8b98cd5bca kwalletd: rebase KMD5 QCryptographicHash transition on upstream
not really sure what's going on but I suspect that the
pre-allocated 16 characters and the custom MD5Digest are making
things magically work because when the data was read and written
is was 16 long as it is supposed to be.

Signed-off-by: Ivailo Monev <xakepa10@gmail.com>
2016-04-01 09:27:21 +00:00

681 lines
16 KiB
C++

/* 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 <ksavefile.h>
#include <kstandarddirs.h>
#ifdef HAVE_QGPGME
#include <gpgme++/key.h>
#endif
#include <gcrypt.h>
#include <knotification.h>
#include <QtCore/QFile>
#include <QtCore/QFileInfo>
#include <QtCore/QRegExp>
#include <QtCore/QCryptographicHash>
#include "blowfish.h"
#include "sha1.h"
#include "cbc.h"
#include <assert.h>
// quick fix to get random numbers on win32
#define KWALLET_VERSION_MAJOR 0
#define KWALLET_VERSION_MINOR 1
using namespace KWallet;
#define KWMAGIC "KWALLET\n\r\0\r\n"
class Backend::BackendPrivate
{
};
static void initKWalletDir()
{
KGlobal::dirs()->addResourceType("kwallet", 0, "share/apps/kwallet");
}
Backend::Backend(const QString& name, bool isPath)
: d(0)
, _name(name)
, _useNewHash(false)
, _ref(0)
, _cipherType(KWallet::BACKEND_CIPHER_UNKNOWN)
{
initKWalletDir();
if (isPath) {
_path = name;
} else {
_path = KGlobal::dirs()->saveLocation("kwallet") + _name + ".kwl";
}
_open = false;
}
Backend::~Backend() {
if (_open) {
close();
}
delete d;
}
void Backend::setCipherType(BackendCipherType ct)
{
// changing cipher type on already initialed wallets is not permitted
assert(_cipherType == KWallet::BACKEND_CIPHER_UNKNOWN);
_cipherType = ct;
}
static int password2PBKDF2_SHA512(const QByteArray &password, QByteArray& hash, const QByteArray &salt)
{
if (!gcry_check_version("1.5.0")) {
printf("libcrypt version is too old \n");
return GPG_ERR_USER_2;
}
gcry_error_t error;
bool static gcry_secmem_init = false;
if (!gcry_secmem_init) {
error = gcry_control(GCRYCTL_INIT_SECMEM, 32768, 0);
if (error != 0) {
kWarning() << "Can't get secure memory:" << error;
return error;
}
gcry_secmem_init = true;
}
gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0);
error = gcry_kdf_derive(password.constData(), password.size(),
GCRY_KDF_PBKDF2, GCRY_MD_SHA512,
salt.data(), salt.size(),
PBKDF2_SHA512_ITERATIONS, PBKDF2_SHA512_KEYSIZE, hash.data());
return error;
}
// 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, 0);
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, 0);
// 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, 0);
// 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, 0);
// 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;
}
int Backend::deref() {
if (--_ref < 0) {
kDebug() << "refCount negative!";
_ref = 0;
}
return _ref;
}
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();
}
}
int Backend::open(const QByteArray& password, WId w) {
if (_open) {
return -255; // already open
}
setPassword(password);
return openInternal(w);
}
#ifdef HAVE_QGPGME
int Backend::open(const GpgME::Key& key)
{
if (_open) {
return -255; // already open
}
_gpgKey = key;
return openInternal();
}
#endif // HAVE_QGPGME
int Backend::openPreHashed(const QByteArray &passwordHash)
{
if (_open) {
return -255; // already open
}
// check the password hash for correct size (currently fixed)
if (passwordHash.size() != 20 && passwordHash.size() != 40 &&
passwordHash.size() != 56) {
return -42; // unsupported encryption scheme
}
_passhash = passwordHash;
_newPassHash = passwordHash;
_useNewHash = true;//Only new hash is supported
return openInternal();
}
int Backend::openInternal(WId w)
{
// 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(QIODevice::ReadWrite)) {
return -2; // error opening file
}
newfile.close();
_open = true;
if (sync(w) != 0) {
return -2;
}
}
QFile db(_path);
if (!db.open(QIODevice::ReadOnly)) {
return -2; // error opening file
}
char magicBuf[KWMAGIC_LEN];
db.read(magicBuf, KWMAGIC_LEN);
if (memcmp(magicBuf, KWMAGIC, KWMAGIC_LEN) != 0) {
return -3; // bad magic
}
db.read(magicBuf, 4);
// First byte is major version, second byte is minor version
if (magicBuf[0] != KWALLET_VERSION_MAJOR) {
return -4; // unknown version
}
//0 has been the MINOR version until 4.13, from that point we use it to upgrade the hash
if (magicBuf[1] == 1) {
kDebug() << "Wallet new enough, using new hash";
swapToNewHash();
} else if (magicBuf[1] != 0){
kDebug() << "Wallet is old, sad panda :(";
return -4; // unknown version
}
BackendPersistHandler *phandler = BackendPersistHandler::getPersistHandler(magicBuf);
if (0 == phandler){
return 42; // unknown cipher or hash
}
return phandler->read(this, db, w);
}
void Backend::swapToNewHash()
{
//Runtime error happened and we can't use the new hash
if (!_useNewHash) {
kDebug() << "Runtime error on the new hash";
return;
}
_passhash.fill(0);//Making sure the old passhash is not around in memory
_passhash = _newPassHash;//Use the new hash, means the wallet is modern enough
}
QByteArray Backend::createAndSaveSalt(const QString& path) const
{
QFile saltFile(path);
saltFile.remove();
if (!saltFile.open(QIODevice::WriteOnly)) {
return QByteArray();
}
char *randomData = (char*) gcry_random_bytes(PBKDF2_SHA512_SALTSIZE, GCRY_STRONG_RANDOM);
QByteArray salt(randomData, PBKDF2_SHA512_SALTSIZE);
free(randomData);
if (saltFile.write(salt) != PBKDF2_SHA512_SALTSIZE) {
return QByteArray();
}
saltFile.close();
return salt;
}
int Backend::sync(WId w) {
if (!_open) {
return -255; // not open yet
}
KSaveFile sf(_path);
if (!sf.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) {
return -1; // error opening file
}
sf.setPermissions(QFile::ReadUser|QFile::WriteUser);
if (sf.write(KWMAGIC, KWMAGIC_LEN) != KWMAGIC_LEN) {
sf.abort();
return -4; // write error
}
// Write the version number
QByteArray version(4, 0);
version[0] = KWALLET_VERSION_MAJOR;
if (_useNewHash) {
version[1] = KWALLET_VERSION_MINOR;
//Use the sync to update the hash to PBKDF2_SHA512
swapToNewHash();
} else {
version[1] = 0; //was KWALLET_VERSION_MINOR before the new hash
}
BackendPersistHandler *phandler = BackendPersistHandler::getPersistHandler(_cipherType);
if (0 == phandler) {
return -4; // write error
}
int rc = phandler->write(this, sf, version, w);
if (rc<0) {
// Oops! wallet file sync filed! Display a notification about that
// TODO: change kwalletd status flags, when status flags will be implemented
KNotification *notification = new KNotification( "syncFailed" );
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").arg(_name).arg(rc).arg(sf.errorString()) );
notification->sendEvent();
}
return rc;
}
int Backend::close(bool save) {
// save if requested
if (save) {
int rc = sync(0);
if (rc != 0) {
return rc;
}
}
// do the actual close
for (FolderMap::ConstIterator i = _entries.constBegin(); i != _entries.constEnd(); ++i) {
for (EntryMap::ConstIterator j = i.value().constBegin(); j != i.value().constEnd(); ++j) {
delete j.value();
}
}
_entries.clear();
// empty the password hash
_passhash.fill(0);
_newPassHash.fill(0);
_open = false;
return 0;
}
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;
}
QList<Entry*> Backend::readEntryList(const QString& key) {
QList<Entry*> rc;
if (!_open) {
return rc;
}
QRegExp re(key, Qt::CaseSensitive, QRegExp::Wildcard);
const EntryMap& map = _entries[_folder];
for (EntryMap::ConstIterator i = map.begin(); i != map.end(); ++i) {
if (re.exactMatch(i.key())) {
rc.append(i.value());
}
}
return rc;
}
bool Backend::createFolder(const QString& f) {
if (_entries.contains(f)) {
return false;
}
_entries.insert(f, EntryMap());
QCryptographicHash folderMd5(QCryptographicHash::Md5);
folderMd5.addData(f.toUtf8());
_hashes.insert(MD5Digest(folderMd5.result()), QList<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.value();
emap.erase(oi);
emap[newName] = e;
QCryptographicHash folderMd5(QCryptographicHash::Md5);
folderMd5.addData(_folder.toUtf8());
HashMap::iterator i = _hashes.find(MD5Digest(folderMd5.result()));
if (i != _hashes.end()) {
QCryptographicHash oldMd5(QCryptographicHash::Md5), newMd5(QCryptographicHash::Md5);
oldMd5.addData(oldName.toUtf8());
newMd5.addData(newName.toUtf8());
i.value().removeAll(MD5Digest(oldMd5.result()));
i.value().append(MD5Digest(newMd5.result()));
}
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);
QCryptographicHash folderMd5(QCryptographicHash::Md5);
folderMd5.addData(_folder.toUtf8());
HashMap::iterator i = _hashes.find(MD5Digest(folderMd5.result()));
if (i != _hashes.end()) {
QCryptographicHash md5(QCryptographicHash::Md5);
md5.addData(e->key().toUtf8());
i.value().append(MD5Digest(md5.result()));
}
}
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.value().find(key);
if (fi != _entries.end() && ei != fi.value().end()) {
delete ei.value();
fi.value().erase(ei);
QCryptographicHash folderMd5(QCryptographicHash::Md5);
folderMd5.addData(_folder.toUtf8());
HashMap::iterator i = _hashes.find(MD5Digest(folderMd5.result()));
if (i != _hashes.end()) {
QCryptographicHash md5(QCryptographicHash::Md5);
md5.addData(key.toUtf8());
i.value().removeAll(MD5Digest(md5.result()));
}
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.clear();
}
for (EntryMap::Iterator ei = fi.value().begin(); ei != fi.value().end(); ++ei) {
delete ei.value();
}
_entries.erase(fi);
QCryptographicHash folderMd5(QCryptographicHash::Md5);
folderMd5.addData(f.toUtf8());
_hashes.remove(MD5Digest(folderMd5.result()));
return true;
}
return false;
}
bool Backend::folderDoesNotExist(const QString& folder) const {
QCryptographicHash md5(QCryptographicHash::Md5);
md5.addData(folder.toUtf8());
return !_hashes.contains(MD5Digest(md5.result()));
}
bool Backend::entryDoesNotExist(const QString& folder, const QString& entry) const {
QCryptographicHash md5(QCryptographicHash::Md5);
md5.addData(folder.toUtf8());
HashMap::const_iterator i = _hashes.find(MD5Digest(md5.result()));
if (i != _hashes.end()) {
md5.reset();
md5.addData(entry.toUtf8());
return !i.value().contains(MD5Digest(md5.result()));
}
return true;
}
void Backend::setPassword(const QByteArray &password) {
_passhash.fill(0); // empty just in case
BlowFish _bf;
CipherBlockChain bf(&_bf);
_passhash.resize(bf.keyLen()/8);
_newPassHash.resize(bf.keyLen()/8);
_newPassHash.fill(0);
password2hash(password, _passhash);
QByteArray salt;
QFile saltFile(KGlobal::dirs()->saveLocation("kwallet") + _name + ".salt");
if (!saltFile.exists() || saltFile.size() == 0) {
salt = createAndSaveSalt(saltFile.fileName());
} else {
if (!saltFile.open(QIODevice::ReadOnly)) {
salt = createAndSaveSalt(saltFile.fileName());
} else {
salt = saltFile.readAll();
}
}
if (!salt.isEmpty() && password2PBKDF2_SHA512(password, _newPassHash, salt) == 0) {
kDebug() << "Setting useNewHash to true";
_useNewHash = true;
}
}
#ifdef HAVE_QGPGME
const GpgME::Key &Backend::gpgKey() const {
return _gpgKey;
}
#endif