Тихая угроза: жестко закодированные ключи шифрования в приложениях Java

Тихая угроза: жестко закодированные ключи шифрования в приложениях 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);
    }
}

Этот код выше работает без проблем, но это катастрофа безопасности, которая вот-вот случится. Давайте разберемся, почему.

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

  1. Раскрытие исходного кода: Если ваш исходный код когда-либо будет раскрыт (из-за взлома, внутренней угрозы или публичного репозитория), ваш ключ шифрования будет скомпрометирован.
  2. Сложность вращения ключа: Изменение ключа требует изменения кода и повторного развертывания приложения, что делает регулярную ротацию ключей нецелесообразной.
  3. Последовательность окружающей среды: Во всех средах (разработка, подготовка, производство) используется один и тот же ключ, что нарушает принцип наименьших привилегий.
  4. Обратный инжиниринг: Байт-код Java можно декомпилировать, что потенциально может раскрыть ваш ключ, даже если распространяется только скомпилированный код.
  5. Нарушения соответствия: Многие стандарты безопасности (например, 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);
    }
}

Теперь нам нужно предоставить ключ шифрования. Но где? Давайте рассмотрим несколько вариантов:

  1. Переменные среды

       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код выше и используйте его для получения ключа.

Реализация ротации ключей

С нашим внешним ключом реализация ротации ключей становится намного проще. Вот базовая стратегия:

  1. Создайте новый ключ и добавьте его в секретное хранилище (например, AWS Secrets Manager).

  2. Обновите приложение, чтобы использовать и старый, и новый ключи, как показано ниже.

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».


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE