Skip to content

Commit

Permalink
Merge pull request #1213 from deXol/develop1044ImplementTOTPFromQRCode
Browse files Browse the repository at this point in the history
Add TOTP secret from QR code, fix #1044
  • Loading branch information
limpkin authored May 19, 2024
2 parents f7bafb2 + 4f9a9e9 commit 1293022
Show file tree
Hide file tree
Showing 17 changed files with 492 additions and 34 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "src/QZXing"]
path = src/QZXing
url = https://github.com/mooltipass/qzxing.git
7 changes: 5 additions & 2 deletions gui.pro
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ TARGET = moolticute

CONFIG += c++11

INCLUDEPATH += $$PWD/src $$PWD/src/Settings
INCLUDEPATH += $$PWD/src $$PWD/src/Settings $$PWD/src/utils

mac {
LIBS += -framework ApplicationServices -framework IOKit -framework CoreFoundation -framework Cocoa -framework Foundation
Expand All @@ -19,7 +19,8 @@ linux {
}

include(src/QtAwesome/QtAwesome/QtAwesome.pri)
include (src/QSimpleUpdater/QSimpleUpdater.pri)
include(src/QSimpleUpdater/QSimpleUpdater.pri)
include(src/QZXing/src/QZXing.pri)

greaterThan(QT_MAJOR_VERSION, 5) {
include (src/qtcsv6/qtcsv.pri)
Expand All @@ -46,6 +47,7 @@ SOURCES += src/main_gui.cpp \
src/PasswordLineEdit.cpp \
src/CredentialsManagement.cpp \
src/utils/GridLayoutUtil.cpp \
src/utils/TOTPReader.cpp \
src/zxcvbn-c/zxcvbn.c \
src/FilesManagement.cpp \
src/SSHManagement.cpp \
Expand Down Expand Up @@ -92,6 +94,7 @@ HEADERS += src/MainWindow.h \
src/WSClient.h \
src/RotateSpinner.h \
src/utils/GridLayoutUtil.h \
src/utils/TOTPReader.h \
src/utils/qurltlds_p.h \
src/version.h \
src/AppGui.h \
Expand Down
17 changes: 15 additions & 2 deletions src/CredentialModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -307,15 +307,25 @@ qint8 CredentialModel::getAvailableFavorite(qint8 newFav)
}
}

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

return QModelIndex();
}

QModelIndex CredentialModel::getServiceIndexByName(const QString &sServiceName, int column) const
{
return getServiceIndex(sServiceName, Qt::MatchExactly, column);
}

QModelIndex CredentialModel::getServiceIndexByNamePart(const QString &sServiceName, int column) const
{
return getServiceIndex(sServiceName, Qt::MatchContains, column);
}

