НейроАгент

Как защитить ключи шифрования в программе

Узнайте эффективные методы защиты ключей шифрования в локальных приложениях на Python и C++. Рассматриваем KDF, безопасное хранение памяти и системные механизмы защиты.

Вопрос

Как защитить ключи шифрования в программе?

Я создаю локальное приложение на Python и C++ и хочу защитить код с помощью шифрования файлов. Основная проблема заключается в защите ключей шифрования, которые используются для расшифровки файлов во время работы программы. Я не могу оставлять скрипты расшифровки открытыми, и обфускация, вероятно, не обеспечит достаточной защиты.

Какие существуют эффективные методы защиты ключей шифрования в приложениях? Какие альтернативные подходы можно использовать для безопасного хранения и использования ключей в локальных программах на Python и C++?

НейроАгент

Защита ключей шифрования в локальных приложениях требует многоуровневого подхода, сочетающего безопасное хранение, производное вычисление ключей и методы защиты памяти. Основные эффективные методы включают использование функций производного получения ключей (KDF), таких как PBKDF2 и scrypt, безопасное хранение в памяти с требованием ввода пользователя при запуске, а также применение систем защиты данных операционной системы, таких как DPAPI в Windows.

Содержание

Функции производного получения ключей (KDF)

Одним из наиболее эффективных методов защиты ключей шифрования является использование функций производного получения ключей (Key Derivation Functions). Эти криптографические алгоритмы берут секретное значение (вводимый пользователем пароль) и дополнительные параметры (соль, количество итераций) для генерации одного или нескольких выходных ключей.

PBKDF2 (Password-Based Key Derivation Function) является стандартным методом для получения ключей из паролей. Функция вызывает дополнительную функцию указанное количество раз (определяемое счетчиком итераций) для получения ключа из исходных данных. DPAPI использует SHA-1 для этой базовой функции.

python
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import os

def derive_key(password: bytes, salt: bytes, iterations: int = 100000) -> bytes:
    kdf = PBKDF2HMAC(
        algorithm=hashes.SHA256(),
        length=32,
        salt=salt,
        iterations=iterations,
    )
    return kdf.derive(password)

Scrypt был разработан для использования в системе резервного копирования Tarsnap и создан для обеспечения значительно большей безопасности против аппаратных атак методом перебора по сравнению с альтернативными функциями, такими как PBKDF2 или bcrypt.

cpp
#include <scrypt.h>
#include <vector>

std::vector<unsigned char> derive_key(const std::string& password, 
                                     const std::vector<unsigned char>& salt) {
    std::vector<unsigned char> derived_key(32); // 256 бит
    scrypt(reinterpret_cast<const uint8_t*>(password.c_str()), 
           password.length(),
           salt.data(), salt.size(),
           16384,  // N
           8,      // r
           1,      // p
           derived_key.data(), derived_key.size());
    return derived_key;
}

Важно: Соль должна быть случайной и уникальной для каждого пользователя или ключа. Количество итераций должно быть достаточно большим - современные системы рекомендуют минимум 100,000 итераций для PBKDF2.

Безопасное хранение ключей в памяти

Ключи шифрования никогда не должны храниться в постоянном хранилище в открытом виде. Вместо этого они должны храниться только в памяти и требовать ввода пользователя при запуске приложения.

Метод “in-memory only” заключается в том, чтобы:

  • Запрашивать ключ у пользователя при запуске приложения
  • Хранить ключ только в оперативной памяти
  • Не сохранять ключ на диск
  • Обнулять память, содержащую ключ, после использования
python
import getpass
from cryptography.fernet import Fernet

def get_master_key():
    """Запрашивает мастер-ключ у пользователя и возвращает его в байтах"""
    password = getpass.getpass("Введите мастер-пароль: ")
    return password.encode()

def decrypt_files(master_key):
    """Расшифровывает файлы, используя мастер-ключ"""
    # Производим ключ из пароля
    derived_key = derive_key(master_key, SALT, ITERATIONS)
    # Используем ключ для расшифровки
    cipher = Fernet(derived_key)
    # Расшифровываем файлы...
    
    # Очищаем ключ из памяти
    del derived_key

