fix!: guard against incorrect password inputs using an encrypted challenge
Previously entering an incorrect password would appear to successfully "unlock" accounts, contrary to expectations. By introducing a challenge object as part of the master key parameters, an incorrect password can now be detected and signalled accordingly. This fix introduces a backwards incompatible change to the accounts data as stored on disk, meaning old Keysmith accounts configuration will no longer load and must be recreated from scratch.master
parent
4507c5e0be
commit
cbd069085e
|
@ -23,6 +23,7 @@ class RequestAccountPasswordTest: public QObject // clazy:exclude=ctor-missing-p
|
|||
private Q_SLOTS:
|
||||
void testExistingPassword(void);
|
||||
void testExistingPasswordAbort(void);
|
||||
void testExistingPasswordRetry(void);
|
||||
void testNewPassword(void);
|
||||
void testNewPasswordAbort(void);
|
||||
void testAbortBeforeRun(void);
|
||||
|
@ -47,6 +48,7 @@ void RequestAccountPasswordTest::testAbortBeforeRun(void)
|
|||
QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
|
||||
QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
|
||||
QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
|
||||
|
||||
accounts::RequestAccountPassword uut(settings, &secret);
|
||||
|
@ -67,6 +69,7 @@ void RequestAccountPasswordTest::testAbortBeforeRun(void)
|
|||
QCOMPARE(existingPasswordNeeded.count(), 0);
|
||||
QCOMPARE(passwordAvailable.count(), 0);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(passwordRequestsCancelled.count(), 1);
|
||||
QCOMPARE(failed.count(), 1);
|
||||
QCOMPARE(unlocked.count(), 0);
|
||||
|
@ -95,6 +98,7 @@ void RequestAccountPasswordTest::testNewPassword(void)
|
|||
QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
|
||||
QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
|
||||
QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
|
||||
|
||||
accounts::RequestAccountPassword uut(settings, &secret);
|
||||
|
@ -129,6 +133,7 @@ void RequestAccountPasswordTest::testNewPassword(void)
|
|||
QCOMPARE(existingPasswordNeeded.count(), 0);
|
||||
QCOMPARE(passwordAvailable.count(), 1);
|
||||
QCOMPARE(keyAvailable.count(), 1);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(passwordRequestsCancelled.count(), 0);
|
||||
QCOMPARE(failed.count(), 0);
|
||||
QCOMPARE(unlocked.count(), 1);
|
||||
|
@ -157,6 +162,7 @@ void RequestAccountPasswordTest::testNewPasswordAbort(void)
|
|||
QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
|
||||
QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
|
||||
QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
|
||||
|
||||
accounts::RequestAccountPassword uut(settings, &secret);
|
||||
|
@ -185,6 +191,7 @@ void RequestAccountPasswordTest::testNewPasswordAbort(void)
|
|||
QCOMPARE(existingPasswordNeeded.count(), 0);
|
||||
QCOMPARE(passwordAvailable.count(), 0);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(passwordRequestsCancelled.count(), 1);
|
||||
QCOMPARE(failed.count(), 1);
|
||||
QCOMPARE(unlocked.count(), 0);
|
||||
|
@ -213,6 +220,7 @@ void RequestAccountPasswordTest::testExistingPassword(void)
|
|||
QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
|
||||
QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
|
||||
QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
|
||||
|
||||
accounts::RequestAccountPassword uut(settings, &secret);
|
||||
|
@ -245,6 +253,76 @@ void RequestAccountPasswordTest::testExistingPassword(void)
|
|||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(passwordAvailable.count(), 1);
|
||||
QCOMPARE(keyAvailable.count(), 1);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(passwordRequestsCancelled.count(), 0);
|
||||
QCOMPARE(failed.count(), 0);
|
||||
QCOMPARE(unlocked.count(), 1);
|
||||
|
||||
QFile result(actualIni);
|
||||
QVERIFY2(result.exists(), "accounts file should still exist");
|
||||
QCOMPARE(test::slurp(actualIni), test::slurp(existingPasswordIniResource));
|
||||
}
|
||||
|
||||
void RequestAccountPasswordTest::testExistingPasswordRetry(void)
|
||||
{
|
||||
const QString isolated(QStringLiteral("supply-existing-password.ini"));
|
||||
QVERIFY2(test::copyResourceAsWritable(existingPasswordIniResource, isolated), "accounts INI resource should be available as file");
|
||||
|
||||
int openCounter = 0;
|
||||
const QString actualIni = test::path(isolated);
|
||||
const accounts::SettingsProvider settings([&openCounter, &actualIni](const accounts::PersistenceAction &action) -> void
|
||||
{
|
||||
QSettings data(actualIni, QSettings::IniFormat);
|
||||
openCounter++;
|
||||
action(data);
|
||||
});
|
||||
|
||||
accounts::AccountSecret secret;
|
||||
QSignalSpy existingPasswordNeeded(&secret, &accounts::AccountSecret::existingPasswordNeeded);
|
||||
QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
|
||||
QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
|
||||
QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
|
||||
|
||||
accounts::RequestAccountPassword uut(settings, &secret);
|
||||
|
||||
QSignalSpy failed(&uut, &accounts::RequestAccountPassword::failed);
|
||||
QSignalSpy unlocked(&uut, &accounts::RequestAccountPassword::unlocked);
|
||||
QSignalSpy jobFinished(&uut, &accounts::RequestAccountPassword::finished);
|
||||
|
||||
uut.run();
|
||||
|
||||
QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "(existing) password should be asked for");
|
||||
QCOMPARE(openCounter, 1);
|
||||
QCOMPARE(newPasswordNeeded.count(), 0);
|
||||
QCOMPARE(failed.count(), 0);
|
||||
QCOMPARE(unlocked.count(), 0);
|
||||
QCOMPARE(jobFinished.count(), 0);
|
||||
|
||||
QString incorrect(QStringLiteral("incorrect"));
|
||||
QVERIFY2(secret.answerExistingPassword(incorrect), "should be able to answer (existing) password");
|
||||
|
||||
QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "(existing) password attempt should be accepted");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(keyFailed), "should fail to derive key for incorrect password");
|
||||
QCOMPARE(openCounter, 1);
|
||||
|
||||
QString correct(QStringLiteral("hello, world"));
|
||||
QVERIFY2(secret.answerExistingPassword(correct), "should be able to retry (existing) password");
|
||||
|
||||
QVERIFY2(test::signal_eventually_emitted_twice(passwordAvailable), "second attempt for (existing) password should be accepted");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(keyAvailable), "key should be derived");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(unlocked), "accounts should be unlocked");
|
||||
QCOMPARE(openCounter, 2);
|
||||
|
||||
QVERIFY2(test::signal_eventually_emitted_once(jobFinished), "job should be finished");
|
||||
|
||||
QCOMPARE(openCounter, 2);
|
||||
QCOMPARE(newPasswordNeeded.count(), 0);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(passwordAvailable.count(), 2);
|
||||
QCOMPARE(keyAvailable.count(), 1);
|
||||
QCOMPARE(keyFailed.count(), 1);
|
||||
QCOMPARE(passwordRequestsCancelled.count(), 0);
|
||||
QCOMPARE(failed.count(), 0);
|
||||
QCOMPARE(unlocked.count(), 1);
|
||||
|
@ -273,6 +351,7 @@ void RequestAccountPasswordTest::testExistingPasswordAbort(void)
|
|||
QSignalSpy newPasswordNeeded(&secret, &accounts::AccountSecret::newPasswordNeeded);
|
||||
QSignalSpy passwordAvailable(&secret, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy keyAvailable(&secret, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&secret, &accounts::AccountSecret::keyFailed);
|
||||
QSignalSpy passwordRequestsCancelled(&secret, &accounts::AccountSecret::requestsCancelled);
|
||||
|
||||
accounts::RequestAccountPassword uut(settings, &secret);
|
||||
|
@ -301,6 +380,7 @@ void RequestAccountPasswordTest::testExistingPasswordAbort(void)
|
|||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(passwordAvailable.count(), 0);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(passwordRequestsCancelled.count(), 1);
|
||||
QCOMPARE(failed.count(), 1);
|
||||
QCOMPARE(unlocked.count(), 0);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
[master-key]
|
||||
algorithm=2
|
||||
challenge="8Crw0DTl6z7hb/ZFcDbtf5m4kLJkCfcbZcSP4w=="
|
||||
cpu=1
|
||||
length=32
|
||||
memory=8192
|
||||
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
|
||||
salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
[master-key]
|
||||
algorithm=2
|
||||
challenge="bvw8JNdx+PBUt/CzbBGcTCMkdAY8NhlQfR1P2A=="
|
||||
cpu=3
|
||||
length=32
|
||||
memory=268435456
|
||||
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
|
||||
salt="QUFBQUFBQUFBQUFBQUFBQQ=="
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
|
||||
#
|
||||
|
||||
set(Test_DEP_LIBS Qt5::Core Qt5::Test account_lib account_test_lib test_lib)
|
||||
set(Test_DEP_LIBS Qt5::Core Qt5::Test account_lib account_test_lib secrets_test_lib test_lib)
|
||||
|
||||
set(account_secret_test_SRCS
|
||||
account-secret-password-flow.cpp
|
||||
|
|
|
@ -4,11 +4,78 @@
|
|||
*/
|
||||
#include "account/keys.h"
|
||||
|
||||
#include "../../secrets/test-utils/random.h"
|
||||
#include "../../test-utils/spy.h"
|
||||
|
||||
#include <QSignalSpy>
|
||||
#include <QTest>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
static QByteArray fill(int size)
|
||||
{
|
||||
QByteArray a;
|
||||
a.resize(size);
|
||||
/*
|
||||
* Because this value is used to generate the expected challenge value(s) up front, this salt has to match the
|
||||
* behaviour of test::fakeRandom() in case of 'new' passwords where the actual salt will be drawn 'randomly'
|
||||
* (from test::fakeRandom()).
|
||||
*/
|
||||
a.fill('A', -1);
|
||||
return a;
|
||||
}
|
||||
|
||||
static QByteArray salt()
|
||||
{
|
||||
return fill(crypto_pwhash_SALTBYTES);
|
||||
}
|
||||
|
||||
static QByteArray masterPassword(void)
|
||||
{
|
||||
static QByteArray MASTER_PASSWORD("hello, world");
|
||||
return MASTER_PASSWORD;
|
||||
}
|
||||
|
||||
static secrets::SecureMemory * secret(void)
|
||||
{
|
||||
const auto master = masterPassword();
|
||||
size_t size = (size_t) master.size();
|
||||
auto memory = secrets::SecureMemory::allocate(size);
|
||||
if (memory) {
|
||||
std::memcpy(memory->data(), master.constData(), size);
|
||||
}
|
||||
return memory;
|
||||
}
|
||||
|
||||
static std::optional<secrets::KeyDerivationParameters> keyParams = secrets::KeyDerivationParameters::create(
|
||||
crypto_secretbox_KEYBYTES, crypto_pwhash_ALG_DEFAULT, crypto_pwhash_MEMLIMIT_MIN, crypto_pwhash_OPSLIMIT_MIN
|
||||
);
|
||||
|
||||
static secrets::SecureMasterKey * key(secrets::SecureMemory *password)
|
||||
{
|
||||
if (!keyParams) {
|
||||
qDebug() << "Unable to setup() dummy master key to generate test data with";
|
||||
return nullptr;
|
||||
}
|
||||
return secrets::SecureMasterKey::derive(password, *keyParams, salt(), &test::fakeRandom);
|
||||
}
|
||||
|
||||
static std::optional<secrets::EncryptedSecret> challenge(void)
|
||||
{
|
||||
QScopedPointer<secrets::SecureMemory> s(secret());
|
||||
if (!s) {
|
||||
qDebug() << "Unable to generate challenge(), unable to allocate buffer for password secret.";
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
QScopedPointer<secrets::SecureMasterKey> k(key(s.data()));
|
||||
if (!k) {
|
||||
qDebug() << "Unable to generate challenge(), unable to setup dummy master key.";
|
||||
return std::nullopt;
|
||||
}
|
||||
return k->encrypt(s.data());
|
||||
}
|
||||
|
||||
class PasswordFlowTest : public QObject // clazy:exclude=ctor-missing-parent-argument
|
||||
{
|
||||
Q_OBJECT
|
||||
|
@ -18,27 +85,28 @@ private Q_SLOTS:
|
|||
void cancelExistingPassword(void);
|
||||
void supplyNewPassword(void);
|
||||
void cancelNewPassword(void);
|
||||
void retryExistingPassword(void);
|
||||
private:
|
||||
QByteArray m_salt;
|
||||
std::optional<secrets::KeyDerivationParameters> m_keyParams = secrets::KeyDerivationParameters::create(
|
||||
crypto_secretbox_KEYBYTES, crypto_pwhash_ALG_DEFAULT, crypto_pwhash_MEMLIMIT_MIN, crypto_pwhash_OPSLIMIT_MIN
|
||||
);
|
||||
QByteArray m_salt = salt();
|
||||
std::optional<secrets::EncryptedSecret> m_challenge = challenge();
|
||||
};
|
||||
|
||||
void PasswordFlowTest::initTestCase(void)
|
||||
{
|
||||
m_salt.resize(crypto_pwhash_SALTBYTES);
|
||||
QVERIFY2(m_keyParams, "should be able to construct key derivation parameters");
|
||||
QVERIFY2(keyParams, "should be able to construct key derivation parameters");
|
||||
QVERIFY2(m_challenge, "should be able to construct password challenge");
|
||||
qDebug() << "Running with challenge:" << m_challenge->cryptText().toBase64() << "nonce:" << m_challenge->nonce().toBase64();
|
||||
}
|
||||
|
||||
void PasswordFlowTest::supplyNewPassword(void)
|
||||
{
|
||||
accounts::AccountSecret uut;
|
||||
accounts::AccountSecret uut(&test::fakeRandom);
|
||||
QSignalSpy existingPasswordNeeded(&uut, &accounts::AccountSecret::existingPasswordNeeded);
|
||||
QSignalSpy newPasswordNeeded(&uut, &accounts::AccountSecret::newPasswordNeeded);
|
||||
QSignalSpy passwordAvailable(&uut, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy requestsCancelled(&uut, &accounts::AccountSecret::requestsCancelled);
|
||||
QSignalSpy keyAvailable(&uut, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&uut, &accounts::AccountSecret::keyFailed);
|
||||
|
||||
// check correct initial state is reported
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
|
@ -47,6 +115,7 @@ void PasswordFlowTest::supplyNewPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
QVERIFY2(!uut.challenge(), "should not have a (generated) password challenge yet");
|
||||
|
||||
// advance the state: request password
|
||||
QVERIFY2(uut.requestNewPassword(), "should be able to request a (new) password");
|
||||
|
@ -59,20 +128,22 @@ void PasswordFlowTest::supplyNewPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
QVERIFY2(!uut.challenge(), "should not have a (generated) password challenge yet");
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 1);
|
||||
QCOMPARE(passwordAvailable.count(), 0);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 0);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
|
||||
// advance the state: supply password
|
||||
QString password(QStringLiteral("hello, world"));
|
||||
QVERIFY2(m_keyParams, "should be able to construct key derivation parameters");
|
||||
QString password = QString::fromUtf8(masterPassword());
|
||||
QString wiped = QStringLiteral("*").repeated(password.size());
|
||||
|
||||
QVERIFY2(uut.answerNewPassword(password, *m_keyParams), "(new) password should be accepted");
|
||||
QVERIFY2(uut.answerNewPassword(password, *keyParams), "(new) password should be accepted");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "availability of the (new) password should be signalled");
|
||||
QCOMPARE(password, QStringLiteral("************"));
|
||||
QCOMPARE(password, wiped);
|
||||
|
||||
// check the state is correctly updated
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
|
@ -81,11 +152,13 @@ void PasswordFlowTest::supplyNewPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), true);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
QVERIFY2(!uut.challenge(), "should still not have a (generated) password challenge yet");
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 1);
|
||||
QCOMPARE(passwordAvailable.count(), 1);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 0);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
|
||||
// advance the state: derive the master key
|
||||
|
@ -99,22 +172,28 @@ void PasswordFlowTest::supplyNewPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), true);
|
||||
QVERIFY2(uut.key(), "should have a master key by now");
|
||||
const auto generatedChallenge = uut.challenge();
|
||||
QVERIFY2(generatedChallenge, "should have a (generated) password challenge by now");
|
||||
QCOMPARE(generatedChallenge->cryptText(), m_challenge->cryptText());
|
||||
QCOMPARE(generatedChallenge->nonce(), m_challenge->nonce());
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 1);
|
||||
QCOMPARE(passwordAvailable.count(), 1);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 0);
|
||||
QCOMPARE(keyAvailable.count(), 1);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
}
|
||||
|
||||
void PasswordFlowTest::cancelNewPassword(void)
|
||||
{
|
||||
accounts::AccountSecret uut;
|
||||
accounts::AccountSecret uut(&test::fakeRandom);
|
||||
QSignalSpy existingPasswordNeeded(&uut, &accounts::AccountSecret::existingPasswordNeeded);
|
||||
QSignalSpy newPasswordNeeded(&uut, &accounts::AccountSecret::newPasswordNeeded);
|
||||
QSignalSpy passwordAvailable(&uut, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy requestsCancelled(&uut, &accounts::AccountSecret::requestsCancelled);
|
||||
QSignalSpy keyAvailable(&uut, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&uut, &accounts::AccountSecret::keyFailed);
|
||||
|
||||
// check correct initial state is reported
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
|
@ -123,6 +202,7 @@ void PasswordFlowTest::cancelNewPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
QVERIFY2(!uut.challenge(), "should not have a (generated) password challenge yet");
|
||||
|
||||
// advance the state: request password
|
||||
QVERIFY2(uut.requestNewPassword(), "should be able to request a (new) password");
|
||||
|
@ -135,11 +215,13 @@ void PasswordFlowTest::cancelNewPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
QVERIFY2(!uut.challenge(), "should still not have a (generated) password challenge yet");
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 1);
|
||||
QCOMPARE(passwordAvailable.count(), 0);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 0);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
|
||||
// advance the state: cancel the request
|
||||
|
@ -153,11 +235,13 @@ void PasswordFlowTest::cancelNewPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
QVERIFY2(!uut.challenge(), "should still not acknowledge a (generated) password challenge");
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 1);
|
||||
QCOMPARE(passwordAvailable.count(), 0);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 0);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 1);
|
||||
}
|
||||
|
||||
|
@ -169,6 +253,7 @@ void PasswordFlowTest::cancelExistingPassword(void)
|
|||
QSignalSpy passwordAvailable(&uut, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy requestsCancelled(&uut, &accounts::AccountSecret::requestsCancelled);
|
||||
QSignalSpy keyAvailable(&uut, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&uut, &accounts::AccountSecret::keyFailed);
|
||||
|
||||
// check correct initial state is reported
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
|
@ -177,9 +262,10 @@ void PasswordFlowTest::cancelExistingPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
QVERIFY2(!uut.challenge(), "should not have a password challenge yet");
|
||||
|
||||
// advance the state: request password
|
||||
QVERIFY2(uut.requestExistingPassword(m_salt, *m_keyParams), "should be able to request a (existing) password");
|
||||
QVERIFY2(uut.requestExistingPassword(*m_challenge, m_salt, *keyParams), "should be able to request a (existing) password");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "request for (existing) password should be signalled");
|
||||
|
||||
// check the state is correctly updated
|
||||
|
@ -189,11 +275,16 @@ void PasswordFlowTest::cancelExistingPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
const auto preservedChallenge = uut.challenge();
|
||||
QVERIFY2(preservedChallenge, "should have the supplied password challenge by now");
|
||||
QCOMPARE(preservedChallenge->cryptText(), m_challenge->cryptText());
|
||||
QCOMPARE(preservedChallenge->nonce(), m_challenge->nonce());
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 0);
|
||||
QCOMPARE(passwordAvailable.count(), 0);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
|
||||
// advance the state: cancel the request
|
||||
|
@ -207,11 +298,13 @@ void PasswordFlowTest::cancelExistingPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
QVERIFY2(!uut.challenge(), "should no longer acknowledge to the supplied password challenge");
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 0);
|
||||
QCOMPARE(passwordAvailable.count(), 0);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 1);
|
||||
}
|
||||
|
||||
|
@ -223,6 +316,7 @@ void PasswordFlowTest::supplyExistingPassword(void)
|
|||
QSignalSpy passwordAvailable(&uut, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy requestsCancelled(&uut, &accounts::AccountSecret::requestsCancelled);
|
||||
QSignalSpy keyAvailable(&uut, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&uut, &accounts::AccountSecret::keyFailed);
|
||||
|
||||
// check correct initial state is reported
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
|
@ -231,10 +325,15 @@ void PasswordFlowTest::supplyExistingPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
QVERIFY2(!uut.challenge(), "should not have a password challenge yet");
|
||||
|
||||
// advance the state: request password
|
||||
QVERIFY2(uut.requestExistingPassword(m_salt, *m_keyParams), "should be able to request a (existing) password");
|
||||
QVERIFY2(uut.requestExistingPassword(*m_challenge, m_salt, *keyParams), "should be able to request a (existing) password");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "request for (existing) password should be signalled");
|
||||
const auto suppliedChallenge = uut.challenge();
|
||||
QVERIFY2(suppliedChallenge, "should have the supplied password challenge by now");
|
||||
QCOMPARE(suppliedChallenge->cryptText(), m_challenge->cryptText());
|
||||
QCOMPARE(suppliedChallenge->nonce(), m_challenge->nonce());
|
||||
|
||||
// check the state is correctly updated
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
|
@ -248,14 +347,16 @@ void PasswordFlowTest::supplyExistingPassword(void)
|
|||
QCOMPARE(passwordAvailable.count(), 0);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
|
||||
// advance the state: supply password
|
||||
QString password(QStringLiteral("hello, world"));
|
||||
QString password = QString::fromUtf8(masterPassword());
|
||||
QString wiped = QStringLiteral("*").repeated(password.size());
|
||||
|
||||
QVERIFY2(uut.answerExistingPassword(password), "(existing) password should be accepted");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "availability of the (existing) password should be signalled");
|
||||
QCOMPARE(password, QStringLiteral("************"));
|
||||
QCOMPARE(password, wiped);
|
||||
|
||||
// check the state is correctly updated
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
|
@ -264,11 +365,16 @@ void PasswordFlowTest::supplyExistingPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), true);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
const auto preservedChallenge = uut.challenge();
|
||||
QVERIFY2(preservedChallenge, "should still have the same supplied password challenge after answering with a password");
|
||||
QCOMPARE(preservedChallenge->cryptText(), m_challenge->cryptText());
|
||||
QCOMPARE(preservedChallenge->nonce(), m_challenge->nonce());
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 0);
|
||||
QCOMPARE(passwordAvailable.count(), 1);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
|
||||
// advance the state: derive the master key
|
||||
|
@ -282,11 +388,119 @@ void PasswordFlowTest::supplyExistingPassword(void)
|
|||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), true);
|
||||
QVERIFY2(uut.key(), "should have a master key by now");
|
||||
const auto finalChallenge = uut.challenge();
|
||||
QVERIFY2(finalChallenge, "should still have the same supplied password challenge after key derivation");
|
||||
QCOMPARE(finalChallenge->cryptText(), m_challenge->cryptText());
|
||||
QCOMPARE(finalChallenge->nonce(), m_challenge->nonce());
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 0);
|
||||
QCOMPARE(passwordAvailable.count(), 1);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(keyAvailable.count(), 1);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
}
|
||||
|
||||
void PasswordFlowTest::retryExistingPassword(void)
|
||||
{
|
||||
accounts::AccountSecret uut;
|
||||
QSignalSpy existingPasswordNeeded(&uut, &accounts::AccountSecret::existingPasswordNeeded);
|
||||
QSignalSpy newPasswordNeeded(&uut, &accounts::AccountSecret::newPasswordNeeded);
|
||||
QSignalSpy passwordAvailable(&uut, &accounts::AccountSecret::passwordAvailable);
|
||||
QSignalSpy requestsCancelled(&uut, &accounts::AccountSecret::requestsCancelled);
|
||||
QSignalSpy keyAvailable(&uut, &accounts::AccountSecret::keyAvailable);
|
||||
QSignalSpy keyFailed(&uut, &accounts::AccountSecret::keyFailed);
|
||||
|
||||
// check correct initial state is reported
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
QCOMPARE(uut.isNewPasswordRequested(), false);
|
||||
QCOMPARE(uut.isExistingPasswordRequested(), false);
|
||||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
QVERIFY2(!uut.challenge(), "should not have a password challenge yet");
|
||||
|
||||
// advance the state: request password
|
||||
QVERIFY2(uut.requestExistingPassword(*m_challenge, m_salt, *keyParams), "should be able to request a (existing) password");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(existingPasswordNeeded), "request for (existing) password should be signalled");
|
||||
const auto suppliedChallenge = uut.challenge();
|
||||
QVERIFY2(suppliedChallenge, "should have the supplied password challenge by now");
|
||||
QCOMPARE(suppliedChallenge->cryptText(), m_challenge->cryptText());
|
||||
QCOMPARE(suppliedChallenge->nonce(), m_challenge->nonce());
|
||||
|
||||
// check the state is correctly updated
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
QCOMPARE(uut.isNewPasswordRequested(), false);
|
||||
QCOMPARE(uut.isExistingPasswordRequested(), true);
|
||||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 0);
|
||||
QCOMPARE(passwordAvailable.count(), 0);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(keyFailed.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
|
||||
// advance the state: supply wrong password
|
||||
QString wrongPassword(QStringLiteral("wrong"));
|
||||
QString wipedWrongPassword = QStringLiteral("*").repeated(wrongPassword.size());
|
||||
|
||||
QVERIFY2(uut.answerExistingPassword(wrongPassword), "password attempt should be accepted");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(passwordAvailable), "availability of an attempt should be signalled");
|
||||
QCOMPARE(wrongPassword, wipedWrongPassword);
|
||||
|
||||
// advance the state: attempt to derive the master key
|
||||
QVERIFY2(!uut.deriveKey(), "key derivation should fail on wrong password");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(keyFailed), "failure to derive the master key should be signalled");
|
||||
|
||||
// check the state is correctly updated
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
QCOMPARE(uut.isNewPasswordRequested(), false);
|
||||
QCOMPARE(uut.isExistingPasswordRequested(), true);
|
||||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), false);
|
||||
QCOMPARE(uut.key(), nullptr);
|
||||
const auto stillPreservedChallenge = uut.challenge();
|
||||
QVERIFY2(stillPreservedChallenge, "should still have the same supplied password challenge after answering with a password");
|
||||
QCOMPARE(stillPreservedChallenge->cryptText(), m_challenge->cryptText());
|
||||
QCOMPARE(stillPreservedChallenge->nonce(), m_challenge->nonce());
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 0);
|
||||
QCOMPARE(passwordAvailable.count(), 1);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(keyAvailable.count(), 0);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
|
||||
// advance the state: supply correct password
|
||||
QString correctPassword = QString::fromUtf8(masterPassword());
|
||||
QString wipedCorrectPassword = QStringLiteral("*").repeated(correctPassword.size());
|
||||
|
||||
QVERIFY2(uut.answerExistingPassword(correctPassword), "(existing) password should be accepted");
|
||||
QVERIFY2(test::signal_eventually_emitted_twice(passwordAvailable), "availability of the (existing) password should be signalled");
|
||||
QCOMPARE(correctPassword, wipedCorrectPassword);
|
||||
|
||||
// advance the state: attempt to derive the master key
|
||||
QVERIFY2(uut.deriveKey(), "key derivation should succeed");
|
||||
QVERIFY2(test::signal_eventually_emitted_once(keyAvailable), "availability of the master key should be signalled");
|
||||
|
||||
// check the state is correctly updated
|
||||
QCOMPARE(uut.isStillAlive(), true);
|
||||
QCOMPARE(uut.isNewPasswordRequested(), false);
|
||||
QCOMPARE(uut.isExistingPasswordRequested(), true);
|
||||
QCOMPARE(uut.isPasswordAvailable(), false);
|
||||
QCOMPARE(uut.isKeyAvailable(), true);
|
||||
QVERIFY2(uut.key(), "should have a master key by now");
|
||||
const auto finalChallenge = uut.challenge();
|
||||
QVERIFY2(finalChallenge, "should still have the same supplied password challenge after key derivation");
|
||||
QCOMPARE(finalChallenge->cryptText(), m_challenge->cryptText());
|
||||
QCOMPARE(finalChallenge->nonce(), m_challenge->nonce());
|
||||
|
||||
QCOMPARE(newPasswordNeeded.count(), 0);
|
||||
QCOMPARE(passwordAvailable.count(), 2);
|
||||
QCOMPARE(existingPasswordNeeded.count(), 1);
|
||||
QCOMPARE(keyAvailable.count(), 1);
|
||||
QCOMPARE(requestsCancelled.count(), 0);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
[master-key]
|
||||
algorithm=2
|
||||
challenge=G8MxLVGBZDbJ0ieJMUV5AV90YazOD4tE
|
||||
cpu=1
|
||||
length=32
|
||||
memory=8192
|
||||
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
|
||||
salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
|
||||
|
||||
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
[master-key]
|
||||
algorithm=2
|
||||
challenge=G8MxLVGBZDbJ0ieJMUV5AV90YazOD4tE
|
||||
cpu=1
|
||||
length=32
|
||||
memory=8192
|
||||
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
|
||||
salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
|
||||
|
||||
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
[master-key]
|
||||
algorithm=2
|
||||
challenge=G8MxLVGBZDbJ0ieJMUV5AV90YazOD4tE
|
||||
cpu=1
|
||||
length=32
|
||||
memory=8192
|
||||
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
|
||||
salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
|
||||
|
||||
[%7B3ff3fc9b-9e8c-50aa-8f51-99d213843761%7D]
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
[master-key]
|
||||
algorithm=2
|
||||
challenge=G8MxLVGBZDbJ0ieJMUV5AV90YazOD4tE
|
||||
cpu=1
|
||||
length=32
|
||||
memory=8192
|
||||
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
|
||||
salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
[master-key]
|
||||
algorithm=2
|
||||
challenge=G8MxLVGBZDbJ0ieJMUV5AV90YazOD4tE
|
||||
cpu=1
|
||||
length=32
|
||||
memory=8192
|
||||
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
|
||||
salt="MDEyMzQ1Njc4OUFCQ0RFRg=="
|
||||
|
||||
[%7B072a645d-6c26-57cc-81eb-d9ef3b9b39e2%7D]
|
||||
|
|
|
@ -18,10 +18,18 @@ namespace test
|
|||
salt.resize(crypto_pwhash_SALTBYTES);
|
||||
salt.fill('\x0', -1);
|
||||
QString password(QStringLiteral("password"));
|
||||
return useDummyPassword(secret, password, salt);
|
||||
QByteArray challenge = QByteArray::fromBase64("HG8yZFZRDbtkViPnLQCiRZco3PdjFuvn");
|
||||
QByteArray nonce = QByteArray::fromBase64("QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB");
|
||||
|
||||
std::optional<secrets::EncryptedSecret> verify = secrets::EncryptedSecret::from(challenge, nonce);
|
||||
if (!verify) {
|
||||
qDebug () << "Failed to construct password challenge object";
|
||||
return nullptr;
|
||||
}
|
||||
return useDummyPassword(secret, password, salt, *verify);
|
||||
}
|
||||
|
||||
secrets::SecureMasterKey * useDummyPassword(accounts::AccountSecret *secret, QString &password, QByteArray &salt)
|
||||
secrets::SecureMasterKey * useDummyPassword(accounts::AccountSecret *secret, QString &password, QByteArray &salt, const secrets::EncryptedSecret &challenge)
|
||||
{
|
||||
if (!secret) {
|
||||
qDebug () << "No account secret provided...";
|
||||
|
@ -36,7 +44,7 @@ namespace test
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
if (!secret->requestExistingPassword(salt, *keyParams)) {
|
||||
if (!secret->requestExistingPassword(challenge, salt, *keyParams)) {
|
||||
qDebug() << "Failed to simulate password request";
|
||||
return nullptr;
|
||||
}
|
||||
|
|
|
@ -16,9 +16,12 @@
|
|||
namespace test
|
||||
{
|
||||
secrets::SecureMasterKey * useDummyPassword(accounts::AccountSecret *secret);
|
||||
secrets::SecureMasterKey * useDummyPassword(accounts::AccountSecret *secret, QString &password, QByteArray &salt);
|
||||
secrets::SecureMasterKey * useDummyPassword(accounts::AccountSecret *secret,
|
||||
QString &password, QByteArray &salt,
|
||||
const secrets::EncryptedSecret &challenge);
|
||||
|
||||
std::optional<secrets::EncryptedSecret> encrypt(const accounts::AccountSecret *secret, const QByteArray &tokenSecret);
|
||||
std::optional<secrets::EncryptedSecret> encrypt(const accounts::AccountSecret *secret,
|
||||
const QByteArray &tokenSecret);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
|
|
@ -221,11 +221,25 @@ namespace accounts
|
|||
m_failed = true;
|
||||
QObject::disconnect(m_secret, &AccountSecret::requestsCancelled, this, &RequestAccountPassword::fail);
|
||||
QObject::disconnect(m_secret, &AccountSecret::passwordAvailable, this, &RequestAccountPassword::unlock);
|
||||
QObject::disconnect(m_secret, &AccountSecret::keyAvailable, this, &RequestAccountPassword::finish);
|
||||
Q_EMIT failed();
|
||||
Q_EMIT finished();
|
||||
}
|
||||
|
||||
void RequestAccountPassword::unlock(void)
|
||||
{
|
||||
secrets::SecureMasterKey * derived = m_secret->deriveKey();
|
||||
std::optional<secrets::EncryptedSecret> challenge = m_secret->challenge();
|
||||
if (derived && challenge) {
|
||||
qCInfo(logger) << "Successfully derived key for storage";
|
||||
return;
|
||||
} else {
|
||||
qCInfo(logger) << "Failed to unlock storage:"
|
||||
<< "Unable to derive secret encryption/decryption key or generate its matching challenge";
|
||||
}
|
||||
}
|
||||
|
||||
void RequestAccountPassword::finish(void)
|
||||
{
|
||||
if (m_succeeded || m_failed) {
|
||||
qCDebug(logger) << "Suppressing 'success' in unlocking accounts: already handled";
|
||||
|
@ -234,9 +248,20 @@ namespace accounts
|
|||
|
||||
QObject::disconnect(m_secret, &AccountSecret::requestsCancelled, this, &RequestAccountPassword::fail);
|
||||
QObject::disconnect(m_secret, &AccountSecret::passwordAvailable, this, &RequestAccountPassword::unlock);
|
||||
secrets::SecureMasterKey * derived = m_secret->deriveKey();
|
||||
QObject::disconnect(m_secret, &AccountSecret::keyAvailable, this, &RequestAccountPassword::finish);
|
||||
std::optional<secrets::EncryptedSecret> challenge = m_secret->challenge();
|
||||
secrets::SecureMasterKey * derived = m_secret->key();
|
||||
if (!derived) {
|
||||
qCInfo(logger) << "Failed to unlock storage: unable to derive secret encryption/decryption key";
|
||||
qCInfo(logger) << "Failed to finish unlocking storage: no secret encryption/decryption key";
|
||||
m_failed = true;
|
||||
Q_EMIT failed();
|
||||
Q_EMIT finished();
|
||||
return;
|
||||
}
|
||||
|
||||
// sanity check: challenge should be available once key derivation has completed successfully
|
||||
if (!challenge) {
|
||||
qCInfo(logger) << "Failed to finish unlocking storage: no challenge for encryption/decryption key";
|
||||
m_failed = true;
|
||||
Q_EMIT failed();
|
||||
Q_EMIT finished();
|
||||
|
@ -244,7 +269,7 @@ namespace accounts
|
|||
}
|
||||
|
||||
bool ok = false;
|
||||
m_settings([derived, &ok](QSettings &settings) -> void
|
||||
m_settings([derived, &challenge, &ok](QSettings &settings) -> void
|
||||
{
|
||||
if (!settings.isWritable()) {
|
||||
qCWarning(logger) << "Unable to save account secret key parameters: storage not writable";
|
||||
|
@ -254,12 +279,16 @@ namespace accounts
|
|||
const secrets::KeyDerivationParameters params = derived->params();
|
||||
|
||||
QString encodedSalt = QString::fromUtf8(derived->salt().toBase64(QByteArray::Base64Encoding));
|
||||
QString encodedChallenge = QString::fromUtf8(challenge->cryptText().toBase64(QByteArray::Base64Encoding));
|
||||
QString encodedNonce = QString::fromUtf8(challenge->nonce().toBase64(QByteArray::Base64Encoding));
|
||||
settings.beginGroup(QStringLiteral("master-key"));
|
||||
settings.setValue(QStringLiteral("salt"), encodedSalt);
|
||||
settings.setValue(QStringLiteral("cpu"), params.cpuCost());
|
||||
settings.setValue(QStringLiteral("memory"), (quint64) params.memoryCost());
|
||||
settings.setValue(QStringLiteral("algorithm"), params.algorithm());
|
||||
settings.setValue(QStringLiteral("length"), params.keyLength());
|
||||
settings.setValue(QStringLiteral("nonce"), encodedNonce);
|
||||
settings.setValue(QStringLiteral("challenge"), encodedChallenge);
|
||||
settings.endGroup();
|
||||
ok = true;
|
||||
});
|
||||
|
@ -269,7 +298,7 @@ namespace accounts
|
|||
m_succeeded = true;
|
||||
Q_EMIT unlocked();
|
||||
} else {
|
||||
qCInfo(logger) << "Failed to unlock storage: unable to store parameters";
|
||||
qCInfo(logger) << "Failed to finish unlocking storage: unable to store parameters";
|
||||
m_failed = true;
|
||||
Q_EMIT failed();
|
||||
}
|
||||
|
@ -288,6 +317,7 @@ namespace accounts
|
|||
|
||||
QObject::connect(m_secret, &AccountSecret::passwordAvailable, this, &RequestAccountPassword::unlock);
|
||||
QObject::connect(m_secret, &AccountSecret::requestsCancelled, this, &RequestAccountPassword::fail);
|
||||
QObject::connect(m_secret, &AccountSecret::keyAvailable, this, &RequestAccountPassword::finish);
|
||||
|
||||
if (!m_secret->isStillAlive()) {
|
||||
qCDebug(logger) << "Unable to request accounts password: account secret marked for death";
|
||||
|
@ -312,6 +342,8 @@ namespace accounts
|
|||
|
||||
settings.beginGroup(QStringLiteral("master-key"));
|
||||
QByteArray salt;
|
||||
QByteArray nonce;
|
||||
QByteArray challenge;
|
||||
quint64 cpuCost = 0ULL;
|
||||
quint64 keyLength = 0ULL;
|
||||
size_t memoryCost = 0ULL;
|
||||
|
@ -331,18 +363,29 @@ namespace accounts
|
|||
if (ok) {
|
||||
QByteArray encodedSalt = settings.value(QStringLiteral("salt")).toString().toUtf8();
|
||||
salt = QByteArray::fromBase64(encodedSalt, QByteArray::Base64Encoding);
|
||||
ok = secrets::SecureMasterKey::validate(salt);
|
||||
ok = !salt.isEmpty() && secrets::SecureMasterKey::validate(salt);
|
||||
}
|
||||
if (ok) {
|
||||
QByteArray encodedChallenge = settings.value(QStringLiteral("challenge")).toString().toUtf8();
|
||||
challenge = QByteArray::fromBase64(encodedChallenge, QByteArray::Base64Encoding);
|
||||
ok = !challenge.isEmpty();
|
||||
}
|
||||
if (ok) {
|
||||
QByteArray encodedNonce = settings.value(QStringLiteral("nonce")).toString().toUtf8();
|
||||
nonce = QByteArray::fromBase64(encodedNonce, QByteArray::Base64Encoding);
|
||||
ok = !nonce.isEmpty();
|
||||
}
|
||||
settings.endGroup();
|
||||
|
||||
const auto params = secrets::KeyDerivationParameters::create(keyLength, algorithm, memoryCost, cpuCost);
|
||||
if (!ok || !params || !secrets::SecureMasterKey::validate(*params)) {
|
||||
qCDebug(logger) << "Unable to request 'existing' password: invalid salt or key derivation parameters";
|
||||
const auto encryptedChallenge = secrets::EncryptedSecret::from(challenge, nonce);
|
||||
if (!ok || !params || !secrets::SecureMasterKey::validate(*params) || !encryptedChallenge) {
|
||||
qCDebug(logger) << "Unable to request 'existing' password: invalid challenge, nonce, salt or key derivation parameters";
|
||||
return;
|
||||
}
|
||||
|
||||
qCInfo(logger) << "Requesting 'existing' password for accounts";
|
||||
ok = m_secret->requestExistingPassword(salt, *params);
|
||||
ok = m_secret->requestExistingPassword(*encryptedChallenge, salt, *params);
|
||||
});
|
||||
|
||||
if (!ok) {
|
||||
|
|
|
@ -52,6 +52,7 @@ namespace accounts
|
|||
private Q_SLOTS:
|
||||
void fail(void);
|
||||
void unlock(void);
|
||||
void finish(void);
|
||||
Q_SIGNALS:
|
||||
void unlocked(void);
|
||||
void failed(void);
|
||||
|
|
|
@ -13,7 +13,8 @@ KEYSMITH_LOGGER(logger, ".accounts.keys")
|
|||
namespace accounts
|
||||
{
|
||||
AccountSecret::AccountSecret(const secrets::SecureRandom &random, QObject *parent) :
|
||||
QObject(parent), m_stillAlive(true), m_newPassword(false), m_passwordRequested(false), m_random(random), m_salt(std::nullopt), m_key(nullptr), m_password(nullptr), m_keyParams(std::nullopt)
|
||||
QObject(parent), m_stillAlive(true), m_newPassword(false), m_passwordRequested(false), m_random(random),
|
||||
m_salt(std::nullopt), m_challenge(std::nullopt), m_key(nullptr), m_password(nullptr), m_keyParams(std::nullopt)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -47,7 +48,8 @@ namespace accounts
|
|||
return true;
|
||||
}
|
||||
|
||||
bool AccountSecret::requestExistingPassword(const QByteArray& salt, const secrets::KeyDerivationParameters &keyParams)
|
||||
bool AccountSecret::requestExistingPassword(const secrets::EncryptedSecret &challenge,
|
||||
const QByteArray& salt, const secrets::KeyDerivationParameters &keyParams)
|
||||
{
|
||||
if (!m_stillAlive) {
|
||||
qCDebug(logger) << "Ignoring request for 'existing' password: account secret is marked for death";
|
||||
|
@ -74,6 +76,7 @@ namespace accounts
|
|||
m_newPassword = false;
|
||||
m_keyParams.emplace(keyParams);
|
||||
m_salt.emplace(salt);
|
||||
m_challenge.emplace(challenge);
|
||||
Q_EMIT existingPasswordNeeded();
|
||||
return true;
|
||||
}
|
||||
|
@ -93,7 +96,7 @@ namespace accounts
|
|||
return false;
|
||||
}
|
||||
|
||||
if (m_key || m_password) {
|
||||
if (m_key || (m_password && !m_challenge)) {
|
||||
qCDebug(logger) << "Ignoring password: duplicate/conflicting password";
|
||||
password.fill(QLatin1Char('*'), -1);
|
||||
return false;
|
||||
|
@ -129,7 +132,7 @@ namespace accounts
|
|||
|
||||
bool AccountSecret::answerExistingPassword(QString &password)
|
||||
{
|
||||
bool result = acceptPassword(password, m_keyParams && m_salt);
|
||||
bool result = acceptPassword(password, m_keyParams && m_salt && m_challenge);
|
||||
if (result) {
|
||||
Q_EMIT passwordAvailable();
|
||||
}
|
||||
|
@ -144,7 +147,7 @@ namespace accounts
|
|||
return false;
|
||||
}
|
||||
|
||||
bool result = acceptPassword(password, !m_keyParams && !m_salt);
|
||||
bool result = acceptPassword(password, !m_keyParams && !m_salt && !m_challenge);
|
||||
if (result) {
|
||||
m_keyParams.emplace(keyParams);
|
||||
Q_EMIT passwordAvailable();
|
||||
|
@ -177,6 +180,11 @@ namespace accounts
|
|||
return m_stillAlive && m_password;
|
||||
}
|
||||
|
||||
bool AccountSecret::isChallengeAvailable(void) const
|
||||
{
|
||||
return m_stillAlive && m_challenge;
|
||||
}
|
||||
|
||||
secrets::SecureMasterKey * AccountSecret::deriveKey(void)
|
||||
{
|
||||
if (!m_stillAlive) {
|
||||
|
@ -197,18 +205,50 @@ namespace accounts
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
m_key.reset(m_salt
|
||||
secrets::SecureMasterKey * derived = m_salt
|
||||
? secrets::SecureMasterKey::derive(m_password.data(), *m_keyParams, *m_salt, m_random)
|
||||
: secrets::SecureMasterKey::derive(m_password.data(), *m_keyParams, m_random)
|
||||
);
|
||||
: secrets::SecureMasterKey::derive(m_password.data(), *m_keyParams, m_random);
|
||||
|
||||
if (!m_key) {
|
||||
if (!derived) {
|
||||
qCDebug(logger) << "Failed to derive encryption/decryption key for account secrets";
|
||||
m_password.reset(nullptr);
|
||||
Q_EMIT keyFailed();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (m_challenge) {
|
||||
QScopedPointer<secrets::SecureMemory> result(derived->decrypt(*m_challenge));
|
||||
if (!result) {
|
||||
qCDebug(logger) << "Failed to derive encryption/decryption key for account secrets: challenge failed";
|
||||
m_password.reset(nullptr);
|
||||
delete derived;
|
||||
Q_EMIT keyFailed();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool sizeMismatch = result->size() != m_password->size();
|
||||
const unsigned char * const other = sizeMismatch ? m_password->constData() : result->constData();
|
||||
if (std::memcmp(m_password->constData(), other, m_password->size()) != 0 || sizeMismatch) {
|
||||
qCDebug(logger) << "Failed to derive encryption/decryption key for account secrets: challenge failed";
|
||||
m_password.reset(nullptr);
|
||||
delete derived;
|
||||
Q_EMIT keyFailed();
|
||||
return nullptr;
|
||||
}
|
||||
} else {
|
||||
std::optional<secrets::EncryptedSecret> challenge = derived->encrypt(m_password.data());
|
||||
if (!challenge) {
|
||||
qCDebug(logger) << "Failed to derive encryption/decryption key for account secrets: unable to generate challenge";
|
||||
m_password.reset(nullptr);
|
||||
delete derived;
|
||||
Q_EMIT keyFailed();
|
||||
return nullptr;
|
||||
}
|
||||
m_challenge.emplace(*challenge);
|
||||
}
|
||||
|
||||
qCDebug(logger) << "Successfully derived encryption/decryption key for account secrets";
|
||||
m_key.reset(derived);
|
||||
m_salt.emplace(m_key->salt());
|
||||
m_password.reset(nullptr);
|
||||
Q_EMIT keyAvailable();
|
||||
|
@ -217,7 +257,12 @@ namespace accounts
|
|||
|
||||
secrets::SecureMasterKey * AccountSecret::key(void) const
|
||||
{
|
||||
return m_stillAlive && m_key ? m_key.data() : nullptr;
|
||||
return isKeyAvailable() ? m_key.data() : nullptr;
|
||||
}
|
||||
|
||||
std::optional<secrets::EncryptedSecret> AccountSecret::challenge(void) const
|
||||
{
|
||||
return isChallengeAvailable() ? m_challenge : std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<secrets::EncryptedSecret> AccountSecret::encrypt(const secrets::SecureMemory *secret) const
|
||||
|
|
|
@ -18,6 +18,7 @@ namespace accounts
|
|||
Q_SIGNALS:
|
||||
void newPasswordNeeded(void);
|
||||
void existingPasswordNeeded(void);
|
||||
void keyFailed(void);
|
||||
void passwordAvailable(void);
|
||||
void keyAvailable(void);
|
||||
void requestsCancelled(void);
|
||||
|
@ -25,7 +26,8 @@ namespace accounts
|
|||
AccountSecret(const secrets::SecureRandom &random = secrets::defaultSecureRandom, QObject *parent = nullptr);
|
||||
void cancelRequests(void);
|
||||
bool requestNewPassword(void);
|
||||
bool requestExistingPassword(const QByteArray& salt, const secrets::KeyDerivationParameters &keyParams);
|
||||
bool requestExistingPassword(const secrets::EncryptedSecret &challenge,
|
||||
const QByteArray& salt, const secrets::KeyDerivationParameters &keyParams);
|
||||
|
||||
bool answerExistingPassword(QString &password);
|
||||
bool answerNewPassword(QString &password, const secrets::KeyDerivationParameters &keyParams);
|
||||
|
@ -33,6 +35,7 @@ namespace accounts
|
|||
secrets::SecureMasterKey * deriveKey(void);
|
||||
|
||||
secrets::SecureMasterKey * key(void) const;
|
||||
std::optional<secrets::EncryptedSecret> challenge(void) const;
|
||||
std::optional<secrets::EncryptedSecret> encrypt(const secrets::SecureMemory *secret) const;
|
||||
secrets::SecureMemory * decrypt(const secrets::EncryptedSecret &secret) const;
|
||||
bool isStillAlive(void) const;
|
||||
|
@ -40,6 +43,7 @@ namespace accounts
|
|||
bool isExistingPasswordRequested(void) const;
|
||||
bool isKeyAvailable(void) const;
|
||||
bool isPasswordAvailable(void) const;
|
||||
bool isChallengeAvailable(void) const;
|
||||
private:
|
||||
bool acceptPassword(QString &password, bool answerMatchesRequest);
|
||||
private:
|
||||
|
@ -48,6 +52,7 @@ namespace accounts
|
|||
bool m_passwordRequested;
|
||||
const secrets::SecureRandom m_random;
|
||||
std::optional<QByteArray> m_salt;
|
||||
std::optional<secrets::EncryptedSecret> m_challenge;
|
||||
QScopedPointer<secrets::SecureMasterKey> m_key;
|
||||
QScopedPointer<secrets::SecureMemory> m_password;
|
||||
std::optional<secrets::KeyDerivationParameters> m_keyParams;
|
||||
|
|
Loading…
Reference in New Issue