Skip to content

Commit

Permalink
Support KeePass2 TOTP settings
Browse files Browse the repository at this point in the history
* Fixes #7263
* Also improves handling of custom TOTP settings
  • Loading branch information
droidmonkey committed Sep 4, 2024
1 parent 2f01604 commit c8fc25e
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 26 deletions.
5 changes: 5 additions & 0 deletions src/core/Entry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,11 @@ void Entry::updateTotp()
m_attributes->value(Totp::ATTRIBUTE_SEED));
} else if (m_attributes->contains(Totp::ATTRIBUTE_OTP)) {
m_data.totpSettings = Totp::parseSettings(m_attributes->value(Totp::ATTRIBUTE_OTP));
} else if (m_attributes->contains(Totp::KP2_TOTP_SECRET)) {
m_data.totpSettings = Totp::fromKeePass2Totp(m_attributes->value(Totp::KP2_TOTP_SECRET),
m_attributes->value(Totp::KP2_TOTP_ALGORITHM),
m_attributes->value(Totp::KP2_TOTP_LENGTH),
m_attributes->value(Totp::KP2_TOTP_PERIOD));
} else {
m_data.totpSettings.reset();
}
Expand Down
53 changes: 39 additions & 14 deletions src/core/Totp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,11 @@ static QList<Totp::Encoder> totpEncoders{

static Totp::Algorithm getHashTypeByName(const QString& name)
{
if (name.compare(QString("SHA512"), Qt::CaseInsensitive) == 0) {
auto nameUpper = name.toUpper();
if (nameUpper == "SHA512" || nameUpper == "HMAC-SHA-512") {
return Totp::Algorithm::Sha512;
}
if (name.compare(QString("SHA256"), Qt::CaseInsensitive) == 0) {
if (nameUpper == "SHA256" || nameUpper == "HMAC-SHA-256") {
return Totp::Algorithm::Sha256;
}
return Totp::Algorithm::Sha1;
Expand All @@ -57,6 +58,30 @@ static QString getNameForHashType(const Totp::Algorithm hashType)
}
}

QSharedPointer<Totp::Settings>
Totp::fromKeePass2Totp(const QString& secret, const QString& algorithm, const QString& length, const QString& period)
{
// Must have at least a secret to continue
if (secret.isEmpty()) {
return {};
}

// Create default settings
auto settings = createSettings(secret);

if (!algorithm.isEmpty()) {
settings->algorithm = getHashTypeByName(algorithm);
}
if (!length.isEmpty()) {
settings->digits = length.toUInt();
}
if (!period.isEmpty()) {
settings->step = period.toUInt();
}

return settings;
}

QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, const QString& key)
{
// Early out if both strings are empty
Expand All @@ -65,7 +90,7 @@ QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, c
}

// Create default settings
auto settings = createSettings(key, DEFAULT_DIGITS, DEFAULT_STEP);
auto settings = createSettings(key);

QUrl url(rawSettings);
if (url.isValid() && url.scheme() == "otpauth") {
Expand Down Expand Up @@ -113,6 +138,7 @@ QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, c
if (vars[1] == STEAM_SHORTNAME) {
// Explicit steam encoder
settings->encoder = steamEncoder();
settings->digits = STEAM_DIGITS;
} else {
// Extract step and digits
settings->step = vars[0].toUInt();
Expand All @@ -126,13 +152,6 @@ QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, c
settings->digits = qBound(1u, settings->digits, 10u);
settings->step = qBound(1u, settings->step, 86400u);

// Detect custom settings, used by setup GUI
if (settings->encoder.shortName.isEmpty()
&& (settings->digits != DEFAULT_DIGITS || settings->step != DEFAULT_STEP
|| settings->algorithm != DEFAULT_ALGORITHM)) {
settings->custom = true;
}

return settings;
}

Expand All @@ -143,9 +162,8 @@ QSharedPointer<Totp::Settings> Totp::createSettings(const QString& key,
const QString& encoderShortName,
const Totp::Algorithm algorithm)
{
bool isCustom = digits != DEFAULT_DIGITS || step != DEFAULT_STEP || algorithm != DEFAULT_ALGORITHM;
return QSharedPointer<Totp::Settings>(
new Totp::Settings{format, getEncoderByShortName(encoderShortName), algorithm, key, isCustom, digits, step});
new Totp::Settings{format, getEncoderByShortName(encoderShortName), algorithm, key, digits, step});
}

QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
Expand Down Expand Up @@ -200,8 +218,8 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const
}

