refactor: prepare AccountStorage for more complex tokens

With this the AccountStorage module now fully supports some HOTP/TOTP parameters which are uncommon (but still part of HOTP/TOTP specifications).

 - Better types for offset, tokenLength. Make this consistent throughout
 - Finish support for offset, checksum parameters for HOTP tokens in AccountStorage
 - Finish support for hashing algorithm, epoch parameters for TOTP tokens in AccountStorage
 - Better API for creating oath::Algorithm instances
 - Code formatting (break up long lines)

Issues: #7
master
Johan Ouwerkerk 2020-06-27 16:20:01 +02:00
parent 3c7aa2010c
commit 09df98d3a4
30 changed files with 675 additions and 294 deletions

View File

@ -25,7 +25,7 @@ private:
static QByteArray rfcSecret("12345678901234567890");
// the RFC test vector consists of 6-character tokens
static int tokenLength = 6;
static uint tokenLength = 6;
void ComputeHotpTest::initTestCase(void)
{
@ -39,7 +39,7 @@ void ComputeHotpTest::testDefaults(void)
std::optional<secrets::EncryptedSecret> tokenSecret = test::encrypt(&m_secret, rfcSecret);
QVERIFY2(tokenSecret, "should be able to encrypt the token secret");
accounts::ComputeHotp uut(&m_secret, *tokenSecret, counter, tokenLength);
accounts::ComputeHotp uut(&m_secret, *tokenSecret, tokenLength, counter, std::nullopt, false);
QSignalSpy tokenGenerated(&uut, &accounts::ComputeHotp::otp);
QSignalSpy jobFinished(&uut, &accounts::ComputeHotp::finished);

View File

@ -25,7 +25,7 @@ private:
static QByteArray rfcSecret("12345678901234567890");
// the RFC test vector consists of 6-character tokens
static int tokenLength = 6;
static uint tokenLength = 6;
// the default TOTP timestep is 30s
static uint timeStep = 30;
@ -48,7 +48,7 @@ void ComputeTotpTest::testDefaults(void)
std::optional<secrets::EncryptedSecret> tokenSecret = test::encrypt(&m_secret, rfcSecret);
QVERIFY2(tokenSecret, "should be able to encrypt the token secret");
accounts::ComputeTotp uut(&m_secret, *tokenSecret, epoch, timeStep, tokenLength, accounts::Account::Hash::Default, clock);
accounts::ComputeTotp uut(&m_secret, *tokenSecret, tokenLength, epoch, timeStep, accounts::Account::Hash::Sha1, clock);
QSignalSpy tokenGenerated(&uut, &accounts::ComputeTotp::otp);
QSignalSpy jobFinished(&uut, &accounts::ComputeTotp::finished);

View File

@ -35,6 +35,11 @@ private:
static QByteArray rawSecret = QByteArray::fromBase64(QByteArray("8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="), QByteArray::Base64Encoding);
static QByteArray rawNonce = QByteArray::fromBase64(QByteArray("QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB"), QByteArray::Base64Encoding);
static qint64 dummyClock(void)
{
return 1'234'567'890LL;
}
void LoadAccountsTest::initTestCase(void)
{
QVERIFY2(test::ensureOutputDirectory(), "output directory should be available");
@ -54,7 +59,7 @@ void LoadAccountsTest::emptyAccountsFile(void)
action(data);
});
accounts::LoadAccounts uut(settings, &m_secret);
accounts::LoadAccounts uut(settings, &m_secret, &dummyClock);
QSignalSpy hotpFound(&uut, &accounts::LoadAccounts::foundHotp);
QSignalSpy totpFound(&uut, &accounts::LoadAccounts::foundTotp);
@ -80,7 +85,7 @@ void LoadAccountsTest::sampleAccountsFile(void)
action(data);
});
accounts::LoadAccounts uut(settings, &m_secret);
accounts::LoadAccounts uut(settings, &m_secret, &dummyClock);
QSignalSpy hotpFound(&uut, &accounts::LoadAccounts::foundHotp);
QSignalSpy totpFound(&uut, &accounts::LoadAccounts::foundTotp);
@ -101,8 +106,11 @@ void LoadAccountsTest::sampleAccountsFile(void)
QCOMPARE(firstHotp.at(2).toString(), QString());
QCOMPARE(firstHotp.at(3).toByteArray(), rawSecret);
QCOMPARE(firstHotp.at(4).toByteArray(), rawNonce);
QCOMPARE(firstHotp.at(5).toULongLong(), 0ULL);
QCOMPARE(firstHotp.at(6).toInt(), 6);
QCOMPARE(firstHotp.at(5).toUInt(), 6U);
QCOMPARE(firstHotp.at(6).toULongLong(), 0ULL);
QCOMPARE(firstHotp.at(7).toBool(), false);
QCOMPARE(firstHotp.at(8).toUInt(), 0U);
QCOMPARE(firstHotp.at(9).toBool(), false);
const auto secondHotp = hotpFound.at(1);
QCOMPARE(secondHotp.at(0).toUuid(), QUuid(QLatin1String("437c23aa-2fb0-519a-9a34-a5a2671eea24")));
@ -110,8 +118,11 @@ void LoadAccountsTest::sampleAccountsFile(void)
QCOMPARE(secondHotp.at(2).toString(), QLatin1String("autotests"));
QCOMPARE(secondHotp.at(3).toByteArray(), rawSecret);
QCOMPARE(secondHotp.at(4).toByteArray(), rawNonce);
QCOMPARE(secondHotp.at(5).toULongLong(), 0ULL);
QCOMPARE(secondHotp.at(6).toInt(), 6);
QCOMPARE(secondHotp.at(5).toUInt(), 6U);
QCOMPARE(secondHotp.at(6).toULongLong(), 0ULL);
QCOMPARE(secondHotp.at(7).toBool(), true);
QCOMPARE(secondHotp.at(8).toUInt(), 12U);
QCOMPARE(secondHotp.at(9).toBool(), true);
const auto firstTotp = totpFound.at(0);
QCOMPARE(firstTotp.at(0).toUuid(), QUuid(QLatin1String("534cc72e-e9ec-5e39-a1ff-9f017c9be8cc")));
@ -119,8 +130,10 @@ void LoadAccountsTest::sampleAccountsFile(void)
QCOMPARE(firstTotp.at(2).toString(), QString());
QCOMPARE(firstHotp.at(3).toByteArray(), rawSecret);
QCOMPARE(firstHotp.at(4).toByteArray(), rawNonce);
QCOMPARE(firstTotp.at(5).toUInt(), 30);
QCOMPARE(firstTotp.at(6).toInt(), 6);
QCOMPARE(firstTotp.at(5).toUInt(), 6);
QCOMPARE(firstTotp.at(6).toUInt(), 30U);
QCOMPARE(firstTotp.at(7).toDateTime(), QDateTime::fromMSecsSinceEpoch(0));
QCOMPARE(firstTotp.at(8).value<accounts::Account::Hash>(), accounts::Account::Hash::Sha1);
const auto secondTotp = totpFound.at(1);
QCOMPARE(secondTotp.at(0).toUuid(), QUuid(QLatin1String("6537d6a5-005e-5a92-b560-b09df3c2e676")));
@ -128,8 +141,10 @@ void LoadAccountsTest::sampleAccountsFile(void)
QCOMPARE(secondTotp.at(2).toString(), QLatin1String("autotests"));
QCOMPARE(secondHotp.at(3).toByteArray(), rawSecret);
QCOMPARE(secondHotp.at(4).toByteArray(), rawNonce);
QCOMPARE(secondTotp.at(5).toUInt(), 30);
QCOMPARE(secondTotp.at(6).toInt(), 6);
QCOMPARE(secondTotp.at(5).toUInt(), 6U);
QCOMPARE(secondTotp.at(6).toUInt(), 30U);
QCOMPARE(secondTotp.at(7).toDateTime(), QDateTime::fromMSecsSinceEpoch(1'234'567'890LL).toUTC());
QCOMPARE(secondTotp.at(8).value<accounts::Account::Hash>(), accounts::Account::Hash::Sha256);
}
void LoadAccountsTest::invalidSampleAccountsFile(void)
@ -142,7 +157,7 @@ void LoadAccountsTest::invalidSampleAccountsFile(void)
action(data);
});
accounts::LoadAccounts uut(settings, &m_secret);
accounts::LoadAccounts uut(settings, &m_secret, &dummyClock);
QSignalSpy hotpFound(&uut, &accounts::LoadAccounts::foundHotp);
QSignalSpy totpFound(&uut, &accounts::LoadAccounts::foundTotp);

View File

@ -1,3 +1,55 @@
[%7B0644b04a-58c0-4852-9a31-0e427d802e66%7D]
account=0644b04a-58c0-4852-9a31-0e427d802e66
issuer=""
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=totp
[%7B0a1c83c9-62a8-4854-b985-7f364251dbf1%7D]
account=0a1c83c9-62a8-4854-b985-7f364251dbf1
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="FB1uuIu7qNWfqyGhFcemi7pxgcZgLNXoZZZtbdA="
timeStep=30
type=totp
[%7B19a1b960-e2ac-44f3-8e51-547d74e595d1%7D]
account=19a1b960-e2ac-44f3-8e51-547d74e595d1
hash=foo
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=totp
[%7B322d41d9-bf76-44e9-abfc-222f6534644e%7D]
account=322d41d9-bf76-44e9-abfc-222f6534644e
checksum=1
counter=0
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7B5aef5e62-d494-4a8d-aa44-a49b7f4c2b28%7D]
account=5aef5e62-d494-4a8d-aa44-a49b7f4c2b28
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=
type=totp
[%7B62274f6a-ee48-49e0-a63d-c3ee41f4e262%7D]
account=62274f6a-ee48-49e0-a63d-c3ee41f4e262
counter=0
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
offset=-1
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7B6814578e-2019-4ecb-b7c6-844c318379a5%7D]
account=
counter=0
@ -6,32 +58,33 @@ pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7Bb2167344-25d2-49fe-a420-7096aa511f67%7D]
account=invalid-hotp-sample-2
counter=-1
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7Bf6fd1348-b8af-4eb3-945f-fbc94026a6c7%7D]
account=invalid-hotp-sample-3
[%7B712764dd-db11-4e68-a764-2825ef5bad9d%7D]
account=712764dd-db11-4e68-a764-2825ef5bad9d
counter=0
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFZ
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
offset=17
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7B75394fe2-35bf-4ca3-8e0c-9102810b9d55%7D]
account=invalid-hotp-sample-4
account=75394fe2-35bf-4ca3-8e0c-9102810b9d55
counter=0
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=0
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7Bb2167344-25d2-49fe-a420-7096aa511f67%7D]
account=b2167344-25d2-49fe-a420-7096aa511f67
counter=-1
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7Bc02ae4fe-b6fb-4191-93fb-304a7197320c%7D]
account=invalid-hotp-sample-5
account=c02ae4fe-b6fb-4191-93fb-304a7197320c
counter=
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
@ -39,7 +92,7 @@ secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7Bd06103a4-8fa9-4a1f-a370-911bb84825a0%7D]
account=invalid-hotp-sample-6
account=d06103a4-8fa9-4a1f-a370-911bb84825a0
counter=0
issuer=auto:tests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
@ -47,51 +100,52 @@ pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7Bfff1868c-e1db-4cd7-b931-7d1e51154c64%7D]
account=invalid-totp-sample-1
[%7Bd3a8bf31-f27a-4c41-9828-1bd038a99645%7D]
account=d3a8bf31-f27a-4c41-9828-1bd038a99645
epoch=1980-01-01T00:00:00Z
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=0
type=totp
[%7B5aef5e62-d494-4a8d-aa44-a49b7f4c2b28%7D]
account=invalid-totp-sample-2
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=
timeStep=30
type=totp
[%7Bd80d6d02-ea5e-47a0-bf51-e4b37b016ec2%7D]
account=invalid-totp-sample-3
account=d80d6d02-ea5e-47a0-bf51-e4b37b016ec2
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=totp
[%7Be52475a5-3708-49a5-a7ce-eb538a8a7383%7D]
account=e52475a5-3708-49a5-a7ce-eb538a8a7383
epoch=foo
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=totp
[%7Be59f254d-729f-4631-8d7f-69fa314ed664%7D]
account=invalid-totp-sample-4
account=e59f254d-729f-4631-8d7f-69fa314ed664
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=foo
[%7B0a1c83c9-62a8-4854-b985-7f364251dbf1%7D]
account=invalid-totp-sample-5
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
[%7Bf6fd1348-b8af-4eb3-945f-fbc94026a6c7%7D]
account=f6fd1348-b8af-4eb3-945f-fbc94026a6c7
counter=0
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFZ
pinLength=6
secret="FB1uuIu7qNWfqyGhFcemi7pxgcZgLNXoZZZtbdA="
timeStep=30
type=totp
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7B0644b04a-58c0-4852-9a31-0e427d802e66%7D]
account=invalid-totp-sample-6
issuer=""
[%7Bfff1868c-e1db-4cd7-b931-7d1e51154c64%7D]
account=fff1868c-e1db-4cd7-b931-7d1e51154c64
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
timeStep=0
type=totp

