feat(network): add a keyring-backed secret agent that asks for Wi-Fi passwords before connecting

pull/18/head
reionwong 1 month ago
parent 410e35392f
commit 40667cd0f1

@ -28,6 +28,12 @@ set(CUTEFISH_NETWORK_SOURCES
handler.cpp
handler.h
secretagent.cpp
secretagent.h
secretstore.cpp
secretstore.h
configuration.cpp
configuration.h
@ -71,3 +77,7 @@ target_link_libraries(cutefish-framework-network PUBLIC
)
cutefish_install_qml_module(cutefish-framework-network)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()

@ -73,7 +73,9 @@ public:
WpaEap,
Wpa2Psk,
Wpa2Eap,
SAE
SAE,
Wpa3SuiteB192,
OWE
};
};

@ -40,6 +40,8 @@
#include <QDBusPendingReply>
#include <QIcon>
#include <memory>
#include <pwd.h>
#include <sys/types.h>
@ -47,6 +49,11 @@
#define AGENT_PATH "/modules/networkmanagement"
#define AGENT_IFACE "org.kde.plasmanetworkmanagement"
// The shell's secret agent, which asks for a new network's password up front.
#define PROMPT_SERVICE "com.cutefish.SecretAgent"
#define PROMPT_PATH "/SecretAgent"
#define PROMPT_IFACE "com.cutefish.SecretAgent"
// 10 seconds
#define NM_REQUESTSCAN_LIMIT_RATE 10000
@ -168,6 +175,12 @@ QString Handler::wifiCode(const QString &connectionPath, const QString &ssid,
void Handler::addAndActivateConnection(const QString &device, const QString &specificObject,
const QString &password)
{
addAndActivateWireless(device, specificObject, password, true);
}
void Handler::addAndActivateWireless(const QString &device, const QString &specificObject,
const QString &password, bool askForPassword)
{
NetworkManager::AccessPoint::Ptr ap;
NetworkManager::WirelessDevice::Ptr wifiDev;
@ -211,18 +224,46 @@ void Handler::addAndActivateConnection(const QString &device, const QString &spe
(ap->mode() == NetworkManager::AccessPoint::Adhoc), ap->capabilities(), ap->wpaFlags(),
ap->rsnFlags());
// Asked for before anything is added: activating first would save the network
// and show it connecting while the user is still typing, or has cancelled.
const bool staticSecret = securityType == NetworkManager::StaticWep
|| securityType == NetworkManager::WpaPsk || securityType == NetworkManager::Wpa2Psk
|| securityType == NetworkManager::SAE;
if (askForPassword && password.isEmpty() && staticSecret) {
auto message = QDBusMessage::createMethodCall(
QStringLiteral(PROMPT_SERVICE), QStringLiteral(PROMPT_PATH),
QStringLiteral(PROMPT_IFACE), QStringLiteral("ConnectWireless"));
message << device << specificObject << ap->ssid()
<< (securityType == NetworkManager::StaticWep);
auto *watcher = new QDBusPendingCallWatcher(
QDBusConnection::sessionBus().asyncCall(message), this);
connect(watcher, &QDBusPendingCallWatcher::finished, this,
[this, device, specificObject](QDBusPendingCallWatcher *watcher) {
watcher->deleteLater();
// Without the shell there is no one to ask first; NetworkManager
// asks whichever agent there is once activation needs the secret.
if (watcher->isError())
addAndActivateWireless(device, specificObject, QString(), false);
});
return;
}
if (securityType != NetworkManager::NoneSecurity) {
wifiSecurity->setInitialized(true);
wifiSetting->setSecurity("802-11-wireless-security");
}
if (securityType == NetworkManager::Leap || securityType == NetworkManager::DynamicWep
|| securityType == NetworkManager::Wpa2Eap || securityType == NetworkManager::WpaEap) {
|| securityType == NetworkManager::Wpa2Eap || securityType == NetworkManager::WpaEap
|| securityType == NetworkManager::Wpa3SuiteB192) {
if (securityType == NetworkManager::DynamicWep || securityType == NetworkManager::Leap) {
wifiSecurity->setKeyMgmt(NetworkManager::WirelessSecuritySetting::Ieee8021x);
if (securityType == NetworkManager::Leap) {
wifiSecurity->setAuthAlg(NetworkManager::WirelessSecuritySetting::Leap);
}
} else if (securityType == NetworkManager::Wpa3SuiteB192) {
wifiSecurity->setKeyMgmt(NetworkManager::WirelessSecuritySetting::WpaEapSuiteB192);
} else {
wifiSecurity->setKeyMgmt(NetworkManager::WirelessSecuritySetting::WpaEap);
}
@ -231,28 +272,60 @@ void Handler::addAndActivateConnection(const QString &device, const QString &spe
m_tmpSpecificPath = specificObject;
} else {
// The connection is this user's alone, so its password goes to the
// user's keyring through the session's secret agent.
if (securityType == NetworkManager::StaticWep) {
wifiSecurity->setKeyMgmt(NetworkManager::WirelessSecuritySetting::Wep);
wifiSecurity->setWepKey0(password);
wifiSecurity->setWepKeyFlags(NetworkManager::Setting::AgentOwned);
} else if (securityType == NetworkManager::OWE) {
wifiSecurity->setKeyMgmt(NetworkManager::WirelessSecuritySetting::OWE);
} else {
if (ap->mode() == NetworkManager::AccessPoint::Adhoc) {
wifiSecurity->setKeyMgmt(NetworkManager::WirelessSecuritySetting::WpaNone);
} else if (securityType == NetworkManager::SAE) {
wifiSecurity->setKeyMgmt(NetworkManager::WirelessSecuritySetting::SAE);
} else {
wifiSecurity->setKeyMgmt(NetworkManager::WirelessSecuritySetting::WpaPsk);
}
wifiSecurity->setPsk(password);
wifiSecurity->setPskFlags(NetworkManager::Setting::AgentOwned);
}
QDBusPendingReply<QDBusObjectPath> reply =
QDBusPendingReply<QDBusObjectPath, QDBusObjectPath> reply =
NetworkManager::addAndActivateConnection(settings->toMap(), device, specificObject);
QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
watcher->setProperty("action", Handler::AddAndActivateConnection);
watcher->setProperty("connection", settings->name());
connect(watcher, &QDBusPendingCallWatcher::finished, this, &Handler::replyFinished);
connect(watcher, &QDBusPendingCallWatcher::finished, this,
[this](QDBusPendingCallWatcher *watcher) {
watcher->deleteLater();
QDBusPendingReply<QDBusObjectPath, QDBusObjectPath> reply = *watcher;
if (!reply.isError())
forgetUnlessActivated(reply.argumentAt<0>().path(),
reply.argumentAt<1>().path());
});
}
settings.clear();
}
void Handler::forgetUnlessActivated(const QString &connectionPath, const QString &activePath)
{
// A wrong password the user then gave up on would otherwise stay behind as a
// known network.
NetworkManager::ActiveConnection::Ptr active(new NetworkManager::ActiveConnection(activePath));
auto watch = std::make_shared<QMetaObject::Connection>();
*watch = connect(active.data(), &NetworkManager::ActiveConnection::stateChanged, this,
[this, active, watch, connectionPath](NetworkManager::ActiveConnection::State state) {
if (state == NetworkManager::ActiveConnection::Deactivated)
removeConnection(connectionPath);
else if (state != NetworkManager::ActiveConnection::Activated)
return;
// Breaks the cycle the lambda holds on the active connection.
QObject::disconnect(*watch);
});
}
void Handler::addConnection(const NMVariantMapMap &map)
{
QDBusPendingReply<QDBusObjectPath> reply = NetworkManager::addConnection(map);

@ -75,6 +75,10 @@ public Q_SLOTS:
*
* Works automatically for wireless connections with WEP/WPA security, for wireless connections
* with WPA/WPA it will open the connection editor for advanced configuration.
*
* Without a password, a network that needs one has it asked for by the session's
* secret agent before anything is added, and a new connection that never gets to
* connect is removed again.
* */
void addAndActivateConnection(const QString &device, const QString &specificParameter,
const QString &password = QString());
@ -135,6 +139,11 @@ public Q_SLOTS:
void createHotspot();
void stopHotspot();
private:
void addAndActivateWireless(const QString &device, const QString &specificObject,
const QString &password, bool askForPassword);
void forgetUnlessActivated(const QString &connectionPath, const QString &activePath);
private Q_SLOTS:
void secretAgentError(const QString &connectionPath, const QString &message);
void replyFinished(QDBusPendingCallWatcher *watcher);

@ -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…
Cancel
Save