diff --git a/src/pass.cpp b/src/pass.cpp index 2674a99..86bc5d6 100644 --- a/src/pass.cpp +++ b/src/pass.cpp @@ -1,437 +1,437 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2017 Jason A. Donenfeld SPDX-FileCopyrightText: 2020 Charlie Waters SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "pass.h" #include "gpgmehelpers.h" #include "settings.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include // TODO remove #include using namespace std; // TODO remove Q_REQUIRED_RESULT static bool ensurePassStore(const QString &file, const char *location) { if (file.startsWith(Settings::getPassStore())) { return true; } QMessageBox::critical(nullptr, QStringLiteral("error"), QStringLiteral("file not in pass store called from %1 %2 %3").arg(QString::fromLocal8Bit(location),file,Settings::getPassStore())); return false; } // TODO remove Q_REQUIRED_RESULT static bool ensureSuffix(const QString &file, const char *location) { if (file.endsWith(QStringLiteral(".gpg"))) { return true; } qWarning() << "file without suffix called from " << location; QMessageBox::critical(nullptr, QStringLiteral("error"), QStringLiteral("file without suffix called from %1 %2").arg(QString::fromLocal8Bit(location),file)); return false; } /** * @brief Pass::Pass wrapper for using either pass or the pass imitation */ Pass::Pass() { } /** * @brief Pass::Generate use either pwgen or internal password * generator * @param length of the desired password * @param charset to use for generation * @return the password */ QString Pass::Generate_b(unsigned int length, const QString &charset) { if (charset.length() > 0) { return generateRandomPassword(charset, length); } else { Q_EMIT critical(i18n("No characters chosen"), i18n("Can't generate password, there are no characters to choose from " "set in the configuration!")); } return {}; } /** * @brief Pass::listKeys list users * @param keystrings * @param secret list private keys * @return QList users */ QList Pass::listKeys(const QStringList& keystrings, bool secret) { auto job = protocol->keyListJob(); std::vector keys; job->addMode(GpgME::KeyListMode::WithSecret); auto result = job->exec(keystrings, secret, keys); if (!isSuccess(result.error())) { return {}; } QList users; for (const auto &key : keys) { UserInfo ui; ui.created = QDateTime::fromTime_t(key.subkey(0).creationTime()); ui.key_id = fromGpgmeCharStar(key.keyID()); ui.name = createCombinedNameString(key.userID(0)); ui.validity = key.userID(0).validityAsString(); ui.expiry = QDateTime::fromTime_t(key.subkey(0).expirationTime()); ui.have_secret = key.hasSecret(); users.append(ui); } return users; } /** * @brief Pass::listKeys list users * @param keystring * @param secret list private keys * @return QList users */ QList Pass::listKeys(const QString& keystring, bool secret) { return listKeys(QStringList(keystring), secret); } /** * @brief Pass::getRecipientList return list of gpg-id's to encrypt for * @param for_file which file (folder) would you like recepients for * @return recepients gpg-id contents */ QStringList Pass::getRecipientList(const QString& for_file) { if (!ensurePassStore(for_file, Q_FUNC_INFO)) { return {}; } QDir gpgIdPath(QFileInfo(for_file).absoluteDir()); bool found = false; while (gpgIdPath.exists() && gpgIdPath.absolutePath().startsWith(Settings::getPassStore())) { if (QFile(gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id"))).exists()) { found = true; break; } if (!gpgIdPath.cdUp()) break; } QFile gpgId(found ? gpgIdPath.absoluteFilePath(QStringLiteral(".gpg-id")) : Settings::getPassStore() + QStringLiteral(".gpg-id")); if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) return QStringList(); QStringList recipients; while (!gpgId.atEnd()) { QString recipient(QString::fromLocal8Bit(gpgId.readLine())); recipient = recipient.trimmed(); if (!recipient.isEmpty()) recipients += recipient; } return recipients; } void Pass::Show(const QString& file) { if (!(ensureSuffix(file, Q_FUNC_INFO) && ensurePassStore(file,Q_FUNC_INFO))) { return; } QFile f(file); f.open(QIODevice::ReadOnly); auto data = f.readAll(); auto job = protocol->decryptJob(); connect(job, &QGpgME::DecryptJob::result, this, [this](auto &&result, QByteArray plainText, QString auditLog, auto &&logError) { if (isSuccess(result.error())) { Q_EMIT finishedShow(QString::fromUtf8(plainText)); } else { Q_EMIT errorString(QString::fromLocal8Bit(result.error().asString())); - Q_EMIT processErrorExit(result.error().code(), QString::fromLocal8Bit(result.error().asString())); + Q_EMIT decryptionError(); } // Q_EMIT log(auditLog); Q_UNUSED(logError); Q_UNUSED(auditLog); }); job->start(data); } void Pass::Insert(const QString& file, const QString& newValue) { if (!(ensureSuffix(file, Q_FUNC_INFO) && ensurePassStore(file,Q_FUNC_INFO))) { return; } QStringList recipients = Pass::getRecipientList(file); auto job = protocol->encryptJob(); std::vector keys; auto ctx = QGpgME::Job::context(job); for (const auto &keyId : recipients) { GpgME::Error error; auto key = ctx->key(keyId.toUtf8().data(), error, false); if (!error && !key.isNull()) { keys.push_back(key); } } if (keys.empty()) { Q_EMIT critical(i18n("Can not edit"), i18n("Could not read encryption key to use, .gpg-id " "file missing or invalid.")); return; } connect(job, &QGpgME::EncryptJob::result, this, [this, file](auto &&result, const QByteArray &ciphertext, const QString &log, auto &&auditResult) { if (isSuccess(result.error())) { QSaveFile f(file); bool open = f.open(QIODevice::WriteOnly | QIODevice::Truncate); if (!open) { Q_EMIT errorString(QStringLiteral("File open failed: %1").arg(f.errorString())); return; } f.write(ciphertext); if (f.error() == QFileDevice::NoError) { f.commit(); Q_EMIT finishedInsert(); } else { Q_EMIT errorString(QStringLiteral("File write failed: %1").arg(f.errorString())); } } else { Q_EMIT errorString(QString::fromUtf8(result.error().asString())); } Q_UNUSED(log); Q_UNUSED(auditResult); }); job->start(keys, newValue.toUtf8()); } void Pass::Remove(const QString& file, bool isDir) { if (!ensurePassStore(file, Q_FUNC_INFO)) { return; } if (!isDir) { if(!ensureSuffix(file, Q_FUNC_INFO)) { return; } QFile(file).remove(); } else { QDir dir(file); dir.removeRecursively(); } } void Pass::Init(const QString& path, const QList &users) { if (!ensurePassStore(path, Q_FUNC_INFO)) { return; } QString gpgIdFile = path + QStringLiteral(".gpg-id"); QFile gpgId(gpgIdFile); if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) { Q_EMIT critical(i18n("Cannot update"), i18n("Failed to open .gpg-id for writing.")); return; } bool secret_selected = false; for (const UserInfo &user : users) { if (user.enabled) { gpgId.write((user.key_id + QStringLiteral("\n")).toUtf8()); secret_selected |= user.have_secret; } } gpgId.close(); if (!secret_selected) { Q_EMIT critical(i18n("Check selected users!"), i18n("None of the selected keys have a secret key available.\n" "You will not be able to decrypt any newly added passwords!")); return; } reencryptPath(path); } /** * @brief ImitatePass::reencryptPath reencrypt all files under the chosen * directory * * This is stil quite experimental.. * @param dir */ void Pass::reencryptPath(const QString &dir) { if (!ensurePassStore(dir, Q_FUNC_INFO)) { return; } Q_EMIT startReencryptPath(); QDir currentDir; QDirIterator gpgFiles(dir, QStringList() << QStringLiteral("*.gpg"), QDir::Files, QDirIterator::Subdirectories); QStringList gpgId; while (gpgFiles.hasNext()) { QString fileName = gpgFiles.next(); if (gpgFiles.fileInfo().path() != currentDir.path()) { gpgId = getRecipientList(fileName); gpgId.sort(); } QByteArray plainText; QFile f(fileName); if (!f.open(QIODevice::ReadOnly)) { Q_EMIT errorString(QStringLiteral("Failed to open file: %1").arg(fileName)); continue; } QByteArray cipherText = f.readAll(); f.close(); auto decryptJob = protocol->decryptJob(); auto context = QGpgME::Job::context(decryptJob); auto decryptResult = decryptJob->exec(cipherText, plainText); if (!isSuccess(decryptResult.error())) { Q_EMIT errorString(fromGpgmeCharStar(decryptResult.error().asString())); continue; } auto actualRecipients = decryptResult.recipients(); QStringList actualIds; for (auto &recipient : actualRecipients) { GpgME::Error error; auto key = context->key(recipient.keyID(), error, false); if (!error) { actualIds.append(fromGpgmeCharStar(key.keyID())); } } actualIds.sort(); if (actualIds != gpgId) { auto encryptJob = protocol->encryptJob(); std::vector keys; auto ctx = QGpgME::Job::context(encryptJob); for (const auto &keyId : qAsConst(gpgId)) { GpgME::Error error; auto key = ctx->key(keyId.toUtf8().data(), error, false); if (!error && !key.isNull()) { keys.push_back(key); } } auto encryptResult = encryptJob->exec(keys, plainText, false, cipherText); if (!isSuccess(encryptResult.error())) { Q_EMIT errorString(fromGpgmeCharStar(encryptResult.error().asString())); continue; } QSaveFile save(fileName); bool open = save.open(QIODevice::WriteOnly | QIODevice::Truncate); if (!open) { Q_EMIT errorString(QStringLiteral("open file failed %1").arg(fileName)); continue; } save.write(cipherText); if (save.error() != QFileDevice::NoError) { Q_EMIT errorString(QStringLiteral("Writing file failed: %1").arg(fileName)); continue; } else { save.commit(); } } } Q_EMIT endReencryptPath(); } void Pass::Move(const QString& src, const QString& dest, const bool force) { QFileInfo srcFileInfo(src); QFileInfo destFileInfo(dest); QString destFile; QString srcFileBaseName = srcFileInfo.fileName(); if (srcFileInfo.isFile()) { if (destFileInfo.isFile()) { if (!force) { return; } } else if (destFileInfo.isDir()) { destFile = QDir(dest).filePath(srcFileBaseName); } else { destFile = dest; } if (destFile.endsWith(QStringLiteral(".gpg"), Qt::CaseInsensitive)) destFile.chop(4); // make sure suffix is lowercase destFile.append(QStringLiteral(".gpg")); } else if (srcFileInfo.isDir()) { if (destFileInfo.isDir()) { destFile = QDir(dest).filePath(srcFileBaseName); } else if (destFileInfo.isFile()) { return; } else { destFile = dest; } } else { return; } QDir qDir; if (force) { qDir.remove(destFile); } qDir.rename(src, destFile); } void Pass::Copy(const QString& src, const QString& dest, const bool force) { QFileInfo destFileInfo(dest); QDir qDir; if (force) { qDir.remove(dest); } if (!QFile::copy(src, dest)) { Q_EMIT errorString(QStringLiteral("Failed to copy file")); } // reecrypt all files under the new folder if (destFileInfo.isDir()) { reencryptPath(destFileInfo.absoluteFilePath()); } else if (destFileInfo.isFile()) { reencryptPath(destFileInfo.dir().path()); } } quint32 Pass::boundedRandom(quint32 bound) { if (bound < 2) { return 0; } quint32 randval; const quint32 max_mod_bound = (1 + ~bound) % bound; do { randval = QRandomGenerator::system()->generate(); } while (randval < max_mod_bound); return randval % bound; } QString Pass::generateRandomPassword(const QString &charset, unsigned int length) { QString out; for (unsigned int i = 0; i < length; ++i) { out.append(charset.at(static_cast(boundedRandom(static_cast(charset.length()))))); } return out; } diff --git a/src/pass.h b/src/pass.h index dc09f2c..53e599c 100644 --- a/src/pass.h +++ b/src/pass.h @@ -1,63 +1,63 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2017 Jason A. Donenfeld SPDX-FileCopyrightText: 2020 Charlie Waters SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #ifndef PASS_H #define PASS_H #include "userinfo.h" #include #include #include #include #include /*! \class Pass \brief Acts as an abstraction for pass or pass imitation */ class Pass final : public QObject { Q_OBJECT public: Pass(); void Show(const QString& file); void Insert(const QString& file, const QString& value); void Remove(const QString& file, bool isDir); void Move(const QString& srcDir, const QString& dest, const bool force = false); void Copy(const QString& srcDir, const QString& dest, const bool force = false); void Init(const QString &path, const QList &users); QString Generate_b(unsigned int length, const QString &charset); QList listKeys(const QStringList& keystrings, bool secret = false); QList listKeys(const QString& keystring = QString{}, bool secret = false); static QStringList getRecipientList(const QString& for_file); private: QGpgME::Protocol *protocol = QGpgME::openpgp(); QString generateRandomPassword(const QString &charset, unsigned int length); quint32 boundedRandom(quint32 bound); void reencryptPath(const QString &dir); Q_SIGNALS: void startReencryptPath(); void endReencryptPath(); void errorString(const QString &err); - void statusMsg(const QString&, int); void critical(const QString&, const QString&); - void processErrorExit(int exitCode, const QString &err); + // messages are provided via errorString + void decryptionError(); void finishedShow(const QString &plainTextPassword); void finishedInsert(); }; #endif // PASS_H diff --git a/src/passworddialog.cpp b/src/passworddialog.cpp index 0180cee..db5d7b3 100644 --- a/src/passworddialog.cpp +++ b/src/passworddialog.cpp @@ -1,242 +1,242 @@ /* SPDX-FileCopyrightText: 2014-2023 Anne Jan Brouwer SPDX-FileCopyrightText: 2018 Lukas Vogel SPDX-FileCopyrightText: 2018 Claudio Maradonna SPDX-FileCopyrightText: 2023 g10 Code GmbH SPDX-FileContributor: Sune Stolborg Vuorela SPDX-License-Identifier: GPL-3.0-or-later */ #include "passworddialog.h" #include "filecontent.h" #include "pass.h" #include "passwordconfiguration.h" #include "settings.h" #include "ui_passworddialog.h" #include #include #include /** * @brief PasswordDialog::PasswordDialog basic constructor. * @param passConfig configuration constant * @param parent */ PasswordDialog::PasswordDialog(Pass &pass, const PasswordConfiguration &passConfig, QWidget *parent) : QDialog(parent) , ui(std::make_unique()) , m_pass(pass) , m_passConfig(passConfig) { m_templating = false; m_allFields = false; m_isNew = false; ui->setupUi(this); setLength(m_passConfig.length); setPasswordCharTemplate(m_passConfig.selected); connect(&m_pass, &Pass::finishedShow, this, &PasswordDialog::setPass); } /** * @brief PasswordDialog::PasswordDialog complete constructor. * @param file * @param isNew * @param parent pointer */ PasswordDialog::PasswordDialog(Pass &pass, const QString &file, const bool &isNew, QWidget *parent) : QDialog(parent) , ui(std::make_unique()) , m_pass(pass) , m_file(file) , m_isNew(isNew) { if (!isNew) m_pass.Show(m_file); ui->setupUi(this); setWindowTitle(this->windowTitle() + QStringLiteral(" ") + QFileInfo(m_file).baseName()); m_passConfig = Settings::getPasswordConfiguration(); setTemplate(Settings::getPassTemplate(), Settings::isUseTemplate()); templateAll(Settings::isTemplateAllFields()); setLength(m_passConfig.length); setPasswordCharTemplate(m_passConfig.selected); connect(&m_pass, &Pass::finishedShow, this, &PasswordDialog::setPass); - connect(&m_pass, &Pass::processErrorExit, this, &PasswordDialog::close); + connect(&m_pass, &Pass::decryptionError, this, &PasswordDialog::close); connect(this, &PasswordDialog::accepted, this, &PasswordDialog::dialogAccepted); connect(this, &PasswordDialog::rejected, this, &PasswordDialog::dialogCancelled); connect(ui->createPasswordButton, &QAbstractButton::clicked, this, &PasswordDialog::createPassword); connect(ui->checkBoxShow, &QCheckBox::stateChanged, this, &PasswordDialog::showPasswordChanged); } /** * @brief Pass{}{}wordDialog::~PasswordDialog basic destructor. */ PasswordDialog::~PasswordDialog() = default; /** * @brief PasswordDialog::on_checkBoxShow_stateChanged hide or show passwords. * @param arg1 */ void PasswordDialog::showPasswordChanged(int arg1) { if (arg1) ui->lineEditPassword->setEchoMode(QLineEdit::Normal); else ui->lineEditPassword->setEchoMode(QLineEdit::Password); } /** * @brief PasswordDialog::on_createPasswordButton_clicked generate a random * passwords. * @todo refactor when process is untangled from MainWindow class. */ void PasswordDialog::createPassword() { ui->widget->setEnabled(false); QString newPass = m_pass.Generate_b(static_cast(ui->spinBox_pwdLength->value()), m_passConfig.Characters[static_cast(ui->passwordTemplateSwitch->currentIndex())]); if (newPass.length() > 0) ui->lineEditPassword->setText(newPass); ui->widget->setEnabled(true); } /** * @brief PasswordDialog::on_accepted handle Ok click for QDialog */ void PasswordDialog::dialogAccepted() { QString newValue = getPassword(); if (newValue.isEmpty()) return; if (newValue.right(1) != QLatin1Char('\n')) newValue += QLatin1Char('\n'); m_pass.Insert(m_file, newValue); } /** * @brief PasswordDialog::on_rejected handle Cancel click for QDialog */ void PasswordDialog::dialogCancelled() { setPassword(QString()); } /** * @brief PasswordDialog::setPassword populate the (templated) fields. * @param password */ void PasswordDialog::setPassword(const QString& password) { FileContent fileContent = FileContent::parse(password, m_templating ? m_fields : QStringList(), m_allFields); ui->lineEditPassword->setText(fileContent.getPassword()); QWidget *previous = ui->checkBoxShow; // first set templated values NamedValues namedValues = fileContent.getNamedValues(); for (QLineEdit *line : qAsConst(templateLines)) { line->setText(namedValues.takeValue(line->objectName())); previous = line; } // show remaining values (if there are) otherLines.clear(); for (const NamedValue &nv : qAsConst(namedValues)) { auto *line = new QLineEdit(); line->setObjectName(nv.name); line->setText(nv.value); ui->formLayout->addRow(new QLabel(nv.name), line); setTabOrder(previous, line); otherLines.append(line); previous = line; } ui->plainTextEdit->insertPlainText(fileContent.getRemainingData()); } /** * @brief PasswordDialog::getPassword join the (templated) fields to a QString * for writing back. * @return collappsed password. */ QString PasswordDialog::getPassword() { QString passFile = ui->lineEditPassword->text() + QLatin1Char('\n'); QList allLines(templateLines); allLines.append(otherLines); for (QLineEdit *line : allLines) { QString text = line->text(); if (text.isEmpty()) continue; passFile += line->objectName() + QStringLiteral(": ") + text + QLatin1Char('\n'); } passFile += ui->plainTextEdit->toPlainText(); return passFile; } /** * @brief PasswordDialog::setTemplate set the template and create the fields. * @param rawFields */ void PasswordDialog::setTemplate(const QString& rawFields, bool useTemplate) { m_fields = rawFields.split(QLatin1Char('\n')); m_templating = useTemplate; templateLines.clear(); if (m_templating) { QWidget *previous = ui->checkBoxShow; for (const QString &field : qAsConst(m_fields)) { if (field.isEmpty()) continue; auto *line = new QLineEdit(); line->setObjectName(field); ui->formLayout->addRow(new QLabel(field), line); setTabOrder(previous, line); templateLines.append(line); previous = line; } } } /** * @brief PasswordDialog::templateAll basic setter for use in * PasswordDialog::setPassword templating all tokenisable lines. * @param templateAll */ void PasswordDialog::templateAll(bool templateAll) { m_allFields = templateAll; } /** * @brief PasswordDialog::setLength * PasswordDialog::setLength password length. * @param l */ void PasswordDialog::setLength(int l) { ui->spinBox_pwdLength->setValue(l); } /** * @brief PasswordDialog::setPasswordCharTemplate * PasswordDialog::setPasswordCharTemplate chose the template style. * @param t */ void PasswordDialog::setPasswordCharTemplate(int t) { ui->passwordTemplateSwitch->setCurrentIndex(t); } void PasswordDialog::setPass(const QString &output) { setPassword(output); // TODO(bezet): enable ui }