Line data Source code
1 : // SPDX-FileCopyrightText: 2018 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Implements QtPass; init() holds the settings migrations run on every
6 : * start-up and showTextAsQRCode() drives the qrencode process.
7 : */
8 : #include "qtpass.h"
9 : #include "executor.h"
10 : #include "imitatepass.h"
11 : #include "qtpasssettings.h"
12 : #include "settingsconstants.h"
13 : #include "util.h"
14 : #include <QApplication>
15 : #include <QDialog>
16 : #include <QLabel>
17 : #include <QPixmap>
18 : #include <QVBoxLayout>
19 :
20 : #include "qtpasslogging.h"
21 :
22 146 : QtPass::QtPass(QObject *parent) : QObject(parent), m_clipboard(this) {
23 : // Both implementations are wired up: the active one can change through the
24 : // configuration dialog, and reconnecting on every switch is not worth it.
25 146 : connectPassSignalHandlers(QtPassSettings::getRealPass());
26 146 : connectPassSignalHandlers(QtPassSettings::getImitatePass());
27 146 : }
28 :
29 277 : QtPass::~QtPass() = default;
30 :
31 134 : void QtPass::init() {
32 134 : QString passStore = QtPassSettings::getPassStore(Util::findPasswordStore());
33 134 : QtPassSettings::setPassStore(passStore);
34 :
35 134 : QtPassSettings::initExecutables();
36 :
37 268 : QString version = QtPassSettings::getVersion();
38 :
39 : // Config updates
40 134 : if (version.isEmpty()) {
41 4 : qCDebug(lcQtPass) << "assuming fresh install";
42 4 : AppSettings s = QtPassSettings::load();
43 4 : if (s.autoclearSeconds < 5)
44 4 : s.autoclearSeconds = 10;
45 4 : if (s.autoclearPanelSeconds < 5)
46 4 : s.autoclearPanelSeconds = 10;
47 4 : s.usePwgen = !s.pwgenExecutable.isEmpty();
48 4 : s.passTemplate = QStringLiteral("login\nurl\nOTP");
49 4 : QtPassSettings::save(s);
50 : // A fresh profile already has the new useOtp default, so record the
51 : // migration as done. Without this, turning OTP off during the first session
52 : // would be undone by the migration branch on the next launch.
53 8 : QtPassSettings::getInstance()->setValue(
54 8 : SettingsConstants::otpMigratedToNative, true);
55 4 : } else {
56 130 : AppSettings s = QtPassSettings::load();
57 : bool changed = false;
58 130 : if (s.passTemplate.isEmpty()) {
59 1 : s.passTemplate = QStringLiteral("login\nurl\nOTP");
60 : changed = true;
61 : }
62 : // `useOtp` used to gate the Unix-only pass-otp extension and now gates
63 : // built-in TOTP, so an existing profile's stored false is stale rather than
64 : // a preference. The new default of true never reaches these users: the
65 : // fresh-install branch above already persisted the key, and the serializer
66 : // writes it unconditionally. Enable it once, remembered by its own key so a
67 : // deliberate opt-out is not overridden on the next launch.
68 130 : if (!QtPassSettings::getInstance()
69 260 : ->value(SettingsConstants::otpMigratedToNative, false)
70 130 : .toBool()) {
71 1 : s.useOtp = true;
72 2 : QtPassSettings::getInstance()->setValue(
73 2 : SettingsConstants::otpMigratedToNative, true);
74 : changed = true;
75 : }
76 129 : if (changed) {
77 1 : QtPassSettings::save(s);
78 : }
79 130 : }
80 :
81 268 : QtPassSettings::setVersion(VERSION);
82 134 : }
83 :
84 292 : void QtPass::connectPassSignalHandlers(Pass *pass) {
85 292 : connect(pass, &Pass::processErrorExit, this, &QtPass::processErrorExit);
86 292 : connect(pass, &Pass::finishedGitInit, this, &QtPass::passStoreChanged);
87 292 : connect(pass, &Pass::finishedGitPull, this, &QtPass::processFinished);
88 292 : connect(pass, &Pass::finishedGitPush, this, &QtPass::processFinished);
89 292 : connect(pass, &Pass::finishedInsert, this, &QtPass::finishedInsert);
90 292 : connect(pass, &Pass::finishedRemove, this, &QtPass::passStoreChanged);
91 292 : connect(pass, &Pass::finishedInit, this, &QtPass::passStoreChanged);
92 292 : connect(pass, &Pass::finishedMove, this, &QtPass::passStoreChanged);
93 292 : connect(pass, &Pass::finishedCopy, this, &QtPass::passStoreChanged);
94 292 : connect(pass, &Pass::finishedGenerateGPGKeys, this,
95 292 : &QtPass::onKeyGenerationComplete);
96 292 : }
97 :
98 37 : void QtPass::reportError(int exitCode, const QString &error) {
99 37 : if (error.isEmpty()) {
100 12 : return;
101 : }
102 : const QString colour =
103 31 : exitCode == 0 ? QStringLiteral("darkgray") : QStringLiteral("red");
104 50 : emit outputReady(formatOutput(
105 50 : error, QStringLiteral("<span style=\"color: %1;\">").arg(colour),
106 25 : QStringLiteral("</span><br />")));
107 : }
108 :
109 23 : void QtPass::processErrorExit(int exitCode, const QString &error) {
110 23 : reportError(exitCode, error);
111 23 : emit operationFinished();
112 23 : }
113 :
114 14 : void QtPass::processFinished(const QString &output, const QString &errout) {
115 : // A silent command (git push with nothing to push, say) has nothing to
116 : // show; re-setting the browser's HTML for it would only reset its scroll.
117 14 : if (!output.isEmpty()) {
118 6 : emit outputReady(formatOutput(output));
119 : }
120 14 : reportError(0, errout);
121 14 : emit operationFinished();
122 14 : }
123 :
124 5 : void QtPass::passStoreChanged(const QString &output, const QString &errout) {
125 5 : processFinished(output, errout);
126 5 : doGitPush();
127 5 : }
128 :
129 2 : void QtPass::finishedInsert(const QString &output, const QString &errout) {
130 2 : processFinished(output, errout);
131 2 : doGitPush();
132 2 : emit entryInserted();
133 2 : }
134 :
135 2 : void QtPass::onKeyGenerationComplete(const QString &output,
136 : const QString &errout) {
137 : // The KeygenDialog listens to the same signal and closes itself.
138 2 : emit statusMessage(tr("GPG key pair generated successfully"), 10000);
139 2 : processFinished(output, errout);
140 2 : }
141 :
142 45 : auto QtPass::formatOutput(QString output, const QString &prefix,
143 : const QString &postfix) -> QString {
144 : // Escapes and links only launchable http(s) URLs; anything else stays
145 : // plain text (the text browser opens external links on click).
146 45 : output = Util::linkifyUrls(output);
147 90 : output.replace(QStringLiteral("\n"), "<br />");
148 90 : return prefix + output + postfix;
149 : }
150 :
151 7 : void QtPass::doGitPush() {
152 7 : if (QtPassSettings::isAutoPush()) {
153 5 : emit pushRequested();
154 : }
155 7 : }
156 :
157 5 : void QtPass::showTextAsQRCode(const QString &text) {
158 5 : const AppSettings s = QtPassSettings::load();
159 : const QString qrExe = s.qrencodeExecutable.isEmpty()
160 5 : ? QStringLiteral("/usr/bin/qrencode")
161 : : s.qrencodeExecutable;
162 5 : QProcess qrencode;
163 5 : Executor::detachFromTerminal(qrencode);
164 15 : qrencode.start(qrExe, QStringList() << "-o-"
165 10 : << "-tPNG");
166 : // A missing or non-executable binary never "finishes": exitStatus() stays
167 : // NormalExit and exitCode() 0, which used to fall through to an empty popup.
168 5 : if (!qrencode.waitForStarted()) {
169 2 : emit statusMessage(tr("Could not start qrencode: %1").arg(qrExe), 2000);
170 1 : return;
171 : }
172 4 : qrencode.write(text.toUtf8());
173 4 : qrencode.closeWriteChannel();
174 : // A hung qrencode also leaves exitStatus()/exitCode() at their defaults.
175 4 : if (!qrencode.waitForFinished()) {
176 0 : qrencode.kill();
177 0 : emit statusMessage(tr("qrencode did not finish in time"), 2000);
178 0 : return;
179 : }
180 4 : QByteArray output(qrencode.readAllStandardOutput());
181 :
182 4 : const bool crashed = qrencode.exitStatus() != QProcess::NormalExit;
183 : // exitCode() is meaningless after a crash, so only consult it otherwise.
184 4 : const bool failed = !crashed && qrencode.exitCode() != 0;
185 2 : if (crashed || failed) {
186 6 : QString error(qrencode.readAllStandardError());
187 3 : if (error.trimmed().isEmpty()) {
188 : error = crashed
189 4 : ? tr("qrencode crashed")
190 3 : : tr("qrencode exited with code %1").arg(qrencode.exitCode());
191 : }
192 3 : emit statusMessage(error, 2000);
193 : } else {
194 1 : QPixmap image;
195 1 : image.loadFromData(output, "PNG");
196 1 : QDialog *popup = createQRCodePopup(image);
197 1 : popup->exec();
198 1 : }
199 10 : }
200 :
201 2 : QDialog *QtPass::createQRCodePopup(const QPixmap &image) {
202 2 : auto *popup = new QDialog(nullptr, Qt::Popup | Qt::FramelessWindowHint);
203 2 : popup->setAttribute(Qt::WA_DeleteOnClose);
204 2 : auto *layout = new QVBoxLayout;
205 2 : auto *popupLabel = new QLabel();
206 2 : layout->addWidget(popupLabel);
207 2 : popupLabel->setPixmap(image);
208 2 : popupLabel->setScaledContents(true);
209 2 : popupLabel->show();
210 2 : popup->setLayout(layout);
211 2 : popup->move(QCursor::pos());
212 2 : return popup;
213 : }
|