View File

@ -8,9 +8,11 @@ type=hotp
[%7B437c23aa-2fb0-519a-9a34-a5a2671eea24%7D]
account=valid-hotp-sample-2
checksum=true
counter=0
issuer=autotests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
offset=12
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
@ -31,3 +33,5 @@ pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=totp
epoch=1970-01-15T06:56:07.890Z
hash=Sha256

View File

@ -1,5 +1,6 @@
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
account=valid-hotp-sample-1
checksum=false
counter=0
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6

View File

@ -1,8 +1,10 @@
[%7B437c23aa-2fb0-519a-9a34-a5a2671eea24%7D]
account=valid-hotp-sample-2
checksum=true
counter=0
issuer=autotests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
offset=12
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp

View File

@ -1,5 +1,7 @@
[%7B534cc72e-e9ec-5e39-a1ff-9f017c9be8cc%7D]
account=valid-totp-sample-1
epoch=1970-01-01T00:00:00.000Z
hash=Sha1
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="

View File

@ -1,5 +1,7 @@
[%7B6537d6a5-005e-5a92-b560-b09df3c2e676%7D]
account=valid-totp-sample-2
epoch=1970-01-15T06:56:07.890Z
hash=Sha512
issuer=autotests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6

View File

@ -17,6 +17,10 @@
#include <QUuid>
#include <QtDebug>
#include <limits>
Q_DECLARE_METATYPE(std::optional<uint>);
class SaveHotpTest: public QObject
{
Q_OBJECT
@ -37,13 +41,15 @@ static void define_test_data(void)
QTest::addColumn<QString>("issuer");
QTest::addColumn<quint64>("counter");
QTest::addColumn<int>("tokenLength");
QTest::addColumn<std::optional<uint>>("offset");
QTest::addColumn<bool>("checksum");
QTest::addColumn<QString>("actualAccountsIni");
QTest::addColumn<QString>("expectedAccountsIni");
}
static void define_test_case(const char * label, const QUuid &id, const QString &accountName, const QString &issuer, quint64 counter, int tokenLength, const QString &actualIni, const QString &expectedIni)
static void define_test_case(const char * label, const QUuid &id, const QString &accountName, const QString &issuer, quint64 counter, int tokenLength, const std::optional<uint> &offset, bool checksum, const QString &actualIni, const QString &expectedIni)
{
QTest::newRow(label) << id << accountName << issuer << counter << tokenLength << actualIni << expectedIni;
QTest::newRow(label) << id << accountName << issuer << counter << tokenLength << offset << checksum << actualIni << expectedIni;
}
void SaveHotpTest::validHotp(void)
@ -53,6 +59,8 @@ void SaveHotpTest::validHotp(void)
QFETCH(QString, issuer);
QFETCH(quint64, counter);
QFETCH(int, tokenLength);
QFETCH(std::optional<uint>, offset);
QFETCH(bool, checksum);
QFETCH(QString, actualAccountsIni);
QFETCH(QString, expectedAccountsIni);
@ -70,7 +78,7 @@ void SaveHotpTest::validHotp(void)
std::optional<secrets::EncryptedSecret> tokenSecret = test::encrypt(&m_secret, QByteArray("Hello, world!"));
QVERIFY2(tokenSecret, "should be able to encrypt the token secret");
accounts::SaveHotp uut(settings, id, name, issuer, *tokenSecret, counter, tokenLength);
accounts::SaveHotp uut(settings, id, name, issuer, *tokenSecret, counter, tokenLength, offset, checksum);
QSignalSpy invalidAccount(&uut, &accounts::SaveHotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveHotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveHotp::finished);
@ -99,6 +107,8 @@ void SaveHotpTest::invalidHotp(void)
QFETCH(QString, issuer);
QFETCH(quint64, counter);
QFETCH(int, tokenLength);
QFETCH(std::optional<uint>, offset);
QFETCH(bool, checksum);
QFETCH(QString, actualAccountsIni);
QFETCH(QString, expectedAccountsIni);
@ -116,7 +126,7 @@ void SaveHotpTest::invalidHotp(void)
std::optional<secrets::EncryptedSecret> tokenSecret = test::encrypt(&m_secret, QByteArray("Hello, world!"));
QVERIFY2(tokenSecret, "should be able to encrypt the token secret");
accounts::SaveHotp uut(settings, id, name, issuer, *tokenSecret, counter, tokenLength);
accounts::SaveHotp uut(settings, id, name, issuer, *tokenSecret, counter, tokenLength, offset, checksum);
QSignalSpy invalidAccount(&uut, &accounts::SaveHotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveHotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveHotp::finished);
@ -138,20 +148,44 @@ void SaveHotpTest::invalidHotp(void)
void SaveHotpTest::validHotp_data(void)
{
define_test_data();
define_test_case("valid-hotp-sample-1", QUuid("072a645d-6c26-57cc-81eb-d9ef3b9b39e2"), QLatin1String("valid-hotp-sample-1"), QString(), 0, 6, QLatin1String("save-valid-hotp-accounts-1.ini"), QLatin1String(":/save-hotp/expected-accounts-1.ini"));
define_test_case("valid-hotp-sample-2", QUuid("437c23aa-2fb0-519a-9a34-a5a2671eea24"), QLatin1String("valid-hotp-sample-2"), QLatin1String("autotests"), 0, 6, QLatin1String("save-valid-hotp-accounts-2.ini"), QLatin1String(":/save-hotp/expected-accounts-2.ini"));
define_test_case("valid-hotp-sample-1", QUuid("072a645d-6c26-57cc-81eb-d9ef3b9b39e2"), QLatin1String("valid-hotp-sample-1"), QString(), 6U,
0U, std::nullopt, false,
QLatin1String("save-valid-hotp-accounts-1.ini"), QLatin1String(":/save-hotp/expected-accounts-1.ini"));
define_test_case("valid-hotp-sample-2", QUuid("437c23aa-2fb0-519a-9a34-a5a2671eea24"), QLatin1String("valid-hotp-sample-2"), QLatin1String("autotests"), 6U,
0U, std::optional<uint>(12U), true,
QLatin1String("save-valid-hotp-accounts-2.ini"), QLatin1String(":/save-hotp/expected-accounts-2.ini"));
}
void SaveHotpTest::invalidHotp_data(void)
{
define_test_data();
define_test_case("null UUID", QUuid(), QLatin1String("null UUID"), QString(), 0, 6, QLatin1String("save-hotp-dummy-accounts-1.ini"), QString());
define_test_case("null account name", QUuid("00611bbf-5e0b-5c6a-9847-ad865315ce86"), QString(), QString(), 0, 6, QLatin1String("save-hotp-dummy-accounts-2.ini"), QString());
define_test_case("empty account name", QUuid("1e42b907-99d8-5da3-a59b-89b257e49c83"), QLatin1String(""), QString(), 0, 6, QLatin1String("save-hotp-dummy-accounts-3.ini"), QString());
define_test_case("empty issuer name", QUuid("533b406b-ad04-5203-a26f-5deb0afeba22"), QLatin1String("empty issuer name"), QLatin1String(""), 0, 6, QLatin1String("save-hotp-dummy-accounts-4.ini"), QString());
define_test_case("invalid issuer name", QUuid("1c1ffa42-bb9f-5413-a8a7-6c5b0eb8a36f"), QLatin1String("invalid issuer name"), QLatin1String(":"), 0, 6, QLatin1String("save-hotp-dummy-accounts-5.ini"), QString());
define_test_case("tokenLength too small", QUuid("bca12e13-4b5b-5e4e-b162-3b86a6284dea"), QLatin1String("tokenLength too small"), QString(), 0, 5, QLatin1String("save-hotp-dummy-accounts-6.ini"), QString());
define_test_case("tokenLength too large", QUuid("5c10d530-fb22-5438-848d-3d4d1f738610"), QLatin1String("tokenLength too large"), QString(), 0, 11, QLatin1String("save-hotp-dummy-accounts-7.ini"), QString());
define_test_case("null UUID", QUuid(), QLatin1String("null UUID"), QString(), 6U,
0U, std::nullopt, false,
QLatin1String("save-hotp-dummy-accounts-1.ini"), QString());
define_test_case("null account name", QUuid("00611bbf-5e0b-5c6a-9847-ad865315ce86"), QString(), QString(), 6U,
0U, std::nullopt, false,
QLatin1String("save-hotp-dummy-accounts-2.ini"), QString());
define_test_case("empty account name", QUuid("1e42b907-99d8-5da3-a59b-89b257e49c83"), QLatin1String(""), QString(), 6U,
0U, std::nullopt, false,
QLatin1String("save-hotp-dummy-accounts-3.ini"), QString());
define_test_case("empty issuer name", QUuid("533b406b-ad04-5203-a26f-5deb0afeba22"), QLatin1String("empty issuer name"), QLatin1String(""), 6U,
0U, std::nullopt, false,
QLatin1String("save-hotp-dummy-accounts-4.ini"), QString());
define_test_case("invalid issuer name", QUuid("1c1ffa42-bb9f-5413-a8a7-6c5b0eb8a36f"), QLatin1String("invalid issuer name"), QLatin1String(":"), 6U,
0U, std::nullopt, false,
QLatin1String("save-hotp-dummy-accounts-5.ini"), QString());
define_test_case("tokenLength too small", QUuid("bca12e13-4b5b-5e4e-b162-3b86a6284dea"), QLatin1String("tokenLength too small"), QString(), 5U,
0U, std::nullopt, false,
QLatin1String("save-hotp-dummy-accounts-6.ini"), QString());
define_test_case("tokenLength too large", QUuid("5c10d530-fb22-5438-848d-3d4d1f738610"), QLatin1String("tokenLength too large"), QString(), 11U,
0U, std::nullopt, false,
QLatin1String("save-hotp-dummy-accounts-7.ini"), QString());
define_test_case("offset too large", QUuid("d0f545da-cc1e-57aa-b793-d856757f33e8"), QLatin1String("offset too large"), QString(), 6U,
0U, std::optional<uint>(17U), false,
QLatin1String("save-hotp-dummy-accounts-8.ini"), QString());
define_test_case("offset out of range", QUuid("b31acaca-ee8f-54aa-948e-d67789fbe74c"), QLatin1String("offset out of range"), QString(), 6U,
0U, std::optional<uint>(std::numeric_limits<uint>::max()), false,
QLatin1String("save-hotp-dummy-accounts-9.ini"), QString());
}
void SaveHotpTest::initTestCase(void)

View File

