Тихая угроза: жестко закодированные ключи шифрования в приложениях Java
3 сентября 2024 г.Добро пожаловать обратно в наш глубокий обзор подводных камней криптографии Java! В нашей последней статье мы рассмотрели проблемуплохая случайность. Сегодня мы прольем свет на еще одну распространенную ошибку безопасности, которую, как я видел, допускают даже опытные разработчики: жестко закодированные ключи шифрования. Мы рассмотрим, почему эта практика опасна, и как реализовать более безопасное решение.
Ловушка: жесткое кодирование ключей шифрования
Давайте начнем с распространенного сценария. Вы создаете приложение Spring, которому нужно шифровать конфиденциальные пользовательские данные. Чтобы сэкономить время, вы поддались искушению и написали что-то вроде этого:
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
@Service
public class UserDataEncryptionService {
private static final String ENCRYPTION_KEY = "MySecretKey12345"; // DON'T DO THIS!
private static final String ALGORITHM = "AES";
public String encryptData(String data) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(ENCRYPTION_KEY.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encryptedBytes = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
}
public String decryptData(String encryptedData) throws Exception {
SecretKeySpec keySpec = new SecretKeySpec(ENCRYPTION_KEY.getBytes(), ALGORITHM);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decryptedBytes);
}
}
Этот код выше работает без проблем, но это катастрофа безопасности, которая вот-вот случится. Давайте разберемся, почему.
Опасности жестко закодированных ключей шифрования
- Раскрытие исходного кода: Если ваш исходный код когда-либо будет раскрыт (из-за взлома, внутренней угрозы или публичного репозитория), ваш ключ шифрования будет скомпрометирован.
- Сложность вращения ключа: Изменение ключа требует изменения кода и повторного развертывания приложения, что делает регулярную ротацию ключей нецелесообразной.
- Последовательность окружающей среды: Во всех средах (разработка, подготовка, производство) используется один и тот же ключ, что нарушает принцип наименьших привилегий.
- Обратный инжиниринг: Байт-код Java можно декомпилировать, что потенциально может раскрыть ваш ключ, даже если распространяется только скомпилированный код.
- Нарушения соответствия: Многие стандарты безопасности (например, PCI DSS) прямо запрещают жесткое кодирование конфиденциальной информации.
Теперь вы можете понять тикающую бомбу замедленного действия в коде выше. Давайте посмотрим, как мы собираемся это исправить
Лучший подход: внешняя конфигурация с помощью Spring
Spring обеспечивает надежную поддержку внешней конфигурации. Давайте реорганизуем нашу службу, чтобы использовать это:
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
@Service
public class SecureUserDataEncryptionService {
private final SecretKeySpec keySpec;
private static final String ALGORITHM = "AES";
public SecureUserDataEncryptionService(@Value("${encryption.key}") String encryptionKey) {
this.keySpec = new SecretKeySpec(encryptionKey.getBytes(), ALGORITHM);
}
public String encryptData(String data) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encryptedBytes = cipher.doFinal(data.getBytes());
return Base64.getEncoder().encodeToString(encryptedBytes);
}
public String decryptData(String encryptedData) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec);
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
return new String(decryptedBytes);
}
}
Теперь нам нужно предоставить ключ шифрования. Но где? Давайте рассмотрим несколько вариантов:
Переменные среды
In the application.properties or application.yml as below
encryption:
key: ${ENCRYPTION_KEY}
УстановитеENCRYPTION_KEY
переменная среды на вашем сервере или в конфигурации развертывания.
2.Сервер конфигурации Spring Cloud
For distributed systems, use Spring Cloud Config Server to centralize your configuration:
yaml-файл:
spring:
cloud:
config:
uri: http://config-server:9999
Сохраните свой ключ шифрования на сервере конфигурации, который может поддерживаться репозиторием Git или базой данных.
3.Менеджер секретов AWS
Для облачных приложений рассмотрите возможность использования AWS Secrets Manager:
import com.amazonaws.services.secretsmanager.AWSSecretsManager;
import com.amazonaws.services.secretsmanager.model.GetSecretValueRequest;
import org.springframework.stereotype.Service;
@Service
public class AwsSecretManagerService {
private final AWSSecretsManager secretsManager;
public AwsSecretManagerService(AWSSecretsManager secretsManager) {
this.secretsManager = secretsManager;
}
public String getEncryptionKey() {
GetSecretValueRequest request = new GetSecretValueRequest()
.withSecretId("myapp/encryption-key");
return secretsManager.getSecretValue(request).getSecretString();
}
}
Затем внедрите эту услугу в свойSecureUserDataEncryptionService
код выше и используйте его для получения ключа.
Реализация ротации ключей
С нашим внешним ключом реализация ротации ключей становится намного проще. Вот базовая стратегия:
Создайте новый ключ и добавьте его в секретное хранилище (например, AWS Secrets Manager).
Обновите приложение, чтобы использовать и старый, и новый ключи, как показано ниже.
public RotatableEncryptionService(
@Value("${encryption.current-key}") String currentKey,
@Value("${encryption.old-key}") String oldKey) {
this.currentKey = new SecretKeySpec(currentKey.getBytes(), "AES");
this.oldKey = new SecretKeySpec(oldKey.getBytes(), "AES");
}
public String encryptData(String data) throws Exception {
// Always encrypt with the current key
return encrypt(data, currentKey);
}
public String decryptData(String encryptedData) throws Exception {
try {
// Try decrypting with the current key first
return decrypt(encryptedData, currentKey);
} catch (Exception e) {
// If that fails, try the old key
return decrypt(encryptedData, oldKey);
}
}
// ... encrypt and decrypt methods ...
}
3. Повторно зашифруйте существующие данные с помощью нового ключа (это можно делать постепенно по мере доступа к данным).
4. По истечении соответствующего переходного периода выньте старый ключ.
Лучшие практики
- Никогда не кодируйте ключи шифрования или другие секреты жестко в исходном коде.
- Используйте надежные, случайно сгенерированные ключи.Не используйте в качестве ключей пароли или другие легко угадываемые строки.
- Реализуйте ротацию ключей.Регулярно обновляйте ключи шифрования.
- Используйте разные ключи для разных сред и целей.
- Аудит и мониторинг использования ключей.Знайте, кто имеет доступ к вашим ключам и когда они используются.
Отказ от жестко закодированных ключей шифрования — важный шаг в обеспечении безопасности ваших приложений Java. Держите ключи в секрете, код — чистым, а данные — в безопасности. Хотя правильное управление ключами имеет важное значение, это всего лишь одна часть головоломки. В нашей следующей и последней статье этой серии мы рассмотрим еще один важный аспект безопасности приложений, который часто упускают из виду: безопасное хранение паролей. Поэтому следите за нашим глубоким погружением в «Подводные камни хеширования паролей: защита учетных данных пользователей в приложениях Java».
Оригинал