Случайная адресация памяти - техника, при которой каждый раз при запуске приложения ключи хранятся по разным адресам в памяти. Это усложняет анализ памяти для извлечения ключей.

В C++ для этого можно использовать:

cpp
#include <random>
#include <memory>

std::vector<unsigned char> generate_and_store_key() {
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> distrib(0, 255);
    
    std::vector<unsigned char> key(32);
    for (auto& byte : key) {
        byte = distrib(gen);
    }
    
    // Ключ теперь хранится в случайно выделенной памяти
    return key;
}

Использование систем защиты данных операционной системы

Операционные системы предоставляют встроенные механизмы для защиты данных, которые можно использовать для шифрования ключей.

DPAPI (Data Protection API) в Windows - это мощный механизм, который использует мастер-ключи, связанные с учетными данными пользователя, для шифрования данных. Основное шифрование/дешифрование ключа производится из пароля пользователя с помощью функции PBKDF2.

cpp
#include <windows.h>
#include <stdio.h>

BOOL EncryptDataWithDPAPI(PBYTE pbData, DWORD cbData, PBYTE* ppbEncryptedData, DWORD* pcbEncryptedData) {
    DATA_BLOB DataIn;
    DATA_BLOB DataOut;
    
    DataIn.pbData = pbData;
    DataIn.cbData = cbData;
    
    if (!CryptProtectData(&DataIn, NULL, NULL, NULL, NULL, 0, &DataOut)) {
        printf("CryptProtectData failed with error 0x%x\n", GetLastError());
        return FALSE;
    }
    
    *ppbEncryptedData = DataOut.pbData;
    *pcbEncryptedData = DataOut.cbData;
    return TRUE;
}

Keychain (macOS) и Credential Manager (Linux) также предоставляют безопасные хранилища для секретов, интегрированные с системными механизмами аутентификации.

Шифрование ключей

Даже при использовании KDF и безопасного хранения памяти, дополнительный уровень защиты можно получить путем шифрования самих ключей.

AES-GCM - один из наиболее современных и безопасных режимов шифрования, предоставляющий как конфиденциальность, так и целостность данных.

python
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
import os

