From 04983ce4cd3ca3e835e05b627907859ac97db5d3 Mon Sep 17 00:00:00 2001 From: Bryan Jacobs Date: Wed, 10 Apr 2019 20:58:27 +1000 Subject: [PATCH] Support for RFC-compliant TOTP hashes #873 #1566 This implements support for SHA-256 and SHA-512 hash algorithms when generating TOTP codes. These algorithms are specified by RFC6238. The implementation is compatible with Google's OTP URL format, as well as with the KeeOTP plugin for KeePass. The implementation is not wired into the GUI, as the main project developer expressed strong negative sentiment about adding more options there. It is possible to configure codes by putting the appropriate string into the entry's otp property, or using another program with a less opinionated UI and a compatible on-disk format. --- src/gui/TotpSetupDialog.cpp | 3 +- src/totp/totp.cpp | 64 +++++++++++++++++++++++++++++++++---- src/totp/totp.h | 10 ++++++ tests/TestTotp.cpp | 18 ++++++++++- 4 files changed, 86 insertions(+), 9 deletions(-) diff --git a/src/gui/TotpSetupDialog.cpp b/src/gui/TotpSetupDialog.cpp index 0e2c8da5a..4710824e0 100644 --- a/src/gui/TotpSetupDialog.cpp +++ b/src/gui/TotpSetupDialog.cpp @@ -59,7 +59,8 @@ void TotpSetupDialog::saveSettings() } } - auto settings = Totp::createSettings(m_ui->seedEdit->text(), digits, step, encShortName, m_entry->totpSettings()); + auto settings = Totp::createSettings( + m_ui->seedEdit->text(), digits, step, encShortName, Totp::HashType::Sha1, m_entry->totpSettings()); m_entry->setTotp(settings); emit totpUpdated(); close(); diff --git a/src/totp/totp.cpp b/src/totp/totp.cpp index 7188288a1..14ae994b1 100644 --- a/src/totp/totp.cpp +++ b/src/totp/totp.cpp @@ -35,6 +35,29 @@ static QList encoders{ {"steam", Totp::STEAM_SHORTNAME, "23456789BCDFGHJKMNPQRTVWXY", Totp::STEAM_DIGITS, Totp::DEFAULT_STEP, true}, }; +static Totp::HashType getHashTypeByName(const QString& name) +{ + if (name.compare(QString("SHA512"), Qt::CaseInsensitive) == 0) { + return Totp::HashType::Sha512; + } + if (name.compare(QString("SHA256"), Qt::CaseInsensitive) == 0) { + return Totp::HashType::Sha256; + } + return Totp::HashType::Sha1; +} + +static QString getNameForHashType(const Totp::HashType hashType) +{ + switch (hashType) { + case Totp::HashType::Sha512: + return QString("SHA512"); + case Totp::HashType::Sha256: + return QString("SHA256"); + default: + return QString("SHA1"); + } +} + QSharedPointer Totp::parseSettings(const QString& rawSettings, const QString& key) { // Create default settings @@ -51,6 +74,9 @@ QSharedPointer Totp::parseSettings(const QString& rawSettings, c if (query.hasQueryItem("encoder")) { settings->encoder = getEncoderByName(query.queryItemValue("encoder")); } + if (query.hasQueryItem("algorithm")) { + settings->hashType = getHashTypeByName(query.queryItemValue("algorithm")); + } } else { QUrlQuery query(rawSettings); if (query.hasQueryItem("key")) { @@ -65,6 +91,9 @@ QSharedPointer Totp::parseSettings(const QString& rawSettings, c if (query.hasQueryItem("step")) { settings->step = query.queryItemValue("step").toUInt(); } + if (query.hasQueryItem("otpHashMode")) { + settings->hashType = getHashTypeByName(query.queryItemValue("otpHashMode")); + } } else { // Parse semi-colon separated values ([step];[digits|S]) auto vars = rawSettings.split(";"); @@ -98,19 +127,21 @@ QSharedPointer Totp::createSettings(const QString& key, const uint digits, const uint step, const QString& encoderShortName, + const Totp::HashType hashType, QSharedPointer prevSettings) { bool isCustom = digits != DEFAULT_DIGITS || step != DEFAULT_STEP; if (prevSettings) { prevSettings->key = key; + prevSettings->hashType = hashType; prevSettings->digits = digits; prevSettings->step = step; prevSettings->encoder = Totp::getEncoderByShortName(encoderShortName); prevSettings->custom = isCustom; return prevSettings; } else { - return QSharedPointer( - new Totp::Settings{getEncoderByShortName(encoderShortName), key, false, false, isCustom, digits, step}); + return QSharedPointer(new Totp::Settings{ + getEncoderByShortName(encoderShortName), hashType, key, false, false, isCustom, digits, step}); } } @@ -135,13 +166,20 @@ QString Totp::writeSettings(const QSharedPointer& settings, if (!settings->encoder.name.isEmpty()) { urlstring.append("&encoder=").append(settings->encoder.name); } + if (settings->hashType != Totp::DEFAULT_HASHTYPE) { + urlstring.append("&algorithm=").append(getNameForHashType(settings->hashType)); + } return urlstring; } else if (settings->keeOtp) { // KeeOtp output - return QString("key=%1&size=%2&step=%3") - .arg(QString(Base32::sanitizeInput(settings->key.toLatin1()))) - .arg(settings->digits) - .arg(settings->step); + auto keyString = QString("key=%1&size=%2&step=%3") + .arg(QString(Base32::sanitizeInput(settings->key.toLatin1()))) + .arg(settings->digits) + .arg(settings->step); + if (settings->hashType != Totp::DEFAULT_HASHTYPE) { + keyString.append("&otpHashMode=").append(getNameForHashType(settings->hashType)); + } + return keyString; } else if (!settings->encoder.shortName.isEmpty()) { // Semicolon output [step];[encoder] return QString("%1;%2").arg(settings->step).arg(settings->encoder.shortName); @@ -174,7 +212,19 @@ QString Totp::generateTotp(const QSharedPointer& settings, const return QObject::tr("Invalid Key", "TOTP"); } - QMessageAuthenticationCode code(QCryptographicHash::Sha1); + QCryptographicHash::Algorithm cryptoHash; + switch (settings->hashType) { + case Totp::HashType::Sha512: + cryptoHash = QCryptographicHash::Sha512; + break; + case Totp::HashType::Sha256: + cryptoHash = QCryptographicHash::Sha256; + break; + default: + cryptoHash = QCryptographicHash::Sha1; + break; + } + QMessageAuthenticationCode code(cryptoHash); code.setKey(secret.toByteArray()); code.addData(QByteArray(reinterpret_cast(¤t), sizeof(current))); QByteArray hmac = code.result(); diff --git a/src/totp/totp.h b/src/totp/totp.h index 499973bf9..ccece84fe 100644 --- a/src/totp/totp.h +++ b/src/totp/totp.h @@ -39,9 +39,17 @@ namespace Totp bool reverse; }; + enum HashType + { + Sha1, + Sha256, + Sha512, + }; + struct Settings { Totp::Encoder encoder; + Totp::HashType hashType; QString key; bool otpUrl; bool keeOtp; @@ -53,6 +61,7 @@ namespace Totp constexpr uint DEFAULT_STEP = 30u; constexpr uint DEFAULT_DIGITS = 6u; constexpr uint STEAM_DIGITS = 5u; + constexpr Totp::HashType DEFAULT_HASHTYPE = Sha1; static const QString STEAM_SHORTNAME = "S"; static const QString ATTRIBUTE_OTP = "otp"; @@ -64,6 +73,7 @@ namespace Totp const uint digits, const uint step, const QString& encoderShortName = {}, + const Totp::HashType hashType = DEFAULT_HASHTYPE, QSharedPointer prevSettings = {}); QString writeSettings(const QSharedPointer& settings, const QString& title = {}, diff --git a/tests/TestTotp.cpp b/tests/TestTotp.cpp index f4de2c6ad..c44cd4e8c 100644 --- a/tests/TestTotp.cpp +++ b/tests/TestTotp.cpp @@ -41,15 +41,29 @@ void TestTotp::testParseSecret() QCOMPARE(settings->custom, false); QCOMPARE(settings->digits, 6u); QCOMPARE(settings->step, 30u); + QCOMPARE(settings->hashType, Totp::HashType::Sha1); + + // OTP URL with non-default hash type + secret = "otpauth://totp/" + "ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm=" + "SHA512&digits=6&period=30"; + settings = Totp::parseSettings(secret); + QVERIFY(!settings.isNull()); + QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ")); + QCOMPARE(settings->custom, false); + QCOMPARE(settings->digits, 6u); + QCOMPARE(settings->step, 30u); + QCOMPARE(settings->hashType, Totp::HashType::Sha512); // KeeOTP Parsing - secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8"; + secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8&otpHashMode=Sha256"; settings = Totp::parseSettings(secret); QVERIFY(!settings.isNull()); QCOMPARE(settings->key, QString("HXDMVJECJJWSRBY=")); QCOMPARE(settings->custom, true); QCOMPARE(settings->digits, 8u); QCOMPARE(settings->step, 25u); + QCOMPARE(settings->hashType, Totp::HashType::Sha256); // Semi-colon delineated "TOTP Settings" secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; @@ -59,6 +73,7 @@ void TestTotp::testParseSecret() QCOMPARE(settings->custom, true); QCOMPARE(settings->digits, 8u); QCOMPARE(settings->step, 30u); + QCOMPARE(settings->hashType, Totp::HashType::Sha1); // Bare secret (no "TOTP Settings" attribute) secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; @@ -68,6 +83,7 @@ void TestTotp::testParseSecret() QCOMPARE(settings->custom, false); QCOMPARE(settings->digits, 6u); QCOMPARE(settings->step, 30u); + QCOMPARE(settings->hashType, Totp::HashType::Sha1); } void TestTotp::testTotpCode()