...
 
Commits (3)
......@@ -91,25 +91,45 @@ void LoadAccountsTest::sampleAccountsFile(void)
QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished");
QVERIFY2(actionRun, "accounts action should have run");
QCOMPARE(hotpFound.count(), 1);
QCOMPARE(totpFound.count(), 1);
QCOMPARE(hotpFound.count(), 2);
QCOMPARE(totpFound.count(), 2);
QCOMPARE(loadingError.count(), 0);
const auto firstHotp = hotpFound.at(0);
QCOMPARE(firstHotp.at(0).toUuid(), QUuid(QLatin1String("072a645d-6c26-57cc-81eb-d9ef3b9b39e2")));
QCOMPARE(firstHotp.at(1).toString(), QLatin1String("valid-hotp-sample-1"));
QCOMPARE(firstHotp.at(2).toByteArray(), rawSecret);
QCOMPARE(firstHotp.at(3).toByteArray(), rawNonce);
QCOMPARE(firstHotp.at(4).toULongLong(), 0ULL);
QCOMPARE(firstHotp.at(5).toInt(), 6);
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);
const auto secondHotp = hotpFound.at(1);
QCOMPARE(secondHotp.at(0).toUuid(), QUuid(QLatin1String("437c23aa-2fb0-519a-9a34-a5a2671eea24")));
QCOMPARE(secondHotp.at(1).toString(), QLatin1String("valid-hotp-sample-2"));
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);
const auto firstTotp = totpFound.at(0);
QCOMPARE(firstTotp.at(0).toUuid(), QUuid(QLatin1String("534cc72e-e9ec-5e39-a1ff-9f017c9be8cc")));
QCOMPARE(firstTotp.at(1).toString(), QLatin1String("valid-totp-sample-1"));
QCOMPARE(firstHotp.at(2).toByteArray(), rawSecret);
QCOMPARE(firstHotp.at(3).toByteArray(), rawNonce);
QCOMPARE(firstTotp.at(4).toUInt(), 30);
QCOMPARE(firstTotp.at(5).toInt(), 6);
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);
const auto secondTotp = totpFound.at(1);
QCOMPARE(secondTotp.at(0).toUuid(), QUuid(QLatin1String("6537d6a5-005e-5a92-b560-b09df3c2e676")));
QCOMPARE(secondTotp.at(1).toString(), QLatin1String("valid-totp-sample-2"));
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);
}
void LoadAccountsTest::invalidSampleAccountsFile(void)
......
......@@ -38,6 +38,15 @@ pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7Bd06103a4-8fa9-4a1f-a370-911bb84825a0%7D]
account=invalid-hotp-sample-6
counter=0
issuer=auto:tests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7Bfff1868c-e1db-4cd7-b931-7d1e51154c64%7D]
account=invalid-totp-sample-1
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
......@@ -71,9 +80,18 @@ timeStep=30
type=foo
[%7B0a1c83c9-62a8-4854-b985-7f364251dbf1%7D]
account=valid-totp-sample-5
account=invalid-totp-sample-5
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="FB1uuIu7qNWfqyGhFcemi7pxgcZgLNXoZZZtbdA="
timeStep=30
type=totp
[%7B0644b04a-58c0-4852-9a31-0e427d802e66%7D]
account=invalid-totp-sample-6
issuer=""
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=totp
......@@ -6,6 +6,15 @@ pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7B437c23aa-2fb0-519a-9a34-a5a2671eea24%7D]
account=valid-hotp-sample-2
counter=0
issuer=autotests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
[%7B534cc72e-e9ec-5e39-a1ff-9f017c9be8cc%7D]
account=valid-totp-sample-1
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
......@@ -13,3 +22,12 @@ pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=totp
[%7B6537d6a5-005e-5a92-b560-b09df3c2e676%7D]
account=valid-totp-sample-2
issuer=autotests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=totp
......@@ -8,8 +8,10 @@
<file>load-accounts/sample-accounts.ini</file>
<file>load-accounts/invalid-accounts.ini</file>
<file>load-accounts/empty-accounts.ini</file>
<file>save-hotp/expected-accounts.ini</file>
<file>save-totp/expected-accounts.ini</file>
<file>save-hotp/expected-accounts-1.ini</file>
<file>save-hotp/expected-accounts-2.ini</file>
<file>save-totp/expected-accounts-1.ini</file>
<file>save-totp/expected-accounts-2.ini</file>
<file>delete-accounts/sample-accounts.ini</file>
<file>delete-accounts/empty-accounts.ini</file>
<file>delete-accounts/only-hotp-left.ini</file>
......
[%7B437c23aa-2fb0-519a-9a34-a5a2671eea24%7D]
account=valid-hotp-sample-2
counter=0
issuer=autotests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
[%7B6537d6a5-005e-5a92-b560-b09df3c2e676%7D]
account=valid-totp-sample-2
issuer=autotests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=totp
SPDX-License-Identifier: CC0-1.0
SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
......@@ -34,24 +34,30 @@ static void define_test_data(void)
{
QTest::addColumn<QUuid>("id");
QTest::addColumn<QString>("name");
QTest::addColumn<QString>("issuer");
QTest::addColumn<quint64>("counter");
QTest::addColumn<int>("tokenLength");
QTest::addColumn<QString>("actualAccountsIni");
QTest::addColumn<QString>("expectedAccountsIni");
}
static void define_test_case(const char * label, const QUuid &id, const QString &accountName, quint64 counter, int tokenLength)
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)
{
QTest::newRow(label) << id << accountName << counter << tokenLength;
QTest::newRow(label) << id << accountName << issuer << counter << tokenLength << actualIni << expectedIni;
}
void SaveHotpTest::validHotp(void)
{
QFETCH(QUuid, id);
QFETCH(QString, name);
QFETCH(QString, issuer);
QFETCH(quint64, counter);
QFETCH(int, tokenLength);
QFETCH(QString, actualAccountsIni);
QFETCH(QString, expectedAccountsIni);
const QString actual = test::path("actual-accounts.ini");
const QString lock = test::path("actual-accounts.ini.lock");
const QString actual = test::path(actualAccountsIni);
const QString lock = test::path(actualAccountsIni + QLatin1String(".lock"));
bool actionRun = false;
const accounts::SettingsProvider settings([&actual, &actionRun](const accounts::PersistenceAction &action) -> void
......@@ -64,7 +70,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, *tokenSecret, counter, tokenLength);
accounts::SaveHotp uut(settings, id, name, issuer, *tokenSecret, counter, tokenLength);
QSignalSpy invalidAccount(&uut, &accounts::SaveHotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveHotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveHotp::finished);
......@@ -76,7 +82,7 @@ void SaveHotpTest::validHotp(void)
QFile result(actual);
QVERIFY2(result.exists(), "accounts file should have been created");
QCOMPARE(test::slurp(actual), test::slurp(":/save-hotp/expected-accounts.ini"));
QCOMPARE(test::slurp(actual), test::slurp(expectedAccountsIni));
QFile lockFile(lock);
QVERIFY2(!lockFile.exists(), "lock file should no longer exist");
......@@ -90,11 +96,14 @@ void SaveHotpTest::invalidHotp(void)
{
QFETCH(QUuid, id);
QFETCH(QString, name);
QFETCH(QString, issuer);
QFETCH(quint64, counter);
QFETCH(int, tokenLength);
QFETCH(QString, actualAccountsIni);
QFETCH(QString, expectedAccountsIni);
const QString actual = test::path("dummy-accounts.ini");
const QString lock = test::path("dummy-accounts.ini.lock");
const QString actual = test::path(actualAccountsIni);
const QString lock = test::path(actualAccountsIni + QLatin1String(".lock"));
bool actionRun = false;
const accounts::SettingsProvider settings([&actual, &actionRun](const accounts::PersistenceAction &action) -> void
......@@ -107,7 +116,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, *tokenSecret, counter, tokenLength);
accounts::SaveHotp uut(settings, id, name, issuer, *tokenSecret, counter, tokenLength);
QSignalSpy invalidAccount(&uut, &accounts::SaveHotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveHotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveHotp::finished);
......@@ -129,17 +138,20 @@ 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"), 0, 6);
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"));
}
void SaveHotpTest::invalidHotp_data(void)
{
define_test_data();
define_test_case("null UUID", QUuid(), QLatin1String("null UUID"), 0, 6);
define_test_case("null account name", QUuid("00611bbf-5e0b-5c6a-9847-ad865315ce86"), QString(), 0, 6);
define_test_case("empty account name", QUuid("1e42b907-99d8-5da3-a59b-89b257e49c83"), QLatin1String(""), 0, 6);
define_test_case("tokenLength too small", QUuid("bca12e13-4b5b-5e4e-b162-3b86a6284dea"), QLatin1String("tokenLength too small"), 0, 5);
define_test_case("tokenLength too large", QUuid("5c10d530-fb22-5438-848d-3d4d1f738610"), QLatin1String("tokenLength too large"), 0, 11);
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());
}
void SaveHotpTest::initTestCase(void)
......
......@@ -22,10 +22,10 @@ class SaveTotpTest: public QObject
Q_OBJECT
private Q_SLOTS:
void initTestCase(void);
void validHotp(void);
void validHotp_data(void);
void invalidHotp(void);
void invalidHotp_data(void);
void validTotp(void);
void validTotp_data(void);
void invalidTotp(void);
void invalidTotp_data(void);
private:
accounts::AccountSecret m_secret {&test::fakeRandom};
};
......@@ -34,24 +34,30 @@ static void define_test_data(void)
{
QTest::addColumn<QUuid>("id");
QTest::addColumn<QString>("name");
QTest::addColumn<QString>("issuer");
QTest::addColumn<uint>("timeStep");
QTest::addColumn<int>("tokenLength");
QTest::addColumn<QString>("actualAccountsIni");
QTest::addColumn<QString>("expectedAccountsIni");
}
static void define_test_case(const char * label, const QUuid &id, const QString &accountName, uint timeStep, int tokenLength)
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)
{
QTest::newRow(label) << id << accountName << timeStep << tokenLength;
QTest::newRow(label) << id << accountName << issuer << timeStep << tokenLength << actualIni << expectedIni;
}
void SaveTotpTest::validHotp(void)
void SaveTotpTest::validTotp(void)
{
QFETCH(QUuid, id);
QFETCH(QString, name);
QFETCH(QString, issuer);
QFETCH(uint, timeStep);
QFETCH(int, tokenLength);
QFETCH(QString, actualAccountsIni);
QFETCH(QString, expectedAccountsIni);
const QString actual = test::path("actual-accounts.ini");
const QString lock = test::path("actual-accounts.ini.lock");
const QString actual = test::path(actualAccountsIni);
const QString lock = test::path(actualAccountsIni + QLatin1String(".lock"));
bool actionRun = false;
const accounts::SettingsProvider settings([&actual, &actionRun](const accounts::PersistenceAction &action) -> void
......@@ -64,7 +70,7 @@ void SaveTotpTest::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::SaveTotp uut(settings, id, name, *tokenSecret, timeStep, tokenLength);
accounts::SaveTotp uut(settings, id, name, issuer, *tokenSecret, timeStep, tokenLength);
QSignalSpy invalidAccount(&uut, &accounts::SaveTotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveTotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveTotp::finished);
......@@ -76,7 +82,7 @@ void SaveTotpTest::validHotp(void)
QFile result(actual);
QVERIFY2(result.exists(), "accounts file should have been created");
QCOMPARE(test::slurp(actual), test::slurp(":/save-totp/expected-accounts.ini"));
QCOMPARE(test::slurp(actual), test::slurp(expectedAccountsIni));
QFile lockFile(lock);
QVERIFY2(!lockFile.exists(), "lock file should no longer exist");
......@@ -86,15 +92,18 @@ void SaveTotpTest::validHotp(void)
QCOMPARE(savedAccount.count(), 1);
}
void SaveTotpTest::invalidHotp(void)
void SaveTotpTest::invalidTotp(void)
{
QFETCH(QUuid, id);
QFETCH(QString, name);
QFETCH(QString, issuer);
QFETCH(uint, timeStep);
QFETCH(int, tokenLength);
QFETCH(QString, actualAccountsIni);
QFETCH(QString, expectedAccountsIni);
const QString actual = test::path("dummy-accounts.ini");
const QString lock = test::path("dummy-accounts.ini.lock");
const QString actual = test::path(actualAccountsIni);
const QString lock = test::path(actualAccountsIni + QLatin1String(".lock"));
bool actionRun = false;
const accounts::SettingsProvider settings([&actual, &actionRun](const accounts::PersistenceAction &action) -> void
......@@ -107,7 +116,7 @@ void SaveTotpTest::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::SaveTotp uut(settings, id, name, *tokenSecret, timeStep, tokenLength);
accounts::SaveTotp uut(settings, id, name, issuer, *tokenSecret, timeStep, tokenLength);
QSignalSpy invalidAccount(&uut, &accounts::SaveTotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveTotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveTotp::finished);
......@@ -126,21 +135,24 @@ void SaveTotpTest::invalidHotp(void)
QVERIFY2(!lockFile.exists(), "lock file should not have been created");
}
void SaveTotpTest::validHotp_data(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"), 30, 6);
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"));
}
void SaveTotpTest::invalidHotp_data(void)
void SaveTotpTest::invalidTotp_data(void)
{
define_test_data();
define_test_case("null UUID", QUuid(), QLatin1String("null UUID"), 30, 6);
define_test_case("null account name", QUuid("00611bbf-5e0b-5c6a-9847-ad865315ce86"), QString(), 30, 6);
define_test_case("empty account name", QUuid("1e42b907-99d8-5da3-a59b-89b257e49c83"), QLatin1String(""), 30, 6);
define_test_case("timeStep too small", QUuid("5ab8749b-f973-5f48-a70e-c261ebd0521a"), QLatin1String("timeStep too small"), 0, 6);
define_test_case("tokenLength too small", QUuid("bca12e13-4b5b-5e4e-b162-3b86a6284dea"), QLatin1String("tokenLength too small"), 30, 5);
define_test_case("tokenLength too large", QUuid("5c10d530-fb22-5438-848d-3d4d1f738610"), QLatin1String("tokenLength too large"), 30, 11);
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());
}
void SaveTotpTest::initTestCase(void)
......
......@@ -101,6 +101,7 @@ void HotpCounterUpdateTest::testCounterUpdate(void)
QSignalSpy sampleAccountTokenUpdated(sampleAccount, &accounts::Account::tokenChanged);
QCOMPARE(sampleAccount->name(), sampleAccountName);
QCOMPARE(sampleAccount->issuer(), QString());
QCOMPARE(sampleAccount->algorithm(), accounts::Account::Hotp);
QCOMPARE(sampleAccount->token(), QString());
......
......@@ -5,8 +5,9 @@ length=32
memory=8192
salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
[%7B534cc72e-e9ec-5e39-a1ff-9f017c9be8cc%7D]
[%7B3ff3fc9b-9e8c-50aa-8f51-99d213843761%7D]
account=valid-totp-sample-1
issuer=autotests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=8
secret="LXM8veM3T1qY/gAYsTGZNEdwfrPWTNlXU1OykwY="
......
......@@ -8,6 +8,7 @@ salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
account=valid-hotp-sample-1
counter=42
issuer=autotests
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=7
secret="LXM8veM3T1qY/gAYsTGZNEdwfrPWTNlXU1OykwY="
......
......@@ -34,7 +34,7 @@ void StorageDefaultLifeCycleTest::initTestCase(void)
void StorageDefaultLifeCycleTest::testLifecycle(void)
{
const QString iniResource = test::path(testIniResource);
const QString sampleAccountName(QLatin1String("valid-hotp-sample-1"));
const QString samleAccountFullName(QLatin1String("autotests:valid-hotp-sample-1"));
const accounts::SettingsProvider settings([&iniResource](const accounts::PersistenceAction &action) -> void
{
......@@ -79,9 +79,9 @@ void StorageDefaultLifeCycleTest::testLifecycle(void)
QCOMPARE(uut->hasError(), false);
QCOMPARE(error.count(), 0);
QCOMPARE(accountAdded.count(), 1);
QCOMPARE(accountAdded.at(0).at(0), sampleAccountName);
QCOMPARE(accountAdded.at(0).at(0), samleAccountFullName);
accounts::Account *sampleAccount = uut->get(sampleAccountName);
accounts::Account *sampleAccount = uut->get(samleAccountFullName);
QVERIFY2(sampleAccount != nullptr, "get() should return the sample account");
QSignalSpy sampleAccountCleaned(sampleAccount, &accounts::Account::destroyed);
......
#
# SPDX-License-Identifier: BSD-2-Clause
# SPDX-FileCopyrightText: 2019 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
# SPDX-FileCopyrightText: 2019-2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
#
set(validator_lib_test_SRCS
base32-validator.cpp
name-validator.cpp
issuer-validator.cpp
unsigned-long-validator.cpp
unsigned-long-parsing.cpp
)
......
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#include "test-util.h"
#include "validators/issuervalidator.h"
using namespace validators::test;
static void define_valid_table(void)
{
define_test_case(QLatin1String(""), QLatin1String(""), QValidator::Acceptable);
define_test_case(QLatin1String("Issuer"), QLatin1String("Issuer"), QValidator::Acceptable);
define_test_case(QLatin1String("test issuer"), QLatin1String("test issuer"), QValidator::Acceptable);
}
static void define_invalid_table(void)
{
define_test_case(QLatin1String("test\tissuer"), QLatin1String("test issuer"), QValidator::Invalid);
define_test_case(QLatin1String("\r \n\ttest\r\t \nissuer \r\t\n"), QLatin1String("test issuer"), QValidator::Invalid);
define_test_case(QLatin1String("test "), QLatin1String("test "), QValidator::Invalid);
define_test_case(QLatin1String("test:issuer"), QLatin1String("testissuer"), QValidator::Invalid);
}
static void define_empty_table(void)
{
define_test_case(QLatin1String(" "), QLatin1String(""), QValidator::Invalid);
define_test_case(QLatin1String("\t"), QLatin1String(""), QValidator::Invalid);
define_test_case(QLatin1String("\r\n"), QLatin1String(""), QValidator::Invalid);
}
static void define_data(void)
{
define_empty_table();
define_valid_table();
define_invalid_table();
}
DEFINE_VALIDATOR_TEST(IssuerValidatorTest, validators::IssuerValidator, define_data);
QTEST_APPLESS_MAIN(IssuerValidatorTest)
#include "issuer-validator.moc"
......@@ -24,6 +24,12 @@ namespace accounts
return d->name();
}
QString Account::issuer(void) const
{
Q_D(const Account);
return d->issuer();
}
QString Account::token(void) const
{
Q_D(const Account);
......@@ -146,16 +152,26 @@ namespace accounts
d->load(handler);
}
bool AccountStorage::contains(const QString &name) const
bool AccountStorage::contains(const QString &fullName) const
{
Q_D(const AccountStorage);
return d->contains(name);
return d->contains(fullName);
}
bool AccountStorage::contains(const QString &name, const QString &issuer) const
{
return contains(AccountPrivate::toFullName(name, issuer));
}
Account * AccountStorage::get(const QString &name) const
Account * AccountStorage::get(const QString &fullName) const
{
Q_D(const AccountStorage);
return d->get(name);
return d->get(fullName);
}
Account * AccountStorage::get(const QString &name, const QString &issuer) const
{
return get(AccountPrivate::toFullName(name, issuer));
}
AccountSecret * AccountStorage::secret(void) const
......@@ -164,13 +180,18 @@ namespace accounts
return d->secret();
}
bool AccountStorage::isNameStillAvailable(const QString &name) const
bool AccountStorage::isAccountStillAvailable(const QString &fullName) const
{
Q_D(const AccountStorage);
return d->isNameStillAvailable(name);
return d->isAccountStillAvailable(fullName);
}
bool AccountStorage::isAccountStillAvailable(const QString &name, const QString &issuer) const
{
return isAccountStillAvailable(AccountPrivate::toFullName(name, issuer));
}
void AccountStorage::addHotp(const QString &name, const QString &secret, quint64 counter, int tokenLength, int offset, bool addChecksum)
void AccountStorage::addHotp(const QString &name, const QString &issuer, const QString &secret, quint64 counter, int tokenLength, int offset, bool addChecksum)
{
Q_D(AccountStorage);
const std::function<void(SaveHotp*)> handler([this](SaveHotp *job) -> void
......@@ -178,12 +199,12 @@ namespace accounts
QObject::connect(job, &SaveHotp::saved, this, &AccountStorage::handleHotp);
QObject::connect(job, &SaveHotp::invalid, this, &AccountStorage::handleError);
});
if (!d->addHotp(handler, name, secret, counter, tokenLength, offset, addChecksum)) {
if (!d->addHotp(handler, name, issuer.isEmpty() ? QString() : issuer, secret, counter, tokenLength, offset, addChecksum)) {
Q_EMIT error();
}
}
void AccountStorage::addTotp(const QString &name, 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, int timeStep, int tokenLength, const QDateTime &epoch, Account::Hash hash)
{
Q_D(AccountStorage);
const std::function<void(SaveTotp*)> handler([this](SaveTotp *job) -> void
......@@ -191,7 +212,7 @@ namespace accounts
QObject::connect(job, &SaveTotp::saved, this, &AccountStorage::handleTotp);
QObject::connect(job, &SaveTotp::invalid, this, &AccountStorage::handleError);
});
if (!d->addTotp(handler, name, secret, timeStep, tokenLength, epoch, hash)) {
if (!d->addTotp(handler, name, issuer.isEmpty() ? QString() : issuer, secret, timeStep, tokenLength, epoch, hash)) {
Q_EMIT error();
}
}
......@@ -204,9 +225,9 @@ namespace accounts
Account *account = from ? qobject_cast<Account*>(from) : nullptr;
Q_ASSERT_X(account, Q_FUNC_INFO, "event should be sent by an account");
const QString name = account->name();
Q_EMIT removed(name);
d->acceptAccountRemoval(name);
const QString fullName = AccountPrivate::toFullName(account->name(), account->issuer());
d->acceptAccountRemoval(fullName);
Q_EMIT removed(fullName);
}
QVector<QString> AccountStorage::accounts(void) const
......@@ -215,7 +236,7 @@ namespace accounts
return d->activeAccounts();
}
void AccountStorage::handleHotp(const QUuid id, const QString name, 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, quint64 counter, int tokenLength)
{
Q_D(AccountStorage);
if (!d->isStillOpen()) {
......@@ -225,10 +246,10 @@ namespace accounts
return;
}
if (!isNameStillAvailable(name)) {
if (!isAccountStillAvailable(name, issuer)) {
qCDebug(logger)
<< "Not handling HOTP account:" << id
<< "Account name not available";
<< "Account name or issuer not available";
return;
}
......@@ -240,13 +261,13 @@ namespace accounts
return;
}
Account *accepted = d->acceptHotpAccount(id, name, *encryptedSecret, counter, tokenLength);
Account *accepted = d->acceptHotpAccount(id, name, issuer, *encryptedSecret, counter, tokenLength);
QObject::connect(accepted, &Account::removed, this, &AccountStorage::accountRemoved);
Q_EMIT added(name);
Q_EMIT added(AccountPrivate::toFullName(name, issuer));
}
void AccountStorage::handleTotp(const QUuid id, const QString name, 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 timeStep, int tokenLength)
{
Q_D(AccountStorage);
if (!d->isStillOpen()) {
......@@ -256,10 +277,10 @@ namespace accounts
return;
}
if (!isNameStillAvailable(name)) {
if (!isAccountStillAvailable(name, issuer)) {
qCDebug(logger)
<< "Not handling TOTP account:" << id
<< "Account name not available";
<< "Account name or issuer not available";
return;
}
......@@ -271,10 +292,10 @@ namespace accounts
return;
}
Account *accepted = d->acceptTotpAccount(id, name, *encryptedSecret, timeStep, tokenLength);
Account *accepted = d->acceptTotpAccount(id, name, issuer, *encryptedSecret, timeStep, tokenLength);
QObject::connect(accepted, &Account::removed, this, &AccountStorage::accountRemoved);
Q_EMIT added(name);
Q_EMIT added(AccountPrivate::toFullName(name, issuer));
}
void AccountStorage::dispose(void)
......
......@@ -40,6 +40,7 @@ namespace accounts
explicit Account(AccountPrivate *d, QObject *parent = nullptr);
QString name(void) const;
QString token(void) const;
QString issuer(void) const;
quint64 counter(void) const;
QDateTime epoch(void) const;
uint timeStep(void) const;
......@@ -69,19 +70,24 @@ namespace accounts
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 isNameStillAvailable(const QString &name) const;
bool contains(const QString &name) const;
Account * get(const QString &name) const;
bool isAccountStillAvailable(const QString &fullName) const;
bool isAccountStillAvailable(const QString &name, const QString &issuer) const;
bool contains(const QString &fullName) const;
bool contains(const QString &name, const QString &issuer) const;
Account * get(const QString &fullName) const;
Account * get(const QString &name, const QString &issuer) const;
AccountSecret * secret(void) const;
QVector<QString> accounts(void) const;
void dispose(void);
void addHotp(const QString &name,
const QString &issuer,
const QString &secret,
quint64 counter = 0ULL,
int tokenLength = 6,
int offset = -1,
bool addChecksum = false);
void addTotp(const QString &name,
const QString &issuer,
const QString &secret,
int timeStep = 30,
int tokenLength = 6,
......@@ -91,8 +97,8 @@ namespace accounts
bool hasError(void) const;
bool isLoaded(void) const;
Q_SIGNALS:
void added(const QString name);
void removed(const QString name);
void added(const QString fullName);
void removed(const QString fullName);
void error(void);
void loaded(void);
void disposed(void);
......@@ -103,8 +109,8 @@ namespace accounts
void handleDisposal(void);
void handleError(void);
void handleLoaded(void);
void handleHotp(const QUuid id, const QString name, const QByteArray secret, const QByteArray nonce, quint64 counter, int tokenLength);
void handleTotp(const QUuid id, const QString name, 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, 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);
private:
QScopedPointer<AccountStoragePrivate> m_dptr;
Q_DECLARE_PRIVATE_D(m_dptr, AccountStorage)
......
......@@ -29,6 +29,12 @@ namespace accounts
return m_name;
}
QString AccountPrivate::issuer(void) const
{
return m_issuer;
}
QString AccountPrivate::token(void) const
{
return m_token;
......@@ -94,7 +100,7 @@ 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_secret, counter, m_tokenLength);
SaveHotp *job = new SaveHotp(m_storage->settings(),m_id, m_name, m_issuer, m_secret, counter, m_tokenLength);
m_actions->queueAndProceed(job, [counter, job, q, this](void) -> void
{
new HandleCounterUpdate(this, m_storage, counter, job, q);
......@@ -209,7 +215,7 @@ namespace accounts
}
QSet<QString> self;
self.insert(m_name);
self.insert(AccountPrivate::toFullName(m_name, m_issuer));
m_storage->removeAccounts(self);
}
......@@ -224,18 +230,18 @@ namespace accounts
}
AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account, AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const secrets::EncryptedSecret &secret, quint64 counter, int tokenLength, int offset, bool addChecksum) :
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_secret(secret), m_tokenLength(tokenLength),
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
{
}
AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account, AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const secrets::EncryptedSecret &secret, const QDateTime &epoch, uint timeStep, int tokenLength, Account::Hash hash) :
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_secret(secret), m_tokenLength(tokenLength),
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
m_epoch(epoch), m_timeStep(timeStep), m_hash(hash)
{
......@@ -269,14 +275,19 @@ namespace accounts
return m_is_still_open;
}
bool AccountStoragePrivate::isNameStillAvailable(const QString &name) const
bool AccountStoragePrivate::isAccountStillAvailable(const QString &account) const
{
if (!m_is_still_open) {
qCDebug(logger) << "Pretending no name is available: account storage no longer open";
return false;
}
return !m_names.contains(name);
return !m_names.contains(account);
}
QString AccountPrivate::toFullName(const QString &name, const QString &issuer)
{
return issuer.isEmpty() ? name : issuer + QLatin1Char(':') + name;
}
bool AccountStoragePrivate::contains(const QString &account) const
......@@ -448,12 +459,12 @@ namespace accounts
return m_secret->encrypt(decoded.data());
}
bool AccountStoragePrivate::validateGenericNewToken(const QString &name, const QString &secret, int tokenLength) const
bool AccountStoragePrivate::validateGenericNewToken(const QString &name, const QString &issuer, const QString &secret, int tokenLength) const
{
return checkTokenLength(tokenLength) && checkName(name) && isNameStillAvailable(name) && 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 &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, quint64 counter, int tokenLength, int offset, bool addChecksum)
{
Q_UNUSED(offset);
Q_UNUSED(addChecksum);
......@@ -462,7 +473,7 @@ namespace accounts
return false;
}
if (!validateGenericNewToken(name, secret, tokenLength)) {
if (!validateGenericNewToken(name, issuer, secret, tokenLength)) {
qCDebug(logger) << "Will not add new HOTP account: invalid account details";
return false;
}
......@@ -473,11 +484,11 @@ namespace accounts
return false;
}
QUuid id = generateId(name);
QUuid id = generateId(AccountPrivate::toFullName(name, issuer));
qCDebug(logger) << "Requesting to store details for new HOTP account:" << id;
m_ids.insert(id);
SaveHotp *job = new SaveHotp(m_settings, id, name, *encryptedSecret, counter, tokenLength);
SaveHotp *job = new SaveHotp(m_settings, id, name, issuer, *encryptedSecret, counter, tokenLength);
m_actions->queueAndProceed(job, [job, &handler](void) -> void
{
handler(job);
......@@ -485,7 +496,7 @@ namespace accounts
return true;
}
bool AccountStoragePrivate::addTotp(const std::function<void(SaveTotp*)> &handler, const QString &name, 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 timeStep, int tokenLength, const QDateTime &epoch, Account::Hash hash)
{
Q_UNUSED(epoch);
Q_UNUSED(hash);
......@@ -494,7 +505,7 @@ namespace accounts
return false;
}
if (!validateGenericNewToken(name, secret, tokenLength) || !checkTimeStep(timeStep)) {
if (!validateGenericNewToken(name, issuer, secret, tokenLength) || !checkTimeStep(timeStep)) {
qCDebug(logger) << "Will not add new TOTP account: invalid account details";
return false;
}
......@@ -505,11 +516,11 @@ namespace accounts
return false;
}
QUuid id = generateId(name);
QUuid id = generateId(AccountPrivate::toFullName(name, issuer));
qCDebug(logger) << "Requesting to store details for new TOTP account:" << id;
m_ids.insert(id);
SaveTotp *job = new SaveTotp(m_settings, id, name, *encryptedSecret, timeStep, tokenLength);
SaveTotp *job = new SaveTotp(m_settings, id, name, issuer, *encryptedSecret, timeStep, tokenLength);
m_actions->queueAndProceed(job, [job, &handler](void) -> void
{
handler(job);
......@@ -546,7 +557,7 @@ namespace accounts
});
}
Account * AccountStoragePrivate::acceptHotpAccount(const QUuid &id, const QString &name, 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, quint64 counter, int tokenLength, int offset, bool addChecksum)
{
Q_Q(AccountStorage);
qCDebug(logger) << "Registering HOTP account:" << id;
......@@ -557,14 +568,14 @@ namespace accounts
return account;
});
m_ids.insert(id);
m_names.insert(name, id);
m_accountsPrivate.insert(id, new AccountPrivate(registration, this, m_actions, id, name, secret, counter, tokenLength, offset, addChecksum));
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));
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 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 timeStep, int tokenLength, const QDateTime &epoch, Account::Hash hash)
{
Q_Q(AccountStorage);
qCDebug(logger) << "Registering TOTP account:" << id;
......@@ -575,8 +586,8 @@ namespace accounts
return account;
});
m_ids.insert(id);
m_names.insert(name, id);
m_accountsPrivate.insert(id, new AccountPrivate(registration, this, m_actions, id, name, secret, epoch, timeStep, tokenLength, hash));
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));
Q_ASSERT_X(m_accounts.contains(id), Q_FUNC_INFO, "account should have been registered");
return m_accounts[id];
......
......@@ -29,6 +29,7 @@ namespace accounts
int offset(void) const;
QString name(void) const;
QString token(void) const;
QString issuer(void) const;
quint64 counter(void) const;
QDateTime epoch(void) const;
uint timeStep(void) const;
......@@ -37,13 +38,14 @@ namespace accounts
int tokenLength(void) const;
bool checksum(void) const;
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 secrets::EncryptedSecret &secret,
const QString &name, const QString &issuer, const secrets::EncryptedSecret &secret,
quint64 counter, int tokenLength, int offset, bool addChecksum);
explicit AccountPrivate(const std::function<Account*(AccountPrivate*)> &account,
AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const secrets::EncryptedSecret &secret,
const QString &name, const QString &issuer, const secrets::EncryptedSecret &secret,
const QDateTime &epoch, uint timeStep, int tokenLength, Account::Hash hash);
void recompute(void);
void setCounter(quint64 counter);
......@@ -65,6 +67,7 @@ namespace accounts
private:
QString m_token;
const QString m_name;
const QString m_issuer;
const secrets::EncryptedSecret m_secret;
const int m_tokenLength;
quint64 m_counter;
......@@ -87,13 +90,14 @@ namespace accounts
bool isStillOpen(void) const;
bool contains(const QString &account) const;
SettingsProvider settings(void) const;
bool isNameStillAvailable(const QString &name) const;
bool isAccountStillAvailable(const QString &account) const;
Account * get(const QString &account) const;
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,
......@@ -101,6 +105,7 @@ namespace accounts
bool addChecksum = false);
Account * acceptTotpAccount(const QUuid &id,
const QString &name,
const QString &issuer,
const secrets::EncryptedSecret &secret,
uint timeStep = 30,
int tokenLength = 6,
......@@ -108,6 +113,7 @@ namespace accounts
Account::Hash hash = Account::Hash::Default);
bool addHotp(const std::function<void(SaveHotp*)> &handler,
const QString &name,
const QString &issuer,
const QString &secret,
quint64 counter = 0ULL,
int tokenLength = 6,
......@@ -115,6 +121,7 @@ namespace accounts
bool addChecksum = false);
bool addTotp(const std::function<void(SaveTotp*)> &handler,
const QString &name,
const QString &issuer,
const QString &secret,
uint timeStep = 30,
int tokenLength = 6,
......@@ -126,7 +133,7 @@ namespace accounts
void clearError(void);
bool hasError(void) const;
private:
bool validateGenericNewToken(const QString &name, const QString &secret, int tokenLength) const;
bool validateGenericNewToken(const QString &name, const QString &issuer, const QString &secret, int tokenLength) const;
std::optional<secrets::EncryptedSecret> encrypt(const QString &secret) const;
QUuid generateId(const QString &name) const;
private:
......
......@@ -51,19 +51,19 @@ namespace accounts
{
}
SaveHotp::SaveHotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const secrets::EncryptedSecret &secret, quint64 counter, int tokenLength) :
AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), 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, 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)
{
}
SaveTotp::SaveTotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const secrets::EncryptedSecret &secret, uint timeStep, int tokenLength) :
AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), 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 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)
{
}
void SaveHotp::run(void)
{
if (!checkId(m_id) || !checkName(m_accountName) || !checkTokenLength(m_tokenLength)) {
if (!checkId(m_id) || !checkName(m_accountName) || !checkIssuer(m_issuer) || !checkTokenLength(m_tokenLength)) {
qCDebug(logger)
<< "Unable to save HOTP account:" << m_id
<< "Invalid account details";
......@@ -88,6 +88,9 @@ namespace accounts
settings.remove(group);
settings.beginGroup(group);
settings.setValue("account", m_accountName);
if (!m_issuer.isNull()) {
settings.setValue("issuer", m_issuer);
}
settings.setValue("type", "hotp");
QString encodedNonce = QString::fromUtf8(m_secret.nonce().toBase64(QByteArray::Base64Encoding));
QString encodedSecret = QString::fromUtf8(m_secret.cryptText().toBase64(QByteArray::Base64Encoding));
......@@ -100,7 +103,7 @@ namespace accounts
// 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_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_counter, m_tokenLength);
});
m_settings(act);
......@@ -109,7 +112,7 @@ namespace accounts
void SaveTotp::run(void)
{
if (!checkId(m_id) || !checkName(m_accountName) || !checkTokenLength(m_tokenLength) || !checkTimeStep(m_timeStep)) {
if (!checkId(m_id) || !checkName(m_accountName) || !checkIssuer(m_issuer) || !checkTokenLength(m_tokenLength) || !checkTimeStep(m_timeStep)) {
qCDebug(logger)
<< "Unable to save TOTP account:" << m_id
<< "Invalid account details";
......@@ -134,6 +137,9 @@ namespace accounts
settings.remove(group);
settings.beginGroup(group);
settings.setValue("account", m_accountName);
if (!m_issuer.isNull()) {
settings.setValue("issuer", m_issuer);
}
settings.setValue("type", "totp");
QString encodedNonce = QString::fromUtf8(m_secret.nonce().toBase64(QByteArray::Base64Encoding));
QString encodedSecret = QString::fromUtf8(m_secret.cryptText().toBase64(QByteArray::Base64Encoding));
......@@ -146,7 +152,7 @@ namespace accounts
// 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_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_timeStep, m_tokenLength);
});
m_settings(act);
......@@ -352,6 +358,15 @@ namespace accounts
continue;
}
const QString issuer = settings.value("issuer", QString()).toString();
if (!checkIssuer(issuer)) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid account issuer";
settings.endGroup();
continue;
}
const QString type = settings.value("type").toString();
if (type != QLatin1String("hotp") && type != QLatin1String("totp")) {
qCWarning(logger)
......@@ -411,7 +426,7 @@ namespace accounts
}
qCInfo(logger) << "Found valid TOTP account:" << id;
Q_EMIT foundTotp(id, accountName, secret, nonce, timeStep, tokenLength);
Q_EMIT foundTotp(id, accountName, issuer, secret, nonce, timeStep, tokenLength);
}
if (type == QLatin1String("hotp")) {
......@@ -427,7 +442,7 @@ namespace accounts
}
qCInfo(logger) << "Found valid HOTP account:" << id;
Q_EMIT foundHotp(id, accountName, secret, nonce, counter, tokenLength);
Q_EMIT foundHotp(id, accountName, issuer, secret, nonce, counter, tokenLength);
}
settings.endGroup();
......
......@@ -69,8 +69,8 @@ namespace accounts
explicit LoadAccounts(const SettingsProvider &settings, const AccountSecret *secret);
void run(void) override;
Q_SIGNALS:
void foundHotp(const QUuid id, const QString name, const QByteArray secret, const QByteArray nonce, quint64 counter, int tokenLength);
void foundTotp(const QUuid id, const QString name, 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, 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 failedToLoadAllAccounts(void);
private:
const SettingsProvider m_settings;
......@@ -94,15 +94,16 @@ namespace accounts
{
Q_OBJECT
public:
explicit SaveHotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, 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, quint64 counter, int tokenLength);
void run(void) override;
Q_SIGNALS:
void invalid(void);
void saved(const QUuid id, const QString accountName, 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, quint64 counter, int tokenLength);
private:
const SettingsProvider m_settings;
const QUuid m_id;
const QString m_accountName;
const QString m_issuer;
const secrets::EncryptedSecret m_secret;
const quint64 m_counter;
const int m_tokenLength;
......@@ -112,15 +113,16 @@ namespace accounts
{
Q_OBJECT
public:
explicit SaveTotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, 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 timeStep, int tokenLength);
void run(void) override;
Q_SIGNALS:
void invalid(void);
void saved(const QUuid id, const QString accountName, 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 timeStep, int tokenLength);
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_timeStep;
const int m_tokenLength;
......
......@@ -24,6 +24,11 @@ namespace accounts
return !name.isEmpty();
}
bool checkIssuer(const QString &issuer)
{
return issuer.isNull() || (!issuer.isEmpty() && !issuer.contains(QLatin1Char(':')));
}
bool checkTokenLength(int tokenLength)
{
return tokenLength >= 6 && tokenLength <= 10;
......
......@@ -13,6 +13,7 @@ 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 checkTimeStep(uint timeStep);
}
......
......@@ -5,7 +5,7 @@
import QtQuick 2.1
import QtQuick.Layouts 1.2
import org.kde.kirigami 2.8 as Kirigami
import org.kde.kirigami 2.10 as Kirigami
import Keysmith.Application 1.0
import Keysmith.Models 1.0 as Models
......@@ -136,6 +136,12 @@ Kirigami.ScrollablePage {
return null;
}
}
section {
property: "account.issuer"
delegate: Kirigami.ListSectionHeader {
text: section
}
}
}
actions.main: Kirigami.Action {
......
......@@ -17,7 +17,7 @@ Kirigami.Page {
title: i18nc("@title:window", "Add new account")
signal dismissed
property Models.AccountListModel accounts: Keysmith.accountListModel()
property bool acceptable: accountName.acceptableInput && tokenDetails.acceptable
property bool acceptable: accountName.acceptableInput && issuerName.acceptableInput && tokenDetails.acceptable
ColumnLayout {
anchors {
......@@ -28,7 +28,29 @@ Kirigami.Page {
id: accountName
Kirigami.FormData.label: i18nc("@label:textbox", "Account Name:")
validator: Validators.AccountNameValidator {
id: accountNameValidator
accounts: root.accounts
issuer: issuerName.text
}
}
Controls.TextField {
id: issuerName
Kirigami.FormData.label: i18nc("@label:textbox", "Account Issuer:")
validator: Validators.AccountIssuerValidator {}
/*
* When the issuer changes, the account name should be revalidated as well. It may have become eligible or in-eligible depending on
* whether or not other accounts with the same name for the same new issuer value already exist.
*
* Unfortunately because the property binding only affects the validator, there seems to be nothing to explicitly trigger revalidation
* on the text field. Work around is to force revalidation to happen by "editing" the value in the text field directly.
*/
onTextChanged: {
/*
* This signal handler may run before property bindings have been fully (re-)evaluated.
* First update the account name validator to the correct new issuer value before triggering revalidation.
*/
accountNameValidator.issuer = issuerName.text;
accountName.insert(accountName.text.length, "");
}
}
}
......@@ -43,10 +65,10 @@ Kirigami.Page {
enabled: acceptable
onTriggered: {
if (tokenDetails.isTotp) {
accounts.addTotp(accountName.text, tokenDetails.secret, parseInt(tokenDetails.timeStep), tokenDetails.tokenLength);
accounts.addTotp(accountName.text, issuerName.text, tokenDetails.secret, parseInt(tokenDetails.timeStep), tokenDetails.tokenLength);
}
if (tokenDetails.isHotp) {
accounts.addHotp(accountName.text, tokenDetails.secret, parseInt(tokenDetails.counter), tokenDetails.tokenLength);
accounts.addHotp(accountName.text, issuerName.text, tokenDetails.secret, parseInt(tokenDetails.counter), tokenDetails.tokenLength);
}
root.dismissed();
}
......
......@@ -15,6 +15,7 @@
#include "app/keysmith.h"
#include "model/accounts.h"
#include "validators/countervalidator.h"
#include "validators/issuervalidator.h"
#include "validators/secretvalidator.h"
Q_DECL_EXPORT int main(int argc, char *argv[])
......@@ -36,6 +37,7 @@ Q_DECL_EXPORT int main(int argc, char *argv[])
qmlRegisterUncreatableType<model::AccountView>("Keysmith.Models", 1, 0, "Account", "Use an AccountListModel from the Keysmith singleton to obtain an Account");
qmlRegisterType<model::SortedAccountsListModel>("Keysmith.Models", 1, 0, "SortedAccountListModel");
qmlRegisterType<model::AccountNameValidator>("Keysmith.Validators", 1, 0, "AccountNameValidator");
qmlRegisterType<validators::IssuerValidator>("Keysmith.Validators", 1, 0, "AccountIssuerValidator");
qmlRegisterType<validators::Base32Validator>("Keysmith.Validators", 1, 0, "Base32SecretValidator");
qmlRegisterType<validators::UnsignedLongValidator>("Keysmith.Validators", 1, 0, "HOTPCounterValidator");
qmlRegisterSingletonType<app::Keysmith>("Keysmith.Application", 1, 0, "Keysmith", [](QQmlEngine *qml, QJSEngine *js) -> QObject *
......
......@@ -57,6 +57,11 @@ namespace model
return m_model->name();
}
QString AccountView::issuer(void) const
{
return m_model->issuer();
}
QString AccountView::token(void) const
{
return m_model->token();
......@@ -131,14 +136,14 @@ namespace model
return m_storage->isLoaded();
}
void SimpleAccountListModel::addTotp(const QString &account, const QString &secret, uint timeStep, int tokenLength)
void SimpleAccountListModel::addTotp(const QString &account, const QString &issuer, const QString &secret, uint timeStep, int tokenLength)
{
m_storage->addTotp(account, secret, timeStep, tokenLength);
m_storage->addTotp(account, issuer, secret, timeStep, tokenLength);
}
void SimpleAccountListModel::addHotp(const QString &account, const QString &secret, quint64 counter, int tokenLength)
void SimpleAccountListModel::addHotp(const QString &account, const QString &issuer, const QString &secret, quint64 counter, int tokenLength)
{
m_storage->addHotp(account, secret, counter, tokenLength);
m_storage->addHotp