FdoSecrets: reject setting refs via the API (#7043)

* FdoSecrets: add TOTP as a readonly attribute

* FdoSecrets: reject setting fields containing refs, fixes #6802

It is still possible to set refs using KPXC UI.
This commit is contained in:
Aetf 2021-10-24 10:22:50 -04:00 committed by GitHub
parent c8f135aaed
commit 2a9d92faeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 144 additions and 53 deletions

View File

@ -9,8 +9,9 @@ can connect and access the exposed database in KeePassXC.
## Configurable settings ## Configurable settings
* The user can specify if a database is exposed on DBus, and which group is exposed. * The user can specify if a database is exposed on DBus, and which group is exposed.
* Whether to show desktop notification is shown when an entry is retrieved. * Whether to show desktop notification is shown when an entry's secret is retrieved.
* Whether to skip confirmation for entries deleted from DBus * Whether to confirm for entries deleted from DBus
* Whether to confirm each entry's access
## Implemented Attributes on Item Object ## Implemented Attributes on Item Object
@ -22,10 +23,11 @@ The following attributes are exposed:
|UserName|The entry user name| |UserName|The entry user name|
|URL|The entry URL| |URL|The entry URL|
|Notes|The entry notes| |Notes|The entry notes|
|TOTP|The TOTP code if the entry has one|
In addition, all non-protected custom attributes are also exposed. In addition, all non-protected custom attributes are also exposed.
## Architecture ## Implementation
* `FdoSecrets::Service` is the top level DBus service * `FdoSecrets::Service` is the top level DBus service
* There is one and only one `FdoSecrets::Collection` per opened database tab * There is one and only one `FdoSecrets::Collection` per opened database tab
@ -33,8 +35,11 @@ In addition, all non-protected custom attributes are also exposed.
### Signal connections ### Signal connections
Collection here means the `Collection` object in code. Not the logical concept "collection"
that the user interacts with.
- Collections are created when a corresponding database tab opened - Collections are created when a corresponding database tab opened
- If the database is locked, a collection is created - If the database is locked, a collection is still created
- When the database is unlocked, collection populates its children - When the database is unlocked, collection populates its children
- If the unlocked database's exposed group is none, collection deletes itself - If the unlocked database's exposed group is none, collection deletes itself
- If the database's exposed group changes, collection repopulates - If the database's exposed group changes, collection repopulates

View File

@ -32,10 +32,25 @@
namespace FdoSecrets namespace FdoSecrets
{ {
struct EntryUpdater
{
Entry* entry;
explicit EntryUpdater(Entry* entry)
: entry(entry)
{
entry->beginUpdate();
}
const QSet<QString> Item::ReadOnlyAttributes(QSet<QString>() << ItemAttributes::UuidKey << ItemAttributes::PathKey); ~EntryUpdater()
{
entry->endUpdate();
}
};
static void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType); const QSet<QString> Item::ReadOnlyAttributes(QSet<QString>() << ItemAttributes::UuidKey << ItemAttributes::PathKey
<< ItemAttributes::TotpKey);
static DBusResult setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType);
static Secret getEntrySecret(Entry* entry); static Secret getEntrySecret(Entry* entry);
namespace namespace
@ -110,6 +125,9 @@ namespace FdoSecrets
// add some informative and readonly attributes // add some informative and readonly attributes
attrs[ItemAttributes::UuidKey] = m_backend->uuidToHex(); attrs[ItemAttributes::UuidKey] = m_backend->uuidToHex();
attrs[ItemAttributes::PathKey] = path(); attrs[ItemAttributes::PathKey] = path();
if (m_backend->hasTotp()) {
attrs[ItemAttributes::TotpKey] = m_backend->totp();
}
return {}; return {};
} }
@ -124,23 +142,28 @@ namespace FdoSecrets
return ret; return ret;
} }
m_backend->beginUpdate(); // set on a temp variable first and check for errors to avoid partial updates
EntryAttributes tempAttrs;
auto entryAttrs = m_backend->attributes(); tempAttrs.copyDataFrom(m_backend->attributes());
for (auto it = attrs.constBegin(); it != attrs.constEnd(); ++it) { for (auto it = attrs.constBegin(); it != attrs.constEnd(); ++it) {
if (entryAttrs->isProtected(it.key()) || it.key() == EntryAttributes::PasswordKey) { if (tempAttrs.isProtected(it.key()) || it.key() == EntryAttributes::PasswordKey) {
continue; return QDBusError::InvalidArgs;
} }
if (ReadOnlyAttributes.contains(it.key())) { if (ReadOnlyAttributes.contains(it.key())) {
continue; return QDBusError::InvalidArgs;
} }
entryAttrs->set(it.key(), it.value()); if (EntryAttributes::matchReference(it.value()).hasMatch()) {
return QDBusError::InvalidArgs;
}
tempAttrs.set(it.key(), it.value());
} }
m_backend->endUpdate(); // actually update the backend
EntryUpdater eu(m_backend);
m_backend->attributes()->copyDataFrom(&tempAttrs);
return {}; return {};
} }
@ -170,10 +193,12 @@ namespace FdoSecrets
return ret; return ret;
} }
m_backend->beginUpdate(); if (EntryAttributes::matchReference(label).hasMatch()) {
m_backend->setTitle(label); return QDBusError::InvalidArgs;
m_backend->endUpdate(); }
EntryUpdater eu(m_backend);
m_backend->setTitle(label);
return {}; return {};
} }
@ -278,12 +303,14 @@ namespace FdoSecrets
// decode using session // decode using session
auto decoded = secret.session->decode(secret); auto decoded = secret.session->decode(secret);
// set in backend // block references
m_backend->beginUpdate(); if (EntryAttributes::matchReference(decoded.value).hasMatch()) {
setEntrySecret(m_backend, decoded.value, decoded.contentType); return QDBusError::InvalidArgs;
m_backend->endUpdate(); }
return {}; EntryUpdater eu(m_backend);
// set in backend
return setEntrySecret(m_backend, decoded.value, decoded.contentType);
} }
DBusResult Item::setProperties(const QVariantMap& properties) DBusResult Item::setProperties(const QVariantMap& properties)
@ -378,7 +405,7 @@ namespace FdoSecrets
return pathComponents.join('/'); return pathComponents.join('/');
} }
void setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType) DBusResult setEntrySecret(Entry* entry, const QByteArray& data, const QString& contentType)
{ {
auto mimeName = contentType.split(';').takeFirst().trimmed(); auto mimeName = contentType.split(';').takeFirst().trimmed();
@ -397,23 +424,28 @@ namespace FdoSecrets
} }
if (!mimeType.isValid() || !mimeType.inherits(QStringLiteral("text/plain")) || !codec) { if (!mimeType.isValid() || !mimeType.inherits(QStringLiteral("text/plain")) || !codec) {
if (EntryAttributes::matchReference(contentType).hasMatch()) {
return QDBusError::InvalidArgs;
}
// we can't handle this content type, save the data as attachment, and clear the password field // we can't handle this content type, save the data as attachment, and clear the password field
entry->setPassword(""); entry->setPassword("");
entry->attachments()->set(FDO_SECRETS_DATA, data); entry->attachments()->set(FDO_SECRETS_DATA, data);
entry->attributes()->set(FDO_SECRETS_CONTENT_TYPE, contentType); entry->attributes()->set(FDO_SECRETS_CONTENT_TYPE, contentType);
return; } else {
auto password = codec->toUnicode(data);
if (EntryAttributes::matchReference(password).hasMatch()) {
return QDBusError::InvalidArgs;
}
// save the data to password field
entry->setPassword(password);
if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) {
entry->attachments()->remove(FDO_SECRETS_DATA);
}
if (entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE)) {
entry->attributes()->remove(FDO_SECRETS_CONTENT_TYPE);
}
} }
return {};
// save the data to password field
if (entry->attachments()->hasKey(FDO_SECRETS_DATA)) {
entry->attachments()->remove(FDO_SECRETS_DATA);
}
if (entry->attributes()->hasKey(FDO_SECRETS_CONTENT_TYPE)) {
entry->attributes()->remove(FDO_SECRETS_CONTENT_TYPE);
}
Q_ASSERT(codec);
entry->setPassword(codec->toUnicode(data));
} }
Secret getEntrySecret(Entry* entry) Secret getEntrySecret(Entry* entry)

