Различные статьи по Qt и c++

Итоговый мини-проект: безопасное хранилище паролей с шифрованием и мастер-паролем

Введение

В заключительной статье цикла собираем итоговый проект — простой менеджер паролей, объединяющий все техники цикла: вывод ключа из мастер-пароля пользователя (статьи 372, 379), шифрование хранимых паролей (статья 373), и защищённое хранение производного ключа через системное хранилище (статья 377), демонстрируя их совместную, согласованную работу в одном приложении.

Концепция

Архитектура проекта: пользователь вводит единственный мастер-пароль при первом запуске, из которого через PBKDF2/Argon2 (статья 372) выводится ключ шифрования; этот ключ используется для шифрования (AES-GCM, статья 373) каждой отдельно сохраняемой пары «сайт/пароль» перед записью в локальную базу данных; сама соль (но не сам ключ и не сам мастер-пароль) хранится открыто рядом с зашифрованными данными, а опционально сам производный ключ может быть дополнительно закеширован в системном хранилище секретов (статья 377) для избежания повторного ввода мастер-пароля при каждом запуске на доверенном устройстве.

Пример кода: Итоговый мини-проект

// passwordvault.h — итоговый класс, объединяющий все техники цикла
#pragma once
#include <QByteArray>
#include <QSqlDatabase>
#include <QSqlQuery>

class PasswordVault
{
public:
    bool initializeWithMasterPassword(const QString &masterPassword, bool isFirstRun)
    {
        if (isFirstRun) {
            // Новый, случайный salt для этого хранилища (статья 372, 378)
            m_salt = generateSecureRandomBytesOpenSsl(16);
            saveSaltToDatabase(m_salt);
        } else {
            m_salt = loadSaltFromDatabase();
        }

        // Вывод ключа шифрования из мастер-пароля через PBKDF2 (статья 379)
        m_derivedKey.resize(32);
        PKCS5_PBKDF2_HMAC(masterPassword.toUtf8().constData(), masterPassword.toUtf8().size(),
                          reinterpret_cast<const unsigned char *>(m_salt.constData()), m_salt.size(),
                          200000, EVP_sha256(), m_derivedKey.size(),
                          reinterpret_cast<unsigned char *>(m_derivedKey.data()));

        return isFirstRun || verifyMasterPassword();
    }

    void addEntry(const QString &site, const QString &username, const QString &password)
    {
        // Шифрование пароля через AES-GCM с производным ключом (статья 373)
        EncryptedData encrypted = encryptAesGcm(password.toUtf8(), m_derivedKey);

        QSqlQuery query;
        query.prepare("INSERT INTO entries (site, username, ciphertext, iv, tag) VALUES (?, ?, ?, ?, ?)");
        query.addBindValue(site);
        query.addBindValue(username);
        query.addBindValue(encrypted.ciphertext);
        query.addBindValue(encrypted.iv);
        query.addBindValue(encrypted.tag);
        query.exec();
    }

    QString getPassword(const QString &site)
    {
        QSqlQuery query;
        query.prepare("SELECT ciphertext, iv, tag FROM entries WHERE site = ?");
        query.addBindValue(site);
        query.exec();

        if (!query.next()) {
            return QString();
        }

        EncryptedData encrypted;
        encrypted.ciphertext = query.value(0).toByteArray();
        encrypted.iv = query.value(1).toByteArray();
        encrypted.tag = query.value(2).toByteArray();

        try {
            QByteArray decrypted = decryptAesGcm(encrypted, m_derivedKey);
            return QString::fromUtf8(decrypted);
        } catch (const std::runtime_error &) {
            // Неудачная расшифровка — неверный мастер-пароль или повреждённые данные
            return QString();
        }
    }

private:
    bool verifyMasterPassword()
    {
        // Проверка через попытку расшифровать специальный, заранее известный "канареечный" элемент,
        // сохранённый при первом запуске — неудачная расшифровка означает неверный мастер-пароль
        QString canaryValue = getPassword("__canary__");
        return canaryValue == "valid";
    }

