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.
This commit is contained in:
Bryan Jacobs 2019-04-10 20:58:27 +10:00 committed by Jonathan White
parent 61b1f8c966
commit 04983ce4cd
4 changed files with 86 additions and 9 deletions

View File

@ -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); m_entry->setTotp(settings);
emit totpUpdated(); emit totpUpdated();
close(); close();

View File

@ -35,6 +35,29 @@ static QList<Totp::Encoder> encoders{
{"steam", Totp::STEAM_SHORTNAME, "23456789BCDFGHJKMNPQRTVWXY", Totp::STEAM_DIGITS, Totp::DEFAULT_STEP, true}, {"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::Settings> Totp::parseSettings(const QString& rawSettings, const QString& key) QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, const QString& key)
{ {
// Create default settings // Create default settings
@ -51,6 +74,9 @@ QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, c
if (query.hasQueryItem("encoder")) { if (query.hasQueryItem("encoder")) {
settings->encoder = getEncoderByName(query.queryItemValue("encoder")); settings->encoder = getEncoderByName(query.queryItemValue("encoder"));
} }
if (query.hasQueryItem("algorithm")) {
settings->hashType = getHashTypeByName(query.queryItemValue("algorithm"));
}
} else { } else {
QUrlQuery query(rawSettings); QUrlQuery query(rawSettings);
if (query.hasQueryItem("key")) { if (query.hasQueryItem("key")) {
@ -65,6 +91,9 @@ QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, c
if (query.hasQueryItem("step")) { if (query.hasQueryItem("step")) {
settings->step = query.queryItemValue("step").toUInt(); settings->step = query.queryItemValue("step").toUInt();
} }
if (query.hasQueryItem("otpHashMode")) {
settings->hashType = getHashTypeByName(query.queryItemValue("otpHashMode"));
}
} else { } else {
// Parse semi-colon separated values ([step];[digits|S]) // Parse semi-colon separated values ([step];[digits|S])
auto vars = rawSettings.split(";"); auto vars = rawSettings.split(";");
@ -98,19 +127,21 @@ QSharedPointer<Totp::Settings> Totp::createSettings(const QString& key,
const uint digits, const uint digits,
const uint step, const uint step,
const QString& encoderShortName, const QString& encoderShortName,
const Totp::HashType hashType,
QSharedPointer<Totp::Settings> prevSettings) QSharedPointer<Totp::Settings> prevSettings)
{ {
bool isCustom = digits != DEFAULT_DIGITS || step != DEFAULT_STEP; bool isCustom = digits != DEFAULT_DIGITS || step != DEFAULT_STEP;
if (prevSettings) { if (prevSettings) {
prevSettings->key = key; prevSettings->key = key;
prevSettings->hashType = hashType;
prevSettings->digits = digits; prevSettings->digits = digits;
prevSettings->step = step; prevSettings->step = step;
prevSettings->encoder = Totp::getEncoderByShortName(encoderShortName); prevSettings->encoder = Totp::getEncoderByShortName(encoderShortName);
prevSettings->custom = isCustom; prevSettings->custom = isCustom;
return prevSettings; return prevSettings;
} else { } else {
return QSharedPointer<Totp::Settings>( return QSharedPointer<Totp::Settings>(new Totp::Settings{
new Totp::Settings{getEncoderByShortName(encoderShortName), key, false, false, isCustom, digits, step}); getEncoderByShortName(encoderShortName), hashType, key, false, false, isCustom, digits, step});
} }
} }
@ -135,13 +166,20 @@ QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
if (!settings->encoder.name.isEmpty()) { if (!settings->encoder.name.isEmpty()) {
urlstring.append("&encoder=").append(settings->encoder.name); urlstring.append("&encoder=").append(settings->encoder.name);
} }
if (settings->hashType != Totp::DEFAULT_HASHTYPE) {
urlstring.append("&algorithm=").append(getNameForHashType(settings->hashType));
}
return urlstring; return urlstring;
} else if (settings->keeOtp) { } else if (settings->keeOtp) {
// KeeOtp output // KeeOtp output
return QString("key=%1&size=%2&step=%3") auto keyString = QString("key=%1&size=%2&step=%3")
.arg(QString(Base32::sanitizeInput(settings->key.toLatin1()))) .arg(QString(Base32::sanitizeInput(settings->key.toLatin1())))
.arg(settings->digits) .arg(settings->digits)
.arg(settings->step); .arg(settings->step);
if (settings->hashType != Totp::DEFAULT_HASHTYPE) {
keyString.append("&otpHashMode=").append(getNameForHashType(settings->hashType));
}
return keyString;
} else if (!settings->encoder.shortName.isEmpty()) { } else if (!settings->encoder.shortName.isEmpty()) {
// Semicolon output [step];[encoder] // Semicolon output [step];[encoder]
return QString("%1;%2").arg(settings->step).arg(settings->encoder.shortName); return QString("%1;%2").arg(settings->step).arg(settings->encoder.shortName);
@ -174,7 +212,19 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const
return QObject::tr("Invalid Key", "TOTP"); 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.setKey(secret.toByteArray());
code.addData(QByteArray(reinterpret_cast<char*>(&current), sizeof(current))); code.addData(QByteArray(reinterpret_cast<char*>(&current), sizeof(current)));
QByteArray hmac = code.result(); QByteArray hmac = code.result();

View File

@ -39,9 +39,17 @@ namespace Totp
bool reverse; bool reverse;
}; };
enum HashType
{
Sha1,
Sha256,
Sha512,
};
struct Settings struct Settings
{ {
Totp::Encoder encoder; Totp::Encoder encoder;
Totp::HashType hashType;
QString key; QString key;
bool otpUrl; bool otpUrl;
bool keeOtp; bool keeOtp;
@ -53,6 +61,7 @@ namespace Totp
constexpr uint DEFAULT_STEP = 30u; constexpr uint DEFAULT_STEP = 30u;
constexpr uint DEFAULT_DIGITS = 6u; constexpr uint DEFAULT_DIGITS = 6u;
constexpr uint STEAM_DIGITS = 5u; constexpr uint STEAM_DIGITS = 5u;
constexpr Totp::HashType DEFAULT_HASHTYPE = Sha1;
static const QString STEAM_SHORTNAME = "S"; static const QString STEAM_SHORTNAME = "S";
static const QString ATTRIBUTE_OTP = "otp"; static const QString ATTRIBUTE_OTP = "otp";
@ -64,6 +73,7 @@ namespace Totp
const uint digits, const uint digits,
const uint step, const uint step,
const QString& encoderShortName = {}, const QString& encoderShortName = {},
const Totp::HashType hashType = DEFAULT_HASHTYPE,
QSharedPointer<Totp::Settings> prevSettings = {}); QSharedPointer<Totp::Settings> prevSettings = {});
QString writeSettings(const QSharedPointer<Totp::Settings>& settings, QString writeSettings(const QSharedPointer<Totp::Settings>& settings,
const QString& title = {}, const QString& title = {},

View File

@ -41,15 +41,29 @@ void TestTotp::testParseSecret()
QCOMPARE(settings->custom, false); QCOMPARE(settings->custom, false);
QCOMPARE(settings->digits, 6u); QCOMPARE(settings->digits, 6u);
QCOMPARE(settings->step, 30u); 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 // KeeOTP Parsing
secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8"; secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8&otpHashMode=Sha256";
settings = Totp::parseSettings(secret); settings = Totp::parseSettings(secret);
QVERIFY(!settings.isNull()); QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("HXDMVJECJJWSRBY=")); QCOMPARE(settings->key, QString("HXDMVJECJJWSRBY="));
QCOMPARE(settings->custom, true); QCOMPARE(settings->custom, true);
QCOMPARE(settings->digits, 8u); QCOMPARE(settings->digits, 8u);
QCOMPARE(settings->step, 25u); QCOMPARE(settings->step, 25u);
QCOMPARE(settings->hashType, Totp::HashType::Sha256);
// Semi-colon delineated "TOTP Settings" // Semi-colon delineated "TOTP Settings"
secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
@ -59,6 +73,7 @@ void TestTotp::testParseSecret()
QCOMPARE(settings->custom, true); QCOMPARE(settings->custom, true);
QCOMPARE(settings->digits, 8u); QCOMPARE(settings->digits, 8u);
QCOMPARE(settings->step, 30u); QCOMPARE(settings->step, 30u);
QCOMPARE(settings->hashType, Totp::HashType::Sha1);
// Bare secret (no "TOTP Settings" attribute) // Bare secret (no "TOTP Settings" attribute)
secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
@ -68,6 +83,7 @@ void TestTotp::testParseSecret()
QCOMPARE(settings->custom, false); QCOMPARE(settings->custom, false);
QCOMPARE(settings->digits, 6u); QCOMPARE(settings->digits, 6u);
QCOMPARE(settings->step, 30u); QCOMPARE(settings->step, 30u);
QCOMPARE(settings->hashType, Totp::HashType::Sha1);
} }
void TestTotp::testTotpCode() void TestTotp::testTotpCode()