def generate_aes_key(bit_length=256):
    """Генерирует безопасный AES-ключ"""
    if bit_length not in (128, 192, 256):
        raise ValueError("Длина ключа должна быть 128, 192 или 256")
    return os.urandom(bit_length // 8)

def encrypt_aes_gcm(message, key):
    """Шифрует сообщение с помощью AES-GCM"""
    if isinstance(message, str):
        message = message.encode()
    
    aesgcm = AESGCM(key)
    nonce = os.urandom(12)  # Стандартный размер nonce для GCM
    
    ciphertext = aesgcm.encrypt(nonce, message, "")
    return nonce + ciphertext

def decrypt_aes_gcm(encrypted_data, key):
    """Расшифровывает сообщение с помощью AES-GCM"""
    nonce = encrypted_data[:12]
    ciphertext = encrypted_data[12:]
    
    aesgcm = AESGCM(key)
    return aesgcm.decrypt(nonce, ciphertext, "")

Fernet Symmetric Encryption в Python предоставляет простой в использовании интерфейс для шифрования с использованием 128-битного ключа AES в режиме CBC с HMAC для проверки целостности.

python
from cryptography.fernet import Fernet

def create_fernet_key():
    """Создает ключ Fernet"""
    return Fernet.generate_key()

def encrypt_with_fernet(data: bytes, key: bytes) -> bytes:
    """Шифрует данные с помощью Fernet"""
    f = Fernet(key)
    return f.encrypt(data)

def decrypt_with_fernet(encrypted_data: bytes, key: bytes) -> bytes:
    """Расшифровывает данные с помощью Fernet"""
    f = Fernet(key)
    return f.decrypt(encrypted_data)

Альтернативные подходы и архитектурные решения

Zero-Knowledge Storage (zero-knowledge-хранилище) - архитектурный подход, при котором никто, кроме пользователя, загрузившего данные и имеющего мастер-пароль, не может использовать эти данные каким-либо образом. Это означает, что серверное приложение не имеет доступа к ключам шифрования.

Многоуровневая безопасность предполагает использование нескольких методов защиты одновременно:

  1. Ключи производятся из пароля пользователя с помощью KDF
  2. Ключи шифруются дополнительным мастер-ключом
  3. Мастер-ключ хранится в системном защищенном хранилище
  4. Приложение запрашивает пароль пользователя при запуске

Разделение ключей (Key Splitting) - метод, при который ключ разбивается на несколько частей, хранящихся в разных местах. Для восстановления ключа требуется большинство частей.

python
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
import numpy as np

def split_key(master_key: bytes, parts: int, threshold: int) -> list:
    """Разделяет ключ на несколько частей по схеме Шамира"""
    # Пример реализации простой схемы разделения ключей
    # В реальных приложениях следует использовать криптографически безопасные схемы
    coefficients = [int.from_bytes(master_key[i:i+4], 'big') for i in range(0, len(master_key), 4)]
    shares = []
    
    for i in range(parts):
        x = i + 1
        y = sum(coeff * (x ** power) for power, coeff in enumerate(coefficients))
        shares.append((x, y))
    
    return shares

def reconstruct_key(shares: list) -> bytes:
    """Восстанавливает ключ из частей"""
    # Обратное преобразование для восстановления ключа
    # В реальных приложениях следует использовать криптографически безопасные схемы
    if len(shares) < threshold:
        raise ValueError("Недостаточно частей для восстановления ключа")
    
    reconstructed = []
    for i in range(0, len(shares[0][1]), 4):
        point = [(x, y >> (i*8) & 0xFF) for x, y in shares]
        # Используем интерполяцию Лагранжа для восстановления коэффициента
        coeff = 0
        for j, (xj, yj) in enumerate(point):
            li = 1
            for k, (xk, yk) in enumerate(point):
                if k != j:
                    li *= (0 - xk) / (xj - xk)
            coeff += yj * li
        
        reconstructed.append(coeff)
    
    return bytes(reconstructed)

Практические рекомендации для Python и C++

Для Python приложений:

  1. Используйте библиотеку cryptography вместо встроенных модулей, так как она предоставляет высокоуровневые и безопасные API.
python
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
import os

class SecureKeyManager:
    def __init__(self):
        self.backend = default_backend()
    
    def derive_key_from_password(self, password: bytes, salt: bytes, iterations: int = 100000) -> bytes:
        kdf = PBKDF2HMAC(
            algorithm=hashes.SHA256(),
            length=32,
            salt=salt,
            iterations=iterations,
            backend=self.backend
        )
        return kdf.derive(password)
    
    def encrypt_data(self, data: bytes, key: bytes) -> bytes:
        iv = os.urandom(16)  # Initialization vector
        cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=self.backend)
        encryptor = cipher.encryptor()
        
        # Дополнение данных до размера блока
        padded_data = self._pad_data(data)
        encrypted_data = encryptor.update(padded_data) + encryptor.finalize()
        
        return iv + encrypted_data
    
    def decrypt_data(self, encrypted_data: bytes, key: bytes) -> bytes:
        iv = encrypted_data[:16]
        ciphertext = encrypted_data[16:]
        
        cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=self.backend)
        decryptor = cipher.decryptor()
        
        padded_data = decryptor.update(ciphertext) + decryptor.finalize()
        return self._unpad_data(padded_data)
  1. Используйте getpass для безопасного ввода паролей без отображения их на экране.

  2. Управляйте жизненным циклом ключей - создавайте ключи только при необходимости и уничтожайте их после использования.

python
import ctypes
import gc

class SecureMemory:
    @staticmethod
    def secure_clear(data: bytes):
        """Безопасно очищает данные из памяти"""
        if not data:
            return
        
        # Используем memset для обнуления памяти
        cdata = ctypes.create_string_buffer(data)
        ctypes.memset(cdata, 0, len(data))
        
        # Принудительный сбор мусора
        del cdata
        gc.collect()
    
    @staticmethod
    def secure_string_clear(s: str):
        """Безопасно очищает строку из памяти"""
        if not s:
            return
        
        # Преобразуем строку в байты и очищаем
        data = s.encode()
        SecureMemory.secure_clear(data)

