Опирается на правила: R-SEC-CRYPTO-1R-SEC-CRYPTO-5 и R-SEC-CRYPTO-X1 из Security Style Guide → раздел 5. Криптография в коде.

Важно знать

  • Пароли: BCryptPasswordEncoder (Spring Security) с factor ≥ 12. Никогда MD5/SHA1/SHA256 без salt.
  • Random: java.security.SecureRandom. java.util.Random — только тесты и не-security код.
  • Симметричное шифрование: AES/GCM/NoPadding с 12-байтным IV (рандомный на каждый encrypt).
  • TLS: минимум 1.2 на стороне клиента и сервера (Spring Boot default). TLS 1.0/1.1 отключены на reverse-proxy.
  • JWT verification — через oauth2ResourceServer().jwt(). Manual parsing — критическая ошибка.
  • Hardcoded ключи/IV — запрещены. Только Vault/KMS + env.

Не пиши свою криптографию — используй стандартные библиотеки. Это правило не означает «не понимай как работает», означает «не реализуй сам, не выбирай экзотические алгоритмы, не custom-mixing». UCP формулирует минимальный набор: один password hasher, один symmetric cipher, один JWT verifier — всё с дефолтами Spring Security.

Пароли — BCryptPasswordEncoder

R-SEC-CRYPTO-1: один правильный hasher.

@Configuration
public class PasswordConfig {

    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12);
    }
}
@UseCase
@RequiredArgsConstructor
public class RegisterUserHandler implements UseCaseHandler<RegisterUserCommand, User> {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    @Transactional
    public User handle(RegisterUserCommand command) {
        var hashed = passwordEncoder.encode(command.password());
        var user = User.create(command.email(), hashed);
        return userRepository.save(user);
    }
}

@UseCase
@RequiredArgsConstructor
public class LoginHandler implements UseCaseHandler<LoginCommand, AuthResult> {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;

    @Override
    public AuthResult handle(LoginCommand command) {
        var user = userRepository.findByEmail(command.email())
            .orElseThrow(() -> new BadCredentialsException("Invalid credentials"));

        if (!passwordEncoder.matches(command.password(), user.passwordHash())) {
            throw new BadCredentialsException("Invalid credentials");
        }
        return AuthResult.ofUser(user);
    }
}

Свойства BCrypt:

  • Adaptive — factor (work factor, по умолчанию 10, рекомендуем 12) определяет затраты. 12 = ~250ms на hash в 2026, exponentially увеличивается с factor.
  • Built-in salt — salt генерируется автоматически, хранится внутри hash (формат $2a$12$saltsaltsaltsalt$hash).
  • Cross-version compatibilitymatches(raw, hash) работает с любым factor, можно постепенно повышать.

Альтернативы для специальных кейсов:

  • Argon2 — победитель Password Hashing Competition 2015, более устойчивый к GPU attacks. Spring Security 5.1+ имеет Argon2PasswordEncoder.
  • scrypt — устойчив к memory-hard attacks. SCryptPasswordEncoder.

В UCP дефолт — BCrypt, factor 12. Не custom hashing, не plain MD5/SHA.

// КАТАСТРОФА
public String hashPassword(String raw) {
    return DigestUtils.md5DigestAsHex(raw.getBytes());
}

public String hashPassword(String raw) {
    return DigestUtils.sha256DigestAsHex(raw.getBytes());
}

MD5/SHA без salt — взламываются rainbow-tables за секунды. SpotBugs/FindSecBugs ловят WEAK_MESSAGE_DIGEST_* (см. SAST).

SecureRandom для security

R-SEC-CRYPTO-2: только java.security.SecureRandom.

// ХОРОШО
var random = new SecureRandom();
var token = new byte[32];
random.nextBytes(token);
var encoded = Base64.getUrlEncoder().withoutPadding().encodeToString(token);

// КАТАСТРОФА
var random = new Random();  // predictable seed, deterministic
var token = random.nextLong();

java.util.Randompredictable: seed детерминированный (System.currentTimeMillis по дефолту), attacker может предсказать sequence. Использование для:

  • Generation tokens (session, CSRF, password reset).
  • Cryptographic nonces.
  • API keys.

catastrophic.

SecureRandom — берёт entropy из /dev/urandom (Linux) или OS entropy pool. Unpredictable.

java.util.Random приемлем только для:

  • Тестов (deterministic выгоден).
  • Jitter в retry policies (R-RES-RE-3).
  • Shuffle non-security collections.

AES-GCM для symmetric

R-SEC-CRYPTO-3: один правильный mode.

public final class AesGcm {

    private static final int IV_LENGTH = 12;
    private static final int TAG_LENGTH = 128;
    private static final SecureRandom RANDOM = new SecureRandom();

    public static byte[] encrypt(byte[] plaintext, SecretKey key) {
        var iv = new byte[IV_LENGTH];
        RANDOM.nextBytes(iv);

        try {
            var cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_LENGTH, iv));
            var ciphertext = cipher.doFinal(plaintext);

