Implement encryption/decryption of token secrets

With this change token secrets are encrypted prior to writing them to
storage, and decrypted as and when needed to generate tokens. Additional
validation is performed to verify that token secrets can be decrypted
successfully when loading accounts from storage.

With this change issue #6 should finally be resolved.
master
Johan Ouwerkerk 2020-03-02 19:42:03 +01:00
parent a90c16cf82
commit 035348caa5
30 changed files with 491 additions and 206 deletions

View File

@ -13,6 +13,5 @@ Some todo items include,
- QR code scanning - QR code scanning
- Backup and Restore of accounts - Backup and Restore of accounts
- Encrypted storage of the secret token
This code is largely based on the [authenticator-ng](https://github.com/dobey/authenticator-ng) application by the Rodney Dawes and Michael Zanetti for the Ubuntu Touch. This code is largely based on the [authenticator-ng](https://github.com/dobey/authenticator-ng) application by the Rodney Dawes and Michael Zanetti for the Ubuntu Touch.

View File

@ -4,6 +4,7 @@
*/ */
#include "account/actions_p.h" #include "account/actions_p.h"
#include "../test-utils/secret.h"
#include "../test-utils/spy.h" #include "../test-utils/spy.h"
#include <QSignalSpy> #include <QSignalSpy>
@ -13,24 +14,32 @@ class ComputeHotpTest: public QObject
{ {
Q_OBJECT Q_OBJECT
private Q_SLOTS: private Q_SLOTS:
void initTestCase(void);
void testDefaults(void); void testDefaults(void);
void testDefaults_data(void); void testDefaults_data(void);
private:
accounts::AccountSecret m_secret;
}; };
/* // RFC test vector uses the key: 12345678901234567890
* RFC test vector uses the key: 12345678901234567890 static QByteArray rfcSecret("12345678901234567890");
* The secret value below is the bas32 encoded version of that
*/
static QLatin1String secret("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");
// the RFC test vector consists of 6-character tokens // the RFC test vector consists of 6-character tokens
static int tokenLength = 6; static int tokenLength = 6;
void ComputeHotpTest::initTestCase(void)
{
QVERIFY2(test::useDummyPassword(&m_secret), "should be able to set up the master key");
}
void ComputeHotpTest::testDefaults(void) void ComputeHotpTest::testDefaults(void)
{ {
QFETCH(quint64, counter); QFETCH(quint64, counter);
accounts::ComputeHotp uut(secret, counter, tokenLength); 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);
QSignalSpy tokenGenerated(&uut, &accounts::ComputeHotp::otp); QSignalSpy tokenGenerated(&uut, &accounts::ComputeHotp::otp);
QSignalSpy jobFinished(&uut, &accounts::ComputeHotp::finished); QSignalSpy jobFinished(&uut, &accounts::ComputeHotp::finished);

View File

@ -4,6 +4,7 @@
*/ */
#include "account/actions_p.h" #include "account/actions_p.h"
#include "../test-utils/secret.h"
#include "../test-utils/spy.h" #include "../test-utils/spy.h"
#include <QSignalSpy> #include <QSignalSpy>
@ -13,15 +14,15 @@ class ComputeTotpTest: public QObject
{ {
Q_OBJECT Q_OBJECT
private Q_SLOTS: private Q_SLOTS:
void initTestCase(void);
void testDefaults(void); void testDefaults(void);
void testDefaults_data(void); void testDefaults_data(void);
private:
accounts::AccountSecret m_secret;
}; };
/* // RFC test vector uses the key: 12345678901234567890
* RFC test vector uses the key: 12345678901234567890 static QByteArray rfcSecret("12345678901234567890");
* The secret value below is the bas32 encoded version of that
*/
static QLatin1String secret("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ");
// the RFC test vector consists of 6-character tokens // the RFC test vector consists of 6-character tokens
static int tokenLength = 6; static int tokenLength = 6;
@ -32,6 +33,11 @@ static uint timeStep = 30;
// the default TOTP epoch is the Unix epoch // the default TOTP epoch is the Unix epoch
static QDateTime epoch = QDateTime::fromMSecsSinceEpoch(0); static QDateTime epoch = QDateTime::fromMSecsSinceEpoch(0);
void ComputeTotpTest::initTestCase(void)
{
QVERIFY2(test::useDummyPassword(&m_secret), "should be able to set up the master key");
}
void ComputeTotpTest::testDefaults(void) void ComputeTotpTest::testDefaults(void)
{ {
QFETCH(qint64, counter); QFETCH(qint64, counter);
@ -39,7 +45,10 @@ void ComputeTotpTest::testDefaults(void)
return counter * timeStep * 1000; return counter * timeStep * 1000;
}); });
accounts::ComputeTotp uut(secret, epoch, timeStep, tokenLength, accounts::Account::Hash::Default, clock); 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);
QSignalSpy tokenGenerated(&uut, &accounts::ComputeTotp::otp); QSignalSpy tokenGenerated(&uut, &accounts::ComputeTotp::otp);
QSignalSpy jobFinished(&uut, &accounts::ComputeTotp::finished); QSignalSpy jobFinished(&uut, &accounts::ComputeTotp::finished);

View File

@ -3,7 +3,7 @@
# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> # SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
# #
set(Test_DEP_LIBS Qt5::Core Qt5::Test account_lib account_test_lib) set(Test_DEP_LIBS Qt5::Core Qt5::Test account_lib account_test_lib secrets_test_lib)
qt5_add_resources(RCC_SOURCES resources/resources.qrc) qt5_add_resources(RCC_SOURCES resources/resources.qrc)
ecm_add_test(load-accounts.cpp ${RCC_SOURCES} LINK_LIBRARIES ${Test_DEP_LIBS} TEST_NAME load-accounts NAME_PREFIX account-jobs-) ecm_add_test(load-accounts.cpp ${RCC_SOURCES} LINK_LIBRARIES ${Test_DEP_LIBS} TEST_NAME load-accounts NAME_PREFIX account-jobs-)

View File

@ -5,8 +5,11 @@
#include "account/actions_p.h" #include "account/actions_p.h"
#include "../test-utils/output.h" #include "../test-utils/output.h"
#include "../test-utils/secret.h"
#include "../test-utils/spy.h" #include "../test-utils/spy.h"
#include "../../secrets/test-utils/random.h"
#include <QSignalSpy> #include <QSignalSpy>
#include <QString> #include <QString>
#include <QTest> #include <QTest>
@ -23,23 +26,33 @@ private Q_SLOTS:
void initTestCase(void); void initTestCase(void);
void emptyAccountsFile(void); void emptyAccountsFile(void);
void sampleAccountsFile(void); void sampleAccountsFile(void);
private:
accounts::AccountSecret m_secret {&test::fakeRandom};
}; };
static QByteArray rawSecret = QByteArray::fromBase64(QByteArray("8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="), QByteArray::Base64Encoding);
static QByteArray rawNonce = QByteArray::fromBase64(QByteArray("QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB"), QByteArray::Base64Encoding);
void LoadAccountsTest::initTestCase(void) void LoadAccountsTest::initTestCase(void)
{ {
QVERIFY2(test::ensureOutputDirectory(), "output directory should be available"); QVERIFY2(test::ensureOutputDirectory(), "output directory should be available");
QVERIFY2(test::copyResource(":/load-accounts/empty-accounts.ini", emptyIniResource), "empty INI resource should be available as file"); QVERIFY2(test::copyResource(":/load-accounts/empty-accounts.ini", emptyIniResource), "empty INI resource should be available as file");
QVERIFY2(test::copyResource(":/load-accounts/sample-accounts.ini", corpusIniResource), "test corpus INI resource should be available as file"); QVERIFY2(test::copyResource(":/load-accounts/sample-accounts.ini", corpusIniResource), "test corpus INI resource should be available as file");
QVERIFY2(test::useDummyPassword(&m_secret), "should be able to set up the master key");
} }
void LoadAccountsTest::emptyAccountsFile(void) void LoadAccountsTest::emptyAccountsFile(void)
{ {
accounts::LoadAccounts uut([](const accounts::PersistenceAction &action) -> void bool actionRun = false;
const accounts::SettingsProvider settings([&actionRun](const accounts::PersistenceAction &action) -> void
{ {
QSettings data(test::path(emptyIniResource), QSettings::IniFormat); QSettings data(test::path(emptyIniResource), QSettings::IniFormat);
actionRun = true;
action(data); action(data);
}); });
accounts::LoadAccounts uut(settings, &m_secret);
QSignalSpy hotpFound(&uut, &accounts::LoadAccounts::foundHotp); QSignalSpy hotpFound(&uut, &accounts::LoadAccounts::foundHotp);
QSignalSpy totpFound(&uut, &accounts::LoadAccounts::foundTotp); QSignalSpy totpFound(&uut, &accounts::LoadAccounts::foundTotp);
QSignalSpy jobFinished(&uut, &accounts::LoadAccounts::finished); QSignalSpy jobFinished(&uut, &accounts::LoadAccounts::finished);
@ -47,18 +60,23 @@ void LoadAccountsTest::emptyAccountsFile(void)
uut.run(); uut.run();
QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished"); QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished");
QVERIFY2(actionRun, "accounts action should have run");
QCOMPARE(hotpFound.count(), 0); QCOMPARE(hotpFound.count(), 0);
QCOMPARE(totpFound.count(), 0); QCOMPARE(totpFound.count(), 0);
} }
void LoadAccountsTest::sampleAccountsFile(void) void LoadAccountsTest::sampleAccountsFile(void)
{ {
accounts::LoadAccounts uut([](const accounts::PersistenceAction &action) -> void bool actionRun = false;
const accounts::SettingsProvider settings([&actionRun](const accounts::PersistenceAction &action) -> void
{ {
QSettings data(test::path(corpusIniResource), QSettings::IniFormat); QSettings data(test::path(corpusIniResource), QSettings::IniFormat);
actionRun = true;
action(data); action(data);
}); });
accounts::LoadAccounts uut(settings, &m_secret);
QSignalSpy hotpFound(&uut, &accounts::LoadAccounts::foundHotp); QSignalSpy hotpFound(&uut, &accounts::LoadAccounts::foundHotp);
QSignalSpy totpFound(&uut, &accounts::LoadAccounts::foundTotp); QSignalSpy totpFound(&uut, &accounts::LoadAccounts::foundTotp);
QSignalSpy jobFinished(&uut, &accounts::LoadAccounts::finished); QSignalSpy jobFinished(&uut, &accounts::LoadAccounts::finished);
@ -66,22 +84,25 @@ void LoadAccountsTest::sampleAccountsFile(void)
uut.run(); uut.run();
QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished"); QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished");
QVERIFY2(actionRun, "accounts action should have run");
QCOMPARE(hotpFound.count(), 1); QCOMPARE(hotpFound.count(), 1);
QCOMPARE(totpFound.count(), 1); QCOMPARE(totpFound.count(), 1);
const auto firstHotp = hotpFound.at(0); const auto firstHotp = hotpFound.at(0);
QCOMPARE(firstHotp.at(0).toUuid(), QUuid(QLatin1String("072a645d-6c26-57cc-81eb-d9ef3b9b39e2"))); 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(1).toString(), QLatin1String("valid-hotp-sample-1"));
QCOMPARE(firstHotp.at(2).toString(), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ=")); QCOMPARE(firstHotp.at(2).toByteArray(), rawSecret);
QCOMPARE(firstHotp.at(3).toULongLong(), 0ULL); QCOMPARE(firstHotp.at(3).toByteArray(), rawNonce);
QCOMPARE(firstHotp.at(4).toInt(), 6); QCOMPARE(firstHotp.at(4).toULongLong(), 0ULL);
QCOMPARE(firstHotp.at(5).toInt(), 6);
const auto firstTotp = totpFound.at(0); const auto firstTotp = totpFound.at(0);
QCOMPARE(firstTotp.at(0).toUuid(), QUuid(QLatin1String("534cc72e-e9ec-5e39-a1ff-9f017c9be8cc"))); QCOMPARE(firstTotp.at(0).toUuid(), QUuid(QLatin1String("534cc72e-e9ec-5e39-a1ff-9f017c9be8cc")));
QCOMPARE(firstTotp.at(1).toString(), QLatin1String("valid-totp-sample-1")); QCOMPARE(firstTotp.at(1).toString(), QLatin1String("valid-totp-sample-1"));
QCOMPARE(firstTotp.at(2).toString(), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ=")); QCOMPARE(firstHotp.at(2).toByteArray(), rawSecret);
QCOMPARE(firstTotp.at(3).toUInt(), 30); QCOMPARE(firstHotp.at(3).toByteArray(), rawNonce);
QCOMPARE(firstTotp.at(4).toInt(), 6); QCOMPARE(firstTotp.at(4).toUInt(), 30);
QCOMPARE(firstTotp.at(5).toInt(), 6);
} }
QTEST_MAIN(LoadAccountsTest) QTEST_MAIN(LoadAccountsTest)

