QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
passworddisplaypanel.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2014 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
9#include "appsettings.h"
10#include "otpcodewidget.h"
11#include "qpushbuttonasqrcode.h"
14#include "secretlineedit.h"
15#include "secrettextbrowser.h"
16#include "totp.h"
17#include "util.h"
18
19#include <QBoxLayout>
20#include <QDesktopServices>
21#include <QDrag>
22#include <QFrame>
23#include <QGridLayout>
24#include <QHBoxLayout>
25#include <QIcon>
26#include <QLabel>
27#include <QLayout>
28#include <QLineEdit>
29#include <QMouseEvent>
30#include <QPalette>
31#include <QPushButton>
32#include <QTextBrowser>
33#include <QUrl>
34#include <algorithm>
35
37 QBoxLayout *container,
38 QWidget *widgetParent,
39 QObject *parent)
40 : QObject(parent), m_grid(grid), m_container(container),
41 m_widgetParent(widgetParent) {}
42
44 while (m_grid->count() > 0) {
45 QLayoutItem *item = m_grid->takeAt(0);
46 if (QWidget *widget = item->widget()) {
47 // A drag out of a field value runs QDrag::exec() inside the value
48 // widget's own mouse handler, and clear() can be reached from that
49 // nested event loop (a drop on the search box, the panel autoclear
50 // timer, a second "qtpass NAME"). Deleting the row then - even with
51 // deleteLater(), which the drag's loop would deliver - frees it under
52 // its own handler and crashes. A row that has a QDrag among its children
53 // is hidden now and deleted once that QDrag is gone, which Qt does after
54 // the drag ends; rows without a drag are deleted at once as before.
55 const QList<QDrag *> drags = widget->findChildren<QDrag *>();
56 if (drags.isEmpty()) {
57 delete widget;
58 } else {
59 widget->hide();
60 connect(drags.last(), &QObject::destroyed, widget,
61 [widget]() { widget->deleteLater(); });
62 }
63 }
64 delete item;
65 }
66 m_container->setSpacing(0);
67}
68
69auto PasswordDisplayPanel::currentOtpCode() const -> QString {
70 // displayFields() renders at most one OTP row, so the first match is it.
71 for (int i = 0; i < m_grid->count(); ++i) {
72 QWidget *widget = m_grid->itemAt(i)->widget();
73 if (widget == nullptr) {
74 continue;
75 }
76 if (auto *otp = widget->findChild<OtpCodeWidget *>()) {
77 return otp->code();
78 }
79 }
80 return {};
81}
82
83void PasswordDisplayPanel::displayFields(const QString &password,
84 const NamedValues &namedValues,
85 const AppSettings &s,
86 const QString &otpConfig) {
87 // Rows are numbered as they are added rather than assuming the password
88 // occupies row 0: an entry written by `pass otp insert` has no password row,
89 // and starting at 1 regardless left an empty grid row above the OTP row.
90 int position = 0;
91 // Defence in depth: getPasswordForDisplay() already blanks a password line
92 // that is an otpauth URI (`pass otp insert`); refuse one here too.
93 if (!password.isEmpty() && !FileContent::isOtpUriValue(password)) {
94 // The password is hidden in addField when needed.
95 addField(position, QObject::tr("Password"), password, s);
96 ++position;
97 }
98 bool otpRendered = false;
99 for (const NamedValue &nv : namedValues) {
100 // Keyed on the value too: a field called anything whose value is an otpauth
101 // URI is still a shared secret, and isLineHidden() hides it from the text
102 // browser, so suppressing only OTP/TOTP names leaked `2fa:` and friends.
103 if (FileContent::isOtpFieldName(nv.name) ||
104 FileContent::isOtpUriValue(nv.value)) {
105 // Never render an OTP field verbatim: addField() would display the
106 // shared secret and hand it to a copy button. Unconditional, so it stays
107 // hidden when OTP support is off and otpConfig is empty.
108 if (!otpRendered && !otpConfig.isEmpty()) {
109 addOtpField(position, otpConfig, s);
110 ++position;
111 otpRendered = true;
112 }
113 continue;
114 }
115 addField(position, nv.name, nv.value, s);
116 ++position;
117 }
118 // The configuration came from the entry body rather than from a field.
119 if (!otpConfig.isEmpty() && !otpRendered) {
120 addOtpField(position, otpConfig, s);
121 }
122 m_container->setSpacing(m_grid->count() == 0 ? 0 : 6);
123}
124
125namespace {
126
127// Scope every rule to the widget type so the transparent background does not
128// cascade into the field's standard context menu (a child QMenu), which would
129// otherwise render transparent too.
130const char kButtonStyle[] =
131 "QPushButton { border-style: none; background: transparent; padding: 0; "
132 "margin: 0; icon-size: 16px; color: inherit; }";
133
134// 26px matches the action-button visual height for consistent alignment.
135constexpr int kFieldHeight = 26;
136
141auto valueBrowser(const QString &field, const QString &value,
142 const QString &lineStyle) -> SecretTextBrowser * {
143 auto *browser = new SecretTextBrowser();
144 browser->setOpenExternalLinks(true);
145 browser->setOpenLinks(true);
146 browser->setMaximumHeight(kFieldHeight);
147 browser->setMinimumHeight(kFieldHeight);
148 browser->setSizePolicy(
149 QSizePolicy(QSizePolicy::Expanding, QSizePolicy::Minimum));
150 browser->setObjectName(field);
151 bool linked = false;
152 const QString linkedText = Util::linkifyUrls(value, &linked);
153 if (linked) {
154 // Always HTML here: the text is escaped and carries anchor tags, so do
155 // not leave the interpretation to setText()'s auto-detection.
156 browser->setHtml(linkedText);
157 } else {
158 // Plain text: escaping plus setText() showed `a&b` as `a&amp;b` unless
159 // the value happened to contain a '<'.
160 browser->setPlainText(value);
161 }
162 browser->setReadOnly(true);
163 browser->setStyleSheet(lineStyle);
164 browser->setContentsMargins(0, 0, 0, 0);
165 return browser;
166}
167
169auto urlButton(const QString &url, QWidget *parent) -> QPushButton * {
170 auto *button = new QPushButton(parent);
171 button->setIcon(QIcon::fromTheme(QStringLiteral("applications-internet"),
172 QIcon(":/icons/open-url.svg")));
173 // Escape for the tooltip only; the launched URL stays the validated value.
174 // <qt> forces rich text: auto-detection would show `&amp;` literally, and
175 // plain text misreads a URL containing `&lt;`. Rich text wraps, breaking
176 // the URL at `/` and `?`; white-space:nowrap keeps it on one line (<nobr>
177 // is not enough: Qt only makes its spaces non-breaking).
178 button->setToolTip(
179 QStringLiteral("<qt style=\"white-space:nowrap\">%1</qt>")
180 .arg(QObject::tr("Open %1 in browser").arg(url.toHtmlEscaped())));
181 button->setStyleSheet(QString::fromLatin1(kButtonStyle));
182 button->setCursor(Qt::PointingHandCursor);
183 QObject::connect(button, &QPushButton::clicked, button, [url]() {
184 // Re-validate: never hand an unvalidated string to the OS URL handler.
185 if (Util::isLaunchableWebUrl(url)) {
186 QDesktopServices::openUrl(QUrl(url));
187 }
188 });
189 return button;
190}
191
192} // namespace
193
194void PasswordDisplayPanel::setMarkedSecrets(const QStringList &names) {
195 m_markedSecrets = names;
196}
197
198auto PasswordDisplayPanel::hides(const QString &field,
199 const AppSettings &s) const -> bool {
200 return s.hidePassword &&
201 (s.hideAllFields || field == QObject::tr("Password") ||
202 isSecret(field, s));
203}
204
205auto PasswordDisplayPanel::isSecret(const QString &field,
206 const AppSettings &s) const -> bool {
207 if (!s.hidePassword) {
208 return false;
209 }
210 const bool marked =
211 std::any_of(m_markedSecrets.cbegin(), m_markedSecrets.cend(),
212 [&field](const QString &name) {
213 return field.compare(name, Qt::CaseInsensitive) == 0;
214 });
215 return marked || (s.hideSecretFields &&
216 FileContent::isSecretFieldName(field, s.secretFieldNames));
217}
218
220 const QString &field,
221 const QString &value,
222 const AppSettings &s) {
223 const QString buttonStyle = QString::fromLatin1(kButtonStyle);
225 auto *copy = new QPushButtonWithClipboard(value, m_widgetParent);
226 connect(copy, &QPushButtonWithClipboard::clicked, this,
228 copy->setStyleSheet(buttonStyle);
229 layout->addWidget(copy);
230 }
231 if (s.useQrencode) {
232 auto *qr = new QPushButtonAsQRCode(value, m_widgetParent);
233 connect(qr, &QPushButtonAsQRCode::clicked, this,
235 qr->setStyleSheet(buttonStyle);
236 layout->addWidget(qr);
237 }
238 // Never on the password field, nor on any field hidden like it: its value
239 // must not reach a tooltip or the browser. A field shown in plain view
240 // keeps its link.
241 if (field != QObject::tr("Password") && !hides(field, s) &&
243 layout->addWidget(urlButton(value, m_widgetParent));
244 }
245}
246
248 const QString &field,
249 const QString &value,
250 const QString &lineStyle) {
251 // Shown with the eye button, the password can be selected and copied;
252 // SecretLineEdit marks that copy and reports it, as the value browsers do.
253 auto *passwordLineEdit = new SecretLineEdit();
254 connect(passwordLineEdit, &SecretLineEdit::copied, this,
256 passwordLineEdit->setObjectName(field);
257 passwordLineEdit->setText(value);
258 passwordLineEdit->setReadOnly(true);
259 passwordLineEdit->setStyleSheet(lineStyle);
260 passwordLineEdit->setContentsMargins(0, 0, 0, 0);
261 passwordLineEdit->setEchoMode(QLineEdit::Password);
262 auto *showButton =
263 new QPushButtonShowPassword(passwordLineEdit, m_widgetParent);
264 showButton->setStyleSheet(QString::fromLatin1(kButtonStyle));
265 showButton->setContentsMargins(0, 0, 0, 0);
266 layout->addWidget(showButton);
267 layout->addWidget(passwordLineEdit);
268}
269
270void PasswordDisplayPanel::addField(int position, const QString &field,
271 const QString &value,
272 const AppSettings &s) {
273 const QString trimmedField = field.trimmed();
274 // The password row keeps its value exactly: a leading or trailing space is
275 // part of the password, so the copy button, QR code and revealed text must
276 // return what is stored, as Ctrl+C on the tree and the edit dialog do. Only
277 // a trailing CR from a CRLF-stored entry is dropped — it is a line ending,
278 // not part of the secret. Every other row is `name: value`, where the
279 // whitespace around the value is layout and is trimmed.
280 QString displayValue;
281 if (trimmedField == QObject::tr("Password")) {
282 displayValue = value;
283 if (displayValue.endsWith(QLatin1Char('\r'))) {
284 displayValue.chop(1);
285 }
286 } else {
287 displayValue = value.trimmed();
288 }
289 QFrame *frame = createFieldFrame();
290 addActionButtons(frame->layout(), trimmedField, displayValue, s);
291 const QString lineStyle =
293 ? "QLineEdit, QTextBrowser { border-style: none; background: "
294 "transparent; font-family: monospace; }"
295 : "QLineEdit, QTextBrowser { border-style: none; background: "
296 "transparent; }";
297 // The password, and with it the other secrets an entry holds (a PIN, a
298 // CVV, an API key, what a template marks): behind the eye button too.
299 if (hides(trimmedField, s)) {
300 addHiddenPassword(frame->layout(), trimmedField, displayValue, lineStyle);
301 } else {
302 SecretTextBrowser *browser =
303 valueBrowser(trimmedField, displayValue, lineStyle);
304 connect(browser, &SecretTextBrowser::copied, this,
306 frame->layout()->addWidget(browser);
307 }
308 // Plain text before the name is set: a field name comes from the entry,
309 // and as rich text <img src="/dev/zero"> has Qt read that file without end.
310 auto *label = new QLabel();
311 label->setTextFormat(Qt::PlainText);
312 label->setText(trimmedField);
313 addRow(position, label, frame);
314}
315
316void PasswordDisplayPanel::addRow(int position, QLabel *label, QFrame *frame) {
317 m_grid->addWidget(label, position, 0);
318 m_grid->addWidget(frame, position, 1);
319 label->installEventFilter(this);
320 frame->installEventFilter(this);
321 // The value widgets swallow mouse events before the frame sees them.
322 for (QWidget *child : frame->findChildren<QWidget *>()) {
323 child->installEventFilter(this);
324 }
325}
326
327auto PasswordDisplayPanel::eventFilter(QObject *watched, QEvent *event)
328 -> bool {
329 if (event->type() == QEvent::MouseButtonDblClick &&
330 static_cast<QMouseEvent *>(event)->button() == Qt::LeftButton) {
331 emit editRequested();
332 return true;
333 }
334 return QObject::eventFilter(watched, event);
335}
336
337// The colour is baked into the stylesheet (a `palette(mid)` reference
338// resolves once at polish time), so refreshPalette() re-runs this.
339void PasswordDisplayPanel::applyFrameStyle(QFrame *frame) const {
340 const QString borderColor =
341 m_widgetParent->palette().color(QPalette::Mid).name();
342 frame->setStyleSheet(QStringLiteral(".QFrame{border: 1px solid %1; "
343 "border-radius: 5px;}")
344 .arg(borderColor));
345}
346
348 for (int i = 0; i < m_grid->count(); ++i) {
349 if (auto *frame = qobject_cast<QFrame *>(m_grid->itemAt(i)->widget())) {
350 applyFrameStyle(frame);
351 }
352 }
353}
354
355// Shared by addField() and addOtpField() so they cannot drift apart on
356// spacing or border colour.
358 auto *frame = new QFrame();
359 auto *frameLayout = new QHBoxLayout();
360 frameLayout->setContentsMargins(5, 2, 2, 2);
361 frameLayout->setSpacing(0);
362 frame->setLayout(frameLayout);
363 applyFrameStyle(frame);
364 return frame;
365}
366
367// One label plus one frame, like every row, so grid rows match
368// displayFields()' position counter. hidePassword does not apply (the code
369// expires in seconds, next to a visible countdown), and there is no QR
370// button: a QR of the configuration would show the shared secret.
371void PasswordDisplayPanel::addOtpField(int position, const QString &otpConfig,
372 const AppSettings &s) {
373 const std::optional<Totp::Settings> settings = Totp::parse(otpConfig);
374 if (!settings.has_value()) {
375 // Report the problem without echoing what the user stored: the value is
376 // still a would-be secret, so it gets neither a copy nor a QR button.
377 AppSettings inert = s;
379 inert.useQrencode = false;
380 inert.hidePassword = false; // a message, never behind the eye button
381 addField(position, QObject::tr("OTP code"),
382 QObject::tr("No OTP code found in this password entry"), inert);
383 return;
384 }
385
386 auto *frame = createFieldFrame();
387 auto *otpWidget =
389 s.useMonospace, frame);
390 connect(otpWidget, &OtpCodeWidget::copyRequested, this,
392 frame->layout()->addWidget(otpWidget);
393
394 addRow(position, new QLabel(QObject::tr("OTP code")), frame);
395}
AppSettings, the value object holding every flat QtPass setting.
static auto isOtpFieldName(const QString &name) -> bool
Whether a field name designates the one-time password field.
static auto isSecretFieldName(const QString &name, const QStringList &secrets) -> bool
Whether a field name names a secret: one of secrets occurs in it as whole words, compared case-insens...
static auto isOtpUriValue(const QString &value) -> bool
Whether a field value is an otpauth URI, and therefore a secret.
The NamedValues class is mostly a list of NamedValue but also has a method to take a specific NamedVa...
Definition filecontent.h:58
Live one-time password display: code, copy button and countdown.
OtpCodeWidget(Totp::Settings settings, bool withCopyButton, bool monospace, QWidget *parent=nullptr)
Build a live OTP code display and start its refresh timer.
void copyRequested(const QString &text)
Emitted when the copy button is activated.
auto isSecret(const QString &field, const AppSettings &s) const -> bool
Whether a field is a secret "Hide password" hides: one a template marks (setMarkedSecrets()),...
void qrRequested(const QString &text)
Emitted when a field's QR button is clicked.
QWidget * m_widgetParent
Parent for the buttons the panel creates and source of the palette used by applyFrameStyle(); not own...
void refreshPalette()
Re-derive palette-dependent styling after a runtime theme change.
QStringList m_markedSecrets
Field names the current template marks as secret, compared case-insensitively in isSecret(); set by s...
void setMarkedSecrets(const QStringList &names)
Set the field names a template marks as secret.
void displayFields(const QString &password, const NamedValues &namedValues, const AppSettings &s, const QString &otpConfig={})
Render the password and template fields of a decrypted entry.
auto eventFilter(QObject *watched, QEvent *event) -> bool override
Turn a left double-click on a field row into editRequested().
QGridLayout * m_grid
The grid the rows go into; owned by MainWindow's .ui.
void addActionButtons(QLayout *layout, const QString &field, const QString &value, const AppSettings &s)
Add the copy, QR and open-in-browser buttons a field gets, per the settings.
QBoxLayout * m_container
Layout around the grid whose spacing is 0 while empty and 6 once rows are shown; owned by MainWindow'...
void addField(int position, const QString &field, const QString &value, const AppSettings &s)
Render one name: value row: action buttons, then the value either behind the eye button (see hides())...
void clear()
Remove all field rows and collapse the container spacing.
auto createFieldFrame() -> QFrame *
New bordered frame with a tight horizontal layout, styled by applyFrameStyle(); shared by addField() ...
PasswordDisplayPanel(QGridLayout *grid, QBoxLayout *container, QWidget *widgetParent, QObject *parent=nullptr)
Construct a panel that renders into the given layouts.
void textCopied(const QString &text)
Text was selected and copied out of a field's value (Ctrl+C, the context menu, a drag,...
void addRow(int position, QLabel *label, QFrame *frame)
Put a row's label and value frame into the grid, listening for a double-click on either (and on the f...
void copyRequested(const QString &text)
Emitted when a field's copy button is clicked.
void addOtpField(int position, const QString &otpConfig, const AppSettings &s)
Render the live OTP row: an OtpCodeWidget in a field frame under an "OTP code" label....
void addHiddenPassword(QLayout *layout, const QString &field, const QString &value, const QString &lineStyle)
Add the password, hidden, with a button that shows it.
auto currentOtpCode() const -> QString
The one-time password currently on display, if there is one.
void editRequested()
Emitted on a double-click anywhere on a field row: the user wants to change what they are looking at.
auto hides(const QString &field, const AppSettings &s) const -> bool
Whether a field is shown behind the eye button: the password, a field a template marks,...
void applyFrameStyle(QFrame *frame) const
Give a field frame its rounded border in the current QPalette::Mid colour. The colour is baked into t...
void clicked(const QString &text)
Emitted when the button is activated with the current text payload.
QPushButtonAsQRCode(QString textToCopy="", QWidget *parent=nullptr)
Construct a QPushButtonAsQRCode with an optional initial text.
QPushButtonShowPassword(QLineEdit *line, QWidget *parent=nullptr)
Construct a show-password button linked to the given QLineEdit.
void clicked(const QString &text)
Emitted when the button is activated with the stored text.
QPushButtonWithClipboard(QString textToCopy="", QWidget *parent=nullptr)
Construct a QPushButtonWithClipboard with an optional initial text.
void copied(const QString &text)
Selected text was copied out of the line edit.
SecretLineEdit(QWidget *parent=nullptr)
Constructs the line edit.
A QTextBrowser for decrypted entry text: whatever is copied out of it is marked as a secret and repor...
SecretTextBrowser(QWidget *parent=nullptr)
Constructs the browser.
void copied(const QString &text)
Selected text was copied out of the browser.
static auto parse(const QString &config) -> std::optional< Settings >
Parse an OTP configuration string.
Definition totp.cpp:106
static auto isLaunchableWebUrl(const QString &value) -> bool
Check whether a value is a safe, launchable web URL.
Definition util.cpp:596
static auto linkifyUrls(const QString &text, bool *linked=nullptr) -> QString
Turn plain text into HTML with clickable links for safe web URLs.
Definition util.cpp:627
@ CLIPBOARD_NEVER
Definition enums.h:25
Self-refreshing widget showing an entry's current TOTP code with a countdown bar and optional copy bu...
PasswordDisplayPanel: renders the decrypted fields of an entry (password, template fields,...
Icon button that asks for a stored secret to be shown as a QR code.
Eye-icon button that toggles a QLineEdit between password and plain-text echo mode.
Copy-to-clipboard icon button that carries the text of the field it sits next to.
QLineEdit whose copies, cuts and primary-selection fills carry the secret clipboard hints and are rep...
QTextBrowser for showing decrypted entry text that marks everything copied out of it as a secret.
Plain value-object holding all flat QtPass configuration.
Definition appsettings.h:36
bool hidePassword
Definition appsettings.h:85
Enums::ClipBoardType clipBoardType
Clipboard copy behaviour.
Definition appsettings.h:73
bool useMonospace
Render content in a monospace font.
Definition appsettings.h:96
bool useQrencode
Enable qrencode support.
A name/value pair parsed from a password file field.
Definition filecontent.h:21
RFC 6238 TOTP: otpauth URI parsing, code generation and canonical re-serialisation.
Util: static helpers for the process environment, binary discovery, store paths, URL safety and the l...