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