            var result = new byte[IV_LENGTH + ciphertext.length];
            System.arraycopy(iv, 0, result, 0, IV_LENGTH);
            System.arraycopy(ciphertext, 0, result, IV_LENGTH, ciphertext.length);
            return result;
        } catch (GeneralSecurityException e) {
            throw new CryptoException(e);
        }
    }

    public static byte[] decrypt(byte[] data, SecretKey key) {
        var iv = Arrays.copyOfRange(data, 0, IV_LENGTH);
        var ciphertext = Arrays.copyOfRange(data, IV_LENGTH, data.length);

        try {
            var cipher = Cipher.getInstance("AES/GCM/NoPadding");
            cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_LENGTH, iv));
            return cipher.doFinal(ciphertext);
        } catch (GeneralSecurityException e) {
            throw new CryptoException(e);
        }
    }
}

Почему AES-GCM:

  • Authenticated encryption — GCM встроенно проверяет integrity (tag). Без — attacker может modify ciphertext, decrypt вернёт мусор который ты не отличишь.
  • No padding — нет padding-oracle attacks (как у CBC).
  • 12-byte IV — стандартный для GCM, не 16.

Запреты:

  • AES/ECB/... — same plaintext → same ciphertext, видны patterns. Catastrophic для anything > 1 block.
  • AES/CBC/PKCS5Padding без MAC — padding oracle attack возможен.
  • IV хранится отдельно или фиксированный — IV reuse в GCM completely breaks security.

TLS минимум 1.2

R-SEC-CRYPTO-4: server и client.

server:
  ssl:
    enabled: true
    enabled-protocols: TLSv1.2,TLSv1.3
    ciphers: ECDHE-RSA-AES128-GCM-SHA256,ECDHE-RSA-AES256-GCM-SHA384

В UCP-сервисах TLS обычно terminates на reverse-proxy (Nginx, Envoy, Ingress) — Java-app получает plain HTTP внутри cluster. Proxy конфигурируется:

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers on;

TLS 1.0/1.1 — deprecated, имеют известные уязвимости (BEAST, POODLE). Никогда не enable.

Для client-side (outbound HTTP) — Spring Boot default использует JVM-level TLS settings. JVM 21 по умолчанию — TLS 1.2/1.3.

JWT — только oauth2ResourceServer

R-SEC-CRYPTO-5: см. Auth → JWT validation.

// КАТАСТРОФА — manual parsing
public Claims parseJwt(String token) {
    var jwt = Jwts.parser().parseClaimsJwt(token);  // НЕ проверяет подпись!
    return jwt.getBody();
}

// ХОРОШО — Spring Security
http.oauth2ResourceServer(oauth -> oauth.jwt());

Jwts.parser().parseClaimsJwt(...) (без setSigningKey) не проверяет подпись — accept любой подделанный JWT. Это классический сценарий уязвимости.

SpotBugs ловит часть случаев. Полная защита — AUTH-4: использовать только oauth2ResourceServer.

Hardcoded keys/IV — запрещены

R-SEC-CRYPTO-X1:

// КАТАСТРОФА
private static final String SECRET_KEY = "MyVerySecretKey123";
private static final byte[] IV = "1234567890123456".getBytes();

Любой с доступом к репо/binary имеет ключ. JWT signing key, encryption key, API token — никогда не в коде.

Корректно:

@ConfigurationProperties("encryption")
@Validated
public record EncryptionSettings(
    @NotBlank String keyBase64
) {
    public SecretKey toKey() {
        var bytes = Base64.getDecoder().decode(keyBase64);
        return new SecretKeySpec(bytes, "AES");
    }
}
encryption:
  key-base64: ${ENCRYPTION_KEY_BASE64}

Откуда брать значение — Vault/KMS/cloud Secret Manager. См. Секреты в коде.

Что запрещено

АнтипаттернПравилоЧто взамен
MD5/SHA1/SHA256 без salt для паролейR-SEC-CRYPTO-1BCryptPasswordEncoder(12)
java.util.Random для tokensR-SEC-CRYPTO-2SecureRandom
AES/ECB или AES/CBC без MACR-SEC-CRYPTO-3AES/GCM/NoPadding + 12-byte random IV
Фиксированный IV в GCMR-SEC-CRYPTO-3новый IV на каждый encrypt
TLS 1.0/1.1 enabledR-SEC-CRYPTO-4минимум 1.2
Jwts.parser() без setSigningKeyR-SEC-CRYPTO-5oauth2ResourceServer().jwt()
Hardcoded key в кодеR-SEC-CRYPTO-X1env / Vault / KMS
BCrypt factor < 12R-SEC-CRYPTO-1минимум 12 в 2026
Свой кастомный crypto (custom hash, custom AES wrapper)R-SEC-CRYPTO-1стандартные библиотеки

Куда дальше

  • Security → раздел 5. Криптография в коде — нормативные формулировки.
  • SAST по коду — FindSecBugs ловит WEAK_MESSAGE_DIGEST_*.
  • Секреты в коде и истории — keys через env, не в коде.
  • Auth → JWT validation — стандартный flow.
  • Auth → PII и секреты — Vault для credentials.
  • Container/image-уязвимости — TLS на reverse-proxy.