keepassxc

Форк
0
/
SSHAgent.cpp 
544 строки · 13.9 Кб
1
/*
2
 *  Copyright (C) 2017 Toni Spets <toni.spets@iki.fi>
3
 *  Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
4
 *
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.
9
 *
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.
14
 *
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/>.
17
 */
18

19
#include "SSHAgent.h"
20

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"
26

27
#include <QFileInfo>
28
#include <QLocalSocket>
29
#include <QThread>
30

31
#ifdef Q_OS_WIN
32
#include <QtEndian>
33
#include <windows.h>
34
#endif
35

36
Q_GLOBAL_STATIC(SSHAgent, s_sshAgent);
37

38
SSHAgent* SSHAgent::instance()
39
{
40
    return s_sshAgent;
41
}
42

43
bool SSHAgent::isEnabled() const
44
{
45
    return config()->get(Config::SSHAgent_Enabled).toBool();
46
}
47

48
void SSHAgent::setEnabled(bool enabled)
49
{
50
    if (isEnabled() && !enabled) {
51
        removeAllIdentities();
52
    }
53

54
    config()->set(Config::SSHAgent_Enabled, enabled);
55

56
    emit enabledChanged(enabled);
57
}
58

59
QString SSHAgent::authSockOverride() const
60
{
61
    return config()->get(Config::SSHAgent_AuthSockOverride).toString();
62
}
63

64
QString SSHAgent::securityKeyProviderOverride() const
65
{
66
    return config()->get(Config::SSHAgent_SecurityKeyProviderOverride).toString();
67
}
68

69
void SSHAgent::setAuthSockOverride(QString& authSockOverride)
70
{
71
    config()->set(Config::SSHAgent_AuthSockOverride, authSockOverride);
72
}
73

74
void SSHAgent::setSecurityKeyProviderOverride(QString& securityKeyProviderOverride)
75
{
76
    config()->set(Config::SSHAgent_SecurityKeyProviderOverride, securityKeyProviderOverride);
77
}
78

79
#ifdef Q_OS_WIN
80
bool SSHAgent::useOpenSSH() const
81
{
82
    return config()->get(Config::SSHAgent_UseOpenSSH).toBool();
83
}
84

85
bool SSHAgent::usePageant() const
86
{
87
    return config()->get(Config::SSHAgent_UsePageant).toBool();
88
}
89

90
void SSHAgent::setUseOpenSSH(bool useOpenSSH)
91
{
92
    config()->set(Config::SSHAgent_UseOpenSSH, useOpenSSH);
93
}
94

95
void SSHAgent::setUsePageant(bool usePageant)
96
{
97
    config()->set(Config::SSHAgent_UsePageant, usePageant);
98
}
99
#endif
100

101
QString SSHAgent::socketPath(bool allowOverride) const
102
{
103
    QString socketPath;
104

105
#ifndef Q_OS_WIN
106
    if (allowOverride) {
107
        socketPath = authSockOverride();
108
    }
109

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");
113
    }
114
#else
115
    Q_UNUSED(allowOverride)
116
    socketPath = "\\\\.\\pipe\\openssh-ssh-agent";
117
#endif
118

119
    return socketPath;
120
}
121

122
QString SSHAgent::securityKeyProvider(bool allowOverride) const
123
{
124
    QString skProvider;
125

126
    if (allowOverride) {
127
        skProvider = securityKeyProviderOverride();
128
    }
129

130
    if (skProvider.isEmpty()) {
131
        skProvider = QProcessEnvironment::systemEnvironment().value("SSH_SK_PROVIDER", "internal");
132
    }
133

134
    return skProvider;
135
}
136

137
const QString SSHAgent::errorString() const
138
{
139
    return m_error;
140
}
141

142
bool SSHAgent::isAgentRunning() const
143
{
144
#ifndef Q_OS_WIN
145
    QFileInfo socketFileInfo(socketPath());
146
    return !socketFileInfo.path().isEmpty() && socketFileInfo.exists();
147
#else
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);
154
    } else {
155
        return false;
156
    }
