2
* Copyright (C) 2017 Toni Spets <toni.spets@iki.fi>
3
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
5
* This program is free software: you can redistribute it and/or modify
6
* it under the terms of the GNU General Public License as published by
7
* the Free Software Foundation, either version 2 or (at your option)
8
* version 3 of the License.
10
* This program is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
* GNU General Public License for more details.
15
* You should have received a copy of the GNU General Public License
16
* along with this program. If not, see <http://www.gnu.org/licenses/>.
21
#include "core/Config.h"
22
#include "core/Group.h"
23
#include "core/Metadata.h"
24
#include "sshagent/BinaryStream.h"
25
#include "sshagent/KeeAgentSettings.h"
28
#include <QLocalSocket>
36
Q_GLOBAL_STATIC(SSHAgent, s_sshAgent);
38
SSHAgent* SSHAgent::instance()
43
bool SSHAgent::isEnabled() const
45
return config()->get(Config::SSHAgent_Enabled).toBool();
48
void SSHAgent::setEnabled(bool enabled)
50
if (isEnabled() && !enabled) {
51
removeAllIdentities();
54
config()->set(Config::SSHAgent_Enabled, enabled);
56
emit enabledChanged(enabled);
59
QString SSHAgent::authSockOverride() const
61
return config()->get(Config::SSHAgent_AuthSockOverride).toString();
64
QString SSHAgent::securityKeyProviderOverride() const
66
return config()->get(Config::SSHAgent_SecurityKeyProviderOverride).toString();
69
void SSHAgent::setAuthSockOverride(QString& authSockOverride)
71
config()->set(Config::SSHAgent_AuthSockOverride, authSockOverride);
74
void SSHAgent::setSecurityKeyProviderOverride(QString& securityKeyProviderOverride)
76
config()->set(Config::SSHAgent_SecurityKeyProviderOverride, securityKeyProviderOverride);
80
bool SSHAgent::useOpenSSH() const
82
return config()->get(Config::SSHAgent_UseOpenSSH).toBool();
85
bool SSHAgent::usePageant() const
87
return config()->get(Config::SSHAgent_UsePageant).toBool();
90
void SSHAgent::setUseOpenSSH(bool useOpenSSH)
92
config()->set(Config::SSHAgent_UseOpenSSH, useOpenSSH);
95
void SSHAgent::setUsePageant(bool usePageant)
97
config()->set(Config::SSHAgent_UsePageant, usePageant);
101
QString SSHAgent::socketPath(bool allowOverride) const
107
socketPath = authSockOverride();
110
// if the overridden path is empty (no override set), default to environment
111
if (socketPath.isEmpty()) {
112
socketPath = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK");
115
Q_UNUSED(allowOverride)
116
socketPath = "\\\\.\\pipe\\openssh-ssh-agent";
122
QString SSHAgent::securityKeyProvider(bool allowOverride) const
127
skProvider = securityKeyProviderOverride();
130
if (skProvider.isEmpty()) {
131
skProvider = QProcessEnvironment::systemEnvironment().value("SSH_SK_PROVIDER", "internal");
137
const QString SSHAgent::errorString() const
142
bool SSHAgent::isAgentRunning() const
145
QFileInfo socketFileInfo(socketPath());
146
return !socketFileInfo.path().isEmpty() && socketFileInfo.exists();
148
if (usePageant() && useOpenSSH()) {
149
return (FindWindowA("Pageant", "Pageant") != nullptr) && WaitNamedPipe(socketPath().toLatin1().data(), 100);
150
} else if (useOpenSSH()) {
151
return WaitNamedPipe(socketPath().toLatin1().data(), 100);
152
} else if (usePageant()) {
153
return (FindWindowA("Pageant", "Pageant") != nullptr);
160
bool SSHAgent::sendMessage(const QByteArray& in, QByteArray& out)
163
if (usePageant() && !sendMessagePageant(in, out)) {
166
if (useOpenSSH() && !sendMessageOpenSSH(in, out)) {
171
return sendMessageOpenSSH(in, out);
175
bool SSHAgent::sendMessageOpenSSH(const QByteArray& in, QByteArray& out)
178
BinaryStream stream(&socket);
180
socket.connectToServer(socketPath());
181
if (!socket.waitForConnected(500)) {
182
m_error = tr("Agent connection failed.");
186
stream.writeString(in);
189
if (!stream.readString(out)) {
190
m_error = tr("Agent protocol error.");
199
bool SSHAgent::sendMessagePageant(const QByteArray& in, QByteArray& out)
201
HWND hWnd = FindWindowA("Pageant", "Pageant");
204
m_error = tr("Agent connection failed.");
208
if (static_cast<quint32>(in.length()) > AGENT_MAX_MSGLEN - 4) {
209
m_error = tr("Agent connection failed.");
213
auto threadId = reinterpret_cast<qlonglong>(QThread::currentThreadId());
214
QByteArray mapName = (QString("SSHAgentRequest%1").arg(threadId, 8, 16, QChar('0'))).toLatin1();
216
HANDLE handle = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapName.data());
219
m_error = tr("Agent connection failed.");
223
LPVOID ptr = MapViewOfFile(handle, FILE_MAP_WRITE, 0, 0, 0);
227
m_error = tr("Agent connection failed.");
231
quint32* requestLength = reinterpret_cast<quint32*>(ptr);
232
void* requestData = reinterpret_cast<void*>(reinterpret_cast<char*>(ptr) + 4);
234
*requestLength = qToBigEndian<quint32>(in.length());
235
memcpy(requestData, in.data(), in.length());
238
data.dwData = AGENT_COPYDATA_ID;
239
data.cbData = mapName.length() + 1;
240
data.lpData = reinterpret_cast<LPVOID>(mapName.data());
242
LRESULT res = SendMessageA(hWnd, WM_COPYDATA, 0, reinterpret_cast<LPARAM>(&data));
245
quint32 responseLength = qFromBigEndian<quint32>(*requestLength);
246
if (responseLength <= AGENT_MAX_MSGLEN) {
247
out.resize(responseLength);
248
memcpy(out.data(), requestData, responseLength);
250
m_error = tr("Agent protocol error.");
253
m_error = tr("Agent protocol error.");
256
UnmapViewOfFile(ptr);
264
* Add the identity to the SSH agent.
266
* @param key identity / key to add
267
* @param settings constraints (lifetime, confirm), remove-on-lock
268
* @param databaseUuid database that owns the key for remove-on-lock
269
* @return true on success
271
bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, const QUuid& databaseUuid)
273
if (!isAgentRunning()) {
274
m_error = tr("No agent running, cannot add identity.");
278
if (m_addedKeys.contains(key) && m_addedKeys[key].first != databaseUuid) {
279
m_error = tr("Key identity ownership conflict. Refusing to add.");
283
QByteArray requestData;
284
BinaryStream request(&requestData);
285
bool isSecurityKey = key.type().startsWith("sk-");
288
(settings.useLifetimeConstraintWhenAdding() || settings.useConfirmConstraintWhenAdding() || isSecurityKey)
289
? SSH_AGENTC_ADD_ID_CONSTRAINED
290
: SSH_AGENTC_ADD_IDENTITY);
291
key.writePrivate(request);
293
if (settings.useLifetimeConstraintWhenAdding()) {
294
request.write(SSH_AGENT_CONSTRAIN_LIFETIME);
295
request.write(static_cast<quint32>(settings.lifetimeConstraintDuration()));
298
if (settings.useConfirmConstraintWhenAdding()) {
299
request.write(SSH_AGENT_CONSTRAIN_CONFIRM);
303
request.write(SSH_AGENT_CONSTRAIN_EXTENSION);
304
request.writeString(QString("sk-provider@openssh.com"));
305
request.writeString(securityKeyProvider());
308
QByteArray responseData;
309
if (!sendMessage(requestData, responseData)) {
313
if (responseData.length() < 1 || static_cast<quint8>(responseData[0]) != SSH_AGENT_SUCCESS) {
315
tr("Agent refused this identity. Possible reasons include:") + "\n" + tr("The key has already been added.");
317
if (settings.useLifetimeConstraintWhenAdding()) {
318
m_error += "\n" + tr("Restricted lifetime is not supported by the agent (check options).");
321
if (settings.useConfirmConstraintWhenAdding()) {
322
m_error += "\n" + tr("A confirmation request is not supported by the agent (check options).");
327
"\n" + tr("Security keys are not supported by the agent or the security key provider is unavailable.");
333
OpenSSHKey keyCopy = key;
334
keyCopy.clearPrivate();
335
m_addedKeys[keyCopy] = qMakePair(databaseUuid, settings.removeAtDatabaseClose());
340
* Remove an identity from the SSH agent.
342
* @param key identity to remove
343
* @return true on success
345
bool SSHAgent::removeIdentity(OpenSSHKey& key)
347
if (!isAgentRunning()) {
348
m_error = tr("No agent running, cannot remove identity.");
352
QByteArray requestData;
353
BinaryStream request(&requestData);
356
BinaryStream keyStream(&keyData);
357
key.writePublic(keyStream);
359
request.write(SSH_AGENTC_REMOVE_IDENTITY);
360
request.writeString(keyData);
362
QByteArray responseData;
363
return sendMessage(requestData, responseData);
367
* Get a list of identities from the SSH agent.
369
* @param list list of keys to append
370
* @return true on success
372
bool SSHAgent::listIdentities(QList<QSharedPointer<OpenSSHKey>>& list)
374
if (!isAgentRunning()) {
375
m_error = tr("No agent running, cannot list identities.");
379
QByteArray requestData;
380
BinaryStream request(&requestData);
382
request.write(SSH_AGENTC_REQUEST_IDENTITIES);
384
QByteArray responseData;
385
if (!sendMessage(requestData, responseData)) {
389
BinaryStream response(&responseData);
392
if (!response.read(responseType) || responseType != SSH_AGENT_IDENTITIES_ANSWER) {
393
m_error = tr("Agent protocol error.");
398
if (!response.read(nKeys)) {
399
m_error = tr("Agent protocol error.");
403
for (quint32 i = 0; i < nKeys; i++) {
404
QByteArray publicData;
407
if (!response.readString(publicData)) {
408
m_error = tr("Agent protocol error.");
412
if (!response.readString(comment)) {
413
m_error = tr("Agent protocol error.");
417
OpenSSHKey* key = new OpenSSHKey();
418
key->setComment(comment);
420
list.append(QSharedPointer<OpenSSHKey>(key));
422
BinaryStream publicDataStream(&publicData);
423
if (!key->readPublic(publicDataStream)) {
424
m_error = key->errorString();
433
* Check if this identity is loaded in the SSH Agent.
435
* @param key identity to remove
436
* @param loaded is the key loaded
437
* @return true on success
439
bool SSHAgent::checkIdentity(const OpenSSHKey& key, bool& loaded)
441
QList<QSharedPointer<OpenSSHKey>> list;
443
if (!listIdentities(list)) {
449
for (const auto& it : list) {
460
* Remove all identities known to this instance
462
void SSHAgent::removeAllIdentities()
464
auto it = m_addedKeys.begin();
465
while (it != m_addedKeys.end()) {
466
// Remove key if requested to remove on lock
467
if (it.value().second) {
468
OpenSSHKey key = it.key();
471
it = m_addedKeys.erase(it);
476
* Change "remove identity on lock" setting for a key already added to the agent.
477
* Will to nothing if the key has not been added to the agent.
479
* @param key key to change setting for
480
* @param autoRemove whether to remove the key from the agent when database is locked
482
void SSHAgent::setAutoRemoveOnLock(const OpenSSHKey& key, bool autoRemove)
484
if (m_addedKeys.contains(key)) {
485
m_addedKeys[key].second = autoRemove;
489
void SSHAgent::databaseLocked(const QSharedPointer<Database>& db)
495
auto it = m_addedKeys.begin();
496
while (it != m_addedKeys.end()) {
497
if (it.value().first != db->uuid()) {
501
OpenSSHKey key = it.key();
502
if (it.value().second) {
503
if (!removeIdentity(key)) {
507
it = m_addedKeys.erase(it);
511
void SSHAgent::databaseUnlocked(const QSharedPointer<Database>& db)
513
if (!db || !isEnabled()) {
517
for (auto entry : db->rootGroup()->entriesRecursive()) {
518
if (entry->isRecycled()) {
522
KeeAgentSettings settings;
524
if (!settings.fromEntry(entry)) {
528
if (!settings.allowUseOfSshKey() || !settings.addAtDatabaseOpen()) {
534
if (!settings.toOpenSSHKey(entry, key, true)) {
538
// Add key to agent; ignore errors if we have previously added the key
539
bool known_key = m_addedKeys.contains(key);
540
if (!addIdentity(key, settings, db->uuid()) && !known_key) {