const Encoder& encoder = settings->encoder;
uint step = settings->custom ? settings->step : encoder.step;
uint digits = settings->custom ? settings->digits : encoder.digits;
uint step = settings->step;
uint digits = settings->digits;

quint64 current;
if (time == 0) {
Expand Down Expand Up @@ -277,6 +295,13 @@ QList<QPair<QString, Totp::Algorithm>> Totp::supportedAlgorithms()
return algorithms;
}

bool Totp::hasCustomSettings(const QSharedPointer<Totp::Settings>& settings)
{
return settings
&& (settings->digits != DEFAULT_DIGITS || settings->step != DEFAULT_STEP
|| settings->algorithm != DEFAULT_ALGORITHM);
}

Totp::Encoder& Totp::defaultEncoder()
{
// The first encoder is always the default
Expand Down
16 changes: 13 additions & 3 deletions src/core/Totp.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ namespace Totp
Totp::Encoder encoder;
Totp::Algorithm algorithm;
QString key;
bool custom;
uint digits;
uint step;
};
Expand All @@ -72,10 +71,19 @@ namespace Totp
static const QString ATTRIBUTE_SEED = "TOTP Seed";
static const QString ATTRIBUTE_SETTINGS = "TOTP Settings";

// Support for KeePass2 TOTP
static const QString KP2_TOTP_SECRET = "TimeOtp-Secret-Base32";
static const QString KP2_TOTP_ALGORITHM = "TimeOtp-Algorithm";
static const QString KP2_TOTP_LENGTH = "TimeOtp-Length";
static const QString KP2_TOTP_PERIOD = "TimeOtp-Period";

QSharedPointer<Totp::Settings>
fromKeePass2Totp(const QString& secret, const QString& algorithm, const QString& length, const QString& period);

QSharedPointer<Totp::Settings> parseSettings(const QString& rawSettings, const QString& key = {});
QSharedPointer<Totp::Settings> createSettings(const QString& key,
const uint digits,
const uint step,
const uint digits = DEFAULT_DIGITS,
const uint step = DEFAULT_STEP,
const Totp::StorageFormat format = DEFAULT_FORMAT,
const QString& encoderShortName = {},
const Totp::Algorithm algorithm = DEFAULT_ALGORITHM);
Expand All @@ -86,6 +94,8 @@ namespace Totp

QString generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time = 0ull);

bool hasCustomSettings(const QSharedPointer<Totp::Settings>& settings);

QList<QPair<QString, QString>> supportedEncoders();
QList<QPair<QString, Algorithm>> supportedAlgorithms();

