keepassxc
1/*
2* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
3* Copyright (C) 2011 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 "FileKey.h"20
21#include "core/Tools.h"22#include "crypto/CryptoHash.h"23#include "crypto/Random.h"24
25#include <QDataStream>26#include <QFile>27#include <QXmlStreamReader>28
29QUuid FileKey::UUID("a584cbc4-c9b4-437e-81bb-362ca9709273");30
31constexpr int FileKey::SHA256_SIZE;32
33FileKey::FileKey()34: Key(UUID)35, m_key(SHA256_SIZE)36{
37}
38
39/**
40* Read key file from device while trying to detect its file format.
41*
42* If no legacy key file format was detected, the SHA-256 hash of the
43* key file will be used, allowing usage of arbitrary files as key files.
44* In case of a detected legacy key file format, the raw byte contents
45* will be extracted from the file.
46*
47* Supported legacy formats are:
48* - KeePass 2 XML key file
49* - Fixed 32 byte binary
50* - Fixed 32 byte ASCII hex-encoded binary
51*
52* Usage of legacy formats is discouraged and support for them may be
53* removed in a future version.
54*
55* @param device input device
56* @param errorMsg error message in case of fatal failure
57* @return true if key file was loaded successfully
58*/
59bool FileKey::load(QIODevice* device, QString* errorMsg)60{
61m_type = None;62
63// we may need to read the file multiple times64if (device->isSequential()) {65return false;66}67
68if (device->size() == 0 || !device->reset()) {69return false;70}71
72// load XML key file v1 or v273QString xmlError;74if (loadXml(device, &xmlError)) {75return true;76}77
78if (!device->reset() || !xmlError.isEmpty()) {79if (errorMsg) {80*errorMsg = xmlError;81}82return false;83}84
85// try legacy key file formats86if (loadBinary(device)) {87return true;88}89
90if (!device->reset()) {91return false;92}93
94if (loadHex(device)) {95return true;96}97
98// if no legacy format was detected, generate SHA-256 hash of key file99if (!device->reset()) {100return false;101}102if (loadHashed(device)) {103return true;104}105
106return false;107}
108
109/**
110* Load key file from path while trying to detect its file format.
111*
112* If no legacy key file format was detected, the SHA-256 hash of the
113* key file will be used, allowing usage of arbitrary files as key files.
114* In case of a detected legacy key file format, the raw byte contents
115* will be extracted from the file.
116*
117* Supported legacy formats are:
118* - KeePass 2 XML key file
119* - Fixed 32 byte binary
120* - Fixed 32 byte ASCII hex-encoded binary
121*
122* Usage of legacy formats is discouraged and support for them may be
123* removed in a future version.
124*
125* @param fileName input file name
126* @param errorMsg error message if loading failed
127* @return true if key file was loaded successfully
128*/
129bool FileKey::load(const QString& fileName, QString* errorMsg)130{
131QFile file(fileName);132if (!file.open(QFile::ReadOnly)) {133if (errorMsg) {134*errorMsg = file.errorString();135}136return false;137}138
139bool result = load(&file, errorMsg);140file.close();141
142if (errorMsg && !errorMsg->isEmpty()) {143return false;144}145
146if (file.error()) {147result = false;148if (errorMsg) {149*errorMsg = file.errorString();150}151} else {152// Store the file path for serialization153m_file = fileName;154}155
156return result;157}
158
159/**
160* @return key data as bytes
161*/
162QByteArray FileKey::rawKey() const163{
164return {m_key.data(), int(m_key.size())};165}
166
167void FileKey::setRawKey(const QByteArray& data)168{
169Q_ASSERT(data.size() == SHA256_SIZE);170m_key.assign(data.begin(), data.end());171}
172
173QByteArray FileKey::serialize() const174{
175QByteArray data;176QDataStream stream(&data, QIODevice::WriteOnly);177stream << uuid().toRfc4122() << rawKey() << static_cast<qint32>(m_type) << m_file;178return data;179}
180
181void FileKey::deserialize(const QByteArray& data)182{
183QDataStream stream(data);184QByteArray uuidData;185stream >> uuidData;186if (uuid().toRfc4122() == uuidData) {187QByteArray key;188qint32 type;189stream >> key >> type >> m_file;190
191setRawKey(key);192m_type = static_cast<Type>(type);193}194}
195
196/**
197* Generate a new key file with random bytes.
198*
199* @param device output device
200* @param number of random bytes to generate
201*/
202void FileKey::createRandom(QIODevice* device, int size)203{
204device->write(randomGen()->randomArray(size));205}
206
207/**
208* Generate a new key file in the KeePass2 XML format v2.
209*
210* @param device output device
211* @param number of random bytes to generate
212*/
213void FileKey::createXMLv2(QIODevice* device, int size)214{
215QXmlStreamWriter w(device);216w.setAutoFormatting(true);217w.setAutoFormattingIndent(4);218w.writeStartDocument();219
220w.writeStartElement("KeyFile");221
222w.writeStartElement("Meta");223w.writeTextElement("Version", "2.0");224w.writeEndElement();225
226w.writeStartElement("Key");227w.writeStartElement("Data");228
229QByteArray key = randomGen()->randomArray(size);230CryptoHash hash(CryptoHash::Sha256);231hash.addData(key);232QByteArray result = hash.result().left(4);233key = key.toHex().toUpper();234
235w.writeAttribute("Hash", result.toHex().toUpper());236w.writeCharacters("\n ");237for (int i = 0; i < key.size(); ++i) {238// Pretty-print hex value (not strictly necessary, but nicer to read and KeePass2 does it)239if (i != 0 && i % 32 == 0) {240w.writeCharacters("\n ");241} else if (i != 0 && i % 8 == 0) {242w.writeCharacters(" ");243}244w.writeCharacters(QChar(key[i]));245}246Botan::secure_scrub_memory(key.data(), static_cast<std::size_t>(key.capacity()));247w.writeCharacters("\n ");248
249w.writeEndElement();250w.writeEndElement();251
252w.writeEndDocument();253}
254
255/**
256* Create a new key file from random bytes.
257*
258* @param fileName output file name
259* @param errorMsg error message if generation failed
260* @param number of random bytes to generate
261* @return true on successful creation
262*/
263bool FileKey::create(const QString& fileName, QString* errorMsg)264{
265QFile file(fileName);266if (!file.open(QFile::WriteOnly)) {267if (errorMsg) {268*errorMsg = file.errorString();269}270return false;271}272if (fileName.endsWith(".keyx")) {273createXMLv2(&file);274} else {275createRandom(&file);276}277file.close();278file.setPermissions(QFile::ReadUser);279
280if (file.error()) {281if (errorMsg) {282*errorMsg = file.errorString();283}284
285return false;286}287
288return true;289}
290
291/**
292* Load key file in legacy KeePass 2 XML format.
293*
294* @param device input device
295* @return true on success
296*/
297bool FileKey::loadXml(QIODevice* device, QString* errorMsg)298{
299QXmlStreamReader xmlReader(device);300
301if (xmlReader.error()) {302return false;303}304if (xmlReader.readNextStartElement() && xmlReader.name() != "KeyFile") {305return false;306}307
308struct309{310QString version;311QByteArray hash;312QByteArray data;313} keyFileData;314
315while (!xmlReader.error() && xmlReader.readNextStartElement()) {316if (xmlReader.name() == "Meta") {317while (!xmlReader.error() && xmlReader.readNextStartElement()) {318if (xmlReader.name() == "Version") {319keyFileData.version = xmlReader.readElementText();320if (keyFileData.version.startsWith("1.0")) {321m_type = KeePass2XML;322} else if (keyFileData.version == "2.0") {323m_type = KeePass2XMLv2;324} else {325if (errorMsg) {326*errorMsg = QObject::tr("Unsupported key file version: %1").arg(keyFileData.version);327}328return false;329}330}331}332} else if (xmlReader.name() == "Key") {333while (!xmlReader.error() && xmlReader.readNextStartElement()) {334if (xmlReader.name() == "Data") {335keyFileData.hash = QByteArray::fromHex(xmlReader.attributes().value("Hash").toLatin1());336keyFileData.data = xmlReader.readElementText().simplified().replace(" ", "").toLatin1();337
338if (keyFileData.version.startsWith("1.0") && Tools::isBase64(keyFileData.data)) {339keyFileData.data = QByteArray::fromBase64(keyFileData.data);340} else if (keyFileData.version == "2.0" && Tools::isHex(keyFileData.data)) {341keyFileData.data = QByteArray::fromHex(keyFileData.data);342
343CryptoHash hash(CryptoHash::Sha256);344hash.addData(keyFileData.data);345QByteArray result = hash.result().left(4);346if (keyFileData.hash != result) {347if (errorMsg) {348*errorMsg = QObject::tr("Checksum mismatch! Key file may be corrupt.");349}350return false;351}352} else {353if (errorMsg) {354*errorMsg = QObject::tr("Unexpected key file data! Key file may be corrupt.");355}356return false;357}358}359}360}361}362
363bool ok = false;364if (!xmlReader.error() && !keyFileData.data.isEmpty()) {365std::memcpy(m_key.data(), keyFileData.data.data(), std::min(SHA256_SIZE, keyFileData.data.size()));366ok = true;367}368
369Botan::secure_scrub_memory(keyFileData.data.data(), static_cast<std::size_t>(keyFileData.data.capacity()));370
371return ok;372}
373
374/**
375* Load fixed 32-bit binary key file.
376*
377* @param device input device
378* @return true on success
379* @deprecated
380*/
381bool FileKey::loadBinary(QIODevice* device)382{
383if (device->size() != 32) {384return false;385}386
387Botan::secure_vector<char> data(32);388if (device->read(data.data(), 32) != 32 || !device->atEnd()) {389return false;390}391
392m_key = data;393m_type = FixedBinary;394return true;395}
396
397/**
398* Load hex-encoded representation of fixed 32-bit binary key file.
399*
400* @param device input device
401* @return true on success
402* @deprecated
403*/
404bool FileKey::loadHex(QIODevice* device)405{
406if (device->size() != 64) {407return false;408}409
410QByteArray data;411if (!Tools::readAllFromDevice(device, data) || data.size() != 64) {412return false;413}414
415if (!Tools::isHex(data)) {416return false;417}418
419data = QByteArray::fromHex(data);420if (data.size() != 32) {421return false;422}423
424std::memcpy(m_key.data(), data.data(), std::min(SHA256_SIZE, data.size()));425Botan::secure_scrub_memory(data.data(), static_cast<std::size_t>(data.capacity()));426
427m_type = FixedBinaryHex;428return true;429}
430
431/**
432* Generate SHA-256 hash of arbitrary text or binary key file.
433*
434* @param device input device
435* @return true on success
436*/
437bool FileKey::loadHashed(QIODevice* device)438{
439CryptoHash cryptoHash(CryptoHash::Sha256);440
441QByteArray buffer;442do {443if (!Tools::readFromDevice(device, buffer)) {444return false;445}446cryptoHash.addData(buffer);447} while (!buffer.isEmpty());448
449buffer = cryptoHash.result();450std::memcpy(m_key.data(), buffer.data(), std::min(SHA256_SIZE, buffer.size()));451Botan::secure_scrub_memory(buffer.data(), static_cast<std::size_t>(buffer.capacity()));452
453m_type = Hashed;454return true;455}
456
457/**
458* @return type of loaded key file
459*/
460FileKey::Type FileKey::type() const461{
462return m_type;463}
464