From bb16dc6d016b53d06a951724916bc41498ae63df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adolfo=20E=2E=20Garc=C3=ADa?= Date: Fri, 19 Oct 2018 12:42:49 -0600 Subject: [PATCH] Add QR code generator for TOTP export (#1167) * Resolves #764 * Add libqrencode and qtsvg dependencies * Ensure QR code remains square * Auto-close QR code dialog when database is locked * Add databaseLocked() Signal to databaseWidget * Correct otpauth URI output in Totp::writeSettings(...) --- CMakeLists.txt | 8 +- Dockerfile | 6 +- ci/trusty/Dockerfile | 4 +- cmake/FindQREncode.cmake | 22 +++++ snapcraft.yaml | 2 + src/CMakeLists.txt | 11 ++- src/core/Entry.cpp | 2 +- src/gui/DatabaseWidget.cpp | 14 +++ src/gui/DatabaseWidget.h | 2 + src/gui/MainWindow.cpp | 2 + src/gui/MainWindow.ui | 8 +- src/gui/SquareSvgWidget.cpp | 28 ++++++ src/gui/SquareSvgWidget.h | 33 +++++++ src/gui/TotpExportSettingsDialog.cpp | 117 ++++++++++++++++++++++++ src/gui/TotpExportSettingsDialog.h | 57 ++++++++++++ src/qrcode/CMakeLists.txt | 21 +++++ src/qrcode/QrCode.cpp | 132 +++++++++++++++++++++++++++ src/qrcode/QrCode.h | 78 ++++++++++++++++ src/qrcode/QrCode_p.h | 33 +++++++ src/totp/totp.cpp | 17 +++- src/totp/totp.h | 3 +- 21 files changed, 584 insertions(+), 16 deletions(-) create mode 100644 cmake/FindQREncode.cmake create mode 100644 src/gui/SquareSvgWidget.cpp create mode 100644 src/gui/SquareSvgWidget.h create mode 100644 src/gui/TotpExportSettingsDialog.cpp create mode 100644 src/gui/TotpExportSettingsDialog.h create mode 100644 src/qrcode/CMakeLists.txt create mode 100644 src/qrcode/QrCode.cpp create mode 100644 src/qrcode/QrCode.h create mode 100644 src/qrcode/QrCode_p.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 0edd766ff..41a6b9403 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -294,16 +294,16 @@ endif(WITH_TESTS) include(CLangFormat) if(UNIX AND NOT APPLE) - find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Test LinguistTools DBus REQUIRED) + find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Svg Test LinguistTools DBus REQUIRED) elseif(APPLE) - find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Test LinguistTools REQUIRED + find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Svg Test LinguistTools REQUIRED HINTS /usr/local/Cellar/qt/*/lib/cmake ENV PATH ) find_package(Qt5 COMPONENTS MacExtras HINTS /usr/local/Cellar/qt/*/lib/cmake ENV PATH ) else() - find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Test LinguistTools REQUIRED) + find_package(Qt5 COMPONENTS Core Network Concurrent Widgets Svg Test LinguistTools REQUIRED) endif() if(Qt5Core_VERSION VERSION_LESS "5.2.0") @@ -339,6 +339,8 @@ find_package(Argon2 REQUIRED) find_package(ZLIB REQUIRED) +find_package(QREncode REQUIRED) + set(CMAKE_REQUIRED_INCLUDES ${ZLIB_INCLUDE_DIR}) if(ZLIB_VERSION_STRING VERSION_LESS "1.2.0") diff --git a/Dockerfile b/Dockerfile index 89ee04464..1054872ad 100644 --- a/Dockerfile +++ b/Dockerfile @@ -16,7 +16,7 @@ FROM ubuntu:14.04 -ENV REBUILD_COUNTER=8 +ENV REBUILD_COUNTER=10 ENV QT5_VERSION=qt510 ENV QT5_PPA_VERSION=qt-5.10.1 @@ -50,12 +50,14 @@ RUN set -x \ ${QT5_VERSION}x11extras \ ${QT5_VERSION}translations \ ${QT5_VERSION}imageformats \ + ${QT5_VERSION}svg \ zlib1g-dev \ libxi-dev \ libxtst-dev \ mesa-common-dev \ libyubikey-dev \ - libykpers-1-dev + libykpers-1-dev \ + libqrencode-dev ENV PATH="/opt/${QT5_VERSION}/bin:${PATH}" ENV CMAKE_PREFIX_PATH="/opt/${QT5_VERSION}/lib/cmake" diff --git a/ci/trusty/Dockerfile b/ci/trusty/Dockerfile index 04aee25a5..cd69639d2 100644 --- a/ci/trusty/Dockerfile +++ b/ci/trusty/Dockerfile @@ -18,7 +18,7 @@ FROM ubuntu:14.04 -ENV REBUILD_COUNTER=4 +ENV REBUILD_COUNTER=5 ENV QT5_VERSION=qt53 ENV QT5_PPA_VERSION=${QT5_VERSION}2 @@ -49,11 +49,13 @@ RUN set -x \ ${QT5_VERSION}tools \ ${QT5_VERSION}x11extras \ ${QT5_VERSION}translations \ + ${QT5_VERSION}svg \ zlib1g-dev \ libyubikey-dev \ libykpers-1-dev \ libxi-dev \ libxtst-dev \ + libqrencode-dev \ xvfb ENV PATH="/opt/${QT5_VERSION}/bin:${PATH}" diff --git a/cmake/FindQREncode.cmake b/cmake/FindQREncode.cmake new file mode 100644 index 000000000..6328d9699 --- /dev/null +++ b/cmake/FindQREncode.cmake @@ -0,0 +1,22 @@ +# Copyright (C) 2017 KeePassXC Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 or (at your option) +# version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +find_path(QRENCODE_INCLUDE_DIR qrencode.h) +find_library(QRENCODE_LIBRARY qrencode) + +mark_as_advanced(QRENCODE_LIBRARY QRENCODE_INCLUDE_DIR) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(QREncode DEFAULT_MSG QRENCODE_LIBRARY QRENCODE_INCLUDE_DIR) diff --git a/snapcraft.yaml b/snapcraft.yaml index d9321d748..d9c08ec9c 100644 --- a/snapcraft.yaml +++ b/snapcraft.yaml @@ -39,6 +39,7 @@ parts: - libgcrypt20-dev - libqt5x11extras5-dev - qtbase5-dev + - qtsvg5-dev - qttools5-dev - qttools5-dev-tools - zlib1g-dev @@ -48,6 +49,7 @@ parts: - libykpers-1-dev - libsodium-dev - libargon2-0-dev + - libqrencode-dev stage-packages: - dbus - qttranslations5-l10n # common translations diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 51add8968..c70de316b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -127,8 +127,10 @@ set(keepassx_SOURCES gui/ApplicationSettingsWidget.cpp gui/SearchWidget.cpp gui/SortFilterHideProxyModel.cpp + gui/SquareSvgWidget.cpp gui/TotpSetupDialog.cpp gui/TotpDialog.cpp + gui/TotpExportSettingsDialog.cpp gui/UnlockDatabaseWidget.cpp gui/UnlockDatabaseDialog.cpp gui/WelcomeWidget.cpp @@ -225,6 +227,8 @@ endif() add_subdirectory(autotype) add_subdirectory(cli) +add_subdirectory(qrcode) +set(qrcode_LIB qrcode) add_subdirectory(sshagent) if(WITH_XC_SSHAGENT) @@ -270,9 +274,10 @@ target_link_libraries(keepassx_core autotype ${keepassxcbrowser_LIB} ${sshagent_LIB} + ${qrcode_LIB} Qt5::Core - Qt5::Network Qt5::Concurrent + Qt5::Network Qt5::Widgets ${CURL_LIBRARIES} ${YUBIKEY_LIBRARIES} @@ -280,7 +285,9 @@ target_link_libraries(keepassx_core ${ARGON2_LIBRARIES} ${GCRYPT_LIBRARIES} ${GPGERROR_LIBRARIES} - ${ZLIB_LIBRARIES}) + ${YUBIKEY_LIBRARIES} + ${ZLIB_LIBRARIES} + ${ZXCVBN_LIBRARIES}) if(APPLE) target_link_libraries(keepassx_core "-framework Foundation") diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 929447f9c..6ce613d91 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -369,7 +369,7 @@ void Entry::setTotp(QSharedPointer settings) beginUpdate(); m_data.totpSettings = settings; - auto text = Totp::writeSettings(m_data.totpSettings); + auto text = Totp::writeSettings(m_data.totpSettings, title(), username()); if (m_attributes->hasKey(Totp::ATTRIBUTE_OTP)) { m_attributes->set(Totp::ATTRIBUTE_OTP, text, true); } else { diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 1e3854998..869afbd24 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -49,6 +49,7 @@ #include "gui/MessageBox.h" #include "gui/TotpSetupDialog.h" #include "gui/TotpDialog.h" +#include "gui/TotpExportSettingsDialog.h" #include "gui/UnlockDatabaseDialog.h" #include "gui/UnlockDatabaseWidget.h" #include "gui/entry/EditEntryWidget.h" @@ -572,6 +573,18 @@ void DatabaseWidget::copyAttribute(QAction* action) currentEntry->resolveMultiplePlaceholders(currentEntry->attributes()->value(action->data().toString()))); } +void DatabaseWidget::showTotpKeyQrCode() +{ + Entry* currentEntry = m_entryView->currentEntry(); + Q_ASSERT(currentEntry); + if (!currentEntry) { + return; + } + + auto totpDisplayDialog = new TotpExportSettingsDialog(this, currentEntry); + totpDisplayDialog->open(); +} + void DatabaseWidget::setClipboardTextAndMinimize(const QString& text) { clipboard()->setText(text); @@ -1171,6 +1184,7 @@ void DatabaseWidget::lock() Database* newDb = new Database(); newDb->metadata()->setName(m_db->metadata()->name()); replaceDatabase(newDb); + emit lockedDatabase(); } void DatabaseWidget::updateFilePath(const QString& filePath) diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index d1598041f..a15158c2c 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -125,6 +125,7 @@ signals: void pressedEntry(Entry* selectedEntry); void pressedGroup(Group* selectedGroup); void unlockedDatabase(); + void lockedDatabase(); void listModeAboutToActivate(); void listModeActivated(); void searchModeAboutToActivate(); @@ -146,6 +147,7 @@ public slots: void copyNotes(); void copyAttribute(QAction* action); void showTotp(); + void showTotpKeyQrCode(); void copyTotp(); void setupTotp(); void performAutoType(); diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 733ac0163..a729f588e 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -287,6 +287,7 @@ MainWindow::MainWindow() m_actionMultiplexer.connect(m_ui->actionEntrySetupTotp, SIGNAL(triggered()), SLOT(setupTotp())); m_actionMultiplexer.connect(m_ui->actionEntryCopyTotp, SIGNAL(triggered()), SLOT(copyTotp())); + m_actionMultiplexer.connect(m_ui->actionEntryTotpQRCode, SIGNAL(triggered()), SLOT(showTotpKeyQrCode())); m_actionMultiplexer.connect(m_ui->actionEntryCopyTitle, SIGNAL(triggered()), SLOT(copyTitle())); m_actionMultiplexer.connect(m_ui->actionEntryCopyUsername, SIGNAL(triggered()), SLOT(copyUsername())); m_actionMultiplexer.connect(m_ui->actionEntryCopyPassword, SIGNAL(triggered()), SLOT(copyPassword())); @@ -478,6 +479,7 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode) m_ui->actionEntryTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); m_ui->actionEntryCopyTotp->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); m_ui->actionEntrySetupTotp->setEnabled(singleEntrySelected); + m_ui->actionEntryTotpQRCode->setEnabled(singleEntrySelected && dbWidget->currentEntryHasTotp()); m_ui->actionGroupNew->setEnabled(groupSelected); m_ui->actionGroupEdit->setEnabled(groupSelected); m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup()); diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 9f6041312..2c74c706e 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -251,6 +251,7 @@ + @@ -577,7 +578,12 @@ - Show TOTP + Show TOTP... + + + + + Show TOTP QR Code... diff --git a/src/gui/SquareSvgWidget.cpp b/src/gui/SquareSvgWidget.cpp new file mode 100644 index 000000000..5a907e95b --- /dev/null +++ b/src/gui/SquareSvgWidget.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "SquareSvgWidget.h" + +bool SquareSvgWidget::hasHeightForWidth() const +{ + return true; +} + +int SquareSvgWidget::heightForWidth(int width) const +{ + return width; +} diff --git a/src/gui/SquareSvgWidget.h b/src/gui/SquareSvgWidget.h new file mode 100644 index 000000000..3fcbbbffb --- /dev/null +++ b/src/gui/SquareSvgWidget.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSX_SquareSvgWidget_H +#define KEEPASSX_SquareSvgWidget_H + +#include + +class SquareSvgWidget : public QSvgWidget +{ +public: + SquareSvgWidget() = default; + ~SquareSvgWidget() override = default; + + bool hasHeightForWidth() const override; + int heightForWidth(int width) const override; +}; + +#endif // KEEPASSX_SquareSvgWidget_H diff --git a/src/gui/TotpExportSettingsDialog.cpp b/src/gui/TotpExportSettingsDialog.cpp new file mode 100644 index 000000000..dc7fcafd7 --- /dev/null +++ b/src/gui/TotpExportSettingsDialog.cpp @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "TotpExportSettingsDialog.h" +#include "core/Config.h" +#include "core/Entry.h" +#include "gui/Clipboard.h" +#include "gui/DatabaseWidget.h" +#include "gui/SquareSvgWidget.h" +#include "qrcode/QrCode.h" +#include "totp/totp.h" + +#include +#include +#include +#include +#include +#include +#include + +TotpExportSettingsDialog::TotpExportSettingsDialog(DatabaseWidget* parent, Entry* entry) + : QDialog(parent) + , m_timer(new QTimer(this)) + , m_verticalLayout(new QVBoxLayout()) + , m_totpSvgWidget(new SquareSvgWidget()) + , m_countDown(new QLabel()) + , m_warningLabel(new QLabel()) + , m_buttonBox(new QDialogButtonBox(QDialogButtonBox::Close | QDialogButtonBox::Ok)) +{ + m_verticalLayout->addWidget(m_warningLabel); + m_verticalLayout->addItem(new QSpacerItem(0, 0)); + + m_verticalLayout->addStretch(0); + m_verticalLayout->addWidget(m_totpSvgWidget); + m_verticalLayout->addStretch(0); + m_verticalLayout->addWidget(m_countDown); + m_verticalLayout->addWidget(m_buttonBox); + + setLayout(m_verticalLayout); + setAttribute(Qt::WA_DeleteOnClose); + + connect(m_buttonBox, SIGNAL(rejected()), SLOT(close())); + connect(m_buttonBox, SIGNAL(accepted()), SLOT(copyToClipboard())); + connect(m_timer, SIGNAL(timeout()), this, SLOT(autoClose())); + connect(parent, SIGNAL(lockedDatabase()), this, SLOT(close())); + + m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Copy")); + m_countDown->setAlignment(Qt::AlignCenter); + + m_secTillClose = 45; + autoClose(); + m_timer->start(1000); + + const auto totpSettings = entry->totpSettings(); + if (totpSettings->custom || !totpSettings->encoder.shortName.isEmpty()) { + m_warningLabel->setWordWrap(true); + m_warningLabel->setMargin(5); + m_warningLabel->setText(tr("NOTE: These TOTP settings are custom and may not work with other authenticators.", + "TOTP QR code dialog warning")); + } else { + m_warningLabel->hide(); + } + + m_totpUri = Totp::writeSettings(entry->totpSettings(), entry->title(), entry->username(), true); + const QrCode qrc(m_totpUri); + + if (qrc.isValid()) { + QBuffer buffer; + qrc.writeSvg(&buffer, logicalDpiX()); + m_totpSvgWidget->load(buffer.data()); + const int minsize = static_cast(logicalDpiX() * 2.5); + m_totpSvgWidget->setMinimumSize(minsize, minsize); + } else { + auto errorBox = new QMessageBox(parent); + errorBox->setAttribute(Qt::WA_DeleteOnClose); + errorBox->setIcon(QMessageBox::Warning); + errorBox->setText(tr("There was an error creating the QR code.")); + errorBox->exec(); + close(); + } + + show(); +} + +void TotpExportSettingsDialog::copyToClipboard() +{ + clipboard()->setText(m_totpUri); + if (config()->get("MinimizeOnCopy").toBool()) { + static_cast(parent())->window()->showMinimized(); + } +} + +void TotpExportSettingsDialog::autoClose() +{ + if (--m_secTillClose > 0) { + m_countDown->setText(tr("Closing in %1 seconds.").arg(m_secTillClose)); + } else { + m_timer->stop(); + close(); + } +} + +TotpExportSettingsDialog::~TotpExportSettingsDialog() = default; diff --git a/src/gui/TotpExportSettingsDialog.h b/src/gui/TotpExportSettingsDialog.h new file mode 100644 index 000000000..7797533d0 --- /dev/null +++ b/src/gui/TotpExportSettingsDialog.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSX_TotpExportSettingsDialog_H +#define KEEPASSX_TotpExportSettingsDialog_H + +#include "core/Database.h" +#include "core/Entry.h" +#include "gui/DatabaseWidget.h" +#include +#include +#include + +class QVBoxLayout; +class SquareSvgWidget; +class QLabel; +class QDialogButtonBox; + +class TotpExportSettingsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit TotpExportSettingsDialog(DatabaseWidget* parent = nullptr, Entry* entry = nullptr); + ~TotpExportSettingsDialog(); + +private slots: + void copyToClipboard(); + void autoClose(); + +private: + int m_secTillClose; + QString m_totpUri; + QTimer* m_timer; + + QVBoxLayout* m_verticalLayout; + SquareSvgWidget* m_totpSvgWidget; + QLabel* m_countDown; + QLabel* m_warningLabel; + QDialogButtonBox* m_buttonBox; +}; + +#endif // KEEPASSX_TOTPEXPORTSETTINGSDIALOG_H diff --git a/src/qrcode/CMakeLists.txt b/src/qrcode/CMakeLists.txt new file mode 100644 index 000000000..9ffcf5808 --- /dev/null +++ b/src/qrcode/CMakeLists.txt @@ -0,0 +1,21 @@ +# Copyright (C) 2017 KeePassXC Team +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 or (at your option) +# version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +set(qrcode_SOURCES + QrCode.cpp +) + +add_library(qrcode STATIC ${qrcode_SOURCES}) +target_link_libraries(qrcode Qt5::Core Qt5::Widgets Qt5::Svg ${QRENCODE_LIBRARY}) diff --git a/src/qrcode/QrCode.cpp b/src/qrcode/QrCode.cpp new file mode 100644 index 000000000..961bcfa8d --- /dev/null +++ b/src/qrcode/QrCode.cpp @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "QrCode.h" +#include "QrCode_p.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +QrCodePrivate::QrCodePrivate() + : m_qrcode(nullptr) +{ +} + +QrCodePrivate::~QrCodePrivate() +{ + if (m_qrcode) { + QRcode_free(m_qrcode); + } +} + +QrCode::QrCode() + : d_ptr(new QrCodePrivate()) +{ +} + +QrCode::QrCode(const QString& data, const Version version, const ErrorCorrectionLevel ecl, const bool caseSensitive) + : d_ptr(new QrCodePrivate()) +{ + init(data, version, ecl, caseSensitive); +} + +QrCode::QrCode(const QByteArray& data, const Version version, const ErrorCorrectionLevel ecl) + : d_ptr(new QrCodePrivate()) +{ + init(data, version, ecl); +} + +QrCode::~QrCode() = default; + +void QrCode::init(const QString& data, const Version version, const ErrorCorrectionLevel ecl, bool caseSensitive) +{ + if (data.isEmpty()) { + return; + } + + d_ptr->m_qrcode = QRcode_encodeString(data.toLocal8Bit().data(), + static_cast(version), + static_cast(ecl), + QR_MODE_8, + caseSensitive ? 1 : 0); +} + +void QrCode::init(const QByteArray& data, const Version version, const ErrorCorrectionLevel ecl) +{ + if (data.isEmpty()) { + return; + } + + d_ptr->m_qrcode = QRcode_encodeData(data.size(), + reinterpret_cast(data.data()), + static_cast(version), + static_cast(ecl)); +} + +bool QrCode::isValid() const +{ + return d_ptr->m_qrcode != nullptr; +} + +void QrCode::writeSvg(QIODevice* outputDevice, const int dpi, const int margin) const +{ + if (margin < 0 || d_ptr->m_qrcode == nullptr || outputDevice == nullptr) { + return; + } + + const int width = d_ptr->m_qrcode->width + margin * 2; + + QSvgGenerator generator; + generator.setSize(QSize(width, width)); + generator.setViewBox(QRect(0, 0, width, width)); + generator.setResolution(dpi); + generator.setOutputDevice(outputDevice); + + QPainter painter; + painter.begin(&generator); + + // Background + painter.setClipRect(QRect(0, 0, width, width)); + painter.fillRect(QRect(0, 0, width, width), Qt::white); + + // Foreground + // "Dots" are stored in a quint8 x quint8 array using row-major order. + // A dot is black if the LSB of its corresponding quint8 is 1. + const QPen pen(Qt::black, 0, Qt::SolidLine, Qt::FlatCap, Qt::MiterJoin); + const QBrush brush(Qt::black); + painter.setPen(pen); + painter.setBrush(brush); + + const int rowSize = d_ptr->m_qrcode->width; + unsigned char* dot = d_ptr->m_qrcode->data; + for (int y = 0; y < rowSize; ++y) { + for (int x = 0; x < rowSize; ++x) { + if (quint8(0x01) == (static_cast(*dot++) & quint8(0x01))) { + painter.drawRect(margin + x, margin + y, 1, 1); + } + } + } + + painter.end(); +} diff --git a/src/qrcode/QrCode.h b/src/qrcode/QrCode.h new file mode 100644 index 000000000..ffb49d139 --- /dev/null +++ b/src/qrcode/QrCode.h @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSX_QRCODE_H +#define KEEPASSX_QRCODE_H + +#include +#include + +class QImage; +class QIODevice; +class QString; +class QByteArray; + +struct QrCodePrivate; + +class QrCode +{ + +public: + enum class ErrorCorrectionLevel : int + { + LOW = 0, + MEDIUM, + QUARTILE, + HIGH + }; + + // See: http://www.qrcode.com/en/about/version.html + // clang-format off + enum class Version : int + { + AUTO = 0, + V1, V2, V3, V4, V5, V6, V7, V8, V9, V10, V11, V12, V13, V14, V15, V16, V17, V18, V19, V20, + V21, V22, V23, V24, V25, V26, V27, V28, V29, V30, V31, V32, V33, V34, V35, V36, V37, V38, V39, V40 + }; + // clang-format on + + // Uses QRcode_encodeString (can't contain NUL characters) + explicit QrCode(const QString& data, + const Version version = Version::AUTO, + const ErrorCorrectionLevel ecl = ErrorCorrectionLevel::HIGH, + const bool caseSensitive = true); + + // Uses QRcode_encodeData (can contain NUL characters) + explicit QrCode(const QByteArray& data, + const Version version = Version::AUTO, + const ErrorCorrectionLevel ecl = ErrorCorrectionLevel::HIGH); + + QrCode(); + ~QrCode(); + + bool isValid() const; + void writeSvg(QIODevice* outputDevice, const int dpi, const int margin = 4) const; + +private: + void init(const QString& data, const Version version, const ErrorCorrectionLevel ecl, const bool caseSensitive); + + void init(const QByteArray& data, const Version version, const ErrorCorrectionLevel ecl); + + QScopedPointer d_ptr; +}; + +#endif // KEEPASSX_QRCODE_H diff --git a/src/qrcode/QrCode_p.h b/src/qrcode/QrCode_p.h new file mode 100644 index 000000000..0161b7916 --- /dev/null +++ b/src/qrcode/QrCode_p.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* This class exists to isolate from the rest of the code base. */ + +#ifndef KEEPASSX_QRCODEPRIVATE_H +#define KEEPASSX_QRCODEPRIVATE_H + +#include + +struct QrCodePrivate +{ + QRcode* m_qrcode; + + QrCodePrivate(); + ~QrCodePrivate(); +}; + +#endif // KEEPASSX_QRCODEPRIVATE_H diff --git a/src/totp/totp.cpp b/src/totp/totp.cpp index efd83c8aa..4140993c7 100644 --- a/src/totp/totp.cpp +++ b/src/totp/totp.cpp @@ -22,7 +22,8 @@ #include #include -#include +#include +#include #include #include #include @@ -79,7 +80,7 @@ QSharedPointer Totp::parseSettings(const QString& rawSettings, c settings->step = qBound(1u, settings->step, 60u); // Detect custom settings, used by setup GUI - if (settings->encoder.shortName != STEAM_SHORTNAME + if (settings->encoder.shortName.isEmpty() && (settings->digits != DEFAULT_DIGITS || settings->step != DEFAULT_STEP)) { settings->custom = true; } @@ -96,15 +97,21 @@ QSharedPointer Totp::createSettings(const QString& key, const ui }); } -QString Totp::writeSettings(const QSharedPointer settings) +QString Totp::writeSettings(const QSharedPointer settings, const QString& title, const QString& username, bool forceOtp) { if (settings.isNull()) { return {}; } // OTP Url output - if (settings->otpUrl) { - auto urlstring = QString("key=%1&step=%2&size=%3").arg(settings->key).arg(settings->step).arg(settings->digits); + if (settings->otpUrl || forceOtp) { + auto urlstring = QString("otpauth://totp/%1:%2?secret=%3&period=%4&digits=%5&issuer=%1") + .arg(title.isEmpty() ? "KeePassXC" : QString(QUrl::toPercentEncoding(title))) + .arg(username.isEmpty() ? "none" : QString(QUrl::toPercentEncoding(username))) + .arg(QString(Base32::sanitizeInput(settings->key.toLatin1()))) + .arg(settings->step) + .arg(settings->digits); + if (!settings->encoder.name.isEmpty()) { urlstring.append("&encoder=").append(settings->encoder.name); } diff --git a/src/totp/totp.h b/src/totp/totp.h index b4a592918..ba11ba2b0 100644 --- a/src/totp/totp.h +++ b/src/totp/totp.h @@ -60,7 +60,8 @@ static const QString ATTRIBUTE_SETTINGS = "TOTP Settings"; QSharedPointer parseSettings(const QString& rawSettings, const QString& key = {}); QSharedPointer createSettings(const QString& key, const uint digits, const uint step, const QString& encoderShortName = {}); -QString writeSettings(const QSharedPointer settings); +QString writeSettings(const QSharedPointer settings, const QString& title = {}, + const QString& username = {}, bool forceOtp = false); QString generateTotp(const QSharedPointer settings, const quint64 time = 0ull);