Skip to content

Commit 1293022

Browse files
authored
Merge pull request #1213 from deXol/develop1044ImplementTOTPFromQRCode
Add TOTP secret from QR code, fix #1044
2 parents f7bafb2 + 4f9a9e9 commit 1293022

17 files changed

+492
-34
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "src/QZXing"]
2+
path = src/QZXing
3+
url = https://github.com/mooltipass/qzxing.git

gui.pro

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ TARGET = moolticute
66

77
CONFIG += c++11
88

9-
INCLUDEPATH += $$PWD/src $$PWD/src/Settings
9+
INCLUDEPATH += $$PWD/src $$PWD/src/Settings $$PWD/src/utils
1010

1111
mac {
1212
LIBS += -framework ApplicationServices -framework IOKit -framework CoreFoundation -framework Cocoa -framework Foundation
@@ -19,7 +19,8 @@ linux {
1919
}
2020

2121
include(src/QtAwesome/QtAwesome/QtAwesome.pri)
22-
include (src/QSimpleUpdater/QSimpleUpdater.pri)
22+
include(src/QSimpleUpdater/QSimpleUpdater.pri)
23+
include(src/QZXing/src/QZXing.pri)
2324

2425
greaterThan(QT_MAJOR_VERSION, 5) {
2526
include (src/qtcsv6/qtcsv.pri)
@@ -46,6 +47,7 @@ SOURCES += src/main_gui.cpp \
4647
src/PasswordLineEdit.cpp \
4748
src/CredentialsManagement.cpp \
4849
src/utils/GridLayoutUtil.cpp \
50+
src/utils/TOTPReader.cpp \
4951
src/zxcvbn-c/zxcvbn.c \
5052
src/FilesManagement.cpp \
5153
src/SSHManagement.cpp \
@@ -92,6 +94,7 @@ HEADERS += src/MainWindow.h \
9294
src/WSClient.h \
9395
src/RotateSpinner.h \
9496
src/utils/GridLayoutUtil.h \
97+
src/utils/TOTPReader.h \
9598
src/utils/qurltlds_p.h \
9699
src/version.h \
97100
src/AppGui.h \

src/CredentialModel.cpp

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,15 +307,25 @@ qint8 CredentialModel::getAvailableFavorite(qint8 newFav)
307307
}
308308
}
309309

310-
QModelIndex CredentialModel::getServiceIndexByName(const QString &sServiceName, int column) const
310+
QModelIndex CredentialModel::getServiceIndex(const QString &sServiceName, Qt::MatchFlag flag, int column) const
311311
{
312-
QModelIndexList lMatches = match(index(0, column, QModelIndex()), Qt::DisplayRole, sServiceName, 1, Qt::MatchExactly);
312+
QModelIndexList lMatches = match(index(0, column, QModelIndex()), Qt::DisplayRole, sServiceName, 1, flag);
313313
if (!lMatches.isEmpty())
314314
return lMatches.first();
315315

316316
return QModelIndex();
317317
}
318318