157
#endif
158
}
159

160
bool SSHAgent::sendMessage(const QByteArray& in, QByteArray& out)
161
{
162
#ifdef Q_OS_WIN
163
    if (usePageant() && !sendMessagePageant(in, out)) {
164
        return false;
165
    }
166
    if (useOpenSSH() && !sendMessageOpenSSH(in, out)) {
167
        return false;
168
    }
169
    return true;
170
#else
171
    return sendMessageOpenSSH(in, out);
172
#endif
173
}
174

175
bool SSHAgent::sendMessageOpenSSH(const QByteArray& in, QByteArray& out)
176
{
177
    QLocalSocket socket;
178
    BinaryStream stream(&socket);
179

180
    socket.connectToServer(socketPath());
181
    if (!socket.waitForConnected(500)) {
182
        m_error = tr("Agent connection failed.");
183
        return false;
184
    }
185

186
    stream.writeString(in);
187
    stream.flush();
188

189
    if (!stream.readString(out)) {
190
        m_error = tr("Agent protocol error.");
191
        return false;
192
    }
193

194
    socket.close();
195
    return true;
196
}
197

198
#ifdef Q_OS_WIN
199
bool SSHAgent::sendMessagePageant(const QByteArray& in, QByteArray& out)
200
{
201
    HWND hWnd = FindWindowA("Pageant", "Pageant");
202

203
    if (!hWnd) {
204
        m_error = tr("Agent connection failed.");
205
        return false;
206
    }
207

208
    if (static_cast<quint32>(in.length()) > AGENT_MAX_MSGLEN - 4) {
209
        m_error = tr("Agent connection failed.");
210
        return false;
211
    }
212

213
    auto threadId = reinterpret_cast<qlonglong>(QThread::currentThreadId());
214
    QByteArray mapName = (QString("SSHAgentRequest%1").arg(threadId, 8, 16, QChar('0'))).toLatin1();
215

216
    HANDLE handle = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapName.data());
217

218
    if (!handle) {
219
        m_error = tr("Agent connection failed.");
220
        return false;
221
    }
222

223
    LPVOID ptr = MapViewOfFile(handle, FILE_MAP_WRITE, 0, 0, 0);
224

225
    if (!ptr) {
226
        CloseHandle(handle);
227
        m_error = tr("Agent connection failed.");
228
        return false;
229
    }
230

231
    quint32* requestLength = reinterpret_cast<quint32*>(ptr);
232
    void* requestData = reinterpret_cast<void*>(reinterpret_cast<char*>(ptr) + 4);
233

234
    *requestLength = qToBigEndian<quint32>(in.length());
235
    memcpy(requestData, in.data(), in.length());
236

237
    COPYDATASTRUCT data;
238
    data.dwData = AGENT_COPYDATA_ID;
239
    data.cbData = mapName.length() + 1;
240
    data.lpData = reinterpret_cast<LPVOID>(mapName.data());
241

242
    LRESULT res = SendMessageA(hWnd, WM_COPYDATA, 0, reinterpret_cast<LPARAM>(&data));
243

244
    if (res) {
245
        quint32 responseLength = qFromBigEndian<quint32>(*requestLength);
246
        if (responseLength <= AGENT_MAX_MSGLEN) {
247
            out.resize(responseLength);
248
            memcpy(out.data(), requestData, responseLength);
249
        } else {
250
            m_error = tr("Agent protocol error.");
251
        }
252
    } else {
253
        m_error = tr("Agent protocol error.");
254
    }
255

256
    UnmapViewOfFile(ptr);
257
    CloseHandle(handle);
258

259
    return (res > 0);
260
}
261
#endif
262

263
/**
264
 * Add the identity to the SSH agent.
265
 *
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
270
 */
