Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/keepassxreboot/keepassxc.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'src/format/OpVaultReaderAttachments.cpp')
-rw-r--r--src/format/OpVaultReaderAttachments.cpp250
1 files changed, 250 insertions, 0 deletions
diff --git a/src/format/OpVaultReaderAttachments.cpp b/src/format/OpVaultReaderAttachments.cpp
new file mode 100644
index 000000000..49367e306
--- /dev/null
+++ b/src/format/OpVaultReaderAttachments.cpp
@@ -0,0 +1,250 @@
+/*
+ * 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;
+ }
+
+ if (!metadata.contains("contentsSize")) {
+ qWarning() << "Expected attachment metadata to contain \"contentsSize\" but nope: " << metadata;
+ return false;
+ } else if (!metadata["contentsSize"].isDouble()) {
+ qWarning() << "Expected attachment metadata to contain numeric \"contentsSize\" but nope: " << metadata;
+ return false;
+ }
+ int bytesLen = metadata["contentsSize"].toInt();
+ const QByteArray encData = file.readAll();
+ if (encData.size() < bytesLen) {
+ qCritical() << "Unable to read all of the attachment payload; wanted " << bytesLen << "but got"
+ << encData.size();
+ return false;
+ }
+
+ OpData01 att01;
+ 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);
+}