Files
keepassxc/src/sshagent/KeeAgentSettings.cpp
Jonathan White 80809ace67 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-05 22:56:03 -04:00

475 lines
14 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 "KeeAgentSettings.h"
#include "core/Tools.h"
KeeAgentSettings::KeeAgentSettings()
{
reset();
}
bool KeeAgentSettings::operator==(const KeeAgentSettings& other) const
{
// clang-format off
return (m_allowUseOfSshKey == other.m_allowUseOfSshKey && m_addAtDatabaseOpen == other.m_addAtDatabaseOpen
&& m_removeAtDatabaseClose == other.m_removeAtDatabaseClose
&& m_useConfirmConstraintWhenAdding == other.m_useConfirmConstraintWhenAdding
&& m_useLifetimeConstraintWhenAdding == other.m_useLifetimeConstraintWhenAdding
&& m_lifetimeConstraintDuration == other.m_lifetimeConstraintDuration
&& m_selectedType == other.m_selectedType
&& m_attachmentName == other.m_attachmentName
&& m_saveAttachmentToTempFile == other.m_saveAttachmentToTempFile
&& m_fileName == other.m_fileName);
// clang-format on
}
bool KeeAgentSettings::operator!=(const KeeAgentSettings& other) const
{
return !(*this == other);
}
/**
* Test if this instance is at default settings.
*
* @return true if is at default settings
*/
bool KeeAgentSettings::isDefault() const
{
KeeAgentSettings defaultSettings;
return (*this == defaultSettings);
}
/**
* Reset this instance to default settings
*/
void KeeAgentSettings::reset()
{
m_allowUseOfSshKey = false;
m_addAtDatabaseOpen = false;
m_removeAtDatabaseClose = false;
m_useConfirmConstraintWhenAdding = false;
m_useLifetimeConstraintWhenAdding = false;
m_lifetimeConstraintDuration = 600;
m_selectedType = QStringLiteral("file");
m_attachmentName.clear();
m_saveAttachmentToTempFile = false;
m_fileName.clear();
m_error.clear();
}
/**
* Get last error as a QString.
*
* @return translated error message
*/
const QString KeeAgentSettings::errorString() const
{
return m_error;
}
bool KeeAgentSettings::allowUseOfSshKey() const
{
return m_allowUseOfSshKey;
}
bool KeeAgentSettings::addAtDatabaseOpen() const
{
return m_addAtDatabaseOpen;
}
bool KeeAgentSettings::removeAtDatabaseClose() const
{
return m_removeAtDatabaseClose;
}
bool KeeAgentSettings::useConfirmConstraintWhenAdding() const
{
return m_useConfirmConstraintWhenAdding;
}
bool KeeAgentSettings::useLifetimeConstraintWhenAdding() const
{
return m_useLifetimeConstraintWhenAdding;
}
int KeeAgentSettings::lifetimeConstraintDuration() const
{
return m_lifetimeConstraintDuration;
}
const QString KeeAgentSettings::selectedType() const
{
return m_selectedType;
}
const QString KeeAgentSettings::attachmentName() const
{
return m_attachmentName;
}
bool KeeAgentSettings::saveAttachmentToTempFile() const
{
return m_saveAttachmentToTempFile;
}
const QString KeeAgentSettings::fileName() const
{
return m_fileName;
}
const QString KeeAgentSettings::fileNameEnvSubst(QProcessEnvironment environment) const
{
return Tools::envSubstitute(m_fileName, environment);
}
void KeeAgentSettings::setAllowUseOfSshKey(bool allowUseOfSshKey)
{
m_allowUseOfSshKey = allowUseOfSshKey;
}
void KeeAgentSettings::setAddAtDatabaseOpen(bool addAtDatabaseOpen)
{
m_addAtDatabaseOpen = addAtDatabaseOpen;
}
void KeeAgentSettings::setRemoveAtDatabaseClose(bool removeAtDatabaseClose)
{
m_removeAtDatabaseClose = removeAtDatabaseClose;
}
void KeeAgentSettings::setUseConfirmConstraintWhenAdding(bool useConfirmConstraintWhenAdding)
{
m_useConfirmConstraintWhenAdding = useConfirmConstraintWhenAdding;
}
void KeeAgentSettings::setUseLifetimeConstraintWhenAdding(bool useLifetimeConstraintWhenAdding)
{
m_useLifetimeConstraintWhenAdding = useLifetimeConstraintWhenAdding;
}
void KeeAgentSettings::setLifetimeConstraintDuration(int lifetimeConstraintDuration)
{
m_lifetimeConstraintDuration = lifetimeConstraintDuration;
}
void KeeAgentSettings::setSelectedType(const QString& selectedType)
{
m_selectedType = selectedType;
}
void KeeAgentSettings::setAttachmentName(const QString& attachmentName)
{
m_attachmentName = attachmentName;
}
void KeeAgentSettings::setSaveAttachmentToTempFile(bool saveAttachmentToTempFile)
{
m_saveAttachmentToTempFile = saveAttachmentToTempFile;
}
void KeeAgentSettings::setFileName(const QString& fileName)
{
m_fileName = fileName;
}
bool KeeAgentSettings::readBool(QXmlStreamReader& reader)
{
reader.readNext();
bool ret = (reader.text().startsWith("t", Qt::CaseInsensitive));
reader.readNext(); // tag end
return ret;
}
int KeeAgentSettings::readInt(QXmlStreamReader& reader)
{
reader.readNext();
int ret = reader.text().toInt();
reader.readNext(); // tag end
return ret;
}
/**
* Read settings from an XML document.
*
* Sets error string on error.
*
* @param ba XML document
* @return success
*/
bool KeeAgentSettings::fromXml(const QByteArray& ba)
{
QXmlStreamReader reader;
reader.addData(ba);
if (reader.error() || !reader.readNextStartElement()) {
m_error = reader.errorString();
return false;
}
if (reader.qualifiedName() != "EntrySettings") {
m_error = QCoreApplication::translate("KeeAgentSettings", "Invalid KeeAgent settings file structure.");
return false;
}
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "AllowUseOfSshKey") {
m_allowUseOfSshKey = readBool(reader);
} else if (reader.name() == "AddAtDatabaseOpen") {
m_addAtDatabaseOpen = readBool(reader);
} else if (reader.name() == "RemoveAtDatabaseClose") {
m_removeAtDatabaseClose = readBool(reader);
} else if (reader.name() == "UseConfirmConstraintWhenAdding") {
m_useConfirmConstraintWhenAdding = readBool(reader);
} else if (reader.name() == "UseLifetimeConstraintWhenAdding") {
m_useLifetimeConstraintWhenAdding = readBool(reader);
} else if (reader.name() == "LifetimeConstraintDuration") {
m_lifetimeConstraintDuration = readInt(reader);
} else if (reader.name() == "Location") {
while (!reader.error() && reader.readNextStartElement()) {
if (reader.name() == "SelectedType") {
reader.readNext();
m_selectedType = reader.text().toString();
reader.readNext();
} else if (reader.name() == "AttachmentName") {
reader.readNext();
m_attachmentName = reader.text().toString();
reader.readNext();
} else if (reader.name() == "SaveAttachmentToTempFile") {
m_saveAttachmentToTempFile = readBool(reader);
} else if (reader.name() == "FileName") {
reader.readNext();
m_fileName = reader.text().toString();
reader.readNext();
} else {
qWarning() << "Skipping location element" << reader.name();
reader.skipCurrentElement();
}
}
} else {
qWarning() << "Skipping element" << reader.name();
reader.skipCurrentElement();
}
}
return true;
}
/**
* Write settings to an XML document.
*
* @return XML document
*/
QByteArray KeeAgentSettings::toXml() const
{
QByteArray ba;
QXmlStreamWriter writer(&ba);
// real KeeAgent can only read UTF-16
writer.setCodec(QTextCodec::codecForName("UTF-16"));
writer.setAutoFormatting(true);
writer.setAutoFormattingIndent(2);
writer.writeStartDocument();
writer.writeStartElement("EntrySettings");
writer.writeAttribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema");
writer.writeAttribute("xmlns:xsi", "http://www.w3.org/2001/XMLSchema-instance");
writer.writeTextElement("AllowUseOfSshKey", m_allowUseOfSshKey ? "true" : "false");
writer.writeTextElement("AddAtDatabaseOpen", m_addAtDatabaseOpen ? "true" : "false");
writer.writeTextElement("RemoveAtDatabaseClose", m_removeAtDatabaseClose ? "true" : "false");
writer.writeTextElement("UseConfirmConstraintWhenAdding", m_useConfirmConstraintWhenAdding ? "true" : "false");
writer.writeTextElement("UseLifetimeConstraintWhenAdding", m_useLifetimeConstraintWhenAdding ? "true" : "false");
writer.writeTextElement("LifetimeConstraintDuration", QString::number(m_lifetimeConstraintDuration));
writer.writeStartElement("Location");
writer.writeTextElement("SelectedType", m_selectedType);
if (!m_attachmentName.isEmpty()) {
writer.writeTextElement("AttachmentName", m_attachmentName);
} else {
writer.writeEmptyElement("AttachmentName");
}
writer.writeTextElement("SaveAttachmentToTempFile", m_saveAttachmentToTempFile ? "true" : "false");
if (!m_fileName.isEmpty()) {
writer.writeTextElement("FileName", m_fileName);
} else {
writer.writeEmptyElement("FileName");
}
writer.writeEndElement(); // Location
writer.writeEndElement(); // EntrySettings
writer.writeEndDocument();
return ba;
}
/**
* Check if entry attachments have KeeAgent settings configured
*
* @param attachments EntryAttachments to check the key
* @return true if XML document exists
*/
bool KeeAgentSettings::inEntryAttachments(const EntryAttachments* attachments)
{
return attachments->hasKey("KeeAgent.settings");
}
/**
* Read settings from an entry as an XML attachment.
*
* Sets error string on error.
*
* @param entry Entry to read the attachment from
* @return true if XML document was loaded
*/
bool KeeAgentSettings::fromEntry(const Entry* entry)
{
return fromXml(entry->attachments()->value("KeeAgent.settings"));
}
/**
* Write settings to an entry as an XML attachment.
*
* @param entry Entry to create the attachment to
*/
void KeeAgentSettings::toEntry(Entry* entry) const
{
if (isDefault()) {
if (entry->attachments()->hasKey("KeeAgent.settings")) {
entry->attachments()->remove("KeeAgent.settings");
}
} else {
entry->attachments()->set("KeeAgent.settings", toXml());
}
}
/**
* Test if a SSH key is currently set to be used
*
* @return true if key is configured
*/
bool KeeAgentSettings::keyConfigured() const
{
if (m_selectedType == "attachment") {
return !m_attachmentName.isEmpty();
} else {
return !m_fileName.isEmpty();
}
}
/**
* Read a SSH key based on settings from entry to key.
*
* Sets error string on error.
*
* @param entry input entry to read attachment and decryption key
* @param key output key object
* @param decrypt avoid private key decryption if possible (old RSA keys are always decrypted)
* @return true if key was properly opened
*/
bool KeeAgentSettings::toOpenSSHKey(const Entry* entry, OpenSSHKey& key, bool decrypt)
{
return toOpenSSHKey(entry->username(), entry->password(), entry->attachments(), key, decrypt);
}
/**
* Read a SSH key based on settings to key.
*
* Sets error string on error.
*
* @param username username to set on key if empty
* @param password password to decrypt key if needed
* @param attachments attachments to read an attachment key from
* @param key output key object
* @param decrypt avoid private key decryption if possible (old RSA keys are always decrypted)
* @return true if key was properly opened
*/
bool KeeAgentSettings::toOpenSSHKey(const QString& username,
const QString& password,
const EntryAttachments* attachments,
OpenSSHKey& key,
bool decrypt)
{
QString fileName;
QByteArray privateKeyData;
if (m_selectedType == "attachment") {
if (!attachments) {
m_error = QCoreApplication::translate("KeeAgentSettings",
"Private key is an attachment but no attachments provided.");
return false;
}
fileName = m_attachmentName;
privateKeyData = attachments->value(fileName);
} else {
QFile localFile(fileNameEnvSubst());
QFileInfo localFileInfo(localFile);
fileName = localFileInfo.fileName();
if (localFile.fileName().isEmpty()) {
m_error = QCoreApplication::translate("KeeAgentSettings", "Private key is empty");
return false;
}
if (localFile.size() > 1024 * 1024) {
m_error = QCoreApplication::translate("KeeAgentSettings", "File too large to be a private key");
return false;
}
if (!localFile.open(QIODevice::ReadOnly)) {
m_error = QCoreApplication::translate("KeeAgentSettings", "Failed to open private key");
return false;
}
privateKeyData = localFile.readAll();
}
if (privateKeyData.isEmpty()) {
m_error = QCoreApplication::translate("KeeAgentSettings", "Private key is empty");
return false;
}
if (!key.parsePKCS1PEM(privateKeyData)) {
m_error = key.errorString();
return false;
}
if (key.encrypted() && decrypt) {
if (!key.openKey(password)) {
m_error = key.errorString();
return false;
}
}
if (key.comment().isEmpty()) {
key.setComment(username);
}
if (key.comment().isEmpty()) {
key.setComment(fileName);
}
return true;
}