keepassxc/src/sshagent/SSHAgent.cpp

484 lines
12 KiB
C++
Raw Normal View History

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"
Replace all crypto libraries with Botan Selected the [Botan crypto library](https://github.com/randombit/botan) due to its feature list, maintainer support, availability across all deployment platforms, and ease of use. Also evaluated Crypto++ as a viable candidate, but the additional features of Botan (PKCS#11, TPM, etc) won out. The random number generator received a backend upgrade. Botan prefers hardware-based RNG's and will provide one if available. This is transparent to KeePassXC and a significant improvement over gcrypt. Replaced Argon2 library with built-in Botan implementation that supports i, d, and id. This requires Botan 2.11.0 or higher. Also simplified the parameter test across KDF's. Aligned SymmetricCipher parameters with available modes. All encrypt and decrypt operations are done in-place instead of returning new objects. This allows use of secure vectors in the future with no additional overhead. Took this opportunity to decouple KeeShare from SSH Agent. Removed leftover code from OpenSSHKey and consolidated the SSH Agent code into the same directory. Removed bcrypt and blowfish inserts since they are provided by Botan. Additionally simplified KeeShare settings interface by removing raw certificate byte data from the user interface. KeeShare will be further refactored in a future PR. NOTE: This PR breaks backwards compatibility with KeeShare certificates due to different RSA key storage with Botan. As a result, new "own" certificates will need to be generated and trust re-established. Removed YKChallengeResponseKeyCLI in favor of just using the original implementation with signal/slots. Removed TestRandom stub since it was just faking random numbers and not actually using the backend. TestRandomGenerator now uses the actual RNG. Greatly simplified Secret Service plugin's use of crypto functions with Botan.
2021-04-04 12:56:00 +00:00
#include "BinaryStream.h"
#include "OpenSSHKey.h"
#include "core/Config.h"
2021-01-31 21:47:35 +00:00
#include "core/Database.h"
#include "core/Group.h"
#include "core/Metadata.h"
#include "sshagent/KeeAgentSettings.h"
2017-10-29 15:17:24 +00:00
#include <QtNetwork>
#ifdef Q_OS_WIN
2017-10-29 15:17:24 +00:00
#include <windows.h>
#endif
Q_GLOBAL_STATIC(SSHAgent, s_sshAgent);
2017-10-29 15:17:24 +00:00
SSHAgent* SSHAgent::instance()
2017-10-29 15:17:24 +00:00
{
return s_sshAgent;
2017-10-29 15:17:24 +00:00
}
bool SSHAgent::isEnabled() const
2017-10-29 15:17:24 +00:00
{
return config()->get(Config::SSHAgent_Enabled).toBool();
}
void SSHAgent::setEnabled(bool enabled)
{
if (isEnabled() && !enabled) {
removeAllIdentities();
2017-10-29 15:17:24 +00:00
}
config()->set(Config::SSHAgent_Enabled, enabled);
2018-05-10 13:12:36 +00:00
emit enabledChanged(enabled);
}
QString SSHAgent::authSockOverride() const
{
return config()->get(Config::SSHAgent_AuthSockOverride).toString();
}
void SSHAgent::setAuthSockOverride(QString& authSockOverride)
{
config()->set(Config::SSHAgent_AuthSockOverride, authSockOverride);
}
#ifdef Q_OS_WIN
bool SSHAgent::useOpenSSH() const
{
return config()->get(Config::SSHAgent_UseOpenSSH).toBool();
2017-10-29 15:17:24 +00:00
}
void SSHAgent::setUseOpenSSH(bool useOpenSSH)
2017-10-29 15:17:24 +00:00
{
config()->set(Config::SSHAgent_UseOpenSSH, useOpenSSH);
}
#endif
QString SSHAgent::socketPath(bool allowOverride) const
{
QString socketPath;
#ifndef Q_OS_WIN
if (allowOverride) {
socketPath = authSockOverride();
}
// if the overridden path is empty (no override set), default to environment
if (socketPath.isEmpty()) {
socketPath = QProcessEnvironment::systemEnvironment().value("SSH_AUTH_SOCK");
}
#else
Q_UNUSED(allowOverride)
socketPath = "\\\\.\\pipe\\openssh-ssh-agent";
#endif
return socketPath;
2017-10-29 15:17:24 +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
QFileInfo socketFileInfo(socketPath());
return !socketFileInfo.path().isEmpty() && socketFileInfo.exists();
2017-10-29 15:17:24 +00:00
#else
if (!useOpenSSH()) {
return (FindWindowA("Pageant", "Pageant") != nullptr);
} else {
return WaitNamedPipe(socketPath().toLatin1().data(), 100);
}
2017-10-29 15:17:24 +00:00
#endif
}
bool SSHAgent::sendMessage(const QByteArray& in, QByteArray& out)
2017-10-29 15:17:24 +00:00
{
#ifdef Q_OS_WIN
if (!useOpenSSH()) {
return sendMessagePageant(in, out);
}
#endif
2017-10-29 15:17:24 +00:00
QLocalSocket socket;
BinaryStream stream(&socket);
socket.connectToServer(socketPath());
2017-10-29 15:17:24 +00:00
if (!socket.waitForConnected(500)) {
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)) {
m_error = tr("Agent protocol error.");
2017-10-29 15:17:24 +00:00
return false;
}
socket.close();
return true;
}
#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) {
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) {
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) {
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);
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);
} else {
m_error = tr("Agent protocol error.");
2017-10-29 15:17:24 +00:00
}
} else {
m_error = tr("Agent protocol error.");
2017-10-29 15:17:24 +00:00
}
UnmapViewOfFile(ptr);
CloseHandle(handle);
return (res > 0);
}
#endif
2017-10-29 15:17:24 +00:00
/**
* Add the identity to the SSH agent.
*
* @param key identity / key to add
* @param settings constraints (lifetime, confirm), remove-on-lock
* @param databaseUuid database that owns the key for remove-on-lock
* @return true on success
*/
bool SSHAgent::addIdentity(OpenSSHKey& key, const KeeAgentSettings& settings, const QUuid& databaseUuid)
2017-10-29 15:17:24 +00:00
{
if (!isAgentRunning()) {
m_error = tr("No agent running, cannot add identity.");
return false;
}
if (m_addedKeys.contains(key) && m_addedKeys[key].first != databaseUuid) {
m_error = tr("Key identity ownership conflict. Refusing to add.");
return false;
}
2017-10-29 15:17:24 +00:00
QByteArray requestData;
BinaryStream request(&requestData);
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);
if (settings.useLifetimeConstraintWhenAdding()) {
2017-10-29 15:17:24 +00:00
request.write(SSH_AGENT_CONSTRAIN_LIFETIME);
request.write(static_cast<quint32>(settings.lifetimeConstraintDuration()));
2017-10-29 15:17:24 +00:00
}
if (settings.useConfirmConstraintWhenAdding()) {
2017-10-29 15:17:24 +00:00
request.write(SSH_AGENT_CONSTRAIN_CONFIRM);
}
QByteArray responseData;
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.");
if (settings.useLifetimeConstraintWhenAdding()) {
m_error += "\n" + tr("Restricted lifetime is not supported by the agent (check options).");
}
if (settings.useConfirmConstraintWhenAdding()) {
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;
}
OpenSSHKey keyCopy = key;
keyCopy.clearPrivate();
m_addedKeys[keyCopy] = qMakePair(databaseUuid, settings.removeAtDatabaseClose());
2017-10-29 15:17:24 +00:00
return true;
}
/**
* Remove an identity from the SSH agent.
*
* @param key identity to remove
* @return true on success
*/
bool SSHAgent::removeIdentity(OpenSSHKey& key)
2017-10-29 15:17:24 +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;
return sendMessage(requestData, responseData);
2017-10-29 15:17:24 +00:00
}
/**
* Get a list of identities from the SSH agent.
*
* @param list list of keys to append
* @return true on success
*/
bool SSHAgent::listIdentities(QList<QSharedPointer<OpenSSHKey>>& list)
{
if (!isAgentRunning()) {
m_error = tr("No agent running, cannot list identities.");
return false;
}
QByteArray requestData;
BinaryStream request(&requestData);
request.write(SSH_AGENTC_REQUEST_IDENTITIES);
QByteArray responseData;
if (!sendMessage(requestData, responseData)) {
return false;
}
BinaryStream response(&responseData);
quint8 responseType;
if (!response.read(responseType) || responseType != SSH_AGENT_IDENTITIES_ANSWER) {
m_error = tr("Agent protocol error.");
return false;
}
quint32 nKeys;
if (!response.read(nKeys)) {
m_error = tr("Agent protocol error.");
return false;
}
for (quint32 i = 0; i < nKeys; i++) {
QByteArray publicData;
QString comment;
if (!response.readString(publicData)) {
m_error = tr("Agent protocol error.");
return false;
}
if (!response.readString(comment)) {
m_error = tr("Agent protocol error.");
return false;
}
OpenSSHKey* key = new OpenSSHKey();
key->setComment(comment);
list.append(QSharedPointer<OpenSSHKey>(key));
BinaryStream publicDataStream(&publicData);
if (!key->readPublic(publicDataStream)) {
m_error = key->errorString();
return false;
}
}
return true;
}
/**
* Check if this identity is loaded in the SSH Agent.
*
* @param key identity to remove
* @param loaded is the key laoded
* @return true on success
*/
bool SSHAgent::checkIdentity(const OpenSSHKey& key, bool& loaded)
{
QList<QSharedPointer<OpenSSHKey>> list;
if (!listIdentities(list)) {
return false;
}
loaded = false;
for (const auto& it : list) {
if (*it == key) {
loaded = true;
break;
}
}
return true;
}
/**
* Remove all identities known to this instance
*/
void SSHAgent::removeAllIdentities()
{
auto it = m_addedKeys.begin();
while (it != m_addedKeys.end()) {
// Remove key if requested to remove on lock
if (it.value().second) {
OpenSSHKey key = it.key();
removeIdentity(key);
}
it = m_addedKeys.erase(it);
}
}
/**
* 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
{
if (m_addedKeys.contains(key)) {
m_addedKeys[key].second = autoRemove;
}
2017-10-29 15:17:24 +00:00
}
2021-01-31 21:47:35 +00:00
void SSHAgent::databaseLocked(QSharedPointer<Database> db)
2017-10-29 15:17:24 +00:00
{
2021-01-31 21:47:35 +00:00
if (!db) {
2017-10-29 15:17:24 +00:00
return;
}
auto it = m_addedKeys.begin();
while (it != m_addedKeys.end()) {
2021-01-31 21:47:35 +00:00
if (it.value().first != db->uuid()) {
++it;
continue;
}
OpenSSHKey key = it.key();
if (it.value().second) {
if (!removeIdentity(key)) {
emit error(m_error);
}
2017-10-29 15:17:24 +00:00
}
it = m_addedKeys.erase(it);
}
}
2017-10-29 15:17:24 +00:00
2021-01-31 21:47:35 +00:00
void SSHAgent::databaseUnlocked(QSharedPointer<Database> db)
{
2021-01-31 21:47:35 +00:00
if (!db || !isEnabled()) {
return;
}
2021-01-31 21:47:35 +00:00
for (Entry* e : db->rootGroup()->entriesRecursive()) {
if (db->metadata()->recycleBinEnabled() && e->group() == db->metadata()->recycleBin()) {
continue;
}
2017-10-29 15:17:24 +00:00
KeeAgentSettings settings;
if (!settings.fromEntry(e)) {
continue;
}
2017-10-29 15:17:24 +00:00
if (!settings.allowUseOfSshKey() || !settings.addAtDatabaseOpen()) {
continue;
}
2017-10-29 15:17:24 +00:00
OpenSSHKey key;
if (!settings.toOpenSSHKey(e, key, true)) {
continue;
}
// Add key to agent; ignore errors if we have previously added the key
bool known_key = m_addedKeys.contains(key);
2021-01-31 21:47:35 +00:00
if (!addIdentity(key, settings, db->uuid()) && !known_key) {
emit error(m_error);
2017-10-29 15:17:24 +00:00
}
}
}