Expand Down
2 changes: 1 addition & 1 deletion src/gui/TotpExportSettingsDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ TotpExportSettingsDialog::TotpExportSettingsDialog(DatabaseWidget* parent, Entry
m_timer->start(1000);

const auto totpSettings = entry->totpSettings();
if (totpSettings->custom || !totpSettings->encoder.shortName.isEmpty()) {
if (Totp::hasCustomSettings(totpSettings) || !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.",
Expand Down
2 changes: 1 addition & 1 deletion src/gui/TotpSetupDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ void TotpSetupDialog::init()

if (settings->encoder.shortName == Totp::STEAM_SHORTNAME) {
m_ui->radioSteam->setChecked(true);
} else if (settings->custom) {
} else if (Totp::hasCustomSettings(settings)) {
m_ui->radioCustom->setChecked(true);
m_ui->digitsSpinBox->setValue(settings->digits);
int index = m_ui->algorithmComboBox->findData(settings->algorithm);
Expand Down
57 changes: 50 additions & 7 deletions tests/TestTotp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ void TestTotp::testParseSecret()
auto settings = Totp::parseSettings(secret);
QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"));
QCOMPARE(settings->custom, false);
QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
QCOMPARE(settings->digits, 6u);
QCOMPARE(settings->step, 30u);
QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
QCOMPARE(Totp::hasCustomSettings(settings), false);

// OTP URL with non-default hash type
secret = "otpauth://totp/"
Expand All @@ -53,11 +53,11 @@ void TestTotp::testParseSecret()
settings = Totp::parseSettings(secret);
QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"));
QCOMPARE(settings->custom, true);
QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
QCOMPARE(settings->digits, 6u);
QCOMPARE(settings->step, 30u);
QCOMPARE(settings->algorithm, Totp::Algorithm::Sha512);
QCOMPARE(Totp::hasCustomSettings(settings), true);

// Max TOTP step of 24-hours
secret.replace("period=30", "period=90000");
Expand All @@ -70,33 +70,33 @@ void TestTotp::testParseSecret()
settings = Totp::parseSettings(secret);
QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("HXDMVJECJJWSRBY="));
QCOMPARE(settings->custom, true);
QCOMPARE(settings->format, Totp::StorageFormat::KEEOTP);
QCOMPARE(settings->digits, 8u);
QCOMPARE(settings->step, 25u);
QCOMPARE(settings->algorithm, Totp::Algorithm::Sha256);
QCOMPARE(Totp::hasCustomSettings(settings), true);

// Semi-colon delineated "TOTP Settings"
secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
settings = Totp::parseSettings("30;8", secret);
QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
QCOMPARE(settings->custom, true);
QCOMPARE(settings->format, Totp::StorageFormat::LEGACY);
QCOMPARE(settings->digits, 8u);
QCOMPARE(settings->step, 30u);
QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
QCOMPARE(Totp::hasCustomSettings(settings), true);

// Bare secret (no "TOTP Settings" attribute)
secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
settings = Totp::parseSettings("", secret);
QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
QCOMPARE(settings->custom, false);
QCOMPARE(settings->format, Totp::StorageFormat::LEGACY);
QCOMPARE(settings->digits, 6u);
QCOMPARE(settings->step, 30u);
QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
QCOMPARE(Totp::hasCustomSettings(settings), false);

// Blank settings (expected failure)
settings = Totp::parseSettings("", "");
Expand All @@ -122,7 +122,6 @@ void TestTotp::testTotpCode()

// Test 8 digit TOTP (custom)
settings->digits = 8;
settings->custom = true;
time = 1111111111;
QCOMPARE(Totp::generateTotp(settings, time), QString("14050471"));

Expand All @@ -132,11 +131,19 @@ void TestTotp::testTotpCode()

void TestTotp::testSteamTotp()
{
// Legacy parsing
auto settings = Totp::parseSettings("30;S", "63BEDWCQZKTQWPESARIERL5DTTQFCJTK");
QCOMPARE(settings->key, QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK"));
QCOMPARE(settings->encoder.shortName, Totp::STEAM_SHORTNAME);
QCOMPARE(settings->format, Totp::StorageFormat::LEGACY);
QCOMPARE(settings->digits, Totp::STEAM_DIGITS);
QCOMPARE(settings->step, 30u);

// OTP URL Parsing
QString secret = "otpauth://totp/"
"test:[email protected]?secret=63BEDWCQZKTQWPESARIERL5DTTQFCJTK&issuer=Valve&algorithm="
"SHA1&digits=5&period=30&encoder=steam";
auto settings = Totp::parseSettings(secret);
settings = Totp::parseSettings(secret);

QCOMPARE(settings->key, QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK"));
QCOMPARE(settings->encoder.shortName, Totp::STEAM_SHORTNAME);
Expand Down Expand Up @@ -177,3 +184,39 @@ void TestTotp::testEntryHistory()
QVERIFY(!entry.hasTotp());
QCOMPARE(entry.historyItems().size(), 3);
}

void TestTotp::testKeePass2()
{
Entry entry;
auto attr = entry.attributes();

// Default settings
attr->set("TimeOtp-Secret-Base32", "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");

auto settings = entry.totpSettings();
QVERIFY(settings);
QCOMPARE(settings->key, QString("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"));
QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
QCOMPARE(settings->digits, 6u);
QCOMPARE(settings->step, 30u);
QCOMPARE(Totp::hasCustomSettings(settings), false);

// Custom settings
attr->set("TimeOtp-Algorithm", "HMAC-SHA-256");
attr->set("TimeOtp-Length", "8");

settings = entry.totpSettings();
QVERIFY(settings);
QCOMPARE(settings->key, QString("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"));
QCOMPARE(settings->algorithm, Totp::Algorithm::Sha256);
QCOMPARE(settings->digits, 8u);
QCOMPARE(settings->step, 30u);
QCOMPARE(Totp::hasCustomSettings(settings), true);

// Base64 and other encodings are not supported
attr->remove("TimeOtp-Secret-Base32");
attr->set("TimeOtp-Secret-Base64", "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");

settings = entry.totpSettings();
QVERIFY(!settings);
}
1 change: 1 addition & 0 deletions tests/TestTotp.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ private slots:
void testTotpCode();
void testSteamTotp();
void testEntryHistory();
void testKeePass2();
};

#endif // KEEPASSX_TESTTOTP_H

0 comments on commit c8fc25e

Please sign in to comment.