mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-03-20 05:16:07 -04:00
Extract ShareImport and ShareExport
Moved import/export functionality out of ShareObserver into tooling classes ShareImport and ShareExport
This commit is contained in:
parent
37c53f326c
commit
07da5de880
@ -12,6 +12,8 @@ if(WITH_XC_KEESHARE)
|
||||
group/EditGroupPageKeeShare.cpp
|
||||
KeeShare.cpp
|
||||
KeeShareSettings.cpp
|
||||
ShareImport.cpp
|
||||
ShareExport.cpp
|
||||
ShareObserver.cpp
|
||||
Signature.cpp
|
||||
)
|
||||
|
@ -246,21 +246,38 @@ void KeeShare::connectDatabase(QSharedPointer<Database> newDb, QSharedPointer<Da
|
||||
}
|
||||
}
|
||||
|
||||
const QString& KeeShare::signedContainerFileType()
|
||||
const QString KeeShare::signedContainerFileType()
|
||||
{
|
||||
static const QString filetype("kdbx.share");
|
||||
return filetype;
|
||||
}
|
||||
|
||||
const QString& KeeShare::unsignedContainerFileType()
|
||||
const QString KeeShare::unsignedContainerFileType()
|
||||
{
|
||||
static const QString filetype("kdbx");
|
||||
return filetype;
|
||||
}
|
||||
|
||||
bool KeeShare::isContainerType(const QFileInfo& fileInfo, const QString type)
|
||||
{
|
||||
return fileInfo.fileName().endsWith(type, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
void KeeShare::handleSettingsChanged(const QString& key)
|
||||
{
|
||||
if (key == KeeShare_Active) {
|
||||
emit activeChanged();
|
||||
}
|
||||
}
|
||||
|
||||
const QString KeeShare::signatureFileName()
|
||||
{
|
||||
static const QString fileName("container.share.signature");
|
||||
return fileName;
|
||||
}
|
||||
|
||||
const QString KeeShare::containerFileName()
|
||||
{
|
||||
static const QString fileName("container.share.kdbx");
|
||||
return fileName;
|
||||
}
|
||||
|
@ -59,9 +59,12 @@ public:
|
||||
|
||||
void connectDatabase(QSharedPointer<Database> newDb, QSharedPointer<Database> oldDb);
|
||||
|
||||
static const QString& signedContainerFileType();
|
||||
static const QString& unsignedContainerFileType();
|
||||
static const QString signedContainerFileType();
|
||||
static const QString unsignedContainerFileType();
|
||||
static bool isContainerType(const QFileInfo& fileInfo, const QString type);
|
||||
|
||||
static const QString signatureFileName();
|
||||
static const QString containerFileName();
|
||||
signals:
|
||||
void activeChanged();
|
||||
void sharingMessage(QString, MessageWidget::MessageType);
|
||||
|
233
src/keeshare/ShareExport.cpp
Normal file
233
src/keeshare/ShareExport.cpp
Normal file
@ -0,0 +1,233 @@
|
||||
/*
|
||||
* 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 "ShareExport.h"
|
||||
#include "config-keepassx.h"
|
||||
#include "core/Group.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "format/KeePass2Writer.h"
|
||||
#include "keeshare/KeeShare.h"
|
||||
#include "keeshare/Signature.h"
|
||||
#include "keys/PasswordKey.h"
|
||||
|
||||
#if defined(WITH_XC_KEESHARE_SECURE)
|
||||
#include <quazip.h>
|
||||
#include <quazipfile.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
void resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb)
|
||||
{
|
||||
for (const auto& attribute : EntryAttributes::DefaultAttributes) {
|
||||
const auto standardValue = targetEntry->attributes()->value(attribute);
|
||||
const auto type = targetEntry->placeholderType(standardValue);
|
||||
if (type != Entry::PlaceholderType::Reference) {
|
||||
// No reference to resolve
|
||||
continue;
|
||||
}
|
||||
const auto* referencedTargetEntry = targetEntry->resolveReference(standardValue);
|
||||
if (referencedTargetEntry) {
|
||||
// References is within scope, no resolving needed
|
||||
continue;
|
||||
}
|
||||
// We could do more sophisticated **** trying to point the reference to the next in-scope reference
|
||||
// but those cases with high propability constructed examples and very rare in real usage
|
||||
const auto* sourceReference = sourceDb->rootGroup()->findEntryByUuid(targetEntry->uuid());
|
||||
const auto resolvedValue = sourceReference->resolveMultiplePlaceholders(standardValue);
|
||||
targetEntry->setUpdateTimeinfo(false);
|
||||
targetEntry->attributes()->set(attribute, resolvedValue, targetEntry->attributes()->isProtected(attribute));
|
||||
targetEntry->setUpdateTimeinfo(true);
|
||||
}
|
||||
}
|
||||
|
||||
Database* extractIntoDatabase(const KeeShareSettings::Reference& reference, const Group* sourceRoot)
|
||||
{
|
||||
const auto* sourceDb = sourceRoot->database();
|
||||
auto* targetDb = new Database();
|
||||
auto* targetMetadata = targetDb->metadata();
|
||||
targetMetadata->setRecycleBinEnabled(false);
|
||||
auto key = QSharedPointer<CompositeKey>::create();
|
||||
key->addKey(QSharedPointer<PasswordKey>::create(reference.password));
|
||||
|
||||
// Copy the source root as the root of the export database, memory manage the old root node
|
||||
auto* targetRoot = sourceRoot->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
|
||||
const bool updateTimeinfo = targetRoot->canUpdateTimeinfo();
|
||||
targetRoot->setUpdateTimeinfo(false);
|
||||
KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference());
|
||||
targetRoot->setUpdateTimeinfo(updateTimeinfo);
|
||||
const auto sourceEntries = sourceRoot->entriesRecursive(false);
|
||||
for (const Entry* sourceEntry : sourceEntries) {
|
||||
auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
|
||||
const bool updateTimeinfo = targetEntry->canUpdateTimeinfo();
|
||||
targetEntry->setUpdateTimeinfo(false);
|
||||
targetEntry->setGroup(targetRoot);
|
||||
targetEntry->setUpdateTimeinfo(updateTimeinfo);
|
||||
const auto iconUuid = targetEntry->iconUuid();
|
||||
if (!iconUuid.isNull() && !targetMetadata->containsCustomIcon(iconUuid)) {
|
||||
targetMetadata->addCustomIcon(iconUuid, sourceEntry->icon());
|
||||
}
|
||||
}
|
||||
|
||||
targetDb->setKey(key);
|
||||
auto* obsoleteRoot = targetDb->rootGroup();
|
||||
targetDb->setRootGroup(targetRoot);
|
||||
delete obsoleteRoot;
|
||||
|
||||
targetDb->metadata()->setName(sourceRoot->name());
|
||||
|
||||
// Push all deletions of the source database to the target
|
||||
// simple moving out of a share group will not trigger a deletion in the
|
||||
// target - a more elaborate mechanism may need the use of another custom
|
||||
// attribute to share unshared entries from the target db
|
||||
for (const auto& object : sourceDb->deletedObjects()) {
|
||||
targetDb->addDeletedObject(object);
|
||||
}
|
||||
for (auto* targetEntry : targetRoot->entriesRecursive(false)) {
|
||||
if (targetEntry->hasReferences()) {
|
||||
resolveReferenceAttributes(targetEntry, sourceDb);
|
||||
}
|
||||
}
|
||||
return targetDb;
|
||||
}
|
||||
|
||||
ShareObserver::Result
|
||||
intoSignedContainer(const QString& resolvedPath, const KeeShareSettings::Reference& reference, Database* targetDb)
|
||||
{
|
||||
#if !defined(WITH_XC_KEESHARE_SECURE)
|
||||
Q_UNUSED(targetDb);
|
||||
Q_UNUSED(resolvedPath);
|
||||
return {reference.path,
|
||||
ShareObserver::Result::Warning,
|
||||
ShareExport::tr("Overwriting signed share container is not supported - export prevented")};
|
||||
#else
|
||||
QByteArray bytes;
|
||||
{
|
||||
QBuffer buffer(&bytes);
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
KeePass2Writer writer;
|
||||
writer.writeDatabase(&buffer, targetDb);
|
||||
if (writer.hasError()) {
|
||||
qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data());
|
||||
return {reference.path, ShareObserver::Result::Error, writer.errorString()};
|
||||
}
|
||||
}
|
||||
const auto own = KeeShare::own();
|
||||
QuaZip zip(resolvedPath);
|
||||
zip.setFileNameCodec("UTF-8");
|
||||
const bool zipOpened = zip.open(QuaZip::mdCreate);
|
||||
if (!zipOpened) {
|
||||
::qWarning("Opening export file failed: %d", zip.getZipError());
|
||||
return {reference.path,
|
||||
ShareObserver::Result::Error,
|
||||
ShareExport::tr("Could not write export container (%1)").arg(zip.getZipError())};
|
||||
}
|
||||
{
|
||||
QuaZipFile file(&zip);
|
||||
const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare::signatureFileName()));
|
||||
if (!signatureOpened) {
|
||||
::qWarning("Embedding signature failed: Could not open file to write (%d)", zip.getZipError());
|
||||
return {reference.path,
|
||||
ShareObserver::Result::Error,
|
||||
ShareExport::tr("Could not embed signature: Could not open file to write (%1)")
|
||||
.arg(file.getZipError())};
|
||||
}
|
||||
QTextStream stream(&file);
|
||||
KeeShareSettings::Sign sign;
|
||||
auto sshKey = own.key.sshKey();
|
||||
sshKey.openKey(QString());
|
||||
const Signature signer;
|
||||
sign.signature = signer.create(bytes, sshKey);
|
||||
sign.certificate = own.certificate;
|
||||
stream << KeeShareSettings::Sign::serialize(sign);
|
||||
stream.flush();
|
||||
if (file.getZipError() != ZIP_OK) {
|
||||
::qWarning("Embedding signature failed: Could not write file (%d)", zip.getZipError());
|
||||
return {
|
||||
reference.path,
|
||||
ShareObserver::Result::Error,
|
||||
ShareExport::tr("Could not embed signature: Could not write file (%1)").arg(file.getZipError())};
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
{
|
||||
QuaZipFile file(&zip);
|
||||
const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare::containerFileName()));
|
||||
if (!dbOpened) {
|
||||
::qWarning("Embedding database failed: Could not open file to write (%d)", zip.getZipError());
|
||||
return {reference.path,
|
||||
ShareObserver::Result::Error,
|
||||
ShareExport::tr("Could not embed database: Could not open file to write (%1)")
|
||||
.arg(file.getZipError())};
|
||||
}
|
||||
file.write(bytes);
|
||||
if (file.getZipError() != ZIP_OK) {
|
||||
::qWarning("Embedding database failed: Could not write file (%d)", zip.getZipError());
|
||||
return {reference.path,
|
||||
ShareObserver::Result::Error,
|
||||
ShareExport::tr("Could not embed database: Could not write file (%1)").arg(file.getZipError())};
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
zip.close();
|
||||
return {reference.path};
|
||||
#endif
|
||||
}
|
||||
|
||||
ShareObserver::Result
|
||||
intoUnsignedContainer(const QString& resolvedPath, const KeeShareSettings::Reference& reference, Database* targetDb)
|
||||
{
|
||||
#if !defined(WITH_XC_KEESHARE_INSECURE)
|
||||
Q_UNUSED(targetDb);
|
||||
Q_UNUSED(resolvedPath);
|
||||
return {reference.path,
|
||||
ShareObserver::Result::Warning,
|
||||
ShareExport::tr("Overwriting unsigned share container is not supported - export prevented")};
|
||||
#else
|
||||
QFile file(resolvedPath);
|
||||
const bool fileOpened = file.open(QIODevice::WriteOnly);
|
||||
if (!fileOpened) {
|
||||
::qWarning("Opening export file failed");
|
||||
return {reference.path, ShareObserver::Result::Error, ShareExport::tr("Could not write export container")};
|
||||
}
|
||||
KeePass2Writer writer;
|
||||
writer.writeDatabase(&file, targetDb);
|
||||
if (writer.hasError()) {
|
||||
qWarning("Exporting dabase failed: %s.", writer.errorString().toLatin1().data());
|
||||
return {reference.path, ShareObserver::Result::Error, writer.errorString()};
|
||||
}
|
||||
file.close();
|
||||
#endif
|
||||
return {reference.path};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ShareObserver::Result ShareExport::intoContainer(const QString& resolvedPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
const Group* group)
|
||||
{
|
||||
QScopedPointer<Database> targetDb(extractIntoDatabase(reference, group));
|
||||
const QFileInfo info(resolvedPath);
|
||||
if (KeeShare::isContainerType(info, KeeShare::signedContainerFileType())) {
|
||||
return intoSignedContainer(resolvedPath, reference, targetDb.data());
|
||||
}
|
||||
if (KeeShare::isContainerType(info, KeeShare::unsignedContainerFileType())) {
|
||||
return intoUnsignedContainer(resolvedPath, reference, targetDb.data());
|
||||
}
|
||||
Q_ASSERT(false);
|
||||
return {reference.path, ShareObserver::Result::Error, tr("Unexpected export error occurred")};
|
||||
}
|
36
src/keeshare/ShareExport.h
Normal file
36
src/keeshare/ShareExport.h
Normal file
@ -0,0 +1,36 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
#ifndef KEEPASSXC_SHAREEXPORT_H
|
||||
#define KEEPASSXC_SHAREEXPORT_H
|
||||
|
||||
#include "keeshare/KeeShareSettings.h"
|
||||
#include "keeshare/ShareObserver.h"
|
||||
|
||||
class Database;
|
||||
|
||||
class ShareExport
|
||||
{
|
||||
Q_DECLARE_TR_FUNCTIONS(ShareExport)
|
||||
public:
|
||||
static ShareObserver::Result
|
||||
intoContainer(const QString& resolvedPath, const KeeShareSettings::Reference& reference, const Group* group);
|
||||
|
||||
private:
|
||||
ShareExport() = delete;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_SHAREEXPORT_H
|
351
src/keeshare/ShareImport.cpp
Normal file
351
src/keeshare/ShareImport.cpp
Normal file
@ -0,0 +1,351 @@
|
||||
/*
|
||||
* 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 "ShareImport.h"
|
||||
#include "config-keepassx.h"
|
||||
#include "core/Merger.h"
|
||||
#include "format/KeePass2Reader.h"
|
||||
#include "keeshare/KeeShare.h"
|
||||
#include "keeshare/Signature.h"
|
||||
#include "keys/PasswordKey.h"
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QPushButton>
|
||||
|
||||
#if defined(WITH_XC_KEESHARE_SECURE)
|
||||
#include <quazip.h>
|
||||
#include <quazipfile.h>
|
||||
#endif
|
||||
|
||||
namespace
|
||||
{
|
||||
enum Trust
|
||||
{
|
||||
Invalid,
|
||||
Own,
|
||||
UntrustedForever,
|
||||
UntrustedOnce,
|
||||
TrustedOnce,
|
||||
TrustedForever,
|
||||
};
|
||||
|
||||
QPair<Trust, KeeShareSettings::Certificate>
|
||||
check(QByteArray& data,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
const KeeShareSettings::Certificate& ownCertificate,
|
||||
const QList<KeeShareSettings::ScopedCertificate>& knownCertificates,
|
||||
const KeeShareSettings::Sign& sign)
|
||||
{
|
||||
KeeShareSettings::Certificate certificate;
|
||||
if (!sign.signature.isEmpty()) {
|
||||
certificate = sign.certificate;
|
||||
auto key = sign.certificate.sshKey();
|
||||
key.openKey(QString());
|
||||
const auto signer = Signature();
|
||||
if (!signer.verify(data, sign.signature, key)) {
|
||||
qCritical("Invalid signature for shared container %s.", qPrintable(reference.path));
|
||||
return {Invalid, KeeShareSettings::Certificate()};
|
||||
}
|
||||
|
||||
if (ownCertificate.key == sign.certificate.key) {
|
||||
return {Own, ownCertificate};
|
||||
}
|
||||
}
|
||||
enum Scope
|
||||
{
|
||||
Invalid,
|
||||
Global,
|
||||
Local
|
||||
};
|
||||
Scope scope = Invalid;
|
||||
KeeShareSettings::Trust trusted = KeeShareSettings::Trust::Ask;
|
||||
for (const auto& scopedCertificate : knownCertificates) {
|
||||
if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) {
|
||||
// Global scope is overwritten by local scope
|
||||
scope = Global;
|
||||
trusted = scopedCertificate.trust;
|
||||
}
|
||||
if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) {
|
||||
scope = Local;
|
||||
trusted = scopedCertificate.trust;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (scope != Invalid && trusted != KeeShareSettings::Trust::Ask) {
|
||||
// we introduce now scopes if there is a global
|
||||
return {trusted == KeeShareSettings::Trust::Trusted ? TrustedForever : UntrustedForever, certificate};
|
||||
}
|
||||
|
||||
QMessageBox warning;
|
||||
if (sign.signature.isEmpty()) {
|
||||
warning.setIcon(QMessageBox::Warning);
|
||||
warning.setWindowTitle(ShareImport::tr("Import from container without signature"));
|
||||
warning.setText(ShareImport::tr("We cannot verify the source of the shared container because it is not "
|
||||
"signed. Do you really want to import from %1?")
|
||||
.arg(reference.path));
|
||||
} else {
|
||||
warning.setIcon(QMessageBox::Question);
|
||||
warning.setWindowTitle(ShareImport::tr("Import from container with certificate"));
|
||||
warning.setText(ShareImport::tr("Do you want to trust %1 with the fingerprint of %2 from %3?")
|
||||
.arg(certificate.signer, certificate.fingerprint(), reference.path));
|
||||
}
|
||||
auto untrustedOnce = warning.addButton(ShareImport::tr("Not this time"), QMessageBox::ButtonRole::NoRole);
|
||||
auto untrustedForever = warning.addButton(ShareImport::tr("Never"), QMessageBox::ButtonRole::NoRole);
|
||||
auto trustedForever = warning.addButton(ShareImport::tr("Always"), QMessageBox::ButtonRole::YesRole);
|
||||
auto trustedOnce = warning.addButton(ShareImport::tr("Just this time"), QMessageBox::ButtonRole::YesRole);
|
||||
warning.setDefaultButton(untrustedOnce);
|
||||
warning.exec();
|
||||
if (warning.clickedButton() == trustedForever) {
|
||||
return {TrustedForever, certificate};
|
||||
}
|
||||
if (warning.clickedButton() == trustedOnce) {
|
||||
return {TrustedOnce, certificate};
|
||||
}
|
||||
if (warning.clickedButton() == untrustedOnce) {
|
||||
return {UntrustedOnce, certificate};
|
||||
}
|
||||
if (warning.clickedButton() == untrustedForever) {
|
||||
return {UntrustedForever, certificate};
|
||||
}
|
||||
return {UntrustedOnce, certificate};
|
||||
}
|
||||
|
||||
ShareObserver::Result
|
||||
signedContainerInto(const QString& resolvedPath, const KeeShareSettings::Reference& reference, Group* targetGroup)
|
||||
{
|
||||
#if !defined(WITH_XC_KEESHARE_SECURE)
|
||||
Q_UNUSED(targetGroup);
|
||||
Q_UNUSED(resolvedPath);
|
||||
return {reference.path,
|
||||
ShareObserver::Result::Warning,
|
||||
ShareImport::tr("Signed share container are not supported - import prevented")};
|
||||
#else
|
||||
QuaZip zip(resolvedPath);
|
||||
if (!zip.open(QuaZip::mdUnzip)) {
|
||||
qCritical("Unable to open file %s.", qPrintable(reference.path));
|
||||
return {reference.path, ShareObserver::Result::Error, ShareImport::tr("File is not readable")};
|
||||
}
|
||||
const auto expected = QSet<QString>() << KeeShare::signatureFileName() << KeeShare::containerFileName();
|
||||
const auto files = zip.getFileInfoList();
|
||||
QSet<QString> actual;
|
||||
for (const auto& file : files) {
|
||||
actual << file.name;
|
||||
}
|
||||
if (expected != actual) {
|
||||
qCritical("Invalid sharing container %s.", qPrintable(reference.path));
|
||||
return {reference.path, ShareObserver::Result::Error, ShareImport::tr("Invalid sharing container")};
|
||||
}
|
||||
|
||||
zip.setCurrentFile(KeeShare::signatureFileName());
|
||||
QuaZipFile signatureFile(&zip);
|
||||
signatureFile.open(QuaZipFile::ReadOnly);
|
||||
QTextStream stream(&signatureFile);
|
||||
|
||||
const auto sign = KeeShareSettings::Sign::deserialize(stream.readAll());
|
||||
signatureFile.close();
|
||||
|
||||
zip.setCurrentFile(KeeShare::containerFileName());
|
||||
QuaZipFile databaseFile(&zip);
|
||||
databaseFile.open(QuaZipFile::ReadOnly);
|
||||
auto payload = databaseFile.readAll();
|
||||
databaseFile.close();
|
||||
QBuffer buffer(&payload);
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
|
||||
KeePass2Reader reader;
|
||||
auto key = QSharedPointer<CompositeKey>::create();
|
||||
key->addKey(QSharedPointer<PasswordKey>::create(reference.password));
|
||||
auto sourceDb = QSharedPointer<Database>::create();
|
||||
if (!reader.readDatabase(&buffer, key, sourceDb.data())) {
|
||||
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
|
||||
return {reference.path, ShareObserver::Result::Error, reader.errorString()};
|
||||
}
|
||||
|
||||
auto foreign = KeeShare::foreign();
|
||||
auto own = KeeShare::own();
|
||||
auto trust = check(payload, reference, own.certificate, foreign.certificates, sign);
|
||||
switch (trust.first) {
|
||||
case Invalid:
|
||||
qWarning("Prevent untrusted import");
|
||||
return {reference.path, ShareObserver::Result::Error, ShareImport::tr("Untrusted import prevented")};
|
||||
|
||||
case UntrustedForever:
|
||||
case TrustedForever: {
|
||||
bool found = false;
|
||||
const auto trusted =
|
||||
trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted;
|
||||
for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) {
|
||||
if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) {
|
||||
scopedCertificate.certificate.signer = trust.second.signer;
|
||||
scopedCertificate.path = reference.path;
|
||||
scopedCertificate.trust = trusted;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
foreign.certificates << KeeShareSettings::ScopedCertificate{reference.path, trust.second, trusted};
|
||||
}
|
||||
// update foreign certificates with new settings
|
||||
KeeShare::setForeign(foreign);
|
||||
|
||||
if (trust.first == TrustedForever) {
|
||||
qDebug("Synchronize %s %s with %s",
|
||||
qPrintable(reference.path),
|
||||
qPrintable(targetGroup->name()),
|
||||
qPrintable(sourceDb->rootGroup()->name()));
|
||||
Merger merger(sourceDb->rootGroup(), targetGroup);
|
||||
merger.setForcedMergeMode(Group::Synchronize);
|
||||
auto changelist = merger.merge();
|
||||
if (!changelist.isEmpty()) {
|
||||
return {
|
||||
reference.path, ShareObserver::Result::Success, ShareImport::tr("Successful signed import")};
|
||||
}
|
||||
}
|
||||
// Silent ignore of untrusted import or unchanging import
|
||||
return {};
|
||||
}
|
||||
case TrustedOnce:
|
||||
case Own: {
|
||||
qDebug("Synchronize %s %s with %s",
|
||||
qPrintable(reference.path),
|
||||
qPrintable(targetGroup->name()),
|
||||
qPrintable(sourceDb->rootGroup()->name()));
|
||||
Merger merger(sourceDb->rootGroup(), targetGroup);
|
||||
merger.setForcedMergeMode(Group::Synchronize);
|
||||
auto changelist = merger.merge();
|
||||
if (!changelist.isEmpty()) {
|
||||
return {reference.path, ShareObserver::Result::Success, ShareImport::tr("Successful signed import")};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return {reference.path, ShareObserver::Result::Error, ShareImport::tr("Unexpected error")};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
ShareObserver::Result
|
||||
unsignedContainerInto(const QString& resolvedPath, const KeeShareSettings::Reference& reference, Group* targetGroup)
|
||||
{
|
||||
#if !defined(WITH_XC_KEESHARE_INSECURE)
|
||||
Q_UNUSED(targetGroup);
|
||||
Q_UNUSED(resolvedPath);
|
||||
return {reference.path,
|
||||
ShareObserver::Result::Warning,
|
||||
tr("Unsigned share container are not supported - import prevented")};
|
||||
#else
|
||||
QFile file(resolvedPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qCritical("Unable to open file %s.", qPrintable(reference.path));
|
||||
return {reference.path, ShareObserver::Result::Error, ShareImport::tr("File is not readable")};
|
||||
}
|
||||
auto payload = file.readAll();
|
||||
file.close();
|
||||
QBuffer buffer(&payload);
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
|
||||
KeePass2Reader reader;
|
||||
auto key = QSharedPointer<CompositeKey>::create();
|
||||
key->addKey(QSharedPointer<PasswordKey>::create(reference.password));
|
||||
auto sourceDb = QSharedPointer<Database>::create();
|
||||
if (!reader.readDatabase(&buffer, key, sourceDb.data())) {
|
||||
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
|
||||
return {reference.path, ShareObserver::Result::Error, reader.errorString()};
|
||||
}
|
||||
|
||||
auto foreign = KeeShare::foreign();
|
||||
const auto own = KeeShare::own();
|
||||
const auto sign = KeeShareSettings::Sign(); // invalid sign
|
||||
auto trust = check(payload, reference, own.certificate, foreign.certificates, sign);
|
||||
switch (trust.first) {
|
||||
case UntrustedForever:
|
||||
case TrustedForever: {
|
||||
bool found = false;
|
||||
const auto trusted =
|
||||
trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted;
|
||||
for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) {
|
||||
if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) {
|
||||
scopedCertificate.certificate.signer = trust.second.signer;
|
||||
scopedCertificate.path = reference.path;
|
||||
scopedCertificate.trust = trusted;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
foreign.certificates << KeeShareSettings::ScopedCertificate{reference.path, trust.second, trusted};
|
||||
}
|
||||
// update foreign certificates with new settings
|
||||
KeeShare::setForeign(foreign);
|
||||
|
||||
if (trust.first == TrustedForever) {
|
||||
qDebug("Synchronize %s %s with %s",
|
||||
qPrintable(reference.path),
|
||||
qPrintable(targetGroup->name()),
|
||||
qPrintable(sourceDb->rootGroup()->name()));
|
||||
Merger merger(sourceDb->rootGroup(), targetGroup);
|
||||
merger.setForcedMergeMode(Group::Synchronize);
|
||||
auto changelist = merger.merge();
|
||||
if (!changelist.isEmpty()) {
|
||||
return {
|
||||
reference.path, ShareObserver::Result::Success, ShareImport::tr("Successful signed import")};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
case TrustedOnce: {
|
||||
qDebug("Synchronize %s %s with %s",
|
||||
qPrintable(reference.path),
|
||||
qPrintable(targetGroup->name()),
|
||||
qPrintable(sourceDb->rootGroup()->name()));
|
||||
Merger merger(sourceDb->rootGroup(), targetGroup);
|
||||
merger.setForcedMergeMode(Group::Synchronize);
|
||||
auto changelist = merger.merge();
|
||||
if (!changelist.isEmpty()) {
|
||||
return {reference.path, ShareObserver::Result::Success, ShareImport::tr("Successful unsigned import")};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
default:
|
||||
qWarning("Prevent untrusted import");
|
||||
return {reference.path, ShareObserver::Result::Warning, ShareImport::tr("Untrusted import prevented")};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
ShareObserver::Result ShareImport::containerInto(const QString& resolvedPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Group* targetGroup)
|
||||
{
|
||||
const QFileInfo info(resolvedPath);
|
||||
if (!info.exists()) {
|
||||
qCritical("File %s does not exist.", qPrintable(info.absoluteFilePath()));
|
||||
return {reference.path, ShareObserver::Result::Warning, tr("File does not exist")};
|
||||
}
|
||||
|
||||
if (KeeShare::isContainerType(info, KeeShare::signedContainerFileType())) {
|
||||
return signedContainerInto(resolvedPath, reference, targetGroup);
|
||||
}
|
||||
if (KeeShare::isContainerType(info, KeeShare::unsignedContainerFileType())) {
|
||||
return unsignedContainerInto(resolvedPath, reference, targetGroup);
|
||||
}
|
||||
return {reference.path, ShareObserver::Result::Error, tr("Unknown share container type")};
|
||||
}
|
33
src/keeshare/ShareImport.h
Normal file
33
src/keeshare/ShareImport.h
Normal file
@ -0,0 +1,33 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
#ifndef KEEPASSXC_SHAREIMPORT_H
|
||||
#define KEEPASSXC_SHAREIMPORT_H
|
||||
|
||||
#include "keeshare/ShareObserver.h"
|
||||
|
||||
class ShareImport
|
||||
{
|
||||
Q_DECLARE_TR_FUNCTIONS(ShareImport)
|
||||
public:
|
||||
static ShareObserver::Result
|
||||
containerInto(const QString& resolvedPath, const KeeShareSettings::Reference& reference, Group* targetGroup);
|
||||
|
||||
public:
|
||||
ShareImport() = delete;
|
||||
};
|
||||
|
||||
#endif // KEEPASSXC_SHAREIMPORT_H
|
@ -16,147 +16,22 @@
|
||||
*/
|
||||
|
||||
#include "ShareObserver.h"
|
||||
#include "config-keepassx.h"
|
||||
#include "core/Clock.h"
|
||||
#include "core/Config.h"
|
||||
#include "core/CustomData.h"
|
||||
#include "core/Database.h"
|
||||
#include "core/DatabaseIcons.h"
|
||||
#include "core/Entry.h"
|
||||
#include "core/FilePath.h"
|
||||
#include "core/FileWatcher.h"
|
||||
#include "core/Global.h"
|
||||
#include "core/Group.h"
|
||||
#include "core/Merger.h"
|
||||
#include "core/Metadata.h"
|
||||
#include "format/KeePass2Reader.h"
|
||||
#include "format/KeePass2Writer.h"
|
||||
#include "keeshare/KeeShare.h"
|
||||
#include "keeshare/KeeShareSettings.h"
|
||||
#include "keeshare/Signature.h"
|
||||
#include "keys/PasswordKey.h"
|
||||
|
||||
#include <QBuffer>
|
||||
#include <QDebug>
|
||||
#include <QFileInfo>
|
||||
#include <QIcon>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
#include <QPushButton>
|
||||
#include <QStringBuilder>
|
||||
|
||||
#if defined(WITH_XC_KEESHARE_SECURE)
|
||||
#include <quazip.h>
|
||||
#include <quazipfile.h>
|
||||
#endif
|
||||
#include "keeshare/ShareExport.h"
|
||||
#include "keeshare/ShareImport.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
static const QString KeeShare_Signature("container.share.signature");
|
||||
static const QString KeeShare_Container("container.share.kdbx");
|
||||
|
||||
enum Trust
|
||||
{
|
||||
Invalid,
|
||||
Own,
|
||||
UntrustedForever,
|
||||
UntrustedOnce,
|
||||
TrustedOnce,
|
||||
TrustedForever,
|
||||
};
|
||||
|
||||
bool isOfExportType(const QFileInfo& fileInfo, const QString type)
|
||||
{
|
||||
return fileInfo.fileName().endsWith(type, Qt::CaseInsensitive);
|
||||
}
|
||||
|
||||
QPair<Trust, KeeShareSettings::Certificate>
|
||||
check(QByteArray& data,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
const KeeShareSettings::Certificate& ownCertificate,
|
||||
const QList<KeeShareSettings::ScopedCertificate>& knownCertificates,
|
||||
const KeeShareSettings::Sign& sign)
|
||||
{
|
||||
KeeShareSettings::Certificate certificate;
|
||||
if (!sign.signature.isEmpty()) {
|
||||
certificate = sign.certificate;
|
||||
auto key = sign.certificate.sshKey();
|
||||
key.openKey(QString());
|
||||
const auto signer = Signature();
|
||||
if (!signer.verify(data, sign.signature, key)) {
|
||||
qCritical("Invalid signature for shared container %s.", qPrintable(reference.path));
|
||||
return {Invalid, KeeShareSettings::Certificate()};
|
||||
}
|
||||
|
||||
if (ownCertificate.key == sign.certificate.key) {
|
||||
return {Own, ownCertificate};
|
||||
}
|
||||
}
|
||||
enum Scope
|
||||
{
|
||||
Invalid,
|
||||
Global,
|
||||
Local
|
||||
};
|
||||
Scope scope = Invalid;
|
||||
KeeShareSettings::Trust trusted = KeeShareSettings::Trust::Ask;
|
||||
for (const auto& scopedCertificate : knownCertificates) {
|
||||
if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) {
|
||||
// Global scope is overwritten by local scope
|
||||
scope = Global;
|
||||
trusted = scopedCertificate.trust;
|
||||
}
|
||||
if (scopedCertificate.certificate.key == certificate.key && scopedCertificate.path == reference.path) {
|
||||
scope = Local;
|
||||
trusted = scopedCertificate.trust;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (scope != Invalid && trusted != KeeShareSettings::Trust::Ask) {
|
||||
// we introduce now scopes if there is a global
|
||||
return {trusted == KeeShareSettings::Trust::Trusted ? TrustedForever : UntrustedForever, certificate};
|
||||
}
|
||||
|
||||
QMessageBox warning;
|
||||
if (sign.signature.isEmpty()) {
|
||||
warning.setIcon(QMessageBox::Warning);
|
||||
warning.setWindowTitle(ShareObserver::tr("Import from container without signature"));
|
||||
warning.setText(ShareObserver::tr("We cannot verify the source of the shared container because it is not "
|
||||
"signed. Do you really want to import from %1?")
|
||||
.arg(reference.path));
|
||||
} else {
|
||||
warning.setIcon(QMessageBox::Question);
|
||||
warning.setWindowTitle(ShareObserver::tr("Import from container with certificate"));
|
||||
warning.setText(ShareObserver::tr("Do you want to trust %1 with the fingerprint of %2 from %3?")
|
||||
.arg(certificate.signer, certificate.fingerprint(), reference.path));
|
||||
}
|
||||
auto untrustedOnce = warning.addButton(ShareObserver::tr("Not this time"), QMessageBox::ButtonRole::NoRole);
|
||||
auto untrustedForever = warning.addButton(ShareObserver::tr("Never"), QMessageBox::ButtonRole::NoRole);
|
||||
auto trustedForever = warning.addButton(ShareObserver::tr("Always"), QMessageBox::ButtonRole::YesRole);
|
||||
auto trustedOnce = warning.addButton(ShareObserver::tr("Just this time"), QMessageBox::ButtonRole::YesRole);
|
||||
warning.setDefaultButton(untrustedOnce);
|
||||
warning.exec();
|
||||
if (warning.clickedButton() == trustedForever) {
|
||||
return {TrustedForever, certificate};
|
||||
}
|
||||
if (warning.clickedButton() == trustedOnce) {
|
||||
return {TrustedOnce, certificate};
|
||||
}
|
||||
if (warning.clickedButton() == untrustedOnce) {
|
||||
return {UntrustedOnce, certificate};
|
||||
}
|
||||
if (warning.clickedButton() == untrustedForever) {
|
||||
return {UntrustedForever, certificate};
|
||||
}
|
||||
return {UntrustedOnce, certificate};
|
||||
}
|
||||
|
||||
QString resolvePath(const QString& path, QSharedPointer<Database> database)
|
||||
{
|
||||
const QFileInfo info(database->filePath());
|
||||
return info.absoluteDir().absoluteFilePath(path);
|
||||
}
|
||||
|
||||
} // End Namespace
|
||||
|
||||
ShareObserver::ShareObserver(QSharedPointer<Database> db, QObject* parent)
|
||||
@ -238,7 +113,7 @@ void ShareObserver::reinitialize()
|
||||
|
||||
if (update.newReference.isImporting()) {
|
||||
imported[update.newReference.path] << update.group->name();
|
||||
const auto result = this->importFromReferenceContainer(update.newReference.path);
|
||||
const auto result = this->importShare(update.newReference.path);
|
||||
if (!result.isValid()) {
|
||||
// tolerable result - blocked import or missing source
|
||||
continue;
|
||||
@ -317,7 +192,7 @@ void ShareObserver::handleFileDeleted(const QString& path)
|
||||
|
||||
void ShareObserver::handleFileUpdated(const QString& path)
|
||||
{
|
||||
const Result result = this->importFromReferenceContainer(path);
|
||||
const Result result = this->importShare(path);
|
||||
if (!result.isValid()) {
|
||||
return;
|
||||
}
|
||||
@ -336,228 +211,7 @@ void ShareObserver::handleFileUpdated(const QString& path)
|
||||
notifyAbout(success, warning, error);
|
||||
}
|
||||
|
||||
ShareObserver::Result ShareObserver::importSignedContainerInto(const QString& realPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Group* targetGroup)
|
||||
{
|
||||
#if !defined(WITH_XC_KEESHARE_SECURE)
|
||||
Q_UNUSED(targetGroup);
|
||||
Q_UNUSED(realPath);
|
||||
return {reference.path, Result::Warning, tr("Signed share container are not supported - import prevented")};
|
||||
#else
|
||||
QuaZip zip(realPath);
|
||||
if (!zip.open(QuaZip::mdUnzip)) {
|
||||
qCritical("Unable to open file %s.", qPrintable(reference.path));
|
||||
return {reference.path, Result::Error, tr("File is not readable")};
|
||||
}
|
||||
const auto expected = QSet<QString>() << KeeShare_Signature << KeeShare_Container;
|
||||
const auto files = zip.getFileInfoList();
|
||||
QSet<QString> actual;
|
||||
for (const auto& file : files) {
|
||||
actual << file.name;
|
||||
}
|
||||
if (expected != actual) {
|
||||
qCritical("Invalid sharing container %s.", qPrintable(reference.path));
|
||||
return {reference.path, Result::Error, tr("Invalid sharing container")};
|
||||
}
|
||||
|
||||
zip.setCurrentFile(KeeShare_Signature);
|
||||
QuaZipFile signatureFile(&zip);
|
||||
signatureFile.open(QuaZipFile::ReadOnly);
|
||||
QTextStream stream(&signatureFile);
|
||||
|
||||
const auto sign = KeeShareSettings::Sign::deserialize(stream.readAll());
|
||||
signatureFile.close();
|
||||
|
||||
zip.setCurrentFile(KeeShare_Container);
|
||||
QuaZipFile databaseFile(&zip);
|
||||
databaseFile.open(QuaZipFile::ReadOnly);
|
||||
auto payload = databaseFile.readAll();
|
||||
databaseFile.close();
|
||||
QBuffer buffer(&payload);
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
|
||||
KeePass2Reader reader;
|
||||
auto key = QSharedPointer<CompositeKey>::create();
|
||||
key->addKey(QSharedPointer<PasswordKey>::create(reference.password));
|
||||
auto sourceDb = QSharedPointer<Database>::create();
|
||||
if (!reader.readDatabase(&buffer, key, sourceDb.data())) {
|
||||
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
|
||||
return {reference.path, Result::Error, reader.errorString()};
|
||||
}
|
||||
|
||||
auto foreign = KeeShare::foreign();
|
||||
auto own = KeeShare::own();
|
||||
auto trust = check(payload, reference, own.certificate, foreign.certificates, sign);
|
||||
switch (trust.first) {
|
||||
case Invalid:
|
||||
qWarning("Prevent untrusted import");
|
||||
return {reference.path, Result::Error, tr("Untrusted import prevented")};
|
||||
|
||||
case UntrustedForever:
|
||||
case TrustedForever: {
|
||||
bool found = false;
|
||||
const auto trusted =
|
||||
trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted;
|
||||
for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) {
|
||||
if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) {
|
||||
scopedCertificate.certificate.signer = trust.second.signer;
|
||||
scopedCertificate.path = reference.path;
|
||||
scopedCertificate.trust = trusted;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
foreign.certificates << KeeShareSettings::ScopedCertificate{reference.path, trust.second, trusted};
|
||||
}
|
||||
// update foreign certificates with new settings
|
||||
KeeShare::setForeign(foreign);
|
||||
|
||||
if (trust.first == TrustedForever) {
|
||||
qDebug("Synchronize %s %s with %s",
|
||||
qPrintable(reference.path),
|
||||
qPrintable(targetGroup->name()),
|
||||
qPrintable(sourceDb->rootGroup()->name()));
|
||||
Merger merger(sourceDb->rootGroup(), targetGroup);
|
||||
merger.setForcedMergeMode(Group::Synchronize);
|
||||
const QStringList changeList = merger.merge();
|
||||
if (!changeList.isEmpty()) {
|
||||
return {reference.path, Result::Success, tr("Successful signed import")};
|
||||
}
|
||||
}
|
||||
// Silent ignore of untrusted import or unchanging import
|
||||
return {};
|
||||
}
|
||||
case TrustedOnce:
|
||||
case Own: {
|
||||
qDebug("Synchronize %s %s with %s",
|
||||
qPrintable(reference.path),
|
||||
qPrintable(targetGroup->name()),
|
||||
qPrintable(sourceDb->rootGroup()->name()));
|
||||
Merger merger(sourceDb->rootGroup(), targetGroup);
|
||||
merger.setForcedMergeMode(Group::Synchronize);
|
||||
const QStringList changeList = merger.merge();
|
||||
if (!changeList.isEmpty()) {
|
||||
return {reference.path, Result::Success, tr("Successful signed import")};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
default:
|
||||
Q_ASSERT(false);
|
||||
return {reference.path, Result::Error, tr("Unexpected error")};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
ShareObserver::Result ShareObserver::importUnsignedContainerInto(const QString& realPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Group* targetGroup)
|
||||
{
|
||||
#if !defined(WITH_XC_KEESHARE_INSECURE)
|
||||
Q_UNUSED(targetGroup);
|
||||
Q_UNUSED(realPath);
|
||||
return {reference.path, Result::Warning, tr("Unsigned share container are not supported - import prevented")};
|
||||
#else
|
||||
QFile file(realPath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
qCritical("Unable to open file %s.", qPrintable(reference.path));
|
||||
return {reference.path, Result::Error, tr("File is not readable")};
|
||||
}
|
||||
auto payload = file.readAll();
|
||||
file.close();
|
||||
QBuffer buffer(&payload);
|
||||
buffer.open(QIODevice::ReadOnly);
|
||||
|
||||
KeePass2Reader reader;
|
||||
auto key = QSharedPointer<CompositeKey>::create();
|
||||
key->addKey(QSharedPointer<PasswordKey>::create(reference.password));
|
||||
auto sourceDb = QSharedPointer<Database>::create();
|
||||
if (!reader.readDatabase(&buffer, key, sourceDb.data())) {
|
||||
qCritical("Error while parsing the database: %s", qPrintable(reader.errorString()));
|
||||
return {reference.path, Result::Error, reader.errorString()};
|
||||
}
|
||||
|
||||
auto foreign = KeeShare::foreign();
|
||||
const auto own = KeeShare::own();
|
||||
const auto sign = KeeShareSettings::Sign(); // invalid sign
|
||||
auto trust = check(payload, reference, own.certificate, foreign.certificates, sign);
|
||||
switch (trust.first) {
|
||||
case UntrustedForever:
|
||||
case TrustedForever: {
|
||||
bool found = false;
|
||||
const auto trusted =
|
||||
trust.first == TrustedForever ? KeeShareSettings::Trust::Trusted : KeeShareSettings::Trust::Untrusted;
|
||||
for (KeeShareSettings::ScopedCertificate& scopedCertificate : foreign.certificates) {
|
||||
if (scopedCertificate.certificate.key == trust.second.key && scopedCertificate.path == reference.path) {
|
||||
scopedCertificate.certificate.signer = trust.second.signer;
|
||||
scopedCertificate.path = reference.path;
|
||||
scopedCertificate.trust = trusted;
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
foreign.certificates << KeeShareSettings::ScopedCertificate{reference.path, trust.second, trusted};
|
||||
}
|
||||
// update foreign certificates with new settings
|
||||
KeeShare::setForeign(foreign);
|
||||
|
||||
if (trust.first == TrustedForever) {
|
||||
qDebug("Synchronize %s %s with %s",
|
||||
qPrintable(reference.path),
|
||||
qPrintable(targetGroup->name()),
|
||||
qPrintable(sourceDb->rootGroup()->name()));
|
||||
Merger merger(sourceDb->rootGroup(), targetGroup);
|
||||
merger.setForcedMergeMode(Group::Synchronize);
|
||||
const QStringList changeList = merger.merge();
|
||||
if (!changeList.isEmpty()) {
|
||||
return {reference.path, Result::Success, tr("Successful signed import")};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
case TrustedOnce: {
|
||||
qDebug("Synchronize %s %s with %s",
|
||||
qPrintable(reference.path),
|
||||
qPrintable(targetGroup->name()),
|
||||
qPrintable(sourceDb->rootGroup()->name()));
|
||||
Merger merger(sourceDb->rootGroup(), targetGroup);
|
||||
merger.setForcedMergeMode(Group::Synchronize);
|
||||
const QStringList changeList = merger.merge();
|
||||
if (!changeList.isEmpty()) {
|
||||
return {reference.path, Result::Success, tr("Successful unsigned import")};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
default:
|
||||
qWarning("Prevent untrusted import");
|
||||
return {reference.path, Result::Warning, tr("Untrusted import prevented")};
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
ShareObserver::Result ShareObserver::importContainerInto(const QString& realPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Group* targetGroup)
|
||||
{
|
||||
const QFileInfo info(realPath);
|
||||
if (!info.exists()) {
|
||||
qCritical("File %s does not exist.", qPrintable(info.absoluteFilePath()));
|
||||
return {reference.path, Result::Warning, tr("File does not exist")};
|
||||
}
|
||||
|
||||
if (isOfExportType(info, KeeShare::signedContainerFileType())) {
|
||||
return importSignedContainerInto(realPath, reference, targetGroup);
|
||||
}
|
||||
if (isOfExportType(info, KeeShare::unsignedContainerFileType())) {
|
||||
return importUnsignedContainerInto(realPath, reference, targetGroup);
|
||||
}
|
||||
return {reference.path, Result::Error, tr("Unknown share container type")};
|
||||
}
|
||||
|
||||
ShareObserver::Result ShareObserver::importFromReferenceContainer(const QString& path)
|
||||
ShareObserver::Result ShareObserver::importShare(const QString& path)
|
||||
{
|
||||
if (!KeeShare::active().in) {
|
||||
return {};
|
||||
@ -582,81 +236,7 @@ ShareObserver::Result ShareObserver::importFromReferenceContainer(const QString&
|
||||
Q_ASSERT(shareGroup->database() == m_db);
|
||||
Q_ASSERT(shareGroup == m_db->rootGroup()->findGroupByUuid(shareGroup->uuid()));
|
||||
const auto resolvedPath = resolvePath(reference.path, m_db);
|
||||
return importContainerInto(resolvedPath, reference, shareGroup);
|
||||
}
|
||||
|
||||
void ShareObserver::resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb)
|
||||
{
|
||||
for (const auto& attribute : EntryAttributes::DefaultAttributes) {
|
||||
const auto standardValue = targetEntry->attributes()->value(attribute);
|
||||
const auto type = targetEntry->placeholderType(standardValue);
|
||||
if (type != Entry::PlaceholderType::Reference) {
|
||||
// No reference to resolve
|
||||
continue;
|
||||
}
|
||||
const auto* referencedTargetEntry = targetEntry->resolveReference(standardValue);
|
||||
if (referencedTargetEntry) {
|
||||
// References is within scope, no resolving needed
|
||||
continue;
|
||||
}
|
||||
// We could do more sophisticated **** trying to point the reference to the next in-scope reference
|
||||
// but those cases with high propability constructed examples and very rare in real usage
|
||||
const auto* sourceReference = sourceDb->rootGroup()->findEntryByUuid(targetEntry->uuid());
|
||||
const auto resolvedValue = sourceReference->resolveMultiplePlaceholders(standardValue);
|
||||
targetEntry->setUpdateTimeinfo(false);
|
||||
targetEntry->attributes()->set(attribute, resolvedValue, targetEntry->attributes()->isProtected(attribute));
|
||||
targetEntry->setUpdateTimeinfo(true);
|
||||
}
|
||||
}
|
||||
|
||||
Database* ShareObserver::exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot)
|
||||
{
|
||||
const auto* sourceDb = sourceRoot->database();
|
||||
auto* targetDb = new Database();
|
||||
auto* targetMetadata = targetDb->metadata();
|
||||
targetMetadata->setRecycleBinEnabled(false);
|
||||
auto key = QSharedPointer<CompositeKey>::create();
|
||||
key->addKey(QSharedPointer<PasswordKey>::create(reference.password));
|
||||
|
||||
// Copy the source root as the root of the export database, memory manage the old root node
|
||||
auto* targetRoot = sourceRoot->clone(Entry::CloneNoFlags, Group::CloneNoFlags);
|
||||
auto updateTimeinfo = targetRoot->canUpdateTimeinfo();
|
||||
targetRoot->setUpdateTimeinfo(false);
|
||||
KeeShare::setReferenceTo(targetRoot, KeeShareSettings::Reference());
|
||||
targetRoot->setUpdateTimeinfo(updateTimeinfo);
|
||||
const auto sourceEntries = sourceRoot->entriesRecursive(false);
|
||||
for (const Entry* sourceEntry : sourceEntries) {
|
||||
auto* targetEntry = sourceEntry->clone(Entry::CloneIncludeHistory);
|
||||
updateTimeinfo = targetEntry->canUpdateTimeinfo();
|
||||
targetEntry->setUpdateTimeinfo(false);
|
||||
targetEntry->setGroup(targetRoot);
|
||||
targetEntry->setUpdateTimeinfo(updateTimeinfo);
|
||||
const auto iconUuid = targetEntry->iconUuid();
|
||||
if (!iconUuid.isNull() && !targetMetadata->containsCustomIcon(iconUuid)) {
|
||||
targetMetadata->addCustomIcon(iconUuid, sourceEntry->icon());
|
||||
}
|
||||
}
|
||||
|
||||
targetDb->setKey(key);
|
||||
auto* obsoleteRoot = targetDb->rootGroup();
|
||||
targetDb->setRootGroup(targetRoot);
|
||||
delete obsoleteRoot;
|
||||
|
||||
targetDb->metadata()->setName(sourceRoot->name());
|
||||
|
||||
// Push all deletions of the source database to the target
|
||||
// simple moving out of a share group will not trigger a deletion in the
|
||||
// target - a more elaborate mechanism may need the use of another custom
|
||||
// attribute to share unshared entries from the target db
|
||||
for (const auto& object : sourceDb->deletedObjects()) {
|
||||
targetDb->addDeletedObject(object);
|
||||
}
|
||||
for (auto* targetEntry : targetRoot->entriesRecursive(false)) {
|
||||
if (targetEntry->hasReferences()) {
|
||||
resolveReferenceAttributes(targetEntry, sourceDb);
|
||||
}
|
||||
}
|
||||
return targetDb;
|
||||
return ShareImport::containerInto(resolvedPath, reference, shareGroup);
|
||||
}
|
||||
|
||||
QSharedPointer<Database> ShareObserver::database()
|
||||
@ -664,113 +244,7 @@ QSharedPointer<Database> ShareObserver::database()
|
||||
return m_db;
|
||||
}
|
||||
|
||||
ShareObserver::Result ShareObserver::exportIntoReferenceSignedContainer(const QString& realPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Database* targetDb)
|
||||
{
|
||||
#if !defined(WITH_XC_KEESHARE_SECURE)
|
||||
Q_UNUSED(targetDb);
|
||||
Q_UNUSED(realPath);
|
||||
return {
|
||||
reference.path, Result::Warning, tr("Overwriting signed share container is not supported - export prevented")};
|
||||
#else
|
||||
QByteArray bytes;
|
||||
{
|
||||
QBuffer buffer(&bytes);
|
||||
buffer.open(QIODevice::WriteOnly);
|
||||
KeePass2Writer writer;
|
||||
writer.writeDatabase(&buffer, targetDb);
|
||||
if (writer.hasError()) {
|
||||
qWarning("Serializing export dabase failed: %s.", writer.errorString().toLatin1().data());
|
||||
return {reference.path, Result::Error, writer.errorString()};
|
||||
}
|
||||
}
|
||||
const auto own = KeeShare::own();
|
||||
QuaZip zip(realPath);
|
||||
zip.setFileNameCodec("UTF-8");
|
||||
const bool zipOpened = zip.open(QuaZip::mdCreate);
|
||||
if (!zipOpened) {
|
||||
::qWarning("Opening export file failed: %d", zip.getZipError());
|
||||
return {reference.path, Result::Error, tr("Could not write export container (%1)").arg(zip.getZipError())};
|
||||
}
|
||||
{
|
||||
QuaZipFile file(&zip);
|
||||
const auto signatureOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Signature));
|
||||
if (!signatureOpened) {
|
||||
::qWarning("Embedding signature failed: Could not open file to write (%d)", zip.getZipError());
|
||||
return {reference.path,
|
||||
Result::Error,
|
||||
tr("Could not embed signature: Could not open file to write (%1)").arg(file.getZipError())};
|
||||
}
|
||||
QTextStream stream(&file);
|
||||
KeeShareSettings::Sign sign;
|
||||
auto sshKey = own.key.sshKey();
|
||||
sshKey.openKey(QString());
|
||||
const Signature signer;
|
||||
sign.signature = signer.create(bytes, sshKey);
|
||||
sign.certificate = own.certificate;
|
||||
stream << KeeShareSettings::Sign::serialize(sign);
|
||||
stream.flush();
|
||||
if (file.getZipError() != ZIP_OK) {
|
||||
::qWarning("Embedding signature failed: Could not write file (%d)", zip.getZipError());
|
||||
return {reference.path,
|
||||
Result::Error,
|
||||
tr("Could not embed signature: Could not write file (%1)").arg(file.getZipError())};
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
{
|
||||
QuaZipFile file(&zip);
|
||||
const auto dbOpened = file.open(QIODevice::WriteOnly, QuaZipNewInfo(KeeShare_Container));
|
||||
if (!dbOpened) {
|
||||
::qWarning("Embedding database failed: Could not open file to write (%d)", zip.getZipError());
|
||||
return {reference.path,
|
||||
Result::Error,
|
||||
tr("Could not embed database: Could not open file to write (%1)").arg(file.getZipError())};
|
||||
}
|
||||
file.write(bytes);
|
||||
if (file.getZipError() != ZIP_OK) {
|
||||
::qWarning("Embedding database failed: Could not write file (%d)", zip.getZipError());
|
||||
return {reference.path,
|
||||
Result::Error,
|
||||
tr("Could not embed database: Could not write file (%1)").arg(file.getZipError())};
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
zip.close();
|
||||
return {reference.path};
|
||||
#endif
|
||||
}
|
||||
|
||||
ShareObserver::Result ShareObserver::exportIntoReferenceUnsignedContainer(const QString& realPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Database* targetDb)
|
||||
{
|
||||
#if !defined(WITH_XC_KEESHARE_INSECURE)
|
||||
Q_UNUSED(targetDb);
|
||||
Q_UNUSED(realPath);
|
||||
return {reference.path,
|
||||
Result::Warning,
|
||||
tr("Overwriting unsigned share container is not supported - export prevented")};
|
||||
#else
|
||||
QFile file(realPath);
|
||||
const bool fileOpened = file.open(QIODevice::WriteOnly);
|
||||
if (!fileOpened) {
|
||||
::qWarning("Opening export file failed");
|
||||
return {reference.path, Result::Error, tr("Could not write export container")};
|
||||
}
|
||||
KeePass2Writer writer;
|
||||
writer.writeDatabase(&file, targetDb);
|
||||
if (writer.hasError()) {
|
||||
qWarning("Exporting dabase failed: %s.", writer.errorString().toLatin1().data());
|
||||
return {reference.path, Result::Error, writer.errorString()};
|
||||
}
|
||||
file.close();
|
||||
#endif
|
||||
return {reference.path};
|
||||
}
|
||||
|
||||
QList<ShareObserver::Result> ShareObserver::exportIntoReferenceContainers()
|
||||
QList<ShareObserver::Result> ShareObserver::exportShares()
|
||||
{
|
||||
QList<Result> results;
|
||||
struct Reference
|
||||
@ -809,20 +283,8 @@ QList<ShareObserver::Result> ShareObserver::exportIntoReferenceContainers()
|
||||
const auto& reference = it.value().first();
|
||||
const QString resolvedPath = resolvePath(reference.config.path, m_db);
|
||||
m_fileWatcher->ignoreFileChanges(resolvedPath);
|
||||
QScopedPointer<Database> targetDb(exportIntoContainer(reference.config, reference.group));
|
||||
QFileInfo info(resolvedPath);
|
||||
if (isOfExportType(info, KeeShare::signedContainerFileType())) {
|
||||
results << exportIntoReferenceSignedContainer(resolvedPath, reference.config, targetDb.data());
|
||||
m_fileWatcher->observeFileChanges(true);
|
||||
continue;
|
||||
}
|
||||
if (isOfExportType(info, KeeShare::unsignedContainerFileType())) {
|
||||
results << exportIntoReferenceUnsignedContainer(resolvedPath, reference.config, targetDb.data());
|
||||
m_fileWatcher->observeFileChanges(true);
|
||||
continue;
|
||||
}
|
||||
Q_ASSERT(false);
|
||||
results << Result{reference.config.path, Result::Error, tr("Unexpected export error occurred")};
|
||||
ShareExport::intoContainer(resolvedPath, reference.config, reference.group);
|
||||
m_fileWatcher->observeFileChanges(true);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
@ -836,7 +298,7 @@ void ShareObserver::handleDatabaseSaved()
|
||||
QStringList warning;
|
||||
QStringList success;
|
||||
|
||||
const auto results = exportIntoReferenceContainers();
|
||||
const auto results = exportShares();
|
||||
for (const Result& result : results) {
|
||||
if (!result.isValid()) {
|
||||
Q_ASSERT(result.isValid());
|
||||
|
@ -20,17 +20,13 @@
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
|
||||
#include "gui/MessageWidget.h"
|
||||
#include "keeshare/KeeShareSettings.h"
|
||||
|
||||
class BulkFileWatcher;
|
||||
class Entry;
|
||||
class Group;
|
||||
class CustomData;
|
||||
class Database;
|
||||
|
||||
class ShareObserver : public QObject
|
||||
@ -43,17 +39,6 @@ public:
|
||||
|
||||
QSharedPointer<Database> database();
|
||||
|
||||
signals:
|
||||
void sharingMessage(QString, MessageWidget::MessageType);
|
||||
|
||||
private slots:
|
||||
void handleDatabaseChanged();
|
||||
void handleDatabaseSaved();
|
||||
void handleFileCreated(const QString& path);
|
||||
void handleFileUpdated(const QString& path);
|
||||
void handleFileDeleted(const QString& path);
|
||||
|
||||
private:
|
||||
struct Result
|
||||
{
|
||||
enum Type
|
||||
@ -76,27 +61,19 @@ private:
|
||||
bool isInfo() const;
|
||||
};
|
||||
|
||||
static void resolveReferenceAttributes(Entry* targetEntry, const Database* sourceDb);
|
||||
signals:
|
||||
void sharingMessage(QString, MessageWidget::MessageType);
|
||||
|
||||
static Database* exportIntoContainer(const KeeShareSettings::Reference& reference, const Group* sourceRoot);
|
||||
static Result exportIntoReferenceUnsignedContainer(const QString& realPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Database* targetDb);
|
||||
static Result exportIntoReferenceSignedContainer(const QString& realPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Database* targetDb);
|
||||
static Result importSignedContainerInto(const QString& realPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Group* targetGroup);
|
||||
static Result importUnsignedContainerInto(const QString& realPath,
|
||||
const KeeShareSettings::Reference& reference,
|
||||
Group* targetGroup);
|
||||
static Result
|
||||
importContainerInto(const QString& realPath, const KeeShareSettings::Reference& reference, Group* targetGroup);
|
||||
static Result importDatabaseInto();
|
||||
private slots:
|
||||
void handleDatabaseChanged();
|
||||
void handleDatabaseSaved();
|
||||
void handleFileCreated(const QString& path);
|
||||
void handleFileUpdated(const QString& path);
|
||||
void handleFileDeleted(const QString& path);
|
||||
|
||||
Result importFromReferenceContainer(const QString& path);
|
||||
QList<Result> exportIntoReferenceContainers();
|
||||
private:
|
||||
Result importShare(const QString& path);
|
||||
QList<Result> exportShares();
|
||||
|
||||
void deinitialize();
|
||||
void reinitialize();
|
||||
|
Loading…
x
Reference in New Issue
Block a user