keepassxc/src/sshagent/SSHAgent.cpp
Steven Noonan dc1aead2a2 OpenSSHKey: when writing to agent, ensure comment string is at least one byte
This unbreaks adding keys to gpg-agent.

Signed-off-by: Steven Noonan <steven@uplinklabs.net>
2018-03-10 23:59:37 -05:00

331 lines
8.6 KiB
C++

/*
* 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"
#include "BinaryStream.h"
#include "KeeAgentSettings.h"
#ifndef Q_OS_WIN
#include <QtNetwork>
#else
#include <windows.h>
#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<OpenSSHKey> 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<quint32>(in.length()) > AGENT_MAX_MSGLEN - 4) {
m_error = tr("Agent connection failed.");
return false;
}
QByteArray mapName = (QString("SSHAgentRequest") + reinterpret_cast<intptr_t>(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<quint32*>(ptr);
void *requestData = reinterpret_cast<void*>(reinterpret_cast<char*>(ptr) + 4);
*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);
} 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<quint8>(responseData[0]) != SSH_AGENT_SUCCESS) {
m_error = tr("Agent refused this identity.");
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<quint8>(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<DatabaseWidget*>(sender());
if (widget == nullptr) {
return;
}
Uuid uuid = widget->database()->uuid();
if (mode == DatabaseWidget::LockedMode && m_keys.contains(uuid.toHex())) {
QSet<OpenSSHKey> 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);
}
}
}
}
}