Store connection configuration as key value pairs

Add migration for the sqlite database.
Because the Qt SQL library is a bit hard to work with use sqlite through custom wrapper.
This commit is contained in:
eelke 2025-02-22 19:59:24 +01:00
parent 4caccf1000
commit aac55b0ed1
17 changed files with 276439 additions and 384 deletions

View file

@ -0,0 +1,53 @@
#include "KeyStrengthener.h"
#include <botan/base64.h>
KeyStrengthener::KeyStrengthener(std::unique_ptr<Botan::PasswordHash> hasher, Botan::secure_vector<uint8_t> salt, size_t keysize)
: m_hasher (std::move(hasher))
, m_salt (std::move(salt))
, m_keySize(keysize)
{}
KeyStrengthener::KeyStrengthener(KeyStrengthener &&rhs)
: m_hasher (std::move(rhs.m_hasher))
, m_salt (std::move(rhs.m_salt))
, m_keySize(rhs.m_keySize)
{}
KeyStrengthener &KeyStrengthener::operator=(KeyStrengthener &&rhs)
{
if (&rhs != this) {
m_hasher = std::move(rhs.m_hasher);
m_salt = std::move(rhs.m_salt);
m_keySize = rhs.m_keySize;
}
return *this;
}
Botan::secure_vector<uint8_t> KeyStrengthener::derive(const std::string &passphrase)
{
Botan::secure_vector<uint8_t> master_key(m_keySize);
m_hasher->derive_key(master_key.data(), master_key.size(), passphrase.c_str(), passphrase.length(), m_salt.data(), m_salt.size());
return master_key;
}
void KeyStrengthener::saveParams(SQLiteConnection &db, const QString &table_name)
{
size_t i1 = m_hasher->memory_param();
size_t i2 = m_hasher->iterations();
size_t i3 = m_hasher->parallelism();
auto salt_str = QString::fromUtf8(Botan::base64_encode(m_salt).c_str());
// SAVE parameters in database
auto stmt = db.Prepare("INSERT OR REPLACE INTO " + table_name + "(id, algo, i1, i2, i3, ks, salt) "
+ "VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7)");
stmt.Bind(1, 1);
stmt.Bind(2, "Scrypt");
stmt.Bind(3, i1);
stmt.Bind(4, i2);
stmt.Bind(5, i3);
stmt.Bind(6, m_keySize);
stmt.Bind(7, salt_str);
stmt.Step();
}

View file

@ -0,0 +1,29 @@
#ifndef KEYSTRENGTHENER_H
#define KEYSTRENGTHENER_H
#include <botan/pwdhash.h>
#include <botan/secmem.h>
#include <memory>
#include "sqlite/SQLiteConnection.h"
class KeyStrengthener {
public:
KeyStrengthener() = default;
KeyStrengthener(std::unique_ptr<Botan::PasswordHash> hasher, Botan::secure_vector<uint8_t> salt, size_t keysize);
KeyStrengthener(const KeyStrengthener&) = delete;
KeyStrengthener& operator=(const KeyStrengthener &) = delete;
KeyStrengthener(KeyStrengthener &&rhs);
KeyStrengthener& operator=(KeyStrengthener &&rhs);
Botan::secure_vector<uint8_t> derive(const std::string &passphrase);
void saveParams(SQLiteConnection &db, const QString &table_name);
private:
std::unique_ptr<Botan::PasswordHash> m_hasher;
Botan::secure_vector<uint8_t> m_salt;
size_t m_keySize;
};
#endif // KEYSTRENGTHENER_H

View file