271
bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, const QUuid& databaseUuid)
272
{
273
    if (!isAgentRunning()) {
274
        m_error = tr("No agent running, cannot add identity.");
275
        return false;
276
    }
277

278
    if (m_addedKeys.contains(key) && m_addedKeys[key].first != databaseUuid) {
279
        m_error = tr("Key identity ownership conflict. Refusing to add.");
280
        return false;
281
    }
282

283
    QByteArray requestData;
284
    BinaryStream request(&requestData);
285
    bool isSecurityKey = key.type().startsWith("sk-");
286

287
    request.write(
288
        (settings.useLifetimeConstraintWhenAdding() || settings.useConfirmConstraintWhenAdding() || isSecurityKey)
289
            ? SSH_AGENTC_ADD_ID_CONSTRAINED
290
            : SSH_AGENTC_ADD_IDENTITY);
291
    key.writePrivate(request);
292

293
    if (settings.useLifetimeConstraintWhenAdding()) {
294
        request.write(SSH_AGENT_CONSTRAIN_LIFETIME);
295
        request.write(static_cast<quint32>(settings.lifetimeConstraintDuration()));
296
    }
297

298
    if (settings.useConfirmConstraintWhenAdding()) {
299
        request.write(SSH_AGENT_CONSTRAIN_CONFIRM);
300
    }
301

302
    if (isSecurityKey) {
303
        request.write(SSH_AGENT_CONSTRAIN_EXTENSION);
304
        request.writeString(QString("sk-provider@openssh.com"));
305
        request.writeString(securityKeyProvider());
306
    }
307

308
    QByteArray responseData;
309
    if (!sendMessage(requestData, responseData)) {
310
        return false;
311
    }
312

313
    if (responseData.length() < 1 || static_cast<quint8>(responseData[0]) != SSH_AGENT_SUCCESS) {
314
        m_error =
315
            tr("Agent refused this identity. Possible reasons include:") + "\n" + tr("The key has already been added.");
316

317
        if (settings.useLifetimeConstraintWhenAdding()) {
318
            m_error += "\n" + tr("Restricted lifetime is not supported by the agent (check options).");
319
        }
320

321
        if (settings.useConfirmConstraintWhenAdding()) {
322
            m_error += "\n" + tr("A confirmation request is not supported by the agent (check options).");
323
        }
324

325
        if (isSecurityKey) {
326
            m_error +=
327
                "\n" + tr("Security keys are not supported by the agent or the security key provider is unavailable.");
328
        }
329

330
        return false;
331
    }
332

333
    OpenSSHKey keyCopy = key;
334
    keyCopy.clearPrivate();
335
    m_addedKeys[keyCopy] = qMakePair(databaseUuid, settings.removeAtDatabaseClose());
336
    return true;
337
}
338

339
/**
340
 * Remove an identity from the SSH agent.
341
 *
342
 * @param key identity to remove
343
 * @return true on success
344
 */
345
bool SSHAgent::removeIdentity(OpenSSHKey& key)
346
{
347
    if (!isAgentRunning()) {
348
        m_error = tr("No agent running, cannot remove identity.");
349
        return false;
350
    }
351

352
    QByteArray requestData;
353
    BinaryStream request(&requestData);
354

355
    QByteArray keyData;
356
    BinaryStream keyStream(&keyData);
357
    key.writePublic(keyStream);
358

359
    request.write(SSH_AGENTC_REMOVE_IDENTITY);
360
    request.writeString(keyData);
361

362
    QByteArray responseData;
363
    return sendMessage(requestData, responseData);
364
}
365

366
/**
367
 * Get a list of identities from the SSH agent.
368
 *
369
 * @param list list of keys to append
370
 * @return true on success
371
 */
