diff options
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | src/configdialog.cpp | 33 | ||||
-rw-r--r-- | src/configdialog.h | 4 | ||||
-rw-r--r-- | src/configdialog.ui | 5 | ||||
-rw-r--r-- | src/imitatepass.cpp | 104 | ||||
-rw-r--r-- | src/imitatepass.h | 1 | ||||
-rw-r--r-- | src/mainwindow.cpp | 10 | ||||
-rw-r--r-- | src/pass.cpp | 64 | ||||
-rw-r--r-- | src/pass.h | 1 | ||||
-rw-r--r-- | src/qtpasssettings.cpp | 47 | ||||
-rw-r--r-- | src/qtpasssettings.h | 9 | ||||
-rw-r--r-- | src/settingsconstants.cpp | 1 | ||||
-rw-r--r-- | src/settingsconstants.h | 1 |
13 files changed, 239 insertions, 42 deletions
@@ -79,6 +79,7 @@ qmake && make && make install ## Using profiles Profiles allow to group passwords. Each profile might use a different git repository and/or different gpg key. +Each profile also can be associated with a pass store singing key to verify the detached .gpg-id signature. A typical use case is to separate personal and work passwords. > **Hint**<br> diff --git a/src/configdialog.cpp b/src/configdialog.cpp index 4a711b03..3794c6f1 100644 --- a/src/configdialog.cpp +++ b/src/configdialog.cpp @@ -97,7 +97,8 @@ ConfigDialog::ConfigDialog(MainWindow *parent) useTemplate(QtPassSettings::isUseTemplate()); ui->profileTable->verticalHeader()->hide(); - ui->profileTable->horizontalHeader()->setStretchLastSection(true); + ui->profileTable->horizontalHeader()->setSectionResizeMode( + 1, QHeaderView::Stretch); ui->label->setText(ui->label->text() + VERSION); ui->comboBoxClipboard->clear(); @@ -170,7 +171,7 @@ void ConfigDialog::validate(QTableWidgetItem *item) { for (int j = 0; j < ui->profileTable->columnCount(); j++) { QTableWidgetItem *_item = ui->profileTable->item(i, j); - if (_item->text().isEmpty()) { + if (_item->text().isEmpty() && j != 2) { _item->setBackground(Qt::red); status = false; break; @@ -181,7 +182,7 @@ void ConfigDialog::validate(QTableWidgetItem *item) { break; } } else { - if (item->text().isEmpty()) { + if (item->text().isEmpty() && item->column() != 2) { item->setBackground(Qt::red); status = false; } @@ -460,8 +461,8 @@ void ConfigDialog::genKey(QString batch, QDialog *dialog) { * @param profiles * @param profile */ -void ConfigDialog::setProfiles(QHash<QString, QString> profiles, - QString profile) { +void ConfigDialog::setProfiles(QHash<QString, QHash<QString, QString>> profiles, + QString currentProfile) { // dbg()<< profiles; if (profiles.contains("")) { profiles.remove(""); @@ -469,15 +470,18 @@ void ConfigDialog::setProfiles(QHash<QString, QString> profiles, } ui->profileTable->setRowCount(profiles.count()); - QHashIterator<QString, QString> i(profiles); + QHashIterator<QString, QHash<QString, QString>> i(profiles); int n = 0; while (i.hasNext()) { i.next(); if (!i.value().isEmpty() && !i.key().isEmpty()) { ui->profileTable->setItem(n, 0, new QTableWidgetItem(i.key())); - ui->profileTable->setItem(n, 1, new QTableWidgetItem(i.value())); + ui->profileTable->setItem(n, 1, + new QTableWidgetItem(i.value().value("path"))); + ui->profileTable->setItem( + n, 2, new QTableWidgetItem(i.value().value("signingKey"))); // dbg()<< "naam:" + i.key(); - if (i.key() == profile) + if (i.key() == currentProfile) ui->profileTable->selectRow(n); } ++n; @@ -488,17 +492,23 @@ void ConfigDialog::setProfiles(QHash<QString, QString> profiles, * @brief ConfigDialog::getProfiles return profile list. * @return */ -QHash<QString, QString> ConfigDialog::getProfiles() { - QHash<QString, QString> profiles; +QHash<QString, QHash<QString, QString>> ConfigDialog::getProfiles() { + QHash<QString, QHash<QString, QString>> profiles; // Check? for (int i = 0; i < ui->profileTable->rowCount(); ++i) { + QHash<QString, QString> profile; QTableWidgetItem *pathItem = ui->profileTable->item(i, 1); if (nullptr != pathItem) { QTableWidgetItem *item = ui->profileTable->item(i, 0); if (item == nullptr) { continue; } - profiles.insert(item->text(), pathItem->text()); + profile["path"] = pathItem->text(); + QTableWidgetItem *signingKeyItem = ui->profileTable->item(i, 2); + if (nullptr != signingKeyItem) { + profile["signingKey"] = signingKeyItem->text(); + } + profiles.insert(item->text(), profile); } } return profiles; @@ -512,6 +522,7 @@ void ConfigDialog::on_addButton_clicked() { ui->profileTable->insertRow(n); ui->profileTable->setItem(n, 0, new QTableWidgetItem()); ui->profileTable->setItem(n, 1, new QTableWidgetItem(ui->storePath->text())); + ui->profileTable->setItem(n, 2, new QTableWidgetItem()); ui->profileTable->selectRow(n); ui->deleteButton->setEnabled(true); diff --git a/src/configdialog.h b/src/configdialog.h index a0f162b0..4460d4bf 100644 --- a/src/configdialog.h +++ b/src/configdialog.h @@ -31,7 +31,7 @@ public: void useSelection(bool useSelection); void useAutoclear(bool useAutoclear); void useAutoclearPanel(bool useAutoclearPanel); - QHash<QString, QString> getProfiles(); + QHash<QString, QHash<QString, QString>> getProfiles(); void wizard(); void genKey(QString, QDialog *); void useTrayIcon(bool useSystray); @@ -76,7 +76,7 @@ private: QStringList getSecretKeys(); void setGitPath(QString); - void setProfiles(QHash<QString, QString>, QString); + void setProfiles(QHash<QString, QHash<QString, QString>>, QString); void usePass(bool usePass); void setGroupBoxState(); diff --git a/src/configdialog.ui b/src/configdialog.ui index 874bbb3f..68281ca7 100644 --- a/src/configdialog.ui +++ b/src/configdialog.ui @@ -913,6 +913,11 @@ <string>Path</string> </property> </column> + <column> + <property name="text"> + <string>Signing Key</string> + </property> + </column> </widget> </item> <item> diff --git a/src/imitatepass.cpp b/src/imitatepass.cpp index 1cdb8575..307670de 100644 --- a/src/imitatepass.cpp +++ b/src/imitatepass.cpp @@ -89,6 +89,12 @@ void ImitatePass::OtpGenerate(QString file) { */ void ImitatePass::Insert(QString file, QString newValue, bool overwrite) { file = file + ".gpg"; + QString gpgIdPath = Pass::getGpgIdPath(file); + if (!verifyGpgIdFile(gpgIdPath)) { + emit critical(tr("Check .gpgid file signature!"), + tr("Signature for %1 is invalid.").arg(gpgIdPath)); + return; + } transactionHelper trans(this, PASS_INSERT); QStringList recipients = Pass::getRecipientList(file); if (recipients.isEmpty()) { @@ -166,6 +172,38 @@ void ImitatePass::Remove(QString file, bool isDir) { * path */ void ImitatePass::Init(QString path, const QList<UserInfo> &users) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + QStringList signingKeys = + QtPassSettings::getPassSigningKey().split(" ", Qt::SkipEmptyParts); +#else + QStringList signingKeys = + QtPassSettings::getPassSigningKey().split(" ", QString::SkipEmptyParts); +#endif + QString gpgIdSigFile = path + ".gpg-id.sig"; + bool addSigFile = false; + if (!signingKeys.isEmpty()) { + QString out; + QStringList args = + QStringList{"--status-fd=1", "--list-secret-keys"} + signingKeys; + exec.executeBlocking(QtPassSettings::getGpgExecutable(), args, &out); + bool found = false; + for (auto &key : signingKeys) { + if (out.contains("[GNUPG:] KEY_CONSIDERED " + key)) { + found = true; + break; + } + } + if (!found) { + emit critical(tr("No signing key!"), + tr("None of the secret signing keys is available.\n" + "You will not be able to change the user list!")); + return; + } + QFileInfo checkFile(gpgIdSigFile); + if (!checkFile.exists() || !checkFile.isFile()) + addSigFile = true; + } + QString gpgIdFile = path + ".gpg-id"; QFile gpgId(gpgIdFile); bool addFile = false; @@ -196,6 +234,20 @@ void ImitatePass::Init(QString path, const QList<UserInfo> &users) { return; } + if (!signingKeys.isEmpty()) { + QStringList args; + for (auto &key : signingKeys) { + args.append(QStringList{"--default-key", key}); + } + args.append(QStringList{"--yes", "--detach-sign", gpgIdFile}); + exec.executeBlocking(QtPassSettings::getGpgExecutable(), args); + if (!verifyGpgIdFile(gpgIdFile)) { + emit critical(tr("Check .gpgid file signature!"), + tr("Signature for %1 is invalid.").arg(gpgIdFile)); + return; + } + } + if (!QtPassSettings::isUseWebDav() && QtPassSettings::isUseGit() && !QtPassSettings::getGitExecutable().isEmpty()) { if (addFile) @@ -203,11 +255,52 @@ void ImitatePass::Init(QString path, const QList<UserInfo> &users) { QString commitPath = gpgIdFile; commitPath.replace(QRegularExpression("\\.gpg$"), ""); GitCommit(gpgIdFile, "Added " + commitPath + " using QtPass."); + if (!signingKeys.isEmpty()) { + if (addSigFile) + executeGit(GIT_ADD, {"add", pgit(gpgIdSigFile)}); + commitPath = gpgIdSigFile; + commitPath.replace(QRegularExpression("\\.gpg$"), ""); + GitCommit(gpgIdSigFile, "Added " + commitPath + " using QtPass."); + } } reencryptPath(path); } /** + * @brief ImitatePass::verifyGpgIdFile verify detached gpgid file signature. + * @param file which gpgid file. + * @return was verification succesful? + */ +bool ImitatePass::verifyGpgIdFile(const QString &file) { +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + QStringList signingKeys = + QtPassSettings::getPassSigningKey().split(" ", Qt::SkipEmptyParts); +#else + QStringList signingKeys = + QtPassSettings::getPassSigningKey().split(" ", QString::SkipEmptyParts); +#endif + if (signingKeys.isEmpty()) + return true; + QString out; + QStringList args = + QStringList{"--verify", "--status-fd=1", pgpg(file) + ".sig", pgpg(file)}; + exec.executeBlocking(QtPassSettings::getGpgExecutable(), args, &out); + QRegularExpression re( + "^\\[GNUPG:\\] VALIDSIG ([A-F0-9]{40}) .* ([A-F0-9]{40})\\r?$", + QRegularExpression::MultilineOption); + QRegularExpressionMatch m = re.match(out); + if (!m.hasMatch()) + return false; + QStringList fingerprints = m.capturedTexts(); + fingerprints.removeFirst(); + for (auto &key : signingKeys) { + if (fingerprints.contains(key)) + return true; + } + return false; +} + +/** * @brief ImitatePass::removeDir delete folder recursive. * @param dirName which folder. * @return was removal succesful? @@ -252,10 +345,21 @@ void ImitatePass::reencryptPath(const QString &dir) { QDir currentDir; QDirIterator gpgFiles(dir, QStringList() << "*.gpg", QDir::Files, QDirIterator::Subdirectories); + QStringList gpgIdFilesVerified; QStringList gpgId; while (gpgFiles.hasNext()) { QString fileName = gpgFiles.next(); if (gpgFiles.fileInfo().path() != currentDir.path()) { + QString gpgIdPath = Pass::getGpgIdPath(fileName); + if (!gpgIdFilesVerified.contains(gpgIdPath)) { + if (!verifyGpgIdFile(gpgIdPath)) { + emit critical(tr("Check .gpgid file signature!"), + tr("Signature for %1 is invalid.").arg(gpgIdPath)); + emit endReencryptPath(); + return; + } + gpgIdFilesVerified.append(gpgIdPath); + } gpgId = getRecipientList(fileName); gpgId.sort(); } diff --git a/src/imitatepass.h b/src/imitatepass.h index 4bfd3a94..d29a25d4 100644 --- a/src/imitatepass.h +++ b/src/imitatepass.h @@ -11,6 +11,7 @@ class ImitatePass : public Pass, private simpleTransaction { Q_OBJECT + bool verifyGpgIdFile(const QString &file); bool removeDir(const QString &dirName); void GitCommit(const QString &file, const QString &msg); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 60b3d0ca..960fc62d 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -766,7 +766,8 @@ void MainWindow::generateKeyPair(QString batch, QDialog *keygenWindow) { * select a more appropriate one to view too */ void MainWindow::updateProfileBox() { - QHash<QString, QString> profiles = QtPassSettings::getProfiles(); + QHash<QString, QHash<QString, QString>> profiles = + QtPassSettings::getProfiles(); if (profiles.isEmpty()) { ui->profileWidget->hide(); @@ -774,7 +775,7 @@ void MainWindow::updateProfileBox() { ui->profileWidget->show(); ui->profileBox->setEnabled(profiles.size() > 1); ui->profileBox->clear(); - QHashIterator<QString, QString> i(profiles); + QHashIterator<QString, QHash<QString, QString>> i(profiles); while (i.hasNext()) { i.next(); if (!i.key().isEmpty()) @@ -800,7 +801,10 @@ void MainWindow::on_profileBox_currentIndexChanged(QString name) { QtPassSettings::setProfile(name); - QtPassSettings::setPassStore(QtPassSettings::getProfiles()[name]); + QtPassSettings::setPassStore( + QtPassSettings::getProfiles().value(name).value("path")); + QtPassSettings::setPassSigningKey( + QtPassSettings::getProfiles().value(name).value("signingKey")); ui->statusBar->showMessage(tr("Profile changed to %1").arg(name), 2000); QtPassSettings::getPass()->updateEnv(); diff --git a/src/pass.cpp b/src/pass.cpp index f01e71f1..a8770307 100644 --- a/src/pass.cpp +++ b/src/pass.cpp @@ -245,8 +245,29 @@ void Pass::finished(int id, int exitCode, const QString &out, * switching profiles) */ void Pass::updateEnv() { - QStringList store = env.filter("PASSWORD_STORE_DIR="); + // put PASSWORD_STORE_SIGNING_KEY in env + QStringList envSigningKey = env.filter("PASSWORD_STORE_SIGNING_KEY="); + QString currentSigningKey = QtPassSettings::getPassSigningKey(); + if (envSigningKey.isEmpty()) { + if (!currentSigningKey.isEmpty()) { + // dbg()<< "Added + // PASSWORD_STORE_SIGNING_KEY with" + currentSigningKey; + env.append("PASSWORD_STORE_SIGNING_KEY=" + currentSigningKey); + } + } else { + if (currentSigningKey.isEmpty()) { + // dbg() << "Removed + // PASSWORD_STORE_SIGNING_KEY"; + env.removeAll(envSigningKey.first()); + } else { + // dbg()<< "Update + // PASSWORD_STORE_SIGNING_KEY with " + currentSigningKey; + env.replaceInStrings(envSigningKey.first(), + "PASSWORD_STORE_SIGNING_KEY=" + currentSigningKey); + } + } // put PASSWORD_STORE_DIR in env + QStringList store = env.filter("PASSWORD_STORE_DIR="); if (store.isEmpty()) { // dbg()<< "Added // PASSWORD_STORE_DIR"; @@ -261,27 +282,40 @@ void Pass::updateEnv() { } /** - * @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 + * @brief Pass::getGpgIdPath return gpgid file path for some file (folder). + * @param for_file which file (folder) would you like the gpgid file path for. + * @return path to the gpgid file. */ -QStringList Pass::getRecipientList(QString for_file) { - QDir gpgIdPath(QFileInfo(for_file.startsWith(QtPassSettings::getPassStore()) - ? for_file - : QtPassSettings::getPassStore() + for_file) - .absoluteDir()); +QString Pass::getGpgIdPath(QString for_file) { + QString passStore = + QDir::fromNativeSeparators(QtPassSettings::getPassStore()); + QDir gpgIdDir( + QFileInfo(QDir::fromNativeSeparators(for_file).startsWith(passStore) + ? for_file + : QtPassSettings::getPassStore() + for_file) + .absoluteDir()); bool found = false; - while (gpgIdPath.exists() && - gpgIdPath.absolutePath().startsWith(QtPassSettings::getPassStore())) { - if (QFile(gpgIdPath.absoluteFilePath(".gpg-id")).exists()) { + while (gpgIdDir.exists() && gpgIdDir.absolutePath().startsWith(passStore)) { + if (QFile(gpgIdDir.absoluteFilePath(".gpg-id")).exists()) { found = true; break; } - if (!gpgIdPath.cdUp()) + if (!gpgIdDir.cdUp()) break; } - QFile gpgId(found ? gpgIdPath.absoluteFilePath(".gpg-id") - : QtPassSettings::getPassStore() + ".gpg-id"); + QString gpgIdPath(found ? gpgIdDir.absoluteFilePath(".gpg-id") + : QtPassSettings::getPassStore() + ".gpg-id"); + + return gpgIdPath; +} + +/** + * @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(QString for_file) { + QFile gpgId(getGpgIdPath(for_file)); if (!gpgId.open(QIODevice::ReadOnly | QIODevice::Text)) return QStringList(); QStringList recipients; @@ -57,6 +57,7 @@ public: QList<UserInfo> listKeys(QStringList keystrings, bool secret = false); QList<UserInfo> listKeys(QString keystring = "", bool secret = false); void updateEnv(); + static QString getGpgIdPath(QString for_file); static QStringList getRecipientList(QString for_file); // TODO(bezet): getRecipientString is useless, refactor static QStringList getRecipientString(QString for_file, diff --git a/src/qtpasssettings.cpp b/src/qtpasssettings.cpp index b32a6525..e76ae886 100644 --- a/src/qtpasssettings.cpp +++ b/src/qtpasssettings.cpp @@ -58,13 +58,30 @@ void QtPassSettings::setPasswordConfiguration( config.Characters[PasswordConfiguration::CUSTOM]); } -QHash<QString, QString> QtPassSettings::getProfiles() { +QHash<QString, QHash<QString, QString>> QtPassSettings::getProfiles() { getInstance()->beginGroup(SettingsConstants::profile); - - QStringList childrenKeys = getInstance()->childKeys(); - QHash<QString, QString> profiles; - foreach (QString key, childrenKeys) { - profiles.insert(key, getInstance()->value(key).toString()); + QHash<QString, QHash<QString, QString>> profiles; + + // migration from version <= v1.3.2: profiles datastructure + QStringList childKeys = getInstance()->childKeys(); + if (!childKeys.empty()) { + foreach (QString key, childKeys) { + QHash<QString, QString> profile; + profile.insert("path", getInstance()->value(key).toString()); + profile.insert("signingKey", ""); + profiles.insert(key, profile); + } + } + // /migration from version <= v1.3.2 + + QStringList childGroups = getInstance()->childGroups(); + foreach (QString group, childGroups) { + QHash<QString, QString> profile; + profile.insert("path", getInstance()->value(group + "/path").toString()); + profile.insert("signingKey", + getInstance()->value(group + "/signingKey").toString()); + // profiles.insert(group, getInstance()->value(group).toString()); + profiles.insert(group, profile); } getInstance()->endGroup(); @@ -72,13 +89,16 @@ QHash<QString, QString> QtPassSettings::getProfiles() { return profiles; } -void QtPassSettings::setProfiles(const QHash<QString, QString> &profiles) { +void QtPassSettings::setProfiles( + const QHash<QString, QHash<QString, QString>> &profiles) { getInstance()->remove(SettingsConstants::profile); getInstance()->beginGroup(SettingsConstants::profile); - QHash<QString, QString>::const_iterator i = profiles.begin(); + QHash<QString, QHash<QString, QString>>::const_iterator i = profiles.begin(); for (; i != profiles.end(); ++i) { - getInstance()->setValue(i.key(), i.value()); + getInstance()->setValue(i.key() + "/path", i.value().value("path")); + getInstance()->setValue(i.key() + "/signingKey", + i.value().value("signingKey")); } getInstance()->endGroup(); @@ -303,6 +323,15 @@ void QtPassSettings::setPassStore(const QString &passStore) { getInstance()->setValue(SettingsConstants::passStore, passStore); } +QString QtPassSettings::getPassSigningKey(const QString &defaultValue) { + return getInstance() + ->value(SettingsConstants::passSigningKey, defaultValue) + .toString(); +} +void QtPassSettings::setPassSigningKey(const QString &passSigningKey) { + getInstance()->setValue(SettingsConstants::passSigningKey, passSigningKey); +} + void QtPassSettings::initExecutables() { QString passExecutable = QtPassSettings::getPassExecutable(Util::findBinaryInPath("pass")); diff --git a/src/qtpasssettings.h b/src/qtpasssettings.h index e3d5b3f0..d63f37fd 100644 --- a/src/qtpasssettings.h +++ b/src/qtpasssettings.h @@ -108,6 +108,10 @@ public: getPassStore(const QString &defaultValue = QVariant().toString()); static void setPassStore(const QString &passStore); + static QString + getPassSigningKey(const QString &defaultValue = QVariant().toString()); + static void setPassSigningKey(const QString &passSigningKey); + static void initExecutables(); static QString getPassExecutable(const QString &defaultValue = QVariant().toString()); @@ -210,8 +214,9 @@ public: isTemplateAllFields(const bool &defaultValue = QVariant().toBool()); static void setTemplateAllFields(const bool &templateAllFields); - static QHash<QString, QString> getProfiles(); - static void setProfiles(const QHash<QString, QString> &profiles); + static QHash<QString, QHash<QString, QString>> getProfiles(); + static void + setProfiles(const QHash<QString, QHash<QString, QString>> &profiles); static Pass *getPass(); static RealPass *getRealPass(); diff --git a/src/settingsconstants.cpp b/src/settingsconstants.cpp index 863d110f..00b004fd 100644 --- a/src/settingsconstants.cpp +++ b/src/settingsconstants.cpp @@ -32,6 +32,7 @@ const QString SettingsConstants::displayAsIs = "displayAsIs"; const QString SettingsConstants::noLineWrapping = "noLineWrapping"; const QString SettingsConstants::addGPGId = "addGPGId"; const QString SettingsConstants::passStore = "passStore"; +const QString SettingsConstants::passSigningKey = "passSigningKey"; const QString SettingsConstants::passExecutable = "passExecutable"; const QString SettingsConstants::gitExecutable = "gitExecutable"; const QString SettingsConstants::gpgExecutable = "gpgExecutable"; diff --git a/src/settingsconstants.h b/src/settingsconstants.h index bb237ab3..f95aeba2 100644 --- a/src/settingsconstants.h +++ b/src/settingsconstants.h @@ -31,6 +31,7 @@ public: const static QString noLineWrapping; const static QString addGPGId; const static QString passStore; + const static QString passSigningKey; const static QString passExecutable; const static QString gitExecutable; const static QString gpgExecutable; |