@ -37,13 +37,16 @@ static void define_test_data(void)
QTest::addColumn<QString>("issuer");
QTest::addColumn<uint>("timeStep");
QTest::addColumn<int>("tokenLength");
QTest::addColumn<QDateTime>("epoch");
QTest::addColumn<accounts::Account::Hash>("hash");
QTest::addColumn<qint64>("now");
QTest::addColumn<QString>("actualAccountsIni");
QTest::addColumn<QString>("expectedAccountsIni");
}
static void define_test_case(const char * label, const QUuid &id, const QString &accountName, const QString &issuer, uint timeStep, int tokenLength, const QString &actualIni, const QString &expectedIni)
static void define_test_case(const char * label, const QUuid &id, const QString &accountName, const QString &issuer, uint timeStep, int tokenLength, const QDateTime &epoch, accounts::Account::Hash hash, qint64 now, const QString &actualIni, const QString &expectedIni)
{
QTest::newRow(label) << id << accountName << issuer << timeStep << tokenLength << actualIni << expectedIni;
QTest::newRow(label) << id << accountName << issuer << timeStep << tokenLength << epoch << hash << now << actualIni << expectedIni;
}
void SaveTotpTest::validTotp(void)
@ -53,9 +56,17 @@ void SaveTotpTest::validTotp(void)
QFETCH(QString, issuer);
QFETCH(uint, timeStep);
QFETCH(int, tokenLength);
QFETCH(QDateTime, epoch);
QFETCH(accounts::Account::Hash, hash);
QFETCH(qint64, now);
QFETCH(QString, actualAccountsIni);
QFETCH(QString, expectedAccountsIni);
const std::function<qint64(void)> clock([now](void) -> qint64
{
return now;
});
const QString actual = test::path(actualAccountsIni);
const QString lock = test::path(actualAccountsIni + QLatin1String(".lock"));
bool actionRun = false;
@ -70,7 +81,7 @@ void SaveTotpTest::validTotp(void)
std::optional<secrets::EncryptedSecret> tokenSecret = test::encrypt(&m_secret, QByteArray("Hello, world!"));
QVERIFY2(tokenSecret, "should be able to encrypt the token secret");
accounts::SaveTotp uut(settings, id, name, issuer, *tokenSecret, timeStep, tokenLength);
accounts::SaveTotp uut(settings, id, name, issuer, *tokenSecret, timeStep, tokenLength, epoch, hash, clock);
QSignalSpy invalidAccount(&uut, &accounts::SaveTotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveTotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveTotp::finished);
@ -99,9 +110,17 @@ void SaveTotpTest::invalidTotp(void)
QFETCH(QString, issuer);
QFETCH(uint, timeStep);
QFETCH(int, tokenLength);
QFETCH(QDateTime, epoch);
QFETCH(accounts::Account::Hash, hash);
QFETCH(qint64, now);
QFETCH(QString, actualAccountsIni);
QFETCH(QString, expectedAccountsIni);
const std::function<qint64(void)> clock([now](void) -> qint64
{
return now;
});
const QString actual = test::path(actualAccountsIni);
const QString lock = test::path(actualAccountsIni + QLatin1String(".lock"));
bool actionRun = false;
@ -116,7 +135,7 @@ void SaveTotpTest::invalidTotp(void)
std::optional<secrets::EncryptedSecret> tokenSecret = test::encrypt(&m_secret, QByteArray("Hello, world!"));
QVERIFY2(tokenSecret, "should be able to encrypt the token secret");
accounts::SaveTotp uut(settings, id, name, issuer, *tokenSecret, timeStep, tokenLength);
accounts::SaveTotp uut(settings, id, name, issuer, *tokenSecret, timeStep, tokenLength, epoch, hash, clock);
QSignalSpy invalidAccount(&uut, &accounts::SaveTotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveTotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveTotp::finished);
@ -138,21 +157,47 @@ void SaveTotpTest::invalidTotp(void)
void SaveTotpTest::validTotp_data(void)
{
define_test_data();
define_test_case("valid-totp-sample-1", QUuid("534cc72e-e9ec-5e39-a1ff-9f017c9be8cc"), QLatin1String("valid-totp-sample-1"), QString(), 30, 6, QLatin1String("save-valid-totp-accounts-1.ini"), QLatin1String(":/save-totp/expected-accounts-1.ini"));
define_test_case("valid-totp-sample-2", QUuid("6537d6a5-005e-5a92-b560-b09df3c2e676"), QLatin1String("valid-totp-sample-2"), QLatin1String("autotests"), 30, 6, QLatin1String("save-valid-totp-accounts-2.ini"), QLatin1String(":/save-totp/expected-accounts-2.ini"));
define_test_case("valid-totp-sample-1", QUuid("534cc72e-e9ec-5e39-a1ff-9f017c9be8cc"), QLatin1String("valid-totp-sample-1"), QString(), 6U,
30U, QDateTime::fromMSecsSinceEpoch(0), accounts::Account::Hash::Sha1,
0LL, QLatin1String("save-valid-totp-accounts-1.ini"), QLatin1String(":/save-totp/expected-accounts-1.ini"));
define_test_case("valid-totp-sample-2", QUuid("6537d6a5-005e-5a92-b560-b09df3c2e676"), QLatin1String("valid-totp-sample-2"), QLatin1String("autotests"), 6U,
30U, QDateTime::fromMSecsSinceEpoch(1'234'567'890LL), accounts::Account::Hash::Sha512,
2'000'000'000LL, QLatin1String("save-valid-totp-accounts-2.ini"), QLatin1String(":/save-totp/expected-accounts-2.ini"));
}
void SaveTotpTest::invalidTotp_data(void)
{
define_test_data();
define_test_case("null UUID", QUuid(), QLatin1String("null UUID"), QString(), 30, 6, QLatin1String("save-totp-dummy-accounts-1.ini"), QString());
define_test_case("null account name", QUuid("00611bbf-5e0b-5c6a-9847-ad865315ce86"), QString(), QString(), 30, 6, QLatin1String("save-totp-dummy-accounts-2.ini"), QString());
define_test_case("empty account name", QUuid("1e42b907-99d8-5da3-a59b-89b257e49c83"), QLatin1String(""), QString(), 30, 6, QLatin1String("save-totp-dummy-accounts-3.ini"), QString());
define_test_case("empty issuer name", QUuid("533b406b-ad04-5203-a26f-5deb0afeba22"), QLatin1String("empty issuer name"), QLatin1String(""), 30, 6, QLatin1String("save-totp-dummy-accounts-4.ini"), QString());
define_test_case("empty issuer name", QUuid("1c1ffa42-bb9f-5413-a8a7-6c5b0eb8a36f"), QLatin1String("invalid issuer name"), QLatin1String(":"), 30, 6, QLatin1String("save-totp-dummy-accounts-5.ini"), QString());
define_test_case("timeStep too small", QUuid("5ab8749b-f973-5f48-a70e-c261ebd0521a"), QLatin1String("timeStep too small"), QString(), 0, 6, QLatin1String("save-totp-dummy-accounts-6.ini"), QString());
define_test_case("tokenLength too small", QUuid("bca12e13-4b5b-5e4e-b162-3b86a6284dea"), QLatin1String("tokenLength too small"), QString(), 30, 5, QLatin1String("save-totp-dummy-accounts-7.ini"), QString());
define_test_case("tokenLength too large", QUuid("5c10d530-fb22-5438-848d-3d4d1f738610"), QLatin1String("tokenLength too large"), QString(), 30, 11, QLatin1String("save-totp-dummy-accounts-8.ini"), QString());
define_test_case("null UUID", QUuid(), QLatin1String("null UUID"), QString(), 6U,
30U, QDateTime::fromMSecsSinceEpoch(0LL), accounts::Account::Hash::Sha1,
0LL, QLatin1String("save-totp-dummy-accounts-1.ini"), QString());
define_test_case("null account name", QUuid("00611bbf-5e0b-5c6a-9847-ad865315ce86"), QString(), QString(), 6U,
30U, QDateTime::fromMSecsSinceEpoch(0LL), accounts::Account::Hash::Sha1,
0LL, QLatin1String("save-totp-dummy-accounts-2.ini"), QString());
define_test_case("empty account name", QUuid("1e42b907-99d8-5da3-a59b-89b257e49c83"), QLatin1String(""), QString(), 6U,
30U, QDateTime::fromMSecsSinceEpoch(0LL), accounts::Account::Hash::Sha1,
0LL, QLatin1String("save-totp-dummy-accounts-3.ini"), QString());
define_test_case("empty issuer name", QUuid("533b406b-ad04-5203-a26f-5deb0afeba22"), QLatin1String("empty issuer name"), QLatin1String(""), 6U,
30U, QDateTime::fromMSecsSinceEpoch(0LL), accounts::Account::Hash::Sha1,
0LL, QLatin1String("save-totp-dummy-accounts-4.ini"), QString());
define_test_case("empty issuer name", QUuid("1c1ffa42-bb9f-5413-a8a7-6c5b0eb8a36f"), QLatin1String("invalid issuer name"), QLatin1String(":"), 6U,
30U, QDateTime::fromMSecsSinceEpoch(0LL), accounts::Account::Hash::Sha1,
0LL, QLatin1String("save-totp-dummy-accounts-5.ini"), QString());
define_test_case("timeStep too small", QUuid("5ab8749b-f973-5f48-a70e-c261ebd0521a"), QLatin1String("timeStep too small"), QString(), 6U,
0U, QDateTime::fromMSecsSinceEpoch(0LL), accounts::Account::Hash::Sha1,
0LL, QLatin1String("save-totp-dummy-accounts-6.ini"), QString());
define_test_case("tokenLength too small", QUuid("bca12e13-4b5b-5e4e-b162-3b86a6284dea"), QLatin1String("tokenLength too small"), QString(), 5U,
30U, QDateTime::fromMSecsSinceEpoch(0LL), accounts::Account::Hash::Sha1,
0LL, QLatin1String("save-totp-dummy-accounts-7.ini"), QString());
define_test_case("tokenLength too large", QUuid("5c10d530-fb22-5438-848d-3d4d1f738610"), QLatin1String("tokenLength too large"), QString(), 11U,
30U, QDateTime::fromMSecsSinceEpoch(0LL), accounts::Account::Hash::Sha1,
0LL, QLatin1String("save-totp-dummy-accounts-8.ini"), QString());
define_test_case("null/invalid epoch datetime", QUuid("e719ed90-e0c0-5510-81c1-ccfd7a5e962c"), QLatin1String("null/invalid epoch datetime"), QString(), 6U,
30U, QDateTime(), accounts::Account::Hash::Sha1,
2'000'000'000LL, QLatin1String("save-totp-dummy-accounts-9.ini"), QString());
define_test_case("future epoch datetime", QUuid("0e03a2ed-8e49-54ac-8e46-20536078e5a1"), QLatin1String("future epoch datetime"), QString(), 6U,
30U, QDateTime::fromMSecsSinceEpoch(1'234'567'890LL), accounts::Account::Hash::Sha1,
0LL, QLatin1String("save-totp-dummy-accounts-10.ini"), QString());
}
void SaveTotpTest::initTestCase(void)

View File

@ -107,7 +107,7 @@ void HotpCounterUpdateTest::testCounterUpdate(void)
QCOMPARE(sampleAccount->counter(), 0ULL);
QCOMPARE(sampleAccount->tokenLength(), 6);
QCOMPARE(sampleAccount->offset(), -1);
QCOMPARE(sampleAccount->offset(), std::nullopt);
QCOMPARE(sampleAccount->checksum(), false);
QFile initialLockFile(test::path(testIniLockFile));

View File

@ -7,6 +7,7 @@ salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
account=valid-hotp-sample-1
checksum=false
counter=1
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6

View File

@ -7,6 +7,8 @@ salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
[%7B3ff3fc9b-9e8c-50aa-8f51-99d213843761%7D]
account=valid-totp-sample-1
epoch=1970-01-01T00:00:00.000Z
hash=Sha1
issuer=autotests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=8

View File

@ -130,7 +130,7 @@ void StorageLifeCyclesTest::testLifecycle(void)
QCOMPARE(initialAccount->counter(), 42ULL);
QCOMPARE(initialAccount->tokenLength(), 7);
QCOMPARE(initialAccount->offset(), -1);
QCOMPARE(initialAccount->offset(), std::nullopt);
QCOMPARE(initialAccount->checksum(), false);
QFile initialLockFile(test::path(testIniLockFile));
@ -170,7 +170,7 @@ void StorageLifeCyclesTest::testLifecycle(void)
QVERIFY2(test::signal_eventually_emitted_once(initialAccountCleaned), "sample account should be cleaned up by now");
// third phase: check that new account objects can be added to storage
uut->addTotp(addedAccountName, accountIssuer, QLatin1String("NBSWY3DPFQQHO33SNRSCC==="), 42, 8);
uut->addTotp(addedAccountName, accountIssuer, QLatin1String("NBSWY3DPFQQHO33SNRSCC==="), 8U, 42U);
QVERIFY2(test::signal_eventually_emitted_twice(accountAdded), "new account should be added to storage by now");
QCOMPARE(error.count(), 0);
@ -200,7 +200,7 @@ void StorageLifeCyclesTest::testLifecycle(void)
QCOMPARE(addedAccount->timeStep(), 42U);
QCOMPARE(addedAccount->tokenLength(), 8);
QCOMPARE(addedAccount->epoch(), QDateTime::fromMSecsSinceEpoch(0));
QCOMPARE(addedAccount->hash(), accounts::Account::Default);
QCOMPARE(addedAccount->hash(), accounts::Account::Sha1);
QFile afterAddingLockFile(test::path(testIniLockFile));
QVERIFY2(!afterAddingLockFile.exists(), "after adding: lock file should not be present anymore");

View File

@ -36,7 +36,7 @@ void HotpAlgorithmTest::rfcTestVector(void)
QFETCH(quint64, counter);
QFETCH(bool, checksum);
std::optional<oath::Algorithm> uut = oath::Algorithm::usingDynamicTruncation(QCryptographicHash::Sha1, tokenLength, checksum);
std::optional<oath::Algorithm> uut = oath::Algorithm::hotp(std::nullopt, tokenLength, checksum);
QVERIFY2(uut, "should be able to construct the algorithm using settings of the RFC test vector");
QByteArray copy(secret);

View File

@ -57,7 +57,7 @@ void TotpAlgorithmTest::rfcTestVector(void)
QVERIFY2(counter, "should be able to count timesteps using the settings of the RFC test vector");
std::optional<oath::Algorithm> uut = oath::Algorithm::usingDynamicTruncation((QCryptographicHash::Algorithm) hash, tokenLength);
std::optional<oath::Algorithm> uut = oath::Algorithm::totp((QCryptographicHash::Algorithm) hash, tokenLength);
QVERIFY2(uut, "should be able to construct the algorithm using settings of the RFC test vector");
QByteArray copy(secret);

View File

@ -14,7 +14,8 @@ KEYSMITH_LOGGER(logger, ".accounts.account")
namespace accounts
{
Account::Account(AccountPrivate *d, QObject *parent) : QObject(parent), m_dptr(d)
Account::Account(AccountPrivate *d, QObject *parent) :
QObject(parent), m_dptr(d)
{
}
@ -54,7 +55,7 @@ namespace accounts
return d->timeStep();
}
int Account::offset(void) const
std::optional<uint> Account::offset(void) const
{
Q_D(const Account);
return d->offset();
@ -109,8 +110,13 @@ namespace accounts
d->remove();
}
AccountStorage::AccountStorage(const SettingsProvider &settings, QThread *worker, AccountSecret *secret, QObject *parent) :
QObject(parent), m_dptr(new AccountStoragePrivate(settings, secret ? secret : new AccountSecret(secrets::defaultSecureRandom, this), this, new Dispatcher(worker, this)))
AccountStorage::AccountStorage(const SettingsProvider &settings, QThread *worker, AccountSecret *secret,
QObject *parent) :
QObject(parent),
m_dptr(new AccountStoragePrivate(settings,
secret ? secret : new AccountSecret(secrets::defaultSecureRandom, this),
this,
new Dispatcher(worker, this)))
{
QTimer::singleShot(0, this, &AccountStorage::unlock);
}
@ -191,7 +197,8 @@ namespace accounts
return isAccountStillAvailable(AccountPrivate::toFullName(name, issuer));
}
void AccountStorage::addHotp(const QString &name, const QString &issuer, const QString &secret, quint64 counter, int tokenLength, int offset, bool addChecksum)
void AccountStorage::addHotp(const QString &name, const QString &issuer, const QString &secret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool addChecksum)
{
Q_D(AccountStorage);
const std::function<void(SaveHotp*)> handler([this](SaveHotp *job) -> void
@ -199,12 +206,13 @@ namespace accounts
QObject::connect(job, &SaveHotp::saved, this, &AccountStorage::handleHotp);
QObject::connect(job, &SaveHotp::invalid, this, &AccountStorage::handleError);
});
if (!d->addHotp(handler, name, issuer.isEmpty() ? QString() : issuer, secret, counter, tokenLength, offset, addChecksum)) {
if (!d->addHotp(handler, name, issuer.isEmpty() ? QString() : issuer, secret, tokenLength, counter, offset, addChecksum)) {
Q_EMIT error();
}
}
void AccountStorage::addTotp(const QString &name, const QString &issuer, const QString &secret, int timeStep, int tokenLength, const QDateTime &epoch, Account::Hash hash)
void AccountStorage::addTotp(const QString &name, const QString &issuer, const QString &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, Account::Hash hash)
{
Q_D(AccountStorage);
const std::function<void(SaveTotp*)> handler([this](SaveTotp *job) -> void
@ -212,7 +220,7 @@ namespace accounts
QObject::connect(job, &SaveTotp::saved, this, &AccountStorage::handleTotp);
QObject::connect(job, &SaveTotp::invalid, this, &AccountStorage::handleError);
});
if (!d->addTotp(handler, name, issuer.isEmpty() ? QString() : issuer, secret, timeStep, tokenLength, epoch, hash)) {
if (!d->addTotp(handler, name, issuer.isEmpty() ? QString() : issuer, secret, tokenLength, timeStep, epoch, hash)) {
Q_EMIT error();
}
}
@ -236,7 +244,9 @@ namespace accounts
return d->activeAccounts();
}
void AccountStorage::handleHotp(const QUuid id, const QString name, const QString issuer, const QByteArray secret, const QByteArray nonce, quint64 counter, int tokenLength)
void AccountStorage::handleHotp(const QUuid id, const QString name, const QString issuer,
const QByteArray secret, const QByteArray nonce, uint tokenLength,
quint64 counter, bool fixedTruncation, uint offset, bool checksum)
{
Q_D(AccountStorage);
if (!d->isStillOpen()) {
@ -261,13 +271,17 @@ namespace accounts
return;
}
Account *accepted = d->acceptHotpAccount(id, name, issuer, *encryptedSecret, counter, tokenLength);
const std::optional<uint> offsetValue = fixedTruncation ? std::optional<uint>((uint) offset) : std::nullopt;
Account *accepted = d->acceptHotpAccount(id, name, issuer,
*encryptedSecret, tokenLength, counter, offsetValue, checksum);
QObject::connect(accepted, &Account::removed, this, &AccountStorage::accountRemoved);
Q_EMIT added(AccountPrivate::toFullName(name, issuer));
}
void AccountStorage::handleTotp(const QUuid id, const QString name, const QString issuer, const QByteArray secret, const QByteArray nonce, uint timeStep, int tokenLength)
void AccountStorage::handleTotp(const QUuid id, const QString name, const QString issuer,
const QByteArray secret, const QByteArray nonce, uint tokenLength,
uint timeStep, const QDateTime epoch, Account::Hash hash)
{
Q_D(AccountStorage);
if (!d->isStillOpen()) {
@ -292,7 +306,8 @@ namespace accounts
return;
}
Account *accepted = d->acceptTotpAccount(id, name, issuer, *encryptedSecret, timeStep, tokenLength);
Account *accepted = d->acceptTotpAccount(id, name, issuer,
*encryptedSecret, tokenLength, timeStep, epoch, hash);
QObject::connect(accepted, &Account::removed, this, &AccountStorage::accountRemoved);
Q_EMIT added(AccountPrivate::toFullName(name, issuer));
@ -304,8 +319,8 @@ namespace accounts
d->dispose([this](Null *job) -> void
{
/*
* Use destroyed() instead of finished() to guarantee the Null job has been disposed of before e.g. threads are cleaned up.
* (If the QThread is disposed of before the Null job is cleaned up, the job would leak.)
* Use destroyed() instead of finished() to guarantee the Null job has been disposed of before e.g. threads
* are cleaned up. If the QThread is disposed of before the Null job is cleaned up, the job would leak.
*/
QObject::connect(job, &Null::destroyed, this, &AccountStorage::handleDisposal);
});

View File

@ -15,6 +15,7 @@
#include <QVector>
#include <functional>
#include <optional>
#include "keys.h"
@ -34,7 +35,7 @@ namespace accounts
};
Q_ENUM(Algorithm)
enum Hash {
Default, Sha256, Sha512
Sha1, Sha256, Sha512
};
Q_ENUM(Hash)
explicit Account(AccountPrivate *d, QObject *parent = nullptr);
@ -44,7 +45,7 @@ namespace accounts
quint64 counter(void) const;
QDateTime epoch(void) const;
uint timeStep(void) const;
int offset(void) const;
std::optional<uint> offset(void) const;
int tokenLength(void) const;
bool checksum(void) const;
Hash hash(void) const;
@ -67,8 +68,10 @@ namespace accounts
{
Q_OBJECT
public:
static AccountStorage * open(const SettingsProvider &settings, AccountSecret *secret = nullptr, QObject *parent = nullptr);
explicit AccountStorage(const SettingsProvider &settings, QThread *thread, AccountSecret *secret = nullptr, QObject *parent = nullptr);
static AccountStorage * open(const SettingsProvider &settings, AccountSecret *secret = nullptr,
QObject *parent = nullptr);
explicit AccountStorage(const SettingsProvider &settings, QThread *thread, AccountSecret *secret = nullptr,
QObject *parent = nullptr);
void removeAll(const QSet<Account*> &accounts) const;
bool isAccountStillAvailable(const QString &fullName) const;
bool isAccountStillAvailable(const QString &name, const QString &issuer) const;
@ -82,17 +85,17 @@ namespace accounts
void addHotp(const QString &name,
const QString &issuer,
const QString &secret,
uint tokenLength = 6U,
quint64 counter = 0ULL,
int tokenLength = 6,
int offset = -1,
const std::optional<uint> &offset = std::nullopt,
bool addChecksum = false);
void addTotp(const QString &name,
const QString &issuer,
const QString &secret,
int timeStep = 30,
int tokenLength = 6,
uint tokenLength = 6U,
uint timeStep = 30U,
const QDateTime &epoch = QDateTime::fromMSecsSinceEpoch(0),
Account::Hash hash = Account::Hash::Default);
Account::Hash hash = Account::Hash::Sha1);
void clearError(void);
bool hasError(void) const;
bool isLoaded(void) const;
@ -109,8 +112,12 @@ namespace accounts
void handleDisposal(void);
void handleError(void);
void handleLoaded(void);
void handleHotp(const QUuid id, const QString name, const QString issuer, const QByteArray secret, const QByteArray nonce, quint64 counter, int tokenLength);
void handleTotp(const QUuid id, const QString name, const QString issuer, const QByteArray secret, const QByteArray nonce, uint timeStep, int tokenLength);
void handleHotp(const QUuid id, const QString name, const QString issuer,
const QByteArray secret, const QByteArray nonce, uint tokenLength,
quint64 counter, bool fixedTruncation, uint offset, bool checksum);
void handleTotp(const QUuid id, const QString name, const QString issuer,
const QByteArray secret, const QByteArray nonce, uint tokenLength,
uint timeStep, const QDateTime epoch, Account::Hash hash);
private:
QScopedPointer<AccountStoragePrivate> m_dptr;
Q_DECLARE_PRIVATE_D(m_dptr, AccountStorage)

View File

@ -19,7 +19,7 @@ namespace accounts
return m_id;
}
int AccountPrivate::offset(void) const
std::optional<uint> AccountPrivate::offset(void) const
{
return m_offset;
}
@ -29,7 +29,6 @@ namespace accounts
return m_name;
}
QString AccountPrivate::issuer(void) const
{
return m_issuer;
@ -100,7 +99,9 @@ namespace accounts
}
qCDebug(logger) << "Requesting to store updated details for account:" << m_id;
SaveHotp *job = new SaveHotp(m_storage->settings(),m_id, m_name, m_issuer, m_secret, counter, m_tokenLength);
SaveHotp *job = new SaveHotp(m_storage->settings(),
m_id, m_name, m_issuer, m_secret, m_tokenLength,
counter, m_offset, m_checksum);
m_actions->queueAndProceed(job, [counter, job, q, this](void) -> void
{
new HandleCounterUpdate(this, m_storage, counter, job, q);
@ -180,13 +181,13 @@ namespace accounts
switch (m_algorithm) {
case Account::Algorithm::Hotp:
hotpJob = new ComputeHotp(m_storage->secret(), m_secret, m_counter, m_tokenLength, m_offset, m_checksum);
hotpJob = new ComputeHotp(m_storage->secret(), m_secret, m_tokenLength, m_counter, m_offset, m_checksum);
m_actions->queueAndProceed(hotpJob, [hotpJob, q, this](void) -> void {
new HandleTokenUpdate(this, hotpJob, q);
});
break;
case Account::Algorithm::Totp:
totpJob = new ComputeTotp(m_storage->secret(), m_secret, m_epoch, m_timeStep, m_tokenLength, m_hash);
totpJob = new ComputeTotp(m_storage->secret(), m_secret, m_tokenLength, m_epoch, m_timeStep, m_hash);
m_actions->queueAndProceed(totpJob, [totpJob, q, this](void) -> void
{
new HandleTokenUpdate(this, totpJob, q);
@ -229,20 +230,30 @@ namespace accounts
return m_is_still_alive;
}
AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account, AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const QString &issuer, const secrets::EncryptedSecret &secret, quint64 counter, int tokenLength, int offset, bool addChecksum) :
q_ptr(account(this)), m_storage(storage), m_actions(dispatcher), m_is_still_alive(true), m_algorithm(Account::Algorithm::Hotp), m_id(id), m_token(QString()),
m_name(name), m_issuer(issuer), m_secret(secret), m_tokenLength(tokenLength),
AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account,
AccountStoragePrivate *storage, Dispatcher *dispatcher,
const QUuid &id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool addChecksum) :
q_ptr(account(this)), m_storage(storage), m_actions(dispatcher), m_is_still_alive(true),
m_algorithm(Account::Algorithm::Hotp), m_id(id), m_token(QString()), m_name(name), m_issuer(issuer),
m_secret(secret), m_tokenLength(tokenLength),
m_counter(counter), m_offset(offset), m_checksum(addChecksum),
m_epoch(QDateTime::fromMSecsSinceEpoch(0)), m_timeStep(30), m_hash(Account::Hash::Default) // not a totp token so these values don't really matter
// not a totp token so these values don't really matter
m_epoch(QDateTime::fromMSecsSinceEpoch(0)), m_timeStep(30), m_hash(Account::Hash::Sha1)
{
}
AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account, AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const QString &issuer, const secrets::EncryptedSecret &secret, const QDateTime &epoch, uint timeStep, int tokenLength, Account::Hash hash) :
q_ptr(account(this)), m_storage(storage), m_actions(dispatcher), m_is_still_alive(true), m_algorithm(Account::Algorithm::Totp), m_id(id), m_token(QString()),
m_name(name), m_issuer(issuer), m_secret(secret), m_tokenLength(tokenLength),
m_counter(0), m_offset(-1), m_checksum(false), // not a hotp token so these values don't really matter
AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account,
AccountStoragePrivate *storage, Dispatcher *dispatcher,
const QUuid &id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
const QDateTime &epoch, uint timeStep, Account::Hash hash) :
q_ptr(account(this)), m_storage(storage), m_actions(dispatcher), m_is_still_alive(true),
m_algorithm(Account::Algorithm::Totp), m_id(id), m_token(QString()), m_name(name), m_issuer(issuer),
m_secret(secret), m_tokenLength(tokenLength),
// not a hotp token so these values don't really matter
m_counter(0), m_offset(std::nullopt), m_checksum(false),
m_epoch(epoch), m_timeStep(timeStep), m_hash(hash)
{
}
@ -459,21 +470,25 @@ namespace accounts
return m_secret->encrypt(decoded.data());
}
bool AccountStoragePrivate::validateGenericNewToken(const QString &name, const QString &issuer, const QString &secret, int tokenLength) const
bool AccountStoragePrivate::validateGenericNewToken(const QString &name, const QString &issuer,
const QString &secret, uint tokenLength) const
{
return checkTokenLength(tokenLength) && checkName(name) && checkIssuer(issuer) && isAccountStillAvailable(AccountPrivate::toFullName(name, issuer)) && checkSecret(secret);
return checkTokenLength(tokenLength) && checkName(name) && checkIssuer(issuer)
&& isAccountStillAvailable(AccountPrivate::toFullName(name, issuer)) && checkSecret(secret);
}
bool AccountStoragePrivate::addHotp(const std::function<void(SaveHotp*)> &handler, const QString &name, const QString &issuer, const QString &secret, quint64 counter, int tokenLength, int offset, bool addChecksum)
bool AccountStoragePrivate::addHotp(const std::function<void(SaveHotp*)> &handler,
const QString &name, const QString &issuer,
const QString &secret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool checksum)
{
Q_UNUSED(offset);
Q_UNUSED(addChecksum);
if (!m_is_still_open) {
qCDebug(logger) << "Will not add new HOTP account: storage no longer open";
return false;
}
if (!validateGenericNewToken(name, issuer, secret, tokenLength)) {
if (!validateGenericNewToken(name, issuer, secret, tokenLength) ||
!checkOffset(offset, QCryptographicHash::Sha1)) {
qCDebug(logger) << "Will not add new HOTP account: invalid account details";
return false;
}
@ -488,7 +503,8 @@ namespace accounts
qCDebug(logger) << "Requesting to store details for new HOTP account:" << id;
m_ids.insert(id);
SaveHotp *job = new SaveHotp(m_settings, id, name, issuer, *encryptedSecret, counter, tokenLength);
SaveHotp *job = new SaveHotp(m_settings, id, name, issuer, *encryptedSecret, tokenLength,
counter, offset, checksum);
m_actions->queueAndProceed(job, [job, &handler](void) -> void
{
handler(job);
@ -496,16 +512,18 @@ namespace accounts
return true;
}
bool AccountStoragePrivate::addTotp(const std::function<void(SaveTotp*)> &handler, const QString &name, const QString &issuer, const QString &secret, uint timeStep, int tokenLength, const QDateTime &epoch, Account::Hash hash)
bool AccountStoragePrivate::addTotp(const std::function<void(SaveTotp*)> &handler,
const QString &name, const QString &issuer,
const QString &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, Account::Hash hash)
{
Q_UNUSED(epoch);
Q_UNUSED(hash);
if (!m_is_still_open) {
qCDebug(logger) << "Will not add new TOTP account: storage no longer open";
return false;
}
if (!validateGenericNewToken(name, issuer, secret, tokenLength) || !checkTimeStep(timeStep)) {
if (!validateGenericNewToken(name, issuer, secret, tokenLength) ||
!checkTimeStep(timeStep) || !checkEpoch(epoch)) {
qCDebug(logger) << "Will not add new TOTP account: invalid account details";
return false;
}
@ -520,7 +538,8 @@ namespace accounts
qCDebug(logger) << "Requesting to store details for new TOTP account:" << id;
m_ids.insert(id);
SaveTotp *job = new SaveTotp(m_settings, id, name, issuer, *encryptedSecret, timeStep, tokenLength);
SaveTotp *job = new SaveTotp(m_settings, id, name, issuer, *encryptedSecret, tokenLength,
timeStep, epoch, hash);
m_actions->queueAndProceed(job, [job, &handler](void) -> void
{
handler(job);
@ -557,7 +576,9 @@ namespace accounts
});
}
Account * AccountStoragePrivate::acceptHotpAccount(const QUuid &id, const QString &name, const QString &issuer, const secrets::EncryptedSecret &secret, quint64 counter, int tokenLength, int offset, bool addChecksum)
Account * AccountStoragePrivate::acceptHotpAccount(const QUuid &id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool checksum)
{
Q_Q(AccountStorage);
qCDebug(logger) << "Registering HOTP account:" << id;
@ -569,13 +590,17 @@ namespace accounts
});
m_ids.insert(id);
m_names.insert(AccountPrivate::toFullName(name, issuer), id);
m_accountsPrivate.insert(id, new AccountPrivate(registration, this, m_actions, id, name, issuer, secret, counter, tokenLength, offset, addChecksum));
const auto p = new AccountPrivate(registration, this, m_actions,
id, name, issuer, secret, tokenLength, counter, offset, checksum);
m_accountsPrivate.insert(id, p);
Q_ASSERT_X(m_accounts.contains(id), Q_FUNC_INFO, "account should have been registered");
return m_accounts[id];
}
Account * AccountStoragePrivate::acceptTotpAccount(const QUuid &id, const QString &name, const QString &issuer, const secrets::EncryptedSecret &secret, uint timeStep, int tokenLength, const QDateTime &epoch, Account::Hash hash)
Account * AccountStoragePrivate::acceptTotpAccount(const QUuid &id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, Account::Hash hash)
{
Q_Q(AccountStorage);
qCDebug(logger) << "Registering TOTP account:" << id;
@ -587,7 +612,9 @@ namespace accounts
});
m_ids.insert(id);
m_names.insert(AccountPrivate::toFullName(name, issuer), id);
m_accountsPrivate.insert(id, new AccountPrivate(registration, this, m_actions, id, name, issuer, secret, epoch, timeStep, tokenLength, hash));
const auto p = new AccountPrivate(registration, this, m_actions,
id, name, issuer, secret, tokenLength, epoch, timeStep, hash);
m_accountsPrivate.insert(id, p);
Q_ASSERT_X(m_accounts.contains(id), Q_FUNC_INFO, "account should have been registered");
return m_accounts[id];
@ -622,12 +649,15 @@ namespace accounts
return m_has_error;
}
AccountStoragePrivate::AccountStoragePrivate(const SettingsProvider &settings, AccountSecret *secret, AccountStorage *storage, Dispatcher *dispatcher) :
q_ptr(storage), m_is_loaded(false), m_has_error(false), m_is_still_open(true), m_actions(dispatcher), m_settings(settings), m_secret(secret)
AccountStoragePrivate::AccountStoragePrivate(const SettingsProvider &settings,
AccountSecret *secret, AccountStorage *storage, Dispatcher *dispatcher) :
q_ptr(storage), m_is_loaded(false), m_has_error(false), m_is_still_open(true),
m_actions(dispatcher), m_settings(settings), m_secret(secret)
{
}
HandleCounterUpdate::HandleCounterUpdate(AccountPrivate *account, AccountStoragePrivate *storage, quint64 counter, SaveHotp *job, QObject *parent) :
HandleCounterUpdate::HandleCounterUpdate(AccountPrivate *account, AccountStoragePrivate *storage,
quint64 counter, SaveHotp *job, QObject *parent) :
QObject(parent), m_accept_on_finish(true), m_counter(counter), m_account(account), m_storage(storage)
{
QObject::connect(job, &SaveHotp::invalid, this, &HandleCounterUpdate::rejected);

View File

@ -18,6 +18,7 @@
#include <QVector>
#include <functional>
#include <optional>
namespace accounts
{
@ -26,7 +27,7 @@ namespace accounts
{
public:
QUuid id(void) const;
int offset(void) const;
std::optional<uint> offset(void) const;
QString name(void) const;
QString token(void) const;
QString issuer(void) const;
@ -40,13 +41,15 @@ namespace accounts
public:
static QString toFullName(const QString &name, const QString &issuer);
explicit AccountPrivate(const std::function<Account*(AccountPrivate*)> &account,
AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const QString &issuer, const secrets::EncryptedSecret &secret,
quint64 counter, int tokenLength, int offset, bool addChecksum);
AccountStoragePrivate *storage, Dispatcher *dispatcher,
const QUuid &id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool addChecksum);
explicit AccountPrivate(const std::function<Account*(AccountPrivate*)> &account,
AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const QString &issuer, const secrets::EncryptedSecret &secret,
const QDateTime &epoch, uint timeStep, int tokenLength, Account::Hash hash);
AccountStoragePrivate *storage, Dispatcher *dispatcher,
const QUuid &id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
const QDateTime &epoch, uint timeStep, Account::Hash hash);
void recompute(void);
void setCounter(quint64 counter);
void remove(void);
@ -69,9 +72,9 @@ namespace accounts
const QString m_name;
const QString m_issuer;
const secrets::EncryptedSecret m_secret;
const int m_tokenLength;
const uint m_tokenLength;
quint64 m_counter;
const int m_offset;
const std::optional<uint> m_offset;
const bool m_checksum;
const QDateTime m_epoch;
const qint64 m_timeStep;
@ -81,7 +84,8 @@ namespace accounts
class AccountStoragePrivate
{
public:
explicit AccountStoragePrivate(const SettingsProvider &settings, AccountSecret *secret, AccountStorage *storage, Dispatcher *dispatcher);
explicit AccountStoragePrivate(const SettingsProvider &settings,
AccountSecret *secret, AccountStorage *storage, Dispatcher *dispatcher);
void dispose(const std::function<void(Null*)> &handler);
void acceptDisposal(void);
void unlock(const std::function<void(RequestAccountPassword*)> &handler);
@ -95,45 +99,28 @@ namespace accounts
AccountSecret *secret(void) const;
void removeAccounts(const QSet<QString> &accountNames);
void acceptAccountRemoval(const QString &accountName);
Account * acceptHotpAccount(const QUuid &id,
const QString &name,
const QString &issuer,
const secrets::EncryptedSecret &secret,
quint64 counter = 0ULL,
int tokenLength = 6,
int offset = -1,
bool addChecksum = false);
Account * acceptTotpAccount(const QUuid &id,
const QString &name,
const QString &issuer,
const secrets::EncryptedSecret &secret,
uint timeStep = 30,
int tokenLength = 6,
const QDateTime &epoch = QDateTime::fromMSecsSinceEpoch(0),
Account::Hash hash = Account::Hash::Default);
Account * acceptHotpAccount(const QUuid &id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool checksum);
Account * acceptTotpAccount(const QUuid &id, const QString &name, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, Account::Hash hash);
bool addHotp(const std::function<void(SaveHotp*)> &handler,
const QString &name,
const QString &issuer,
const QString &secret,
quint64 counter = 0ULL,
int tokenLength = 6,
int offset = -1,
bool addChecksum = false);
const QString &name, const QString &issuer,
const QString &secret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool checksum);
bool addTotp(const std::function<void(SaveTotp*)> &handler,
const QString &name,
const QString &issuer,
const QString &secret,
uint timeStep = 30,
int tokenLength = 6,
const QDateTime &epoch = QDateTime::fromMSecsSinceEpoch(0),
Account::Hash hash = Account::Hash::Default);
const QString &name, const QString &issuer,
const QString &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, Account::Hash hash);
void notifyLoaded(void);
bool isLoaded(void) const;
void notifyError(void);
void clearError(void);
bool hasError(void) const;
private:
bool validateGenericNewToken(const QString &name, const QString &issuer, const QString &secret, int tokenLength) const;
bool validateGenericNewToken(const QString &name, const QString &issuer,
const QString &secret, uint tokenLength) const;
std::optional<secrets::EncryptedSecret> encrypt(const QString &secret) const;
QUuid generateId(const QString &name) const;
private:
@ -158,7 +145,8 @@ namespace accounts
{
Q_OBJECT
public:
explicit HandleCounterUpdate(AccountPrivate *account, AccountStoragePrivate *storage, quint64 counter, SaveHotp *job, QObject *parent = nullptr);
explicit HandleCounterUpdate(AccountPrivate *account, AccountStoragePrivate *storage,
quint64 counter, SaveHotp *job, QObject *parent = nullptr);
private:
bool m_accept_on_finish;
const quint64 m_counter;

View File

@ -9,15 +9,22 @@
#include "../logging_p.h"
#include "../oath/oath.h"
#include <QMetaEnum>
#include <QScopedPointer>
#include <QTimer>
KEYSMITH_LOGGER(logger, ".accounts.actions")
KEYSMITH_LOGGER(dispatcherLogger, ".accounts.dispatcher")
static const QMetaEnum hashEnum = QMetaEnum::fromType<accounts::Account::Hash>();
static const QVariant hashDefault = QVariant::fromValue<accounts::Account::Hash>(accounts::Account::Sha1);
static const QString trueVariantValue(QLatin1String("true"));
static const QString falseVariantValue(QLatin1String("false"));
namespace accounts
{
AccountJob::AccountJob() : QObject()
AccountJob::AccountJob() :
QObject()
{
}
@ -25,7 +32,8 @@ namespace accounts
{
}
Null::Null() : AccountJob()
Null::Null() :
AccountJob()
{
}
@ -39,31 +47,45 @@ namespace accounts
Q_ASSERT_X(false, Q_FUNC_INFO, "should be overridden in derived classes!");
}
RequestAccountPassword::RequestAccountPassword(const SettingsProvider &settings, AccountSecret *secret) : AccountJob(), m_settings(settings), m_secret(secret), m_failed(false), m_succeeded(false)
RequestAccountPassword::RequestAccountPassword(const SettingsProvider &settings, AccountSecret *secret) :
AccountJob(), m_settings(settings), m_secret(secret), m_failed(false), m_succeeded(false)
{
}
LoadAccounts::LoadAccounts(const SettingsProvider &settings, const AccountSecret *secret) : AccountJob(), m_settings(settings), m_secret(secret)
LoadAccounts::LoadAccounts(const SettingsProvider &settings, const AccountSecret *secret,
const std::function<qint64(void)> &clock) :
AccountJob(), m_settings(settings), m_secret(secret), m_clock(clock)
{
}
DeleteAccounts::DeleteAccounts(const SettingsProvider &settings, const QSet<QUuid> &ids) : AccountJob(), m_settings(settings), m_ids(ids)
DeleteAccounts::DeleteAccounts(const SettingsProvider &settings, const QSet<QUuid> &ids) :
AccountJob(), m_settings(settings), m_ids(ids)
{
}
SaveHotp::SaveHotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const QString &issuer, const secrets::EncryptedSecret &secret, quint64 counter, int tokenLength) :
AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), m_issuer(issuer), m_secret(secret), m_counter(counter), m_tokenLength(tokenLength)
SaveHotp::SaveHotp(const SettingsProvider &settings,
const QUuid &id, const QString &accountName, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool checksum) :
AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), m_issuer(issuer),
m_secret(secret), m_tokenLength(tokenLength), m_counter(counter), m_offset(offset), m_checksum(checksum)
{
}
SaveTotp::SaveTotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const QString &issuer, const secrets::EncryptedSecret &secret, uint timeStep, int tokenLength) :
AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), m_issuer(issuer), m_secret(secret), m_timeStep(timeStep), m_tokenLength(tokenLength)
SaveTotp::SaveTotp(const SettingsProvider &settings,
const QUuid &id, const QString &accountName, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, Account::Hash hash,
const std::function<qint64(void)> &clock) :
AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), m_issuer(issuer),
m_secret(secret), m_tokenLength(tokenLength), m_timeStep(timeStep), m_epoch(epoch), m_hash(hash), m_clock(clock)
{
}
void SaveHotp::run(void)
{
if (!checkId(m_id) || !checkName(m_accountName) || !checkIssuer(m_issuer) || !checkTokenLength(m_tokenLength)) {
if (!checkId(m_id) || !checkName(m_accountName) || !checkIssuer(m_issuer) ||
!checkTokenLength(m_tokenLength) || !checkOffset(m_offset, QCryptographicHash::Sha1)) {
qCDebug(logger)
<< "Unable to save HOTP account:" << m_id
<< "Invalid account details";
@ -98,12 +120,17 @@ namespace accounts
settings.setValue("nonce", encodedNonce);
settings.setValue("counter", m_counter);
settings.setValue("pinLength", m_tokenLength);
if (m_offset) {
settings.setValue("offset", *m_offset);
}
settings.setValue("checksum", m_checksum);
settings.endGroup();
// Try to guarantee that data will have been written before claiming the account was actually saved
settings.sync();
Q_EMIT saved(m_id, m_accountName, m_issuer, m_secret.cryptText(), m_secret.nonce(), m_counter, m_tokenLength);
Q_EMIT saved(m_id, m_accountName, m_issuer, m_secret.cryptText(), m_secret.nonce(), m_tokenLength,
m_counter, m_offset.has_value(), m_offset ? *m_offset : 0U, m_checksum);
});
m_settings(act);
@ -112,7 +139,8 @@ namespace accounts
void SaveTotp::run(void)
{
if (!checkId(m_id) || !checkName(m_accountName) || !checkIssuer(m_issuer) || !checkTokenLength(m_tokenLength) || !checkTimeStep(m_timeStep)) {
if (!checkId(m_id) || !checkName(m_accountName) || !checkIssuer(m_issuer) ||
!checkTokenLength(m_tokenLength) || !checkTimeStep(m_timeStep) || !checkEpoch(m_epoch, m_clock)) {
qCDebug(logger)
<< "Unable to save TOTP account:" << m_id
<< "Invalid account details";
@ -147,12 +175,15 @@ namespace accounts
settings.setValue("nonce", encodedNonce);
settings.setValue("timeStep", m_timeStep);
settings.setValue("pinLength", m_tokenLength);
settings.setValue("epoch", m_epoch.toUTC().toString(Qt::ISODateWithMs));
settings.setValue("hash", QVariant::fromValue<Account::Hash>(m_hash).toString());
settings.endGroup();
// Try to guarantee that data will have been written before claiming the account was actually saved
settings.sync();
Q_EMIT saved(m_id, m_accountName, m_issuer, m_secret.cryptText(), m_secret.nonce(), m_timeStep, m_tokenLength);
Q_EMIT saved(m_id, m_accountName, m_issuer, m_secret.cryptText(), m_secret.nonce(), m_tokenLength,
m_timeStep, m_epoch, m_hash);
});
m_settings(act);
@ -304,7 +335,7 @@ namespace accounts
}
settings.endGroup();
std::optional<secrets::KeyDerivationParameters> params = secrets::KeyDerivationParameters::create(keyLength, algorithm, memoryCost, cpuCost);
const auto params = secrets::KeyDerivationParameters::create(keyLength, algorithm, memoryCost, cpuCost);
if (!ok || !params || !secrets::SecureMasterKey::validate(*params)) {
qCDebug(logger) << "Unable to request 'existing' password: invalid salt or key derivation parameters";
return;
@ -393,7 +424,7 @@ namespace accounts
const QByteArray nonce = QByteArray::fromBase64(encodedNonce, QByteArray::Base64Encoding);
const QByteArray secret = QByteArray::fromBase64(encodedSecret, QByteArray::Base64Encoding);
std::optional<secrets::EncryptedSecret> encryptedSecret = secrets::EncryptedSecret::from(secret, nonce);
const auto encryptedSecret = secrets::EncryptedSecret::from(secret, nonce);
if (!encryptedSecret) {
qCWarning(logger)
<< "Skipping invalid account:" << id
@ -425,8 +456,31 @@ namespace accounts
continue;
}
const QDateTime epoch = settings.value("epoch", QDateTime::fromMSecsSinceEpoch(0)).toDateTime();
if (!checkEpoch(epoch, m_clock)) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid epoch";
settings.endGroup();
failed = true;
continue;
}
ok = false;
const QByteArray hashName = settings.value("hash", hashDefault).toByteArray();
int hash = hashEnum.keyToValue(hashName.constData(), &ok);
if (!ok) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid hash";
settings.endGroup();
failed = true;
continue;
}
qCInfo(logger) << "Found valid TOTP account:" << id;
Q_EMIT foundTotp(id, accountName, issuer, secret, nonce, timeStep, tokenLength);
Q_EMIT foundTotp(id, accountName, issuer, secret, nonce, tokenLength,
timeStep, epoch, (Account::Hash) hash);
}
if (type == QLatin1String("hotp")) {
@ -441,8 +495,32 @@ namespace accounts
continue;
}
const QVariant offsetVariant = settings.value("offset");
ok = offsetVariant.isNull();
std::optional<uint> offset = ok ? std::nullopt : std::optional<uint>(offsetVariant.toUInt(&ok));
if (!ok || !checkOffset(offset, QCryptographicHash::Sha1)) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid offset";
settings.endGroup();
failed = true;
continue;
}
const QString checksum = settings.value("checksum", falseVariantValue).toString();
if (checksum != trueVariantValue && checksum != falseVariantValue) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid checksum";
settings.endGroup();
failed = true;
continue;
}
qCInfo(logger) << "Found valid HOTP account:" << id;
Q_EMIT foundHotp(id, accountName, issuer, secret, nonce, counter, tokenLength);
Q_EMIT foundHotp(id, accountName, issuer, secret, nonce, tokenLength,
counter, offset.has_value(), offset ? *offset : 0U, checksum == trueVariantValue);
}
settings.endGroup();
@ -456,8 +534,12 @@ namespace accounts
Q_EMIT finished();
}
ComputeTotp::ComputeTotp(const AccountSecret *secret, const secrets::EncryptedSecret &tokenSecret, const QDateTime &epoch, uint timeStep, int tokenLength, const Account::Hash &hash, const std::function<qint64(void)> &clock) :
AccountJob(), m_secret(secret), m_tokenSecret(tokenSecret), m_epoch(epoch), m_timeStep(timeStep), m_tokenLength(tokenLength), m_hash(hash), m_clock(clock)
ComputeTotp::ComputeTotp(const AccountSecret *secret,
const secrets::EncryptedSecret &tokenSecret, uint tokenLength,
const QDateTime &epoch, uint timeStep, const Account::Hash &hash,
const std::function<qint64(void)> &clock) :
AccountJob(), m_secret(secret), m_tokenSecret(tokenSecret), m_tokenLength(tokenLength),
m_epoch(epoch), m_timeStep(timeStep), m_hash(hash), m_clock(clock)
{
}
@ -470,13 +552,19 @@ namespace accounts
}
if (!checkTokenLength(m_tokenLength)) {
qCDebug(logger) << "Unable to compute THOTP token: invalid token length:" << m_tokenLength;
qCDebug(logger) << "Unable to compute TOTP token: invalid token length:" << m_tokenLength;
Q_EMIT finished();
return;
}
if (!checkTimeStep(m_timeStep)) {
qCDebug(logger) << "Unable to compute THOTP token: invalid time step:" << m_timeStep;
qCDebug(logger) << "Unable to compute TOTP token: invalid time step:" << m_timeStep;
Q_EMIT finished();
return;
}
if (!checkEpoch(m_epoch, m_clock)) {
qCDebug(logger) << "Unable to compute TOTP token: invalid epoch:" << m_epoch;
Q_EMIT finished();
return;
}
@ -484,15 +572,15 @@ namespace accounts
QCryptographicHash::Algorithm hash;
switch(m_hash)
{
case Account::Hash::Sha1:
hash = QCryptographicHash::Sha1;
break;
case Account::Hash::Sha256:
hash = QCryptographicHash::Sha256;
break;
case Account::Hash::Sha512:
hash = QCryptographicHash::Sha512;
break;
case Account::Hash::Default:
hash = QCryptographicHash::Sha1;
break;
default:
qCDebug(logger) << "Unable to compute TOTP token: unknown hashing algorithm:" << m_hash;
Q_EMIT finished();
@ -500,7 +588,7 @@ namespace accounts
}
const std::optional<oath::Algorithm> algorithm = oath::Algorithm::usingDynamicTruncation(hash, m_tokenLength);
const std::optional<oath::Algorithm> algorithm = oath::Algorithm::totp(hash, m_tokenLength);
if (!algorithm) {
qCDebug(logger) << "Unable to compute TOTP token: failed to construct algorithm";
Q_EMIT finished();
@ -521,7 +609,7 @@ namespace accounts
return;
}
const std::optional<QString> token = algorithm->compute(*counter, reinterpret_cast<char*>(secret->data()), secret->size());
const auto token = algorithm->compute(*counter, reinterpret_cast<char*>(secret->data()), secret->size());
if (token) {
Q_EMIT otp(*token);
} else {
@ -531,8 +619,11 @@ namespace accounts
Q_EMIT finished();
}
ComputeHotp::ComputeHotp(const AccountSecret *secret, const secrets::EncryptedSecret &tokenSecret, quint64 counter, int tokenLength, int offset, bool checksum) :
AccountJob(), m_secret(secret), m_tokenSecret(tokenSecret), m_counter(counter), m_tokenLength(tokenLength), m_offset(offset), m_checksum(checksum)
ComputeHotp::ComputeHotp(const AccountSecret *secret,
const secrets::EncryptedSecret &tokenSecret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool checksum) :
AccountJob(), m_secret(secret), m_tokenSecret(tokenSecret), m_tokenLength(tokenLength),
m_counter(counter), m_offset(offset), m_checksum(checksum)
{
}
@ -550,10 +641,13 @@ namespace accounts
return;
}
const oath::Encoder encoder(m_tokenLength, m_checksum);
const std::optional<oath::Algorithm> algorithm = m_offset >=0
? oath::Algorithm::usingTruncationOffset(QCryptographicHash::Sha1, (uint) m_offset, encoder)
: oath::Algorithm::usingDynamicTruncation(QCryptographicHash::Sha1, encoder);
if (!checkOffset(m_offset, QCryptographicHash::Sha1)) {
qCDebug(logger) << "Unable to compute HOTP token: invalid offset:" << *m_offset;
Q_EMIT finished();
return;
}
const std::optional<oath::Algorithm> algorithm = oath::Algorithm::hotp(m_offset, m_tokenLength, m_checksum);
if (!algorithm) {
qCDebug(logger) << "Unable to compute HOTP token: failed to construct algorithm";
Q_EMIT finished();
@ -567,7 +661,7 @@ namespace accounts
return;
}
const std::optional<QString> token = algorithm->compute(m_counter, reinterpret_cast<char*>(secret->data()), secret->size());
const auto token = algorithm->compute(m_counter, reinterpret_cast<char*>(secret->data()), secret->size());
if (token) {
Q_EMIT otp(*token);
} else {
@ -577,7 +671,8 @@ namespace accounts
Q_EMIT finished();
}
Dispatcher::Dispatcher(QThread *thread, QObject *parent) : QObject(parent), m_thread(thread), m_current(nullptr)
Dispatcher::Dispatcher(QThread *thread, QObject *parent) :
QObject(parent), m_thread(thread), m_current(nullptr)
{
}