View File

@ -1,13 +1,15 @@
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D] [%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
account=valid-hotp-sample-1 account=valid-hotp-sample-1
counter=0 counter=0
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6 pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp type=hotp
secret="NBSWY3DPFQQHO33SNRSCCCQ="
[%7B534cc72e-e9ec-5e39-a1ff-9f017c9be8cc%7D] [%7B534cc72e-e9ec-5e39-a1ff-9f017c9be8cc%7D]
account=valid-totp-sample-1 account=valid-totp-sample-1
timeStep=30 nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6 pinLength=6
secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30
type=totp type=totp
secret="NBSWY3DPFQQHO33SNRSCCCQ="

View File

@ -1,6 +1,7 @@
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D] [%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
account=valid-hotp-sample-1 account=valid-hotp-sample-1
counter=0 counter=0
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6 pinLength=6
secret="NBSWY3DPFQQHO33SNRSCCCQ=" secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
type=hotp type=hotp

View File

@ -1,6 +1,7 @@
[%7B534cc72e-e9ec-5e39-a1ff-9f017c9be8cc%7D] [%7B534cc72e-e9ec-5e39-a1ff-9f017c9be8cc%7D]
account=valid-totp-sample-1 account=valid-totp-sample-1
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6 pinLength=6
secret="NBSWY3DPFQQHO33SNRSCCCQ=" secret="8juE9gJFLp3OgL4CxJ5v5q8sw+h7Vbn06+NY4uc="
timeStep=30 timeStep=30
type=totp type=totp

View File

@ -5,8 +5,11 @@
#include "account/actions_p.h" #include "account/actions_p.h"
#include "../test-utils/output.h" #include "../test-utils/output.h"
#include "../test-utils/secret.h"
#include "../test-utils/spy.h" #include "../test-utils/spy.h"
#include "../../secrets/test-utils/random.h"
#include <QFile> #include <QFile>
#include <QSignalSpy> #include <QSignalSpy>
#include <QString> #include <QString>
@ -23,27 +26,27 @@ private Q_SLOTS:
void validHotp_data(void); void validHotp_data(void);
void invalidHotp(void); void invalidHotp(void);
void invalidHotp_data(void); void invalidHotp_data(void);
private:
accounts::AccountSecret m_secret {&test::fakeRandom};
}; };
static void define_test_data(void) static void define_test_data(void)
{ {
QTest::addColumn<QUuid>("id"); QTest::addColumn<QUuid>("id");
QTest::addColumn<QString>("name"); QTest::addColumn<QString>("name");
QTest::addColumn<QString>("secret");
QTest::addColumn<quint64>("counter"); QTest::addColumn<quint64>("counter");
QTest::addColumn<int>("tokenLength"); QTest::addColumn<int>("tokenLength");
} }
static void define_test_case(const char * label, const QUuid &id, const QString &accountName, const QString &secret, quint64 counter, int tokenLength) static void define_test_case(const char * label, const QUuid &id, const QString &accountName, quint64 counter, int tokenLength)
{ {
QTest::newRow(label) << id << accountName << secret << counter << tokenLength; QTest::newRow(label) << id << accountName << counter << tokenLength;
} }
void SaveHotpTest::validHotp(void) void SaveHotpTest::validHotp(void)
{ {
QFETCH(QUuid, id); QFETCH(QUuid, id);
QFETCH(QString, name); QFETCH(QString, name);
QFETCH(QString, secret);
QFETCH(quint64, counter); QFETCH(quint64, counter);
QFETCH(int, tokenLength); QFETCH(int, tokenLength);
@ -58,7 +61,10 @@ void SaveHotpTest::validHotp(void)
action(data); action(data);
}); });
accounts::SaveHotp uut(settings, id, name, secret, counter, tokenLength); 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);
QSignalSpy invalidAccount(&uut, &accounts::SaveHotp::invalid); QSignalSpy invalidAccount(&uut, &accounts::SaveHotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveHotp::saved); QSignalSpy savedAccount(&uut, &accounts::SaveHotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveHotp::finished); QSignalSpy jobFinished(&uut, &accounts::SaveHotp::finished);
@ -84,7 +90,6 @@ void SaveHotpTest::invalidHotp(void)
{ {
QFETCH(QUuid, id); QFETCH(QUuid, id);
QFETCH(QString, name); QFETCH(QString, name);
QFETCH(QString, secret);
QFETCH(quint64, counter); QFETCH(quint64, counter);
QFETCH(int, tokenLength); QFETCH(int, tokenLength);
@ -99,7 +104,10 @@ void SaveHotpTest::invalidHotp(void)
action(data); action(data);
}); });
accounts::SaveHotp uut(settings, id, name, secret, counter, tokenLength); 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);
QSignalSpy invalidAccount(&uut, &accounts::SaveHotp::invalid); QSignalSpy invalidAccount(&uut, &accounts::SaveHotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveHotp::saved); QSignalSpy savedAccount(&uut, &accounts::SaveHotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveHotp::finished); QSignalSpy jobFinished(&uut, &accounts::SaveHotp::finished);
@ -121,24 +129,23 @@ void SaveHotpTest::invalidHotp(void)
void SaveHotpTest::validHotp_data(void) void SaveHotpTest::validHotp_data(void)
{ {
define_test_data(); define_test_data();
define_test_case("valid-hotp-sample-1", QUuid("072a645d-6c26-57cc-81eb-d9ef3b9b39e2"), QLatin1String("valid-hotp-sample-1"), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 0, 6); define_test_case("valid-hotp-sample-1", QUuid("072a645d-6c26-57cc-81eb-d9ef3b9b39e2"), QLatin1String("valid-hotp-sample-1"), 0, 6);
} }
void SaveHotpTest::invalidHotp_data(void) void SaveHotpTest::invalidHotp_data(void)
{ {
define_test_data(); define_test_data();
define_test_case("null UUID", QUuid(), QLatin1String("null UUID"), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 0, 6); define_test_case("null UUID", QUuid(), QLatin1String("null UUID"), 0, 6);
define_test_case("null account name", QUuid("00611bbf-5e0b-5c6a-9847-ad865315ce86"), QString(), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 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(""), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 0, 6); define_test_case("empty account name", QUuid("1e42b907-99d8-5da3-a59b-89b257e49c83"), QLatin1String(""), 0, 6);
define_test_case("null secret", QUuid("6e5ba95c-984d-538c-844e-f9edc1341bd2"), QLatin1String("null secret"), QString(), 0, 6); define_test_case("tokenLength too small", QUuid("bca12e13-4b5b-5e4e-b162-3b86a6284dea"), QLatin1String("tokenLength too small"), 0, 5);
define_test_case("empty secret", QUuid("fe68a65e-287e-5dcd-909b-1837d7ab94ee"), QLatin1String("empty secret"), QLatin1String(""), 0, 6); define_test_case("tokenLength too large", QUuid("5c10d530-fb22-5438-848d-3d4d1f738610"), QLatin1String("tokenLength too large"), 0, 11);
define_test_case("tokenLength too small", QUuid("bca12e13-4b5b-5e4e-b162-3b86a6284dea"), QLatin1String("tokenLength too small"), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 0, 5);
define_test_case("tokenLength too large", QUuid("5c10d530-fb22-5438-848d-3d4d1f738610"), QLatin1String("tokenLength too large"), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 0, 11);
} }
void SaveHotpTest::initTestCase(void) void SaveHotpTest::initTestCase(void)
{ {
QVERIFY2(test::ensureOutputDirectory(), "output directory should be available"); QVERIFY2(test::ensureOutputDirectory(), "output directory should be available");
QVERIFY2(test::useDummyPassword(&m_secret), "should be able to set up the master key");
} }
QTEST_MAIN(SaveHotpTest) QTEST_MAIN(SaveHotpTest)

