2017-10-29 15:17:24 +00:00
|
|
|
/*
|
|
|
|
|
* Copyright (C) 2017 Toni Spets <toni.spets@iki.fi>
|
|
|
|
|
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
|
|
|
|
*
|
|
|
|
|
* 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 <http://www.gnu.org/licenses/>.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
#include "SSHAgent.h"
|
2018-10-01 14:26:24 +00:00
|
|
|
|
2019-01-20 14:50:20 +00:00
|
|
|
#include "core/Config.h"
|
2018-10-01 14:26:24 +00:00
|
|
|
#include "crypto/ssh/BinaryStream.h"
|
2019-01-20 14:50:20 +00:00
|
|
|
#include "crypto/ssh/OpenSSHKey.h"
|
2018-10-01 14:26:24 +00:00
|
|
|
#include "sshagent/KeeAgentSettings.h"
|
2017-10-29 15:17:24 +00:00
|
|
|
|
|
|
|
|
#include <QtNetwork>
|
2018-12-25 19:28:02 +00:00
|
|
|
|
|
|
|
|
#ifdef Q_OS_WIN
|
2017-10-29 15:17:24 +00:00
|
|
|
#include <windows.h>
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
SSHAgent* SSHAgent::m_instance;
|
|
|
|
|
|
2018-03-31 20:01:30 +00:00
|
|
|
SSHAgent::SSHAgent(QObject* parent)
|
|
|
|
|
: QObject(parent)
|
2017-10-29 15:17:24 +00:00
|
|
|
{
|
|
|
|
|
#ifndef Q_OS_WIN
|
|
|
|
|
m_socketPath = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK");
|
2018-12-25 19:28:02 +00:00
|
|
|
#else
|
|
|
|
|
m_socketPath = "\\\\.\\pipe\\openssh-ssh-agent";
|
2017-10-29 15:17:24 +00:00
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SSHAgent::~SSHAgent()
|
|
|
|
|
{
|
2018-11-23 12:49:55 +00:00
|
|
|
auto it = m_addedKeys.begin();
|
|
|
|
|
while (it != m_addedKeys.end()) {
|
2019-04-12 17:55:28 +00:00
|
|
|
// Remove key if requested to remove on lock
|
|
|
|
|
if (it.value()) {
|
|
|
|
|
OpenSSHKey key = it.key();
|
|
|
|
|
removeIdentity(key);
|
|
|
|
|
}
|
2018-11-23 12:49:55 +00:00
|
|
|
it = m_addedKeys.erase(it);
|
2017-10-29 15:17:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
SSHAgent* SSHAgent::instance()
|
|
|
|
|
{
|
2018-11-23 12:49:55 +00:00
|
|
|
if (!m_instance) {
|
2017-10-29 15:17:24 +00:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-03 15:49:00 +00:00
|
|
|
const QString SSHAgent::errorString() const
|
|
|
|
|
{
|
|
|
|
|
return m_error;
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-29 15:17:24 +00:00
|
|
|
bool SSHAgent::isAgentRunning() const
|
|
|
|
|
{
|
|
|
|
|
#ifndef Q_OS_WIN
|
|
|
|
|
return !m_socketPath.isEmpty();
|
|
|
|
|
#else
|
2018-12-25 19:28:02 +00:00
|
|
|
if (!config()->get("SSHAgentOpenSSH").toBool()) {
|
|
|
|
|
return (FindWindowA("Pageant", "Pageant") != nullptr);
|
|
|
|
|
} else {
|
|
|
|
|
return WaitNamedPipe(m_socketPath.toLatin1().data(), 100);
|
|
|
|
|
}
|
2017-10-29 15:17:24 +00:00
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-03 15:49:00 +00:00
|
|
|
bool SSHAgent::sendMessage(const QByteArray& in, QByteArray& out)
|
2017-10-29 15:17:24 +00:00
|
|
|
{
|
2018-12-25 19:28:02 +00:00
|
|
|
#ifdef Q_OS_WIN
|
|
|
|
|
if (!config()->get("SSHAgentOpenSSH").toBool()) {
|
|
|
|
|
return sendMessagePageant(in, out);
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2017-10-29 15:17:24 +00:00
|
|
|
QLocalSocket socket;
|
|
|
|
|
BinaryStream stream(&socket);
|
|
|
|
|
|
|
|
|
|
socket.connectToServer(m_socketPath);
|
|
|
|
|
if (!socket.waitForConnected(500)) {
|
2018-03-03 15:49:00 +00:00
|
|
|
m_error = tr("Agent connection failed.");
|
2017-10-29 15:17:24 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
stream.writeString(in);
|
|
|
|
|
stream.flush();
|
|
|
|
|
|
|
|
|
|
if (!stream.readString(out)) {
|
2018-03-03 15:49:00 +00:00
|
|
|
m_error = tr("Agent protocol error.");
|
2017-10-29 15:17:24 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
socket.close();
|
|
|
|
|
|
|
|
|
|
return true;
|
2018-12-25 19:28:02 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#ifdef Q_OS_WIN
|
|
|
|
|
bool SSHAgent::sendMessagePageant(const QByteArray& in, QByteArray& out)
|
|
|
|
|
{
|
2017-10-29 15:17:24 +00:00
|
|
|
HWND hWnd = FindWindowA("Pageant", "Pageant");
|
|
|
|
|
|
|
|
|
|
if (!hWnd) {
|
2018-03-03 15:49:00 +00:00
|
|
|
m_error = tr("Agent connection failed.");
|
2017-10-29 15:17:24 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-27 19:39:44 +00:00
|
|
|
if (static_cast<quint32>(in.length()) > AGENT_MAX_MSGLEN - 4) {
|
2018-03-03 15:49:00 +00:00
|
|
|
m_error = tr("Agent connection failed.");
|
2017-10-29 15:17:24 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-31 20:01:30 +00:00
|
|
|
QByteArray mapName =
|
|
|
|
|
(QString("SSHAgentRequest") + reinterpret_cast<intptr_t>(QThread::currentThreadId())).toLatin1();
|
2017-10-29 15:17:24 +00:00
|
|
|
|
|
|
|
|
HANDLE handle = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, AGENT_MAX_MSGLEN, mapName.data());
|
|
|
|
|
|
|
|
|
|
if (!handle) {
|
2018-03-03 15:49:00 +00:00
|
|
|
m_error = tr("Agent connection failed.");
|
2017-10-29 15:17:24 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
LPVOID ptr = MapViewOfFile(handle, FILE_MAP_WRITE, 0, 0, 0);
|
|
|
|
|
|
|
|
|
|
if (!ptr) {
|
|
|
|
|
CloseHandle(handle);
|
2018-03-03 15:49:00 +00:00
|
|
|
m_error = tr("Agent connection failed.");
|
2017-10-29 15:17:24 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-31 20:01:30 +00:00
|
|
|
quint32* requestLength = reinterpret_cast<quint32*>(ptr);
|
|
|
|
|
void* requestData = reinterpret_cast<void*>(reinterpret_cast<char*>(ptr) + 4);
|
2017-10-29 15:17:24 +00:00
|
|
|
|
|
|
|
|
*requestLength = qToBigEndian<quint32>(in.length());
|
|
|
|
|
memcpy(requestData, in.data(), in.length());
|
|
|
|
|
|
|
|
|
|
COPYDATASTRUCT data;
|
|
|
|
|
data.dwData = AGENT_COPYDATA_ID;
|
|
|
|
|
data.cbData = mapName.length() + 1;
|
|
|
|
|
data.lpData = reinterpret_cast<LPVOID>(mapName.data());
|
|
|
|
|
|
|
|
|
|
LRESULT res = SendMessageA(hWnd, WM_COPYDATA, 0, reinterpret_cast<LPARAM>(&data));
|
|
|
|
|
|
|
|
|
|
if (res) {
|
|
|
|
|
quint32 responseLength = qFromBigEndian<quint32>(*requestLength);
|
|
|
|
|
if (responseLength <= AGENT_MAX_MSGLEN) {
|
|
|
|
|
out.resize(responseLength);
|
|
|
|
|
memcpy(out.data(), requestData, responseLength);
|
2018-03-03 15:49:00 +00:00
|
|
|
} else {
|
|
|
|
|
m_error = tr("Agent protocol error.");
|
2017-10-29 15:17:24 +00:00
|
|
|
}
|
2018-03-03 15:49:00 +00:00
|
|
|
} else {
|
|
|
|
|
m_error = tr("Agent protocol error.");
|
2017-10-29 15:17:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
UnmapViewOfFile(ptr);
|
|
|
|
|
CloseHandle(handle);
|
|
|
|
|
|
|
|
|
|
return (res > 0);
|
|
|
|
|
}
|
2018-12-25 19:28:02 +00:00
|
|
|
#endif
|
2017-10-29 15:17:24 +00:00
|
|
|
|
2018-11-23 12:49:55 +00:00
|
|
|
/**
|
|
|
|
|
* Add the identity to the SSH agent.
|
|
|
|
|
*
|
|
|
|
|
* @param key identity / key to add
|
|
|
|
|
* @param lifetime time after which the key should expire
|
|
|
|
|
* @param confirm ask for confirmation before adding the key
|
|
|
|
|
* @param removeOnLock autoremove from agent when the Database is locked
|
|
|
|
|
* @return true on success
|
|
|
|
|
*/
|
2019-04-11 14:17:18 +00:00
|
|
|
bool SSHAgent::addIdentity(OpenSSHKey& key, KeeAgentSettings& settings)
|
2017-10-29 15:17:24 +00:00
|
|
|
{
|
2018-03-03 15:49:00 +00:00
|
|
|
if (!isAgentRunning()) {
|
|
|
|
|
m_error = tr("No agent running, cannot add identity.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-29 15:17:24 +00:00
|
|
|
QByteArray requestData;
|
|
|
|
|
BinaryStream request(&requestData);
|
|
|
|
|
|
2019-04-11 14:17:18 +00:00
|
|
|
request.write((settings.useLifetimeConstraintWhenAdding() || settings.useConfirmConstraintWhenAdding())
|
|
|
|
|
? SSH_AGENTC_ADD_ID_CONSTRAINED
|
|
|
|
|
: SSH_AGENTC_ADD_IDENTITY);
|
2017-10-29 15:17:24 +00:00
|
|
|
key.writePrivate(request);
|
|
|
|
|
|
2019-04-11 14:17:18 +00:00
|
|
|
if (settings.useLifetimeConstraintWhenAdding()) {
|
2017-10-29 15:17:24 +00:00
|
|
|
request.write(SSH_AGENT_CONSTRAIN_LIFETIME);
|
2019-04-11 14:17:18 +00:00
|
|
|
request.write(static_cast<quint32>(settings.lifetimeConstraintDuration()));
|
2017-10-29 15:17:24 +00:00
|
|
|
}
|
|
|
|
|
|
2019-04-11 14:17:18 +00:00
|
|
|
if (settings.useConfirmConstraintWhenAdding()) {
|
2017-10-29 15:17:24 +00:00
|
|
|
request.write(SSH_AGENT_CONSTRAIN_CONFIRM);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QByteArray responseData;
|
2018-03-03 15:49:00 +00:00
|
|
|
if (!sendMessage(requestData, responseData)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2017-10-29 15:17:24 +00:00
|
|
|
|
|
|
|
|
if (responseData.length() < 1 || static_cast<quint8>(responseData[0]) != SSH_AGENT_SUCCESS) {
|
2018-03-31 20:01:30 +00:00
|
|
|
m_error =
|
|
|
|
|
tr("Agent refused this identity. Possible reasons include:") + "\n" + tr("The key has already been added.");
|
2018-03-07 22:39:38 +00:00
|
|
|
|
2019-04-11 14:17:18 +00:00
|
|
|
if (settings.useLifetimeConstraintWhenAdding()) {
|
2018-03-07 22:39:38 +00:00
|
|
|
m_error += "\n" + tr("Restricted lifetime is not supported by the agent (check options).");
|
|
|
|
|
}
|
|
|
|
|
|
2019-04-11 14:17:18 +00:00
|
|
|
if (settings.useConfirmConstraintWhenAdding()) {
|
2018-03-07 22:39:38 +00:00
|
|
|
m_error += "\n" + tr("A confirmation request is not supported by the agent (check options).");
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-29 15:17:24 +00:00
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-23 12:49:55 +00:00
|
|
|
OpenSSHKey keyCopy = key;
|
|
|
|
|
keyCopy.clearPrivate();
|
2019-04-11 14:17:18 +00:00
|
|
|
m_addedKeys[keyCopy] = settings.removeAtDatabaseClose();
|
2017-10-29 15:17:24 +00:00
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-23 12:49:55 +00:00
|
|
|
/**
|
|
|
|
|
* Remove an identity from the SSH agent.
|
|
|
|
|
*
|
|
|
|
|
* @param key identity to remove
|
|
|
|
|
* @return true on success
|
|
|
|
|
*/
|
2018-03-03 15:49:00 +00:00
|
|
|
bool SSHAgent::removeIdentity(OpenSSHKey& key)
|
2017-10-29 15:17:24 +00:00
|
|
|
{
|
2018-03-03 15:49:00 +00:00
|
|
|
if (!isAgentRunning()) {
|
|
|
|
|
m_error = tr("No agent running, cannot remove identity.");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-29 15:17:24 +00:00
|
|
|
QByteArray requestData;
|
|
|
|
|
BinaryStream request(&requestData);
|
|
|
|
|
|
|
|
|
|
QByteArray keyData;
|
|
|
|
|
BinaryStream keyStream(&keyData);
|
|
|
|
|
key.writePublic(keyStream);
|
|
|
|
|
|
|
|
|
|
request.write(SSH_AGENTC_REMOVE_IDENTITY);
|
|
|
|
|
request.writeString(keyData);
|
|
|
|
|
|
|
|
|
|
QByteArray responseData;
|
2018-11-23 12:49:55 +00:00
|
|
|
return sendMessage(requestData, responseData);
|
2017-10-29 15:17:24 +00:00
|
|
|
}
|
|
|
|
|
|
2018-11-23 12:49:55 +00:00
|
|
|
/**
|
|
|
|
|
* Change "remove identity on lock" setting for a key already added to the agent.
|
|
|
|
|
* Will to nothing if the key has not been added to the agent.
|
|
|
|
|
*
|
|
|
|
|
* @param key key to change setting for
|
|
|
|
|
* @param autoRemove whether to remove the key from the agent when database is locked
|
|
|
|
|
*/
|
|
|
|
|
void SSHAgent::setAutoRemoveOnLock(const OpenSSHKey& key, bool autoRemove)
|
2017-10-29 15:17:24 +00:00
|
|
|
{
|
2018-11-23 12:49:55 +00:00
|
|
|
if (m_addedKeys.contains(key)) {
|
|
|
|
|
m_addedKeys[key] = autoRemove;
|
|
|
|
|
}
|
2017-10-29 15:17:24 +00:00
|
|
|
}
|
|
|
|
|
|
2018-11-23 12:49:55 +00:00
|
|
|
void SSHAgent::databaseModeChanged()
|
2017-10-29 15:17:24 +00:00
|
|
|
{
|
2018-11-23 12:49:55 +00:00
|
|
|
auto* widget = qobject_cast<DatabaseWidget*>(sender());
|
|
|
|
|
if (!widget) {
|
2017-10-29 15:17:24 +00:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-23 12:49:55 +00:00
|
|
|
if (widget->isLocked()) {
|
|
|
|
|
auto it = m_addedKeys.begin();
|
|
|
|
|
while (it != m_addedKeys.end()) {
|
|
|
|
|
OpenSSHKey key = it.key();
|
|
|
|
|
if (it.value()) {
|
|
|
|
|
if (!removeIdentity(key)) {
|
|
|
|
|
emit error(m_error);
|
|
|
|
|
}
|
|
|
|
|
it = m_addedKeys.erase(it);
|
|
|
|
|
} else {
|
|
|
|
|
// don't remove it yet
|
|
|
|
|
m_addedKeys[key] = false;
|
|
|
|
|
++it;
|
2018-03-03 15:49:00 +00:00
|
|
|
}
|
2017-10-29 15:17:24 +00:00
|
|
|
}
|
|
|
|
|
|
2018-11-23 12:49:55 +00:00
|
|
|
return;
|
|
|
|
|
}
|
2018-01-20 09:19:09 +00:00
|
|
|
|
2018-11-23 12:49:55 +00:00
|
|
|
for (Entry* e : widget->database()->rootGroup()->entriesRecursive()) {
|
|
|
|
|
if (widget->database()->metadata()->recycleBinEnabled()
|
|
|
|
|
&& e->group() == widget->database()->metadata()->recycleBin()) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
2017-10-29 15:17:24 +00:00
|
|
|
|
2018-11-23 12:49:55 +00:00
|
|
|
KeeAgentSettings settings;
|
2018-03-08 04:43:26 +00:00
|
|
|
|
2019-11-09 18:45:08 +00:00
|
|
|
if (!settings.fromEntry(e)) {
|
2018-11-23 12:49:55 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
2017-10-29 15:17:24 +00:00
|
|
|
|
2019-11-09 18:45:08 +00:00
|
|
|
if (!settings.allowUseOfSshKey() || !settings.addAtDatabaseOpen()) {
|
2018-11-23 12:49:55 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
2017-10-29 15:17:24 +00:00
|
|
|
|
2018-11-23 12:49:55 +00:00
|
|
|
OpenSSHKey key;
|
2018-03-01 16:27:53 +00:00
|
|
|
|
2019-11-09 18:45:08 +00:00
|
|
|
if (!settings.toOpenSSHKey(e, key, true)) {
|
2018-11-23 12:49:55 +00:00
|
|
|
continue;
|
|
|
|
|
}
|
2018-03-08 04:43:26 +00:00
|
|
|
|
2019-11-09 18:45:08 +00:00
|
|
|
// Add key to agent; ignore errors if we have previously added the key
|
|
|
|
|
bool known_key = m_addedKeys.contains(key);
|
|
|
|
|
if (!addIdentity(key, settings) && !known_key) {
|
|
|
|
|
emit error(m_error);
|
2017-10-29 15:17:24 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|