319+
QModelIndex CredentialModel::getServiceIndexByName(const QString &sServiceName, int column) const
320+
{
321+
return getServiceIndex(sServiceName, Qt::MatchExactly, column);
322+
}
323+
324+
QModelIndex CredentialModel::getServiceIndexByNamePart(const QString &sServiceName, int column) const
325+
{
326+
return getServiceIndex(sServiceName, Qt::MatchContains, column);
327+
}
328+
319329
LoginItem *CredentialModel::getLoginItemByIndex(const QModelIndex &idx) const
320330
{
321331
return dynamic_cast<LoginItem *>(getItemByIndex(idx));
@@ -346,6 +356,9 @@ void CredentialModel::setTOTP(const QModelIndex &idx, QString secretKey, int tim
346356
if (pLoginItem != nullptr)
347357
{
348358
pLoginItem->setTOTPCredential(secretKey, timeStep, codeSize);
359+
pLoginItem->setTotpTimeStep(timeStep);
360+
pLoginItem->setTotpCodeSize(codeSize);
361+
pLoginItem->setTOTPDeleted(false);
349362
}
350363
}
351364

src/CredentialModel.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class CredentialModel : public QAbstractItemModel
5353
void updateLoginItem(const QModelIndex &idx, const ItemRole &role, const QVariant &vValue);
5454
void clear();
5555
QModelIndex getServiceIndexByName(const QString &sServiceName, int column = 0) const;
56+
QModelIndex getServiceIndexByNamePart(const QString &sServiceName, int column = 0) const;
5657
LoginItem *getLoginItemByIndex(const QModelIndex &idx) const;
5758
ServiceItem *getServiceItemByIndex(const QModelIndex &idx) const;
5859
QString getCategoryName(int catId) const;
@@ -67,6 +68,7 @@ class CredentialModel : public QAbstractItemModel
6768
private:
6869
ServiceItem *addService(const QString &sServiceName);
6970
qint8 getAvailableFavorite(qint8 newFav);
71+
QModelIndex getServiceIndex(const QString &sServiceName, Qt::MatchFlag flag, int column = 0) const;
7072

7173
private:
7274
RootItem *m_pRootItem;

src/CredentialsManagement.cpp

Lines changed: 171 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const QString CredentialsManagement::INVALID_DOMAIN_TEXT =
3535
tr("The following domains are invalid or private: <b><ul><li>%1</li></ul></b>They are not saved.");
3636
const QString CredentialsManagement::INVALID_INPUT_STYLE =
3737
"border: 2px solid red";
38+
const QString CredentialsManagement::TOTP_CONFIRMATION = tr("Confirm TOTP");
3839

3940
CredentialsManagement::CredentialsManagement(QWidget *parent) :
4041
QWidget(parent), ui(new Ui::CredentialsManagement), m_pAddedLoginItem(nullptr)
@@ -256,6 +257,7 @@ void CredentialsManagement::setWsClient(WSClient *c)
256257
connect(wsClient, &WSClient::memMgmtModeChanged, this, &CredentialsManagement::checkDeviceType);
257258
connect(wsClient, &WSClient::advancedMenuChanged, this, &CredentialsManagement::checkDeviceType);
258259
connect(wsClient, &WSClient::deviceConnected, this, &CredentialsManagement::checkDeviceType);
260+
connect(wsClient, &WSClient::memMgmtModeChanged, this, &CredentialsManagement::handleTOTPQR);
259261
connect(wsClient, &WSClient::advancedMenuChanged, this, &CredentialsManagement::handleAdvancedModeChange);
260262
handleAdvancedModeChange(wsClient->get_advancedMenu());
261263
connect(wsClient, &WSClient::displayUserCategories, this,
@@ -597,6 +599,7 @@ void CredentialsManagement::saveSelectedTOTP()
597599
if (pLoginItem != nullptr) {
598600
m_pCredModel->setTOTP(srcIndex, m_pTOTPCred->getSecretKey(), m_pTOTPCred->getTimeStep(), m_pTOTPCred->getCodeSize());
599601
credentialDataChanged();
602+
updateLoginDescription(pLoginItem);
600603
}
601604
}
602605
}
@@ -677,6 +680,14 @@ void CredentialsManagement::saveChanges()
677680
emit wantSaveMemMode();
678681
}
679682

683+
void CredentialsManagement::onMainWindowActivated()
684+
{
685+
if (wsClient->get_memMgmtMode() && wsClient->isMPBLE())
686+
{
687+
onClipboardDataChanged();
688+
}
689+
}
690+
680691
void CredentialsManagement::keyPressEvent(QKeyEvent *event)
681692
{
682693
QWidget::keyPressEvent(event);
@@ -1565,6 +1576,117 @@ QString CredentialsManagement::getFirstDomain(TreeItem *pItem) const
15651576
return Common::getFirstDomain(multDomains);
15661577
}
15671578

