30#include <QSystemTrayIcon>
41auto pathRow(QLineEdit *edit, QWidget *parent,
42 const std::function<QString()> &pick) -> QWidget * {
43 auto *row =
new QWidget(parent);
44 auto *layout =
new QHBoxLayout(row);
45 layout->setContentsMargins(0, 0, 0, 0);
46 layout->addWidget(edit);
47 auto *browse =
new QToolButton(row);
48 browse->setText(QStringLiteral(
"…"));
49 browse->setToolTip(FirstRunWizard::tr(
"Browse"));
50 layout->addWidget(browse);
51 QObject::connect(browse, &QToolButton::clicked, edit, [edit, pick]() {
52 const QString chosen = pick();
53 if (!chosen.isEmpty()) {
54 edit->setText(chosen);
55 emit edit->textEdited(chosen);
61auto runnable(
const QString &exe) ->
bool {
63 (!exe.isEmpty() && QFileInfo(exe).isExecutable());
72 setWindowTitle(tr(
"Welcome to QtPass"));
73 setWizardStyle(QWizard::ModernStyle);
74 setOption(QWizard::NoBackButtonOnStartPage);
75 setOption(QWizard::HaveHelpButton,
false);
77 auto *intro =
new QWizardPage(
this);
78 intro->setTitle(tr(
"Welcome to QtPass"));
79 auto *introLayout =
new QVBoxLayout(intro);
80 auto *introText =
new QLabel(
81 tr(
"QtPass is a graphical front-end for <i>pass</i>, the standard Unix "
82 "password manager: every password is a file encrypted with GnuPG, "
83 "kept in a folder you can put under Git.<br><br>The next pages find "
84 "GnuPG, make sure you have a key to encrypt to and pick the folder "
85 "your passwords live in. Nothing is written until you press "
88 introText->setWordWrap(
true);
89 introText->setTextFormat(Qt::RichText);
90 introLayout->addWidget(introText);
102 if (!runnable(gpgExecutable)) {
108 {QStringLiteral(
"--no-tty"), QStringLiteral(
"--with-colons"),
109 QStringLiteral(
"--with-fingerprint"),
110 QStringLiteral(
"--list-secret-keys")},
119 return !path.isEmpty() &&
120 QFile::exists(QDir(path).filePath(QStringLiteral(
".gpg-id")));
124 QList<UserInfo> recipients;
127 recipients.append(key);
130 const bool created = !QDir(store).exists();
132 QMessageBox::warning(
134 tr(
"Failed to create password-store at: %1").arg(store));
141 SetFileAttributes(store.toStdWString().c_str(), FILE_ATTRIBUTE_HIDDEN);
150 QFile::remove(QDir(store).filePath(QStringLiteral(
".gpg-id")));
151 QFile::remove(QDir(store).filePath(QStringLiteral(
".gpg-id.sig")));
152 QMessageBox::warning(
this, tr(
"Password store not initialised"), note);
155 if (!note.isEmpty()) {
158 QMessageBox::information(
159 this, tr(
"Password store"),
160 tr(
"%1 already contains encrypted files; they were not "
161 "re-encrypted to the ticked keys. Open Users after the start "
163 .arg(QDir::toNativeSeparators(store)));
175 QDir(QDir(store).filePath(QStringLiteral(
".git"))).removeRecursively();
176 QMessageBox::warning(
this, tr(
"Password store not initialised"), note);
183 if (currentPage() !=
nullptr && !currentPage()->validatePage()) {
187 const bool needsGit =
205 : QWizardPage(wizard),
m_wizard(wizard),
m_gpg(new QLineEdit(this)),
206 m_git(new QLineEdit(this)),
m_pass(new QLineEdit(this)),
207 m_usePass(new QCheckBox(tr(
"Use the pass command-line tool"), this)),
209 setTitle(tr(
"Programs"));
210 setSubTitle(tr(
"GnuPG does the encrypting. pass and Git are optional; "
211 "QtPass can do their work itself."));
212 auto *layout =
new QFormLayout(
this);
213 auto pickExe = [
this]() {
214 return QFileDialog::getOpenFileName(
this, tr(
"Select executable"),
215 QString(), tr(
"All files (*)"));
217 layout->addRow(tr(
"GnuPG"), pathRow(
m_gpg,
this, pickExe));
220 layout->addRow(tr(
"Git"), pathRow(
m_git,
this, pickExe));
221 layout->addRow(tr(
"pass"), pathRow(
m_pass,
this, pickExe));
222 m_usePass->setToolTip(tr(
"Run the pass script for every operation instead "
223 "of calling gpg and git directly"));
226 connect(edit, &QLineEdit::textEdited,
this,
236 auto detect = [](QString &stored,
const QStringList &names) {
237 if (runnable(stored)) {
240 for (
const QString &name : names) {
242 if (!found.isEmpty()) {
248 detect(s.
gpgExecutable, {QStringLiteral(
"gpg2"), QStringLiteral(
"gpg")});
260 const QString gpg =
m_gpg->text().trimmed();
262 m_gpgStatus->setText(tr(
"GnuPG was not found. Install it (gpg or gpg2) "
263 "and enter its location here."));
264 }
else if (!runnable(gpg)) {
265 m_gpgStatus->setText(tr(
"%1 is not an executable file.").arg(gpg));
273 emit completeChanged();
277 return runnable(
m_gpg->text().trimmed());
292 : QWizardPage(wizard),
m_wizard(wizard),
m_list(new QListWidget(this)),
293 m_generate(new QPushButton(tr(
"Generate a new key pair…"), this)),
295 setTitle(tr(
"Your key"));
296 setSubTitle(tr(
"Passwords are encrypted to GnuPG keys. Tick the keys that "
297 "should be able to open a new store; you need at least one "
298 "with its secret half on this machine."));
299 auto *layout =
new QVBoxLayout(
this);
300 layout->addWidget(
m_list);
303 auto *buttons =
new QHBoxLayout;
304 buttons->addStretch();
306 layout->addLayout(buttons);
308 connect(
m_list, &QListWidget::itemChanged,
this,
309 [
this]() { emit completeChanged(); });
318 const QDateTime now = QDateTime::currentDateTime();
326 why = tr(
"not usable");
329 QString label = QStringLiteral(
"%1\n%2").arg(key.
name, key.
key_id);
330 if (!why.isEmpty()) {
331 label += QStringLiteral(
" (%1)").arg(why);
333 auto *item =
new QListWidgetItem(label,
m_list);
334 item->setFlags(key.
enabled ? (item->flags() | Qt::ItemIsUserCheckable)
335 : (item->flags() & ~Qt::ItemIsEnabled));
336 item->setCheckState(key.
enabled ? Qt::Checked : Qt::Unchecked);
339 ? tr(
"GnuPG has no secret key yet. Generate one here, "
340 "or import your existing key with gpg first.")
342 emit completeChanged();
359 for (
int i = 0; i <
m_list->count() && i < m_wizard->m_keys.size(); ++i) {
360 m_wizard->m_keys[i].enabled =
m_list->item(i)->checkState() == Qt::Checked;
368 : QWizardPage(wizard),
m_wizard(wizard),
m_path(new QLineEdit(this)),
369 m_useGit(new QCheckBox(tr(
"Keep the store under Git"), this)),
371 setTitle(tr(
"Password store"));
372 setSubTitle(tr(
"The folder your passwords live in. An existing store is "
373 "used as it is; an empty or missing folder is set up for "
374 "the keys you ticked."));
375 auto *layout =
new QFormLayout(
this);
376 layout->addRow(tr(
"Folder"), pathRow(
m_path,
this, [
this]() {
377 return QFileDialog::getExistingDirectory(
378 this, tr(
"Choose the password store folder"),
382 layout->addRow(QString(),
m_status);
383 layout->addRow(QString(),
m_useGit);
389 if (path.isEmpty()) {
392 m_path->setText(QDir::toNativeSeparators(QDir::cleanPath(path)));
393 const QString clean = QDir::cleanPath(path);
394 const bool repository = QDir(clean).exists(QStringLiteral(
".git"));
398 const bool identity =
403 ? tr(
"Git has no name and e-mail to commit with yet. Run\n"
404 "git config --global user.name \"Your Name\"\n"
405 "git config --global user.email you@example.org\n"
406 "and turn Git on in Settings afterwards.")
407 : tr(
"Every change becomes a commit; a folder that is no "
408 "repository yet gets one"));
409 haveGit = haveGit && identity;
422 ticked += key.enabled ? 1 : 0;
428 const QString path = QDir::cleanPath(
m_path->text().trimmed());
430 if (path.isEmpty()) {
431 text = tr(
"Enter a folder.");
433 const int entries =
static_cast<int>(
435 text = tr(
"An existing password store with %n entries.",
nullptr, entries);
436 }
else if (QDir(path).exists()) {
437 text = QDir(path).isEmpty()
438 ? tr(
"An empty folder; it will be set up as a password store.")
439 : tr(
"This folder is not a password store yet; a .gpg-id for "
440 "the ticked keys will be written into it.");
442 text = tr(
"The folder does not exist yet; it will be created.");
445 text += QLatin1Char(
' ') +
446 tr(
"Go back and tick at least one key to encrypt it to.");
449 emit completeChanged();
453 const QString path = QDir::cleanPath(
m_path->text().trimmed());
454 if (path.isEmpty()) {
462 QDir::fromNativeSeparators(
m_path->text().trimmed());
471 m_hidePassword(new QCheckBox(tr(
"Hide passwords until asked"), this)),
472 m_trayIcon(new QCheckBox(tr(
"Show an icon in the system tray"), this)) {
473 setTitle(tr(
"Ready"));
474 setSubTitle(tr(
"Everything else can be changed later in Settings."));
475 auto *layout =
new QVBoxLayout(
this);
479 layout->addSpacing(12);
481 tr(
"Show the password line as dots; Show password reveals it"));
484 layout->addStretch();
490 const QString store = QDir::toNativeSeparators(QDir::cleanPath(s.
passStore));
492 lines << tr(
"Store: %1").arg(store.toHtmlEscaped());
494 lines << tr(
"It is already a password store and is used as it is.");
496 lines << tr(
"It will be set up for the ticked keys and put under Git.");
498 lines << tr(
"It will be set up for the ticked keys.");
500 lines << tr(
"GnuPG: %1").arg(s.
gpgExecutable.toHtmlEscaped());
501 lines << (s.
usePass ? tr(
"Operations run through pass.")
502 : tr(
"Operations run through gpg and git directly."));
503 m_summary->setText(lines.join(QStringLiteral(
"<br>")));
505 const bool tray = QSystemTrayIcon::isSystemTrayAvailable();
QLabel * m_summary
Store, what Finish does with it, gpg, mode.
void initializePage() override
Fill the summary from the settings.
QCheckBox * m_trayIcon
Tray icon; hidden when there is no tray.
FirstRunWizard * m_wizard
Owner; its settings get the preferences.
QCheckBox * m_hidePassword
Show the password line as dots; on.
DoneWizardPage(FirstRunWizard *wizard)
Build the page.
auto validatePage() -> bool override
Store the preferences.
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 parseWslCommand(const QString &app) -> std::optional< WslCommand >
Parse a configured executable as a WSL command.
The first start of QtPass: find GnuPG (and pass and Git), make sure there is a secret key,...
@ DonePage
DoneWizardPage: summary and two preferences.
@ ProgramsPage
ProgramsWizardPage: where gpg, pass and git are.
@ KeyPage
KeyWizardPage: the secret keys, with generation.
@ StorePage
StoreWizardPage: the store folder and the Git choice.
@ IntroPage
What QtPass is and what the wizard will do.
auto initialiseStore(const QString &store) -> bool
Make store a password store for the ticked keys: create the folder when it is missing,...
void accept() override
Write the settings and, when the chosen store has no .gpg-id yet, create and initialise it for the ch...
auto initialiseGit(const QString &store) -> bool
Give an existing store the Git repository it was chosen to have.
static auto isStore(const QString &path) -> bool
Whether path points at an initialised store.
FirstRunWizard(QWidget *parent=nullptr)
Build the wizard from the current settings.
static auto secretKeys(const QString &gpgExecutable) -> QList< UserInfo >
The secret keys gpg lists for gpgExecutable.
Implementation that imitates 'pass' when the real tool is unavailable.
QListWidget * m_list
One checkable row per secret key.
void initializePage() override
List the secret keys for the gpg chosen on the previous page.
void reload()
List the secret keys of the chosen gpg into the wizard and the widget, ticking every usable key and g...
auto isComplete() const -> bool override
Always passable: an existing store needs no recipient choice here (and its key may not be imported ye...
QPushButton * m_generate
Opens KeygenDialog.
void generate()
Run KeygenDialog on a private ImitatePass built from the wizard's settings, so the chosen gpg is used...
FirstRunWizard * m_wizard
Owner; holds the key list this page fills.
QLabel * m_status
Says so when gpg has no secret key yet.
auto validatePage() -> bool override
Remember which keys are enabled.
KeyWizardPage(FirstRunWizard *wizard)
Build the page.
Handles GPG keypair generation.
static void invalidate()
Forget the currently active backend.
void init(const AppSettings &settings)
Initialize the Pass instance with a settings snapshot.
static auto initGit(const QString &dir, const AppSettings &s, QString *note) -> bool
Put an existing store under Git: git init, stage the .gpg entries and the .gpg-id files (nothing else...
static auto gitIdentityConfigured(const QString &dir, const AppSettings &s) -> bool
Whether git has a name and e-mail to commit with, i.e. user.name and user.email resolve (globally or ...
static auto initialise(const QString &dir, const QList< UserInfo > &users, const AppSettings &s, bool useGit, QString *note) -> bool
Write the recipients and, if wanted, put the folder under git.
QLineEdit * m_gpg
The gpg executable; required.
QLineEdit * m_pass
The pass executable; optional.
QLineEdit * m_git
The git executable; optional.
FirstRunWizard * m_wizard
Owner; its settings are what the page edits.
void initializePage() override
Fill the fields from the settings (autodetection already ran).
QLabel * m_gpgStatus
Under the gpg field: why it cannot be used.
void updateStatus()
After an edit: say under the gpg field when it is missing or not executable, allow "use pass" only fo...
auto isComplete() const -> bool override
Whether the gpg path points at something that can run.
ProgramsWizardPage(FirstRunWizard *wizard)
Build the page.
auto validatePage() -> bool override
Store the paths and the pass/Git choices in the settings.
QCheckBox * m_usePass
Run pass instead of gpg/git; needs m_pass.
Singleton that stores QtPass settings, handles persistence and defaults.
static void save(const AppSettings &settings)
Persist an AppSettings value object to the singleton store.
StoreWizardPage(FirstRunWizard *wizard)
Build the page.
QLabel * m_status
What Finish will do with the folder.
auto validatePage() -> bool override
Store the path and the Git choice.
void initializePage() override
Show the configured store path.
auto recipients() const -> int
How many keys are ticked on the key page.
auto isComplete() const -> bool override
Whether a path is given and, for a folder that is not a store yet, at least one key was ticked to enc...
QLineEdit * m_path
The store folder, in native separators.
QCheckBox * m_useGit
Keep the store under Git; off without git.
FirstRunWizard * m_wizard
Owner; its settings get the path and flag.
void updateStatus()
After an edit: describe the folder (existing store with its entry count, empty folder,...
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 findPasswordStore() -> QString
Locate the password store directory.
static auto mkpathForStore(const QString &path) -> bool
Create path and any missing parents as pass would.
static auto normalizeFolderPath(const QString &path) -> QString
Ensure a folder path always ends with '/'.
static auto findBinaryInPath(const QString &binary) -> QString
Locate an executable by searching the process PATH and (on Windows) falling back to WSL.
Executor, the FIFO queue every gpg, git and pass child process of QtPass goes through,...
The first-run wizard and its four pages: programs, key, store and summary.
auto parseGpgColonOutput(const QString &output, bool secret) -> QList< UserInfo >
Parse a gpg colon listing into one UserInfo per key.
Parsing of gpg --with-colons key listings into UserInfo records.
ImitatePass, the backend that does what pass does by running gpg and git itself.
Dialog that collects name, e-mail and passphrase for a new GPG keypair and drives gpg --gen-key --bat...
PassBackendFactory, which constructs the two Pass backends on demand and hands out the one the settin...
Initialising a new profile's store (.gpg-id, signature, Git) with blocking processes,...
QtPassSettings, the QSettings singleton behind every saved setting, the profiles and the Pass backend...
Plain value-object holding all flat QtPass configuration.
QString gpgExecutable
Path to the gpg/gpg2 executable.
bool usePass
Use the pass CLI (true) or native git/gpg.
QString pwgenExecutable
Path to the pwgen executable.
QString passStore
Password-store root path.
QString gitExecutable
Path to the git executable.
bool useTrayIcon
Show a system tray icon.
QString passExecutable
Path to the pass executable.
bool useGit
Enable git integration.
Stores GPG key info including validity, creation date, and more.
bool enabled
Whether this user/key is enabled for normal use.
QString key_id
Hexadecimal representation of the GnuPG key identifier.
auto isValid() const -> bool
Check whether the key has any usable validity level.
QString name
GPG user ID / full name.
QDateTime expiry
Date/time when key expires.
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...