Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for displaying TOTP keys as QR codes #1001

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ option(WITH_APP_BUNDLE "Enable Application Bundle for OS X" ON)
option(WITH_XC_AUTOTYPE "Include Auto-Type." ON)
option(WITH_XC_HTTP "Include KeePassHTTP and Custom Icon Downloads." OFF)
option(WITH_XC_YUBIKEY "Include YubiKey support." OFF)
option(WITH_XC_TOTPDISPLAYKEY "Enable displaying TOTP keys as QR codes." OFF)

# Process ui files automatically from source files
set(CMAKE_AUTOUIC ON)
Expand Down Expand Up @@ -159,6 +160,10 @@ if(WITH_DEV_BUILD)
add_definitions(-DQT_DEPRECATED_WARNINGS -DGCRYPT_NO_DEPRECATED)
endif()

if(WITH_XC_TOTPDISPLAYKEY)
add_definitions(-DWITH_XC_TOTPDISPLAYKEY)
endif()

if(MINGW)
set(CMAKE_RC_COMPILER_INIT windres)
enable_language(RC)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ cmake accepts the following options:
-DWITH_XC_AUTOTYPE=[ON|OFF] Enable/Disable Auto-Type (default: ON)
-DWITH_XC_HTTP=[ON|OFF] Enable/Disable KeePassHTTP and custom icon downloads (default: OFF)
-DWITH_XC_YUBIKEY=[ON|OFF] Enable/Disable YubiKey HMAC-SHA1 authentication support (default: OFF)
-DWITH_XC_TOTPDISPLAYKEY=[ON|OFF] Enable/Disable Support for displaying TOTP keys as QR codes (default: OFF)

