/* * Copyright (C) 2017 Toni Spets * Copyright (C) 2017 KeePassXC Team * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 2 or (at your option) * version 3 of the License. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "SSHAgent.h" #include "BinaryStream.h" #include "KeeAgentSettings.h" #ifndef Q_OS_WIN #include #else #include #endif SSHAgent* SSHAgent::m_instance; SSHAgent::SSHAgent(QObject* parent) : QObject(parent) { #ifndef Q_OS_WIN m_socketPath = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK"); #endif } SSHAgent::~SSHAgent() { for (QSet keys : m_keys.values()) { for (OpenSSHKey key : keys) { removeIdentity(key); } } } SSHAgent* SSHAgent::instance() { if (m_instance == nullptr) { qFatal("Race condition: instance wanted before it was initialized, this is a bug."); } return m_instance; } void SSHAgent::init(QObject* parent) { m_instance = new SSHAgent(parent); } const QString SSHAgent::errorString() const { return m_error; } bool SSHAgent::isAgentRunning() const { #ifndef Q_OS_WIN return !m_socketPath.isEmpty(); #else return (FindWindowA("Pageant", "Pageant") != nullptr); #endif } bool SSHAgent::sendMessage(const QByteArray& in, QByteArray& out) { #ifndef Q_OS_WIN QLocalSocket socket; BinaryStream stream(&socket); socket.connectToServer(m_socketPath); if (!socket.waitForConnected(500)) { m_error = tr("Agent connection failed."); return false; } stream.writeString(in); stream.flush(); if (!stream.readString(out)) { m_error = tr("Agent protocol error."); return false; } socket.close(); return true; #else HWND hWnd = FindWindowA("Pageant", "Pageant"); if (!hWnd) { m_error = tr("Agent connection failed."); return false; } if (static_cast(in.length()) > AGENT_MAX_MSGLEN - 4) { m_error = tr("Agent connection failed."); return false; } QByteArray mapName = (QString("SSHAgentRequest") + reinterpret_cast(QThread::currentThreadId())).toLatin1(); HANDLE handle = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapName.data()); if (!handle) { m_error = tr("Agent connection failed."); return false; } LPVOID ptr = MapViewOfFile(handle, FILE_MAP_WRITE, 0, 0, 0); if (!ptr) { CloseHandle(handle); m_error = tr("Agent connection failed."); return false; } quint32* requestLength = reinterpret_cast(ptr); void* requestData = reinterpret_cast(reinterpret_cast(ptr) + 4); *requestLength = qToBigEndian(in.length()); memcpy(requestData, in.data(), in.length()); COPYDATASTRUCT data; data.dwData = AGENT_COPYDATA_ID; data.cbData = mapName.length() + 1; data.lpData = reinterpret_cast(mapName.data()); LRESULT res = SendMessageA(hWnd, WM_COPYDATA, 0, reinterpret_cast(&data)); if (res) { quint32 responseLength = qFromBigEndian(*requestLength); if (responseLength <= AGENT_MAX_MSGLEN) { out.resize(responseLength); memcpy(out.data(), requestData, responseLength); } else { m_error = tr("Agent protocol error."); } } else { m_error = tr("Agent protocol error."); } UnmapViewOfFile(ptr); CloseHandle(handle); return (res > 0); #endif } bool SSHAgent::addIdentity(OpenSSHKey& key, quint32 lifetime, bool confirm) { if (!isAgentRunning()) { m_error = tr("No agent running, cannot add identity."); return false; } QByteArray requestData; BinaryStream request(&requestData); request.write((lifetime > 0 || confirm) ? SSH_AGENTC_ADD_ID_CONSTRAINED : SSH_AGENTC_ADD_IDENTITY); key.writePrivate(request); if (lifetime > 0) { request.write(SSH_AGENT_CONSTRAIN_LIFETIME); request.write(lifetime); } if (confirm) { request.write(SSH_AGENT_CONSTRAIN_CONFIRM); } QByteArray responseData; if (!sendMessage(requestData, responseData)) { return false; } if (responseData.length() < 1 || static_cast(responseData[0]) != SSH_AGENT_SUCCESS) { m_error = tr("Agent refused this identity. Possible reasons include:") + "\n" + tr("The key has already been added."); if (lifetime > 0) { m_error += "\n" + tr("Restricted lifetime is not supported by the agent (check options)."); } if (confirm) { m_error += "\n" + tr("A confirmation request is not supported by the agent (check options)."); } return false; } return true; } bool SSHAgent::removeIdentity(OpenSSHKey& key) { if (!isAgentRunning()) { m_error = tr("No agent running, cannot remove identity."); return false; } QByteArray requestData; BinaryStream request(&requestData); QByteArray keyData; BinaryStream keyStream(&keyData); key.writePublic(keyStream); request.write(SSH_AGENTC_REMOVE_IDENTITY); request.writeString(keyData); QByteArray responseData; if (!sendMessage(requestData, responseData)) { return false; } if (responseData.length() < 1 || static_cast(responseData[0]) != SSH_AGENT_SUCCESS) { m_error = tr("Agent does not have this identity."); return false; } return true; } void SSHAgent::removeIdentityAtLock(const OpenSSHKey& key, const Uuid& uuid) { OpenSSHKey copy = key; copy.clearPrivate(); m_keys[uuid.toHex()].insert(copy); } void SSHAgent::databaseModeChanged(DatabaseWidget::Mode mode) { DatabaseWidget* widget = qobject_cast(sender()); if (widget == nullptr) { return; } Uuid uuid = widget->database()->uuid(); if (mode == DatabaseWidget::LockedMode && m_keys.contains(uuid.toHex())) { QSet keys = m_keys.take(uuid.toHex()); for (OpenSSHKey key : keys) { if (!removeIdentity(key)) { emit error(m_error); } } } else if (mode == DatabaseWidget::ViewMode && !m_keys.contains(uuid.toHex())) { for (Entry* e : widget->database()->rootGroup()->entriesRecursive()) { if (widget->database()->metadata()->recycleBinEnabled() && e->group() == widget->database()->metadata()->recycleBin()) { continue; } if (!e->attachments()->hasKey("KeeAgent.settings")) { continue; } KeeAgentSettings settings; settings.fromXml(e->attachments()->value("KeeAgent.settings")); if (!settings.allowUseOfSshKey()) { continue; } QByteArray keyData; QString fileName; if (settings.selectedType() == "attachment") { fileName = settings.attachmentName(); keyData = e->attachments()->value(fileName); } else if (!settings.fileName().isEmpty()) { QFile file(settings.fileName()); QFileInfo fileInfo(file); fileName = fileInfo.fileName(); if (file.size() > 1024 * 1024) { continue; } if (!file.open(QIODevice::ReadOnly)) { continue; } keyData = file.readAll(); } if (keyData.isEmpty()) { continue; } OpenSSHKey key; if (!key.parse(keyData)) { continue; } if (!key.openPrivateKey(e->password())) { continue; } if (key.comment().isEmpty()) { key.setComment(e->username()); } if (key.comment().isEmpty()) { key.setComment(fileName); } if (settings.removeAtDatabaseClose()) { removeIdentityAtLock(key, uuid); } if (settings.addAtDatabaseOpen()) { int lifetime = 0; if (settings.useLifetimeConstraintWhenAdding()) { lifetime = settings.lifetimeConstraintDuration(); } if (!addIdentity(key, lifetime, settings.useConfirmConstraintWhenAdding())) { emit error(m_error); } } } } }