Для C++ приложений:

  1. Используйте CryptoAPI Next Generation (CNG) для современных криптографических операций.
cpp
#include <windows.h>
#include <bcrypt.h>
#include <vector>

class CryptoManager {
public:
    static std::vector<BYTE> DeriveKeyFromPassword(
        const std::string& password,
        const std::vector<BYTE>& salt,
        DWORD iterations = 100000) {
        
        BCRYPT_ALG_HANDLE hAlgorithm;
        NTSTATUS status = BCryptOpenAlgorithmProvider(
            &hAlgorithm,
            BCRYPT_PBKDF2_ALGORITHM,
            NULL,
            0);
        
        if (!BCRYPT_SUCCESS(status)) {
            throw std::runtime_error("Failed to open algorithm provider");
        }
        
        // Установка параметров
        BCRYPT_PKCS5_PARAM pbkdf2Params;
        pbkdf2Params.IterationCount = iterations;
        
        DWORD cbDerivedKey;
        status = BCryptDeriveKeyPBKDF2(
            hAlgorithm,
            (PUCHAR)password.c_str(),
            (ULONG)password.length(),
            salt.data(),
            (ULONG)salt.size(),
            &pbkdf2Params,
            NULL,
            0,
            &cbDerivedKey,
            0);
        
        std::vector<BYTE> derivedKey(cbDerivedKey);
        status = BCryptDeriveKeyPBKDF2(
            hAlgorithm,
            (PUCHAR)password.c_str(),
            (ULONG)password.length(),
            salt.data(),
            (ULONG)salt.size(),
            &pbkdf2Params,
            derivedKey.data(),
            cbDerivedKey,
            NULL,
            0);
        
        BCryptCloseAlgorithmProvider(hAlgorithm, 0);
        
        if (!BCRYPT_SUCCESS(status)) {
            throw std::runtime_error("Failed to derive key");
        }
        
        return derivedKey;
    }
    
    static void SecureZeroMemory(void* ptr, size_t len) {
        if (ptr == nullptr) return;
        RtlSecureZeroMemory(ptr, len);
    }
};
  1. Используйте защищенные строки и буферы для работы с секретными данными.

  2. Обрабатывайте ошибки криптографических операций должным образом, так как они могут указывать на атаки.

Заключение и лучшие практики

При защите ключей шифрования в локальных приложениях следует придерживаться следующих основных принципов:

  1. Никогда не храните ключи в открытом виде - используйте функции производного получения ключей (KDF) для создания ключей из паролей пользователей.

  2. Используйте современные алгоритмы - AES-256, PBKDF2 с достаточным количеством итераций, или более безопасные варианты вроде scrypt.

  3. Реализуйте многоуровневую защиту - комбинация безопасного хранения памяти, системной защиты данных и шифрования ключей обеспечивает наилучшую защиту.

  4. Управляйте жизненным циклом ключей - создавайте ключи только при необходимости и уничтожайте их после использования.

  5. Рассмотрите использование специализированных библиотек - для Python используйте cryptography, для C++ используйте CryptoNG или другие современные криптографические библиотеки.

  6. Тестируйте безопасность - регулярно проверяйте приложение на наличие уязвимостей с помощью статического и динамического анализа.

  7. Обновляйте зависимости - следите за обновлениями криптографических библиотек и promptly применяйте исправления уязвимостей.

Защита ключей шифрования - это процесс, а не разовое действие. Требуется постоянное внимание к безопасности и готовность адаптироваться к новым угрозам и технологиям.

Источники

  1. OWASP Cryptographic Storage Cheat Sheet
  2. Python Encryption Techniques for Secure Data Protection - DEV Community
  3. Secure credential storage in Python - Stack Overflow
  4. How to store a crypto key securely? - Stack Overflow
  5. Key Derivation Function in Python - CodeRivers
  6. PyCryptodome Key Derivation Functions Documentation
  7. Data Protection API - Wikipedia
  8. Tarsnap - scrypt key derivation function
  9. CryptoAPI Next Generation (CNG) examples - Stack Overflow
  10. Safeguarding Memory in Higher-Level Programming Languages - Praetorian