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 all commits
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
4 changes: 4 additions & 0 deletions share/translations/keepassx_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,10 @@ This is a one-way migration. You won't be able to open the imported databas
<source>Show TOTP</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show TOTP key</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Find</source>
<translation type="unfinished"></translation>
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
103 changes: 89 additions & 14 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 All @@ -38,15 +42,17 @@ constexpr quint8 ASCII_EQ = static_cast<quint8>('=');

Optional<QByteArray> Base32::decode(const QByteArray& encodedData)
{
if (encodedData.size() <= 0)
if (encodedData.size() <= 0) {
return Optional<QByteArray>("");
}

if (encodedData.size() % 8 != 0)
if (encodedData.size() % 8 != 0) {
return Optional<QByteArray>();
}

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

Expand Down Expand Up @@ -75,10 +81,9 @@ Optional<QByteArray> Base32::decode(const QByteArray& encodedData)
specialOffset = 0;
}


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,12 +101,12 @@ 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 {
if (ASCII_EQ == ch) {
if(i == encodedData.size()) {
if (i == encodedData.size()) {
// finished with special quantum
quantum >>= specialOffset;
nQuantumBytes = nSpecialBytes;
Expand Down Expand Up @@ -132,14 +137,15 @@ Optional<QByteArray> Base32::decode(const QByteArray& encodedData)

QByteArray Base32::encode(const QByteArray& data)
{
if (data.size() < 1)
if (data.size() < 1) {
return QByteArray();
}

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 @@ -170,7 +176,7 @@ QByteArray Base32::encode(const QByteArray& data)
quantum |= static_cast<quint64>(data[i++]) << n;

switch (rBits) {
case 8: // expand to 10 bits
case 8: // expand to 10 bits
quantum <<= 2;
mask = MASK_10BIT;
n = 5;
Expand Down Expand Up @@ -208,3 +214,72 @@ 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);
}
16 changes: 11 additions & 5 deletions src/core/Base32.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,28 @@
* 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

#include "Optional.h"
#include <QtCore/qglobal.h>
#include <QByteArray>
#include <QtCore/qglobal.h>

class Base32
{
public:
Base32() =default;
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
#endif // BASE32_H
35 changes: 14 additions & 21 deletions src/core/Optional.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,31 +22,26 @@
* 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
template <typename T> class Optional
{
public:

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

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

// Some T
Optional(const T& value) :
m_hasValue(true),
m_value(value)
{ };
Optional(const T& value)
: m_hasValue(true)
, m_value(value){};

// Copy
Optional(const Optional& other) :
m_hasValue(other.m_hasValue),
m_value(other.m_value)
{ };
Optional(const Optional& other)
: m_hasValue(other.m_hasValue)
, m_value(other.m_value){};

const Optional& operator=(const Optional& other)
{
Expand All @@ -57,7 +52,7 @@ class Optional

bool operator==(const Optional& other) const
{
if(m_hasValue)
if (m_hasValue)
return other.m_hasValue && m_value == other.m_value;
else
return !other.m_hasValue;
Expand All @@ -77,15 +72,13 @@ class Optional
{
return m_hasValue ? m_value : other;
}

Optional static makeOptional(const T& value)
{
return Optional(value);
}


private:

bool m_hasValue;
T m_value;
};
Expand Down
Loading