2019-05-19 21:49:48 +00:00
|
|
|
/*
|
|
|
|
|
* 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 "OpVaultReader.h"
|
|
|
|
|
|
2021-07-12 02:10:29 +00:00
|
|
|
#include "core/Entry.h"
|
2024-01-30 23:44:43 +00:00
|
|
|
#include "core/Totp.h"
|
2019-05-19 21:49:48 +00:00
|
|
|
|
|
|
|
|
#include <QDebug>
|
|
|
|
|
#include <QJsonArray>
|
|
|
|
|
#include <QJsonObject>
|
2025-10-25 23:16:31 +00:00
|
|
|
#include <QLocale>
|
2019-05-19 21:49:48 +00:00
|
|
|
#include <QUrlQuery>
|
|
|
|
|
|
2020-04-19 15:50:48 +00:00
|
|
|
namespace
|
|
|
|
|
{
|
|
|
|
|
QDateTime resolveDate(const QString& kind, const QJsonValue& value)
|
|
|
|
|
{
|
|
|
|
|
QDateTime date;
|
|
|
|
|
if (kind == "monthYear") {
|
|
|
|
|
// 1Password programmers are sadistic...
|
|
|
|
|
auto dateValue = QString::number(value.toInt());
|
|
|
|
|
date = QDateTime::fromString(dateValue, "yyyyMM");
|
|
|
|
|
date.setTimeSpec(Qt::UTC);
|
|
|
|
|
} else if (value.isString()) {
|
|
|
|
|
date = QDateTime::fromTime_t(value.toString().toUInt(), Qt::UTC);
|
|
|
|
|
} else {
|
|
|
|
|
date = QDateTime::fromTime_t(value.toInt(), Qt::UTC);
|
|
|
|
|
}
|
|
|
|
|
return date;
|
|
|
|
|
}
|
|
|
|
|
} // namespace
|
|
|
|
|
|
2019-05-19 21:49:48 +00:00
|
|
|
void OpVaultReader::fillFromSection(Entry* entry, const QJsonObject& section)
|
|
|
|
|
{
|
|
|
|
|
const auto uuid = entry->uuid();
|
2021-03-24 03:42:31 +00:00
|
|
|
auto sectionTitle = section["title"].toString();
|
2019-05-19 21:49:48 +00:00
|
|
|
|
|
|
|
|
if (!section.contains("fields")) {
|
2021-03-24 03:42:31 +00:00
|
|
|
auto sectionName = section["name"].toString();
|
|
|
|
|
if (!(sectionName.toLower() == "linked items" && sectionTitle.toLower() == "related items")) {
|
2019-05-19 21:49:48 +00:00
|
|
|
qWarning() << R"(Skipping "fields"-less Section in UUID ")" << uuid << "\": <<" << section << ">>";
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
} else if (!section["fields"].isArray()) {
|
|
|
|
|
qWarning() << R"(Skipping non-Array "fields" in UUID ")" << uuid << "\"\n";
|
|
|
|
|
return;
|
|
|
|
|
}
|
2020-04-19 15:50:48 +00:00
|
|
|
|
2019-05-19 21:49:48 +00:00
|
|
|
QJsonArray sectionFields = section["fields"].toArray();
|
|
|
|
|
for (const QJsonValue sectionField : sectionFields) {
|
|
|
|
|
if (!sectionField.isObject()) {
|
|
|
|
|
qWarning() << R"(Skipping non-Object "fields" in UUID ")" << uuid << "\": << " << sectionField << ">>";
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2021-03-24 03:42:31 +00:00
|
|
|
fillFromSectionField(entry, sectionTitle, sectionField.toObject());
|
2019-05-19 21:49:48 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-24 03:42:31 +00:00
|
|
|
void OpVaultReader::fillFromSectionField(Entry* entry, const QString& sectionName, const QJsonObject& field)
|
2019-05-19 21:49:48 +00:00
|
|
|
{
|
|
|
|
|
if (!field.contains("v")) {
|
|
|
|
|
// for our purposes, we don't care if there isn't a value in the field
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Ignore "a" and "inputTraits" fields, they don't apply to KPXC
|
|
|
|
|
|
|
|
|
|
auto attrName = resolveAttributeName(sectionName, field["n"].toString(), field["t"].toString());
|
2020-04-19 15:50:48 +00:00
|
|
|
auto attrValue = field.value("v").toString();
|
2019-05-19 21:49:48 +00:00
|
|
|
auto kind = field["k"].toString();
|
|
|
|
|
|
|
|
|
|
if (attrName.startsWith("TOTP_")) {
|
2022-09-01 10:54:34 +00:00
|
|
|
if (entry->hasTotp()) {
|
|
|
|
|
// Store multiple TOTP definitions as additional otp attributes
|
|
|
|
|
int i = 0;
|
|
|
|
|
QString name("otp");
|
|
|
|
|
auto attributes = entry->attributes()->keys();
|
|
|
|
|
while (attributes.contains(name)) {
|
|
|
|
|
name = QString("otp_%1").arg(++i);
|
|
|
|
|
}
|
2024-03-09 16:59:49 +00:00
|
|
|
entry->attributes()->set(name, attrValue, true);
|
2022-09-01 10:54:34 +00:00
|
|
|
} else if (attrValue.startsWith("otpauth://")) {
|
2019-05-19 21:49:48 +00:00
|
|
|
QUrlQuery query(attrValue);
|
|
|
|
|
// at least as of 1Password 7, they don't append the digits= and period= which totp.cpp requires
|
|
|
|
|
if (!query.hasQueryItem("digits")) {
|
|
|
|
|
query.addQueryItem("digits", QString("%1").arg(Totp::DEFAULT_DIGITS));
|
|
|
|
|
}
|
|
|
|
|
if (!query.hasQueryItem("period")) {
|
|
|
|
|
query.addQueryItem("period", QString("%1").arg(Totp::DEFAULT_STEP));
|
|
|
|
|
}
|
|
|
|
|
attrValue = query.toString(QUrl::FullyEncoded);
|
2020-04-19 15:50:48 +00:00
|
|
|
entry->setTotp(Totp::parseSettings(attrValue));
|
2019-05-19 21:49:48 +00:00
|
|
|
} else {
|
2020-04-19 15:50:48 +00:00
|
|
|
entry->setTotp(Totp::parseSettings({}, attrValue));
|
2019-05-19 21:49:48 +00:00
|
|
|
}
|
|
|
|
|
|
2020-04-19 15:50:48 +00:00
|
|
|
} else if (attrName.startsWith("expir", Qt::CaseInsensitive)) {
|
|
|
|
|
QDateTime expiry = resolveDate(kind, field.value("v"));
|
2019-05-19 21:49:48 +00:00
|
|
|
if (expiry.isValid()) {
|
|
|
|
|
entry->setExpiryTime(expiry);
|
|
|
|
|
entry->setExpires(true);
|
2020-04-19 15:50:48 +00:00
|
|
|
} else {
|
|
|
|
|
qWarning() << QString("[%1] Invalid expiration date found: %2").arg(entry->title(), attrValue);
|
2019-05-19 21:49:48 +00:00
|
|
|
}
|
|
|
|
|
} else {
|
2020-04-19 15:50:48 +00:00
|
|
|
if (kind == "date" || kind == "monthYear") {
|
|
|
|
|
QDateTime date = resolveDate(kind, field.value("v"));
|
2019-05-19 21:49:48 +00:00
|
|
|
if (date.isValid()) {
|
2025-10-25 23:16:31 +00:00
|
|
|
entry->attributes()->set(attrName, QLocale::system().toString(date, QLocale::ShortFormat));
|
2020-04-19 15:50:48 +00:00
|
|
|
} else {
|
|
|
|
|
qWarning()
|
|
|
|
|
<< QString("[%1] Invalid date attribute found: %2 = %3").arg(entry->title(), attrName, attrValue);
|
2019-05-19 21:49:48 +00:00
|
|
|
}
|
2020-04-19 15:50:48 +00:00
|
|
|
} else if (kind == "address") {
|
|
|
|
|
// Expand address into multiple attributes
|
|
|
|
|
auto addrFields = field.value("v").toObject().toVariantMap();
|
2024-03-09 16:59:49 +00:00
|
|
|
for (auto& part : addrFields.keys()) {
|
2020-04-19 15:50:48 +00:00
|
|
|
entry->attributes()->set(attrName + QString("_%1").arg(part), addrFields.value(part).toString());
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2024-03-09 16:59:49 +00:00
|
|
|
if (entry->attributes()->hasKey(attrName)) {
|
|
|
|
|
// Append a random string to the attribute name to avoid collisions
|
|
|
|
|
attrName += QString("_%1").arg(QUuid::createUuid().toString().mid(1, 5));
|
|
|
|
|
}
|
2020-04-19 15:50:48 +00:00
|
|
|
entry->attributes()->set(attrName, attrValue, (kind == "password" || kind == "concealed"));
|
2019-05-19 21:49:48 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QString OpVaultReader::resolveAttributeName(const QString& section, const QString& name, const QString& text)
|
|
|
|
|
{
|
|
|
|
|
// Special case for TOTP
|
|
|
|
|
if (name.startsWith("TOTP_")) {
|
|
|
|
|
return name;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
auto lowName = name.toLower();
|
|
|
|
|
auto lowText = text.toLower();
|
2020-04-19 15:50:48 +00:00
|
|
|
if (section.isEmpty() || name.startsWith("address")) {
|
2019-05-19 21:49:48 +00:00
|
|
|
// Empty section implies these are core attributes
|
|
|
|
|
// try to find username, password, url
|
|
|
|
|
if (lowName == "password" || lowText == "password") {
|
|
|
|
|
return EntryAttributes::PasswordKey;
|
|
|
|
|
} else if (lowName == "username" || lowText == "username") {
|
|
|
|
|
return EntryAttributes::UserNameKey;
|
|
|
|
|
} else if (lowName == "url" || lowText == "url" || lowName == "hostname" || lowText == "server"
|
|
|
|
|
|| lowName == "website") {
|
|
|
|
|
return EntryAttributes::URLKey;
|
|
|
|
|
}
|
2021-03-24 03:42:31 +00:00
|
|
|
return text;
|
2019-05-19 21:49:48 +00:00
|
|
|
}
|
|
|
|
|
|
2021-03-24 03:42:31 +00:00
|
|
|
return QString("%1_%2").arg(section, text);
|
2019-05-19 21:49:48 +00:00
|
|
|
}
|