372
bool SSHAgent::listIdentities(QList<QSharedPointer<OpenSSHKey>>& list)
373
{
374
    if (!isAgentRunning()) {
375
        m_error = tr("No agent running, cannot list identities.");
376
        return false;
377
    }
378

379
    QByteArray requestData;
380
    BinaryStream request(&requestData);
381

382
    request.write(SSH_AGENTC_REQUEST_IDENTITIES);
383

384
    QByteArray responseData;
385
    if (!sendMessage(requestData, responseData)) {
386
        return false;
387
    }
388

389
    BinaryStream response(&responseData);
390

391
    quint8 responseType;
392
    if (!response.read(responseType) || responseType != SSH_AGENT_IDENTITIES_ANSWER) {
393
        m_error = tr("Agent protocol error.");
394
        return false;
395
    }
396

397
    quint32 nKeys;
398
    if (!response.read(nKeys)) {
399
        m_error = tr("Agent protocol error.");
400
        return false;
401
    }
402

403
    for (quint32 i = 0; i < nKeys; i++) {
404
        QByteArray publicData;
405
        QString comment;
406

407
        if (!response.readString(publicData)) {
408
            m_error = tr("Agent protocol error.");
409
            return false;
410
        }
411

412
        if (!response.readString(comment)) {
413
            m_error = tr("Agent protocol error.");
414
            return false;
415
        }
416

417
        OpenSSHKey* key = new OpenSSHKey();
418
        key->setComment(comment);
419

420
        list.append(QSharedPointer<OpenSSHKey>(key));
421

422
        BinaryStream publicDataStream(&publicData);
423
        if (!key->readPublic(publicDataStream)) {
424
            m_error = key->errorString();
425
            return false;
426
        }
427
    }
428

429
    return true;
430
}
431

432
/**
433
 * Check if this identity is loaded in the SSH Agent.
434
 *
435
 * @param key identity to remove
436
 * @param loaded is the key loaded
437
 * @return true on success
438
 */
439
bool SSHAgent::checkIdentity(const OpenSSHKey& key, bool& loaded)
440
{
441
    QList<QSharedPointer<OpenSSHKey>> list;
442

443
    if (!listIdentities(list)) {
444
        return false;
445
    }
446

447
    loaded = false;
448

449
    for (const auto& it : list) {
450
        if (*it == key) {
451
            loaded = true;
452
            break;
453
        }
454
    }
455

456
    return true;
457
}
458

459
/**
460
 * Remove all identities known to this instance
461
 */
462
void SSHAgent::removeAllIdentities()
463
{
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();
469
            removeIdentity(key);
470
        }
471
        it = m_addedKeys.erase(it);
472
    }
473
}
474

475
/**
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.
478
 *
479
 * @param key key to change setting for
480
 * @param autoRemove whether to remove the key from the agent when database is locked
481
 */
482
void SSHAgent::setAutoRemoveOnLock(const OpenSSHKey& key, bool autoRemove)
483
{
484
    if (m_addedKeys.contains(key)) {
485
        m_addedKeys[key].second = autoRemove;
486
    }
487
}
488

489
void SSHAgent::databaseLocked(const QSharedPointer<Database>& db)
490
{
491
    if (!db) {
492
        return;
493
    }
494

495
    auto it = m_addedKeys.begin();
496
    while (it != m_addedKeys.end()) {
497
        if (it.value().first != db->uuid()) {
498
            ++it;
499
            continue;
500
        }
501
        OpenSSHKey key = it.key();
502
        if (it.value().second) {
503
            if (!removeIdentity(key)) {
504
                emit error(m_error);
505
            }
506
        }
507
        it = m_addedKeys.erase(it);
508
    }
509
}
510

511
void SSHAgent::databaseUnlocked(const QSharedPointer<Database>& db)
512
{
513
    if (!db || !isEnabled()) {
514
        return;
515
    }
516

517
    for (auto entry : db->rootGroup()->entriesRecursive()) {
518
        if (entry->isRecycled()) {
519
            continue;
520
        }
521

522
        KeeAgentSettings settings;
523

524
        if (!settings.fromEntry(entry)) {
525
            continue;
526
        }
527

528
        if (!settings.allowUseOfSshKey() || !settings.addAtDatabaseOpen()) {
529
            continue;
530
        }
531

532
        OpenSSHKey key;
533

534
        if (!settings.toOpenSSHKey(entry, key, true)) {
535
            continue;
536
        }
537

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) {
541
            emit error(m_error);
542
        }
543
    }
544
}
545

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.