diff --git a/src/pass.cpp b/src/pass.cpp index 13e9f24..2674a99 100644 --- a/src/pass.cpp +++ b/src/pass.cpp @@ -1,436 +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 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; }