mirror of
https://bitbucket.org/smil3y/kde-workspace.git
synced 2025-02-24 19:02:51 +00:00
569 lines
15 KiB
C++
569 lines
15 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>
|
|
#include <knotification.h>
|
|
|
|
#include <QtCore/QFile>
|
|
#include <QtCore/QFileInfo>
|
|
#include <QtCore/QRegExp>
|
|
#include <QCryptographicHash>
|
|
|
|
#include "blowfish.h"
|
|
#include "cbc.h"
|
|
|
|
#include <gcrypt.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;
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
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);
|
|
_newPassHash.resize(bf.keyLen()/8);
|
|
_newPassHash.fill(0);
|
|
|
|
// this should be SHA-512 for release probably
|
|
_passhash = QCryptographicHash::hash(password, QCryptographicHash::Sha1);
|
|
|
|
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;
|
|
}
|
|
}
|