keepassxc/tests/TestTotp.cpp
Bryan Jacobs 04983ce4cd 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.
2019-04-15 14:23:26 -04:00

154 lines
5.7 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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"));
}