1 /*
2  *  Copyright (C) 2014 Felix Geyer <debfx@fobos.de>
3  *  Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
4  *
5  *  This program is free software: you can redistribute it and/or modify
6  *  it under the terms of the GNU General Public License as published by
7  *  the Free Software Foundation, either version 2 or (at your option)
8  *  version 3 of the License.
9  *
10  *  This program is distributed in the hope that it will be useful,
11  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  *  GNU General Public License for more details.
14  *
15  *  You should have received a copy of the GNU General Public License
16  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include "PasswordEdit.h"
20 
21 #include "core/Config.h"
22 #include "core/Resources.h"
23 #include "gui/Font.h"
24 #include "gui/PasswordGeneratorWidget.h"
25 #include "gui/osutils/OSUtils.h"
26 #include "gui/styles/StateColorPalette.h"
27 
28 #include <QDialog>
29 #include <QTimer>
30 #include <QToolTip>
31 #include <QVBoxLayout>
32 
PasswordEdit(QWidget * parent)33 PasswordEdit::PasswordEdit(QWidget* parent)
34     : QLineEdit(parent)
35 {
36     const QIcon errorIcon = resources()->icon("dialog-error");
37     m_errorAction = addAction(errorIcon, QLineEdit::TrailingPosition);
38     m_errorAction->setVisible(false);
39     m_errorAction->setToolTip(tr("Passwords do not match"));
40 
41     const QIcon correctIcon = resources()->icon("dialog-ok");
42     m_correctAction = addAction(correctIcon, QLineEdit::TrailingPosition);
43     m_correctAction->setVisible(false);
44     m_correctAction->setToolTip(tr("Passwords match so far"));
45 
46     setEchoMode(QLineEdit::Password);
47 
48     // use a monospace font for the password field
49     QFont passwordFont = Font::fixedFont();
50     passwordFont.setLetterSpacing(QFont::PercentageSpacing, 110);
51     setFont(passwordFont);
52 
53     // Prevent conflicts with global Mac shortcuts (force Control on all platforms)
54 #ifdef Q_OS_MAC
55     auto modifier = Qt::META;
56 #else
57     auto modifier = Qt::CTRL;
58 #endif
59 
60     m_toggleVisibleAction = new QAction(
61         resources()->onOffIcon("password-show", false),
62         tr("Toggle Password (%1)").arg(QKeySequence(modifier + Qt::Key_H).toString(QKeySequence::NativeText)),
63         nullptr);
64     m_toggleVisibleAction->setCheckable(true);
65     m_toggleVisibleAction->setShortcut(modifier + Qt::Key_H);
66     m_toggleVisibleAction->setShortcutContext(Qt::WidgetShortcut);
67     addAction(m_toggleVisibleAction, QLineEdit::TrailingPosition);
68     connect(m_toggleVisibleAction, &QAction::triggered, this, &PasswordEdit::setShowPassword);
69 
70     m_passwordGeneratorAction = new QAction(
71         resources()->icon("password-generator"),
72         tr("Generate Password (%1)").arg(QKeySequence(modifier + Qt::Key_G).toString(QKeySequence::NativeText)),
73         nullptr);
74     m_passwordGeneratorAction->setShortcut(modifier + Qt::Key_G);
75     m_passwordGeneratorAction->setShortcutContext(Qt::WidgetShortcut);
76     addAction(m_passwordGeneratorAction, QLineEdit::TrailingPosition);
77     m_passwordGeneratorAction->setVisible(false);
78 
79     m_capslockAction =
80         new QAction(resources()->icon("dialog-warning", true, StateColorPalette().color(StateColorPalette::Error)),
81                     tr("Warning: Caps Lock enabled!"),
82                     nullptr);
83     addAction(m_capslockAction, QLineEdit::LeadingPosition);
84     m_capslockAction->setVisible(false);
85 }
86 
setRepeatPartner(PasswordEdit * repeatEdit)87 void PasswordEdit::setRepeatPartner(PasswordEdit* repeatEdit)
88 {
89     m_repeatPasswordEdit = repeatEdit;
90     m_repeatPasswordEdit->setParentPasswordEdit(this);
91 
92     connect(this, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(autocompletePassword(QString)));
93     connect(this, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(updateRepeatStatus()));
94     connect(m_repeatPasswordEdit, SIGNAL(textChanged(QString)), m_repeatPasswordEdit, SLOT(updateRepeatStatus()));
95 }
96 
setParentPasswordEdit(PasswordEdit * parent)97 void PasswordEdit::setParentPasswordEdit(PasswordEdit* parent)
98 {
99     m_parentPasswordEdit = parent;
100     // Hide actions
101     m_toggleVisibleAction->setVisible(false);
102     m_passwordGeneratorAction->setVisible(false);
103 }
104 
enablePasswordGenerator()105 void PasswordEdit::enablePasswordGenerator()
106 {
107     if (!m_passwordGeneratorAction->isVisible()) {
108         m_passwordGeneratorAction->setVisible(true);
109         connect(m_passwordGeneratorAction, &QAction::triggered, this, &PasswordEdit::popupPasswordGenerator);
110     }
111 }
112 
setShowPassword(bool show)113 void PasswordEdit::setShowPassword(bool show)
114 {
115     setEchoMode(show ? QLineEdit::Normal : QLineEdit::Password);
116     m_toggleVisibleAction->setIcon(resources()->onOffIcon("password-show", show));
117     m_toggleVisibleAction->setChecked(show);
118 
119     if (m_repeatPasswordEdit) {
120         m_repeatPasswordEdit->setEchoMode(show ? QLineEdit::Normal : QLineEdit::Password);
121         if (!config()->get(Config::Security_PasswordsRepeatVisible).toBool()) {
122             m_repeatPasswordEdit->setEnabled(!show);
123             m_repeatPasswordEdit->setText(text());
124         } else {
125             m_repeatPasswordEdit->setEnabled(true);
126         }
127     }
128 }
129 
isPasswordVisible() const130 bool PasswordEdit::isPasswordVisible() const
131 {
132     return echoMode() == QLineEdit::Normal;
133 }
134 
popupPasswordGenerator()135 void PasswordEdit::popupPasswordGenerator()
136 {
137     auto generator = PasswordGeneratorWidget::popupGenerator(this);
138     generator->setPasswordVisible(isPasswordVisible());
139     generator->setPasswordLength(text().length());
140 
141     connect(generator, SIGNAL(appliedPassword(QString)), SLOT(setText(QString)));
142     if (m_repeatPasswordEdit) {
143         connect(generator, SIGNAL(appliedPassword(QString)), m_repeatPasswordEdit, SLOT(setText(QString)));
144     }
145 }
146 
updateRepeatStatus()147 void PasswordEdit::updateRepeatStatus()
148 {
149     static const auto stylesheetTemplate = QStringLiteral("QLineEdit { background: %1; }");
150     if (!m_parentPasswordEdit) {
151         return;
152     }
153 
154     const auto otherPassword = m_parentPasswordEdit->text();
155     const auto password = text();
156     if (otherPassword != password) {
157         bool isCorrect = false;
158         StateColorPalette statePalette;
159         QColor color = statePalette.color(StateColorPalette::ColorRole::Error);
160         if (!password.isEmpty() && otherPassword.startsWith(password)) {
161             color = statePalette.color(StateColorPalette::ColorRole::Incomplete);
162             isCorrect = true;
163         }
164         setStyleSheet(stylesheetTemplate.arg(color.name()));
165         m_correctAction->setVisible(isCorrect);
166         m_errorAction->setVisible(!isCorrect);
167     } else {
168         m_correctAction->setVisible(false);
169         m_errorAction->setVisible(false);
170         setStyleSheet("");
171     }
172 }
173 
autocompletePassword(const QString & password)174 void PasswordEdit::autocompletePassword(const QString& password)
175 {
176     if (!config()->get(Config::Security_PasswordsRepeatVisible).toBool() && echoMode() == QLineEdit::Normal) {
177         setText(password);
178     }
179 }
180 
event(QEvent * event)181 bool PasswordEdit::event(QEvent* event)
182 {
183     if (isVisible()
184         && (event->type() == QEvent::KeyPress || event->type() == QEvent::KeyRelease
185             || event->type() == QEvent::FocusIn)) {
186         checkCapslockState();
187     }
188     return QLineEdit::event(event);
189 }
190 
checkCapslockState()191 void PasswordEdit::checkCapslockState()
192 {
193     if (m_parentPasswordEdit) {
194         return;
195     }
196 
197     bool newCapslockState = osUtils->isCapslockEnabled();
198     if (newCapslockState != m_capslockState) {
199         m_capslockState = newCapslockState;
200         m_capslockAction->setVisible(newCapslockState);
201 
202         // Force repaint to avoid rendering glitches of QLineEdit contents
203         repaint();
204 
205         emit capslockToggled(m_capslockState);
206 
207         if (newCapslockState) {
208             QTimer::singleShot(
209                 150, [this] { QToolTip::showText(mapToGlobal(rect().bottomLeft()), m_capslockAction->text()); });
210         } else if (QToolTip::isVisible()) {
211             QToolTip::hideText();
212         }
213     }
214 }
215