View File

@ -5,8 +5,11 @@
#include "account/actions_p.h" #include "account/actions_p.h"
#include "../test-utils/output.h" #include "../test-utils/output.h"
#include "../test-utils/secret.h"
#include "../test-utils/spy.h" #include "../test-utils/spy.h"
#include "../../secrets/test-utils/random.h"
#include <QFile> #include <QFile>
#include <QSignalSpy> #include <QSignalSpy>
#include <QString> #include <QString>
@ -23,27 +26,27 @@ private Q_SLOTS:
void validHotp_data(void); void validHotp_data(void);
void invalidHotp(void); void invalidHotp(void);
void invalidHotp_data(void); void invalidHotp_data(void);
private:
accounts::AccountSecret m_secret {&test::fakeRandom};
}; };
static void define_test_data(void) static void define_test_data(void)
{ {
QTest::addColumn<QUuid>("id"); QTest::addColumn<QUuid>("id");
QTest::addColumn<QString>("name"); QTest::addColumn<QString>("name");
QTest::addColumn<QString>("secret");
QTest::addColumn<uint>("timeStep"); QTest::addColumn<uint>("timeStep");
QTest::addColumn<int>("tokenLength"); QTest::addColumn<int>("tokenLength");
} }
static void define_test_case(const char * label, const QUuid &id, const QString &accountName, const QString &secret, uint timeStep, int tokenLength) static void define_test_case(const char * label, const QUuid &id, const QString &accountName, uint timeStep, int tokenLength)
{ {
QTest::newRow(label) << id << accountName << secret << timeStep << tokenLength; QTest::newRow(label) << id << accountName << timeStep << tokenLength;
} }
void SaveTotpTest::validHotp(void) void SaveTotpTest::validHotp(void)
{ {
QFETCH(QUuid, id); QFETCH(QUuid, id);
QFETCH(QString, name); QFETCH(QString, name);
QFETCH(QString, secret);
QFETCH(uint, timeStep); QFETCH(uint, timeStep);
QFETCH(int, tokenLength); QFETCH(int, tokenLength);
@ -58,7 +61,10 @@ void SaveTotpTest::validHotp(void)
action(data); action(data);
}); });
accounts::SaveTotp uut(settings, id, name, secret, timeStep, tokenLength); 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);
QSignalSpy invalidAccount(&uut, &accounts::SaveTotp::invalid); QSignalSpy invalidAccount(&uut, &accounts::SaveTotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveTotp::saved); QSignalSpy savedAccount(&uut, &accounts::SaveTotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveTotp::finished); QSignalSpy jobFinished(&uut, &accounts::SaveTotp::finished);
@ -84,7 +90,6 @@ void SaveTotpTest::invalidHotp(void)
{ {
QFETCH(QUuid, id); QFETCH(QUuid, id);
QFETCH(QString, name); QFETCH(QString, name);
QFETCH(QString, secret);
QFETCH(uint, timeStep); QFETCH(uint, timeStep);
QFETCH(int, tokenLength); QFETCH(int, tokenLength);
@ -99,7 +104,10 @@ void SaveTotpTest::invalidHotp(void)
action(data); action(data);
}); });
accounts::SaveTotp uut(settings, id, name, secret, timeStep, tokenLength); 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);
QSignalSpy invalidAccount(&uut, &accounts::SaveTotp::invalid); QSignalSpy invalidAccount(&uut, &accounts::SaveTotp::invalid);
QSignalSpy savedAccount(&uut, &accounts::SaveTotp::saved); QSignalSpy savedAccount(&uut, &accounts::SaveTotp::saved);
QSignalSpy jobFinished(&uut, &accounts::SaveTotp::finished); QSignalSpy jobFinished(&uut, &accounts::SaveTotp::finished);
@ -121,25 +129,24 @@ void SaveTotpTest::invalidHotp(void)
void SaveTotpTest::validHotp_data(void) void SaveTotpTest::validHotp_data(void)
{ {
define_test_data(); define_test_data();
define_test_case("valid-totp-sample-1", QUuid("534cc72e-e9ec-5e39-a1ff-9f017c9be8cc"), QLatin1String("valid-totp-sample-1"), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 30, 6); define_test_case("valid-totp-sample-1", QUuid("534cc72e-e9ec-5e39-a1ff-9f017c9be8cc"), QLatin1String("valid-totp-sample-1"), 30, 6);
} }
void SaveTotpTest::invalidHotp_data(void) void SaveTotpTest::invalidHotp_data(void)
{ {
define_test_data(); define_test_data();
define_test_case("null UUID", QUuid(), QLatin1String("null UUID"), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 30, 6); define_test_case("null UUID", QUuid(), QLatin1String("null UUID"), 30, 6);
define_test_case("null account name", QUuid("00611bbf-5e0b-5c6a-9847-ad865315ce86"), QString(), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 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(""), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 30, 6); define_test_case("empty account name", QUuid("1e42b907-99d8-5da3-a59b-89b257e49c83"), QLatin1String(""), 30, 6);
define_test_case("null secret", QUuid("6e5ba95c-984d-538c-844e-f9edc1341bd2"), QLatin1String("null secret"), QString(), 30, 6); define_test_case("timeStep too small", QUuid("5ab8749b-f973-5f48-a70e-c261ebd0521a"), QLatin1String("timeStep too small"), 0, 6);
define_test_case("empty secret", QUuid("fe68a65e-287e-5dcd-909b-1837d7ab94ee"), QLatin1String("empty secret"), QLatin1String(""), 30, 6); define_test_case("tokenLength too small", QUuid("bca12e13-4b5b-5e4e-b162-3b86a6284dea"), QLatin1String("tokenLength too small"), 30, 5);
define_test_case("timeStep too small", QUuid("5ab8749b-f973-5f48-a70e-c261ebd0521a"), QLatin1String("timeStep too small"), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 0, 6); define_test_case("tokenLength too large", QUuid("5c10d530-fb22-5438-848d-3d4d1f738610"), QLatin1String("tokenLength too large"), 30, 11);
define_test_case("tokenLength too small", QUuid("bca12e13-4b5b-5e4e-b162-3b86a6284dea"), QLatin1String("tokenLength too small"), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 30, 5);
define_test_case("tokenLength too large", QUuid("5c10d530-fb22-5438-848d-3d4d1f738610"), QLatin1String("tokenLength too large"), QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 30, 11);
} }
void SaveTotpTest::initTestCase(void) void SaveTotpTest::initTestCase(void)
{ {
QVERIFY2(test::ensureOutputDirectory(), "output directory should be available"); QVERIFY2(test::ensureOutputDirectory(), "output directory should be available");
QVERIFY2(test::useDummyPassword(&m_secret), "should be able to set up the master key");
} }
QTEST_MAIN(SaveTotpTest) QTEST_MAIN(SaveTotpTest)

View File

@ -3,7 +3,7 @@
# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com> # SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
# #
set(Test_DEP_LIBS Qt5::Core Qt5::Test account_lib account_test_lib) set(Test_DEP_LIBS Qt5::Core Qt5::Test account_lib account_test_lib secrets_test_lib)
qt5_add_resources(RCC_SOURCES resources/resources.qrc) qt5_add_resources(RCC_SOURCES resources/resources.qrc)
ecm_add_test(storage-object-lifecycles.cpp ${RCC_SOURCES} LINK_LIBRARIES ${Test_DEP_LIBS} TEST_NAME storage-object-lifecycles NAME_PREFIX account-) ecm_add_test(storage-object-lifecycles.cpp ${RCC_SOURCES} LINK_LIBRARIES ${Test_DEP_LIBS} TEST_NAME storage-object-lifecycles NAME_PREFIX account-)

View File

