This commit is contained in:
LC 2026-03-09 18:59:34 +01:00 committed by GitHub
commit 96e260f94c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 446 additions and 39 deletions

View file

@ -202,6 +202,7 @@ static const QHash<Config::ConfigKey, ConfigDirective> configStrings = {
{Config::KeeShare_Own, {QS("KeeShare/Own"), Roaming, {}}},
{Config::KeeShare_Foreign, {QS("KeeShare/Foreign"), Roaming, {}}},
{Config::KeeShare_Active, {QS("KeeShare/Active"), Roaming, {}}},
{Config::KeeShare_DeviceId, {QS("KeeShare/DeviceId"), Local, {}}},
// PasswordGenerator
{Config::PasswordGenerator_LowerCase, {QS("PasswordGenerator/LowerCase"), Roaming, true}},

View file

@ -179,6 +179,7 @@ public:
KeeShare_Own,
KeeShare_Foreign,
KeeShare_Active,
KeeShare_DeviceId,
PasswordGenerator_LowerCase,
PasswordGenerator_UpperCase,

View file

@ -23,6 +23,9 @@
#include "gui/DatabaseIcons.h"
#include "keeshare/ShareObserver.h"
#include <QRegularExpression>
#include <QSysInfo>
namespace
{
static const QString KeeShare_Reference("KeeShare/Reference");
@ -51,6 +54,39 @@ void KeeShare::init(QObject* parent)
m_instance = new KeeShare(parent);
}
QString KeeShare::deviceId()
{
auto id = config()->get(Config::KeeShare_DeviceId).toString();
if (id.isEmpty()) {
// Generate fallback from machine unique ID, truncated to 7 chars
auto machineId = QSysInfo::machineUniqueId();
if (!machineId.isEmpty()) {
// machineUniqueId() on Linux returns a hex string directly from /etc/machine-id
id = QString::fromLatin1(machineId).left(7).toUpper();
} else {
// Last resort: use hostname
id = QSysInfo::machineHostName();
}
setDeviceId(id);
// Re-read the sanitized value
id = config()->get(Config::KeeShare_DeviceId).toString();
}
return id;
}
void KeeShare::setDeviceId(const QString& id)
{
// All sanitization consolidated here: strip non-alphanumeric, enforce max
// length, and fall back to DEFAULT if empty
QString sanitized = id;
sanitized.remove(QRegularExpression("[^A-Za-z0-9]"));
sanitized.truncate(32);
if (sanitized.isEmpty()) {
sanitized = QStringLiteral("DEFAULT");
}
config()->set(Config::KeeShare_DeviceId, sanitized);
}
KeeShareSettings::Own KeeShare::own()
{
// Read existing own certificate or generate a new one if none available

View file

@ -54,6 +54,9 @@ public:
static const Group* resolveSharedGroup(const Group* group);
static QString sharingLabel(const Group* group);
static QString deviceId();
static void setDeviceId(const QString& id);
static KeeShareSettings::Own own();
static void setOwn(const KeeShareSettings::Own& own);

View file

@ -25,6 +25,8 @@
#include "gui/DatabaseIcons.h"
#include <QDataStream>
#include <QDir>
#include <QFileInfo>
#include <QTextCodec>
#include <QXmlStreamWriter>
@ -286,6 +288,15 @@ namespace KeeShareSettings
return (type & ImportFrom) != 0 && !path.isEmpty();
}
bool Reference::isPerDeviceMode(const QDir& baseDir) const
{
if (path.isEmpty()) {
return false;
}
const QString resolvedPath = baseDir.absoluteFilePath(path);
return QFileInfo(resolvedPath).isDir();
}
bool Reference::operator<(const Reference& other) const
{
if (type != other.type) {

View file

@ -21,6 +21,8 @@
#include <QSharedPointer>
#include <QUuid>
class QDir;
namespace Botan
{
class Private_Key;
@ -133,6 +135,7 @@ namespace KeeShareSettings
bool isValid() const;
bool isExporting() const;
bool isImporting() const;
bool isPerDeviceMode(const QDir& baseDir) const;
bool operator<(const Reference& other) const;
bool operator==(const Reference& other) const;

View file

@ -23,6 +23,7 @@
#include "gui/MessageBox.h"
#include "keeshare/KeeShare.h"
#include <QRegularExpressionValidator>
#include <QStandardItemModel>
#include <QStandardPaths>
#include <QTextStream>
@ -33,6 +34,9 @@ SettingsWidgetKeeShare::SettingsWidgetKeeShare(QWidget* parent)
{
m_ui->setupUi(this);
m_ui->deviceIdEdit->setValidator(
new QRegularExpressionValidator(QRegularExpression("[A-Za-z0-9]{0,32}"), this));
connect(m_ui->ownCertificateSignerEdit, SIGNAL(textChanged(QString)), SLOT(setVerificationExporter(QString)));
connect(m_ui->generateOwnCerticateButton, SIGNAL(clicked(bool)), SLOT(generateCertificate()));
}
@ -47,6 +51,8 @@ void SettingsWidgetKeeShare::loadSettings()
m_ui->enableExportCheckBox->setChecked(active.out);
m_ui->enableImportCheckBox->setChecked(active.in);
m_ui->deviceIdEdit->setText(KeeShare::deviceId());
m_own = KeeShare::own();
updateOwnCertificate();
}
@ -68,6 +74,14 @@ void SettingsWidgetKeeShare::saveSettings()
KeeShare::setOwn(m_own);
KeeShare::setActive(active);
auto deviceId = m_ui->deviceIdEdit->text().trimmed();
if (!deviceId.isEmpty()) {
KeeShare::setDeviceId(deviceId);
} else {
// Clear stored ID so it will be auto-detected next time
config()->set(Config::KeeShare_DeviceId, QString());
}
config()->set(Config::KeeShare_QuietSuccess, m_ui->quietSuccessCheckBox->isChecked());
}

View file

@ -68,6 +68,48 @@
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="deviceIdentityGroupBox">
<property name="title">
<string>Device Identity</string>
</property>
<layout class="QGridLayout" name="gridLayout_3">
<item row="0" column="0">
<widget class="QLabel" name="deviceIdLabel">
<property name="text">
<string>Device ID:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="deviceIdEdit">
<property name="accessibleName">
<string>Device ID field</string>
</property>
<property name="toolTip">
<string>Unique identifier for this device in per-device sync mode (alphanumeric only)</string>
</property>
<property name="placeholderText">
<string>Auto-detected from system</string>
</property>
</widget>
</item>
<item row="0" column="2">
<spacer name="deviceIdSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="ownCertificateGroupBox">
<property name="title">

View file

@ -23,6 +23,7 @@
#include "keeshare/ShareImport.h"
#include <QDir>
#include <QTimer>
namespace
{
@ -67,6 +68,7 @@ void ShareObserver::deinitialize()
m_groupToReference.clear();
m_shareToGroup.clear();
m_fileWatchers.clear();
m_dirWatchers.clear();
}
void ShareObserver::reinitialize()
@ -83,6 +85,7 @@ void ShareObserver::reinitialize()
m_groupToReference.remove(group);
m_shareToGroup.remove(oldResolvedPath);
m_fileWatchers.remove(oldResolvedPath);
m_dirWatchers.remove(oldResolvedPath);
if (newReference.isValid()) {
m_groupToReference[group] = newReference;
@ -99,6 +102,8 @@ void ShareObserver::reinitialize()
QMap<QString, QStringList> imported;
QMap<QString, QStringList> exported;
const QDir baseDir = QFileInfo(m_db->filePath()).absoluteDir();
for (const auto& share : shares) {
auto group = share.first;
auto& reference = share.second;
@ -109,10 +114,23 @@ void ShareObserver::reinitialize()
if (!reference.path.isEmpty() && reference.type != KeeShareSettings::Inactive) {
const auto newResolvedPath = resolvePath(reference.path, m_db);
auto fileWatcher = QSharedPointer<FileWatcher>::create(this);
connect(fileWatcher.data(), &FileWatcher::fileChanged, this, &ShareObserver::handleFileUpdated);
fileWatcher->start(newResolvedPath, FileWatchPeriod, FileWatchSize);
m_fileWatchers.insert(newResolvedPath, fileWatcher);
if (reference.isPerDeviceMode(baseDir)) {
// Per-device mode: watch the directory for changes
auto dirWatcher = QSharedPointer<QFileSystemWatcher>::create();
if (QDir(newResolvedPath).exists()) {
dirWatcher->addPath(newResolvedPath);
}
connect(dirWatcher.data(), &QFileSystemWatcher::directoryChanged,
this, &ShareObserver::handleDirectoryUpdated);
m_dirWatchers.insert(newResolvedPath, dirWatcher);
} else {
// Classic mode: watch the individual file
auto fileWatcher = QSharedPointer<FileWatcher>::create(this);
connect(fileWatcher.data(), &FileWatcher::fileChanged, this, &ShareObserver::handleFileUpdated);
fileWatcher->start(newResolvedPath, FileWatchPeriod, FileWatchSize);
m_fileWatchers.insert(newResolvedPath, fileWatcher);
}
}
if (reference.isExporting()) {
exported[reference.path] << group->name();
@ -121,21 +139,42 @@ void ShareObserver::reinitialize()
if (reference.isImporting()) {
imported[reference.path] << group->name();
// import has to occur immediately
const auto result = this->importShare(reference.path);
if (!result.isValid()) {
// tolerable result - blocked import or missing source
continue;
}
const auto resolvedDir = resolvePath(reference.path, m_db);
if (result.isError()) {
error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
} else if (result.isWarning()) {
warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
} else if (result.isInfo()) {
success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message);
if (reference.isPerDeviceMode(baseDir)) {
// Per-device mode: import from all device files in the directory
const auto results = importPerDeviceShares(resolvedDir, reference, group);
for (const auto& result : results) {
if (!result.isValid()) {
continue;
}
if (result.isError()) {
error << tr("Import from %1 failed (%2)").arg(result.path, result.message);
} else if (result.isWarning()) {
warning << tr("Import from %1 failed (%2)").arg(result.path, result.message);
} else if (result.isInfo()) {
success << tr("Import from %1 successful (%2)").arg(result.path, result.message);
} else {
success << tr("Imported from %1").arg(result.path);
}
}
} else {
success << tr("Imported from %1").arg(result.path);
// Classic mode: import single file
const auto result = this->importShare(reference.path);
if (!result.isValid()) {
// tolerable result - blocked import or missing source
continue;
}
if (result.isError()) {
error << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
} else if (result.isWarning()) {
warning << tr("Import from %1 failed (%2)").arg(result.path).arg(result.message);
} else if (result.isInfo()) {
success << tr("Import from %1 successful (%2)").arg(result.path).arg(result.message);
} else {
success << tr("Imported from %1").arg(result.path);
}
}
}
}
@ -216,6 +255,87 @@ void ShareObserver::handleFileUpdated(const QString& path)
}
}
void ShareObserver::handleDirectoryUpdated(const QString& dirPath)
{
auto group = m_shareToGroup.value(dirPath);
if (!group) {
return;
}
auto reference = KeeShare::referenceOf(group);
const QDir handleBaseDir = QFileInfo(m_db->filePath()).absoluteDir();
if (!reference.isImporting() || !reference.isPerDeviceMode(handleBaseDir)) {
return;
}
// Re-add the directory to the watcher (Qt removes it after notification)
auto dirWatcher = m_dirWatchers.value(dirPath);
if (dirWatcher && dirWatcher->directories().isEmpty()) {
dirWatcher->addPath(dirPath);
}
if (!m_inDirUpdate) {
QTimer::singleShot(100, this, [this, dirPath] {
auto shareGroup = m_shareToGroup.value(dirPath);
if (!shareGroup) {
m_inDirUpdate = false;
return;
}
auto shareRef = KeeShare::referenceOf(shareGroup);
auto results = importPerDeviceShares(dirPath, shareRef, shareGroup);
m_inDirUpdate = false;
QStringList success;
QStringList warning;
QStringList error;
for (const auto& result : results) {
if (!result.isValid()) {
continue;
}
if (result.isError()) {
error << tr("Import from %1 failed (%2)").arg(result.path, result.message);
} else if (result.isWarning()) {
warning << tr("Import from %1 failed (%2)").arg(result.path, result.message);
} else if (result.isInfo()) {
success << tr("Import from %1 successful (%2)").arg(result.path, result.message);
} else {
success << tr("Imported from %1").arg(result.path);
}
}
notifyAbout(success, warning, error);
});
m_inDirUpdate = true;
}
}
QList<ShareObserver::Result> ShareObserver::importPerDeviceShares(
const QString& resolvedDir,
const KeeShareSettings::Reference& reference,
Group* targetGroup)
{
QList<Result> results;
if (!KeeShare::active().in) {
return results;
}
const QString ownFile = KeeShare::deviceId() + ".kdbx";
QDir dir(resolvedDir);
if (!dir.exists()) {
return results;
}
const auto files = dir.entryList({"*.kdbx"}, QDir::Files, QDir::Name);
for (const auto& fileName : files) {
if (fileName.compare(ownFile, Qt::CaseInsensitive) == 0) {
continue; // Skip own device's file
}
const auto filePath = dir.absoluteFilePath(fileName);
auto result = ShareImport::containerInto(filePath, reference, targetGroup);
result.path = filePath;
results << result;
}
return results;
}
ShareObserver::Result ShareObserver::importShare(const QString& path)
{
if (!KeeShare::active().in) {
@ -283,19 +403,50 @@ QList<ShareObserver::Result> ShareObserver::exportShares()
return results;
}
const QDir exportBaseDir = QFileInfo(m_db->filePath()).absoluteDir();
for (auto it = references.cbegin(); it != references.cend(); ++it) {
auto reference = it.value().first();
const QString resolvedPath = resolvePath(reference.config.path, m_db);
auto watcher = m_fileWatchers.value(resolvedPath);
if (watcher) {
watcher->stop();
}
// TODO: save new path into group settings if not saving to signed container anymore
results << ShareExport::intoContainer(resolvedPath, reference.config, reference.group);
if (reference.config.isPerDeviceMode(exportBaseDir)) {
// Per-device mode: export to {directory}/{DEVICE_ID}.kdbx
QDir dir(resolvedPath);
if (!dir.exists()) {
if (!dir.mkpath(".")) {
results << Result{resolvedPath,
Result::Error,
tr("Could not create directory %1").arg(resolvedPath)};
continue;
}
}
const auto deviceFile = dir.absoluteFilePath(KeeShare::deviceId() + ".kdbx");
if (watcher) {
watcher->start(resolvedPath, FileWatchPeriod, FileWatchSize);
// Pause directory watcher during export
auto dirWatcher = m_dirWatchers.value(resolvedPath);
if (dirWatcher) {
dirWatcher->removePath(resolvedPath);
}
results << ShareExport::intoContainer(deviceFile, reference.config, reference.group);
// Resume directory watcher (also add path if it was newly created)
if (dirWatcher) {
dirWatcher->addPath(resolvedPath);
}
} else {
// Classic mode: export to the file directly
auto watcher = m_fileWatchers.value(resolvedPath);
if (watcher) {
watcher->stop();
}
// TODO: save new path into group settings if not saving to signed container anymore
results << ShareExport::intoContainer(resolvedPath, reference.config, reference.group);
if (watcher) {
watcher->start(resolvedPath, FileWatchPeriod, FileWatchSize);
}
}
}
return results;

View file

@ -18,6 +18,7 @@
#ifndef KEEPASSXC_SHAREOBSERVER_H
#define KEEPASSXC_SHAREOBSERVER_H
#include <QFileSystemWatcher>
#include <QMap>
#include <QObject>
@ -68,10 +69,14 @@ private slots:
void handleDatabaseChanged();
void handleDatabaseSaved();
void handleFileUpdated(const QString& path);
void handleDirectoryUpdated(const QString& dirPath);
private:
Result importShare(const QString& path);
QList<Result> exportShares();
QList<Result> importPerDeviceShares(const QString& resolvedDir,
const KeeShareSettings::Reference& reference,
Group* targetGroup);
void deinitialize();
void reinitialize();
@ -82,7 +87,9 @@ private:
QMap<QPointer<Group>, KeeShareSettings::Reference> m_groupToReference;
QMap<QString, QPointer<Group>> m_shareToGroup;
QMap<QString, QSharedPointer<FileWatcher>> m_fileWatchers;
QMap<QString, QSharedPointer<QFileSystemWatcher>> m_dirWatchers;
bool m_inFileUpdate = false;
bool m_inDirUpdate = false;
bool m_enabled = true;
};

View file

@ -24,6 +24,7 @@
#include "keeshare/KeeShare.h"
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent)
@ -110,20 +111,30 @@ void EditGroupWidgetKeeShare::updateSharingState()
// Custom message for active KeeShare reference
const auto reference = KeeShare::referenceOf(m_temporaryGroup);
const QDir uiBaseDir = QFileInfo(m_database->filePath()).absoluteDir();
if (!reference.path.isEmpty()) {
bool supported = false;
for (const auto& extension : supportedExtensions) {
if (reference.path.endsWith(extension, Qt::CaseInsensitive)) {
supported = true;
break;
if (reference.isPerDeviceMode(uiBaseDir)) {
// Per-device mode: path is a directory, show info message
m_ui->messageWidget->showMessage(
tr("Per-device sync mode: each device writes its own container in this directory.\n"
"Device ID: %1").arg(KeeShare::deviceId()),
MessageWidget::Information);
} else {
// Classic mode: validate file extension
bool supported = false;
for (const auto& extension : supportedExtensions) {
if (reference.path.endsWith(extension, Qt::CaseInsensitive)) {
supported = true;
break;
}
}
if (!supported) {
m_ui->messageWidget->showMessage(tr("Your KeePassXC version does not support sharing this container type.\n"
"Supported extensions are: %1.")
.arg(supportedExtensions.join(", ")),
MessageWidget::Warning);
return;
}
}
if (!supported) {
m_ui->messageWidget->showMessage(tr("Your KeePassXC version does not support sharing this container type.\n"
"Supported extensions are: %1.")
.arg(supportedExtensions.join(", ")),
MessageWidget::Warning);
return;
}
const auto groups = m_database->rootGroup()->groupsRecursive(true);
@ -140,8 +151,12 @@ void EditGroupWidgetKeeShare::updateSharingState()
}
multipleImport |= other.isImporting() && reference.isImporting();
conflictExport |= other.isExporting() && reference.isExporting();
cycleImportExport |=
(other.isImporting() && reference.isExporting()) || (other.isExporting() && reference.isImporting());
// In per-device mode, import+export to the same directory is expected
// (export writes own device file, import reads other devices' files)
if (!reference.isPerDeviceMode(uiBaseDir)) {
cycleImportExport |=
(other.isImporting() && reference.isExporting()) || (other.isExporting() && reference.isImporting());
}
}
if (conflictExport) {
m_ui->messageWidget->showMessage(tr("%1 is already being exported by this database.").arg(reference.path),
@ -239,6 +254,23 @@ void EditGroupWidgetKeeShare::launchPathSelectionDialog()
if (filename.isEmpty()) {
filename = m_temporaryGroup->name();
}
// For SynchronizeWith, offer both file and directory selection
if (reference.type == KeeShareSettings::SynchronizeWith) {
// Try directory selection first for per-device sync
auto dirPath = fileDialog()->getExistingDirectory(
this, tr("Select per-device sync directory"), defaultDirPath);
if (!dirPath.isEmpty()) {
// Directory selected: per-device mode
m_ui->pathEdit->setText(dirPath);
selectPath();
FileDialog::saveLastDir("keeshare", dirPath);
updateSharingState();
return;
}
// User cancelled directory dialog; fall through to file dialog
}
switch (reference.type) {
case KeeShareSettings::ImportFrom:
filename = fileDialog()->getOpenFileName(this, tr("Select import source"), defaultDirPath, filters);

View file

@ -124,6 +124,9 @@
<property name="accessibleName">
<string>Path to share file field</string>
</property>
<property name="toolTip">
<string>File path for classic mode, or directory path for per-device sync</string>
</property>
</widget>
</item>
<item>

View file

@ -48,6 +48,7 @@ Polkit::Polkit()
{
PolkitSubject::registerMetaType();
PolkitAuthorizationResults::registerMetaType();
qDBusRegisterMetaType<QMap<QString, QString>>();
/* Note we explicitly use our own dbus path here, as the ::systemBus() method could be overridden
through an environment variable to return an alternative bus path. This bus could have an application

View file

@ -17,6 +17,7 @@
#include "TestSharing.h"
#include <QDir>
#include <QTest>
#include <QXmlStreamReader>
@ -35,6 +36,14 @@ Q_DECLARE_METATYPE(KeeShareSettings::Certificate)
void TestSharing::initTestCase()
{
QVERIFY(Crypto::init());
m_tempDir = new QTemporaryDir();
QVERIFY(m_tempDir->isValid());
}
void TestSharing::cleanupTestCase()
{
delete m_tempDir;
m_tempDir = nullptr;
}
void TestSharing::testNullObjects()
@ -175,6 +184,92 @@ void TestSharing::testSettingsSerialization_data()
QTest::newRow("5") << false << false << certificate0 << key0;
}
void TestSharing::testPerDeviceMode()
{
QFETCH(QString, path);
QFETCH(QString, baseDir);
QFETCH(bool, expectedPerDevice);
KeeShareSettings::Reference reference;
reference.path = path;
reference.type = KeeShareSettings::SynchronizeWith;
QCOMPARE(reference.isPerDeviceMode(QDir(baseDir)), expectedPerDevice);
}
void TestSharing::testPerDeviceMode_data()
{
QTest::addColumn<QString>("path");
QTest::addColumn<QString>("baseDir");
QTest::addColumn<bool>("expectedPerDevice");
auto base = m_tempDir->path();
// Classic mode paths (file-based — don't need to exist on disk)
QTest::newRow("kdbx file") << "/some/path/share.kdbx" << base << false;
QTest::newRow("kdbx.share file") << "/some/path/share.kdbx.share" << base << false;
QTest::newRow("KDBX uppercase") << "/some/path/share.KDBX" << base << false;
QTest::newRow("KDBX.SHARE uppercase") << "/some/path/share.KDBX.SHARE" << base << false;
QTest::newRow("empty path") << "" << base << false;
QTest::newRow("nonexistent path") << "/nonexistent/path/nowhere" << base << false;
// Per-device mode paths (real directories on disk — absolute)
QDir(base).mkpath("syncdir");
QTest::newRow("directory path") << base + "/syncdir" << base << true;
QDir(base).mkpath("syncdir_slash");
QTest::newRow("directory trailing slash") << base + "/syncdir_slash/" << base << true;
QDir(base).mkpath("sub/shared");
QTest::newRow("nested directory") << base + "/sub/shared" << base << true;
QDir(base).mkpath("path.d/sync");
QTest::newRow("directory with dots") << base + "/path.d/sync" << base << true;
// Per-device mode with relative path (regression test: must resolve against baseDir)
QDir(base).mkpath("reldir");
QTest::newRow("relative directory") << "reldir" << base << true;
QTest::newRow("relative file") << "share.kdbx" << base << false;
}
void TestSharing::testPerDeviceModeImportExport()
{
QFETCH(QString, path);
QFETCH(int, type);
QFETCH(bool, expectedImporting);
QFETCH(bool, expectedExporting);
KeeShareSettings::Reference reference;
reference.path = path;
reference.type = static_cast<KeeShareSettings::Type>(type);
QCOMPARE(reference.isImporting(), expectedImporting);
QCOMPARE(reference.isExporting(), expectedExporting);
}
void TestSharing::testPerDeviceModeImportExport_data()
{
QTest::addColumn<QString>("path");
QTest::addColumn<int>("type");
QTest::addColumn<bool>("expectedImporting");
QTest::addColumn<bool>("expectedExporting");
auto base = m_tempDir->path();
QDir(base).mkpath("importexport");
auto dir = base + "/importexport";
QTest::newRow("per-device sync imports")
<< dir << int(KeeShareSettings::SynchronizeWith) << true << true;
QTest::newRow("per-device import only")
<< dir << int(KeeShareSettings::ImportFrom) << true << false;
QTest::newRow("per-device export only")
<< dir << int(KeeShareSettings::ExportTo) << false << true;
QTest::newRow("classic file sync")
<< dir + "/share.kdbx" << int(KeeShareSettings::SynchronizeWith) << true << true;
QTest::newRow("inactive per-device")
<< dir << int(KeeShareSettings::Inactive) << false << false;
}
const QSharedPointer<Botan::RSA_PrivateKey> TestSharing::stubkey(int index)
{
static QMap<int, QSharedPointer<Botan::RSA_PrivateKey>> keys;

View file

@ -19,6 +19,7 @@
#define KEEPASSXC_TESTSHARING_H
#include <QObject>
#include <QTemporaryDir>
namespace Botan
{
@ -30,15 +31,21 @@ class TestSharing : public QObject
private slots:
void initTestCase();
void cleanupTestCase();
void testNullObjects();
void testKeySerialization();
void testReferenceSerialization();
void testReferenceSerialization_data();
void testSettingsSerialization();
void testSettingsSerialization_data();
void testPerDeviceMode();
void testPerDeviceMode_data();
void testPerDeviceModeImportExport();
void testPerDeviceModeImportExport_data();
private:
const QSharedPointer<Botan::RSA_PrivateKey> stubkey(int index = 0);
QTemporaryDir* m_tempDir = nullptr;
};
#endif // KEEPASSXC_TESTSHARING_H