Files
keepassxc/src/format/OpVaultReaderAttachments.cpp
Jonathan White 612f8d2e5b Improve OPVault handling and replace test opvault
* Fix various bugs in opvault parsing to include: TOTP parsing, date handling, naming convention, attachments, and multiple url's.

* Remove category groups that don't have any entries.

* Simplify tests by focusing on the resulting database instead of the parsing mechanics.

* Remove proprietary "freddy" opvault in favor of self-made "keepassxc" opvault.

* Fix #4069, select opvault file on macOS
2020-05-14 15:17:28 -04:00

237 lines
9.1 KiB
C++

/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 "OpData01.h"
#include "OpVaultReader.h"
#include "core/Group.h"
#include "core/Tools.h"
#include <QDebug>
#include <QJsonDocument>
#include <QJsonObject>
#include <QUuid>
/*!
* This will \c qCritical() if unable to open the file for reading.
* @param file the \c .attachment file to decode
* @return \c nullptr if unable to take action, else a pair of metadata and the actual attachment bits
* \sa https://support.1password.com/opvault-design/#attachments
*/
bool OpVaultReader::readAttachment(const QString& filePath,
const QByteArray& itemKey,
const QByteArray& itemHmacKey,
QJsonObject& metadata,
QByteArray& payload)
{
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
qCritical() << QString("Unable to open \"%s\" for reading").arg(file.fileName());
return false;
}
QString magic("OPCLDAT");
QByteArray magicBytes = file.read(7);
if (magicBytes != magic.toUtf8()) {
qCritical() << "Expected OPCLDAT but found <<" << magicBytes.toHex() << ">>";
return false;
}
QByteArray version = file.read(1);
if (version[0] != '\001' && version[0] != '\002') {
qCritical() << "Unexpected version number; wanted 1 or 2, got <<" << version << ">>";
return false;
}
const QByteArray& metadataLenBytes = file.read(2);
if (metadataLenBytes.size() != 2) {
qCritical() << "Unable to read all metadata length bytes; wanted 2 bytes, got " << metadataLenBytes.size()
<< ": <<" << metadataLenBytes.toHex() << ">>";
return false;
}
const auto b0 = static_cast<unsigned char>(metadataLenBytes[0]);
const auto b1 = static_cast<unsigned char>(metadataLenBytes[1]);
int metadataLen = ((0xFF & b1) << 8) | (0xFF & b0);
// no really: it's labeled "Junk" in the spec
int junkBytesRead = file.read(2).size();
if (junkBytesRead != 2) {
qCritical() << "Unable to read all \"junk\" bytes; wanted 2 bytes, got " << junkBytesRead;
return false;
}
const QByteArray& iconLenBytes = file.read(4);
if (iconLenBytes.size() != 4) {
qCritical() << "Unable to read all \"iconLen\" bytes; wanted 4 bytes, got " << iconLenBytes.size();
return false;
}
int iconLen = 0;
for (int i = 0, len = iconLenBytes.size(); i < len; ++i) {
char ch = iconLenBytes[i];
auto b = static_cast<unsigned char>(ch & 0xFF);
iconLen = (b << (i * 8)) | iconLen;
}
QByteArray metadataJsonBytes = file.read(metadataLen);
if (metadataJsonBytes.size() != metadataLen) {
qCritical() << "Unable to read all bytes of metadata JSON; wanted " << metadataLen << "but read "
<< metadataJsonBytes.size();
return false;
}
QByteArray iconBytes = file.read(iconLen);
if (iconBytes.size() != iconLen) {
qCritical() << "Unable to read all icon bytes; wanted " << iconLen << "but read " << iconBytes.size();
// apologies for the icon being fatal, but it would take some gear-turning
// to re-sync where in the attach header we are
return false;
}
// we don't actually _care_ what the icon bytes are,
// but they damn well better be valid opdata01 and pass its HMAC
OpData01 icon01;
if (!icon01.decode(iconBytes, itemKey, itemHmacKey)) {
qCritical() << "Unable to decipher attachment icon in " << filePath << ": " << icon01.errorString();
return false;
}
QJsonParseError jsError;
QJsonDocument jDoc = QJsonDocument::fromJson(metadataJsonBytes, &jsError);
if (jsError.error != QJsonParseError::ParseError::NoError) {
qCritical() << "Found invalid attachment metadata JSON at offset " << jsError.offset << ": error("
<< jsError.error << "): " << jsError.errorString() << "\n<<" << metadataJsonBytes << ">>";
return false;
}
if (!jDoc.isObject()) {
qCritical() << "Expected " << metadataJsonBytes << "to be a JSON Object";
return false;
}
metadata = jDoc.object();
if (metadata.contains("trashed") && metadata["trashed"].toBool()) {
return false;
}
OpData01 att01;
const QByteArray encData = file.readAll();
if (!att01.decode(encData, itemKey, itemHmacKey)) {
qCritical() << "Unable to decipher attachment payload: " << att01.errorString();
return false;
}
payload = att01.getClearText();
return true;
}
/*!
* \sa https://support.1password.com/opvault-design/#attachments
*/
void OpVaultReader::fillAttachments(Entry* entry,
const QDir& attachmentDir,
const QByteArray& entryKey,
const QByteArray& entryHmacKey)
{
/*!
* Attachment files are named with the UUID of the item that they are attached to followed by an underscore
* and then followed by the UUID of the attachment itself. The file is then given the extension .attachment.
*/
auto fileFilter = QString("%1_*.attachment").arg(entry->uuidToHex().toUpper());
const auto& attachInfoList = attachmentDir.entryInfoList(QStringList() << fileFilter, QDir::Files);
int attachmentCount = attachInfoList.size();
if (attachmentCount == 0) {
return;
}
for (const auto& info : attachInfoList) {
if (!info.isReadable()) {
qCritical() << QString("Attachment file \"%1\" is not readable").arg(info.absoluteFilePath());
continue;
}
fillAttachment(entry, info, entryKey, entryHmacKey);
}
}
void OpVaultReader::fillAttachment(Entry* entry,
const QFileInfo& info,
const QByteArray& entryKey,
const QByteArray& entryHmacKey)
{
QJsonObject attachMetadata;
QByteArray attachPayload;
if (!readAttachment(info.absoluteFilePath(), entryKey, entryHmacKey, attachMetadata, attachPayload)) {
return;
}
if (!attachMetadata.contains("overview")) {
qWarning() << "Expected \"overview\" in attachment metadata";
return;
}
const QString& overB64 = attachMetadata["overview"].toString();
OpData01 over01;
if (over01.decodeBase64(overB64, m_overviewKey, m_overviewHmacKey)) {
QByteArray overviewJson = over01.getClearText();
QJsonDocument overDoc = QJsonDocument::fromJson(overviewJson);
if (overDoc.isObject()) {
QJsonObject overObj = overDoc.object();
attachMetadata.remove("overview");
for (QString& key : overObj.keys()) {
const QJsonValueRef& value = overObj[key];
QString insertAs = key;
for (int aa = 0; attachMetadata.contains(insertAs) && aa < 5; ++aa) {
insertAs = QString("%1_%2").arg(key, aa);
}
attachMetadata[insertAs] = value;
}
} else {
qWarning() << "Expected JSON Object in \"overview\" but nope: " << overDoc;
}
} else {
qCritical()
<< QString("Unable to decode attach.overview for \"%1\": %2").arg(info.fileName(), over01.errorString());
}
QByteArray payload;
payload.append(QString("attachment file is actually %1 bytes\n").arg(info.size()).toUtf8());
for (QString& key : attachMetadata.keys()) {
const QJsonValueRef& value = attachMetadata[key];
QByteArray valueBytes;
if (value.isString()) {
valueBytes = value.toString().toUtf8();
} else if (value.isDouble()) {
valueBytes = QString("%1").arg(value.toInt()).toUtf8();
} else if (value.isBool()) {
valueBytes = value.toBool() ? "true" : "false";
} else {
valueBytes = QString("Unexpected metadata type in attachment: %1").arg(value.type()).toUtf8();
}
payload.append(key.toUtf8()).append(":=").append(valueBytes).append("\n");
}
QString attachKey = info.baseName();
if (attachMetadata.contains("filename")) {
QJsonValueRef attFilename = attachMetadata["filename"];
if (attFilename.isString()) {
attachKey = attFilename.toString();
} else {
qWarning() << QString("Unexpected type of attachment \"filename\": %1").arg(attFilename.type());
}
}
entry->attachments()->set(attachKey, attachPayload);
}