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);
emit totpUpdated();
close();

View File

@ -35,6 +35,29 @@ static QList<Totp::Encoder> 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::Settings> Totp::parseSettings(const QString& rawSettings, const QString& key)
{
// Create default settings
@ -51,6 +74,9 @@ QSharedPointer<Totp::Settings> 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::Settings> 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::Settings> Totp::createSettings(const QString& key,
const uint digits,
const uint step,
const QString& encoderShortName,
const Totp::HashType hashType,
QSharedPointer<Totp::Settings> 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<Totp::Settings>(
new Totp::Settings{getEncoderByShortName(encoderShortName), key, false, false, isCustom, digits, step});
return QSharedPointer<Totp::Settings>(new Totp::Settings{
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()) {
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")
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<Totp::Settings>& 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<char*>(&current), sizeof(current)));
QByteArray hmac = code.result();

View File

@ -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<Totp::Settings> prevSettings = {});
QString writeSettings(const QSharedPointer<Totp::Settings>& settings,
const QString& title = {},

View File

@ -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()