@ -7,6 +7,8 @@
#include "../test-utils/output.h" #include "../test-utils/output.h"
#include "../test-utils/spy.h" #include "../test-utils/spy.h"
#include "../../secrets/test-utils/random.h"
#include <QDateTime> #include <QDateTime>
#include <QFile> #include <QFile>
#include <QSignalSpy> #include <QSignalSpy>
@ -53,13 +55,7 @@ void HotpCounterUpdateTest::testCounterUpdate(void)
thread->start(); thread->start();
QVERIFY2(test::signal_eventually_emitted_once(threadStarted), "worker thread should be running by now"); QVERIFY2(test::signal_eventually_emitted_once(threadStarted), "worker thread should be running by now");
accounts::AccountStorage *uut = new accounts::AccountStorage(settings, thread); accounts::AccountSecret *secret = new accounts::AccountSecret(&test::fakeRandom);
QSignalSpy accountAdded(uut, &accounts::AccountStorage::added);
QSignalSpy accountRemoved(uut, &accounts::AccountStorage::removed);
QSignalSpy storageDisposed(uut, &accounts::AccountStorage::disposed);
QSignalSpy storageCleaned(uut, &accounts::AccountStorage::destroyed);
accounts::AccountSecret *secret = uut->secret();
QSignalSpy existingPasswordNeeded(secret, &accounts::AccountSecret::existingPasswordNeeded); QSignalSpy existingPasswordNeeded(secret, &accounts::AccountSecret::existingPasswordNeeded);
QSignalSpy newPasswordNeeded(secret, &accounts::AccountSecret::newPasswordNeeded); QSignalSpy newPasswordNeeded(secret, &accounts::AccountSecret::newPasswordNeeded);
QSignalSpy passwordAvailable(secret, &accounts::AccountSecret::passwordAvailable); QSignalSpy passwordAvailable(secret, &accounts::AccountSecret::passwordAvailable);
@ -67,6 +63,12 @@ void HotpCounterUpdateTest::testCounterUpdate(void)
QSignalSpy passwordRequestsCancelled(secret, &accounts::AccountSecret::requestsCancelled); QSignalSpy passwordRequestsCancelled(secret, &accounts::AccountSecret::requestsCancelled);
QSignalSpy secretCleaned(secret, &accounts::AccountSecret::destroyed); QSignalSpy secretCleaned(secret, &accounts::AccountSecret::destroyed);
accounts::AccountStorage *uut = new accounts::AccountStorage(settings, thread, secret);
QSignalSpy accountAdded(uut, &accounts::AccountStorage::added);
QSignalSpy accountRemoved(uut, &accounts::AccountStorage::removed);
QSignalSpy storageDisposed(uut, &accounts::AccountStorage::disposed);
QSignalSpy storageCleaned(uut, &accounts::AccountStorage::destroyed);
// first phase: check that account objects can be loaded from storage // first phase: check that account objects can be loaded from storage
// expect that unlocking is scheduled automatically, so advancing the event loop should trigger the signal // expect that unlocking is scheduled automatically, so advancing the event loop should trigger the signal

View File

