2010-09-13 17:24:36 -04:00
|
|
|
/*
|
|
|
|
* Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
|
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 2 or (at your option)
|
|
|
|
* version 3 of the License.
|
|
|
|
*
|
|
|
|
* This program 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 General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "KeePass2Reader.h"
|
|
|
|
|
|
|
|
#include <QtCore/QBuffer>
|
|
|
|
#include <QtCore/QFile>
|
|
|
|
#include <QtCore/QIODevice>
|
|
|
|
|
2010-09-25 06:41:00 -04:00
|
|
|
#include "core/Database.h"
|
2011-11-12 06:20:08 -05:00
|
|
|
#include "core/Endian.h"
|
2010-09-13 17:24:36 -04:00
|
|
|
#include "crypto/CryptoHash.h"
|
2010-09-25 06:41:00 -04:00
|
|
|
#include "format/KeePass2.h"
|
2011-07-06 18:15:52 -04:00
|
|
|
#include "format/KeePass2RandomStream.h"
|
2010-09-25 06:41:00 -04:00
|
|
|
#include "format/KeePass2XmlReader.h"
|
2010-09-13 17:24:36 -04:00
|
|
|
#include "streams/HashedBlockStream.h"
|
2010-09-23 16:27:59 -04:00
|
|
|
#include "streams/QtIOCompressor"
|
2010-09-13 17:24:36 -04:00
|
|
|
#include "streams/SymmetricCipherStream.h"
|
|
|
|
|
2011-07-06 14:21:40 -04:00
|
|
|
KeePass2Reader::KeePass2Reader()
|
|
|
|
{
|
|
|
|
m_saveXml = false;
|
|
|
|
}
|
|
|
|
|
2010-09-13 17:24:36 -04:00
|
|
|
Database* KeePass2Reader::readDatabase(QIODevice* device, const CompositeKey& key)
|
|
|
|
{
|
2012-01-06 14:03:13 -05:00
|
|
|
QScopedPointer<Database> db(new Database());
|
|
|
|
m_db = db.data();
|
2010-09-13 17:24:36 -04:00
|
|
|
m_device = device;
|
|
|
|
m_error = false;
|
|
|
|
m_errorStr = QString();
|
|
|
|
m_headerEnd = false;
|
|
|
|
|
|
|
|
bool ok;
|
|
|
|
|
2010-09-25 06:41:00 -04:00
|
|
|
quint32 signature1 = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok);
|
2010-09-13 17:24:36 -04:00
|
|
|
if (!ok || signature1 != KeePass2::SIGNATURE_1) {
|
2012-01-06 14:03:13 -05:00
|
|
|
raiseError(tr("Not a KeePass database."));
|
2010-09-13 17:24:36 -04:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2010-09-25 06:41:00 -04:00
|
|
|
quint32 signature2 = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok);
|
2010-09-13 17:24:36 -04:00
|
|
|
if (!ok || signature2 != KeePass2::SIGNATURE_2) {
|
2012-01-06 14:03:13 -05:00
|
|
|
raiseError(tr("Not a KeePass database."));
|
2010-09-13 17:24:36 -04:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2010-09-25 06:41:00 -04:00
|
|
|
quint32 version = Endian::readUInt32(m_device, KeePass2::BYTEORDER, &ok) & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
2012-04-21 10:45:46 -04:00
|
|
|
quint32 maxVersion = KeePass2::FILE_VERSION & KeePass2::FILE_VERSION_CRITICAL_MASK;
|
|
|
|
if (!ok || (version < KeePass2::FILE_VERSION_MIN) || (version > maxVersion)) {
|
2012-01-06 14:03:13 -05:00
|
|
|
raiseError(tr("Unsupported KeePass database version."));
|
2010-09-13 17:24:36 -04:00
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2012-01-06 14:03:13 -05:00
|
|
|
while (readHeaderField() && !hasError()) {
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
|
2012-05-11 06:39:06 -04:00
|
|
|
// TODO: check if all header fields have been parsed
|
2011-01-13 16:31:17 -05:00
|
|
|
|
2012-04-11 09:57:11 -04:00
|
|
|
m_db->setKey(key, m_transformSeed, false);
|
2010-09-25 06:41:00 -04:00
|
|
|
|
2010-09-13 17:24:36 -04:00
|
|
|
CryptoHash hash(CryptoHash::Sha256);
|
|
|
|
hash.addData(m_masterSeed);
|
2011-06-29 10:39:39 -04:00
|
|
|
hash.addData(m_db->transformedMasterKey());
|
2010-09-13 17:24:36 -04:00
|
|
|
QByteArray finalKey = hash.result();
|
|
|
|
|
2012-04-18 16:08:22 -04:00
|
|
|
SymmetricCipherStream cipherStream(device, SymmetricCipher::Aes256, SymmetricCipher::Cbc,
|
|
|
|
SymmetricCipher::Decrypt, finalKey, m_encryptionIV);
|
2010-09-13 17:24:36 -04:00
|
|
|
cipherStream.open(QIODevice::ReadOnly);
|
|
|
|
|
|
|
|
QByteArray realStart = cipherStream.read(32);
|
|
|
|
|
|
|
|
if (realStart != m_streamStartBytes) {
|
2012-01-06 14:03:13 -05:00
|
|
|
raiseError(tr("Wrong key or database file is corrupt."));
|
2010-09-19 18:16:30 -04:00
|
|
|
return 0;
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
HashedBlockStream hashedStream(&cipherStream);
|
|
|
|
hashedStream.open(QIODevice::ReadOnly);
|
|
|
|
|
2010-09-23 16:27:59 -04:00
|
|
|
QIODevice* xmlDevice;
|
|
|
|
QScopedPointer<QtIOCompressor> ioCompressor;
|
|
|
|
|
2010-09-25 06:41:00 -04:00
|
|
|
if (m_db->compressionAlgo() == Database::CompressionNone) {
|
2010-09-23 16:27:59 -04:00
|
|
|
xmlDevice = &hashedStream;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
ioCompressor.reset(new QtIOCompressor(&hashedStream));
|
|
|
|
ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat);
|
|
|
|
ioCompressor->open(QIODevice::ReadOnly);
|
|
|
|
xmlDevice = ioCompressor.data();
|
|
|
|
}
|
|
|
|
|
2011-07-06 18:15:52 -04:00
|
|
|
KeePass2RandomStream randomStream(m_protectedStreamKey);
|
2011-01-13 16:31:17 -05:00
|
|
|
|
2011-07-06 14:21:40 -04:00
|
|
|
QScopedPointer<QBuffer> buffer;
|
|
|
|
|
|
|
|
if (m_saveXml) {
|
|
|
|
m_xmlData = xmlDevice->readAll();
|
|
|
|
buffer.reset(new QBuffer(&m_xmlData));
|
|
|
|
buffer->open(QIODevice::ReadOnly);
|
|
|
|
xmlDevice = buffer.data();
|
|
|
|
}
|
|
|
|
|
2010-09-13 17:24:36 -04:00
|
|
|
KeePass2XmlReader xmlReader;
|
2011-07-06 18:15:52 -04:00
|
|
|
xmlReader.readDatabase(xmlDevice, m_db, &randomStream);
|
2012-01-06 14:03:13 -05:00
|
|
|
|
|
|
|
if (xmlReader.hasError()) {
|
|
|
|
raiseError(xmlReader.errorString());
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return db.take();
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
Database* KeePass2Reader::readDatabase(const QString& filename, const CompositeKey& key)
|
|
|
|
{
|
|
|
|
QFile file(filename);
|
2012-01-06 14:03:13 -05:00
|
|
|
if (!file.open(QFile::ReadOnly)) {
|
|
|
|
raiseError(file.errorString());
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
QScopedPointer<Database> db(readDatabase(&file, key));
|
|
|
|
|
|
|
|
if (file.error() != QFile::NoError) {
|
|
|
|
raiseError(file.errorString());
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
return db.take();
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
|
2012-01-06 14:03:13 -05:00
|
|
|
bool KeePass2Reader::hasError()
|
2010-09-13 17:24:36 -04:00
|
|
|
{
|
|
|
|
return m_error;
|
|
|
|
}
|
|
|
|
|
|
|
|
QString KeePass2Reader::errorString()
|
|
|
|
{
|
2012-01-06 14:03:13 -05:00
|
|
|
return m_errorStr;
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
|
2011-07-06 14:21:40 -04:00
|
|
|
void KeePass2Reader::setSaveXml(bool save)
|
|
|
|
{
|
|
|
|
m_saveXml = save;
|
|
|
|
}
|
|
|
|
|
|
|
|
QByteArray KeePass2Reader::xmlData()
|
|
|
|
{
|
|
|
|
return m_xmlData;
|
|
|
|
}
|
|
|
|
|
2010-09-13 17:24:36 -04:00
|
|
|
void KeePass2Reader::raiseError(const QString& str)
|
|
|
|
{
|
|
|
|
m_error = true;
|
2012-01-06 14:03:13 -05:00
|
|
|
m_errorStr = str;
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
bool KeePass2Reader::readHeaderField()
|
|
|
|
{
|
|
|
|
QByteArray fieldIDArray = m_device->read(1);
|
|
|
|
if (fieldIDArray.size() != 1) {
|
|
|
|
raiseError("");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
quint8 fieldID = fieldIDArray.at(0);
|
|
|
|
|
|
|
|
bool ok;
|
2010-09-25 06:41:00 -04:00
|
|
|
quint16 fieldLen = Endian::readUInt16(m_device, KeePass2::BYTEORDER, &ok);
|
2010-09-13 17:24:36 -04:00
|
|
|
if (!ok) {
|
|
|
|
raiseError("");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
QByteArray fieldData;
|
|
|
|
if (fieldLen != 0) {
|
|
|
|
fieldData = m_device->read(fieldLen);
|
|
|
|
if (fieldData.size() != fieldLen) {
|
|
|
|
raiseError("");
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (fieldID) {
|
|
|
|
case KeePass2::EndOfHeader:
|
|
|
|
m_headerEnd = true;
|
|
|
|
break;
|
|
|
|
|
|
|
|
case KeePass2::CipherID:
|
|
|
|
setCipher(fieldData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case KeePass2::CompressionFlags:
|
|
|
|
setCompressionFlags(fieldData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case KeePass2::MasterSeed:
|
|
|
|
setMasterSeed(fieldData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case KeePass2::TransformSeed:
|
|
|
|
setTransformSeed(fieldData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case KeePass2::TransformRounds:
|
|
|
|
setTansformRounds(fieldData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case KeePass2::EncryptionIV:
|
|
|
|
setEncryptionIV(fieldData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case KeePass2::ProtectedStreamKey:
|
|
|
|
setProtectedStreamKey(fieldData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case KeePass2::StreamStartBytes:
|
|
|
|
setStreamStartBytes(fieldData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
case KeePass2::InnerRandomStreamID:
|
|
|
|
setInnerRandomStreamID(fieldData);
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
qWarning("Unknown header field read: id=%d", fieldID);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
return !m_headerEnd;
|
|
|
|
}
|
|
|
|
|
|
|
|
void KeePass2Reader::setCipher(const QByteArray& data)
|
|
|
|
{
|
|
|
|
if (data.size() != Uuid::LENGTH) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
2010-09-25 06:41:00 -04:00
|
|
|
Uuid uuid(data);
|
|
|
|
|
|
|
|
if (uuid != KeePass2::CIPHER_AES) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
m_db->setCipher(uuid);
|
|
|
|
}
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void KeePass2Reader::setCompressionFlags(const QByteArray& data)
|
|
|
|
{
|
|
|
|
if (data.size() != 4) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
2010-09-25 06:41:00 -04:00
|
|
|
quint32 id = Endian::bytesToUInt32(data, KeePass2::BYTEORDER);
|
2010-09-13 17:24:36 -04:00
|
|
|
|
2010-09-25 06:41:00 -04:00
|
|
|
if (id > Database::CompressionAlgorithmMax) {
|
2010-09-13 17:24:36 -04:00
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
2010-09-25 06:41:00 -04:00
|
|
|
m_db->setCompressionAlgo(static_cast<Database::CompressionAlgorithm>(id));
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void KeePass2Reader::setMasterSeed(const QByteArray& data)
|
|
|
|
{
|
|
|
|
if (data.size() != 32) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
m_masterSeed = data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void KeePass2Reader::setTransformSeed(const QByteArray& data)
|
|
|
|
{
|
|
|
|
if (data.size() != 32) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
2011-06-29 10:39:39 -04:00
|
|
|
m_transformSeed = data;
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void KeePass2Reader::setTansformRounds(const QByteArray& data)
|
|
|
|
{
|
|
|
|
if (data.size() != 8) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
2010-09-25 06:41:00 -04:00
|
|
|
m_db->setTransformRounds(Endian::bytesToUInt64(data, KeePass2::BYTEORDER));
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void KeePass2Reader::setEncryptionIV(const QByteArray& data)
|
|
|
|
{
|
|
|
|
if (data.size() != 16) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
m_encryptionIV = data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void KeePass2Reader::setProtectedStreamKey(const QByteArray& data)
|
|
|
|
{
|
2011-01-13 16:31:17 -05:00
|
|
|
if (data.size() != 32) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
m_protectedStreamKey = data;
|
|
|
|
}
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
void KeePass2Reader::setStreamStartBytes(const QByteArray& data)
|
|
|
|
{
|
|
|
|
if (data.size() != 32) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
m_streamStartBytes = data;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
void KeePass2Reader::setInnerRandomStreamID(const QByteArray& data)
|
|
|
|
{
|
2011-01-13 16:31:17 -05:00
|
|
|
if (data.size() != 4) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
quint32 id = Endian::bytesToUInt32(data, KeePass2::BYTEORDER);
|
|
|
|
|
|
|
|
if (id != KeePass2::Salsa20) {
|
|
|
|
raiseError("");
|
|
|
|
}
|
|
|
|
}
|
2010-09-13 17:24:36 -04:00
|
|
|
}
|