    QByteArray m_salt;
    QByteArray m_derivedKey;
};
// Опциональное кеширование производного ключа в системном хранилище (статья 377) —
// чтобы не запрашивать мастер-пароль при каждом запуске на доверенном устройстве пользователя
void cacheKeyInKeychain(const QByteArray &derivedKey)
{
    auto *job = new QKeychain::WritePasswordJob("PasswordVaultApp");
    job->setKey("derived_key_cache");
    job->setBinaryData(derivedKey);
    job->start();
}

Пояснения к коду

initializeWithMasterPassword() объединяет техники нескольких статей цикла в одном связующем методе — генерация/загрузка соли (статьи 372, 378), вывод ключа через PBKDF2 с явно заданным количеством итераций (200000 — выше, чем в примере статьи 379, что отражает реальный, разумный для production выбор параметра стоимости), и проверка корректности введённого мастер-пароля через «канареечный» подход (попытка расшифровать заранее известное, ожидаемое значение — стандартный практический паттерн проверки пароля для систем, где сам производный ключ напрямую не с чем сравнить, в отличие от обычной проверки хешированного пароля статьи 372). getPassword() показывает корректную обработку неудачной расшифровки через перехват исключения — это либо означает, что введённый при инициализации мастер-пароль был неверным (что также обнаружилось бы раньше через verifyMasterPassword()), либо что сами данные в базе были повреждены или подделаны, и в обоих случаях приложение не должно возвращать частично расшифрованные или неопределённые данные как видимый пользователю результат.

Подводные камни

  • Хранение производного ключа в системном хранилище секретов (как показано в cacheKeyInKeychain()) без чёткого понимания компромисса безопасности, который это создаёт — кеширование ключа избавляет пользователя от повторного ввода мастер-пароля, но означает, что любой, кто получит доступ к системному хранилищу секретов текущего пользователя ОС (включая, потенциально, вредоносное ПО, выполняющееся от того же пользователя), сможет расшифровать все сохранённые пароли без знания самого мастер-пароля; это разумный компромисс удобства против абсолютной безопасности, который должен быть явным, осознанным архитектурным решением, а не случайным побочным эффектом реализации.
  • Использование недостаточного количества итераций PBKDF2 (или эквивалентных по смыслу слишком низких параметров Argon2) для вывода ключа из мастер-пароля — поскольку безопасность всего хранилища паролей в конечном счёте сводится к стойкости единственного мастер-пароля и параметров его обработки, недостаточно дорогая по вычислениям функция вывода ключа делает всё хранилище уязвимым для перебора мастер-пароля значительно быстрее, чем предполагает пользователь, рассчитывающий на «надёжное шифрование».
  • Отсутствие механизма восстановления при забытом мастер-пароле, что для менеджера паролей является фундаментальным архитектурным вопросом без простого решения — поскольку сама модель безопасности подразумевает, что без знания мастер-пароля данные принципиально невозможно расшифровать (что и есть желаемое свойство безопасности), забытый мастер-пароль означает полную, невосстановимую потерю доступа ко всем сохранённым паролям; это неотъемлемый, осознанный компромисс данной архитектуры, который стоит явно и понятно объяснить пользователю при первой настройке хранилища, а не пытаться «решить» через резервные лазейки, ослабляющие саму модель безопасности.
  • Отсутствие защиты от атак через перебор мастер-пароля при многократных, повторных попытках входа (например, без ограничения количества последовательных неудачных попыток или нарастающей задержки между ними) — хотя сама функция вывода ключа (PBKDF2/Argon2) уже замедляет каждую отдельную попытку, для дополнительной защиты от автоматизированного перебора стоит рассмотреть дополнительные меры на уровне самого приложения (временная блокировка после нескольких неудачных попыток подряд), особенно если хранилище может быть скопировано и атаковано offline на более мощном оборудовании атакующего, где единственной защитой остаётся именно дороговизна самой функции вывода ключа.

Цикл «Криптография на практике в Qt-приложениях» завершён — план дополнен до 380 статей; далее следует цикл «Дополнительный набор датчиков Arduino» (381-392).