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
Johan Ouwerkerk 2020-11-22 16:20:48 +01:00 committed by Bhushan Shah
parent 4507c5e0be
commit cbd069085e
16 changed files with 454 additions and 41 deletions

View File

@ -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);

View File

@ -1,6 +1,8 @@
[master-key]
algorithm=2
challenge="8Crw0DTl6z7hb/ZFcDbtf5m4kLJkCfcbZcSP4w=="
cpu=1
length=32
memory=8192
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
salt="MDEyMzQ1Njc4OUFCQ0RFRg=="

View File

@ -1,6 +1,8 @@
[master-key]
algorithm=2
challenge="bvw8JNdx+PBUt/CzbBGcTCMkdAY8NhlQfR1P2A=="
cpu=3
length=32
memory=268435456
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
salt="QUFBQUFBQUFBQUFBQUFBQQ=="

View File

@ -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

View File

@ -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);
}

View File

@ -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]

View File

@ -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]

View File

@ -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]

View File

@ -1,6 +1,8 @@
[master-key]
algorithm=2
challenge=G8MxLVGBZDbJ0ieJMUV5AV90YazOD4tE
cpu=1
length=32
memory=8192
nonce=QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB
salt="MDEyMzQ1Njc4OUFCQ0RFRg=="

View File

@ -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]

View File

@ -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;
}

View File

@ -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

View File

@ -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) {

View File

@ -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);

View File

@ -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

View File

@ -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;