LoginItem *CredentialModel::getLoginItemByIndex(const QModelIndex &idx) const
{
return dynamic_cast<LoginItem *>(getItemByIndex(idx));
Expand Down Expand Up @@ -346,6 +356,9 @@ void CredentialModel::setTOTP(const QModelIndex &idx, QString secretKey, int tim
if (pLoginItem != nullptr)
{
pLoginItem->setTOTPCredential(secretKey, timeStep, codeSize);
pLoginItem->setTotpTimeStep(timeStep);
pLoginItem->setTotpCodeSize(codeSize);
pLoginItem->setTOTPDeleted(false);
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/CredentialModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class CredentialModel : public QAbstractItemModel
void updateLoginItem(const QModelIndex &idx, const ItemRole &role, const QVariant &vValue);
void clear();
QModelIndex getServiceIndexByName(const QString &sServiceName, int column = 0) const;
QModelIndex getServiceIndexByNamePart(const QString &sServiceName, int column = 0) const;
LoginItem *getLoginItemByIndex(const QModelIndex &idx) const;
ServiceItem *getServiceItemByIndex(const QModelIndex &idx) const;
QString getCategoryName(int catId) const;
Expand All @@ -67,6 +68,7 @@ class CredentialModel : public QAbstractItemModel
private:
ServiceItem *addService(const QString &sServiceName);
qint8 getAvailableFavorite(qint8 newFav);
QModelIndex getServiceIndex(const QString &sServiceName, Qt::MatchFlag flag, int column = 0) const;

private:
RootItem *m_pRootItem;
Expand Down
173 changes: 171 additions & 2 deletions src/CredentialsManagement.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ const QString CredentialsManagement::INVALID_DOMAIN_TEXT =
tr("The following domains are invalid or private: <b><ul><li>%1</li></ul></b>They are not saved.");
const QString CredentialsManagement::INVALID_INPUT_STYLE =
"border: 2px solid red";
const QString CredentialsManagement::TOTP_CONFIRMATION = tr("Confirm TOTP");

CredentialsManagement::CredentialsManagement(QWidget *parent) :
QWidget(parent), ui(new Ui::CredentialsManagement), m_pAddedLoginItem(nullptr)
Expand Down Expand Up @@ -256,6 +257,7 @@ void CredentialsManagement::setWsClient(WSClient *c)
connect(wsClient, &WSClient::memMgmtModeChanged, this, &CredentialsManagement::checkDeviceType);
connect(wsClient, &WSClient::advancedMenuChanged, this, &CredentialsManagement::checkDeviceType);
connect(wsClient, &WSClient::deviceConnected, this, &CredentialsManagement::checkDeviceType);
connect(wsClient, &WSClient::memMgmtModeChanged, this, &CredentialsManagement::handleTOTPQR);
connect(wsClient, &WSClient::advancedMenuChanged, this, &CredentialsManagement::handleAdvancedModeChange);
handleAdvancedModeChange(wsClient->get_advancedMenu());
connect(wsClient, &WSClient::displayUserCategories, this,
Expand Down Expand Up @@ -597,6 +599,7 @@ void CredentialsManagement::saveSelectedTOTP()
if (pLoginItem != nullptr) {
m_pCredModel->setTOTP(srcIndex, m_pTOTPCred->getSecretKey(), m_pTOTPCred->getTimeStep(), m_pTOTPCred->getCodeSize());
credentialDataChanged();
updateLoginDescription(pLoginItem);
}
}
}
Expand Down Expand Up @@ -677,6 +680,14 @@ void CredentialsManagement::saveChanges()
emit wantSaveMemMode();
}

void CredentialsManagement::onMainWindowActivated()
{
if (wsClient->get_memMgmtMode() && wsClient->isMPBLE())
{
onClipboardDataChanged();
}
}

void CredentialsManagement::keyPressEvent(QKeyEvent *event)
{
QWidget::keyPressEvent(event);
Expand Down Expand Up @@ -1565,6 +1576,117 @@ QString CredentialsManagement::getFirstDomain(TreeItem *pItem) const
return Common::getFirstDomain(multDomains);
}

void CredentialsManagement::addCredAndTOTP(const QString &service, TOTPReader::TOTPResult res)
{
m_pCredModel->addCredential(service,
res.login,
"");
QModelIndex serviceIdx = m_pCredModel->getServiceIndexByName(service);
auto *pServiceItem = m_pCredModel->getServiceItemByIndex(serviceIdx);
auto* pLoginItem = pServiceItem->findLoginByName(res.login);
QModelIndex loginIndex = m_pCredModelFilter->getProxyIndexFromItem(pLoginItem);
ui->credentialTreeView->selectionModel()->setCurrentIndex(loginIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);

pLoginItem->setTOTPCredential(res.secret, res.period, res.digits);
pLoginItem->setTotpTimeStep(res.period);
pLoginItem->setTotpCodeSize(res.digits);
pLoginItem->setTOTPDeleted(false);
credentialDataChanged();
updateLoginDescription(pLoginItem);
}

void CredentialsManagement::processTOTPQR(TOTPReader::TOTPResult res)
{
if (!res.isValid)
{
return;
}

bool matchService = false;
QModelIndex serviceIdx = m_pCredModel->getServiceIndexByName(res.service);
if (!serviceIdx.isValid())
{
ParseDomain parsedService{res.service};
if (parsedService.isWebsite())
{
// e.g.: TOTP is for test.com, there is an existing test service.
serviceIdx = m_pCredModel->getServiceIndexByName(parsedService.domain());
}
else
{
// e.g.: TOTP is for test, there is an existing test.com service.
serviceIdx = m_pCredModel->getServiceIndexByNamePart(res.service);
}
matchService = serviceIdx.isValid();
}

auto *pServiceItem = m_pCredModel->getServiceItemByIndex(serviceIdx);
if (nullptr != pServiceItem)
{
if (matchService)
{
// Found service match, confirm if want to add
auto response = QMessageBox::information(this, TOTP_CONFIRMATION,
tr("Do you want to add TOTP for <b>%1</b> service?").arg(pServiceItem->name()),
QMessageBox::Yes|QMessageBox::No);
if (QMessageBox::No == response)
{
return;
}
}

auto* pLoginItem = pServiceItem->findLoginByName(res.login);
if (nullptr != pLoginItem)
{
QString credName = pLoginItem->getDisplayName();
QModelIndex loginIndex = m_pCredModelFilter->getProxyIndexFromItem(pLoginItem);
ui->credentialTreeView->selectionModel()->setCurrentIndex(loginIndex, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
QString totpMessage = "";
if (pLoginItem->totpCodeSize() != 0)
{
// TOTP already exist, confirm if user wants to overwrite
totpMessage = tr("There is TOTP saved for credential %1\nDo you want to overwrite TOTP information?");
}
else
{
totpMessage = tr("Do you want to set TOTP information for %1?");
}
auto response = QMessageBox::information(this, TOTP_CONFIRMATION,
totpMessage.arg(credName),
QMessageBox::Yes|QMessageBox::No);
if (QMessageBox::Yes == response)
{
pLoginItem->setTOTPCredential(res.secret, res.period, res.digits);
pLoginItem->setTotpTimeStep(res.period);
pLoginItem->setTotpCodeSize(res.digits);
pLoginItem->setTOTPDeleted(false);
credentialDataChanged();
updateLoginDescription(pLoginItem);
}
}
else
{
auto response = QMessageBox::information(this, TOTP_CONFIRMATION,
tr("Do you want to create <b>%1</b> login and add TOTP for <b>%2</b> service?").arg(res.login, pServiceItem->name()),
QMessageBox::Yes|QMessageBox::No);
if (QMessageBox::Yes == response)
{
addCredAndTOTP(pServiceItem->name(), res);
}
}
}
else
{
auto response = QMessageBox::information(this, TOTP_CONFIRMATION,
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),
QMessageBox::Yes|QMessageBox::No);
if (QMessageBox::Yes == response)
{
addCredAndTOTP(res.service, res);
}
}
}

void CredentialsManagement::on_toolButtonFavFilter_clicked()
{
bool favFilter = m_pCredModelFilter->switchFavFilter();
Expand Down Expand Up @@ -1728,7 +1850,7 @@ void CredentialsManagement::on_pushButtonLinkTo_clicked()
LoginItem *pLoginItem = m_pCredModel->getLoginItemByIndex(srcIndex);
if (pLoginItem != nullptr)
{
linkToName = "<" + pLoginItem->parentItem()->name() + "/" + pLoginItem->name() + ">";
linkToName = pLoginItem->getDisplayName();
m_credentialLinkedAddr = pLoginItem->address();
if (m_linkingMode == LinkingMode::NEW_CREDENTIAL)
{
Expand All @@ -1745,7 +1867,7 @@ void CredentialsManagement::on_pushButtonLinkTo_clicked()
LoginItem *pLoginItem = m_pCredModel->getLoginItemByIndex(srcIndex);
if (nullptr != pLoginItem)
{
QString linkName = "<" + pLoginItem->parentItem()->name() + "/" + pLoginItem->name() + ">";
QString linkName = pLoginItem->getDisplayName();
int ret = QMessageBox::warning(this, "Credential link",
tr("%1 will use %2 password.\n"
"Do you want to confirm?").arg(linkName).arg(linkToName),
Expand Down Expand Up @@ -1854,3 +1976,50 @@ void CredentialsManagement::onTreeViewContextMenuRequested(const QPoint& pos)
}
}
}

void CredentialsManagement::handleTOTPQR(bool isMMM)
{
if (!wsClient->isMPBLE())
{
return;
}

QClipboard *clipboard = QGuiApplication::clipboard();
if (isMMM)
{
connect(clipboard, &QClipboard::dataChanged, this, &CredentialsManagement::onClipboardDataChanged);
// When MMM is populated trigger a check for clipboard if QR TOTP image is available
QTimer::singleShot(QR_PROCESSING_TIMEOUT, this, [this](){ onClipboardDataChanged(); });
}
else
{
disconnect(clipboard, &QClipboard::dataChanged, 0, 0);
}
}

void CredentialsManagement::onClipboardDataChanged()
{
TOTPReader::TOTPResult res;
QClipboard *clipboard = QGuiApplication::clipboard();

QImage clipImage = clipboard->image();
static QImage lastImage;
if (clipImage == lastImage)
{
// Only process image from clipboard one time.
return;
}
else
{
lastImage = clipImage;
}

if (!clipImage.isNull() && !m_processingQRImage)
{
m_processingQRImage = true;
res = TOTPReader::getQRCodeResult(clipImage);
// For image dataChanged is triggered twice, prevent double process with this workaround
QTimer::singleShot(QR_PROCESSING_TIMEOUT, this, [this](){m_processingQRImage = false; });
}
processTOTPQR(res);
}
18 changes: 16 additions & 2 deletions src/CredentialsManagement.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
// Application
#include "WSClient.h"
#include "TOTPCredential.h"
#include "TOTPReader.h"

namespace Ui {
class CredentialsManagement;
Expand Down Expand Up @@ -56,6 +57,7 @@ class CredentialsManagement : public QWidget
public slots:
bool confirmDiscardUneditedCredentialChanges(const QModelIndex &proxyIndex = {});
void saveChanges();
void onMainWindowActivated();

protected:
virtual void keyPressEvent(QKeyEvent *event) override;
Expand Down Expand Up @@ -124,8 +126,9 @@ private slots:

void onTreeViewContextMenuRequested(const QPoint& pos);

inline int getMaxLoginLength() const { return wsClient->isMPBLE() ? BLE_LOGIN_LENGTH : MINI_LOGIN_LENGTH; }
inline int getMaxPasswordLength() const { return wsClient->isMPBLE() ? BLE_PASSWORD_LENGTH : BLE_PASSWORD_LENGTH; }
void handleTOTPQR(bool isMMM);

void onClipboardDataChanged();

private:
void updateLoginDescription(const QModelIndex &srcIndex);
Expand Down Expand Up @@ -158,6 +161,13 @@ private slots:

QString getFirstDomain(TreeItem *pItem) const;

void addCredAndTOTP(const QString& service, TOTPReader::TOTPResult res);

void processTOTPQR(TOTPReader::TOTPResult res);

inline int getMaxLoginLength() const { return wsClient->isMPBLE() ? BLE_LOGIN_LENGTH : MINI_LOGIN_LENGTH; }
inline int getMaxPasswordLength() const { return wsClient->isMPBLE() ? BLE_PASSWORD_LENGTH : BLE_PASSWORD_LENGTH; }

Ui::CredentialsManagement *ui;
CredentialModel *m_pCredModel = nullptr;
CredentialModelFilter *m_pCredModelFilter = nullptr;
Expand All @@ -178,6 +188,8 @@ private slots:
bool m_invalidPassword = false;
bool m_invalidDisplayPassword = false;

bool m_processingQRImage = false;

LinkingMode m_linkingMode = LinkingMode::OFF;
QByteArray m_credentialLinkedAddr;
QModelIndex m_credentialToLinkIndex;
Expand All @@ -195,8 +207,10 @@ private slots:
static constexpr int BLE_PASSWORD_LENGTH = 64;
static constexpr int BLE_LOGIN_LENGTH = 63;
static constexpr int MINI_LOGIN_LENGTH = 62;
static constexpr int QR_PROCESSING_TIMEOUT = 500;
static const QString INVALID_DOMAIN_TEXT;
static const QString INVALID_INPUT_STYLE;
static const QString TOTP_CONFIRMATION;

signals:
void wantEnterMemMode();
Expand Down
Loading

0 comments on commit 1293022

Please sign in to comment.