@ -0,0 +1,275 @@
#include "PasswordManager.h"
#include <QDebug>
#include <QVariant>
#include <botan/hash.h>
#include <botan/auto_rng.h>
#include <botan/base64.h>
#include <botan/nist_keywrap.h>
#include <botan/base64.h>
#include <botan/mac.h>
#include <botan/block_cipher.h>
#include <boost/lexical_cast.hpp>
using namespace Botan;
class PasswordCryptoEngine {
public:
PasswordCryptoEngine(const secure_vector<uint8_t>& master_key)
{
m_cipher = BlockCipher::create_or_throw("AES-256");
m_hmac = MessageAuthenticationCode::create_or_throw("HMAC(SHA-256)");
m_hmac->set_key(master_key);
m_cipher->set_key(m_hmac->process("wrap"));
m_hmac->set_key(m_hmac->process("hmac"));
}
std::string set(const std::string& name, const uint8_t val[], size_t len) const
{
/*
* Both as a basic precaution wrt key seperation, and specifically to prevent
* cut-and-paste attacks against the database, each PSK is encrypted with a
* distinct key which is derived by hashing the wrapped key name with HMAC.
*/
const std::vector<uint8_t> wrapped_name =
nist_key_wrap_padded(cast_char_ptr_to_uint8(name.data()),
name.size(),
*m_cipher);
std::unique_ptr<BlockCipher> wrap_cipher(m_cipher->clone());
wrap_cipher->set_key(m_hmac->process(wrapped_name));
const std::vector<uint8_t> wrapped_key = nist_key_wrap_padded(val, len, *wrap_cipher);
return base64_encode(wrapped_key);
}
secure_vector<uint8_t> get(const std::string& name, const std::string_view &wrapped_key) const
{
const std::vector<uint8_t> wrapped_name =
nist_key_wrap_padded(cast_char_ptr_to_uint8(name.data()),
name.size(),
*m_cipher);
const secure_vector<uint8_t> val = base64_decode(wrapped_key.data(), wrapped_key.size());
std::unique_ptr<BlockCipher> wrap_cipher(m_cipher->clone());
wrap_cipher->set_key(m_hmac->process(wrapped_name));
return nist_key_unwrap_padded(val.data(), val.size(), *wrap_cipher);
}
private:
std::unique_ptr<BlockCipher> m_cipher;
std::unique_ptr<MessageAuthenticationCode> m_hmac;
};
// -------------------------
PasswordManager::PasswordManager() = default;
PasswordManager::~PasswordManager() = default;
bool PasswordManager::initialized(SQLiteConnection& db)
{
return isPskStoreInitialized(db);
}
std::tuple<Botan::secure_vector<uint8_t>, Botan::secure_vector<uint8_t>>
PasswordManager::deriveKey(KeyStrengthener &ks, QString passphrase)
{
auto master_key = ks.derive(passphrase.toUtf8().data());
std::unique_ptr<Botan::HashFunction> hash3(Botan::HashFunction::create("SHA-3"));
hash3->update(master_key);
auto mkh = hash3->final();
return { master_key, mkh };
}
bool PasswordManager::createDatabase(SQLiteConnection &db, QString passphrase)
{
m_cryptoEngine.reset();
if (!isPskStoreInitialized(db)) {
initializeNewPskStore(db);
auto ks = createKeyStrengthener();
ks.saveParams(db, m_secretAlgoTableName);
auto [master_key, mkh_bin] = deriveKey(ks, passphrase);
auto mkh = QString::fromUtf8(Botan::base64_encode(mkh_bin).c_str());
auto q_ins_hash = db.Prepare(
"INSERT INTO " + m_secretHashTableName + "(id, hash) VALUES(?1, ?2)");
q_ins_hash.Bind(1, 1);
q_ins_hash.Bind(2, mkh);
q_ins_hash.Step();
m_cryptoEngine = std::make_unique<PasswordCryptoEngine>(master_key);
return true;
}
return false;
}
bool PasswordManager::openDatabase(SQLiteConnection &db, QString passphrase)
{
m_cryptoEngine.reset();
if (isPskStoreInitialized(db)) {
auto ks = getKeyStrengthener(db);
auto [master_key, mkh_bin] = deriveKey(ks, passphrase);
auto q = db.Prepare("SELECT hash FROM " + m_secretHashTableName + " WHERE id=1");
if (q.Step()) {
QByteArray hash_b64 = q.ColumnCharPtr(0);
auto hash_bin = Botan::base64_decode(hash_b64.data(), static_cast<size_t>(hash_b64.size()));
if (hash_bin == mkh_bin) {
m_cryptoEngine = std::make_unique<PasswordCryptoEngine>(master_key);
return true;
}
}
}
return false;
}
void PasswordManager::closeDatabase()
{
m_cryptoEngine.reset();
}
bool PasswordManager::locked() const
{
return m_cryptoEngine == nullptr;
}
void PasswordManager::resetMasterPassword(SQLiteConnection &db)
{
if (!isPskStoreInitialized(db))
return;
closeDatabase();
auto del_algo = db.Prepare("DELETE FROM " + m_secretAlgoTableName + " WHERE id=1");
del_algo.Step();
auto del_hash = db.Prepare("DELETE FROM " + m_secretHashTableName + " WHERE id=1");
del_hash.Step();
}
std::string PasswordManager::encrypt(const std::string &name, const std::string &passwd)
{
if (m_cryptoEngine) {
return m_cryptoEngine->set(name, reinterpret_cast<const uint8_t*>(passwd.data()), passwd.length());
}
else {
throw PasswordManagerLockedException();
}
}
std::string PasswordManager::decrypt(const std::string &id, const std::string_view &encpwd)
{
if (m_cryptoEngine)
{
try
{
secure_vector<uint8_t> decoded = m_cryptoEngine->get(id, encpwd);
return std::string(reinterpret_cast<const char*>(decoded.data()), decoded.size());
}
catch (const Botan::Exception &ex)
{
throw PasswordManagerException(ex.what());
}
}
else {
throw PasswordManagerLockedException();
}
}
void PasswordManager::initializeNewPskStore(SQLiteConnection &db)
{
// // Create tables
// // - psk_masterkey_algo
// // - psk_passwd
{
auto create_tbl = db.Prepare(
"CREATE TABLE IF NOT EXISTS " + m_secretAlgoTableName + "( \n"
" id INTEGER PRIMARY KEY, \n"
" algo TEXT, \n"
" i1 INTEGER, \n"
" i2 INTEGER, \n"
" i3 INTEGER, \n"
" ks INTEGER, \n"
" salt TEXT \n"
");");
create_tbl.Step();
}
auto create_tbl = db.Prepare(
"CREATE TABLE IF NOT EXISTS " + m_secretHashTableName + "( \n"
" id INTEGER PRIMARY KEY, \n"
" hash TEXT \n"
");");
create_tbl.Step();
}
bool PasswordManager::isPskStoreInitialized(SQLiteConnection& db)
{
// Is the table with the secret data present and filled?
auto query = db.Prepare("SELECT name FROM sqlite_master WHERE type='table' AND name=?1");
query.Bind(1, m_secretAlgoTableName);
if (!query.Step()) {
return false;
}
query.Reset();
query.Bind(1, m_secretHashTableName);
if (!query.Step()) {
return false;
}
auto sel_algo = db.Prepare("SELECT algo FROM " + m_secretAlgoTableName + " WHERE id=1");
if (!sel_algo.Step()) {
return false;
}
auto sel_hash = db.Prepare("SELECT hash FROM " + m_secretHashTableName + " WHERE id=1");
if (!sel_hash.Step()) {
return false;
}
return true;
}
KeyStrengthener PasswordManager::getKeyStrengthener(SQLiteConnection &db)
{
auto query = db.Prepare("SELECT algo, i1, i2, i3, ks, salt FROM " + m_secretAlgoTableName + " WHERE id=1");
if (query.Step()) {
std::string algo = query.ColumnCharPtr(0);
size_t i1 = query.ColumnInteger(1);
size_t i2 = query.ColumnInteger(2);
size_t i3 = query.ColumnInteger(3);
size_t ks = query.ColumnInteger(4);
QByteArray salt = query.ColumnCharPtr(5);
auto pwh_fam = Botan::PasswordHashFamily::create(algo);
return KeyStrengthener(
pwh_fam->from_params(i1, i2, i3),
Botan::base64_decode(salt.data(), static_cast<size_t>(salt.size())),
ks
);
}
else {
throw std::runtime_error("fail");
}
}
KeyStrengthener PasswordManager::createKeyStrengthener()
{
size_t key_size = 64;
Botan::secure_vector<uint8_t> salt(key_size);
Botan::AutoSeeded_RNG rng;
rng.randomize(salt.data(), salt.size());
const std::string algo = "Scrypt";
auto pwh_fam = Botan::PasswordHashFamily::create(algo);
return KeyStrengthener(
pwh_fam->tune(key_size, std::chrono::seconds(2), 130),
salt,
key_size
);
}