View File

@ -16,6 +16,7 @@
#include <QVector>
#include <functional>
#include <optional>
#include "../secrets/secrets.h"
#include "keys.h"
@ -34,7 +35,7 @@ namespace accounts
void finished(void);
};
class Null : public AccountJob
class Null: public AccountJob
{
Q_OBJECT
public:
@ -56,7 +57,7 @@ namespace accounts
void failed(void);
private:
const SettingsProvider m_settings;
AccountSecret * m_secret;
AccountSecret * const m_secret;
private:
bool m_failed;
bool m_succeeded;
@ -66,15 +67,21 @@ namespace accounts
{
Q_OBJECT
public:
explicit LoadAccounts(const SettingsProvider &settings, const AccountSecret *secret);
explicit LoadAccounts(const SettingsProvider &settings, const AccountSecret *secret,
const std::function<qint64(void)> &clock = &QDateTime::currentMSecsSinceEpoch);
void run(void) override;
Q_SIGNALS:
void foundHotp(const QUuid id, const QString name, const QString issuer, const QByteArray secret, const QByteArray nonce, quint64 counter, int tokenLength);
void foundTotp(const QUuid id, const QString name, const QString issuer, const QByteArray secret, const QByteArray nonce, uint timeStep, int tokenLength);
void foundHotp(const QUuid id, const QString name, const QString issuer,
const QByteArray secret, const QByteArray nonce, uint tokenLength,
quint64 counter, bool fixedTruncation, uint offset, bool checksum);
void foundTotp(const QUuid id, const QString name, const QString issuer,
const QByteArray secret, const QByteArray nonce, uint tokenLength,
uint timeStep, const QDateTime epoch, accounts::Account::Hash hash);
void failedToLoadAllAccounts(void);
private:
const SettingsProvider m_settings;
const AccountSecret * m_secret;
const AccountSecret * const m_secret;
const std::function<qint64(void)> m_clock;
};
class DeleteAccounts: public AccountJob
@ -94,54 +101,73 @@ namespace accounts
{
Q_OBJECT
public:
explicit SaveHotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const QString &issuer, const secrets::EncryptedSecret &secret, quint64 counter, int tokenLength);
explicit SaveHotp(const SettingsProvider &settings,
const QUuid &id, const QString &accountName, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool checksum);
void run(void) override;
Q_SIGNALS:
void invalid(void);
void saved(const QUuid id, const QString accountName, const QString issuer, const QByteArray secret, const QByteArray nonce, quint64 counter, int tokenLength);
void saved(const QUuid id, const QString accountName, const QString issuer,
const QByteArray secret, const QByteArray nonce, uint tokenLength,
quint64 counter, bool fixedTruncation, uint offset, bool checksum);
private:
const SettingsProvider m_settings;
const QUuid m_id;
const QString m_accountName;
const QString m_issuer;
const secrets::EncryptedSecret m_secret;
const uint m_tokenLength;
const quint64 m_counter;
const int m_tokenLength;
const std::optional<uint> m_offset;
const bool m_checksum;
};
class SaveTotp: public AccountJob
{
Q_OBJECT
public:
explicit SaveTotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const QString &issuer, const secrets::EncryptedSecret &secret, uint timeStep, int tokenLength);
explicit SaveTotp(const SettingsProvider &settings,
const QUuid &id, const QString &accountName, const QString &issuer,
const secrets::EncryptedSecret &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, Account::Hash hash,
const std::function<qint64(void)> &clock = &QDateTime::currentMSecsSinceEpoch);
void run(void) override;
Q_SIGNALS:
void invalid(void);
void saved(const QUuid id, const QString accountName, const QString issuer, const QByteArray secret, const QByteArray nonce, uint timeStep, int tokenLength);
void saved(const QUuid id, const QString accountName, const QString issuer,
const QByteArray secret, const QByteArray nonce, uint tokenLength,
uint timeStep, const QDateTime epoch, accounts::Account::Hash hash);
private:
const SettingsProvider m_settings;
const QUuid m_id;
const QString m_accountName;
const QString m_issuer;
const secrets::EncryptedSecret m_secret;
const uint m_tokenLength;
const uint m_timeStep;
const int m_tokenLength;
const QDateTime m_epoch;
const Account::Hash m_hash;
const std::function<qint64(void)> m_clock;
};
class ComputeTotp: public AccountJob
{
Q_OBJECT
public:
explicit ComputeTotp(const AccountSecret *secret, const secrets::EncryptedSecret &tokenSecret, const QDateTime &epoch, uint timeStep, int tokenLength, const Account::Hash &hash = Account::Hash::Default, const std::function<qint64(void)> &clock = &QDateTime::currentMSecsSinceEpoch);
explicit ComputeTotp(const AccountSecret *secret,
const secrets::EncryptedSecret &tokenSecret, uint tokenLength,
const QDateTime &epoch, uint timeStep, const Account::Hash &hash,
const std::function<qint64(void)> &clock = &QDateTime::currentMSecsSinceEpoch);
void run(void) override;
Q_SIGNALS:
void otp(const QString otp);
private:
const AccountSecret * m_secret;
const AccountSecret * const m_secret;
const secrets::EncryptedSecret m_tokenSecret;
const uint m_tokenLength;
const QDateTime m_epoch;
const uint m_timeStep;
const int m_tokenLength;
const Account::Hash m_hash;
const std::function<qint64(void)> m_clock;
};
@ -150,16 +176,18 @@ namespace accounts
{
Q_OBJECT
public:
explicit ComputeHotp(const AccountSecret *secret, const secrets::EncryptedSecret &tokenSecret, quint64 counter, int tokenLength, int offset = -1, bool checksum = false);
explicit ComputeHotp(const AccountSecret *secret,
const secrets::EncryptedSecret &tokenSecret, uint tokenLength,
quint64 counter, const std::optional<uint> &offset, bool checksum);
void run(void) override;
Q_SIGNALS:
void otp(const QString otp);
private:
const AccountSecret * m_secret;
const AccountSecret * const m_secret;
const secrets::EncryptedSecret m_tokenSecret;
const uint m_tokenLength;
const quint64 m_counter;
const int m_tokenLength;
const int m_offset;
const std::optional<uint> m_offset;
const bool m_checksum;
};