-DWITH_TESTS=[ON|OFF] Enable/Disable building of unit tests (default: ON)
-DWITH_GUI_TESTS=[ON|OFF] Enable/Disable building of GUI tests (default: OFF)
Expand Down
14 changes: 14 additions & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,7 @@ set(keepassx_SOURCES_MAINEXE
add_feature_info(AutoType WITH_XC_AUTOTYPE "Automatic password typing")
add_feature_info(KeePassHTTP WITH_XC_HTTP "Browser integration compatible with ChromeIPass and PassIFox")
add_feature_info(YubiKey WITH_XC_YUBIKEY "YubiKey HMAC-SHA1 challenge-response")
add_feature_info(TotpDisplayKey WITH_XC_TOTPDISPLAYKEY "Display TOTP keys as QR codes")

add_subdirectory(http)
if(WITH_XC_HTTP)
Expand Down Expand Up @@ -211,6 +212,18 @@ else()
list(APPEND keepassx_SOURCES keys/drivers/YubiKeyStub.cpp)
endif()

if(WITH_XC_TOTPDISPLAYKEY)
find_path(QRENCODE_INCLUDE_DIR qrencode.h)
find_library(QRENCODE_LIBRARY qrencode)
set(keepassx_SOURCES ${keepassx_SOURCES}
gui/TotpKeyDisplayDialog.h
gui/TotpKeyDisplayDialog.cpp
core/QRCode.h
core/QRCode_p.h
core/QRCode.cpp
)
endif()

add_library(autotype STATIC ${autotype_SOURCES})
target_link_libraries(autotype Qt5::Core Qt5::Widgets)

Expand All @@ -222,6 +235,7 @@ set_target_properties(keepassx_core PROPERTIES COMPILE_DEFINITIONS KEEPASSX_BUIL
target_link_libraries(keepassx_core
${keepasshttp_LIB}
${autotype_LIB}
${QRENCODE_LIBRARY}
${YUBIKEY_LIBRARIES}
${ZXCVBN_LIBRARIES}
Qt5::Core
Expand Down
81 changes: 74 additions & 7 deletions src/core/Base32.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

// Conforms to RFC 4648. For details, see: https://tools.ietf.org/html/rfc4648
/* Conforms to RFC 4648. For details, see: https://tools.ietf.org/html/rfc4648
* Use the functions Base32::addPadding/1, Base32::removePadding/1 or
* Base32::sanitizeInput/1 to fix input or output for a particular
* applications (e.g. to use with Google Authenticator).
*/

#include "Base32.h"

Expand Down Expand Up @@ -77,8 +81,8 @@ Optional<QByteArray> Base32::decode(const QByteArray& encodedData)


Q_ASSERT(encodedData.size() > 0);
const int nQuantums = encodedData.size() / 8;
const int nBytes = (nQuantums - 1) * 5 + nSpecialBytes;
const int nQuanta = encodedData.size() / 8;
const int nBytes = (nQuanta - 1) * 5 + nSpecialBytes;

QByteArray data(nBytes, Qt::Uninitialized);

Expand All @@ -96,7 +100,7 @@ Optional<QByteArray> Base32::decode(const QByteArray& encodedData)
if (ch >= ALPH_POS_2)
ch -= ASCII_a - ASCII_A;
} else {
if (ch >= ASCII_2 && ch <= ASCII_7) {
if (ASCII_2 <= ch && ch <= ASCII_7) {
ch -= ASCII_2;
ch += ALPH_POS_2;
} else {
Expand Down Expand Up @@ -137,9 +141,9 @@ QByteArray Base32::encode(const QByteArray& data)

const int nBits = data.size() * 8;
const int rBits = nBits % 40; // in {0, 8, 16, 24, 32}
const int nQuantums = nBits / 40 + (rBits > 0 ? 1 : 0);
QByteArray encodedData(nQuantums * 8, Qt::Uninitialized);
const int nQuanta = nBits / 40 + (rBits > 0 ? 1 : 0);
QByteArray encodedData(nQuanta * 8, Qt::Uninitialized);

int i = 0;
int o = 0;
int n;
Expand Down Expand Up @@ -208,3 +212,66 @@ QByteArray Base32::encode(const QByteArray& data)
return encodedData;
}

QByteArray Base32::addPadding(const QByteArray& encodedData)
{
if (encodedData.size() <= 0 || encodedData.size() % 8 == 0)
return encodedData;

const int rBytes = encodedData.size() % 8;
// rBytes must be a member of {2, 4, 5, 7}
if (1 == rBytes || 3 == rBytes || 6 == rBytes)
return encodedData;

QByteArray newEncodedData(encodedData);
for(int nPads = 8 - rBytes; nPads > 0; --nPads) {
newEncodedData.append('=');
}

return newEncodedData;
}

QByteArray Base32::removePadding(const QByteArray& encodedData)
{
if (encodedData.size() <= 0 || encodedData.size() % 8 != 0)
return encodedData; // return same bad input

int nPads = 0;
for (int i = -1; i > -7; --i) {
if ('=' == encodedData[encodedData.size() + i])
++nPads;
}

QByteArray newEncodedData(encodedData);
newEncodedData.remove(encodedData.size() - nPads, nPads);
newEncodedData.resize(encodedData.size() - nPads);

return newEncodedData;
}

QByteArray Base32::sanitizeInput(const QByteArray& encodedData)
{
if(encodedData.size() <= 0)
return encodedData;

QByteArray newEncodedData(encodedData.size(), Qt::Uninitialized);
int i = 0;
for (auto ch : encodedData) {
switch(ch) {
case '0':
newEncodedData[i++] = 'O';
break;
case '1':
newEncodedData[i++] = 'L';
break;
case '8':
newEncodedData[i++] = 'B';
break;
default:
if (('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') || ('2' <= ch && ch <= '7'))
newEncodedData[i++] = ch;
}
}
newEncodedData.resize(i);

return addPadding(newEncodedData);
}
10 changes: 8 additions & 2 deletions src/core/Base32.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

// Conforms to RFC 4648. For details, see: https://tools.ietf.org/html/rfc4648
/* Conforms to RFC 4648. For details, see: https://tools.ietf.org/html/rfc4648
* Use the functions Base32::addPadding/1, Base32::removePadding/1 or
* Base32::sanitizeInput/1 to fix input or output for a particular
* applications (e.g. to use with Google Authenticator).
*/

#ifndef BASE32_H
#define BASE32_H
Expand All @@ -30,7 +34,9 @@ class Base32
Base32() =default;
Q_REQUIRED_RESULT static Optional<QByteArray> decode(const QByteArray&);
Q_REQUIRED_RESULT static QByteArray encode(const QByteArray&);
Q_REQUIRED_RESULT static QByteArray addPadding(const QByteArray&);
Q_REQUIRED_RESULT static QByteArray removePadding(const QByteArray&);
Q_REQUIRED_RESULT static QByteArray sanitizeInput(const QByteArray&);
};


#endif //BASE32_H
10 changes: 5 additions & 5 deletions src/core/Optional.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@
* This utility class is for providing basic support for an option type.
* It can be replaced by std::optional (C++17) or
* std::experimental::optional (C++11) when they become fully supported
* by all the compilers.
* by all the main compiler toolchains.
*/

template <typename T>
class Optional
{
public:

// None
Optional() :
m_hasValue(false),
m_value()
{ };
{ };

// Some T
Optional(const T& value) :
m_hasValue(true),
Expand Down Expand Up @@ -77,7 +77,7 @@ class Optional
{
return m_hasValue ? m_value : other;
}

Optional static makeOptional(const T& value)
{
return Optional(value);
Expand Down
126 changes: 126 additions & 0 deletions src/core/QRCode.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
* Copyright (C) 2017 KeePassXC Team <[email protected]>
*
* 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 <http://www.gnu.org/licenses/>.
*/

#include "QRCode.h"
#include "QRCode_p.h"

#include <QImage>
#include <QPainter>
#include <QPen>
#include <QBrush>
#include <QString>
#include <QByteArray>

QRCodePrivate::QRCodePrivate() : m_qrcode(nullptr)
{
}

QRCodePrivate::~QRCodePrivate()
{
if (nullptr != 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<int>(version),
static_cast<QRecLevel>(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<const unsigned char*>(data.data()),
static_cast<int>(version),
static_cast<QRecLevel>(ecl));
}

QImage QRCode::toQImage(const int size, const int margin) const
{
if (size <= 0 || margin < 0 || nullptr == d_ptr->m_qrcode)
return QImage();

const int width = d_ptr->m_qrcode->width + margin * 2;
QImage img(QSize(width,width), QImage::Format_Mono);

QPainter painter;
painter.begin(&img);

// 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.1, 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<quint8>(*dot++) & quint8(0x01)))
painter.drawRect(margin + x, margin + y, 1, 1);
}
}

painter.end();

return img.scaled(size, size);
}
Loading