1579+
void CredentialsManagement::addCredAndTOTP(const QString &service, TOTPReader::TOTPResult res)
1580+
{
1581+
m_pCredModel->addCredential(service,
1582+
res.login,
1583+
"");
1584+
QModelIndex serviceIdx = m_pCredModel->getServiceIndexByName(service);
1585+
auto *pServiceItem = m_pCredModel->getServiceItemByIndex(serviceIdx);
1586+
auto* pLoginItem = pServiceItem->findLoginByName(res.login);
1587+
QModelIndex loginIndex = m_pCredModelFilter->getProxyIndexFromItem(pLoginItem);
1588+
ui->credentialTreeView->selectionModel()->setCurrentIndex(loginIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
1589+
1590+
pLoginItem->setTOTPCredential(res.secret, res.period, res.digits);
1591+
pLoginItem->setTotpTimeStep(res.period);
1592+
pLoginItem->setTotpCodeSize(res.digits);
1593+
pLoginItem->setTOTPDeleted(false);
1594+
credentialDataChanged();
1595+
updateLoginDescription(pLoginItem);
1596+
}
1597+
1598+
void CredentialsManagement::processTOTPQR(TOTPReader::TOTPResult res)
1599+
{
1600+
if (!res.isValid)
1601+
{
1602+
return;
1603+
}
1604+
1605+
bool matchService = false;
1606+
QModelIndex serviceIdx = m_pCredModel->getServiceIndexByName(res.service);
1607+
if (!serviceIdx.isValid())
1608+
{
1609+
ParseDomain parsedService{res.service};
1610+
if (parsedService.isWebsite())
1611+
{
1612+
// e.g.: TOTP is for test.com, there is an existing test service.
1613+
serviceIdx = m_pCredModel->getServiceIndexByName(parsedService.domain());
1614+
}
1615+
else
1616+
{
1617+
// e.g.: TOTP is for test, there is an existing test.com service.
1618+
serviceIdx = m_pCredModel->getServiceIndexByNamePart(res.service);
1619+
}
1620+
matchService = serviceIdx.isValid();
1621+
}
1622+
1623+
auto *pServiceItem = m_pCredModel->getServiceItemByIndex(serviceIdx);
1624+
if (nullptr != pServiceItem)
1625+
{
1626+
if (matchService)
1627+
{
1628+
// Found service match, confirm if want to add
1629+
auto response = QMessageBox::information(this, TOTP_CONFIRMATION,
1630+
tr("Do you want to add TOTP for <b>%1</b> service?").arg(pServiceItem->name()),
1631+
QMessageBox::Yes|QMessageBox::No);
1632+
if (QMessageBox::No == response)
1633+
{
1634+
return;
1635+
}
1636+
}
1637+
1638+
auto* pLoginItem = pServiceItem->findLoginByName(res.login);
1639+
if (nullptr != pLoginItem)
1640+
{
1641+
QString credName = pLoginItem->getDisplayName();
1642+
QModelIndex loginIndex = m_pCredModelFilter->getProxyIndexFromItem(pLoginItem);
1643+
ui->credentialTreeView->selectionModel()->setCurrentIndex(loginIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
1644+
QString totpMessage = "";
1645+
if (pLoginItem->totpCodeSize() != 0)
1646+
{
1647+
// TOTP already exist, confirm if user wants to overwrite
1648+
totpMessage = tr("There is TOTP saved for credential %1\nDo you want to overwrite TOTP information?");
1649+
}
1650+
else
1651+
{
1652+
totpMessage = tr("Do you want to set TOTP information for %1?");
1653+
}
1654+
auto response = QMessageBox::information(this, TOTP_CONFIRMATION,
1655+
totpMessage.arg(credName),
1656+
QMessageBox::Yes|QMessageBox::No);
1657+
if (QMessageBox::Yes == response)
1658+
{
1659+
pLoginItem->setTOTPCredential(res.secret, res.period, res.digits);
1660+
pLoginItem->setTotpTimeStep(res.period);
1661+
pLoginItem->setTotpCodeSize(res.digits);
1662+
pLoginItem->setTOTPDeleted(false);
1663+
credentialDataChanged();
1664+
updateLoginDescription(pLoginItem);
1665+
}
1666+
}
1667+
else
1668+
{
1669+
auto response = QMessageBox::information(this, TOTP_CONFIRMATION,
1670+
tr("Do you want to create <b>%1</b> login and add TOTP for <b>%2</b> service?").arg(res.login, pServiceItem->name()),
1671+
QMessageBox::Yes|QMessageBox::No);
1672+
if (QMessageBox::Yes == response)
1673+
{
1674+
addCredAndTOTP(pServiceItem->name(), res);
1675+
}
1676+
}
1677+
}
1678+
else
1679+
{
1680+
auto response = QMessageBox::information(this, TOTP_CONFIRMATION,
1681+
tr("<b>%1</b> service does not exist.\nDo you want to create service with <b>%2</b> login and add TOTP for it?").arg(res.service, res.login),
1682+
QMessageBox::Yes|QMessageBox::No);
1683+
if (QMessageBox::Yes == response)
1684+
{
1685+
addCredAndTOTP(res.service, res);
1686+
}
1687+
}
1688+
}
1689+
15681690
void CredentialsManagement::on_toolButtonFavFilter_clicked()
15691691
{
15701692
bool favFilter = m_pCredModelFilter->switchFavFilter();
@@ -1728,7 +1850,7 @@ void CredentialsManagement::on_pushButtonLinkTo_clicked()
17281850
LoginItem *pLoginItem = m_pCredModel->getLoginItemByIndex(srcIndex);
17291851
if (pLoginItem != nullptr)
17301852
{
1731-
linkToName = "<" + pLoginItem->parentItem()->name() + "/" + pLoginItem->name() + ">";
1853+
linkToName = pLoginItem->getDisplayName();
17321854
m_credentialLinkedAddr = pLoginItem->address();
17331855
if (m_linkingMode == LinkingMode::NEW_CREDENTIAL)
17341856
{
@@ -1745,7 +1867,7 @@ void CredentialsManagement::on_pushButtonLinkTo_clicked()
17451867
LoginItem *pLoginItem = m_pCredModel->getLoginItemByIndex(srcIndex);
17461868
if (nullptr != pLoginItem)
17471869
{
1748-
QString linkName = "<" + pLoginItem->parentItem()->name() + "/" + pLoginItem->name() + ">";
1870+
QString linkName = pLoginItem->getDisplayName();
17491871
int ret = QMessageBox::warning(this, "Credential link",
17501872
tr("%1 will use %2 password.\n"
17511873
"Do you want to confirm?").arg(linkName).arg(linkToName),
@@ -1854,3 +1976,50 @@ void CredentialsManagement::onTreeViewContextMenuRequested(const QPoint& pos)
18541976
}
18551977
}
18561978
}
1979+
1980+
void CredentialsManagement::handleTOTPQR(bool isMMM)
1981+
{
1982+
if (!wsClient->isMPBLE())
1983+
{
1984+
return;
1985+
}
1986+
1987+
QClipboard *clipboard = QGuiApplication::clipboard();
1988+
if (isMMM)
1989+
{
1990+
connect(clipboard, &QClipboard::dataChanged, this, &CredentialsManagement::onClipboardDataChanged);
1991+
// When MMM is populated trigger a check for clipboard if QR TOTP image is available
1992+
QTimer::singleShot(QR_PROCESSING_TIMEOUT, this, [this](){ onClipboardDataChanged(); });
1993+
}
1994+
else
1995+
{
1996+
disconnect(clipboard, &QClipboard::dataChanged, 0, 0);
1997+
}
1998+
}
1999+
2000+
void CredentialsManagement::onClipboardDataChanged()
2001+
{
2002+
TOTPReader::TOTPResult res;
2003+
QClipboard *clipboard = QGuiApplication::clipboard();
2004+
2005+
QImage clipImage = clipboard->image();
2006+
static QImage lastImage;
2007+
if (clipImage == lastImage)
2008+
{
2009+
// Only process image from clipboard one time.
2010+
return;
2011+
}
2012+
else
2013+
{
2014+
lastImage = clipImage;
2015+
}
2016+
2017+
if (!clipImage.isNull() && !m_processingQRImage)
2018+
{
2019+
m_processingQRImage = true;
2020+
res = TOTPReader::getQRCodeResult(clipImage);
2021+
// For image dataChanged is triggered twice, prevent double process with this workaround
2022+
QTimer::singleShot(QR_PROCESSING_TIMEOUT, this, [this](){m_processingQRImage = false; });
2023+
}
2024+
processTOTPQR(res);
2025+
}

src/CredentialsManagement.h

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
// Application
2626
#include "WSClient.h"
2727
#include "TOTPCredential.h"
28+
#include "TOTPReader.h"
2829

2930
namespace Ui {
3031
class CredentialsManagement;
@@ -56,6 +57,7 @@ class CredentialsManagement : public QWidget
5657
public slots:
5758
bool confirmDiscardUneditedCredentialChanges(const QModelIndex &proxyIndex = {});
5859
void saveChanges();
60+
void onMainWindowActivated();
5961

6062
protected:
6163
virtual void keyPressEvent(QKeyEvent *event) override;
@@ -124,8 +126,9 @@ private slots:
124126

125127
void onTreeViewContextMenuRequested(const QPoint& pos);
126128

127-
inline int getMaxLoginLength() const { return wsClient->isMPBLE() ? BLE_LOGIN_LENGTH : MINI_LOGIN_LENGTH; }
128-
inline int getMaxPasswordLength() const { return wsClient->isMPBLE() ? BLE_PASSWORD_LENGTH : BLE_PASSWORD_LENGTH; }
129+
void handleTOTPQR(bool isMMM);
130+
131+
void onClipboardDataChanged();
129132

130133
private:
131134
void updateLoginDescription(const QModelIndex &srcIndex);
@@ -158,6 +161,13 @@ private slots:
158161

159162
QString getFirstDomain(TreeItem *pItem) const;
160163

164+
void addCredAndTOTP(const QString& service, TOTPReader::TOTPResult res);
165+
166+
void processTOTPQR(TOTPReader::TOTPResult res);
167+
168+
inline int getMaxLoginLength() const { return wsClient->isMPBLE() ? BLE_LOGIN_LENGTH : MINI_LOGIN_LENGTH; }
169+
inline int getMaxPasswordLength() const { return wsClient->isMPBLE() ? BLE_PASSWORD_LENGTH : BLE_PASSWORD_LENGTH; }
170+
161171
Ui::CredentialsManagement *ui;
162172
CredentialModel *m_pCredModel = nullptr;
163173
CredentialModelFilter *m_pCredModelFilter = nullptr;
@@ -178,6 +188,8 @@ private slots:
178188
bool m_invalidPassword = false;
179189
bool m_invalidDisplayPassword = false;
180190

191+
bool m_processingQRImage = false;
192+
181193
LinkingMode m_linkingMode = LinkingMode::OFF;
182194
QByteArray m_credentialLinkedAddr;
183195
QModelIndex m_credentialToLinkIndex;
@@ -195,8 +207,10 @@ private slots:
195207
static constexpr int BLE_PASSWORD_LENGTH = 64;
196208
static constexpr int BLE_LOGIN_LENGTH = 63;
197209
static constexpr int MINI_LOGIN_LENGTH = 62;
210+
static constexpr int QR_PROCESSING_TIMEOUT = 500;
198211
static const QString INVALID_DOMAIN_TEXT;
199212
static const QString INVALID_INPUT_STYLE;
213+
static const QString TOTP_CONFIRMATION;
200214

201215
signals:
202216
void wantEnterMemMode();

0 commit comments

Comments
 (0)