View File

@ -5,6 +5,7 @@
#include "validation.h"
#include "../base32/base32.h"
#include "../oath/oath.h"
namespace accounts
{
@ -29,13 +30,23 @@ namespace accounts
return issuer.isNull() || (!issuer.isEmpty() && !issuer.contains(QLatin1Char(':')));
}
bool checkTokenLength(int tokenLength)
bool checkTokenLength(uint tokenLength)
{
return tokenLength >= 6 && tokenLength <= 10;
return tokenLength >= 6U && tokenLength <= 10U;
}
bool checkTimeStep(uint timeStep)
{
return timeStep > 0;
return timeStep > 0U;
}
bool checkEpoch(const QDateTime &epoch, const std::function<qint64(void)> &clock)
{
return epoch.isValid() && epoch.toMSecsSinceEpoch() <= clock();
}
bool checkOffset(const std::optional<uint> &offset, QCryptographicHash::Algorithm algorithm)
{
return oath::Algorithm::validate(algorithm, offset);
}
}

View File

@ -5,17 +5,26 @@
#ifndef ACCOUNTS_VALIDATION_H
#define ACCOUNTS_VALIDATION_H
#include "account.h"
#include <QCryptographicHash>
#include <QDateTime>
#include <QString>
#include <QUuid>
#include <functional>
#include <optional>
namespace accounts
{
bool checkId(const QUuid &id);
bool checkSecret(const QString &secret);
bool checkName(const QString &name);
bool checkIssuer(const QString &issuer);
bool checkTokenLength(int tokenLength);
bool checkTokenLength(uint tokenLength);
bool checkTimeStep(uint timeStep);
bool checkEpoch(const QDateTime &epoch, const std::function<qint64(void)> &clock = &QDateTime::currentMSecsSinceEpoch);
bool checkOffset(const std::optional<uint> &offset, QCryptographicHash::Algorithm algorithm);
}
#endif

