Как защитить ключи шифрования в программе?
Я создаю локальное приложение на Python и C++ и хочу защитить код с помощью шифрования файлов. Основная проблема заключается в защите ключей шифрования, которые используются для расшифровки файлов во время работы программы. Я не могу оставлять скрипты расшифровки открытыми, и обфускация, вероятно, не обеспечит достаточной защиты.
Какие существуют эффективные методы защиты ключей шифрования в приложениях? Какие альтернативные подходы можно использовать для безопасного хранения и использования ключей в локальных программах на Python и C++?
Защита ключей шифрования в локальных приложениях требует многоуровневого подхода, сочетающего безопасное хранение, производное вычисление ключей и методы защиты памяти. Основные эффективные методы включают использование функций производного получения ключей (KDF), таких как PBKDF2 и scrypt, безопасное хранение в памяти с требованием ввода пользователя при запуске, а также применение систем защиты данных операционной системы, таких как DPAPI в Windows.
Содержание
- Функции производного получения ключей (KDF)
- Безопасное хранение ключей в памяти
- Использование систем защиты данных операционной системы
- Шифрование ключей
- Альтернативные подходы и архитектурные решения
- Практические рекомендации для Python и C++
- Заключение и лучшие практики
Функции производного получения ключей (KDF)
Одним из наиболее эффективных методов защиты ключей шифрования является использование функций производного получения ключей (Key Derivation Functions). Эти криптографические алгоритмы берут секретное значение (вводимый пользователем пароль) и дополнительные параметры (соль, количество итераций) для генерации одного или нескольких выходных ключей.
PBKDF2 (Password-Based Key Derivation Function) является стандартным методом для получения ключей из паролей. Функция вызывает дополнительную функцию указанное количество раз (определяемое счетчиком итераций) для получения ключа из исходных данных. DPAPI использует SHA-1 для этой базовой функции.
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.
#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” заключается в том, чтобы:
- Запрашивать ключ у пользователя при запуске приложения
- Хранить ключ только в оперативной памяти
- Не сохранять ключ на диск
- Обнулять память, содержащую ключ, после использования
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++ для этого можно использовать:
#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.
#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 - один из наиболее современных и безопасных режимов шифрования, предоставляющий как конфиденциальность, так и целостность данных.
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 для проверки целостности.
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-хранилище) - архитектурный подход, при котором никто, кроме пользователя, загрузившего данные и имеющего мастер-пароль, не может использовать эти данные каким-либо образом. Это означает, что серверное приложение не имеет доступа к ключам шифрования.
Многоуровневая безопасность предполагает использование нескольких методов защиты одновременно:
- Ключи производятся из пароля пользователя с помощью KDF
- Ключи шифруются дополнительным мастер-ключом
- Мастер-ключ хранится в системном защищенном хранилище
- Приложение запрашивает пароль пользователя при запуске
Разделение ключей (Key Splitting) - метод, при который ключ разбивается на несколько частей, хранящихся в разных местах. Для восстановления ключа требуется большинство частей.
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 приложений:
- Используйте библиотеку
cryptographyвместо встроенных модулей, так как она предоставляет высокоуровневые и безопасные API.
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)
-
Используйте
getpassдля безопасного ввода паролей без отображения их на экране. -
Управляйте жизненным циклом ключей - создавайте ключи только при необходимости и уничтожайте их после использования.
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++ приложений:
- Используйте CryptoAPI Next Generation (CNG) для современных криптографических операций.
#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);
}
};
-
Используйте защищенные строки и буферы для работы с секретными данными.
-
Обрабатывайте ошибки криптографических операций должным образом, так как они могут указывать на атаки.
Заключение и лучшие практики
При защите ключей шифрования в локальных приложениях следует придерживаться следующих основных принципов:
-
Никогда не храните ключи в открытом виде - используйте функции производного получения ключей (KDF) для создания ключей из паролей пользователей.
-
Используйте современные алгоритмы - AES-256, PBKDF2 с достаточным количеством итераций, или более безопасные варианты вроде scrypt.
-
Реализуйте многоуровневую защиту - комбинация безопасного хранения памяти, системной защиты данных и шифрования ключей обеспечивает наилучшую защиту.
-
Управляйте жизненным циклом ключей - создавайте ключи только при необходимости и уничтожайте их после использования.
-
Рассмотрите использование специализированных библиотек - для Python используйте
cryptography, для C++ используйте CryptoNG или другие современные криптографические библиотеки. -
Тестируйте безопасность - регулярно проверяйте приложение на наличие уязвимостей с помощью статического и динамического анализа.
-
Обновляйте зависимости - следите за обновлениями криптографических библиотек и promptly применяйте исправления уязвимостей.
Защита ключей шифрования - это процесс, а не разовое действие. Требуется постоянное внимание к безопасности и готовность адаптироваться к новым угрозам и технологиям.
Источники
- OWASP Cryptographic Storage Cheat Sheet
- Python Encryption Techniques for Secure Data Protection - DEV Community
- Secure credential storage in Python - Stack Overflow
- How to store a crypto key securely? - Stack Overflow
- Key Derivation Function in Python - CodeRivers
- PyCryptodome Key Derivation Functions Documentation
- Data Protection API - Wikipedia
- Tarsnap - scrypt key derivation function
- CryptoAPI Next Generation (CNG) examples - Stack Overflow
- Safeguarding Memory in Higher-Level Programming Languages - Praetorian