Опирается на правила:
R-SEC-CRYPTO-1…R-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 compatibility —
matches(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.Random — predictable: 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-1 | BCryptPasswordEncoder(12) |
java.util.Random для tokens | R-SEC-CRYPTO-2 | SecureRandom |
AES/ECB или AES/CBC без MAC | R-SEC-CRYPTO-3 | AES/GCM/NoPadding + 12-byte random IV |
| Фиксированный IV в GCM | R-SEC-CRYPTO-3 | новый IV на каждый encrypt |
| TLS 1.0/1.1 enabled | R-SEC-CRYPTO-4 | минимум 1.2 |
Jwts.parser() без setSigningKey | R-SEC-CRYPTO-5 | oauth2ResourceServer().jwt() |
| Hardcoded key в коде | R-SEC-CRYPTO-X1 | env / Vault / KMS |
| BCrypt factor < 12 | R-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.