View File

@ -65,10 +65,11 @@ Kirigami.Page {
enabled: acceptable
onTriggered: {
if (tokenDetails.isTotp) {
accounts.addTotp(accountName.text, issuerName.text, tokenDetails.secret, parseInt(tokenDetails.timeStep), tokenDetails.tokenLength);
console.log("WTF: ", Models.AccountListModel.Sha1);
accounts.addTotp(accountName.text, issuerName.text, tokenDetails.secret, tokenDetails.tokenLength, parseInt(tokenDetails.timeStep), new Date(0), Models.AccountListModel.Sha1);
}
if (tokenDetails.isHotp) {
accounts.addHotp(accountName.text, issuerName.text, tokenDetails.secret, parseInt(tokenDetails.counter), tokenDetails.tokenLength);
accounts.addHotp(accountName.text, issuerName.text, tokenDetails.secret, tokenDetails.tokenLength, parseInt(tokenDetails.counter), false, 0, false);
}
root.dismissed();
}

View File

@ -33,7 +33,8 @@ namespace model
return diff < 0 ? - (diff % step) : step - (diff % step);
}
AccountView::AccountView(accounts::Account *model, QObject *parent) : QObject(parent), m_model(model)
AccountView::AccountView(accounts::Account *model, QObject *parent) :
QObject(parent), m_model(model)
{
QObject::connect(model, &accounts::Account::tokenChanged, this, &AccountView::tokenChanged);
QObject::connect(this, &AccountView::remove, model, &accounts::Account::remove);
@ -87,7 +88,8 @@ namespace model
return model::millisecondsLeftForToken(m_model->epoch(), m_model->timeStep());
}
SimpleAccountListModel::SimpleAccountListModel(accounts::AccountStorage *storage, QObject *parent) : QAbstractListModel(parent), m_storage(storage), m_has_error(false), m_index(QVector<QString>())
SimpleAccountListModel::SimpleAccountListModel(accounts::AccountStorage *storage, QObject *parent) :
QAbstractListModel(parent), m_storage(storage), m_has_error(false), m_index(QVector<QString>())
{
QObject::connect(storage, &accounts::AccountStorage::added, this, &SimpleAccountListModel::added);
QObject::connect(storage, &accounts::AccountStorage::removed, this, &SimpleAccountListModel::removed);
@ -136,14 +138,34 @@ namespace model
return m_storage->isLoaded();
}
void SimpleAccountListModel::addTotp(const QString &account, const QString &issuer, const QString &secret, uint timeStep, int tokenLength)
accounts::Account::Hash SimpleAccountListModel::toHash(TOTPAlgorithms value)
{
m_storage->addTotp(account, issuer, secret, timeStep, tokenLength);
switch (value) {
case TOTPAlgorithms::Sha1:
return accounts::Account::Hash::Sha1;
case TOTPAlgorithms::Sha256:
return accounts::Account::Hash::Sha256;
case TOTPAlgorithms::Sha512:
return accounts::Account::Hash::Sha512;
default:
Q_ASSERT_X(false, Q_FUNC_INFO, "Unknown/unsupported totp algorithm value");
return accounts::Account::Hash::Sha1;
}
}
void SimpleAccountListModel::addHotp(const QString &account, const QString &issuer, const QString &secret, quint64 counter, int tokenLength)
void SimpleAccountListModel::addTotp(const QString &account, const QString &issuer,
const QString &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, TOTPAlgorithms hash)
{
m_storage->addHotp(account, issuer, secret, counter, tokenLength);
m_storage->addTotp(account, issuer, secret, tokenLength, timeStep, epoch, toHash(hash));
}
void SimpleAccountListModel::addHotp(const QString &account, const QString &issuer,
const QString &secret, uint tokenLength,
quint64 counter, bool fixedTruncation, uint offset, bool checksum)
{
const auto o = fixedTruncation ? std::optional<uint>(offset) : std::nullopt;
m_storage->addHotp(account, issuer, secret, tokenLength, counter, o, checksum);
}
QHash<int, QByteArray> SimpleAccountListModel::roleNames(void) const
@ -162,7 +184,8 @@ namespace model
int accountIndex = account.row();
if (accountIndex < 0 || m_index.size() < accountIndex) {
qCDebug(logger) << "Not returning any data, model index is out of bounds:" << accountIndex << "model size is:" << m_index.size();
qCDebug(logger) << "Not returning any data, model index is out of bounds:" << accountIndex
<< "model size is:" << m_index.size();
return QVariant();
}
@ -231,7 +254,8 @@ namespace model
return m_storage && m_storage->isAccountStillAvailable(name, issuer);
}
AccountNameValidator::AccountNameValidator(QObject *parent) : QValidator(parent), m_issuer(std::nullopt), m_accounts(nullptr), m_delegate(nullptr)
AccountNameValidator::AccountNameValidator(QObject *parent) :
QValidator(parent), m_issuer(std::nullopt), m_accounts(nullptr), m_delegate(nullptr)
{
}
@ -264,7 +288,8 @@ namespace model
}
QValidator::State result = m_delegate.validate(input, pos);
return result != QValidator::Acceptable || m_accounts->isAccountStillAvailable(input, *m_issuer) ? result : QValidator::Intermediate;
return result != QValidator::Acceptable
|| m_accounts->isAccountStillAvailable(input, *m_issuer) ? result : QValidator::Intermediate;
}
void AccountNameValidator::fixup(QString &input) const