@ -8,6 +8,7 @@ salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D] [%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
account=valid-hotp-sample-1 account=valid-hotp-sample-1
counter=1 counter=1
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6 pinLength=6
secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ secret=/8zRRlAF80eHEtU/ZJEFbR4nIeuMVs4YBRHvxRSod8iQculp
type=hotp type=hotp

View File

@ -8,6 +8,7 @@ salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D] [%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
account=valid-hotp-sample-1 account=valid-hotp-sample-1
counter=0 counter=0
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=6 pinLength=6
secret=GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ secret=/8zRRlAF80eHEtU/ZJEFbR4nIeuMVs4YBRHvxRSod8iQculp
type=hotp type=hotp

View File

@ -7,7 +7,8 @@ salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
[%7B534cc72e-e9ec-5e39-a1ff-9f017c9be8cc%7D] [%7B534cc72e-e9ec-5e39-a1ff-9f017c9be8cc%7D]
account=valid-totp-sample-1 account=valid-totp-sample-1
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=8 pinLength=8
secret="NBSWY3DPFQQHO33SNRSCCCQ=" secret="LXM8veM3T1qY/gAYsTGZNEdwfrPWTNlXU1OykwY="
timeStep=42 timeStep=42
type=totp type=totp

View File

@ -8,6 +8,7 @@ salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D] [%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
account=valid-hotp-sample-1 account=valid-hotp-sample-1
counter=42 counter=42
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
pinLength=7 pinLength=7
secret="NBSWY3DPFQQHO33SNRSCCCQ=" secret="LXM8veM3T1qY/gAYsTGZNEdwfrPWTNlXU1OykwY="
type=hotp type=hotp

View File

@ -7,6 +7,8 @@
#include "../test-utils/output.h" #include "../test-utils/output.h"
#include "../test-utils/spy.h" #include "../test-utils/spy.h"
#include "../../secrets/test-utils/random.h"
#include <QDateTime> #include <QDateTime>
#include <QFile> #include <QFile>
#include <QSignalSpy> #include <QSignalSpy>
@ -15,6 +17,8 @@
#include <QVector> #include <QVector>
#include <QtDebug> #include <QtDebug>
#include <string.h>
static QString testIniResource(QLatin1String("test.ini")); static QString testIniResource(QLatin1String("test.ini"));
static QString testIniLockFile(QLatin1String("test.ini.lock")); static QString testIniLockFile(QLatin1String("test.ini.lock"));
@ -51,13 +55,7 @@ void StorageLifeCyclesTest::testLifecycle(void)
thread->start(); thread->start();
QVERIFY2(test::signal_eventually_emitted_once(threadStarted), "worker thread should be running by now"); QVERIFY2(test::signal_eventually_emitted_once(threadStarted), "worker thread should be running by now");
accounts::AccountStorage *uut = new accounts::AccountStorage(settings, thread); accounts::AccountSecret *secret = new accounts::AccountSecret(&test::fakeRandom);
QSignalSpy accountAdded(uut, &accounts::AccountStorage::added);
QSignalSpy accountRemoved(uut, &accounts::AccountStorage::removed);
QSignalSpy storageDisposed(uut, &accounts::AccountStorage::disposed);
QSignalSpy storageCleaned(uut, &accounts::AccountStorage::destroyed);
accounts::AccountSecret *secret = uut->secret();
QSignalSpy existingPasswordNeeded(secret, &accounts::AccountSecret::existingPasswordNeeded); QSignalSpy existingPasswordNeeded(secret, &accounts::AccountSecret::existingPasswordNeeded);
QSignalSpy newPasswordNeeded(secret, &accounts::AccountSecret::newPasswordNeeded); QSignalSpy newPasswordNeeded(secret, &accounts::AccountSecret::newPasswordNeeded);
QSignalSpy passwordAvailable(secret, &accounts::AccountSecret::passwordAvailable); QSignalSpy passwordAvailable(secret, &accounts::AccountSecret::passwordAvailable);
@ -65,6 +63,12 @@ void StorageLifeCyclesTest::testLifecycle(void)
QSignalSpy passwordRequestsCancelled(secret, &accounts::AccountSecret::requestsCancelled); QSignalSpy passwordRequestsCancelled(secret, &accounts::AccountSecret::requestsCancelled);
QSignalSpy secretCleaned(secret, &accounts::AccountSecret::destroyed); QSignalSpy secretCleaned(secret, &accounts::AccountSecret::destroyed);
accounts::AccountStorage *uut = new accounts::AccountStorage(settings, thread, secret);
QSignalSpy accountAdded(uut, &accounts::AccountStorage::added);
QSignalSpy accountRemoved(uut, &accounts::AccountStorage::removed);
QSignalSpy storageDisposed(uut, &accounts::AccountStorage::disposed);
QSignalSpy storageCleaned(uut, &accounts::AccountStorage::destroyed);
// first phase: check that account objects can be loaded from storage // first phase: check that account objects can be loaded from storage
QCOMPARE(accountAdded.count(), 0); QCOMPARE(accountAdded.count(), 0);
QVERIFY2(uut->isNameStillAvailable(initialAccountName), "sample account name should still be available"); QVERIFY2(uut->isNameStillAvailable(initialAccountName), "sample account name should still be available");
@ -141,7 +145,7 @@ void StorageLifeCyclesTest::testLifecycle(void)
QVERIFY2(test::signal_eventually_emitted_once(initialAccountCleaned), "sample account should be cleaned up by now"); 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 // third phase: check that new account objects can be added to storage
uut->addTotp(addedAccountName, QLatin1String("NBSWY3DPFQQHO33SNRSCCCQ="), 42, 8); uut->addTotp(addedAccountName, QLatin1String("NBSWY3DPFQQHO33SNRSCC==="), 42, 8);
QVERIFY2(test::signal_eventually_emitted_twice(accountAdded), "new account should be added to storage by now"); QVERIFY2(test::signal_eventually_emitted_twice(accountAdded), "new account should be added to storage by now");
QCOMPARE(accountAdded.at(1).at(0), addedAccountName); QCOMPARE(accountAdded.at(1).at(0), addedAccountName);

View File

@ -6,8 +6,9 @@
set(account_test_lib_SRCS set(account_test_lib_SRCS
job.cpp job.cpp
output.cpp output.cpp
secret.cpp
spy.cpp spy.cpp
) )
add_library(account_test_lib STATIC ${account_test_lib_SRCS}) add_library(account_test_lib STATIC ${account_test_lib_SRCS})
target_link_libraries(account_test_lib Qt5::Core Qt5::Test) target_link_libraries(account_test_lib Qt5::Core Qt5::Test account_lib)

View File

@ -0,0 +1,72 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#include "secret.h"
#include <QScopedPointer>
#include <QtDebug>
#include <string.h>
namespace test
{
secrets::SecureMasterKey * useDummyPassword(accounts::AccountSecret *secret)
{
QByteArray salt;
salt.resize(crypto_pwhash_SALTBYTES);
salt.fill('\x0', -1);
QString password(QLatin1String("password"));
return useDummyPassword(secret, password, salt);
}
secrets::SecureMasterKey * useDummyPassword(accounts::AccountSecret *secret, QString &password, QByteArray &salt)
{
if (!secret) {
qDebug () << "No account secret provided...";
return nullptr;
}
std::optional<secrets::KeyDerivationParameters> keyParams = secrets::KeyDerivationParameters::create(
crypto_secretbox_KEYBYTES, crypto_pwhash_ALG_DEFAULT, crypto_pwhash_MEMLIMIT_MIN, crypto_pwhash_OPSLIMIT_MIN
);
if (!keyParams) {
qDebug () << "Failed to construct key derivation parameters";
return nullptr;
}
if (!secret->requestExistingPassword(salt, *keyParams)) {
qDebug() << "Failed to simulate password request";
return nullptr;
}
if (!secret->answerExistingPassword(password)) {
qDebug() << "Failed to supply the password";
return nullptr;
}
secrets::SecureMasterKey * k = secret->deriveKey();
if (!k) {
qDebug() << "Failed to derive the master key";
return nullptr;
}
return k;
}
std::optional<secrets::EncryptedSecret> encrypt(const accounts::AccountSecret *secret, const QByteArray &tokenSecret)
{
QScopedPointer<secrets::SecureMemory> memory(secrets::SecureMemory::allocate((size_t) tokenSecret.size()));
if (!memory) {
qDebug () << "Failed to set up secure memory region for token secret";
return std::nullopt;
}
memcpy(memory->data(), tokenSecret.constData(), memory->size());
std::optional<secrets::EncryptedSecret> s = secret->encrypt(memory.data());
if (!s) {
qDebug () << "Failed to encrypt token secret";
return std::nullopt;
}
return s;
}
}

View File

@ -0,0 +1,24 @@
/*
* SPDX-License-Identifier: GPL-3.0-or-later
* SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
*/
#ifndef ACCOUNTS_TEST_UTIL_ACCOUNT_SECRET_H
#define ACCOUNTS_TEST_UTIL_ACCOUNT_SECRET_H
#include "account/keys.h"
#include "secrets/secrets.h"
#include <QByteArray>
#include <QString>
#include <optional>
namespace test
{
secrets::SecureMasterKey * useDummyPassword(accounts::AccountSecret *secret);
secrets::SecureMasterKey * useDummyPassword(accounts::AccountSecret *secret, QString &password, QByteArray &salt);
std::optional<secrets::EncryptedSecret> encrypt(const accounts::AccountSecret *secret, const QByteArray &tokenSecret);
}
#endif

View File

@ -206,7 +206,7 @@ namespace accounts
return d->activeAccounts(); return d->activeAccounts();
} }
void AccountStorage::handleHotp(const QUuid id, const QString name, const QString secret, quint64 counter, int tokenLength) void AccountStorage::handleHotp(const QUuid id, const QString name, const QByteArray secret, const QByteArray nonce, quint64 counter, int tokenLength)
{ {
Q_D(AccountStorage); Q_D(AccountStorage);
if (!d->isStillOpen()) { if (!d->isStillOpen()) {
@ -223,13 +223,21 @@ namespace accounts
return; return;
} }
Account *accepted = d->acceptHotpAccount(id, name, secret, counter, tokenLength); std::optional<secrets::EncryptedSecret> encryptedSecret = secrets::EncryptedSecret::from(secret, nonce);
if (!encryptedSecret) {
qCDebug(logger)
<< "Not handling HOTP account:" << id
<< "Invalid encrypted secret/nonce";
return;
}
Account *accepted = d->acceptHotpAccount(id, name, *encryptedSecret, counter, tokenLength);
QObject::connect(accepted, &Account::removed, this, &AccountStorage::accountRemoved); QObject::connect(accepted, &Account::removed, this, &AccountStorage::accountRemoved);
Q_EMIT added(name); Q_EMIT added(name);
} }
void AccountStorage::handleTotp(const QUuid id, const QString name, const QString secret, uint timeStep, int tokenLength) void AccountStorage::handleTotp(const QUuid id, const QString name, const QByteArray secret, const QByteArray nonce, uint timeStep, int tokenLength)
{ {
Q_D(AccountStorage); Q_D(AccountStorage);
if (!d->isStillOpen()) { if (!d->isStillOpen()) {
@ -246,7 +254,15 @@ namespace accounts
return; return;
} }
Account *accepted = d->acceptTotpAccount(id, name, secret, timeStep, tokenLength); std::optional<secrets::EncryptedSecret> encryptedSecret = secrets::EncryptedSecret::from(secret, nonce);
if (!encryptedSecret) {
qCDebug(logger)
<< "Not handling TOTP account:" << id
<< "Invalid encrypted secret/nonce";
return;
}
Account *accepted = d->acceptTotpAccount(id, name, *encryptedSecret, timeStep, tokenLength);
QObject::connect(accepted, &Account::removed, this, &AccountStorage::accountRemoved); QObject::connect(accepted, &Account::removed, this, &AccountStorage::accountRemoved);
Q_EMIT added(name); Q_EMIT added(name);

View File

@ -96,8 +96,8 @@ namespace accounts
void load(void); void load(void);
void accountRemoved(void); void accountRemoved(void);
void handleDisposal(void); void handleDisposal(void);
void handleHotp(const QUuid id, const QString name, const QString secret, quint64 counter, int tokenLength); 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 QString secret, uint timeStep, int tokenLength); void handleTotp(const QUuid id, const QString name, const QByteArray secret, const QByteArray nonce, uint timeStep, int tokenLength);
private: private:
QScopedPointer<AccountStoragePrivate> m_dptr; QScopedPointer<AccountStoragePrivate> m_dptr;
Q_DECLARE_PRIVATE_D(m_dptr, AccountStorage) Q_DECLARE_PRIVATE_D(m_dptr, AccountStorage)

View File

@ -174,13 +174,13 @@ namespace accounts
switch (m_algorithm) { switch (m_algorithm) {
case Account::Algorithm::Hotp: case Account::Algorithm::Hotp:
hotpJob = new ComputeHotp(m_secret, m_counter, m_tokenLength, m_offset, m_checksum); hotpJob = new ComputeHotp(m_storage->secret(), m_secret, m_counter, m_tokenLength, m_offset, m_checksum);
m_actions->queueAndProceed(hotpJob, [hotpJob, q, this](void) -> void { m_actions->queueAndProceed(hotpJob, [hotpJob, q, this](void) -> void {
new HandleTokenUpdate(this, hotpJob, q); new HandleTokenUpdate(this, hotpJob, q);
}); });
break; break;
case Account::Algorithm::Totp: case Account::Algorithm::Totp:
totpJob = new ComputeTotp(m_secret, m_epoch, m_timeStep, m_tokenLength, m_hash); totpJob = new ComputeTotp(m_storage->secret(), m_secret, m_epoch, m_timeStep, m_tokenLength, m_hash);
m_actions->queueAndProceed(totpJob, [totpJob, q, this](void) -> void m_actions->queueAndProceed(totpJob, [totpJob, q, this](void) -> void
{ {
new HandleTokenUpdate(this, totpJob, q); new HandleTokenUpdate(this, totpJob, q);
@ -224,7 +224,7 @@ namespace accounts
} }
AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account, AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id, AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account, AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const QString &secret, quint64 counter, int tokenLength, int offset, bool addChecksum) : const QString &name, 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()), 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_secret(secret), m_tokenLength(tokenLength),
m_counter(counter), m_offset(offset), m_checksum(addChecksum), m_counter(counter), m_offset(offset), m_checksum(addChecksum),
@ -233,7 +233,7 @@ namespace accounts
} }
AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account, AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id, AccountPrivate::AccountPrivate(const std::function<Account*(AccountPrivate*)> &account, AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const QString &secret, const QDateTime &epoch, uint timeStep, int tokenLength, Account::Hash hash) : const QString &name, 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()), 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_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_counter(0), m_offset(-1), m_checksum(false), // not a hotp token so these values don't really matter
@ -427,6 +427,32 @@ namespace accounts
return attempt; return attempt;
} }
std::optional<secrets::EncryptedSecret> AccountStoragePrivate::encrypt(const QString &secret) const
{
if (!m_is_still_open) {
qCDebug(logger) << "Will not encrypt account secret: storage no longer open";
return std::nullopt;
}
if (!m_secret || !m_secret->key()) {
qCDebug(logger) << "Will not encrypt account secret: encryption key not available";
return std::nullopt;
}
QScopedPointer<secrets::SecureMemory> decoded(secrets::decodeBase32(secret));
if (!decoded) {
qCDebug(logger) << "Will not encrypt account secret: failed to decode base32";
return std::nullopt;
}
return m_secret->encrypt(decoded.data());
}
bool AccountStoragePrivate::validateGenericNewToken(const QString &name, const QString &secret, int tokenLength) const
{
return checkTokenLength(tokenLength) && checkName(name) && isNameStillAvailable(name) && checkSecret(secret);
}
void AccountStoragePrivate::addHotp(const std::function<void(SaveHotp*)> &handler, const QString &name, const QString &secret, quint64 counter, int tokenLength, int offset, bool addChecksum) void AccountStoragePrivate::addHotp(const std::function<void(SaveHotp*)> &handler, const QString &name, const QString &secret, quint64 counter, int tokenLength, int offset, bool addChecksum)
{ {
Q_UNUSED(offset); Q_UNUSED(offset);
@ -435,21 +461,23 @@ namespace accounts
qCDebug(logger) << "Will not add new HOTP account: storage no longer open"; qCDebug(logger) << "Will not add new HOTP account: storage no longer open";
return; return;
} }
if (!isNameStillAvailable(name)) {
qCDebug(logger) << "Will not add new HOTP account: account name not available";
return;
}
QUuid id = generateId(name); if (!validateGenericNewToken(name, secret, tokenLength)) {
if (!checkHotpAccount(id, name, secret, tokenLength)) {
qCDebug(logger) << "Will not add new HOTP account: invalid account details"; qCDebug(logger) << "Will not add new HOTP account: invalid account details";
return; return;
} }
std::optional<secrets::EncryptedSecret> encryptedSecret = encrypt(secret);
if (!encryptedSecret) {
qCDebug(logger) << "Will not add new HOTP account: failed to encrypt secret";
return;
}
QUuid id = generateId(name);
qCDebug(logger) << "Requesting to store details for new HOTP account:" << id; qCDebug(logger) << "Requesting to store details for new HOTP account:" << id;
m_ids.insert(id); m_ids.insert(id);
SaveHotp *job = new SaveHotp(m_settings, id, name, secret, counter, tokenLength); SaveHotp *job = new SaveHotp(m_settings, id, name, *encryptedSecret, counter, tokenLength);
m_actions->queueAndProceed(job, [job, &handler](void) -> void m_actions->queueAndProceed(job, [job, &handler](void) -> void
{ {
handler(job); handler(job);
@ -464,21 +492,23 @@ namespace accounts
qCDebug(logger) << "Will not add new TOTP account: storage no longer open"; qCDebug(logger) << "Will not add new TOTP account: storage no longer open";
return; return;
} }
if (!isNameStillAvailable(name)) {
qCDebug(logger) << "Will not add new TOTP account: account name not available";
return;
}
QUuid id = generateId(name); if (!validateGenericNewToken(name, secret, tokenLength) || !checkTimeStep(timeStep)) {
if (!checkTotpAccount(id, name, secret, tokenLength, timeStep)) {
qCDebug(logger) << "Will not add new TOTP account: invalid account details"; qCDebug(logger) << "Will not add new TOTP account: invalid account details";
return; return;
} }
std::optional<secrets::EncryptedSecret> encryptedSecret = encrypt(secret);
if (!encryptedSecret) {
qCDebug(logger) << "Will not add new TOTP account: failed to encrypt secret";
return;
}
QUuid id = generateId(name);
qCDebug(logger) << "Requesting to store details for new TOTP account:" << id; qCDebug(logger) << "Requesting to store details for new TOTP account:" << id;
m_ids.insert(id); m_ids.insert(id);
SaveTotp *job = new SaveTotp(m_settings, id, name, secret, timeStep, tokenLength); SaveTotp *job = new SaveTotp(m_settings, id, name, *encryptedSecret, timeStep, tokenLength);
m_actions->queueAndProceed(job, [job, &handler](void) -> void m_actions->queueAndProceed(job, [job, &handler](void) -> void
{ {
handler(job); handler(job);
@ -507,14 +537,14 @@ namespace accounts
return; return;
} }
LoadAccounts *job = new LoadAccounts(m_settings); LoadAccounts *job = new LoadAccounts(m_settings, m_secret);
m_actions->queueAndProceed(job, [job, &handler](void) -> void m_actions->queueAndProceed(job, [job, &handler](void) -> void
{ {
handler(job); handler(job);
}); });
} }
Account * AccountStoragePrivate::acceptHotpAccount(const QUuid &id, const QString &name, const QString &secret, quint64 counter, int tokenLength, int offset, bool addChecksum) Account * AccountStoragePrivate::acceptHotpAccount(const QUuid &id, const QString &name, const secrets::EncryptedSecret &secret, quint64 counter, int tokenLength, int offset, bool addChecksum)
{ {
Q_Q(AccountStorage); Q_Q(AccountStorage);
qCDebug(logger) << "Registering HOTP account:" << id; qCDebug(logger) << "Registering HOTP account:" << id;
@ -532,7 +562,7 @@ namespace accounts
return m_accounts[id]; return m_accounts[id];
} }
Account * AccountStoragePrivate::acceptTotpAccount(const QUuid &id, const QString &name, const QString &secret, uint timeStep, int tokenLength, const QDateTime &epoch, Account::Hash hash) Account * AccountStoragePrivate::acceptTotpAccount(const QUuid &id, const QString &name, const secrets::EncryptedSecret &secret, uint timeStep, int tokenLength, const QDateTime &epoch, Account::Hash hash)
{ {
Q_Q(AccountStorage); Q_Q(AccountStorage);
qCDebug(logger) << "Registering TOTP account:" << id; qCDebug(logger) << "Registering TOTP account:" << id;

View File

@ -39,11 +39,11 @@ namespace accounts
public: public:
explicit AccountPrivate(const std::function<Account*(AccountPrivate*)> &account, explicit AccountPrivate(const std::function<Account*(AccountPrivate*)> &account,
AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id, AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const QString &secret, const QString &name, const secrets::EncryptedSecret &secret,
quint64 counter, int tokenLength, int offset, bool addChecksum); quint64 counter, int tokenLength, int offset, bool addChecksum);
explicit AccountPrivate(const std::function<Account*(AccountPrivate*)> &account, explicit AccountPrivate(const std::function<Account*(AccountPrivate*)> &account,
AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id, AccountStoragePrivate *storage, Dispatcher *dispatcher, const QUuid &id,
const QString &name, const QString &secret, const QString &name, const secrets::EncryptedSecret &secret,
const QDateTime &epoch, uint timeStep, int tokenLength, Account::Hash hash); const QDateTime &epoch, uint timeStep, int tokenLength, Account::Hash hash);
void recompute(void); void recompute(void);
void setCounter(quint64 counter); void setCounter(quint64 counter);
@ -65,7 +65,7 @@ namespace accounts
private: private:
QString m_token; QString m_token;
const QString m_name; const QString m_name;
const QString m_secret; const secrets::EncryptedSecret m_secret;
const int m_tokenLength; const int m_tokenLength;
quint64 m_counter; quint64 m_counter;
const int m_offset; const int m_offset;
@ -94,14 +94,14 @@ namespace accounts
void acceptAccountRemoval(const QString &accountName); void acceptAccountRemoval(const QString &accountName);
Account * acceptHotpAccount(const QUuid &id, Account * acceptHotpAccount(const QUuid &id,
const QString &name, const QString &name,
const QString &secret, const secrets::EncryptedSecret &secret,
quint64 counter = 0ULL, quint64 counter = 0ULL,
int tokenLength = 6, int tokenLength = 6,
int offset = -1, int offset = -1,
bool addChecksum = false); bool addChecksum = false);
Account * acceptTotpAccount(const QUuid &id, Account * acceptTotpAccount(const QUuid &id,
const QString &name, const QString &name,
const QString &secret, const secrets::EncryptedSecret &secret,
uint timeStep = 30, uint timeStep = 30,
int tokenLength = 6, int tokenLength = 6,
const QDateTime &epoch = QDateTime::fromMSecsSinceEpoch(0), const QDateTime &epoch = QDateTime::fromMSecsSinceEpoch(0),
@ -121,6 +121,8 @@ namespace accounts
const QDateTime &epoch = QDateTime::fromMSecsSinceEpoch(0), const QDateTime &epoch = QDateTime::fromMSecsSinceEpoch(0),
Account::Hash hash = Account::Hash::Default); Account::Hash hash = Account::Hash::Default);
private: private:
bool validateGenericNewToken(const QString &name, const QString &secret, int tokenLength) const;
std::optional<secrets::EncryptedSecret> encrypt(const QString &secret) const;
QUuid generateId(const QString &name) const; QUuid generateId(const QString &name) const;
private: private:
Q_DECLARE_PUBLIC(AccountStorage); Q_DECLARE_PUBLIC(AccountStorage);

View File

@ -9,6 +9,7 @@
#include "../logging_p.h" #include "../logging_p.h"
#include "../oath/oath.h" #include "../oath/oath.h"
#include <QScopedPointer>
#include <QTimer> #include <QTimer>
KEYSMITH_LOGGER(logger, ".accounts.actions") KEYSMITH_LOGGER(logger, ".accounts.actions")
@ -42,7 +43,7 @@ namespace accounts
{ {
} }
LoadAccounts::LoadAccounts(const SettingsProvider &settings) : AccountJob(), m_settings(settings) LoadAccounts::LoadAccounts(const SettingsProvider &settings, const AccountSecret *secret) : AccountJob(), m_settings(settings), m_secret(secret)
{ {
} }
@ -50,19 +51,19 @@ namespace accounts
{ {
} }
SaveHotp::SaveHotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const QString &secret, quint64 counter, int tokenLength) : 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) AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), m_secret(secret), m_counter(counter), m_tokenLength(tokenLength)
{ {
} }
SaveTotp::SaveTotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const QString &secret, uint timeStep, int 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) AccountJob(), m_settings(settings), m_id(id), m_accountName(accountName), m_secret(secret), m_timeStep(timeStep), m_tokenLength(tokenLength)
{ {
} }
void SaveHotp::run(void) void SaveHotp::run(void)
{ {
if (!checkHotpAccount(m_id, m_accountName, m_secret, m_tokenLength)) { if (!checkId(m_id) || !checkName(m_accountName) || !checkTokenLength(m_tokenLength)) {
qCDebug(logger) qCDebug(logger)
<< "Unable to save HOTP account:" << m_id << "Unable to save HOTP account:" << m_id
<< "Invalid account details"; << "Invalid account details";
@ -88,7 +89,10 @@ namespace accounts
settings.beginGroup(group); settings.beginGroup(group);
settings.setValue("account", m_accountName); settings.setValue("account", m_accountName);
settings.setValue("type", "hotp"); settings.setValue("type", "hotp");
settings.setValue("secret", m_secret); QString encodedNonce = QString::fromUtf8(m_secret.nonce().toBase64(QByteArray::Base64Encoding));
QString encodedSecret = QString::fromUtf8(m_secret.cryptText().toBase64(QByteArray::Base64Encoding));
settings.setValue("secret", encodedSecret);
settings.setValue("nonce", encodedNonce);
settings.setValue("counter", m_counter); settings.setValue("counter", m_counter);
settings.setValue("pinLength", m_tokenLength); settings.setValue("pinLength", m_tokenLength);
settings.endGroup(); settings.endGroup();
@ -96,7 +100,7 @@ namespace accounts
// Try to guarantee that data will have been written before claiming the account was actually saved // Try to guarantee that data will have been written before claiming the account was actually saved
settings.sync(); settings.sync();
Q_EMIT saved(m_id, m_accountName, m_secret, m_counter, m_tokenLength); Q_EMIT saved(m_id, m_accountName, m_secret.cryptText(), m_secret.nonce(), m_counter, m_tokenLength);
}); });
m_settings(act); m_settings(act);
@ -105,7 +109,7 @@ namespace accounts
void SaveTotp::run(void) void SaveTotp::run(void)
{ {
if (!checkTotpAccount(m_id, m_accountName, m_secret, m_tokenLength, m_timeStep)) { if (!checkId(m_id) || !checkName(m_accountName) || !checkTokenLength(m_tokenLength) || !checkTimeStep(m_timeStep)) {
qCDebug(logger) qCDebug(logger)
<< "Unable to save TOTP account:" << m_id << "Unable to save TOTP account:" << m_id
<< "Invalid account details"; << "Invalid account details";
@ -131,7 +135,10 @@ namespace accounts
settings.beginGroup(group); settings.beginGroup(group);
settings.setValue("account", m_accountName); settings.setValue("account", m_accountName);
settings.setValue("type", "totp"); settings.setValue("type", "totp");
settings.setValue("secret", m_secret); QString encodedNonce = QString::fromUtf8(m_secret.nonce().toBase64(QByteArray::Base64Encoding));
QString encodedSecret = QString::fromUtf8(m_secret.cryptText().toBase64(QByteArray::Base64Encoding));
settings.setValue("secret", encodedSecret);
settings.setValue("nonce", encodedNonce);
settings.setValue("timeStep", m_timeStep); settings.setValue("timeStep", m_timeStep);
settings.setValue("pinLength", m_tokenLength); settings.setValue("pinLength", m_tokenLength);
settings.endGroup(); settings.endGroup();
@ -139,7 +146,7 @@ namespace accounts
// Try to guarantee that data will have been written before claiming the account was actually saved // Try to guarantee that data will have been written before claiming the account was actually saved
settings.sync(); settings.sync();
Q_EMIT saved(m_id, m_accountName, m_secret, m_timeStep, m_tokenLength); Q_EMIT saved(m_id, m_accountName, m_secret.cryptText(), m_secret.nonce(), m_timeStep, m_tokenLength);
}); });
m_settings(act); m_settings(act);
@ -309,13 +316,22 @@ namespace accounts
void LoadAccounts::run(void) void LoadAccounts::run(void)
{ {
if (!m_secret || !m_secret->key()) {
qCDebug(logger) << "Unable to load accounts: secret decryption key not available";
Q_EMIT finished();
return;
}
const PersistenceAction act([this](QSettings &settings) -> void const PersistenceAction act([this](QSettings &settings) -> void
{ {
qCInfo(logger, "Loading accounts from storage"); qCInfo(logger, "Loading accounts from storage");
const QStringList entries = settings.childGroups(); const QStringList entries = settings.childGroups();
for (const QString &group : entries) { for (const QString &group : entries) {
const QUuid id(group); if (group == QLatin1String("master-key")) {
continue;
}
const QUuid id(group);
if (id.isNull()) { if (id.isNull()) {
qCDebug(logger) qCDebug(logger)
<< "Ignoring:" << group << "Ignoring:" << group
@ -323,47 +339,87 @@ namespace accounts
continue; continue;
} }
bool ok = false;
settings.beginGroup(group); settings.beginGroup(group);
const QString secret = settings.value("secret").toString();
const QString accountName = settings.value("account").toString(); const QString accountName = settings.value("account").toString();
const QString type = settings.value("type", "hotp").toString(); if (!checkName(accountName)) {
const int tokenLength = settings.value("pinLength").toInt(&ok); qCWarning(logger)
<< "Skipping invalid account:" << id
if (!ok || (type != "hotp" && type != "totp")) { << "Invalid account name";
qCWarning(logger) << "Skipping invalid account:" << id;
settings.endGroup(); settings.endGroup();
continue; continue;
} }
if (type == "totp") { const QString type = settings.value("type", "hotp").toString();
if (type != QLatin1String("hotp") && type != QLatin1String("totp")) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid account type";
settings.endGroup();
continue;
}
bool ok = false;
const int tokenLength = settings.value("pinLength").toInt(&ok);
if (!ok || !checkTokenLength(tokenLength)) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid token length";
settings.endGroup();
continue;
}
const QByteArray encodedNonce = settings.value("nonce").toString().toUtf8();
const QByteArray encodedSecret = settings.value("secret").toString().toUtf8();
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);
if (!encryptedSecret) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Invalid token secret";
settings.endGroup();
continue;
}
QScopedPointer<secrets::SecureMemory> decrypted(m_secret->decrypt(*encryptedSecret));
if (!decrypted) {
qCWarning(logger)
<< "Skipping invalid account:" << id
<< "Unable to decrypt token secret";
settings.endGroup();
continue;
}
if (type == QLatin1String("totp")) {
ok = false; ok = false;
const uint timeStep = settings.value("timeStep").toUInt(&ok); const uint timeStep = settings.value("timeStep").toUInt(&ok);
if (ok && checkTotpAccount(id, accountName, secret, tokenLength, timeStep)) { if (!ok || !checkTimeStep(timeStep)) {
qCInfo(logger) << "Found valid TOTP account:" << id; qCWarning(logger)
Q_EMIT foundTotp( << "Skipping invalid account:" << id
id, << "Invalid time step";
accountName, settings.endGroup();
secret, continue;
timeStep,
tokenLength
);
} }
qCInfo(logger) << "Found valid TOTP account:" << id;
Q_EMIT foundTotp(id, accountName, secret, nonce, timeStep, tokenLength);
} }
if (type == "hotp") {
if (type == QLatin1String("hotp")) {
ok = false; ok = false;
const quint64 counter = settings.value("counter").toULongLong(&ok); const quint64 counter = settings.value("counter").toULongLong(&ok);
if (ok && checkHotpAccount(id, accountName, secret, tokenLength)) { if (!ok) {
qCInfo(logger) << "Found valid HOTP account:" << id; qCWarning(logger)
Q_EMIT foundHotp( << "Skipping invalid account:" << id
id, << "Invalid counter";
accountName, settings.endGroup();
secret, continue;
counter,
tokenLength
);
} }
qCInfo(logger) << "Found valid HOTP account:" << id;
Q_EMIT foundHotp(id, accountName, secret, nonce, counter, tokenLength);
} }
settings.endGroup(); settings.endGroup();
@ -374,22 +430,27 @@ namespace accounts
Q_EMIT finished(); Q_EMIT finished();
} }
ComputeTotp::ComputeTotp(const QString &secret, const QDateTime &epoch, uint timeStep, int tokenLength, const Account::Hash &hash, const std::function<qint64(void)> &clock) : 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_epoch(epoch), m_timeStep(timeStep), m_tokenLength(tokenLength), m_hash(hash), m_clock(clock) AccountJob(), m_secret(secret), m_tokenSecret(tokenSecret), m_epoch(epoch), m_timeStep(timeStep), m_tokenLength(tokenLength), m_hash(hash), m_clock(clock)
{ {
} }
void ComputeTotp::run(void) void ComputeTotp::run(void)
{ {
if (!checkTotp(m_secret, m_tokenLength, m_timeStep)) { if (!m_secret || !m_secret->key()) {
qCDebug(logger) << "Unable to compute TOTP token: invalid token details"; qCDebug(logger) << "Unable to compute TOTP token: secret decryption key not available";
Q_EMIT finished(); Q_EMIT finished();
return; return;
} }
std::optional<QByteArray> secret = base32::decode(m_secret); if (!checkTokenLength(m_tokenLength)) {
if (!secret.has_value()) { qCDebug(logger) << "Unable to compute THOTP token: invalid token length:" << m_tokenLength;
qCDebug(logger) << "Unable to compute TOTP token: unable to decode secret"; Q_EMIT finished();
return;
}
if (!checkTimeStep(m_timeStep)) {
qCDebug(logger) << "Unable to compute THOTP token: invalid time step:" << m_timeStep;
Q_EMIT finished(); Q_EMIT finished();
return; return;
} }
@ -415,19 +476,26 @@ namespace accounts
const std::optional<oath::Algorithm> algorithm = oath::Algorithm::usingDynamicTruncation(hash, m_tokenLength); const std::optional<oath::Algorithm> algorithm = oath::Algorithm::usingDynamicTruncation(hash, m_tokenLength);
if (!algorithm) { if (!algorithm) {
qCDebug(logger) << "Unable to compute TOTP token: unable to set up truncation for token length:" << m_tokenLength; qCDebug(logger) << "Unable to compute TOTP token: failed to construct algorithm";
Q_EMIT finished(); Q_EMIT finished();
return; return;
} }
const std::optional<quint64> counter = oath::count(m_epoch, m_timeStep, m_clock); const std::optional<quint64> counter = oath::count(m_epoch, m_timeStep, m_clock);
if (!counter) { if (!counter) {
qCDebug(logger) << "Unable to compute TOTP token: unable to count time steps"; qCDebug(logger) << "Unable to compute TOTP token: failed to count time steps";
Q_EMIT finished(); Q_EMIT finished();
return; return;
} }
const std::optional<QString> token = algorithm->compute(*counter, secret->data(), secret->size()); QScopedPointer<secrets::SecureMemory> secret(m_secret->decrypt(m_tokenSecret));
if (!secret) {
qCDebug(logger) << "Unable to compute TOTP token: failed to decrypt secret";
Q_EMIT finished();
return;
}
const std::optional<QString> token = algorithm->compute(*counter, reinterpret_cast<char*>(secret->data()), secret->size());
if (token) { if (token) {
Q_EMIT otp(*token); Q_EMIT otp(*token);
} else { } else {
@ -437,22 +505,21 @@ namespace accounts
Q_EMIT finished(); Q_EMIT finished();
} }
ComputeHotp::ComputeHotp(const QString &secret, quint64 counter, int tokenLength, int offset, bool checksum) : ComputeHotp::ComputeHotp(const AccountSecret *secret, const secrets::EncryptedSecret &tokenSecret, quint64 counter, int tokenLength, int offset, bool checksum) :
AccountJob(), m_secret(secret), m_counter(counter), m_tokenLength(tokenLength), m_offset(offset), m_checksum(checksum) AccountJob(), m_secret(secret), m_tokenSecret(tokenSecret), m_counter(counter), m_tokenLength(tokenLength), m_offset(offset), m_checksum(checksum)
{ {
} }
void ComputeHotp::run(void) void ComputeHotp::run(void)
{ {
if (!checkHotp(m_secret, m_tokenLength)) { if (!m_secret || !m_secret->key()) {
qCDebug(logger) << "Unable to compute HOTP token: invalid token details"; qCDebug(logger) << "Unable to compute HOTP token: secret decryption key not available";
Q_EMIT finished(); Q_EMIT finished();
return; return;
} }
std::optional<QByteArray> secret = base32::decode(m_secret); if (!checkTokenLength(m_tokenLength)) {
if (!secret.has_value()) { qCDebug(logger) << "Unable to compute HOTP token: invalid token length:" << m_tokenLength;
qCDebug(logger) << "Unable to compute HOTP token: unable to decode secret";
Q_EMIT finished(); Q_EMIT finished();
return; return;
} }
@ -462,12 +529,19 @@ namespace accounts
? oath::Algorithm::usingTruncationOffset(QCryptographicHash::Sha1, (uint) m_offset, encoder) ? oath::Algorithm::usingTruncationOffset(QCryptographicHash::Sha1, (uint) m_offset, encoder)
: oath::Algorithm::usingDynamicTruncation(QCryptographicHash::Sha1, encoder); : oath::Algorithm::usingDynamicTruncation(QCryptographicHash::Sha1, encoder);
if (!algorithm) { if (!algorithm) {
qCDebug(logger) << "Unable to compute HOTP token: unable to set up truncation for token length:" << m_tokenLength; qCDebug(logger) << "Unable to compute HOTP token: failed to construct algorithm";
Q_EMIT finished(); Q_EMIT finished();
return; return;
} }
const std::optional<QString> token = algorithm->compute(m_counter, secret->data(), secret->size()); QScopedPointer<secrets::SecureMemory> secret(m_secret->decrypt(m_tokenSecret));
if (!secret) {
qCDebug(logger) << "Unable to compute HOTP token: failed to decrypt secret";
Q_EMIT finished();
return;
}
const std::optional<QString> token = algorithm->compute(m_counter, reinterpret_cast<char*>(secret->data()), secret->size());
if (token) { if (token) {
Q_EMIT otp(*token); Q_EMIT otp(*token);
} else { } else {

View File

@ -17,6 +17,7 @@
#include <functional> #include <functional>
#include "../secrets/secrets.h"
#include "keys.h" #include "keys.h"
namespace accounts namespace accounts
@ -65,13 +66,14 @@ namespace accounts
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit LoadAccounts(const SettingsProvider &settings); explicit LoadAccounts(const SettingsProvider &settings, const AccountSecret *secret);
void run(void) override; void run(void) override;
Q_SIGNALS: Q_SIGNALS:
void foundHotp(const QUuid id, const QString name, const QString secret, quint64 counter, int tokenLength); 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 QString secret, uint timeStep, int tokenLength); void foundTotp(const QUuid id, const QString name, const QByteArray secret, const QByteArray nonce, uint timeStep, int tokenLength);
private: private:
const SettingsProvider m_settings; const SettingsProvider m_settings;
const AccountSecret * m_secret;
}; };
class DeleteAccounts: public AccountJob class DeleteAccounts: public AccountJob
@ -91,16 +93,16 @@ namespace accounts
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit SaveHotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const QString &secret, quint64 counter, int tokenLength); explicit SaveHotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const secrets::EncryptedSecret &secret, quint64 counter, int tokenLength);
void run(void) override; void run(void) override;
Q_SIGNALS: Q_SIGNALS:
void invalid(void); void invalid(void);
void saved(const QUuid id, const QString accountName, const QString secret, quint64 counter, int tokenLength); void saved(const QUuid id, const QString accountName, const QByteArray secret, const QByteArray nonce, quint64 counter, int tokenLength);
private: private:
const SettingsProvider m_settings; const SettingsProvider m_settings;
const QUuid m_id; const QUuid m_id;
const QString m_accountName; const QString m_accountName;
const QString m_secret; const secrets::EncryptedSecret m_secret;
const quint64 m_counter; const quint64 m_counter;
const int m_tokenLength; const int m_tokenLength;
}; };
@ -109,16 +111,16 @@ namespace accounts
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit SaveTotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const QString &secret, uint timeStep, int tokenLength); explicit SaveTotp(const SettingsProvider &settings, const QUuid &id, const QString &accountName, const secrets::EncryptedSecret &secret, uint timeStep, int tokenLength);
void run(void) override; void run(void) override;
Q_SIGNALS: Q_SIGNALS:
void invalid(void); void invalid(void);
void saved(const QUuid id, const QString accountName, const QString secret, uint timeStep, int tokenLength); void saved(const QUuid id, const QString accountName, const QByteArray secret, const QByteArray nonce, uint timeStep, int tokenLength);
private: private:
const SettingsProvider m_settings; const SettingsProvider m_settings;
const QUuid m_id; const QUuid m_id;
const QString m_accountName; const QString m_accountName;
const QString m_secret; const secrets::EncryptedSecret m_secret;
const uint m_timeStep; const uint m_timeStep;
const int m_tokenLength; const int m_tokenLength;
}; };
@ -127,12 +129,13 @@ namespace accounts
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ComputeTotp(const QString &secret, 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, const QDateTime &epoch, uint timeStep, int tokenLength, const Account::Hash &hash = Account::Hash::Default, const std::function<qint64(void)> &clock = &QDateTime::currentMSecsSinceEpoch);
void run(void) override; void run(void) override;
Q_SIGNALS: Q_SIGNALS:
void otp(const QString otp); void otp(const QString otp);
private: private:
const QString m_secret; const AccountSecret * m_secret;
const secrets::EncryptedSecret m_tokenSecret;
const QDateTime m_epoch; const QDateTime m_epoch;
const uint m_timeStep; const uint m_timeStep;
const int m_tokenLength; const int m_tokenLength;
@ -144,12 +147,13 @@ namespace accounts
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit ComputeHotp(const QString &secret, quint64 counter, int tokenLength, int offset = -1, bool checksum = false); explicit ComputeHotp(const AccountSecret *secret, const secrets::EncryptedSecret &tokenSecret, quint64 counter, int tokenLength, int offset = -1, bool checksum = false);
void run(void) override; void run(void) override;
Q_SIGNALS: Q_SIGNALS:
void otp(const QString otp); void otp(const QString otp);
private: private:
const QString m_secret; const AccountSecret * m_secret;
const secrets::EncryptedSecret m_tokenSecret;
const quint64 m_counter; const quint64 m_counter;
const int m_tokenLength; const int m_tokenLength;
const int m_offset; const int m_offset;

