diff --git a/src/core/Entry.cpp b/src/core/Entry.cpp index 45c9e36f8f..eeaccc5e6c 100644 --- a/src/core/Entry.cpp +++ b/src/core/Entry.cpp @@ -596,6 +596,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(); } diff --git a/src/core/Totp.cpp b/src/core/Totp.cpp index f55312a9db..3536876836 100644 --- a/src/core/Totp.cpp +++ b/src/core/Totp.cpp @@ -36,10 +36,11 @@ static QList 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; @@ -57,6 +58,30 @@ static QString getNameForHashType(const Totp::Algorithm hashType) } } +QSharedPointer +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::parseSettings(const QString& rawSettings, const QString& key) { // Early out if both strings are empty @@ -65,7 +90,7 @@ QSharedPointer 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") { @@ -113,6 +138,7 @@ QSharedPointer 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(); @@ -126,13 +152,6 @@ QSharedPointer 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; } @@ -143,9 +162,8 @@ QSharedPointer 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( - 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& settings, @@ -200,8 +218,8 @@ QString Totp::generateTotp(const QSharedPointer& 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) { @@ -277,6 +295,13 @@ QList> Totp::supportedAlgorithms() return algorithms; } +bool Totp::hasCustomSettings(const QSharedPointer& 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 diff --git a/src/core/Totp.h b/src/core/Totp.h index c34ac164d1..769fd0ecee 100644 --- a/src/core/Totp.h +++ b/src/core/Totp.h @@ -56,7 +56,6 @@ namespace Totp Totp::Encoder encoder; Totp::Algorithm algorithm; QString key; - bool custom; uint digits; uint step; }; @@ -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 + fromKeePass2Totp(const QString& secret, const QString& algorithm, const QString& length, const QString& period); + QSharedPointer parseSettings(const QString& rawSettings, const QString& key = {}); QSharedPointer 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); @@ -86,6 +94,8 @@ namespace Totp QString generateTotp(const QSharedPointer& settings, const quint64 time = 0ull); + bool hasCustomSettings(const QSharedPointer& settings); + QList> supportedEncoders(); QList> supportedAlgorithms(); diff --git a/src/gui/TotpExportSettingsDialog.cpp b/src/gui/TotpExportSettingsDialog.cpp index 0568996b12..166c99c65c 100644 --- a/src/gui/TotpExportSettingsDialog.cpp +++ b/src/gui/TotpExportSettingsDialog.cpp @@ -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.", diff --git a/src/gui/TotpSetupDialog.cpp b/src/gui/TotpSetupDialog.cpp index e796e1d5e3..d8d2cfcd43 100644 --- a/src/gui/TotpSetupDialog.cpp +++ b/src/gui/TotpSetupDialog.cpp @@ -125,7 +125,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); diff --git a/tests/TestTotp.cpp b/tests/TestTotp.cpp index f2bb3d47a3..315dc66258 100644 --- a/tests/TestTotp.cpp +++ b/tests/TestTotp.cpp @@ -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/" @@ -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"); @@ -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("", ""); @@ -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")); @@ -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:test@example.com?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); @@ -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); +} diff --git a/tests/TestTotp.h b/tests/TestTotp.h index 92fa7a0e1f..108e98cab5 100644 --- a/tests/TestTotp.h +++ b/tests/TestTotp.h @@ -31,6 +31,7 @@ private slots: void testTotpCode(); void testSteamTotp(); void testEntryHistory(); + void testKeePass2(); }; #endif // KEEPASSX_TESTTOTP_H