mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-10-01 01:26:01 -04:00
04983ce4cd
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.
154 lines
5.7 KiB
C++
154 lines
5.7 KiB
C++
/*
|
||
* Copyright (C) 2017 Weslly Honorato <weslly@protonmail.com>
|
||
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
|
||
*
|
||
* This program is free software: you can redistribute it and/or modify
|
||
* it under the terms of the GNU General Public License as published by
|
||
* the Free Software Foundation, either version 2 or (at your option)
|
||
* version 3 of the License.
|
||
*
|
||
* This program is distributed in the hope that it will be useful,
|
||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||
* GNU General Public License for more details.
|
||
*
|
||
* You should have received a copy of the GNU General Public License
|
||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||
*/
|
||
|
||
#include "TestTotp.h"
|
||
#include "TestGlobal.h"
|
||
|
||
#include "crypto/Crypto.h"
|
||
#include "totp/totp.h"
|
||
|
||
QTEST_GUILESS_MAIN(TestTotp)
|
||
|
||
void TestTotp::initTestCase()
|
||
{
|
||
QVERIFY(Crypto::init());
|
||
}
|
||
|
||
void TestTotp::testParseSecret()
|
||
{
|
||
// OTP URL Parsing
|
||
QString secret = "otpauth://totp/"
|
||
"ACME%20Co:john@example.com?secret=HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ&issuer=ACME%20Co&algorithm="
|
||
"SHA1&digits=6&period=30";
|
||
auto 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::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&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";
|
||
settings = Totp::parseSettings("30;8", secret);
|
||
QVERIFY(!settings.isNull());
|
||
QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
|
||
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";
|
||
settings = Totp::parseSettings("", secret);
|
||
QVERIFY(!settings.isNull());
|
||
QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
|
||
QCOMPARE(settings->custom, false);
|
||
QCOMPARE(settings->digits, 6u);
|
||
QCOMPARE(settings->step, 30u);
|
||
QCOMPARE(settings->hashType, Totp::HashType::Sha1);
|
||
}
|
||
|
||
void TestTotp::testTotpCode()
|
||
{
|
||
// Test vectors from RFC 6238
|
||
// https://tools.ietf.org/html/rfc6238#appendix-B
|
||
auto settings = Totp::createSettings("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP);
|
||
|
||
// Test 6 digit TOTP (default)
|
||
quint64 time = 1234567890;
|
||
QCOMPARE(Totp::generateTotp(settings, time), QString("005924"));
|
||
|
||
time = 1111111109;
|
||
QCOMPARE(Totp::generateTotp(settings, time), QString("081804"));
|
||
|
||
// Test 8 digit TOTP (custom)
|
||
settings->digits = 8;
|
||
settings->custom = true;
|
||
time = 1111111111;
|
||
QCOMPARE(Totp::generateTotp(settings, time), QString("14050471"));
|
||
|
||
time = 2000000000;
|
||
QCOMPARE(Totp::generateTotp(settings, time), QString("69279037"));
|
||
}
|
||
|
||
void TestTotp::testSteamTotp()
|
||
{
|
||
// 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);
|
||
|
||
QCOMPARE(settings->key, QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK"));
|
||
QCOMPARE(settings->encoder.shortName, Totp::STEAM_SHORTNAME);
|
||
QCOMPARE(settings->digits, Totp::STEAM_DIGITS);
|
||
QCOMPARE(settings->step, 30u);
|
||
|
||
// These time/value pairs were created by running the Steam Guard function of the
|
||
// Steam mobile app with a throw-away steam account. The above secret was extracted
|
||
// from the Steam app's data for use in testing here.
|
||
quint64 time = 1511200518;
|
||
QCOMPARE(Totp::generateTotp(settings, time), QString("FR8RV"));
|
||
time = 1511200714;
|
||
QCOMPARE(Totp::generateTotp(settings, time), QString("9P3VP"));
|
||
}
|
||
|
||
void TestTotp::testEntryHistory()
|
||
{
|
||
Entry entry;
|
||
uint step = 16;
|
||
uint digits = 6;
|
||
auto settings = Totp::createSettings("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ", digits, step);
|
||
// Test that entry starts without TOTP
|
||
QCOMPARE(entry.historyItems().size(), 0);
|
||
QVERIFY(!entry.hasTotp());
|
||
// Add TOTP to entry
|
||
entry.setTotp(settings);
|
||
QCOMPARE(entry.historyItems().size(), 1);
|
||
QVERIFY(entry.hasTotp());
|
||
QCOMPARE(entry.totpSettings()->key, QString("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"));
|
||
// Change key and verify settings changed
|
||
settings->key = "foo";
|
||
entry.setTotp(settings);
|
||
QCOMPARE(entry.historyItems().size(), 2);
|
||
QCOMPARE(entry.totpSettings()->key, QString("foo"));
|
||
}
|