View File

@ -220,5 +220,25 @@ namespace accounts
return m_stillAlive && m_key ? m_key.data() : nullptr; return m_stillAlive && m_key ? m_key.data() : nullptr;
} }
std::optional<secrets::EncryptedSecret> AccountSecret::encrypt(const secrets::SecureMemory *secret) const
{
secrets::SecureMasterKey *k = key();
if (!k) {
qCDebug(logger) << "Unable to encrypt secret: encryption key not available";
return std::nullopt;
}
return k->encrypt(secret);
}
secrets::SecureMemory * AccountSecret::decrypt(const secrets::EncryptedSecret &secret) const
{
secrets::SecureMasterKey *k = key();
if (!k) {
qCDebug(logger) << "Unable to decrypt secret: decryption key not available";
return nullptr;
}
return k->decrypt(secret);
}
} }

View File

@ -33,6 +33,8 @@ namespace accounts
secrets::SecureMasterKey * deriveKey(void); secrets::SecureMasterKey * deriveKey(void);
secrets::SecureMasterKey * key(void) const; secrets::SecureMasterKey * key(void) const;
std::optional<secrets::EncryptedSecret> encrypt(const secrets::SecureMemory *secret) const;
secrets::SecureMemory * decrypt(const secrets::EncryptedSecret &secret) const;
bool isStillAlive(void) const; bool isStillAlive(void) const;
bool isNewPasswordRequested(void) const; bool isNewPasswordRequested(void) const;
bool isExistingPasswordRequested(void) const; bool isExistingPasswordRequested(void) const;

