feat(network): add a keyring-backed secret agent that asks for Wi-Fi passwords before connecting
parent
410e35392f
commit
40667cd0f1
@ -0,0 +1,391 @@
|
||||
#include "secretagent.h"
|
||||
#include "handler.h"
|
||||
#include "secretstore.h"
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusError>
|
||||
#include <QDebug>
|
||||
|
||||
#include <NetworkManagerQt/Manager>
|
||||
#include <NetworkManagerQt/SecretAgent>
|
||||
#include <NetworkManagerQt/Setting>
|
||||
#include <NetworkManagerQt/WirelessDevice>
|
||||
|
||||
namespace {
|
||||
|
||||
const QString connectionSetting = QStringLiteral("connection");
|
||||
const QString wirelessSecurity = QStringLiteral("802-11-wireless-security");
|
||||
const QString ieee8021x = QStringLiteral("802-1x");
|
||||
|
||||
// NetworkManager keeps one flags property for all four WEP keys.
|
||||
QString flagsKey(const QString &key)
|
||||
{
|
||||
if (key.startsWith(QLatin1String("wep-key")) && key.size() == 8)
|
||||
return QStringLiteral("wep-key-flags");
|
||||
return key + QLatin1String("-flags");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The object NetworkManager calls. It stays out of the QML type, whose type
|
||||
// registrar cannot describe a NetworkManagerQt base class.
|
||||
class SecretAgentService : public NetworkManager::SecretAgent
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SecretAgentService(NetworkSecretAgent *owner)
|
||||
: NetworkManager::SecretAgent(QStringLiteral("com.cutefish.network"), owner)
|
||||
, m_owner(owner)
|
||||
{
|
||||
}
|
||||
|
||||
public Q_SLOTS:
|
||||
NMVariantMapMap GetSecrets(const NMVariantMapMap &connection,
|
||||
const QDBusObjectPath &connectionPath, const QString &settingName,
|
||||
const QStringList &hints, uint flags) override
|
||||
{
|
||||
Q_UNUSED(hints)
|
||||
|
||||
setDelayedReply(true);
|
||||
m_owner->getSecrets(connection, connectionPath.path(), settingName, flags, message());
|
||||
return {};
|
||||
}
|
||||
|
||||
void CancelGetSecrets(const QDBusObjectPath &connectionPath,
|
||||
const QString &settingName) override
|
||||
{
|
||||
m_owner->cancelGetSecrets(connectionPath.path(), settingName);
|
||||
}
|
||||
|
||||
void SaveSecrets(const NMVariantMapMap &connection,
|
||||
const QDBusObjectPath &connectionPath) override
|
||||
{
|
||||
Q_UNUSED(connectionPath)
|
||||
m_owner->saveSecrets(connection);
|
||||
}
|
||||
|
||||
void DeleteSecrets(const NMVariantMapMap &connection,
|
||||
const QDBusObjectPath &connectionPath) override
|
||||
{
|
||||
Q_UNUSED(connectionPath)
|
||||
m_owner->deleteSecrets(connection);
|
||||
}
|
||||
|
||||
private:
|
||||
NetworkSecretAgent *m_owner;
|
||||
};
|
||||
|
||||
NetworkSecretAgent::NetworkSecretAgent(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_service(new SecretAgentService(this))
|
||||
, m_store(new SecretStore(this))
|
||||
, m_handler(new Handler(this))
|
||||
{
|
||||
QDBusConnection bus = QDBusConnection::sessionBus();
|
||||
if (!bus.registerService(QStringLiteral("com.cutefish.SecretAgent"))
|
||||
|| !bus.registerObject(QStringLiteral("/SecretAgent"), this,
|
||||
QDBusConnection::ExportScriptableSlots))
|
||||
qWarning() << "Cannot offer com.cutefish.SecretAgent:" << bus.lastError().message();
|
||||
|
||||
connect(NetworkManager::notifier(), &NetworkManager::Notifier::deviceRemoved, this,
|
||||
[this](const QString &devicePath) {
|
||||
m_watchedDevices.remove(devicePath);
|
||||
dismiss([&](const Request &request) { return request.devicePath == devicePath; });
|
||||
});
|
||||
}
|
||||
|
||||
NetworkSecretAgent::~NetworkSecretAgent()
|
||||
{
|
||||
for (const Request &request : std::as_const(m_lookups) + std::as_const(m_requests)) {
|
||||
if (!request.accessPointPath.isEmpty())
|
||||
continue;
|
||||
m_service->sendError(SecretAgentService::AgentCanceled,
|
||||
QStringLiteral("The agent is going away"), request.message);
|
||||
}
|
||||
}
|
||||
|
||||
QString NetworkSecretAgent::connectionName() const
|
||||
{
|
||||
return m_requests.isEmpty() ? QString() : m_requests.first().connectionName;
|
||||
}
|
||||
|
||||
NetworkSecretAgent::Kind NetworkSecretAgent::kind() const
|
||||
{
|
||||
return m_requests.isEmpty() ? WpaPassword : m_requests.first().kind;
|
||||
}
|
||||
|
||||
QString NetworkSecretAgent::identity() const
|
||||
{
|
||||
return m_requests.isEmpty() ? QString() : m_requests.first().identity;
|
||||
}
|
||||
|
||||
bool NetworkSecretAgent::retry() const
|
||||
{
|
||||
return !m_requests.isEmpty() && m_requests.first().retry;
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::getSecrets(const NMVariantMapMap &connection,
|
||||
const QString &connectionPath, const QString &settingName,
|
||||
uint flags, const QDBusMessage &message)
|
||||
{
|
||||
Request request;
|
||||
request.id = ++m_nextId;
|
||||
request.message = message;
|
||||
request.connectionPath = connectionPath;
|
||||
request.settingName = settingName;
|
||||
request.connectionName = connection.value(connectionSetting).value(QStringLiteral("id")).toString();
|
||||
request.interactive = flags & (SecretAgentService::AllowInteraction | SecretAgentService::RequestNew);
|
||||
request.retry = flags & SecretAgentService::RequestNew;
|
||||
|
||||
const QVariantMap setting = connection.value(settingName);
|
||||
|
||||
if (settingName == wirelessSecurity) {
|
||||
const QString keyMgmt = setting.value(QStringLiteral("key-mgmt")).toString();
|
||||
|
||||
if (keyMgmt == QLatin1String("wpa-psk") || keyMgmt == QLatin1String("sae")
|
||||
|| keyMgmt == QLatin1String("wpa-none")) {
|
||||
request.kind = WpaPassword;
|
||||
request.key = QStringLiteral("psk");
|
||||
} else if (keyMgmt == QLatin1String("none")) {
|
||||
request.kind = WepKey;
|
||||
request.key = QStringLiteral("wep-key%1")
|
||||
.arg(setting.value(QStringLiteral("wep-tx-keyidx")).toUInt());
|
||||
} else if (keyMgmt == QLatin1String("ieee8021x")
|
||||
&& setting.value(QStringLiteral("auth-alg")).toString()
|
||||
== QLatin1String("leap")) {
|
||||
request.kind = LeapPassword;
|
||||
request.key = QStringLiteral("leap-password");
|
||||
request.identity = setting.value(QStringLiteral("leap-username")).toString();
|
||||
}
|
||||
} else if (settingName == ieee8021x) {
|
||||
const QStringList eap = setting.value(QStringLiteral("eap")).toStringList();
|
||||
request.identity = setting.value(QStringLiteral("identity")).toString();
|
||||
|
||||
if (eap == QStringList {QStringLiteral("tls")}) {
|
||||
request.kind = PrivateKeyPassword;
|
||||
request.key = QStringLiteral("private-key-password");
|
||||
} else if (!request.identity.isEmpty()) {
|
||||
// The identity is not a secret, so a reply cannot supply a missing one.
|
||||
request.kind = EapPassword;
|
||||
request.key = QStringLiteral("password");
|
||||
}
|
||||
}
|
||||
|
||||
// RequestNew means what is stored was just rejected. VPN secrets nest in a
|
||||
// map of their own, which the keyring lookup does not rebuild.
|
||||
const QString uuid = connection.value(connectionSetting).value(QStringLiteral("uuid")).toString();
|
||||
if (request.retry || uuid.isEmpty() || settingName == QLatin1String("vpn")) {
|
||||
ask(request);
|
||||
return;
|
||||
}
|
||||
|
||||
m_lookups.append(request);
|
||||
m_store->load(uuid, settingName, [this, id = request.id](const QVariantMap &secrets) {
|
||||
for (int i = 0; i < m_lookups.size(); ++i) {
|
||||
if (m_lookups.at(i).id != id)
|
||||
continue;
|
||||
|
||||
const Request request = m_lookups.takeAt(i);
|
||||
if (secrets.isEmpty()) {
|
||||
ask(request);
|
||||
} else {
|
||||
NMVariantMapMap reply;
|
||||
reply.insert(request.settingName, secrets);
|
||||
QDBusConnection::systemBus().send(
|
||||
request.message.createReply(QVariant::fromValue(reply)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::cancelGetSecrets(const QString &connectionPath,
|
||||
const QString &settingName)
|
||||
{
|
||||
auto matches = [&](const Request &request) {
|
||||
return request.connectionPath == connectionPath && request.settingName == settingName;
|
||||
};
|
||||
|
||||
for (int i = 0; i < m_lookups.size(); ++i) {
|
||||
if (!matches(m_lookups.at(i)))
|
||||
continue;
|
||||
|
||||
m_service->sendError(SecretAgentService::AgentCanceled,
|
||||
QStringLiteral("Canceled by NetworkManager"),
|
||||
m_lookups.takeAt(i).message);
|
||||
return;
|
||||
}
|
||||
|
||||
for (int i = 0; i < m_requests.size(); ++i) {
|
||||
if (!matches(m_requests.at(i)))
|
||||
continue;
|
||||
|
||||
m_service->sendError(SecretAgentService::AgentCanceled,
|
||||
QStringLiteral("Canceled by NetworkManager"),
|
||||
m_requests.takeAt(i).message);
|
||||
if (i == 0)
|
||||
emit requestChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::saveSecrets(const NMVariantMapMap &connection)
|
||||
{
|
||||
const QVariantMap connectionMap = connection.value(connectionSetting);
|
||||
const QString uuid = connectionMap.value(QStringLiteral("uuid")).toString();
|
||||
const QString name = connectionMap.value(QStringLiteral("id")).toString();
|
||||
if (uuid.isEmpty())
|
||||
return;
|
||||
|
||||
// System-owned secrets are NetworkManager's to store; only agent-owned
|
||||
// ones come here to be kept.
|
||||
for (auto setting = connection.cbegin(); setting != connection.cend(); ++setting) {
|
||||
for (auto key = setting->cbegin(); key != setting->cend(); ++key) {
|
||||
const auto flags = setting->value(flagsKey(key.key()));
|
||||
if (!flags.isValid() || key->typeId() != QMetaType::QString
|
||||
|| !(flags.toUInt() & NetworkManager::Setting::AgentOwned))
|
||||
continue;
|
||||
|
||||
m_store->save(uuid, name, setting.key(), key.key(), key->toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::deleteSecrets(const NMVariantMapMap &connection)
|
||||
{
|
||||
const QString uuid = connection.value(connectionSetting).value(QStringLiteral("uuid")).toString();
|
||||
if (!uuid.isEmpty())
|
||||
m_store->remove(uuid);
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::ConnectWireless(const QString &devicePath,
|
||||
const QString &accessPointPath, const QString &name,
|
||||
bool wep)
|
||||
{
|
||||
if (!m_requests.isEmpty() && m_requests.first().accessPointPath == accessPointPath)
|
||||
return;
|
||||
|
||||
// Another network was picked: the prompt switches to it, and whatever Wi-Fi
|
||||
// password was being asked for is given up, as connecting to this one would.
|
||||
for (int i = m_requests.size() - 1; i >= 0; --i) {
|
||||
const Request &request = m_requests.at(i);
|
||||
if (request.accessPointPath.isEmpty()) {
|
||||
if (request.settingName != wirelessSecurity && request.settingName != ieee8021x)
|
||||
continue;
|
||||
m_service->sendError(SecretAgentService::UserCanceled,
|
||||
QStringLiteral("Another network was chosen"), request.message);
|
||||
}
|
||||
m_requests.removeAt(i);
|
||||
}
|
||||
|
||||
Request request;
|
||||
request.id = ++m_nextId;
|
||||
request.connectionName = name;
|
||||
request.kind = wep ? WepKey : WpaPassword;
|
||||
request.interactive = true;
|
||||
request.devicePath = devicePath;
|
||||
request.accessPointPath = accessPointPath;
|
||||
m_requests.prepend(request);
|
||||
watchDevice(devicePath);
|
||||
emit requestChanged();
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::respond(const QString &secret)
|
||||
{
|
||||
if (m_requests.isEmpty())
|
||||
return;
|
||||
|
||||
const Request &request = m_requests.first();
|
||||
|
||||
if (!request.accessPointPath.isEmpty()) {
|
||||
m_handler->addAndActivateConnection(request.devicePath, request.accessPointPath, secret);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
NMVariantMapMap secrets;
|
||||
secrets[request.settingName][request.key] = secret;
|
||||
QDBusConnection::systemBus().send(request.message.createReply(QVariant::fromValue(secrets)));
|
||||
|
||||
finish();
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::cancel()
|
||||
{
|
||||
if (m_requests.isEmpty())
|
||||
return;
|
||||
|
||||
if (!m_requests.first().accessPointPath.isEmpty()) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
m_service->sendError(SecretAgentService::UserCanceled, QStringLiteral("Canceled by the user"),
|
||||
m_requests.first().message);
|
||||
finish();
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::ask(const Request &request)
|
||||
{
|
||||
if (!request.interactive || (request.key.isEmpty() && request.accessPointPath.isEmpty())) {
|
||||
m_service->sendError(SecretAgentService::NoSecrets,
|
||||
QStringLiteral("No %1 secrets to offer").arg(request.settingName),
|
||||
request.message);
|
||||
return;
|
||||
}
|
||||
|
||||
m_requests.append(request);
|
||||
if (m_requests.size() == 1)
|
||||
emit requestChanged();
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::finish()
|
||||
{
|
||||
m_requests.removeFirst();
|
||||
emit requestChanged();
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::dismiss(const std::function<bool(const Request &)> &matches)
|
||||
{
|
||||
bool headChanged = false;
|
||||
for (int i = m_requests.size() - 1; i >= 0; --i) {
|
||||
const Request &request = m_requests.at(i);
|
||||
if (request.accessPointPath.isEmpty() || !matches(request))
|
||||
continue;
|
||||
m_requests.removeAt(i);
|
||||
headChanged |= i == 0;
|
||||
}
|
||||
|
||||
if (headChanged)
|
||||
emit requestChanged();
|
||||
}
|
||||
|
||||
void NetworkSecretAgent::watchDevice(const QString &devicePath)
|
||||
{
|
||||
if (m_watchedDevices.contains(devicePath))
|
||||
return;
|
||||
|
||||
auto device = NetworkManager::findNetworkInterface(devicePath)
|
||||
.objectCast<NetworkManager::WirelessDevice>();
|
||||
if (!device)
|
||||
return;
|
||||
m_watchedDevices.insert(devicePath);
|
||||
|
||||
connect(device.data(), &NetworkManager::WirelessDevice::accessPointDisappeared, this,
|
||||
[this](const QString &accessPointPath) {
|
||||
dismiss([&](const Request &request) {
|
||||
return request.accessPointPath == accessPointPath;
|
||||
});
|
||||
});
|
||||
// Something else started connecting on this device: the prompt is out of date.
|
||||
connect(device.data(), &NetworkManager::Device::activeConnectionChanged, this,
|
||||
[this, devicePath, weak = device.toWeakRef()] {
|
||||
const auto device = weak.toStrongRef();
|
||||
if (!device || !device->activeConnection())
|
||||
return;
|
||||
dismiss([&](const Request &request) { return request.devicePath == devicePath; });
|
||||
});
|
||||
}
|
||||
|
||||
#include "secretagent.moc"
|
||||
@ -0,0 +1,115 @@
|
||||
#ifndef CUTEFISH_NETWORK_SECRETAGENT_H
|
||||
#define CUTEFISH_NETWORK_SECRETAGENT_H
|
||||
|
||||
#include <QDBusMessage>
|
||||
#include <QList>
|
||||
#include <QSet>
|
||||
|
||||
#include <functional>
|
||||
#include <QtQml/qqmlregistration.h>
|
||||
|
||||
#include <NetworkManagerQt/GenericTypes>
|
||||
|
||||
class Handler;
|
||||
class SecretAgentService;
|
||||
class SecretStore;
|
||||
|
||||
/**
|
||||
* The session's NetworkManager secret agent: NetworkManager asks it for a
|
||||
* Wi-Fi password, a WEP key or an 802.1X password whenever an activation
|
||||
* lacks one. Agent-owned secrets are kept in the user's keyring; anything the
|
||||
* keyring cannot answer comes from whoever shows `pending` to the user.
|
||||
*
|
||||
* Handler also comes here, over com.cutefish.SecretAgent on the session bus, for
|
||||
* the password of a network it is about to add, so that nothing is added or
|
||||
* activated before the user has answered.
|
||||
*
|
||||
* It registers with NetworkManager on construction, and NetworkManager accepts
|
||||
* one agent per identifier and user, so only the shell creates one.
|
||||
*/
|
||||
class NetworkSecretAgent : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
Q_CLASSINFO("D-Bus Interface", "com.cutefish.SecretAgent")
|
||||
QML_NAMED_ELEMENT(SecretAgent)
|
||||
Q_PROPERTY(bool pending READ pending NOTIFY requestChanged)
|
||||
Q_PROPERTY(QString connectionName READ connectionName NOTIFY requestChanged)
|
||||
Q_PROPERTY(Kind kind READ kind NOTIFY requestChanged)
|
||||
Q_PROPERTY(QString identity READ identity NOTIFY requestChanged)
|
||||
Q_PROPERTY(bool retry READ retry NOTIFY requestChanged)
|
||||
|
||||
public:
|
||||
enum Kind {
|
||||
WpaPassword,
|
||||
WepKey,
|
||||
LeapPassword,
|
||||
EapPassword,
|
||||
PrivateKeyPassword,
|
||||
};
|
||||
Q_ENUM(Kind)
|
||||
|
||||
explicit NetworkSecretAgent(QObject *parent = nullptr);
|
||||
~NetworkSecretAgent() override;
|
||||
|
||||
bool pending() const { return !m_requests.isEmpty(); }
|
||||
|
||||
QString connectionName() const;
|
||||
Kind kind() const;
|
||||
QString identity() const;
|
||||
bool retry() const;
|
||||
|
||||
Q_INVOKABLE void respond(const QString &secret);
|
||||
Q_INVOKABLE void cancel();
|
||||
|
||||
public Q_SLOTS:
|
||||
Q_SCRIPTABLE void ConnectWireless(const QString &devicePath, const QString &accessPointPath,
|
||||
const QString &name, bool wep);
|
||||
|
||||
Q_SIGNALS:
|
||||
void requestChanged();
|
||||
|
||||
private:
|
||||
friend class SecretAgentService;
|
||||
|
||||
// Calls from NetworkManager; message is the GetSecrets call, answered later.
|
||||
void getSecrets(const NMVariantMapMap &connection, const QString &connectionPath,
|
||||
const QString &settingName, uint flags, const QDBusMessage &message);
|
||||
void cancelGetSecrets(const QString &connectionPath, const QString &settingName);
|
||||
void saveSecrets(const NMVariantMapMap &connection);
|
||||
void deleteSecrets(const NMVariantMapMap &connection);
|
||||
|
||||
struct Request {
|
||||
quint64 id = 0;
|
||||
QDBusMessage message;
|
||||
QString connectionPath;
|
||||
QString settingName;
|
||||
QString connectionName;
|
||||
QString identity;
|
||||
// The key the secret is stored under within settingName.
|
||||
QString key;
|
||||
Kind kind = WpaPassword;
|
||||
bool interactive = false;
|
||||
bool retry = false;
|
||||
// Asked for by Handler rather than NetworkManager: answering adds the
|
||||
// network through accessPointPath instead of replying to message.
|
||||
QString devicePath;
|
||||
QString accessPointPath;
|
||||
};
|
||||
|
||||
void ask(const Request &request);
|
||||
void finish();
|
||||
// Drops unanswered password-first prompts; NetworkManager cancels its own.
|
||||
void dismiss(const std::function<bool(const Request &)> &matches);
|
||||
void watchDevice(const QString &devicePath);
|
||||
|
||||
SecretAgentService *m_service;
|
||||
SecretStore *m_store;
|
||||
Handler *m_handler;
|
||||
// Waiting on the keyring, then waiting on the user.
|
||||
QList<Request> m_lookups;
|
||||
QList<Request> m_requests;
|
||||
quint64 m_nextId = 0;
|
||||
QSet<QString> m_watchedDevices;
|
||||
};
|
||||
|
||||
#endif // CUTEFISH_NETWORK_SECRETAGENT_H
|
||||
@ -0,0 +1,320 @@
|
||||
#include "secretstore.h"
|
||||
|
||||
#include <QDBusArgument>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMetaType>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QDBusVariant>
|
||||
#include <QDebug>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace {
|
||||
|
||||
const QString service = QStringLiteral("org.freedesktop.secrets");
|
||||
const QString servicePath = QStringLiteral("/org/freedesktop/secrets");
|
||||
const QString serviceInterface = QStringLiteral("org.freedesktop.Secret.Service");
|
||||
const QString collectionInterface = QStringLiteral("org.freedesktop.Secret.Collection");
|
||||
const QString itemInterface = QStringLiteral("org.freedesktop.Secret.Item");
|
||||
const QString promptInterface = QStringLiteral("org.freedesktop.Secret.Prompt");
|
||||
const QString sessionInterface = QStringLiteral("org.freedesktop.Secret.Session");
|
||||
const QString propertiesInterface = QStringLiteral("org.freedesktop.DBus.Properties");
|
||||
const QString noPrompt = QStringLiteral("/");
|
||||
|
||||
using Attributes = QMap<QString, QString>;
|
||||
|
||||
struct Secret {
|
||||
QDBusObjectPath session;
|
||||
QByteArray parameters;
|
||||
QByteArray value;
|
||||
QString contentType;
|
||||
};
|
||||
|
||||
QDBusArgument &operator<<(QDBusArgument &argument, const Secret &secret)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument << secret.session << secret.parameters << secret.value << secret.contentType;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
const QDBusArgument &operator>>(const QDBusArgument &argument, Secret &secret)
|
||||
{
|
||||
argument.beginStructure();
|
||||
argument >> secret.session >> secret.parameters >> secret.value >> secret.contentType;
|
||||
argument.endStructure();
|
||||
return argument;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Q_DECLARE_METATYPE(Secret)
|
||||
|
||||
// Completed arrives on the prompt object after the user answered it.
|
||||
class PromptWatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PromptWatcher(std::function<void(bool, const QVariant &)> done, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_done(std::move(done))
|
||||
{
|
||||
}
|
||||
|
||||
public Q_SLOTS:
|
||||
void completed(bool dismissed, const QDBusVariant &result)
|
||||
{
|
||||
finish(dismissed, result.variant());
|
||||
}
|
||||
|
||||
public:
|
||||
void finish(bool dismissed, const QVariant &result)
|
||||
{
|
||||
if (!m_done)
|
||||
return;
|
||||
auto done = std::move(m_done);
|
||||
m_done = nullptr;
|
||||
done(dismissed, result);
|
||||
deleteLater();
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void(bool, const QVariant &)> m_done;
|
||||
};
|
||||
|
||||
SecretStore::SecretStore(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
qDBusRegisterMetaType<Secret>();
|
||||
qDBusRegisterMetaType<Attributes>();
|
||||
}
|
||||
|
||||
void SecretStore::load(const QString &uuid, const QString &settingName,
|
||||
std::function<void(const QVariantMap &)> done)
|
||||
{
|
||||
openSession([this, uuid, settingName, done](const QString &session) {
|
||||
if (session.isEmpty()) {
|
||||
done({});
|
||||
return;
|
||||
}
|
||||
|
||||
search({{QStringLiteral("connection-uuid"), uuid},
|
||||
{QStringLiteral("setting-name"), settingName}},
|
||||
[this, session, done](const Paths &items) {
|
||||
if (items.isEmpty()) {
|
||||
closeSession(session);
|
||||
done({});
|
||||
return;
|
||||
}
|
||||
|
||||
struct Pending {
|
||||
QVariantMap secrets;
|
||||
int calls = 0;
|
||||
};
|
||||
auto pending = std::make_shared<Pending>();
|
||||
pending->calls = items.size();
|
||||
|
||||
auto settle = [this, session, done, pending] {
|
||||
if (--pending->calls > 0)
|
||||
return;
|
||||
closeSession(session);
|
||||
done(pending->secrets);
|
||||
};
|
||||
|
||||
for (const QDBusObjectPath &item : items) {
|
||||
call(item.path(), propertiesInterface, QStringLiteral("Get"),
|
||||
{itemInterface, QStringLiteral("Attributes")},
|
||||
[this, item, session, pending, settle](const QDBusMessage &reply) {
|
||||
const QString key =
|
||||
qdbus_cast<Attributes>(
|
||||
reply.arguments().value(0).value<QDBusVariant>().variant())
|
||||
.value(QStringLiteral("setting-key"));
|
||||
if (key.isEmpty()) {
|
||||
settle();
|
||||
return;
|
||||
}
|
||||
|
||||
call(item.path(), itemInterface, QStringLiteral("GetSecret"),
|
||||
{QVariant::fromValue(QDBusObjectPath(session))},
|
||||
[key, pending, settle](const QDBusMessage &reply) {
|
||||
if (reply.type() == QDBusMessage::ReplyMessage) {
|
||||
const auto secret = qdbus_cast<Secret>(reply.arguments().value(0));
|
||||
pending->secrets.insert(key, QString::fromUtf8(secret.value));
|
||||
}
|
||||
settle();
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void SecretStore::save(const QString &uuid, const QString &connectionName,
|
||||
const QString &settingName, const QString &key, const QString &value)
|
||||
{
|
||||
openSession([=, this](const QString &session) {
|
||||
if (session.isEmpty())
|
||||
return;
|
||||
|
||||
call(servicePath, serviceInterface, QStringLiteral("ReadAlias"), {QStringLiteral("default")},
|
||||
[=, this](const QDBusMessage &reply) {
|
||||
const auto collection = reply.arguments().value(0).value<QDBusObjectPath>();
|
||||
if (reply.type() != QDBusMessage::ReplyMessage || collection.path() == noPrompt) {
|
||||
qWarning() << "Secret Service has no default collection to store" << key << "in";
|
||||
closeSession(session);
|
||||
return;
|
||||
}
|
||||
|
||||
unlock({collection}, [=, this](const Paths &unlocked) {
|
||||
if (unlocked.isEmpty()) {
|
||||
closeSession(session);
|
||||
return;
|
||||
}
|
||||
|
||||
const Attributes attributes {
|
||||
{QStringLiteral("xdg:schema"),
|
||||
QStringLiteral("org.freedesktop.NetworkManager.Connection")},
|
||||
{QStringLiteral("connection-uuid"), uuid},
|
||||
{QStringLiteral("setting-name"), settingName},
|
||||
{QStringLiteral("setting-key"), key},
|
||||
};
|
||||
const QVariantMap properties {
|
||||
{QStringLiteral("org.freedesktop.Secret.Item.Label"),
|
||||
QStringLiteral("Network secret for %1/%2/%3")
|
||||
.arg(connectionName, settingName, key)},
|
||||
{QStringLiteral("org.freedesktop.Secret.Item.Attributes"),
|
||||
QVariant::fromValue(attributes)},
|
||||
};
|
||||
const Secret secret {QDBusObjectPath(session), {}, value.toUtf8(),
|
||||
QStringLiteral("text/plain")};
|
||||
|
||||
call(collection.path(), collectionInterface, QStringLiteral("CreateItem"),
|
||||
{properties, QVariant::fromValue(secret), true},
|
||||
[=, this](const QDBusMessage &reply) {
|
||||
const auto promptPath =
|
||||
reply.arguments().value(1).value<QDBusObjectPath>();
|
||||
if (reply.type() == QDBusMessage::ReplyMessage
|
||||
&& promptPath.path() != noPrompt)
|
||||
prompt(promptPath, [=, this](bool, const QVariant &) {
|
||||
closeSession(session);
|
||||
});
|
||||
else
|
||||
closeSession(session);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void SecretStore::remove(const QString &uuid)
|
||||
{
|
||||
search({{QStringLiteral("connection-uuid"), uuid}}, [this](const Paths &items) {
|
||||
for (const QDBusObjectPath &item : items) {
|
||||
call(item.path(), itemInterface, QStringLiteral("Delete"), {},
|
||||
[this](const QDBusMessage &reply) {
|
||||
const auto promptPath = reply.arguments().value(0).value<QDBusObjectPath>();
|
||||
if (reply.type() == QDBusMessage::ReplyMessage
|
||||
&& promptPath.path() != noPrompt)
|
||||
prompt(promptPath, [](bool, const QVariant &) {});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void SecretStore::call(const QString &path, const QString &interface, const QString &method,
|
||||
const QVariantList &arguments, Reply done)
|
||||
{
|
||||
auto message = QDBusMessage::createMethodCall(service, path, interface, method);
|
||||
message.setArguments(arguments);
|
||||
|
||||
auto *watcher = new QDBusPendingCallWatcher(
|
||||
QDBusConnection::sessionBus().asyncCall(message), this);
|
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this,
|
||||
[done = std::move(done)](QDBusPendingCallWatcher *watcher) {
|
||||
const QDBusMessage reply = watcher->reply();
|
||||
if (reply.type() == QDBusMessage::ErrorMessage)
|
||||
qWarning() << "Secret Service:" << reply.errorMessage();
|
||||
done(reply);
|
||||
watcher->deleteLater();
|
||||
});
|
||||
}
|
||||
|
||||
void SecretStore::openSession(std::function<void(const QString &)> done)
|
||||
{
|
||||
// "plain" sends the secret unencrypted, which is what libsecret falls back
|
||||
// to as well; the session bus never leaves the machine.
|
||||
call(servicePath, serviceInterface, QStringLiteral("OpenSession"),
|
||||
{QStringLiteral("plain"), QVariant::fromValue(QDBusVariant(QString()))},
|
||||
[done](const QDBusMessage &reply) {
|
||||
done(reply.type() == QDBusMessage::ReplyMessage
|
||||
? reply.arguments().value(1).value<QDBusObjectPath>().path()
|
||||
: QString());
|
||||
});
|
||||
}
|
||||
|
||||
void SecretStore::closeSession(const QString &session)
|
||||
{
|
||||
call(session, sessionInterface, QStringLiteral("Close"), {}, [](const QDBusMessage &) {});
|
||||
}
|
||||
|
||||
void SecretStore::search(const Attributes &attributes, std::function<void(const Paths &)> done)
|
||||
{
|
||||
call(servicePath, serviceInterface, QStringLiteral("SearchItems"),
|
||||
{QVariant::fromValue(attributes)}, [this, done](const QDBusMessage &reply) {
|
||||
if (reply.type() != QDBusMessage::ReplyMessage) {
|
||||
done({});
|
||||
return;
|
||||
}
|
||||
|
||||
const auto unlocked = qdbus_cast<Paths>(reply.arguments().value(0));
|
||||
const auto locked = qdbus_cast<Paths>(reply.arguments().value(1));
|
||||
unlock(locked, [unlocked, done](const Paths &paths) {
|
||||
done(unlocked + paths);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void SecretStore::unlock(const Paths &objects, std::function<void(const Paths &)> done)
|
||||
{
|
||||
if (objects.isEmpty()) {
|
||||
done({});
|
||||
return;
|
||||
}
|
||||
|
||||
call(servicePath, serviceInterface, QStringLiteral("Unlock"),
|
||||
{QVariant::fromValue(objects)}, [this, done](const QDBusMessage &reply) {
|
||||
if (reply.type() != QDBusMessage::ReplyMessage) {
|
||||
done({});
|
||||
return;
|
||||
}
|
||||
|
||||
const auto unlocked = qdbus_cast<Paths>(reply.arguments().value(0));
|
||||
const auto promptPath = reply.arguments().value(1).value<QDBusObjectPath>();
|
||||
if (promptPath.path() == noPrompt) {
|
||||
done(unlocked);
|
||||
return;
|
||||
}
|
||||
|
||||
prompt(promptPath, [unlocked, done](bool dismissed, const QVariant &result) {
|
||||
done(dismissed ? unlocked : unlocked + qdbus_cast<Paths>(result));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void SecretStore::prompt(const QDBusObjectPath &prompt,
|
||||
std::function<void(bool, const QVariant &)> done)
|
||||
{
|
||||
auto *watcher = new PromptWatcher(std::move(done), this);
|
||||
QDBusConnection::sessionBus().connect(service, prompt.path(), promptInterface,
|
||||
QStringLiteral("Completed"), watcher,
|
||||
SLOT(completed(bool, QDBusVariant)));
|
||||
|
||||
call(prompt.path(), promptInterface, QStringLiteral("Prompt"), {QString()},
|
||||
[watcher](const QDBusMessage &reply) {
|
||||
if (reply.type() == QDBusMessage::ErrorMessage)
|
||||
watcher->finish(true, {});
|
||||
});
|
||||
}
|
||||
|
||||
#include "secretstore.moc"
|
||||
@ -0,0 +1,43 @@
|
||||
#ifndef CUTEFISH_NETWORK_SECRETSTORE_H
|
||||
#define CUTEFISH_NETWORK_SECRETSTORE_H
|
||||
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusObjectPath>
|
||||
#include <QObject>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include <functional>
|
||||
|
||||
/**
|
||||
* Connection secrets kept in the user's keyring through the freedesktop Secret
|
||||
* Service (gnome-keyring here). Items carry nm-applet's attributes, so secrets
|
||||
* stored by either agent are found by the other.
|
||||
*/
|
||||
class SecretStore : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit SecretStore(QObject *parent = nullptr);
|
||||
|
||||
/** Hands over key → value for one setting of a connection; empty when none are stored. */
|
||||
void load(const QString &uuid, const QString &settingName,
|
||||
std::function<void(const QVariantMap &)> done);
|
||||
void save(const QString &uuid, const QString &connectionName, const QString &settingName,
|
||||
const QString &key, const QString &value);
|
||||
void remove(const QString &uuid);
|
||||
|
||||
private:
|
||||
using Reply = std::function<void(const QDBusMessage &)>;
|
||||
using Paths = QList<QDBusObjectPath>;
|
||||
|
||||
void call(const QString &path, const QString &interface, const QString &method,
|
||||
const QVariantList &arguments, Reply done);
|
||||
void openSession(std::function<void(const QString &)> done);
|
||||
void closeSession(const QString &session);
|
||||
void search(const QMap<QString, QString> &attributes, std::function<void(const Paths &)> done);
|
||||
void unlock(const Paths &objects, std::function<void(const Paths &)> done);
|
||||
void prompt(const QDBusObjectPath &prompt, std::function<void(bool, const QVariant &)> done);
|
||||
};
|
||||
|
||||
#endif // CUTEFISH_NETWORK_SECRETSTORE_H
|
||||
@ -0,0 +1,7 @@
|
||||
find_package(Qt6 REQUIRED COMPONENTS Core DBus Qml Test)
|
||||
find_program(DBUS_RUN_SESSION dbus-run-session REQUIRED)
|
||||
|
||||
add_executable(tst_secretagent tst_secretagent.cpp fakesecrets.h)
|
||||
target_link_libraries(tst_secretagent PRIVATE cutefish-framework-network Qt6::Test)
|
||||
add_test(NAME network-secretagent COMMAND ${DBUS_RUN_SESSION} -- $<TARGET_FILE:tst_secretagent>)
|
||||
set_tests_properties(network-secretagent PROPERTIES TIMEOUT 60)
|
||||
@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
#include <QDBusArgument>
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusMetaType>
|
||||
#include <QDBusObjectPath>
|
||||
#include <QDBusVariant>
|
||||
#include <QDBusVirtualObject>
|
||||
#include <QMap>
|
||||
|
||||
using Attributes = QMap<QString, QString>;
|
||||
|
||||
// The freedesktop Secret Service as gnome-keyring answers it: one unlocked
|
||||
// default collection and plain sessions.
|
||||
class FakeSecretService : public QDBusVirtualObject
|
||||
{
|
||||
public:
|
||||
struct Item {
|
||||
Attributes attributes;
|
||||
QByteArray value;
|
||||
};
|
||||
|
||||
FakeSecretService() { qDBusRegisterMetaType<Attributes>(); }
|
||||
|
||||
QString introspect(const QString &) const override { return {}; }
|
||||
|
||||
bool handleMessage(const QDBusMessage &message, const QDBusConnection &connection) override
|
||||
{
|
||||
const auto args = message.arguments();
|
||||
const QString member = message.member();
|
||||
QDBusMessage reply = message.createReply();
|
||||
|
||||
if (member == QStringLiteral("OpenSession")) {
|
||||
reply << QVariant::fromValue(QDBusVariant(QString()))
|
||||
<< QVariant::fromValue(QDBusObjectPath(sessionPath));
|
||||
} else if (member == QStringLiteral("Close")) {
|
||||
++closedSessions;
|
||||
} else if (member == QStringLiteral("ReadAlias")) {
|
||||
reply << QVariant::fromValue(QDBusObjectPath(collectionPath));
|
||||
} else if (member == QStringLiteral("SearchItems")) {
|
||||
const auto wanted = qdbus_cast<Attributes>(args.value(0));
|
||||
QList<QDBusObjectPath> found;
|
||||
for (auto it = items.cbegin(); it != items.cend(); ++it) {
|
||||
if (matches(it->attributes, wanted))
|
||||
found.append(QDBusObjectPath(it.key()));
|
||||
}
|
||||
reply << QVariant::fromValue(found) << QVariant::fromValue(QList<QDBusObjectPath>());
|
||||
} else if (member == QStringLiteral("Unlock")) {
|
||||
reply << args.value(0) << QVariant::fromValue(QDBusObjectPath(QStringLiteral("/")));
|
||||
} else if (member == QStringLiteral("CreateItem")) {
|
||||
const auto properties = qdbus_cast<QVariantMap>(args.value(0));
|
||||
Item item;
|
||||
item.attributes = qdbus_cast<Attributes>(
|
||||
properties.value(QStringLiteral("org.freedesktop.Secret.Item.Attributes")));
|
||||
const auto secret = args.value(1).value<QDBusArgument>();
|
||||
QDBusObjectPath session;
|
||||
QByteArray parameters;
|
||||
QString contentType;
|
||||
secret.beginStructure();
|
||||
secret >> session >> parameters >> item.value >> contentType;
|
||||
secret.endStructure();
|
||||
|
||||
QString path;
|
||||
for (auto it = items.cbegin(); it != items.cend(); ++it) {
|
||||
if (it->attributes == item.attributes)
|
||||
path = it.key();
|
||||
}
|
||||
if (path.isEmpty())
|
||||
path = collectionPath + QStringLiteral("/%1").arg(++lastItem);
|
||||
items.insert(path, item);
|
||||
reply << QVariant::fromValue(QDBusObjectPath(path))
|
||||
<< QVariant::fromValue(QDBusObjectPath(QStringLiteral("/")));
|
||||
} else if (member == QStringLiteral("GetSecret")) {
|
||||
QDBusArgument secret;
|
||||
secret.beginStructure();
|
||||
secret << QDBusObjectPath(sessionPath) << QByteArray()
|
||||
<< items.value(message.path()).value << QStringLiteral("text/plain");
|
||||
secret.endStructure();
|
||||
reply << QVariant::fromValue(secret);
|
||||
} else if (member == QStringLiteral("Get")) {
|
||||
reply << QVariant::fromValue(
|
||||
QDBusVariant(QVariant::fromValue(items.value(message.path()).attributes)));
|
||||
} else if (member == QStringLiteral("Delete")) {
|
||||
items.remove(message.path());
|
||||
reply << QVariant::fromValue(QDBusObjectPath(QStringLiteral("/")));
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
connection.send(reply);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool matches(const Attributes &attributes, const Attributes &wanted)
|
||||
{
|
||||
for (auto it = wanted.cbegin(); it != wanted.cend(); ++it) {
|
||||
if (attributes.value(it.key()) != it.value())
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const QString sessionPath = QStringLiteral("/org/freedesktop/secrets/session/s1");
|
||||
const QString collectionPath = QStringLiteral("/org/freedesktop/secrets/collection/login");
|
||||
QMap<QString, Item> items;
|
||||
int lastItem = 0;
|
||||
int closedSessions = 0;
|
||||
};
|
||||
|
||||
// Just enough of NetworkManager for NetworkManagerQt to register its agent.
|
||||
class FakeAgentManager : public QDBusVirtualObject
|
||||
{
|
||||
public:
|
||||
QString introspect(const QString &) const override { return {}; }
|
||||
|
||||
bool handleMessage(const QDBusMessage &message, const QDBusConnection &connection) override
|
||||
{
|
||||
if (message.interface() != QStringLiteral("org.freedesktop.NetworkManager.AgentManager"))
|
||||
return false;
|
||||
connection.send(message.createReply());
|
||||
return true;
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,327 @@
|
||||
#include "../secretagent.h"
|
||||
#include "fakesecrets.h"
|
||||
|
||||
#include <QDBusPendingCallWatcher>
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include <memory>
|
||||
|
||||
namespace {
|
||||
|
||||
const QString agentPath = QStringLiteral("/org/freedesktop/NetworkManager/SecretAgent");
|
||||
const QString agentInterface = QStringLiteral("org.freedesktop.NetworkManager.SecretAgent");
|
||||
const QString connectionPath = QStringLiteral("/org/freedesktop/NetworkManager/Settings/7");
|
||||
const QString uuid = QStringLiteral("5e3c1b64-3b43-4a2c-9d53-0d6c1a1f4f10");
|
||||
const QString wirelessSecurity = QStringLiteral("802-11-wireless-security");
|
||||
|
||||
// SecretAgent::GetSecretsFlag
|
||||
constexpr uint allowInteraction = 0x1;
|
||||
constexpr uint requestNew = 0x2;
|
||||
|
||||
NMVariantMapMap wpaConnection(uint pskFlags = 0)
|
||||
{
|
||||
NMVariantMapMap connection;
|
||||
connection[QStringLiteral("connection")] = {{QStringLiteral("id"), QStringLiteral("Cafe")},
|
||||
{QStringLiteral("uuid"), uuid}};
|
||||
connection[wirelessSecurity] = {{QStringLiteral("key-mgmt"), QStringLiteral("wpa-psk")},
|
||||
{QStringLiteral("psk-flags"), pskFlags}};
|
||||
return connection;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class SecretAgentTest : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void init()
|
||||
{
|
||||
server = std::make_unique<QDBusConnection>(QDBusConnection::connectToBus(
|
||||
QDBusConnection::SessionBus, QStringLiteral("fake-services")));
|
||||
QVERIFY(server->isConnected());
|
||||
secrets = std::make_unique<FakeSecretService>();
|
||||
networkManager = std::make_unique<FakeAgentManager>();
|
||||
QVERIFY(server->registerVirtualObject(QStringLiteral("/org/freedesktop/secrets"),
|
||||
secrets.get(), QDBusConnection::SubPath));
|
||||
QVERIFY(server->registerVirtualObject(
|
||||
QStringLiteral("/org/freedesktop/NetworkManager/AgentManager"),
|
||||
networkManager.get()));
|
||||
QVERIFY(server->registerService(QStringLiteral("org.freedesktop.secrets")));
|
||||
QVERIFY(server->registerService(QStringLiteral("org.freedesktop.NetworkManager")));
|
||||
|
||||
agent = std::make_unique<NetworkSecretAgent>();
|
||||
}
|
||||
|
||||
void cleanup()
|
||||
{
|
||||
agent.reset();
|
||||
server->unregisterService(QStringLiteral("org.freedesktop.secrets"));
|
||||
server->unregisterService(QStringLiteral("org.freedesktop.NetworkManager"));
|
||||
server->unregisterObject(QStringLiteral("/org/freedesktop/secrets"),
|
||||
QDBusConnection::UnregisterTree);
|
||||
server->unregisterObject(QStringLiteral("/org/freedesktop/NetworkManager/AgentManager"));
|
||||
networkManager.reset();
|
||||
secrets.reset();
|
||||
server.reset();
|
||||
QDBusConnection::disconnectFromBus(QStringLiteral("fake-services"));
|
||||
}
|
||||
|
||||
void promptsWhenNothingIsStored()
|
||||
{
|
||||
QDBusPendingCallWatcher reply(getSecrets(wpaConnection(), wirelessSecurity, allowInteraction));
|
||||
QTRY_VERIFY(agent->pending());
|
||||
QCOMPARE(agent->connectionName(), QStringLiteral("Cafe"));
|
||||
QCOMPARE(agent->kind(), NetworkSecretAgent::WpaPassword);
|
||||
QVERIFY(!agent->retry());
|
||||
|
||||
agent->respond(QStringLiteral("correct horse"));
|
||||
QTRY_VERIFY(reply.isFinished());
|
||||
QVERIFY(!reply.isError());
|
||||
QCOMPARE(secretsOf(reply).value(wirelessSecurity).value(QStringLiteral("psk")).toString(),
|
||||
QStringLiteral("correct horse"));
|
||||
QVERIFY(!agent->pending());
|
||||
}
|
||||
|
||||
void answersFromTheKeyring()
|
||||
{
|
||||
saveSecrets(QStringLiteral("stored password"));
|
||||
QTRY_COMPARE(secrets->items.size(), 1);
|
||||
const auto item = secrets->items.first();
|
||||
QCOMPARE(item.attributes.value(QStringLiteral("connection-uuid")), uuid);
|
||||
QCOMPARE(item.attributes.value(QStringLiteral("setting-name")), wirelessSecurity);
|
||||
QCOMPARE(item.attributes.value(QStringLiteral("setting-key")), QStringLiteral("psk"));
|
||||
QCOMPARE(item.attributes.value(QStringLiteral("xdg:schema")),
|
||||
QStringLiteral("org.freedesktop.NetworkManager.Connection"));
|
||||
|
||||
// No interaction allowed: what the keyring holds is the whole answer.
|
||||
QDBusPendingCallWatcher reply(getSecrets(wpaConnection(1), wirelessSecurity, 0));
|
||||
QTRY_VERIFY(reply.isFinished());
|
||||
QVERIFY(!reply.isError());
|
||||
QCOMPARE(secretsOf(reply).value(wirelessSecurity).value(QStringLiteral("psk")).toString(),
|
||||
QStringLiteral("stored password"));
|
||||
QVERIFY(!agent->pending());
|
||||
QTRY_COMPARE(secrets->closedSessions, 2);
|
||||
}
|
||||
|
||||
void savesOnlyAgentOwnedSecrets()
|
||||
{
|
||||
auto connection = wpaConnection(0);
|
||||
connection[wirelessSecurity][QStringLiteral("psk")] = QStringLiteral("system owned");
|
||||
call(QStringLiteral("SaveSecrets"),
|
||||
{QVariant::fromValue(connection), QVariant::fromValue(QDBusObjectPath(connectionPath))});
|
||||
QTest::qWait(100);
|
||||
QVERIFY(secrets->items.isEmpty());
|
||||
}
|
||||
|
||||
void rejectedPasswordPromptsAgain()
|
||||
{
|
||||
saveSecrets(QStringLiteral("wrong password"));
|
||||
QTRY_COMPARE(secrets->items.size(), 1);
|
||||
|
||||
QDBusPendingCallWatcher reply(
|
||||
getSecrets(wpaConnection(1), wirelessSecurity, allowInteraction | requestNew));
|
||||
QTRY_VERIFY(agent->pending());
|
||||
QVERIFY(agent->retry());
|
||||
|
||||
agent->cancel();
|
||||
QTRY_VERIFY(reply.isFinished());
|
||||
QCOMPARE(reply.error().name(),
|
||||
QStringLiteral("org.freedesktop.NetworkManager.SecretAgent.UserCanceled"));
|
||||
}
|
||||
|
||||
void nothingToOfferWithoutInteraction()
|
||||
{
|
||||
QDBusPendingCallWatcher reply(getSecrets(wpaConnection(1), wirelessSecurity, 0));
|
||||
QTRY_VERIFY(reply.isFinished());
|
||||
QCOMPARE(reply.error().name(),
|
||||
QStringLiteral("org.freedesktop.NetworkManager.SecretAgent.NoSecrets"));
|
||||
QVERIFY(!agent->pending());
|
||||
}
|
||||
|
||||
void networkManagerCancels()
|
||||
{
|
||||
QDBusPendingCallWatcher reply(getSecrets(wpaConnection(), wirelessSecurity, allowInteraction));
|
||||
QTRY_VERIFY(agent->pending());
|
||||
|
||||
call(QStringLiteral("CancelGetSecrets"),
|
||||
{QVariant::fromValue(QDBusObjectPath(connectionPath)), wirelessSecurity});
|
||||
QTRY_VERIFY(reply.isFinished());
|
||||
QCOMPARE(reply.error().name(),
|
||||
QStringLiteral("org.freedesktop.NetworkManager.SecretAgent.AgentCanceled"));
|
||||
QVERIFY(!agent->pending());
|
||||
}
|
||||
|
||||
void requestsQueue()
|
||||
{
|
||||
auto wep = wpaConnection();
|
||||
wep[QStringLiteral("connection")][QStringLiteral("id")] = QStringLiteral("Old router");
|
||||
wep[QStringLiteral("connection")][QStringLiteral("uuid")] = QStringLiteral("other");
|
||||
wep[wirelessSecurity] = {{QStringLiteral("key-mgmt"), QStringLiteral("none")},
|
||||
{QStringLiteral("wep-tx-keyidx"), 2u}};
|
||||
|
||||
QDBusPendingCallWatcher first(getSecrets(wpaConnection(), wirelessSecurity, allowInteraction));
|
||||
QTRY_VERIFY(agent->pending());
|
||||
QDBusPendingCallWatcher second(getSecrets(wep, wirelessSecurity, allowInteraction));
|
||||
QTest::qWait(100);
|
||||
QCOMPARE(agent->connectionName(), QStringLiteral("Cafe"));
|
||||
|
||||
QSignalSpy changed(agent.get(), &NetworkSecretAgent::requestChanged);
|
||||
agent->respond(QStringLiteral("first password"));
|
||||
QTRY_VERIFY(first.isFinished());
|
||||
QCOMPARE(changed.size(), 1);
|
||||
QVERIFY(agent->pending());
|
||||
QCOMPARE(agent->connectionName(), QStringLiteral("Old router"));
|
||||
QCOMPARE(agent->kind(), NetworkSecretAgent::WepKey);
|
||||
|
||||
agent->respond(QStringLiteral("abcde"));
|
||||
QTRY_VERIFY(second.isFinished());
|
||||
QCOMPARE(secretsOf(second).value(wirelessSecurity).value(QStringLiteral("wep-key2")).toString(),
|
||||
QStringLiteral("abcde"));
|
||||
QVERIFY(!agent->pending());
|
||||
}
|
||||
|
||||
void enterpriseNeedsAnIdentity()
|
||||
{
|
||||
auto connection = wpaConnection();
|
||||
connection[QStringLiteral("802-1x")] = {
|
||||
{QStringLiteral("eap"), QStringList {QStringLiteral("peap")}}};
|
||||
|
||||
QDBusPendingCallWatcher anonymous(
|
||||
getSecrets(connection, QStringLiteral("802-1x"), allowInteraction));
|
||||
QTRY_VERIFY(anonymous.isFinished());
|
||||
QCOMPARE(anonymous.error().name(),
|
||||
QStringLiteral("org.freedesktop.NetworkManager.SecretAgent.NoSecrets"));
|
||||
|
||||
connection[QStringLiteral("802-1x")][QStringLiteral("identity")] = QStringLiteral("alice");
|
||||
QDBusPendingCallWatcher named(getSecrets(connection, QStringLiteral("802-1x"), allowInteraction));
|
||||
QTRY_VERIFY(agent->pending());
|
||||
QCOMPARE(agent->kind(), NetworkSecretAgent::EapPassword);
|
||||
QCOMPARE(agent->identity(), QStringLiteral("alice"));
|
||||
agent->respond(QStringLiteral("hunter2"));
|
||||
QTRY_VERIFY(named.isFinished());
|
||||
QCOMPARE(secretsOf(named).value(QStringLiteral("802-1x")).value(QStringLiteral("password")).toString(),
|
||||
QStringLiteral("hunter2"));
|
||||
}
|
||||
|
||||
void asksBeforeANetworkIsAdded()
|
||||
{
|
||||
auto connect = [this] {
|
||||
auto message = QDBusMessage::createMethodCall(
|
||||
QStringLiteral("com.cutefish.SecretAgent"), QStringLiteral("/SecretAgent"),
|
||||
QStringLiteral("com.cutefish.SecretAgent"), QStringLiteral("ConnectWireless"));
|
||||
message << QStringLiteral("/org/freedesktop/NetworkManager/Devices/3")
|
||||
<< QStringLiteral("/org/freedesktop/NetworkManager/AccessPoint/9")
|
||||
<< QStringLiteral("Old router") << true;
|
||||
return server->asyncCall(message);
|
||||
};
|
||||
|
||||
QDBusPendingCallWatcher first(connect());
|
||||
QTRY_VERIFY(first.isFinished());
|
||||
QVERIFY(!first.isError());
|
||||
QVERIFY(agent->pending());
|
||||
QCOMPARE(agent->connectionName(), QStringLiteral("Old router"));
|
||||
QCOMPARE(agent->kind(), NetworkSecretAgent::WepKey);
|
||||
QVERIFY(!agent->retry());
|
||||
|
||||
// A second click on the same network does not queue a second prompt.
|
||||
QDBusPendingCallWatcher second(connect());
|
||||
QTRY_VERIFY(second.isFinished());
|
||||
QSignalSpy changed(agent.get(), &NetworkSecretAgent::requestChanged);
|
||||
agent->cancel();
|
||||
QCOMPARE(changed.size(), 1);
|
||||
QVERIFY(!agent->pending());
|
||||
}
|
||||
|
||||
void anotherNetworkTakesThePromptOver()
|
||||
{
|
||||
auto connect = [this](const QString &accessPoint, const QString &name) {
|
||||
auto message = QDBusMessage::createMethodCall(
|
||||
QStringLiteral("com.cutefish.SecretAgent"), QStringLiteral("/SecretAgent"),
|
||||
QStringLiteral("com.cutefish.SecretAgent"), QStringLiteral("ConnectWireless"));
|
||||
message << QStringLiteral("/org/freedesktop/NetworkManager/Devices/3") << accessPoint
|
||||
<< name << false;
|
||||
return server->asyncCall(message);
|
||||
};
|
||||
|
||||
// NetworkManager is still waiting on the password of the network being left.
|
||||
QDBusPendingCallWatcher leaving(getSecrets(wpaConnection(), wirelessSecurity,
|
||||
allowInteraction | requestNew));
|
||||
QTRY_VERIFY(agent->pending());
|
||||
QCOMPARE(agent->connectionName(), QStringLiteral("Cafe"));
|
||||
|
||||
QDBusPendingCallWatcher first(connect(QStringLiteral("/org/freedesktop/NetworkManager/AccessPoint/1"),
|
||||
QStringLiteral("Library")));
|
||||
QTRY_COMPARE(agent->connectionName(), QStringLiteral("Library"));
|
||||
QTRY_VERIFY(leaving.isFinished());
|
||||
QCOMPARE(leaving.error().name(),
|
||||
QStringLiteral("org.freedesktop.NetworkManager.SecretAgent.UserCanceled"));
|
||||
|
||||
QDBusPendingCallWatcher second(connect(QStringLiteral("/org/freedesktop/NetworkManager/AccessPoint/2"),
|
||||
QStringLiteral("Station")));
|
||||
QTRY_COMPARE(agent->connectionName(), QStringLiteral("Station"));
|
||||
|
||||
// Only the network picked last is waiting.
|
||||
agent->cancel();
|
||||
QVERIFY(!agent->pending());
|
||||
}
|
||||
|
||||
void deletesWithTheConnection()
|
||||
{
|
||||
saveSecrets(QStringLiteral("stored password"));
|
||||
QTRY_COMPARE(secrets->items.size(), 1);
|
||||
|
||||
call(QStringLiteral("DeleteSecrets"),
|
||||
{QVariant::fromValue(wpaConnection(1)), QVariant::fromValue(QDBusObjectPath(connectionPath))});
|
||||
QTRY_VERIFY(secrets->items.isEmpty());
|
||||
}
|
||||
|
||||
private:
|
||||
QDBusPendingCall call(const QString &method, const QVariantList &arguments)
|
||||
{
|
||||
// The agent lives on the "system" bus, which the test points at the session bus.
|
||||
auto message = QDBusMessage::createMethodCall(QDBusConnection::systemBus().baseService(),
|
||||
agentPath, agentInterface, method);
|
||||
message.setArguments(arguments);
|
||||
return server->asyncCall(message);
|
||||
}
|
||||
|
||||
QDBusPendingCall getSecrets(const NMVariantMapMap &connection, const QString &setting, uint flags)
|
||||
{
|
||||
return call(QStringLiteral("GetSecrets"),
|
||||
{QVariant::fromValue(connection), QVariant::fromValue(QDBusObjectPath(connectionPath)),
|
||||
setting, QStringList(), flags});
|
||||
}
|
||||
|
||||
void saveSecrets(const QString &psk)
|
||||
{
|
||||
auto connection = wpaConnection(1);
|
||||
connection[wirelessSecurity][QStringLiteral("psk")] = psk;
|
||||
QDBusPendingCallWatcher reply(call(QStringLiteral("SaveSecrets"),
|
||||
{QVariant::fromValue(connection),
|
||||
QVariant::fromValue(QDBusObjectPath(connectionPath))}));
|
||||
QTRY_VERIFY(reply.isFinished());
|
||||
QVERIFY(!reply.isError());
|
||||
}
|
||||
|
||||
static NMVariantMapMap secretsOf(const QDBusPendingCallWatcher &reply)
|
||||
{
|
||||
return qdbus_cast<NMVariantMapMap>(reply.reply().arguments().value(0));
|
||||
}
|
||||
|
||||
std::unique_ptr<QDBusConnection> server;
|
||||
std::unique_ptr<FakeSecretService> secrets;
|
||||
std::unique_ptr<FakeAgentManager> networkManager;
|
||||
std::unique_ptr<NetworkSecretAgent> agent;
|
||||
};
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
qputenv("DBUS_SYSTEM_BUS_ADDRESS", qgetenv("DBUS_SESSION_BUS_ADDRESS"));
|
||||
QCoreApplication app(argc, argv);
|
||||
SecretAgentTest test;
|
||||
return QTest::qExec(&test, argc, argv);
|
||||
}
|
||||
|
||||
#include "tst_secretagent.moc"
|
||||
Loading…
Reference in New Issue