QtPass 1.5.1
Multi-platform GUI for pass, the standard unix password manager.
Loading...
Searching...
No Matches
imitatepass.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2016 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
3#include "imitatepass.h"
4#include "qtpasssettings.h"
5#include "util.h"
6#include <QDirIterator>
7#include <QRegularExpression>
8#include <utility>
9
10#ifdef QT_DEBUG
11#include "debughelper.h"
12#endif
13
17using Enums::GIT_ADD;
19using Enums::GIT_COPY;
20using Enums::GIT_INIT;
21using Enums::GIT_MOVE;
22using Enums::GIT_PULL;
23using Enums::GIT_PUSH;
24using Enums::GIT_RM;
26using Enums::INVALID;
35
40ImitatePass::ImitatePass() = default;
41
42static auto pgit(const QString &path) -> QString {
43 if (!QtPassSettings::getGitExecutable().startsWith("wsl ")) {
44 return path;
45 }
46 QString res = "$(wslpath " + path + ")";
47 return res.replace('\\', '/');
48}
49
50static auto pgpg(const QString &path) -> QString {
51 if (!QtPassSettings::getGpgExecutable().startsWith("wsl ")) {
52 return path;
53 }
54 QString res = "$(wslpath " + path + ")";
55 return res.replace('\\', '/');
56}
57
62 executeGit(GIT_INIT, {"init", pgit(QtPassSettings::getPassStore())});
63}
64
68void ImitatePass::GitPull() { executeGit(GIT_PULL, {"pull"}); }
69
76
82 executeGit(GIT_PUSH, {"push"});
83 }
84}
85
89void ImitatePass::Show(QString file) {
90 file = QtPassSettings::getPassStore() + file + ".gpg";
91 QStringList args = {"-d", "--quiet", "--yes", "--no-encrypt-to",
92 "--batch", "--use-agent", pgpg(file)};
93 executeGpg(PASS_SHOW, args);
94}
95
99void ImitatePass::OtpGenerate(QString file) {
100#ifdef QT_DEBUG
101 dbg() << "No OTP generation code for fake pass yet, attempting for file: " +
102 file;
103#else
104 Q_UNUSED(file)
105#endif
106}
107
115void ImitatePass::Insert(QString file, QString newValue, bool overwrite) {
116 file = file + ".gpg";
117 QString gpgIdPath = Pass::getGpgIdPath(file);
118 if (!verifyGpgIdFile(gpgIdPath)) {
119 emit critical(tr("Check .gpgid file signature!"),
120 tr("Signature for %1 is invalid.").arg(gpgIdPath));
121 return;
122 }
123 transactionHelper trans(this, PASS_INSERT);
124 QStringList recipients = Pass::getRecipientList(file);
125 if (recipients.isEmpty()) {
126 // TODO(bezet): probably throw here
127 emit critical(tr("Can not edit"),
128 tr("Could not read encryption key to use, .gpg-id "
129 "file missing or invalid."));
130 return;
131 }
132 QStringList args = {"--batch", "-eq", "--output", pgpg(file)};
133 for (auto &r : recipients) {
134 args.append("-r");
135 args.append(r);
136 }
137 if (overwrite) {
138 args.append("--yes");
139 }
140 args.append("-");
141 executeGpg(PASS_INSERT, args, newValue);
143 // TODO(bezet): why not?
144 if (!overwrite) {
145 executeGit(GIT_ADD, {"add", pgit(file)});
146 }
147 QString path = QDir(QtPassSettings::getPassStore()).relativeFilePath(file);
148 path.replace(Util::endsWithGpg(), "");
149 QString msg =
150 QString(overwrite ? "Edit" : "Add") + " for " + path + " using QtPass.";
151 GitCommit(file, msg);
152 }
153}
154
161void ImitatePass::GitCommit(const QString &file, const QString &msg) {
162 if (file.isEmpty()) {
163 executeGit(GIT_COMMIT, {"commit", "-m", msg});
164 } else {
165 executeGit(GIT_COMMIT, {"commit", "-m", msg, "--", pgit(file)});
166 }
167}
168
172void ImitatePass::Remove(QString file, bool isDir) {
173 file = QtPassSettings::getPassStore() + file;
174 transactionHelper trans(this, PASS_REMOVE);
175 if (!isDir) {
176 file += ".gpg";
177 }
179 executeGit(GIT_RM, {"rm", (isDir ? "-rf" : "-f"), pgit(file)});
180 // TODO(bezet): commit message used to have pass-like file name inside(ie.
181 // getFile(file, true)
182 GitCommit(file, "Remove for " + file + " using QtPass.");
183 } else {
184 if (isDir) {
185 QDir dir(file);
186 dir.removeRecursively();
187 } else {
188 QFile(file).remove();
189 }
190 }
191}
192
200void ImitatePass::Init(QString path, const QList<UserInfo> &users) {
201#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
202 QStringList signingKeys =
203 QtPassSettings::getPassSigningKey().split(" ", Qt::SkipEmptyParts);
204#else
205 QStringList signingKeys =
206 QtPassSettings::getPassSigningKey().split(" ", QString::SkipEmptyParts);
207#endif
208 QString gpgIdSigFile = path + ".gpg-id.sig";
209 bool addSigFile = false;
210 if (!signingKeys.isEmpty()) {
211 QString out;
212 QStringList args =
213 QStringList{"--status-fd=1", "--list-secret-keys"} + signingKeys;
215 bool found = false;
216 for (auto &key : signingKeys) {
217 if (out.contains("[GNUPG:] KEY_CONSIDERED " + key)) {
218 found = true;
219 break;
220 }
221 }
222 if (!found) {
223 emit critical(tr("No signing key!"),
224 tr("None of the secret signing keys is available.\n"
225 "You will not be able to change the user list!"));
226 return;
227 }
228 QFileInfo checkFile(gpgIdSigFile);
229 if (!checkFile.exists() || !checkFile.isFile()) {
230 addSigFile = true;
231 }
232 }
233
234 QString gpgIdFile = path + ".gpg-id";
235 QFile gpgId(gpgIdFile);
236 bool addFile = false;
237 transactionHelper trans(this, PASS_INIT);
238 if (QtPassSettings::isAddGPGId(true)) {
239 QFileInfo checkFile(gpgIdFile);
240 if (!checkFile.exists() || !checkFile.isFile()) {
241 addFile = true;
242 }
243 }
244 if (!gpgId.open(QIODevice::WriteOnly | QIODevice::Text)) {
245 emit critical(tr("Cannot update"),
246 tr("Failed to open .gpg-id for writing."));
247 return;
248 }
249 bool secret_selected = false;
250 foreach (const UserInfo &user, users) {
251 if (user.enabled) {
252 gpgId.write((user.key_id + "\n").toUtf8());
253 secret_selected |= user.have_secret;
254 }
255 }
256 gpgId.close();
257 if (!secret_selected) {
258 emit critical(
259 tr("Check selected users!"),
260 tr("None of the selected keys have a secret key available.\n"
261 "You will not be able to decrypt any newly added passwords!"));
262 return;
263 }
264
265 if (!signingKeys.isEmpty()) {
266 QStringList args;
267 for (auto &key : signingKeys) {
268 args.append(QStringList{"--default-key", key});
269 }
270 args.append(QStringList{"--yes", "--detach-sign", gpgIdFile});
272 if (!verifyGpgIdFile(gpgIdFile)) {
273 emit critical(tr("Check .gpgid file signature!"),
274 tr("Signature for %1 is invalid.").arg(gpgIdFile));
275 return;
276 }
277 }
278
281 if (addFile) {
282 executeGit(GIT_ADD, {"add", pgit(gpgIdFile)});
283 }
284 QString commitPath = gpgIdFile;
285 commitPath.replace(Util::endsWithGpg(), "");
286 GitCommit(gpgIdFile, "Added " + commitPath + " using QtPass.");
287 if (!signingKeys.isEmpty()) {
288 if (addSigFile) {
289 executeGit(GIT_ADD, {"add", pgit(gpgIdSigFile)});
290 }
291 commitPath = gpgIdSigFile;
292 commitPath.replace(QRegularExpression("\\.gpg$"), "");
293 GitCommit(gpgIdSigFile, "Added " + commitPath + " using QtPass.");
294 }
295 }
296 reencryptPath(path);
297}
298
304auto ImitatePass::verifyGpgIdFile(const QString &file) -> bool {
305#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
306 QStringList signingKeys =
307 QtPassSettings::getPassSigningKey().split(" ", Qt::SkipEmptyParts);
308#else
309 QStringList signingKeys =
310 QtPassSettings::getPassSigningKey().split(" ", QString::SkipEmptyParts);
311#endif
312 if (signingKeys.isEmpty()) {
313 return true;
314 }
315 QString out;
316 QStringList args =
317 QStringList{"--verify", "--status-fd=1", pgpg(file) + ".sig", pgpg(file)};
319 QRegularExpression re(
320 R"(^\[GNUPG:\] VALIDSIG ([A-F0-9]{40}) .* ([A-F0-9]{40})\r?$)",
321 QRegularExpression::MultilineOption);
322 QRegularExpressionMatch m = re.match(out);
323 if (!m.hasMatch()) {
324 return false;
325 }
326 QStringList fingerprints = m.capturedTexts();
327 fingerprints.removeFirst();
328 for (auto &key : signingKeys) {
329 if (fingerprints.contains(key)) {
330 return true;
331 }
332 }
333 return false;
334}
335
341auto ImitatePass::removeDir(const QString &dirName) -> bool {
342 bool result = true;
343 QDir dir(dirName);
344
345 if (dir.exists(dirName)) {
346 Q_FOREACH (QFileInfo info,
347 dir.entryInfoList(QDir::NoDotAndDotDot | QDir::System |
348 QDir::Hidden | QDir::AllDirs | QDir::Files,
349 QDir::DirsFirst)) {
350 if (info.isDir()) {
351 result = removeDir(info.absoluteFilePath());
352 } else {
353 result = QFile::remove(info.absoluteFilePath());
354 }
355
356 if (!result) {
357 return result;
358 }
359 }
360 result = dir.rmdir(dirName);
361 }
362 return result;
363}
364
372void ImitatePass::reencryptPath(const QString &dir) {
373 emit statusMsg(tr("Re-encrypting from folder %1").arg(dir), 3000);
374 emit startReencryptPath();
376 // TODO(bezet): move statuses inside actions?
377 emit statusMsg(tr("Updating password-store"), 2000);
378 GitPull_b();
379 }
380 QDir currentDir;
381 QDirIterator gpgFiles(dir, QStringList() << "*.gpg", QDir::Files,
382 QDirIterator::Subdirectories);
383 QStringList gpgIdFilesVerified;
384 QStringList gpgId;
385 while (gpgFiles.hasNext()) {
386 QString fileName = gpgFiles.next();
387 if (gpgFiles.fileInfo().path() != currentDir.path()) {
388 QString gpgIdPath = Pass::getGpgIdPath(fileName);
389 if (!gpgIdFilesVerified.contains(gpgIdPath)) {
390 if (!verifyGpgIdFile(gpgIdPath)) {
391 emit critical(tr("Check .gpgid file signature!"),
392 tr("Signature for %1 is invalid.").arg(gpgIdPath));
393 emit endReencryptPath();
394 return;
395 }
396 gpgIdFilesVerified.append(gpgIdPath);
397 }
398 gpgId = getRecipientList(fileName);
399 gpgId.sort();
400 }
401 // TODO(bezet): enable --with-colons for better future-proofness?
402 QStringList args = {
403 "-v", "--no-secmem-warning", "--no-permission-warning",
404 "--list-only", "--keyid-format=long", pgpg(fileName)};
405 QString keys;
406 QString err;
408 &err);
409 QStringList actualKeys;
410 keys += err;
411#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)
412 QStringList key = keys.split(Util::newLinesRegex(), Qt::SkipEmptyParts);
413#else
414 QStringList key =
415 keys.split(Util::newLinesRegex(), QString::SkipEmptyParts);
416#endif
417 QListIterator<QString> itr(key);
418 while (itr.hasNext()) {
419 QString current = itr.next();
420 QStringList cur = current.split(" ");
421 if (cur.length() > 4) {
422 QString actualKey = cur.takeAt(4);
423 if (actualKey.length() == 16) {
424 actualKeys << actualKey;
425 }
426 }
427 }
428 actualKeys.sort();
429 if (actualKeys != gpgId) {
430 // dbg()<< actualKeys << gpgId << getRecipientList(fileName);
431#ifdef QT_DEBUG
432 dbg() << "reencrypt " << fileName << " for " << gpgId;
433#endif
434 QString local_lastDecrypt = "Could not decrypt";
435 args = QStringList{
436 "-d", "--quiet", "--yes", "--no-encrypt-to",
437 "--batch", "--use-agent", pgpg(fileName)};
439 &local_lastDecrypt);
440
441 if (!local_lastDecrypt.isEmpty() &&
442 local_lastDecrypt != "Could not decrypt") {
443 if (local_lastDecrypt.right(1) != "\n") {
444 local_lastDecrypt += "\n";
445 }
446
447 QStringList recipients = Pass::getRecipientList(fileName);
448 if (recipients.isEmpty()) {
449 emit critical(tr("Can not edit"),
450 tr("Could not read encryption key to use, .gpg-id "
451 "file missing or invalid."));
452 return;
453 }
454 args =
455 QStringList{"--yes", "--batch", "-eq", "--output", pgpg(fileName)};
456 for (auto &i : recipients) {
457 args.append("-r");
458 args.append(i);
459 }
460 args.append("-");
462 local_lastDecrypt);
463
466 {"add", pgit(fileName)});
467 QString path =
468 QDir(QtPassSettings::getPassStore()).relativeFilePath(fileName);
469 path.replace(Util::endsWithGpg(), "");
471 {"commit", pgit(fileName), "-m",
472 "Edit for " + path + " using QtPass."});
473 }
474
475 } else {
476#ifdef QT_DEBUG
477 dbg() << "Decrypt error on re-encrypt";
478#endif
479 }
480 }
481 }
483 emit statusMsg(tr("Updating password-store"), 2000);
484 // TODO(bezet): this is non-blocking and shall be done outside
485 GitPush();
486 }
487 emit endReencryptPath();
488}
489
490void ImitatePass::Move(const QString src, const QString dest,
491 const bool force) {
492 transactionHelper trans(this, PASS_MOVE);
493 QFileInfo srcFileInfo(src);
494 QFileInfo destFileInfo(dest);
495 QString destFile;
496 QString srcFileBaseName = srcFileInfo.fileName();
497
498 if (srcFileInfo.isFile()) {
499 if (destFileInfo.isFile()) {
500 if (!force) {
501#ifdef QT_DEBUG
502 dbg() << "Destination file already exists";
503#endif
504 return;
505 }
506 } else if (destFileInfo.isDir()) {
507 destFile = QDir(dest).filePath(srcFileBaseName);
508 } else {
509 destFile = dest;
510 }
511
512 if (destFile.endsWith(".gpg", Qt::CaseInsensitive)) {
513 destFile.chop(4); // make sure suffix is lowercase
514 }
515 destFile.append(".gpg");
516 } else if (srcFileInfo.isDir()) {
517 if (destFileInfo.isDir()) {
518 destFile = QDir(dest).filePath(srcFileBaseName);
519 } else if (destFileInfo.isFile()) {
520#ifdef QT_DEBUG
521 dbg() << "Destination is a file";
522#endif
523 return;
524 } else {
525 destFile = dest;
526 }
527 } else {
528#ifdef QT_DEBUG
529 dbg() << "Source file does not exist";
530#endif
531 return;
532 }
533
534#ifdef QT_DEBUG
535 dbg() << "Move Source: " << src;
536 dbg() << "Move Destination: " << destFile;
537#endif
538
540 QStringList args;
541 args << "mv";
542 if (force) {
543 args << "-f";
544 }
545 args << pgit(src);
546 args << pgit(destFile);
547 executeGit(GIT_MOVE, args);
548
549 QString relSrc = QDir(QtPassSettings::getPassStore()).relativeFilePath(src);
550 relSrc.replace(Util::endsWithGpg(), "");
551 QString relDest =
552 QDir(QtPassSettings::getPassStore()).relativeFilePath(destFile);
553 relDest.replace(Util::endsWithGpg(), "");
554 QString message = QString("Moved for %1 to %2 using QtPass.");
555 message = message.arg(relSrc, relDest);
556 GitCommit("", message);
557 } else {
558 QDir qDir;
559 if (force) {
560 qDir.remove(destFile);
561 }
562 qDir.rename(src, destFile);
563 }
564}
565
566void ImitatePass::Copy(const QString src, const QString dest,
567 const bool force) {
568 QFileInfo destFileInfo(dest);
569 transactionHelper trans(this, PASS_COPY);
571 QStringList args;
572 args << "cp";
573 if (force) {
574 args << "-f";
575 }
576 args << pgit(src);
577 args << pgit(dest);
578 executeGit(GIT_COPY, args);
579
580 QString message = QString("copied from %1 to %2 using QTPass.");
581 message = message.arg(src, dest);
582 GitCommit("", message);
583 } else {
584 QDir qDir;
585 if (force) {
586 qDir.remove(dest);
587 }
588 QFile::copy(src, dest);
589 }
590 // reecrypt all files under the new folder
591 if (destFileInfo.isDir()) {
592 reencryptPath(destFileInfo.absoluteFilePath());
593 } else if (destFileInfo.isFile()) {
594 reencryptPath(destFileInfo.dir().path());
595 }
596}
597
602void ImitatePass::executeGpg(PROCESS id, const QStringList &args, QString input,
603 bool readStdout, bool readStderr) {
604 executeWrapper(id, QtPassSettings::getGpgExecutable(), args, std::move(input),
605 readStdout, readStderr);
606}
611void ImitatePass::executeGit(PROCESS id, const QStringList &args, QString input,
612 bool readStdout, bool readStderr) {
613 executeWrapper(id, QtPassSettings::getGitExecutable(), args, std::move(input),
614 readStdout, readStderr);
615}
616
627void ImitatePass::finished(int id, int exitCode, const QString &out,
628 const QString &err) {
629#ifdef QT_DEBUG
630 dbg() << "Imitate Pass";
631#endif
632 static QString transactionOutput;
633 PROCESS pid = transactionIsOver(static_cast<PROCESS>(id));
634 transactionOutput.append(out);
635
636 if (exitCode == 0) {
637 if (pid == INVALID) {
638 return;
639 }
640 } else {
641 while (pid == INVALID) {
642 id = exec.cancelNext();
643 if (id == -1) {
644 // this is probably irrecoverable and shall not happen
645#ifdef QT_DEBUG
646 dbg() << "No such transaction!";
647#endif
648 return;
649 }
650 pid = transactionIsOver(static_cast<PROCESS>(id));
651 }
652 }
653 Pass::finished(pid, exitCode, transactionOutput, err);
654 transactionOutput.clear();
655}
656
666void ImitatePass::executeWrapper(PROCESS id, const QString &app,
667 const QStringList &args, QString input,
668 bool readStdout, bool readStderr) {
669 transactionAdd(id);
670 Pass::executeWrapper(id, app, args, input, readStdout, readStderr);
671}
static auto executeBlocking(QString app, const QStringList &args, const QString &input=QString(), QString *process_out=nullptr, QString *process_err=nullptr) -> int
Executor::executeBlocking blocking version of the executor, takes input and presents it as stdin.
Definition executor.cpp:177
auto cancelNext() -> int
Executor::cancelNext cancels execution of first process in queue if it's not already running.
Definition executor.cpp:244
virtual void OtpGenerate(QString file) Q_DECL_OVERRIDE
ImitatePass::OtpGenerate generates an otp code.
virtual void Show(QString file) Q_DECL_OVERRIDE
ImitatePass::Show shows content of file.
ImitatePass()
ImitatePass::ImitatePass for situaions when pass is not available we imitate the behavior of pass htt...
virtual void GitPush() Q_DECL_OVERRIDE
ImitatePass::GitPush git init wrapper.
void Copy(const QString src, const QString dest, const bool force=false) Q_DECL_OVERRIDE
virtual void executeWrapper(PROCESS id, const QString &app, const QStringList &args, QString input, bool readStdout=true, bool readStderr=true) Q_DECL_OVERRIDE
executeWrapper overrided so that every execution is a transaction
virtual void GitInit() Q_DECL_OVERRIDE
ImitatePass::GitInit git init wrapper.
virtual void GitPull_b() Q_DECL_OVERRIDE
ImitatePass::GitPull_b git pull wrapper.
virtual void Insert(QString file, QString newValue, bool overwrite=false) Q_DECL_OVERRIDE
ImitatePass::Insert create new file with encrypted content.
virtual void GitPull() Q_DECL_OVERRIDE
ImitatePass::GitPull git init wrapper.
void endReencryptPath()
virtual void Remove(QString file, bool isDir=false) Q_DECL_OVERRIDE
ImitatePass::Remove custom implementation of "pass remove".
void Move(const QString src, const QString dest, const bool force=false) Q_DECL_OVERRIDE
virtual void finished(int id, int exitCode, const QString &out, const QString &err) Q_DECL_OVERRIDE
ImitatePass::finished this function is overloaded to ensure identical behaviour to RealPass ie....
virtual void Init(QString path, const QList< UserInfo > &users) Q_DECL_OVERRIDE
ImitatePass::Init initialize pass repository.
void startReencryptPath()
void reencryptPath(const QString &dir)
ImitatePass::reencryptPath reencrypt all files under the chosen directory.
void critical(const QString &, const QString &)
void statusMsg(const QString &, int)
void executeWrapper(PROCESS id, const QString &app, const QStringList &args, bool readStdout=true, bool readStderr=true)
Definition pass.cpp:45
static auto getRecipientList(const QString &for_file) -> QStringList
Pass::getRecipientList return list of gpg-id's to encrypt for.
Definition pass.cpp:341
static auto getGpgIdPath(const QString &for_file) -> QString
Pass::getGpgIdPath return gpgid file path for some file (folder).
Definition pass.cpp:312
Executor exec
Definition pass.h:27
virtual void finished(int id, int exitCode, const QString &out, const QString &err)
Pass::processFinished reemits specific signal based on what process has finished.
Definition pass.cpp:217
static auto isAutoPull(const bool &defaultValue=QVariant().toBool()) -> bool
static auto isUseGit(const bool &defaultValue=QVariant().toBool()) -> bool
static auto getPassStore(const QString &defaultValue=QVariant().toString()) -> QString
static auto isAddGPGId(const bool &defaultValue=QVariant().toBool()) -> bool
static auto getGpgExecutable(const QString &defaultValue=QVariant().toString()) -> QString
static auto getPassSigningKey(const QString &defaultValue=QVariant().toString()) -> QString
static auto isUseWebDav(const bool &defaultValue=QVariant().toBool()) -> bool
static auto isAutoPush(const bool &defaultValue=QVariant().toBool()) -> bool
static auto getGitExecutable(const QString &defaultValue=QVariant().toString()) -> QString
static auto endsWithGpg() -> const QRegularExpression &
Definition util.cpp:203
static auto newLinesRegex() -> const QRegularExpression &
Definition util.cpp:214
auto transactionIsOver(Enums::PROCESS) -> Enums::PROCESS
transactionIsOver checks wheather currently finished process is last in current transaction
void transactionAdd(Enums::PROCESS)
transactionAdd If called after call to transactionStart() and before transactionEnd(),...
#define dbg()
Definition debughelper.h:9
@ CLIPBOARD_ALWAYS
Definition enums.h:14
@ CLIPBOARD_NEVER
Definition enums.h:13
@ CLIPBOARD_ON_DEMAND
Definition enums.h:15
PROCESS
Definition enums.h:18
@ PASS_INIT
Definition enums.h:28
@ PASS_OTP_GENERATE
Definition enums.h:36
@ PASS_INSERT
Definition enums.h:26
@ GIT_INIT
Definition enums.h:19
@ PASS_COPY
Definition enums.h:31
@ PASS_MOVE
Definition enums.h:30
@ GIT_MOVE
Definition enums.h:32
@ GPG_GENKEYS
Definition enums.h:29
@ PASS_REMOVE
Definition enums.h:27
@ GIT_COPY
Definition enums.h:33
@ INVALID
Definition enums.h:35
@ GIT_COMMIT
Definition enums.h:21
@ GIT_RM
Definition enums.h:22
@ PASS_SHOW
Definition enums.h:25
@ GIT_ADD
Definition enums.h:20
@ PROCESS_COUNT
Definition enums.h:34
@ GIT_PULL
Definition enums.h:23
@ GIT_PUSH
Definition enums.h:24
Stores key info lines including validity, creation date and more.
Definition userinfo.h:13
bool have_secret
UserInfo::have_secret secret key is available (can decrypt with this key)
Definition userinfo.h:48
bool enabled
UserInfo::enabled.
Definition userinfo.h:52
QString key_id
UserInfo::key_id hexadecimal representation.
Definition userinfo.h:38