1 /*
2  *  Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
3  *  Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
4  *
5  *  This program is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 2 or (at your option)
8  *  version 3 of the License.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "Kdbx3Reader.h"
20 
21 #include "core/AsyncTask.h"
22 #include "core/Endian.h"
23 #include "core/Group.h"
24 #include "crypto/CryptoHash.h"
25 #include "format/KdbxXmlReader.h"
26 #include "format/KeePass2RandomStream.h"
27 #include "streams/HashedBlockStream.h"
28 #include "streams/QtIOCompressor"
29 #include "streams/SymmetricCipherStream.h"
30 
31 #include <QBuffer>
32 
readDatabaseImpl(QIODevice * device,const QByteArray & headerData,QSharedPointer<const CompositeKey> key,Database * db)33 bool Kdbx3Reader::readDatabaseImpl(QIODevice* device,
34                                    const QByteArray& headerData,
35                                    QSharedPointer<const CompositeKey> key,
36                                    Database* db)
37 {
38     Q_ASSERT(m_kdbxVersion <= KeePass2::FILE_VERSION_3_1);
39 
40     if (hasError()) {
41         return false;
42     }
43 
44     // check if all required headers were present
45     if (m_masterSeed.isEmpty() || m_encryptionIV.isEmpty() || m_streamStartBytes.isEmpty()
46         || m_protectedStreamKey.isEmpty() || db->cipher().isNull()) {
47         raiseError(tr("missing database headers"));
48         return false;
49     }
50 
51     bool ok = AsyncTask::runAndWaitForFuture([&] { return db->setKey(key, false); });
52     if (!ok) {
53         raiseError(tr("Unable to calculate database key"));
54         return false;
55     }
56 
57     if (!db->challengeMasterSeed(m_masterSeed)) {
58         raiseError(tr("Unable to issue challenge-response: %1").arg(db->keyError()));
59         return false;
60     }
61 
62     CryptoHash hash(CryptoHash::Sha256);
63     hash.addData(m_masterSeed);
64     hash.addData(db->challengeResponseKey());
65     hash.addData(db->transformedDatabaseKey());
66     QByteArray finalKey = hash.result();
67 
68     SymmetricCipher::Algorithm cipher = SymmetricCipher::cipherToAlgorithm(db->cipher());
69     SymmetricCipherStream cipherStream(
70         device, cipher, SymmetricCipher::algorithmMode(cipher), SymmetricCipher::Decrypt);
71     if (!cipherStream.init(finalKey, m_encryptionIV)) {
72         raiseError(cipherStream.errorString());
73         return false;
74     }
75     if (!cipherStream.open(QIODevice::ReadOnly)) {
76         raiseError(cipherStream.errorString());
77         return false;
78     }
79 
80     QByteArray realStart = cipherStream.read(32);
81 
82     if (realStart != m_streamStartBytes) {
83         raiseError(tr("Invalid credentials were provided, please try again.\n"
84                       "If this reoccurs, then your database file may be corrupt."));
85         return false;
86     }
87 
88     HashedBlockStream hashedStream(&cipherStream);
89     if (!hashedStream.open(QIODevice::ReadOnly)) {
90         raiseError(hashedStream.errorString());
91         return false;
92     }
93 
94     QIODevice* xmlDevice = nullptr;
95     QScopedPointer<QtIOCompressor> ioCompressor;
96 
97     if (db->compressionAlgorithm() == Database::CompressionNone) {
98         xmlDevice = &hashedStream;
99     } else {
100         ioCompressor.reset(new QtIOCompressor(&hashedStream));
101         ioCompressor->setStreamFormat(QtIOCompressor::GzipFormat);
102         if (!ioCompressor->open(QIODevice::ReadOnly)) {
103             raiseError(ioCompressor->errorString());
104             return false;
105         }
106         xmlDevice = ioCompressor.data();
107     }
108 
109     KeePass2RandomStream randomStream(KeePass2::ProtectedStreamAlgo::Salsa20);
110     if (!randomStream.init(m_protectedStreamKey)) {
111         raiseError(randomStream.errorString());
112         return false;
113     }
114 
115     Q_ASSERT(xmlDevice);
116 
117     KdbxXmlReader xmlReader(KeePass2::FILE_VERSION_3_1);
118     xmlReader.readDatabase(xmlDevice, db, &randomStream);
119 
120     if (xmlReader.hasError()) {
121         raiseError(xmlReader.errorString());
122         return false;
123     }
124 
125     Q_ASSERT(!xmlReader.headerHash().isEmpty() || m_kdbxVersion < KeePass2::FILE_VERSION_3_1);
126 
127     if (!xmlReader.headerHash().isEmpty()) {
128         QByteArray headerHash = CryptoHash::hash(headerData, CryptoHash::Sha256);
129         if (headerHash != xmlReader.headerHash()) {
130             raiseError(tr("Header doesn't match hash"));
131             return false;
132         }
133     }
134 
135     return true;
136 }
137 
readHeaderField(StoreDataStream & headerStream,Database * db)138 bool Kdbx3Reader::readHeaderField(StoreDataStream& headerStream, Database* db)
139 {
140     Q_UNUSED(db);
141 
142     QByteArray fieldIDArray = headerStream.read(1);
143     if (fieldIDArray.size() != 1) {
144         raiseError(tr("Invalid header id size"));
145         return false;
146     }
147     char fieldID = fieldIDArray.at(0);
148 
149     bool ok;
150     auto fieldLen = Endian::readSizedInt<quint16>(&headerStream, KeePass2::BYTEORDER, &ok);
151     if (!ok) {
152         raiseError(tr("Invalid header field length"));
153         return false;
154     }
155 
156     QByteArray fieldData;
157     if (fieldLen != 0) {
158         fieldData = headerStream.read(fieldLen);
159         if (fieldData.size() != fieldLen) {
160             raiseError(tr("Invalid header data length"));
161             return false;
162         }
163     }
164 
165     bool headerEnd = false;
166     switch (static_cast<KeePass2::HeaderFieldID>(fieldID)) {
167     case KeePass2::HeaderFieldID::EndOfHeader:
168         headerEnd = true;
169         break;
170 
171     case KeePass2::HeaderFieldID::CipherID:
172         setCipher(fieldData);
173         break;
174 
175     case KeePass2::HeaderFieldID::CompressionFlags:
176         setCompressionFlags(fieldData);
177         break;
178 
179     case KeePass2::HeaderFieldID::MasterSeed:
180         setMasterSeed(fieldData);
181         break;
182 
183     case KeePass2::HeaderFieldID::TransformSeed:
184         setTransformSeed(fieldData);
185         break;
186 
187     case KeePass2::HeaderFieldID::TransformRounds:
188         setTransformRounds(fieldData);
189         break;
190 
191     case KeePass2::HeaderFieldID::EncryptionIV:
192         setEncryptionIV(fieldData);
193         break;
194 
195     case KeePass2::HeaderFieldID::ProtectedStreamKey:
196         setProtectedStreamKey(fieldData);
197         break;
198 
199     case KeePass2::HeaderFieldID::StreamStartBytes:
200         setStreamStartBytes(fieldData);
201         break;
202 
203     case KeePass2::HeaderFieldID::InnerRandomStreamID:
204         setInnerRandomStreamID(fieldData);
205         break;
206 
207     default:
208         qWarning("Unknown header field read: id=%d", fieldID);
209         break;
210     }
211 
212     return !headerEnd;
213 }
214