View File

@ -22,7 +22,8 @@
namespace model
{
qint64 millisecondsLeftForToken(const QDateTime &epoch, uint timeStep, const std::function<qint64(void)> &clock = &QDateTime::currentMSecsSinceEpoch);
qint64 millisecondsLeftForToken(const QDateTime &epoch, uint timeStep,
const std::function<qint64(void)> &clock = &QDateTime::currentMSecsSinceEpoch);
class AccountView : public QObject
{
@ -65,10 +66,18 @@ namespace model
AccountRole = Qt::ItemDataRole::UserRole
};
Q_ENUM(NonStandardRoles)
enum TOTPAlgorithms {
Sha1, Sha256, Sha512
};
Q_ENUM(TOTPAlgorithms)
private:
static accounts::Account::Hash toHash(const TOTPAlgorithms value);
public:
explicit SimpleAccountListModel(accounts::AccountStorage *storage, QObject *parent = nullptr);
Q_INVOKABLE void addTotp(const QString &account, const QString &issuer, const QString &secret, uint timeStep, int tokenLength);
Q_INVOKABLE void addHotp(const QString &account, const QString &issuer, const QString &secret, quint64 counter, int tokenLength);
Q_INVOKABLE void addTotp(const QString &account, const QString &issuer, const QString &secret, uint tokenLength,
uint timeStep, const QDateTime &epoch, model::SimpleAccountListModel::TOTPAlgorithms hash);
Q_INVOKABLE void addHotp(const QString &account, const QString &issuer, const QString &secret, uint tokenLength,
quint64 counter, bool fixedTruncation, uint offset, bool checksum);
Q_INVOKABLE bool isAccountStillAvailable(const QString &name, const QString &issuer) const;
Q_INVOKABLE int rowCount(const QModelIndex &parent = QModelIndex()) const override;
Q_INVOKABLE QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;

View File

@ -93,22 +93,27 @@ namespace oath
return encoder.tokenLength() >= 6;
}
std::optional<Algorithm> Algorithm::usingDynamicTruncation(QCryptographicHash::Algorithm algorithm, uint tokenLength, bool addChecksum, bool requireSaneKeyLength)
bool Algorithm::validate(QCryptographicHash::Algorithm algorithm, const std::optional<uint> &offset)
{
const Encoder encoder(tokenLength, addChecksum);
return usingDynamicTruncation(algorithm, encoder, requireSaneKeyLength);
/*
* An nullopt offset indicates dynamic truncation.
* Dynamic truncation works by taking the last nible and interpreting it as offset for truncation, i.e. it will always be <= 15.
* Accounting for the last nibble (therefore last byte) assume a max truncation offset of 16 if dynamic truncation is used.
*/
uint truncateAt = offset ? *offset : 16U;
/*
* The given algorithm must be supported/have a known digest size.
* There must be at least 4 bytes available at the given truncation offset/limit.
*/
std::optional<uint> digestSize = hmac::outputSize(algorithm);
return digestSize && *digestSize >= 4U && (*digestSize - 4U) >= truncateAt;
}
std::optional<Algorithm> Algorithm::usingDynamicTruncation(QCryptographicHash::Algorithm algorithm, const Encoder &encoder, bool requireSaneKeyLength)
std::optional<Algorithm> Algorithm::create(QCryptographicHash::Algorithm algorithm, const std::optional<uint> &offset, const Encoder &encoder, bool requireSaneKeyLength)
{
std::optional<uint> digestSize = hmac::outputSize(algorithm);
if (!digestSize) {
qCDebug(logger) << "Unable to determine digest size for algorithm:" << algorithm;
return std::nullopt;
}
if ((*digestSize) < 20) {
qCDebug(logger) << "Digest is too small for dynamic truncation with algorithm:" << algorithm << "digest size:" << *digestSize << "needed:" << 20;
if(!validate(algorithm, offset)) {
qCDebug(logger) << "Invalid algorithm:" << algorithm << "or incompatible with truncation offset:" << (offset ? *offset : 16U);
return std::nullopt;
}
@ -117,33 +122,28 @@ namespace oath
return std::nullopt;
}
const std::function<quint32(QByteArray)> truncation(truncateDynamically);
std::function<quint32(QByteArray)> truncation(truncateDynamically);
if (offset) {
uint at = *offset;
truncation = [at](QByteArray bytes) -> quint32
{
return truncate(bytes, at);
};
}
return std::optional<Algorithm>(Algorithm(encoder, truncation, algorithm, requireSaneKeyLength));
}
std::optional<Algorithm> Algorithm::usingTruncationOffset(QCryptographicHash::Algorithm algorithm, uint offset, const Encoder &encoder, bool requireSaneKeyLength)
std::optional<Algorithm> Algorithm::totp(QCryptographicHash::Algorithm algorithm, uint tokenLength, bool requireSaneKeyLength)
{
std::optional<uint> digestSize = hmac::outputSize(algorithm);
if (!digestSize) {
qCDebug(logger) << "Unable to determine digest size for algorithm:" << algorithm;
return std::nullopt;
}
const Encoder encoder(tokenLength, false);
return create(algorithm, std::nullopt, encoder, requireSaneKeyLength);
}
if (offset >= ((*digestSize) - 4)) {
qCDebug(logger) << "Digest is too small for truncation offset:" << offset << "with algorithm:" << algorithm << "digest size:" << *digestSize << "needed:" << offset + 4;
return std::nullopt;
}
if (!validate(encoder)) {
qCDebug(logger) << "Invalid token encoder";
return std::nullopt;
}
const std::function<quint32(QByteArray)> truncation([offset](QByteArray bytes) -> quint32
{
return truncate(bytes, offset);
});
return std::optional<Algorithm>(Algorithm(encoder, truncation, algorithm, requireSaneKeyLength));
std::optional<Algorithm> Algorithm::hotp(const std::optional<uint> &offset, uint tokenLength, bool checksum, bool requireSaneKeyLength)
{
const Encoder encoder(tokenLength, checksum);
return create(QCryptographicHash::Sha1, offset, encoder, requireSaneKeyLength);
}
Algorithm::Algorithm(const Encoder &encoder, const std::function<quint32(QByteArray)> &truncation, QCryptographicHash::Algorithm algorithm, bool requireSaneKeyLength) :