View File

@ -33,24 +33,4 @@ namespace accounts
{ {
return timeStep > 0; return timeStep > 0;
} }
bool checkHotp(const QString &secret, const int tokenLength)
{
return checkSecret(secret) && checkTokenLength(tokenLength);
}
bool checkHotpAccount(const QUuid &id, const QString &name, const QString &secret, const int tokenLength)
{
return checkId(id) && checkName(name) && checkHotp(secret, tokenLength);
}
bool checkTotp(const QString &secret, const int tokenLength, const uint timeStep)
{
return checkSecret(secret) && checkTokenLength(tokenLength) && checkTimeStep(timeStep);
}
bool checkTotpAccount(const QUuid &id, const QString &name, const QString &secret, const int tokenLength, const uint timeStep)
{
return checkId(id) && checkName(name) && checkTotp(secret, tokenLength, timeStep);
}
} }

View File

@ -15,12 +15,6 @@ namespace accounts
bool checkName(const QString &name); bool checkName(const QString &name);
bool checkTokenLength(int tokenLength); bool checkTokenLength(int tokenLength);
bool checkTimeStep(uint timeStep); bool checkTimeStep(uint timeStep);
bool checkHotp(const QString &secret, const int tokenLength);
bool checkTotp(const QString &secret, const int tokenLength, const uint timeStep);
bool checkHotpAccount(const QUuid &id, const QString &name, const QString &secret, const int tokenLength);
bool checkTotpAccount(const QUuid &id, const QString &name, const QString &secret, const int tokenLength, const uint timeStep);
} }
#endif #endif