View File

@ -30,6 +30,7 @@ namespace FdoSecrets
{ {
constexpr const auto UuidKey = "Uuid"; constexpr const auto UuidKey = "Uuid";
constexpr const auto PathKey = "Path"; constexpr const auto PathKey = "Path";
constexpr const auto TotpKey = "TOTP";
} // namespace ItemAttributes } // namespace ItemAttributes
class Session; class Session;

View File

@ -41,6 +41,7 @@
#include <QLineEdit> #include <QLineEdit>
#include <QSignalSpy> #include <QSignalSpy>
#include <QTest> #include <QTest>
#include <utility>
int main(int argc, char* argv[]) int main(int argc, char* argv[])
{ {
@ -1248,30 +1249,24 @@ void TestGuiFdoSecrets::testItemSecret()
// first create Secret in wire format, // first create Secret in wire format,
// then convert to internal format and encrypt // then convert to internal format and encrypt
// finally convert encrypted internal format back to wire format to pass to SetSecret // finally convert encrypted internal format back to wire format to pass to SetSecret
wire::Secret ss; const QByteArray expected = QByteArrayLiteral("NewPassword");
ss.contentType = TEXT_PLAIN; auto encrypted = encryptPassword(expected, TEXT_PLAIN, sess);
ss.value = "NewPassword"; DBUS_VERIFY(item->SetSecret(encrypted));
ss.session = QDBusObjectPath(sess->path()); COMPARE(entry->password().toUtf8(), expected);
auto encrypted = m_clientCipher->encrypt(ss.unmarshal(m_plugin->dbus()));
DBUS_VERIFY(item->SetSecret(encrypted.marshal()));
COMPARE(entry->password().toUtf8(), ss.value);
} }
// set secret with something else is saved as attachment // set secret with something else is saved as attachment
const QByteArray expected = QByteArrayLiteral("NewPasswordBinary");
{ {
wire::Secret expected; auto encrypted = encryptPassword(expected, APPLICATION_OCTET_STREAM, sess);
expected.contentType = APPLICATION_OCTET_STREAM; DBUS_VERIFY(item->SetSecret(encrypted));
expected.value = QByteArrayLiteral("NewPasswordBinary");
expected.session = QDBusObjectPath(sess->path());
DBUS_VERIFY(item->SetSecret(m_clientCipher->encrypt(expected.unmarshal(m_plugin->dbus())).marshal()));
COMPARE(entry->password(), QStringLiteral("")); COMPARE(entry->password(), QStringLiteral(""));
}
{
DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path()))); DBUS_GET(encrypted, item->GetSecret(QDBusObjectPath(sess->path())));
auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus())); auto ss = m_clientCipher->decrypt(encrypted.unmarshal(m_plugin->dbus()));
COMPARE(ss.contentType, expected.contentType); COMPARE(ss.contentType, APPLICATION_OCTET_STREAM);
COMPARE(ss.value, expected.value); COMPARE(ss.value, expected);
} }
} }
@ -1374,6 +1369,51 @@ void TestGuiFdoSecrets::testItemLockState()
DBUS_VERIFY(item->SetSecret(encrypted)); DBUS_VERIFY(item->SetSecret(encrypted));
} }
void TestGuiFdoSecrets::testItemRejectSetReferenceFields()
{
// expose a subgroup, entries in it should not be able to retrieve data from entries outside it
auto rootEntry = m_db->rootGroup()->entries().first();
VERIFY(rootEntry);
auto subgroup = m_db->rootGroup()->findGroupByPath("/Homebanking/Subgroup");
VERIFY(subgroup);
FdoSecrets::settings()->setExposedGroup(m_db, subgroup->uuid());
auto service = enableService();
VERIFY(service);
auto coll = getDefaultCollection(service);
VERIFY(coll);
auto item = getFirstItem(coll);
VERIFY(item);
auto sess = openSession(service, DhIetf1024Sha256Aes128CbcPkcs7::Algorithm);
VERIFY(sess);
const auto refText = QStringLiteral("{REF:P@T:%1}").arg(rootEntry->title());
// reject ref in label
{
auto reply = item->setLabel(refText);
VERIFY(reply.isFinished() && reply.isError());
COMPARE(reply.error().type(), QDBusError::InvalidArgs);
}
// reject ref in custom attributes
{
auto reply = item->setAttributes({{"steal", refText}});
VERIFY(reply.isFinished() && reply.isError());
COMPARE(reply.error().type(), QDBusError::InvalidArgs);
}
// reject ref in password
{
auto reply = item->SetSecret(encryptPassword(refText.toUtf8(), "text/plain", sess));
VERIFY(reply.isFinished() && reply.isError());
COMPARE(reply.error().type(), QDBusError::InvalidArgs);
}
// reject ref in content type
{
auto reply = item->SetSecret(encryptPassword("dummy", refText, sess));
VERIFY(reply.isFinished() && reply.isError());
COMPARE(reply.error().type(), QDBusError::InvalidArgs);
}
}
void TestGuiFdoSecrets::testAlias() void TestGuiFdoSecrets::testAlias()
{ {
auto service = enableService(); auto service = enableService();
@ -1585,6 +1625,16 @@ QSharedPointer<ItemProxy> TestGuiFdoSecrets::createItem(const QSharedPointer<Ses
return getProxy<ItemProxy>(itemPath); return getProxy<ItemProxy>(itemPath);
} }
FdoSecrets::wire::Secret
TestGuiFdoSecrets::encryptPassword(QByteArray value, QString contentType, const QSharedPointer<SessionProxy>& sess)
{
wire::Secret ss;
ss.contentType = std::move(contentType);
ss.value = std::move(value);
ss.session = QDBusObjectPath(sess->path());
return m_clientCipher->encrypt(ss.unmarshal(m_plugin->dbus())).marshal();
}
bool TestGuiFdoSecrets::driveAccessControlDialog(bool remember) bool TestGuiFdoSecrets::driveAccessControlDialog(bool remember)
{ {
processEvents(); processEvents();

View File

@ -89,6 +89,7 @@ private slots:
void testItemSecret(); void testItemSecret();
void testItemDelete(); void testItemDelete();
void testItemLockState(); void testItemLockState();
void testItemRejectSetReferenceFields();
void testAlias(); void testAlias();
void testDefaultAliasAlwaysPresent(); void testDefaultAliasAlwaysPresent();
@ -120,6 +121,8 @@ private:
const FdoSecrets::wire::StringStringMap& attr, const FdoSecrets::wire::StringStringMap& attr,
bool replace, bool replace,
bool expectPrompt = false); bool expectPrompt = false);
FdoSecrets::wire::Secret
encryptPassword(QByteArray value, QString contentType, const QSharedPointer<SessionProxy>& sess);
template <typename Proxy> QSharedPointer<Proxy> getProxy(const QDBusObjectPath& path) const template <typename Proxy> QSharedPointer<Proxy> getProxy(const QDBusObjectPath& path) const
{ {
auto ret = QSharedPointer<Proxy>{ auto ret = QSharedPointer<Proxy>{