20#include <QRegularExpression>
68 const QString &input, QString *process_out,
69 QString *process_err) ->
int {
73 if (QThread::currentThread() == thread()) {
84 QString *process_out, QString *process_err)
86 return execBlocking(app, args, QString(), process_out, process_err);
90 const QString &exe)
const -> QString {
102 QStringList{QStringLiteral(
"-C"), store} + args, out,
134 {
"-C", pgit(m_settings.passStore),
"pull"},
nullptr, &err);
136 emit
statusMsg(tr(
"Git pull failed: %1").arg(err.trimmed()), 5000);
152 QStringList args = {
"-d",
"--quiet",
"--yes",
"--no-encrypt-to",
153 "--batch",
"--use-agent"};
167auto encryptArgs(QStringList leading,
const QString &output,
168 const QStringList &recipients) -> QStringList {
169 leading <<
"-eq" <<
"--compress-algo=none" <<
"--no-encrypt-to" <<
"--output"
171 for (
const QString &recipient : recipients) {
172 leading <<
"-r" << recipient;
181 QStringList *recipients) ->
bool {
189 emit
critical(tr(
"Check .gpg-id file signature!"), why);
192 if (recipients->isEmpty()) {
194 tr(
"Could not read encryption key to use, .gpg-id "
195 "file missing or invalid."));
205 QDir::cleanPath(QDir::isAbsolutePath(file)
216 const QFileInfo target(file);
217 if (!overwrite && (target.exists() || target.isSymLink())) {
218 emit
critical(tr(
"Cannot add"), tr(
"%1 already exists.").arg(file));
221 QStringList recipients;
228 auto scratch = std::make_shared<QTemporaryDir>();
229 if (!scratch->isValid()) {
231 tr(
"Cannot create a temporary directory: %1")
232 .arg(scratch->errorString()));
235 const QString output = scratch->filePath(QStringLiteral(
"entry.gpg"));
237 const QStringList args =
238 encryptArgs({
"--batch",
"--status-fd",
"2"},
pgpg(output), recipients);
251 gitCommit(file, QString(overwrite ?
"Edit" :
"Add") +
" for " + path +
261 {
"-C", pgit(m_settings.passStore),
"config",
262 "--bool",
"--get",
"pass.signcommits"},
264 out.trimmed() == QLatin1String(
"true");
265 return sign ? QStringList{
"commit",
"-S"} : QStringList{
"commit"};
269 if (file.isEmpty()) {
299 tr(
"Could not remove the link %1.").arg(file));
307 gitCommit(file,
"Remove for " + path +
" using QtPass.");
313 gitCommit(file,
"Remove for " + path +
" using QtPass.");
320 QFile(file).remove();
328 [
this](
const QString &app,
const QStringList &args,
329 const QString &input, QString *out, QString *err) {
336 QByteArray *contents) ->
bool {
337 const std::optional<QString> folder =
341 tr(
"%1 is not inside the password store, or a folder "
342 "name on the way holds a line break or another control "
343 "character, which a signed list cannot record.")
349 std::optional<qint64> verifiedOnDisk;
350 std::optional<QString> storeOnDisk;
352 if (signer.verifyFile(gpgIdFile, ¤t)) {
354 if (header && (!header->folder || *header->folder == *folder)) {
355 verifiedOnDisk = header->generation;
356 storeOnDisk = header->store;
362 const std::optional<QString> store =
365 emit
critical(tr(
"Cannot update"), why);
368 const std::optional<qint64> generation =
371 emit
critical(tr(
"Cannot update"), why);
380 const QList<UserInfo> &users,
381 QByteArray *written) ->
bool {
383 bool secret_selected =
false;
384 for (
const UserInfo &user : users) {
386 contents += (user.key_id +
"\n").toUtf8();
387 secret_selected |= user.have_secret;
399 emit
critical(tr(
"Cannot update"), writeError);
402 if (written !=
nullptr) {
411 qCWarning(lcQtPass) <<
"Could not record the written .gpg-id:" << why;
413 tr(
"Recipient list written, but not recorded"),
414 tr(
"%1 Save the recipients once more to get through.").arg(why));
417 if (!secret_selected) {
419 tr(
"Check selected users!"),
420 tr(
"None of the selected keys have a secret key available.\n"
421 "You will not be able to decrypt any newly added passwords!"));
427 const QByteArray &contents) ->
bool {
430 if (!signer.
sign(gpgIdFile, contents, &why)) {
432 tr(
"GPG signing failed!"),
433 why.trimmed().isEmpty()
434 ? tr(
"Failed to sign %1.").arg(gpgIdFile)
435 : tr(
"Failed to sign %1: %2").arg(gpgIdFile, why.trimmed()));
438 QByteArray signedBytes;
439 if (!signer.
verifyFile(gpgIdFile, &signedBytes)) {
440 emit
critical(tr(
"Check .gpg-id file signature!"),
441 tr(
"Signature for %1 is invalid.").arg(gpgIdFile));
451 const QString &gpgIdSigFile, QString *out,
452 QString *err) ->
int {
455 auto run = [&](
const QStringList &args) ->
int {
459 execBlocking(git, QStringList{
"-C", store} + args, &runOut, &runErr);
460 if (out !=
nullptr) {
463 if (err !=
nullptr) {
468 QStringList paths{
pgit(gpgIdFile)};
469 if (!gpgIdSigFile.isEmpty()) {
470 paths <<
pgit(gpgIdSigFile);
473 int rc = run(QStringList{
"add",
"--"} + paths);
480 rc = run(QStringList{
"diff",
"--cached",
"--quiet",
"--"} + paths);
487 QString commitPath = gpgIdFile;
490 QStringList{
"-m",
"Added " + commitPath +
" using QtPass.",
"--"} +
496 {
"-C", pgit(m_settings.passStore),
"ls-files",
497 "--error-unmatch",
"--", pgit(file)}) == 0;
503 const QString folder = QDir::cleanPath(path);
505 const auto refusePlaced = [
this](
const QString &placed) {
509 return refusePlaced(path) ||
510 refusePlaced(folder + QStringLiteral(
"/.gpg-id")) ||
511 refusePlaced(folder + QStringLiteral(
"/.gpg-id.sig"));
515 const QByteArray &written,
517 QString *sigToCommit) ->
bool {
518 const QString gpgIdSigFile = gpgIdFile +
".sig";
519 if (signer.enabled()) {
523 *sigToCommit = gpgIdSigFile;
526 if (!QFile::exists(gpgIdSigFile)) {
530 const bool tracked = useGit &&
gitTracks(gpgIdSigFile);
531 if (!QFile::remove(gpgIdSigFile)) {
534 tr(
"Failed to remove the old signature %1.").arg(gpgIdSigFile));
538 *sigToCommit = gpgIdSigFile;
549 if (!QDir::isAbsolutePath(path)) {
574 emit
critical(tr(
"Recipients not changed"),
575 tr(
"A re-encryption is still running. Wait until it has "
576 "finished, then save the recipients again."));
581 emit
critical(tr(
"No signing key!"),
582 tr(
"None of the secret signing keys is available.\n"
583 "You will not be able to change the user list!"));
588 const QString gpgIdFile = path +
".gpg-id";
602 const int gitExit =
gitAddGpgId(gpgIdFile, sigToCommit, &gitOut, &gitErr);
610 const QString why = gitErr.trimmed();
613 why.isEmpty() ? tr(
"Git exited with code %1.").arg(gitExit) : why);
625 QStringList *recipients, QString *why)
628 if (why !=
nullptr) {
629 *why = tr(
"Signature for %1 is invalid.").arg(gpgIdFile);
643 if (
const QFileInfo info(gpgIdFile); !info.exists() && !info.isSymLink()) {
648 std::optional<QByteArray> bytes =
651 if (why !=
nullptr) {
652 *why = tr(
"%1 cannot be read as a recipient list (a regular file "
659 contents = *std::move(bytes);
666 emit
critical(tr(
"Leftover from an earlier re-encryption"),
667 tr(
"%1 is not a regular file and was not restored. Look "
668 "at it and remove it, then re-encrypt again.")
674 if (QFileInfo(path).lastModified() >
675 QDateTime::currentDateTime().addSecs(-3600)) {
676 qCDebug(lcQtPass) <<
"Leaving a recent temporary alone:" << path;
679 if (!QFile::remove(path)) {
680 qCWarning(lcQtPass) <<
"Could not remove stale temporary" << path;
686 const QFileInfo backup(path);
687 if (backup.isSymLink() || !backup.isFile()) {
691 const QString original =
692 path.chopped(QStringLiteral(
".reencrypt.bak").size());
693 if (QFileInfo::exists(original)) {
694 emit
critical(tr(
"Leftover from an earlier re-encryption"),
695 tr(
"%1 exists next to %2. Both are encrypted copies of the "
696 "entry; check which one you want and delete the other, "
697 "then re-encrypt again.")
698 .arg(path, original));
701 if (!QFile::rename(path, original)) {
702 emit
critical(tr(
"Leftover from an earlier re-encryption"),
703 tr(
"%1 is missing and its backup %2 could not be renamed "
704 "back. Rename it by hand, then re-encrypt again.")
705 .arg(original, path));
708 qCWarning(lcQtPass) <<
"Restored" << original <<
"from" << path;
709 emit
statusMsg(tr(
"Restored %1 from the backup an interrupted "
710 "re-encryption left behind.")
725 const QStringList leftoverNames{QStringLiteral(
".qtpass-??????.tmp"),
726 QStringLiteral(
"*.gpg.reencrypt.tmp"),
727 QStringLiteral(
"*.gpg.??????.tmp"),
728 QStringLiteral(
"*.gpg.reencrypt.bak")};
729 const auto isTemporary = [](
const QString &path) {
730 return path.endsWith(QStringLiteral(
".tmp"));
735 QDir::cleanPath(dir), leftoverNames, &skipped,
true);
737 for (
const QString &path : skipped) {
738 if (!QDir::match(leftoverNames, QFileInfo(path).fileName())) {
744 if (isTemporary(path)) {
745 if (!QFile::remove(path) && !QDir().rmdir(path)) {
746 qCWarning(lcQtPass) <<
"Could not remove stale temporary" << path;
753 for (
const QString &path : leftovers) {
754 if (isTemporary(path)) {
764 QHash<QString, QStringList> &verified,
765 QStringList &gpgId) ->
bool {
774 const auto cached = verified.constFind(gpgIdPath);
775 if (cached != verified.constEnd()) {
776 gpgId = cached.value();
779 QStringList recipients;
784 emit
critical(tr(
"Check .gpg-id file signature!"), why);
788 verified.insert(gpgIdPath, recipients);
795 "-v",
"--no-secmem-warning",
"--no-permission-warning",
796 "--list-only",
"--keyid-format=long",
"--",
804 QStringList actualKeys;
807 QListIterator<QString> itr(key);
808 while (itr.hasNext()) {
809 QString current = itr.next();
810 QStringList cur = current.split(
" ");
811 if (cur.length() > 4) {
812 QString actualKey = cur.takeAt(4);
813 if (actualKey.length() == 16) {
814 actualKeys << actualKey;
824 const QStringList args =
825 QStringList{
"-d",
"--quiet",
"--yes",
"--no-encrypt-to",
826 "--batch",
"--use-agent"} +
829 plaintext->isEmpty()) {
830 qCDebug(lcQtPass) <<
"Decrypt error on re-encrypt for:" << fileName;
833 if (!plaintext->endsWith(u
'\n')) {
834 plaintext->append(u
'\n');
840 const QStringList &recipients,
841 const QString &plaintext) ->
bool {
842 const QStringList args =
843 encryptArgs({
"--yes",
"--batch"},
pgpg(output), recipients);
845 qCDebug(lcQtPass) <<
"Encrypt error on re-encrypt, output:" << output;
852 const QString &plaintext) ->
bool {
854 const QStringList args =
855 QStringList{
"-d",
"--quiet",
"--batch",
"--use-agent"} +
maxOutputArgs() +
856 QStringList{QStringLiteral(
"--"),
pgpg(ciphertext)};
858 decrypted.isEmpty()) {
859 qCDebug(lcQtPass) <<
"Verification failed for:" << ciphertext;
863 if (decrypted.trimmed() != plaintext.trimmed()) {
864 qCDebug(lcQtPass) <<
"Verification content mismatch for:" << ciphertext;
878 {
"-C", store,
"add",
"--", pgit(fileName)}) != 0) {
882 qCDebug(lcQtPass) <<
"git add failed after re-encrypting:" << fileName;
885 QString path = QDir(
m_settings.passStore).relativeFilePath(fileName);
890 "Re-encrypt for " + path +
" using QtPass.",
891 "--",
pgit(fileName)}) != 0) {
892 qCDebug(lcQtPass) <<
"git commit failed after re-encrypting:" << fileName;
899 const QStringList &recipients,
900 QString *why) ->
bool {
901 qCDebug(lcQtPass) <<
"reencrypt" << fileName <<
"for" << recipients.size()
903 if (recipients.isEmpty()) {
905 tr(
"Could not read encryption key to use, .gpg-id "
906 "file missing or invalid."));
917 QTemporaryDir scratch;
918 if (!scratch.isValid()) {
919 qCDebug(lcQtPass) <<
"Cannot create a scratch directory for re-encrypting"
923 const QString tempPath = scratch.filePath(QStringLiteral(
"reencrypted.tmp"));
924 if (!
encryptFor(tempPath, recipients, plaintext) ||
932 const QFileInfo still(fileName);
933 if (!still.exists() && !still.isSymLink()) {
934 qCDebug(lcQtPass) <<
"Entry vanished before it could be replaced:"
942 if (why !=
nullptr && !why->contains(fileName)) {
945 *why = tr(
"%1 could not be re-encrypted: %2").arg(fileName, *why);
956 emit
statusMsg(tr(
"Creating backup commit"), 2000);
965 git, {
"-C", store,
"status",
"--porcelain",
"--untracked-files=no"},
970 tr(
"Backup commit failed"),
971 tr(
"Could not inspect git status. Re-encryption was aborted."));
974 if (!statusOut.trimmed().isEmpty()) {
975 if (
execBlocking(git, {
"-C", store,
"add",
"-u"}) != 0 ||
977 git, QStringList{
"-C", store} +
commitArgs() +
978 QStringList{
"-m",
"Backup before re-encryption"}) != 0) {
980 emit
critical(tr(
"Backup commit failed"),
981 tr(
"Re-encryption was aborted because a git backup "
982 "could not be created."));
1007constexpr int kReencryptRetryMs = 100;
1009constexpr int kReencryptMaxListedFailures = 15;
1023 emit
statusMsg(tr(
"A re-encryption is already running"), 3000);
1030 emit
critical(tr(
"Not a folder of the store"),
1031 tr(
"%1 is, or lies behind, a symbolic link or junction. What "
1032 "that points to is not part of the password store and "
1033 "was not re-encrypted.")
1034 .arg(QDir::toNativeSeparators(QDir::cleanPath(dir))));
1046 emit
statusMsg(tr(
"Re-encrypting from folder %1").arg(dir), 3000);
1070 QTimer::singleShot(kReencryptRetryMs,
this,
1090 QPointer<ImitatePass> self(
this);
1094 QThread *thread = QThread::create([
this, self, dir]() {
1096 QMetaObject::invokeMethod(
1098 [self, result = std::move(result)]() {
1100 self->finishReencrypt(result);
1102 Qt::QueuedConnection);
1105 connect(thread, &QThread::finished,
this, [
this, thread]() {
1109 connect(thread, &QThread::finished, thread, &QObject::deleteLater);
1122 emit
statusMsg(tr(
"Updating password-store"), 2000);
1124 {
"-C", pgit(m_settings.passStore),
"pull"}) == 0) {
1132 {
"-C", pgit(m_settings.passStore),
"ls-files",
"--unmerged"},
1135 emit
critical(tr(
"Git pull failed"),
1136 tr(
"Could not check the store for unmerged files after the "
1137 "pull. Check it with git before re-encrypting."));
1140 if (!unmerged.trimmed().isEmpty()) {
1141 emit
critical(tr(
"Git pull failed"),
1142 tr(
"The pull left the store with unmerged files. Resolve "
1143 "the conflict before re-encrypting."));
1146 emit
statusMsg(tr(
"Git pull failed, re-encrypting the store as it is"), 5000);
1153 QStringList skipped;
1154 const QStringList files =
1156 if (!skipped.isEmpty()) {
1157 emit
statusMsg(tr(
"%n entr(y/ies) skipped: a symlink, junction or special "
1158 "file is not part of the store.",
1159 "",
static_cast<int>(skipped.size())),
1166 QHash<QString, QStringList> &verified,
1171 result.cancelled =
true;
1173 result.aborted =
true;
1177 if (gpgId.isEmpty() && !verified.isEmpty()) {
1178 emit
critical(tr(
"GPG ID verification failed"),
1179 tr(
"Could not verify .gpg-id for directory."));
1180 result.aborted =
true;
1203 emit
critical(tr(
"Unfinished Git operation"), midOp);
1212 emit
critical(tr(
"Not part of the store"),
1213 tr(
"%1 is, or lies behind, a symbolic link or junction "
1214 "after pulling. What that points to is not part of the "
1215 "password store and is left alone.")
1216 .arg(QDir::toNativeSeparators(QDir::cleanPath(dir))));
1238 result.
total = files.size();
1242 QHash<QString, QStringList> gpgIdFilesVerified;
1244 for (
const QString &fileName : std::as_const(files)) {
1249 const QString fileDir = QFileInfo(fileName).path();
1250 if (fileDir != currentDir) {
1254 currentDir = fileDir;
1266 result.
failed << (why.isEmpty() ? fileName : why);
1278 if (!result.
failed.isEmpty()) {
1279 QStringList listed = result.
failed.mid(0, kReencryptMaxListedFailures);
1280 const int more = result.
failed.size() - listed.size();
1282 listed << tr(
"... and %n more",
nullptr, more);
1284 emit
critical(tr(
"Re-encryption failed"),
1285 tr(
"%n file(s) could not be re-encrypted:",
nullptr,
1287 "\n\n" + listed.join(
'\n'));
1291 emit
statusMsg(tr(
"Re-encryption cancelled: %1 of %2 files checked, "
1292 "%3 re-encrypted, %4 failed")
1296 .arg(result.
failed.size()),
1299 if (!result.
failed.isEmpty()) {
1300 emit
statusMsg(tr(
"Re-encryption completed: %1 succeeded, %2 failed")
1302 .arg(result.
failed.size()),
1305 emit
statusMsg(tr(
"Re-encryption completed: %1 files re-encrypted")
1310 if (result.
failed.isEmpty()) {
1311 emit
statusMsg(tr(
"Updating password-store"), 2000);
1314 emit
statusMsg(tr(
"Not pushing: %n file(s) failed to re-encrypt",
1315 nullptr, result.
failed.size()),
1325 result.
failed.isEmpty());
1336 const QString &gitError) {
1353 const QString &gitError,
1355 const QString folder = QDir::toNativeSeparators(QDir::cleanPath(dir));
1358 tr(
"Recipients not committed"),
1359 tr(
"The recipients of %1 were changed and its entries were "
1360 "re-encrypted to them, but the Git commit failed:\n\n%2\n\n"
1361 "The change is not committed or pushed yet. Fix the Git problem, "
1362 "then commit the changes, or save the recipients again.")
1363 .arg(folder, gitError));
1367 tr(
"Recipients not committed"),
1368 tr(
"The recipients of %1 were changed, but the Git commit "
1369 "failed:\n\n%2\n\nRe-encryption did not complete, so some or all "
1370 "entries are still encrypted to the previous recipients, who can "
1371 "still read them. Nothing was committed or pushed. Fix the Git "
1372 "problem, then save the recipients again.")
1373 .arg(folder, gitError));
1377 const QString &dest,
bool force)
1379 QFileInfo srcFileInfo(src);
1380 QFileInfo destFileInfo(dest);
1382 QString srcFileBaseName = srcFileInfo.fileName();
1384 if (srcFileInfo.isFile()) {
1385 if (destFileInfo.isFile()) {
1387 qCDebug(lcQtPass) <<
"Destination file already exists";
1391 }
else if (destFileInfo.isDir()) {
1392 destFile = QDir(dest).filePath(srcFileBaseName);
1401 if (destFile.endsWith(QLatin1String(
".gpg"))) {
1404 destFile.append(
".gpg");
1405 }
else if (srcFileInfo.isDir()) {
1406 if (destFileInfo.isDir()) {
1407 destFile = QDir(dest).filePath(srcFileBaseName);
1408 }
else if (destFileInfo.isFile()) {
1409 qCDebug(lcQtPass) <<
"Destination is a file";
1415 qCDebug(lcQtPass) <<
"Source file does not exist";
1428 args <<
"--" <<
pgit(src) <<
pgit(destFile);
1435 QString message = QString(
"Moved for %1 to %2 using QtPass.");
1436 message = message.arg(relSrc, relDest);
1450 if (destFile.isEmpty()) {
1457 qCDebug(lcQtPass) <<
"Move Source: " << src;
1458 qCDebug(lcQtPass) <<
"Move Destination: " << destFile;
1465 qDir.remove(destFile);
1467 qDir.rename(src, destFile);
1472 bool force) -> QString {
1473 const auto refuse = [
this, &src](
const QString &to) {
1475 tr(
"Could not copy %1 to %2.").arg(src, to));
1482 if (destFile.isEmpty()) {
1483 return refuse(dest);
1490 const QFileInfo destFileInfo(destFile);
1494 if (QFileInfo(src) == destFileInfo || (!force && destFileInfo.exists())) {
1495 return refuse(destFile);
1512 if (destFile.isEmpty()) {
1522 tr(
"Could not copy %1 to %2.").arg(src, destFile) +
"\n" +
1529 QString(
"Copied from %1 to %2 using QtPass.").arg(src, destFile));
1531 const QFileInfo written(destFile);
1532 if (written.isDir()) {
1534 }
else if (written.isFile()) {
1540 bool readStdout,
bool readStderr) {
1542 readStdout, readStderr);
1555 emit
statusMsg(tr(
"Git executable not configured, skipping git"), 3000);
1562 bool readStdout,
bool readStderr) {
1566 readStdout, readStderr);
1572 const QString &err) {
1573 qCDebug(lcQtPass) <<
"Imitate Pass";
1574 QString error = err;
1586 if (exitCode == 0) {
1594 if (!midOp.isEmpty()) {
1609 if (exitCode == 0) {
1618 qCDebug(lcQtPass) <<
"No such transaction!";
1629 bool overwrite, QString *error) ->
bool {
1630 if (QFileInfo(output).size() <= 0) {
1632 *error = tr(
"gpg wrote no ciphertext for %1.").arg(file);
auto isIdle() const -> bool
Whether no command is running and nothing is queued.
static auto executeBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Run a command synchronously and return its exit code.
static auto translatePathForWsl(const QString &path, const QString &exe) -> QString
Translate a native path for a WSL-routed executable.
auto environment() const -> QProcessEnvironment
Return the environment passed to child processes.
auto cancelNext() -> int
Cancel the next queued command without executing it.
static auto folderOf(const QString &gpgIdFile, const QString &storeRoot) -> std::optional< QString >
The store-relative folder of gpgIdFile, as written in and compared against the folder line: ....
static auto reserveNext(const QString &gpgIdFile, std::optional< qint64 > verifiedOnDisk, QString *error=nullptr) -> std::optional< qint64 >
Reserve the generation to write next for gpgIdFile: one above both what this device remembers and ver...
static auto recordWritten(const QString &gpgIdFile, const QByteArray &contents, QString *error=nullptr) -> bool
Record the bytes just written for gpgIdFile, so that this device recognises its own list afterwards a...
static auto withHeader(qint64 generation, const QString &folder, const QByteArray &recipients, const QString &store={}) -> QByteArray
A complete list: the header in front of the recipients.
static auto parse(const QByteArray &contents, QString *error=nullptr) -> std::optional< Header >
The header a .gpg-id's bytes declare.
static auto storeIdFor(const QString &storeRoot, const std::optional< QString > &onDisk, QString *error=nullptr) -> std::optional< QString >
The store id to write into a list of storeRoot: the one this device knows the store by; else onDisk,...
The detached signature on a store's .gpg-id: whether the configured signing key is usable,...
auto enabled() const -> bool
Whether any signing key is configured at all.
auto sign(const QString &gpgIdFile, const QByteArray &contents, QString *error=nullptr) const -> bool
Write <file>.sig, a detached signature by the first key over contents, the bytes the caller just wrot...
auto verifyFile(const QString &gpgIdFile, QByteArray *contents) const -> bool
Read gpgIdFile and verify it against <file>.sig.
static auto keysFromSetting(const QString &passSigningKey) -> QStringList
Split the settings value into keys, upper-cased the way gpg prints fingerprints.
auto haveSecretKey() const -> bool
Whether gpg has the secret key that sign() will use, i.e. the first configured key.
RAII helper for wrapping operations in transactions.
auto commitReencrypted(const QString &fileName) -> bool
Stage and commit a re-encrypted entry, when the store is a Git repository QtPass manages.
auto encryptFor(const QString &output, const QStringList &recipients, const QString &plaintext) -> bool
Encrypt plaintext to recipients, into output.
auto createBackupCommit() -> bool
Create git backup commit before re-encryption.
void finishReencrypt(const ReencryptResult &result)
Report the outcome of a run and emit endReencryptPath().
void Remove(QString file, bool isDir) override
Remove password.
QString m_transactionOutput
stdout of the steps of the transaction in flight, collected by finished() and handed on,...
auto execBlocking(const QString &app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Executor::executeBlocking() for the re-encryption helpers.
void GitInit() override
Initialize Git repository.
auto recipientsForEntry(const QString &file, QStringList *recipients) -> bool
The recipients to encrypt file to: its folder's .gpg-id, verified when a signing key is set.
void executeGpg(PROCESS id, const QStringList &args, QString input=QString(), bool readStdout=true, bool readStderr=true)
Execute GPG command.
auto ciphertextHolds(const QString &ciphertext, const QString &plaintext) -> bool
Decrypt ciphertext again and compare it with plaintext, so nothing replaces an entry until it is know...
auto reencryptSingleFile(const QString &fileName, const QStringList &recipients, QString *why) -> bool
Re-encrypt single file with new recipients.
void reportUncommittedGpgId(const QString &dir, const QString &gitError, bool reencrypted)
Tell the user that the recipients of dir changed on disk but the change is not committed,...
auto copyDestination(const QString &src, const QString &dest, bool force) -> QString
The file a copy of src to dest writes: like pass cp, into an existing folder with ....
bool m_reencryptActive
Set while a re-encryption run is pending or on the worker thread.
void startReencryptWorker(const QString &dir)
Start the worker thread once the Executor queue is idle.
auto entriesToReencrypt(const QString &dir) -> QStringList
The entries under dir a run considers, telling the user about anything skipped for not being a regula...
auto writeGpgIdFile(const QString &gpgIdFile, const QList< UserInfo > &users, QByteArray *written=nullptr) -> bool
Write the enabled recipients to a .gpg-id, atomically: a write that fails halfway (full disk,...
auto commitArgs() -> QStringList
The start of a git commit for the store: "commit", and "-S" when git config pass.signcommits is true ...
auto verifyGpgIdForDir(const QString &file, QHash< QString, QStringList > &verified, QStringList &gpgId) -> bool
Recipients for the directory of file, from its verified .gpg-id.
void Grep(QString pattern, bool caseInsensitive) override
Search all password content by GPG-decrypting each .gpg file.
void Insert(QString file, QString newValue, bool overwrite) override
Insert new password.
auto recipientsForDir(const QString &fileName, QHash< QString, QStringList > &verified, QStringList &gpgId, ReencryptResult &result) -> bool
The verified recipients for the folder fileName sits in.
auto gitTracks(const QString &file) -> bool
Check whether git already tracks a file in the store.
void GitPull_b() override
Pull with rebase.
~ImitatePass() override
Cancel a running content search, and cancel and join a running re-encryption worker,...
void cancelReencryptPath()
Ask a running reencryptPath() to stop.
ImitatePass()
Construct the backend and connect its content search.
auto gitReady() -> bool
gitConfigured() plus a status message when git is enabled but no executable is configured.
void executeGit(PROCESS id, const QStringList &args, QString input=QString(), bool readStdout=true, bool readStderr=true)
Execute git command.
void Init(QString path, const QList< UserInfo > &users) override
Initialize store.
auto loadVerifiedRecipients(const QString &gpgIdFile, QStringList *recipients, QString *why=nullptr) -> bool
Read a .gpg-id, verify its detached signature and parse the recipients from the bytes that were verif...
void Copy(const QString src, const QString dest, const bool force) override
Copy password file.
QThread * m_reencryptThread
The re-encryption worker while it runs, null otherwise. The destructor sets m_reencryptCancel and joi...
auto reencryptFiles(const QString &dir) -> ReencryptResult
Worker-thread body: scan dir and re-encrypt stale files.
auto placeEncryptedFile(const QString &output, const QString &file, bool overwrite, QString *error) -> bool
Bring the ciphertext gpg wrote to output into the store as file (Util::copyFileReplacing): through a ...
auto settleSignature(const QString &gpgIdFile, const QByteArray &written, const GpgIdSigner &signer, bool useGit, QString *sigToCommit) -> bool
Bring the signature in line with the list just written: a new one when signing is on,...
SimpleTransaction m_transaction
auto signGpgIdFile(const QString &gpgIdFile, const QByteArray &contents) -> bool
Sign a .gpg-id with the configured key and verify the result; failures are reported through critical(...
QProcessEnvironment m_reencryptEnv
void reencryptProgress(int current, int total)
Re-encryption progress: current of total files checked.
void executeMoveGit(const QString &src, const QString &destFile, bool force)
Execute git move operation.
void endReencryptPath()
A reencryptPath() run is over, whether it finished, failed, was cancelled or was refused; the main wi...
auto translatePathForWsl(const QString &path, const QString &exe) const -> QString
Translate path for the given exe when WSL-routed: wraps in wslpath substitution, otherwise returns pa...
void finished(int id, int exitCode, const QString &out, const QString &err) override
Handle process completion.
void reencryptWithoutGit(const QString &dir, const QString &gitError)
Re-encrypt dir to the .gpg-id Init() wrote although its commit failed, without any Git step (they wou...
auto addGenerationHeader(const QString &gpgIdFile, const GpgIdSigner &signer, QByteArray *contents) -> bool
Put the generation header on a list that is about to be signed: reserve a generation above what this ...
auto gitAddGpgId(const QString &gpgIdFile, const QString &gpgIdSigFile, QString *out, QString *err) -> int
Stage a .gpg-id and, when given, its .sig, and commit both in one commit, so no commit in the history...
std::optional< UncommittedGpgId > m_uncommittedGpgId
std::atomic< bool > m_reencryptCancel
void GitPull() override
Pull from remote.
auto refuseLinkedGpgIdFolder(const QString &path) -> bool
Refuse a folder, or a .gpg-id/.gpg-id.sig in it, that is a link: writing there would reach outside th...
void Show(QString file) override
Show decrypted password.
auto pgpg(const QString &path) const -> QString
Translate path for gpg when WSL-routed: wraps in wslpath substitution, otherwise returns path unchang...
auto storeGit(const QStringList &args, QString *out) -> bool override
Run git with -C <store> in the store, blocking.
QQueue< PendingInsert > m_pendingInserts
The inserts whose gpg step has not ended yet, oldest first.
NativeGrep m_grep
Background search over the decrypted store; relays to finishedGrep.
auto resolveMoveDestination(const QString &src, const QString &dest, bool force) -> QString
Resolve destination for move operation.
auto recoverReencryptLeftovers(const QString &dir) -> bool
Deal with what a crashed or failed earlier run left under dir before touching anything: stale tempora...
void GitPush() override
Push to remote.
void Move(const QString src, const QString dest, const bool force) override
Move password file.
auto restoreBackup(const QString &path) -> bool
Put a 1.8.x backup back in its entry's place when the entry is missing; report it when both exist (bo...
void dropStaleTemporary(const QString &path)
Remove a stale temporary a crashed run left, unless it was modified within the hour: on a shared stor...
void startReencryptPath()
A reencryptPath() run is about to start its worker thread.
auto decryptEntry(const QString &fileName, QString *plaintext) -> bool
Decrypt fileName for re-encryption.
auto getKeysFromFile(const QString &fileName) -> QStringList
Read recipients from file.
auto reencryptUsesGit() const -> bool
gitConfigured(), unless the current run is one reencryptWithoutGit() started.
auto gitConfigured() const -> bool
Git is enabled and an executable is configured.
void reencryptPath(const QString &dir)
Re-encrypt entire directory.
auto pgit(const QString &path) const -> QString
Translate path for git when WSL-routed: wraps in wslpath substitution, otherwise returns path unchang...
void reportUnrestorable(const QString &path)
Tell the user a leftover is not a regular file and was left.
auto pullBeforeReencrypt() -> bool
Pull before a run, when the store is a Git repository and automatic pulling is on.
void gitCommit(const QString &file, const QString &msg)
Commit changes to git.
void beforeExecute(PROCESS id) override
Open a transaction for every execution.
auto gpgIdSigner() -> GpgIdSigner
The signer for this store's .gpg-id, bound to the configured gpg and signing keys and to execBlocking...
void finished(const QList< QPair< QString, QStringList > > &results)
The newest search is over.
void search(const QString &pattern, bool caseInsensitive, const QString &gpgExe, const QString &storeDir, const QProcessEnvironment &env)
Start a search; finished() follows asynchronously, also for an empty or invalid pattern (with no resu...
void cancel()
Ask running searches to stop without waiting for them: the walk ends at the next file and a gpg that ...
auto storeEnvironment() const -> QProcessEnvironment
The environment QtPass runs the store's processes in.
AppSettings m_settings
Application settings snapshot injected at init() time.
static auto getGpgIdPath(const QString &for_file, const QString &passStore) -> QString
Get .gpg-id file path for a password file.
auto refusePendingPull() -> bool
Refuse a store change while a Git pull is still queued or running.
Enums::PROCESS PROCESS
Alias for Enums::PROCESS used throughout this class.
void queueShow(const QString &file)
Remember what a Show() asked for, so its completion can say so.
void statusMsg(const QString &msg, int timeout)
Progress or a non-fatal problem worth a line in the status bar ("Creating backup commit",...
auto refuseSkippedGpgId(const QString &file, bool signing, const QString &except=QString()) -> bool
With signing on, refuse to encrypt file when the lookup passed over its folder's own list (skippedGpg...
auto unfinishedGitOperationReason() -> QString
The unfinished git operation in progress in the store, if any.
@ PlacedByCaller
Through the target of a linked store root, by a caller that places such a folder itself (Init()): not...
static auto maxOutputArgs() -> QStringList
The gpg options that enforce kMaxPlaintextBytes.
void executeWrapper(PROCESS id, const QString &app, const QStringList &args, bool readStdout=true, bool readStderr=true)
Execute external wrapper command.
auto refuseLinkedPath(const QString &path, LinkScope scope=LinkScope::EntryAndParents, PathSpelling spelling=PathSpelling::Configured) -> bool
Refuse an operation on something that is, or lies behind, a symbolic link or NTFS junction inside the...
void finishedGrep(const QList< QPair< QString, QStringList > > &results)
The newest Grep() is over; an empty list means no entry matched.
Executor exec
Internal command executor for queuing and running subprocesses.
auto verifySignedList(const GpgIdSigner &signer, const QString &gpgIdFile, QByteArray *contents, QString *why) const -> bool
Whether gpgIdFile is a signed list QtPass accepts: its signature verifies (GpgIdSigner::verifyFile())...
void init(const AppSettings &settings)
Initialize the Pass instance with a settings snapshot.
auto withCurrentGitPin(QProcessEnvironment base) const -> QProcessEnvironment
base with GIT_DIR and GIT_WORK_TREE as the store is now (Util::gitStoreVariables()),...
void updateEnv()
Update environment for subprocesses.
virtual void finished(int id, int exitCode, const QString &out, const QString &err)
Handle process completion.
void critical(const QString &title, const QString &message)
Something went wrong that the user has to be told about in a dialog, such as a refused path or a fail...
@ ParentsOnly
Only the folders above it: a removal unlinks a link and touches nothing behind it.
@ EntryAndParents
The entry itself and every folder above it in the store.
auto refuseUnfinishedGitOperation() -> bool
Refuse to change the store while git is mid-rebase, -merge, -cherry-pick or -revert.
void queueInsert()
Count one insert that has started queueing but not yet reported.
static auto parseRecipients(const QByteArray &contents, const QString &sourceName) -> QStringList
Parse the contents of a .gpg-id: one recipient per line, # starts a comment, unusable lines are logge...
auto refuseFolderOutsideStore(const QString &folder, QString *relative=nullptr) -> bool
Refuse recipients for a folder outside the store, as pass refuses a path with .. in it.
void transactionAdd(Enums::PROCESS process)
Note that process has been queued.
auto transactionIsOver(Enums::PROCESS current) -> Enums::PROCESS
Tell whether the process that just finished ends the oldest pending transaction, and take that transa...
static auto regularFilesUnder(const QString &dir, const QStringList &nameFilters, QStringList *skipped=nullptr, bool hiddenFiles=false) -> QStringList
Regular files under dir whose name matches one of nameFilters, found by walking only real directories...
static auto removeTree(const QString &dir) -> bool
Remove dir and everything in it, the way rm -rf does: a symbolic link or NTFS junction,...
static auto endsWithGpg() -> const QRegularExpression &
Returns a regex to match .gpg file extensions.
static auto isUnderLink(const QString &path, const QString &storeRoot, bool includeSelf=true) -> bool
Whether the way from storeRoot down to path passes through a symbolic link or NTFS junction: path its...
static auto writeFileReplacing(const QString &path, const QByteArray &bytes, bool replace, QString *error=nullptr) -> bool
Write bytes as the file path through stageFileReplacing().
static constexpr qint64 kMaxGpgIdBytes
Most bytes of a .gpg-id QtPass reads.
static auto readRegularFile(const QString &path, qint64 maxBytes) -> std::optional< QByteArray >
Read path whole, as the regular file it is (openRegularFile()), when it holds at most maxBytes.
static auto isLinkedFolder(const QString &path) -> bool
Whether path names a symbolic link or NTFS junction, with or without a trailing separator (which woul...
static auto copyFileReplacing(const QString &src, const QString &dst, bool replace, QString *error=nullptr) -> bool
Copy the regular file src to the name dst through stageFileReplacing(): src is read through a handle ...
static auto newLinesRegex() -> const QRegularExpression &
Returns a regex to match newline characters.
static auto normalizeFolderPath(const QString &path) -> QString
Ensure a folder path always ends with '/'.
Executor, the FIFO queue every gpg, git and pass child process of QtPass goes through,...
Rollback detection for signed .gpg-id files: the generation counter and the record of what was last s...
ImitatePass, the backend that does what pass does by running gpg and git itself.
The "qtpass" logging category.
Plain value-object holding all flat QtPass configuration.
QString gpgExecutable
Path to the gpg/gpg2 executable.
QString passStore
Password-store root path.
QString gitExecutable
Path to the git executable.
bool useGit
Enable git integration.
bool addGPGId
Auto-add .gpg-id files to git.
QString file
The entry's path in the store, where output goes.
bool overwrite
Whether an entry already at file may be replaced.
QString output
The ciphertext file gpg writes, inside scratch.
Outcome of one reencryptPath() run; filled on the worker thread and consumed by finishReencrypt() on ...
int reencrypted
Files rewritten for the current recipients.
int checked
Files whose recipients were inspected.
int total
.gpg files found under the directory.
bool aborted
Stopped early on an error already reported.
A .gpg-id Init() wrote but could not commit.
QString folder
The folder whose recipients changed.
QString gitError
What Git said when the commit failed.
Stores GPG key info including validity, creation date, and more.
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...