View File

@ -35,9 +35,10 @@ namespace oath
{
public:
static bool validate(const Encoder& encoder);
static std::optional<Algorithm> usingDynamicTruncation(QCryptographicHash::Algorithm algorithm, uint tokenLength, bool addChecksum = false, bool requireSaneKeyLength = false);
static std::optional<Algorithm> usingDynamicTruncation(QCryptographicHash::Algorithm algorithm, const Encoder &encoder, bool requireSaneKeyLength = false);
static std::optional<Algorithm> usingTruncationOffset(QCryptographicHash::Algorithm algorithm, uint offset, const Encoder &encoder, bool requireSaneKeyLength = false);
static bool validate(QCryptographicHash::Algorithm algorithm, const std::optional<uint> &offset);
static std::optional<Algorithm> create(QCryptographicHash::Algorithm algorithm, const std::optional<uint> &offset, const Encoder &encoder, bool requireSaneKeyLength = false);
static std::optional<Algorithm> totp(QCryptographicHash::Algorithm algorithm, uint tokenLength, bool requireSaneKeyLength = false);
static std::optional<Algorithm> hotp(const std::optional<uint> &offset, uint tokenLength, bool checksum, bool requireSaneKeyLength = false);
std::optional<QString> compute(quint64 counter, char * secretBuffer, int length) const;
private:
Algorithm(const Encoder &encoder, const std::function<quint32(QByteArray)> &truncation, QCryptographicHash::Algorithm algorithm, bool requireSaneKeyLength);