As discussed in #4317, the next KeePass2 release will ship with support for a new generation of XML key files which enable hash integrity checks. This patch adds support for reading and generating this new format. By default, KeePass2 now uses the .keyx extension for generated key files, which was added to KeePassXC's key generation file chooser filter. We continue to generate hashed binary key files by default, but the user can explicitly save the file with the new .keyx extension to generate an XML v2 key file (currently undocumented). When opening a database, the key file type is still determined by content negotation, so the file extension has no impact here. As an additional change, the legacy key file warnings have been improved slightly to be less confusing and more helpful.
451 lines
12 KiB
C++
451 lines
12 KiB
C++
/*
|
|
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
|
* Copyright (C) 2011 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 "FileKey.h"
|
|
|
|
#include "core/Tools.h"
|
|
#include "crypto/CryptoHash.h"
|
|
#include "crypto/Random.h"
|
|
|
|
#include <QFile>
|
|
|
|
#include <algorithm>
|
|
#include <cstring>
|
|
#include <gcrypt.h>
|
|
#include <sodium.h>
|
|
|
|
QUuid FileKey::UUID("a584cbc4-c9b4-437e-81bb-362ca9709273");
|
|
|
|
constexpr int FileKey::SHA256_SIZE;
|
|
|
|
FileKey::FileKey()
|
|
: Key(UUID)
|
|
, m_key(static_cast<char*>(gcry_malloc_secure(SHA256_SIZE)))
|
|
{
|
|
}
|
|
|
|
FileKey::~FileKey()
|
|
{
|
|
if (m_key) {
|
|
gcry_free(m_key);
|
|
m_key = nullptr;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Read key file from device while trying to detect its file format.
|
|
*
|
|
* If no legacy key file format was detected, the SHA-256 hash of the
|
|
* key file will be used, allowing usage of arbitrary files as key files.
|
|
* In case of a detected legacy key file format, the raw byte contents
|
|
* will be extracted from the file.
|
|
*
|
|
* Supported legacy formats are:
|
|
* - KeePass 2 XML key file
|
|
* - Fixed 32 byte binary
|
|
* - Fixed 32 byte ASCII hex-encoded binary
|
|
*
|
|
* Usage of legacy formats is discouraged and support for them may be
|
|
* removed in a future version.
|
|
*
|
|
* @param device input device
|
|
* @param errorMsg error message in case of fatal failure
|
|
* @return true if key file was loaded successfully
|
|
*/
|
|
bool FileKey::load(QIODevice* device, QString* errorMsg)
|
|
{
|
|
m_type = None;
|
|
|
|
// we may need to read the file multiple times
|
|
if (device->isSequential()) {
|
|
return false;
|
|
}
|
|
|
|
if (device->size() == 0 || !device->reset()) {
|
|
return false;
|
|
}
|
|
|
|
// load XML key file v1 or v2
|
|
QString xmlError;
|
|
if (loadXml(device, &xmlError)) {
|
|
return true;
|
|
}
|
|
|
|
if (!device->reset() || !xmlError.isEmpty()) {
|
|
if (errorMsg) {
|
|
*errorMsg = xmlError;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// try legacy key file formats
|
|
if (loadBinary(device)) {
|
|
return true;
|
|
}
|
|
|
|
if (!device->reset()) {
|
|
return false;
|
|
}
|
|
|
|
if (loadHex(device)) {
|
|
return true;
|
|
}
|
|
|
|
// if no legacy format was detected, generate SHA-256 hash of key file
|
|
if (!device->reset()) {
|
|
return false;
|
|
}
|
|
if (loadHashed(device)) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Load key file from path while trying to detect its file format.
|
|
*
|
|
* If no legacy key file format was detected, the SHA-256 hash of the
|
|
* key file will be used, allowing usage of arbitrary files as key files.
|
|
* In case of a detected legacy key file format, the raw byte contents
|
|
* will be extracted from the file.
|
|
*
|
|
* Supported legacy formats are:
|
|
* - KeePass 2 XML key file
|
|
* - Fixed 32 byte binary
|
|
* - Fixed 32 byte ASCII hex-encoded binary
|
|
*
|
|
* Usage of legacy formats is discouraged and support for them may be
|
|
* removed in a future version.
|
|
*
|
|
* @param fileName input file name
|
|
* @param errorMsg error message if loading failed
|
|
* @return true if key file was loaded successfully
|
|
*/
|
|
bool FileKey::load(const QString& fileName, QString* errorMsg)
|
|
{
|
|
QFile file(fileName);
|
|
if (!file.open(QFile::ReadOnly)) {
|
|
if (errorMsg) {
|
|
*errorMsg = file.errorString();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool result = load(&file, errorMsg);
|
|
file.close();
|
|
|
|
if (errorMsg && !errorMsg->isEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
if (file.error()) {
|
|
result = false;
|
|
if (errorMsg) {
|
|
*errorMsg = file.errorString();
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* @return key data as bytes
|
|
*/
|
|
QByteArray FileKey::rawKey() const
|
|
{
|
|
if (!m_key) {
|
|
return {};
|
|
}
|
|
return QByteArray::fromRawData(m_key, SHA256_SIZE);
|
|
}
|
|
|
|
/**
|
|
* Generate a new key file with random bytes.
|
|
*
|
|
* @param device output device
|
|
* @param number of random bytes to generate
|
|
*/
|
|
void FileKey::createRandom(QIODevice* device, int size)
|
|
{
|
|
device->write(randomGen()->randomArray(size));
|
|
}
|
|
|
|
/**
|
|
* Generate a new key file in the KeePass2 XML format v2.
|
|
*
|
|
* @param device output device
|
|
* @param number of random bytes to generate
|
|
*/
|
|
void FileKey::createXMLv2(QIODevice* device, int size)
|
|
{
|
|
QXmlStreamWriter w(device);
|
|
w.setAutoFormatting(true);
|
|
w.setAutoFormattingIndent(4);
|
|
w.writeStartDocument();
|
|
|
|
w.writeStartElement("KeyFile");
|
|
|
|
w.writeStartElement("Meta");
|
|
w.writeTextElement("Version", "2.0");
|
|
w.writeEndElement();
|
|
|
|
w.writeStartElement("Key");
|
|
w.writeStartElement("Data");
|
|
|
|
QByteArray key = randomGen()->randomArray(size);
|
|
CryptoHash hash(CryptoHash::Sha256);
|
|
hash.addData(key);
|
|
QByteArray result = hash.result().left(4);
|
|
key = key.toHex().toUpper();
|
|
|
|
w.writeAttribute("Hash", result.toHex().toUpper());
|
|
w.writeCharacters("\n ");
|
|
for (int i = 0; i < key.size(); ++i) {
|
|
// Pretty-print hex value (not strictly necessary, but nicer to read and KeePass2 does it)
|
|
if (i != 0 && i % 32 == 0) {
|
|
w.writeCharacters("\n ");
|
|
} else if (i != 0 && i % 8 == 0) {
|
|
w.writeCharacters(" ");
|
|
}
|
|
w.writeCharacters(QChar(key[i]));
|
|
}
|
|
sodium_memzero(key.data(), static_cast<std::size_t>(key.capacity()));
|
|
w.writeCharacters("\n ");
|
|
|
|
w.writeEndElement();
|
|
w.writeEndElement();
|
|
|
|
w.writeEndDocument();
|
|
}
|
|
|
|
/**
|
|
* Create a new key file from random bytes.
|
|
*
|
|
* @param fileName output file name
|
|
* @param errorMsg error message if generation failed
|
|
* @param number of random bytes to generate
|
|
* @return true on successful creation
|
|
*/
|
|
bool FileKey::create(const QString& fileName, QString* errorMsg)
|
|
{
|
|
QFile file(fileName);
|
|
if (!file.open(QFile::WriteOnly)) {
|
|
if (errorMsg) {
|
|
*errorMsg = file.errorString();
|
|
}
|
|
return false;
|
|
}
|
|
if (fileName.endsWith(".keyx")) {
|
|
createXMLv2(&file);
|
|
} else {
|
|
createRandom(&file);
|
|
}
|
|
file.close();
|
|
file.setPermissions(QFile::ReadUser);
|
|
|
|
if (file.error()) {
|
|
if (errorMsg) {
|
|
*errorMsg = file.errorString();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Load key file in legacy KeePass 2 XML format.
|
|
*
|
|
* @param device input device
|
|
* @return true on success
|
|
*/
|
|
bool FileKey::loadXml(QIODevice* device, QString* errorMsg)
|
|
{
|
|
QXmlStreamReader xmlReader(device);
|
|
|
|
if (xmlReader.error()) {
|
|
return false;
|
|
}
|
|
if (xmlReader.readNextStartElement() && xmlReader.name() != "KeyFile") {
|
|
return false;
|
|
}
|
|
|
|
struct
|
|
{
|
|
QString version;
|
|
QByteArray hash;
|
|
QByteArray data;
|
|
} keyFileData;
|
|
|
|
while (!xmlReader.error() && xmlReader.readNextStartElement()) {
|
|
if (xmlReader.name() == "Meta") {
|
|
while (!xmlReader.error() && xmlReader.readNextStartElement()) {
|
|
if (xmlReader.name() == "Version") {
|
|
keyFileData.version = xmlReader.readElementText();
|
|
if (keyFileData.version.startsWith("1.0")) {
|
|
m_type = KeePass2XML;
|
|
} else if (keyFileData.version == "2.0") {
|
|
m_type = KeePass2XMLv2;
|
|
} else {
|
|
if (errorMsg) {
|
|
*errorMsg = QObject::tr("Unsupported key file version: %1").arg(keyFileData.version);
|
|
}
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
} else if (xmlReader.name() == "Key") {
|
|
while (!xmlReader.error() && xmlReader.readNextStartElement()) {
|
|
if (xmlReader.name() == "Data") {
|
|
keyFileData.hash = QByteArray::fromHex(xmlReader.attributes().value("Hash").toLatin1());
|
|
QByteArray rawData = xmlReader.readElementText().simplified().replace(" ", "").toLatin1();
|
|
|
|
if (keyFileData.version.startsWith("1.0") && Tools::isBase64(rawData)) {
|
|
keyFileData.data = QByteArray::fromBase64(rawData);
|
|
} else if (keyFileData.version == "2.0" && Tools::isHex(rawData)) {
|
|
keyFileData.data = QByteArray::fromHex(rawData);
|
|
|
|
CryptoHash hash(CryptoHash::Sha256);
|
|
hash.addData(keyFileData.data);
|
|
QByteArray result = hash.result().left(4);
|
|
if (keyFileData.hash != result) {
|
|
if (errorMsg) {
|
|
*errorMsg = QObject::tr("Checksum mismatch! Key file may be corrupt.");
|
|
}
|
|
return false;
|
|
}
|
|
} else {
|
|
if (errorMsg) {
|
|
*errorMsg = QObject::tr("Unexpected key file data! Key file may be corrupt.");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
sodium_memzero(rawData.data(), static_cast<std::size_t>(rawData.capacity()));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool ok = false;
|
|
if (!xmlReader.error() && !keyFileData.data.isEmpty()) {
|
|
std::memcpy(m_key, keyFileData.data.data(), std::min(SHA256_SIZE, keyFileData.data.size()));
|
|
ok = true;
|
|
}
|
|
|
|
sodium_memzero(keyFileData.data.data(), static_cast<std::size_t>(keyFileData.data.capacity()));
|
|
|
|
return ok;
|
|
}
|
|
|
|
/**
|
|
* Load fixed 32-bit binary key file.
|
|
*
|
|
* @param device input device
|
|
* @return true on success
|
|
* @deprecated
|
|
*/
|
|
bool FileKey::loadBinary(QIODevice* device)
|
|
{
|
|
if (device->size() != 32) {
|
|
return false;
|
|
}
|
|
|
|
QByteArray data;
|
|
if (!Tools::readAllFromDevice(device, data) || data.size() != 32) {
|
|
return false;
|
|
}
|
|
|
|
std::memcpy(m_key, data.data(), std::min(SHA256_SIZE, data.size()));
|
|
sodium_memzero(data.data(), static_cast<std::size_t>(data.capacity()));
|
|
m_type = FixedBinary;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Load hex-encoded representation of fixed 32-bit binary key file.
|
|
*
|
|
* @param device input device
|
|
* @return true on success
|
|
* @deprecated
|
|
*/
|
|
bool FileKey::loadHex(QIODevice* device)
|
|
{
|
|
if (device->size() != 64) {
|
|
return false;
|
|
}
|
|
|
|
QByteArray data;
|
|
if (!Tools::readAllFromDevice(device, data) || data.size() != 64) {
|
|
return false;
|
|
}
|
|
|
|
if (!Tools::isHex(data)) {
|
|
return false;
|
|
}
|
|
|
|
QByteArray key = QByteArray::fromHex(data);
|
|
sodium_memzero(data.data(), static_cast<std::size_t>(data.capacity()));
|
|
|
|
if (key.size() != 32) {
|
|
return false;
|
|
}
|
|
|
|
std::memcpy(m_key, key.data(), std::min(SHA256_SIZE, key.size()));
|
|
sodium_memzero(key.data(), static_cast<std::size_t>(key.capacity()));
|
|
|
|
m_type = FixedBinaryHex;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Generate SHA-256 hash of arbitrary text or binary key file.
|
|
*
|
|
* @param device input device
|
|
* @return true on success
|
|
*/
|
|
bool FileKey::loadHashed(QIODevice* device)
|
|
{
|
|
CryptoHash cryptoHash(CryptoHash::Sha256);
|
|
|
|
QByteArray buffer;
|
|
do {
|
|
if (!Tools::readFromDevice(device, buffer)) {
|
|
return false;
|
|
}
|
|
cryptoHash.addData(buffer);
|
|
} while (!buffer.isEmpty());
|
|
|
|
auto result = cryptoHash.result();
|
|
std::memcpy(m_key, result.data(), std::min(SHA256_SIZE, result.size()));
|
|
sodium_memzero(result.data(), static_cast<std::size_t>(result.capacity()));
|
|
|
|
m_type = Hashed;
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @return type of loaded key file
|
|
*/
|
|
FileKey::Type FileKey::type() const
|
|
{
|
|
return m_type;
|
|
}
|