View file

@ -0,0 +1,78 @@
#ifndef PASSWORDMANAGER_H
#define PASSWORDMANAGER_H
#include "utils/KeyStrengthener.h"
#include <botan/secmem.h>
#include <string>
#include <string_view>
#include <tuple>
#include <memory>
#include <botan/pwdhash.h>
namespace Botan {
class Encrypted_PSK_Database;
class PasswordHash;
}
class PasswordManagerException: public std::exception {
public:
using std::exception::exception; //(char const* const _Message);
};
class PasswordManagerLockedException: public PasswordManagerException {
public:
using PasswordManagerException::PasswordManagerException;
};
class PasswordCryptoEngine;
class PasswordManager {
public:
enum Result {
Ok,
Locked,
Error
};
PasswordManager();
~PasswordManager();
/** Check if it has been initialized before.
*
* If returns false then use createDatabase to set it up
* else use openDatabase to get access.
*/
bool initialized(SQLiteConnection &db);
bool createDatabase(SQLiteConnection &db, QString passphrase);
/// Opens the PSK database
bool openDatabase(SQLiteConnection &db, QString passphrase);
void closeDatabase();
bool locked() const;
void resetMasterPassword(SQLiteConnection &db);
std::string encrypt(const std::string &id, const std::string &passwd);
std::string decrypt(const std::string &id, const std::string_view &encpwd);
private:
QString m_passwordTableName = "psk_passwd";
QString m_secretAlgoTableName = "psk_masterkey_algo";
QString m_secretHashTableName = "psk_masterkey_hash";
std::unique_ptr<PasswordCryptoEngine> m_cryptoEngine;
bool isPskStoreInitialized(SQLiteConnection& db);
void initializeNewPskStore(SQLiteConnection &db);
/// Get PasswordHash from parameters in database
KeyStrengthener getKeyStrengthener(SQLiteConnection &db);
KeyStrengthener createKeyStrengthener();
std::tuple<Botan::secure_vector<uint8_t>, Botan::secure_vector<uint8_t>>
deriveKey(KeyStrengthener &ks, QString passphrase);
};
#endif // PASSWORDMANAGER_H