Skip to content

CWE-326: Inadequate Encryption Strength - Java

Overview

Inadequate Encryption Strength in Java applications comes from weak algorithms, insufficient key sizes, or deprecated ciphers. The JCA (Java Cryptography Architecture) and JCE (Java Cryptography Extension) expose the insecure options alongside the secure ones, so the algorithm string passed to Cipher.getInstance() or MessageDigest.getInstance() is what decides which you get.

Common Java Vulnerability Scenarios:

  • Using DES or 3DES instead of AES-256
  • Implementing RSA with 1024- or 2048-bit keys instead of 3072 bits
  • Using MD5 or SHA-1 for security-critical hashing
  • Applying ECB mode which reveals patterns in encrypted data
  • Using weak cipher suites in TLS/SSL connections
  • Implementing password hashing with insufficient iterations

Java Cryptographic Landscape:

  • javax.crypto: Built-in JCE for encryption/decryption
  • java.security: Core security classes including MessageDigest
  • Bouncy Castle: Extended cryptographic provider
  • Spring Security Crypto: High-level encryption utilities
  • Apache Commons Codec: Utility codecs and encoders

Framework-Specific Considerations:

  • Spring Boot: Use Spring Security Crypto module for encryption
  • JAX-RS: Secure API endpoints with proper cryptographic libraries
  • Jakarta EE: Use the platform security APIs with strong algorithms
  • Micronaut: Configure encryption for configuration properties

Primary Defence: Use AES-256 with GCM mode for symmetric encryption, RSA with 3072-bit keys for asymmetric encryption, and SHA-256 or SHA-3 for hashing. NIST rates 2048-bit RSA at 112-bit security and accepts it only through 2030; 3072-bit gives the 128-bit strength needed beyond that, and is what the secure patterns below use.

Common Vulnerable Patterns

Using DES Encryption

// VULNERABLE - DES in ECB mode: a 56-bit key, and identical plaintext blocks
// produce identical ciphertext blocks
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

public class WeakEncryptionService {
    private static final String DES_ALGORITHM = "DES";
    private SecretKey key;

    public WeakEncryptionService() throws Exception {
        // DES has only 56-bit effective key strength
        KeyGenerator keyGen = KeyGenerator.getInstance(DES_ALGORITHM);
        keyGen.init(56);
        this.key = keyGen.generateKey();
    }

    public String encryptSSN(String ssn) throws Exception {
        // Using ECB mode - also vulnerable
        Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, key);

        byte[] encrypted = cipher.doFinal(ssn.getBytes());
        return Base64.getEncoder().encodeToString(encrypted);
    }

    public String decryptSSN(String encryptedSSN) throws Exception {
        Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, key);

        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedSSN));
        return new String(decrypted);
    }
}

// Spring Boot REST Controller using weak encryption
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;

@RestController
@RequestMapping("/api/users")
public class UserController {
    private final WeakEncryptionService encryptionService;

    public UserController() throws Exception {
        this.encryptionService = new WeakEncryptionService();
    }

    @PostMapping
    public ResponseEntity<UserResponse> createUser(@RequestBody UserRequest request) 
            throws Exception {
        String encryptedSSN = encryptionService.encryptSSN(request.getSsn());
        // Store in database
        return ResponseEntity.ok(new UserResponse("created", encryptedSSN));
    }
}

Why this is vulnerable:

  • DES has only 56-bit effective key strength, making brute-force feasible.
  • ECB mode reveals duplicate blocks in the ciphertext, so an attacker can infer the structure of the plaintext without breaking the cipher.

Weak RSA Key Size

// VULNERABLE - Weak RSA Key Size
import java.security.*;
import javax.crypto.Cipher;
import java.util.Base64;

public class WeakRSAEncryption {
    private KeyPair keyPair;

    public WeakRSAEncryption() throws NoSuchAlgorithmException {
        // 1024-bit RSA is considered weak
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(1024);  // Insufficient key size
        this.keyPair = keyGen.generateKeyPair();
    }

    public String encryptAPIKey(String apiKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic());

        byte[] encrypted = cipher.doFinal(apiKey.getBytes());
        return Base64.getEncoder().encodeToString(encrypted);
    }

    public String decryptAPIKey(String encryptedKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());

        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedKey));
        return new String(decrypted);
    }
}

// JAX-RS endpoint using weak RSA

Why this is vulnerable:

  • 1024-bit RSA can be factored by well-resourced attackers.
  • NIST deprecated 1024-bit keys in 2013.
  • 2048-bit keys are rated at 112-bit security and accepted by NIST only through 2030; use 3072-bit for anything with a longer lifetime. SP 800-57 Part 1 Table 4 scopes this to applying protection - decrypting or verifying data already protected at 112-bit stays permitted as legacy use.

Using MD5 for Password Hashing

// VULNERABLE - Using MD5 for Password Hashing
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;

public class WeakPasswordService {

    public String hashPassword(String password) throws NoSuchAlgorithmException {
        // MD5 is cryptographically broken
        MessageDigest md = MessageDigest.getInstance("MD5");
        byte[] hash = md.digest(password.getBytes());
        return HexFormat.of().formatHex(hash);
    }

    public boolean verifyPassword(String password, String hash) 
            throws NoSuchAlgorithmException {
        String computedHash = hashPassword(password);
        return computedHash.equals(hash);
    }
}

// Spring Boot authentication service
import org.springframework.stereotype.Service;
import org.springframework.security.core.userdetails.*;

@Service
public class UserAuthenticationService implements UserDetailsService {
    private final WeakPasswordService passwordService;
    private final UserRepository userRepository;

    public UserAuthenticationService(UserRepository userRepository) 
            throws NoSuchAlgorithmException {
        this.passwordService = new WeakPasswordService();
        this.userRepository = userRepository;
    }

    public void registerUser(String username, String password) throws Exception {
        String hashedPassword = passwordService.hashPassword(password);

        User user = new User();
        user.setUsername(username);
        user.setPasswordHash(hashedPassword);

        userRepository.save(user);
    }

    @Override
    public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        return org.springframework.security.core.userdetails.User
            .withUsername(user.getUsername())
            .password(user.getPasswordHash())
            .authorities("USER")
            .build();
    }
}

Why this is vulnerable:

  • MD5 has practical collision attacks and is cryptographically broken.
  • GPUs brute-force MD5 at billions of hashes per second, so a stolen database is cracked rather than merely leaked.
  • There is no salt, so identical passwords produce identical hashes and a precomputed table reverses the common ones instantly.
  • Speed is the problem, not just the algorithm: a password hash needs a deliberately slow function, which no general-purpose digest is.

Using SHA-1 for Digital Signatures

// VULNERABLE - SHA1withRSA for signing
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;

public class LicenceSigner {

    public byte[] sign(byte[] licence, PrivateKey key) throws Exception {
        // VULNERABLE - SHA-1 is not collision resistant
        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initSign(key);
        signature.update(licence);
        return signature.sign();
    }

    public boolean verify(byte[] licence, byte[] sig, PublicKey key) throws Exception {
        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initVerify(key);
        signature.update(licence);
        return signature.verify(sig);
    }
}

// Attack: a signature commits to the digest, not the document. Given a
// practical collision, an attacker prepares two files with the same SHA-1
// digest, gets the benign one signed, and attaches that signature to the
// other - it verifies, because the digest matches.

Why this is vulnerable:

  • SHA-1 collisions are practical, demonstrated publicly since 2017 and cheaper every year.
  • A signature is computed over the digest, so two documents sharing a digest share a valid signature - the attacker never needs the private key.
  • This matters wherever a signature authorises something: licences, software updates, tokens, signed configuration.
  • Signing is the case where collision resistance is load-bearing, which is why a SHA-1 finding on a signature path is more urgent than one on, say, a non-security checksum.

AES in ECB Mode

import javax.crypto.*;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;

public class WeakAESEncryption {
    private SecretKey key;

    public WeakAESEncryption() {
        // The key size is not the problem here - the mode is
        byte[] keyBytes = new byte[16];  // AES-128
        new SecureRandom().nextBytes(keyBytes);
        this.key = new SecretKeySpec(keyBytes, "AES");
    }

    public String encryptToken(String token) throws Exception {
        // ECB mode is insecure - reveals patterns
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, key);

        byte[] encrypted = cipher.doFinal(token.getBytes());
        return Base64.getEncoder().encodeToString(encrypted);
    }

    public String decryptToken(String encryptedToken) throws Exception {
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, key);

        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedToken));
        return new String(decrypted);
    }
}

// Micronaut controller using weak AES
import io.micronaut.http.annotation.*;
import io.micronaut.http.HttpResponse;

@Controller("/api/tokens")
public class TokenController {
    private final WeakAESEncryption encryption = new WeakAESEncryption();

    @Post
    public HttpResponse<TokenResponse> encryptToken(@Body TokenRequest request) 
            throws Exception {
        String encrypted = encryption.encryptToken(request.getToken());
        return HttpResponse.ok(new TokenResponse(encrypted));
    }
}

Why this is vulnerable:

  • ECB mode makes identical plaintext blocks produce identical ciphertext blocks.
  • Pattern leakage exposes repeated values and structured content.
  • There is no authentication, so a modified ciphertext decrypts without complaint.
  • The AES-128 key is not what makes this example weak. AES-128 carries a full 128-bit security strength, and swapping in a 256-bit key leaves the ECB weakness exactly where it was. Change the mode, not the key size. Prefer AES-256 for new work as a margin decision, and do not report an existing AES-128 deployment as a CWE-326 finding on the key length alone.

SHA-1 for HMAC

// VULNERABLE - SHA-1 for HMAC
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.HexFormat;

public class WeakHMACService {
    private static final String HMAC_ALGORITHM = "HmacSHA1";  // Deprecated
    private SecretKeySpec secretKey;

    public WeakHMACService(byte[] key) {
        this.secretKey = new SecretKeySpec(key, HMAC_ALGORITHM);
    }

    public String generateSignature(String data) 
            throws NoSuchAlgorithmException, InvalidKeyException {
        Mac mac = Mac.getInstance(HMAC_ALGORITHM);
        mac.init(secretKey);

        byte[] signature = mac.doFinal(data.getBytes());
        return HexFormat.of().formatHex(signature);
    }

    public boolean verifySignature(String data, String signature) 
            throws NoSuchAlgorithmException, InvalidKeyException {
        String computed = generateSignature(data);
        return MessageDigest.isEqual(
            computed.getBytes(), 
            signature.getBytes()
        );
    }
}

// JAX-RS webhook endpoint with weak HMAC
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;

@Path("/api/webhook")
public class WebhookResource {
    private static final byte[] SECRET = "weak_secret_key".getBytes();
    private final WeakHMACService hmacService = new WeakHMACService(SECRET);

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response processWebhook(
            @HeaderParam("X-Signature") String signature,
            String payload) throws Exception {

        if (!hmacService.verifySignature(payload, signature)) {
            return Response.status(Response.Status.FORBIDDEN)
                .entity("Invalid signature")
                .build();
        }

        // Process webhook
        return Response.ok().entity("Processed").build();
    }
}

Why this is vulnerable:

  • The 2017 collision attacks are not what makes this a finding. HMAC's security rests on the hash behaving as a pseudorandom function rather than on collision resistance, so HMAC-SHA1 is not broken by them - which is why a SHA-1 finding here is less urgent than one on the signature path above.
  • What does make it a finding is policy: NIST and PCI DSS disallow SHA-1 for new work, so it fails an audit whatever its cryptanalytic state.
  • HmacSHA256 is the replacement. The tag goes from 160 to 256 bits, so whoever verifies the signature has to change at the same time.

Low Iteration PBKDF2

// VULNERABLE - Low Iteration PBKDF2
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.util.Base64;

public class WeakPBKDF2Service {
    private static final int ITERATIONS = 1000;  // Too low!
    private static final int KEY_LENGTH = 128;   // bits - fine on its own
    private static final String FIXED_SALT = "fixed_salt_value";  // Fixed salt!

    public String deriveKey(String password) 
            throws NoSuchAlgorithmException, InvalidKeySpecException {

        PBEKeySpec spec = new PBEKeySpec(
            password.toCharArray(),
            FIXED_SALT.getBytes(),
            ITERATIONS,
            KEY_LENGTH
        );

        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1");
        byte[] hash = factory.generateSecret(spec).getEncoded();

        return Base64.getEncoder().encodeToString(hash);
    }
}

// Spring Boot configuration encryption
import org.springframework.stereotype.Service;

@Service
public class ConfigEncryptionService {
    private final WeakPBKDF2Service pbkdf2Service = new WeakPBKDF2Service();

    public String encryptConfig(String password, String configValue) throws Exception {
        String key = pbkdf2Service.deriveKey(password);
        // Use derived key for encryption
        return encryptWithKey(key, configValue);
    }

    private String encryptWithKey(String key, String value) {
        // Implementation
        return value;
    }
}

Why this is vulnerable:

  • 1,000 iterations are far below OWASP's 600,000+ PBKDF2-SHA256 guidance.
  • Fixed salts make identical passwords produce identical hashes.
  • SHA-1 provides weaker security margins than SHA-256/SHA-512.

3DES Instead of AES

// VULNERABLE - 3DES Instead of AES
import javax.crypto.*;
import javax.crypto.spec.DESedeKeySpec;
import javax.crypto.spec.IvParameterSpec;
import java.security.spec.KeySpec;
import java.util.Base64;

public class Legacy3DESEncryption {
    private SecretKey key;

    public Legacy3DESEncryption(byte[] keyBytes) throws Exception {
        // 3DES is deprecated
        KeySpec keySpec = new DESedeKeySpec(keyBytes);
        SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DESede");
        this.key = keyFactory.generateSecret(keySpec);
    }

    public String encryptCreditCard(String creditCard) throws Exception {
        Cipher cipher = Cipher.getInstance("DESede/CBC/PKCS5Padding");

        byte[] iv = new byte[8];  // 64-bit IV for 3DES
        cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));

        byte[] encrypted = cipher.doFinal(creditCard.getBytes());
        return Base64.getEncoder().encodeToString(encrypted);
    }
}

// Jakarta EE REST service
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

@Path("/api/payments")
@Produces(MediaType.APPLICATION_JSON)
public class PaymentResource {
    private Legacy3DESEncryption encryption;

    public PaymentResource() throws Exception {
        byte[] key = "FIXED_24_BYTE_KEY_123456".getBytes();
        this.encryption = new Legacy3DESEncryption(key);
    }

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    public Response processPayment(PaymentRequest request) throws Exception {
        String encryptedCC = encryption.encryptCreditCard(request.getCreditCard());
        // Process payment
        return Response.ok()
            .entity(new PaymentResponse("processed", encryptedCC))
            .build();
    }
}

Why this is vulnerable:

  • 3DES deprecated by NIST (retired in 2023)
  • 64-bit block size vulnerable to birthday attacks
  • Slower than AES with no security benefit

Weak TLS Cipher Suite Configuration

// VULNERABLE - Weak TLS Cipher Suite Configuration
import javax.net.ssl.*;
import java.security.KeyStore;

public class WeakSSLConfiguration {

    public SSLContext createWeakSSLContext() throws Exception {
        SSLContext context = SSLContext.getInstance("TLS");
        context.init(null, null, null);

        // Default configuration may include weak ciphers
        return context;
    }

    public HttpsURLConnection createConnection(String url) throws Exception {
        HttpsURLConnection connection = 
            (HttpsURLConnection) new URL(url).openConnection();

        SSLContext sslContext = createWeakSSLContext();
        connection.setSSLSocketFactory(sslContext.getSocketFactory());

        // No cipher suite restrictions - may use RC4, 3DES, etc.
        return connection;
    }
}

// Spring Boot with weak SSL
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;

@Configuration
public class WeakHttpClientConfig {

    @Bean
    public HttpClient httpClient() throws Exception {
        WeakSSLConfiguration sslConfig = new WeakSSLConfiguration();
        SSLConnectionSocketFactory socketFactory = 
            new SSLConnectionSocketFactory(sslConfig.createWeakSSLContext());

        return HttpClients.custom()
            .setSSLSocketFactory(socketFactory)
            .build();
    }
}

Why this is vulnerable:

  • May negotiate weak cipher suites (RC4, 3DES, MD5)
  • No explicit cipher suite allowlist
  • Vulnerable to downgrade attacks
  • May not enforce TLS 1.2+ minimum version

Secure Patterns

AES-256-GCM Encryption

// SECURE - AES-256-GCM: authenticated encryption with a fresh IV per message
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;

public class SecureAESGCMEncryption {
    private static final String TRANSFORMATION = "AES/GCM/NoPadding";
    private static final int GCM_TAG_LENGTH = 128;  // bits
    private static final int GCM_IV_LENGTH = 12;    // bytes (96 bits recommended)
    private static final int AES_KEY_SIZE = 256;    // bits

    private SecretKey key;
    private SecureRandom secureRandom;

    public SecureAESGCMEncryption() throws Exception {
        // Generate strong AES-256 key
        KeyGenerator keyGenerator = KeyGenerator.getInstance("AES");
        keyGenerator.init(AES_KEY_SIZE);
        this.key = keyGenerator.generateKey();
        this.secureRandom = new SecureRandom();
    }

    public String encrypt(String plaintext) throws Exception {
        // Generate random IV for each encryption
        byte[] iv = new byte[GCM_IV_LENGTH];
        secureRandom.nextBytes(iv);

        // Configure GCM parameters
        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);

        // Initialize cipher
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec);

        // Optional: Add authenticated associated data (AAD) for context binding
        cipher.updateAAD("CONTEXT_V1".getBytes(StandardCharsets.UTF_8));

        // Encrypt
        byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));

        // Combine IV + ciphertext for storage
        byte[] encrypted = new byte[iv.length + ciphertext.length];
        System.arraycopy(iv, 0, encrypted, 0, iv.length);
        System.arraycopy(ciphertext, 0, encrypted, iv.length, ciphertext.length);

        return Base64.getEncoder().encodeToString(encrypted);
    }

    public String decrypt(String encryptedData) throws Exception {
        byte[] encrypted = Base64.getDecoder().decode(encryptedData);

        // Extract IV and ciphertext
        byte[] iv = new byte[GCM_IV_LENGTH];
        byte[] ciphertext = new byte[encrypted.length - GCM_IV_LENGTH];
        System.arraycopy(encrypted, 0, iv, 0, iv.length);
        System.arraycopy(encrypted, iv.length, ciphertext, 0, ciphertext.length);

        // Configure GCM parameters
        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);

        // Initialize cipher
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec);

        // Add same AAD used during encryption
        cipher.updateAAD("CONTEXT_V1".getBytes(StandardCharsets.UTF_8));

        // Decrypt and verify authentication tag
        byte[] plaintext = cipher.doFinal(ciphertext);

        return new String(plaintext, StandardCharsets.UTF_8);
    }
}

Why this works:

  • AES-256 provides a large key space that makes brute-force infeasible.
  • GCM delivers authenticated encryption (confidentiality + integrity).
  • A random 96-bit IV per encryption prevents deterministic ciphertexts.
  • The 128-bit tag detects tampering before decryption succeeds.
  • AAD binds ciphertext to context to prevent substitution attacks.

RSA-3072 with OAEP Padding

import java.security.*;
import javax.crypto.Cipher;
import javax.crypto.spec.OAEPParameterSpec;
import javax.crypto.spec.PSource;
import java.nio.charset.StandardCharsets;
import java.security.spec.MGF1ParameterSpec;
import java.util.Base64;

public class SecureRSAEncryption {
    private static final int KEY_SIZE = 3072;  // 3072-bit RSA
    private static final String TRANSFORMATION = "RSA/ECB/OAEPWithSHA-256AndMGF1Padding";

    private KeyPair keyPair;

    public SecureRSAEncryption() throws NoSuchAlgorithmException {
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(KEY_SIZE, new SecureRandom());
        this.keyPair = keyGen.generateKeyPair();
    }

    public String encrypt(String plaintext) throws Exception {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);

        // Configure OAEP with SHA-256
        OAEPParameterSpec oaepParams = new OAEPParameterSpec(
            "SHA-256",
            "MGF1",
            MGF1ParameterSpec.SHA256,
            PSource.PSpecified.DEFAULT
        );

        cipher.init(Cipher.ENCRYPT_MODE, keyPair.getPublic(), oaepParams);

        byte[] encrypted = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encrypted);
    }

    public String decrypt(String encryptedData) throws Exception {
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);

        OAEPParameterSpec oaepParams = new OAEPParameterSpec(
            "SHA-256",
            "MGF1",
            MGF1ParameterSpec.SHA256,
            PSource.PSpecified.DEFAULT
        );

        cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate(), oaepParams);

        byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedData));
        return new String(decrypted, StandardCharsets.UTF_8);
    }
}

Why this works:

  • RSA-3072 provides ~128-bit security for long-term protection.
  • OAEP randomizes padding, blocking PKCS#1 v1.5 padding oracles.
  • SHA-256 for both OAEP and MGF1 avoids weak hash defaults.
  • Explicit OAEP parameters prevent provider default regressions.
  • Suitable for small secrets; use hybrid encryption for bulk data.

BCrypt for Password Hashing

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.UUID;

public class SecurePasswordService {
    private final PasswordEncoder passwordEncoder;

    // A real hash of a value nobody can log in as, generated once at startup.
    // It must be a well-formed 60-character BCrypt hash at the same strength:
    // BCryptPasswordEncoder.matches() rejects anything that fails its format
    // check and returns immediately, without hashing.
    private final String dummyHash;

    public SecurePasswordService() {
        // BCrypt with strength 12 (2^12 = 4096 rounds)
        this.passwordEncoder = new BCryptPasswordEncoder(12);
        this.dummyHash = passwordEncoder.encode(UUID.randomUUID().toString());
    }

    public String hashPassword(String password) {
        // BCrypt automatically generates per-password random salt
        return passwordEncoder.encode(password);
    }

    public boolean verifyPassword(String password, String storedHash) {
        // Timing-safe password comparison
        return passwordEncoder.matches(password, storedHash);
    }

    // Prevent username enumeration when the user doesn't exist
    public boolean authenticateUser(String username, String password, String storedHash) {
        if (storedHash == null) {
            // Same work as a real verification, so the response time does not
            // distinguish "no such user" from "wrong password"
            passwordEncoder.matches(password, dummyHash);
            return false;
        }
        return passwordEncoder.matches(password, storedHash);
    }
}

Why this works:

  • BCrypt is intentionally slow: the work factor sets how many key-expansion rounds run, and raising it raises brute-force cost.
  • It is not memory-hard, and the work factor does not change that. BCrypt's state is Blowfish's P-array and S-boxes - 18 plus 1024 32-bit words, about 4 KB - and that size is fixed whatever the strength, because the cost parameter multiplies rounds rather than memory. Memory-hard designs such as Argon2id and scrypt instead make the memory requirement itself a tunable cost, which is what constrains an attacker running many guesses in parallel.
  • OWASP now treats BCrypt as a legacy option: "The bcrypt password hashing function should only be used for password storage in legacy systems where Argon2 and scrypt are not available." Prefer Argon2id for new work. BCrypt at strength 12 is still a defensible position for a system already using it - the migration has a cost of its own, since every stored hash can only be upgraded as users next log in.
  • Cost factor 12 (4096 rounds) is adjustable as hardware improves.
  • Per-password random salts prevent rainbow table attacks.
  • Encoded output stores salt + hash together for safe storage.
  • Constant-time matches() prevents a timing leak on the comparison itself.
  • The dummy hash closes the other timing leak, the one on the unknown-user branch, and it only closes it if it is a real hash. matches() checks the stored value against \A\$2(a|y|b)?\$\d\d\$[./0-9A-Za-z]{53} first and returns false without hashing when it does not match, so a placeholder string such as "$2a$12$dummyhashtopreventtiming" returns in about 0.2 ms against roughly 245 ms for a genuine one at strength 12 - measured on Spring Security 6.5.5, JDK 26. That is the enumeration oracle the branch exists to remove, made a thousand times louder than no dummy check at all.

PBKDF2 with High Iteration Count

import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Base64;

public class SecurePBKDF2Service {
    private static final int ITERATIONS = 600_000;  // OWASP recommended minimum for PBKDF2-HMAC-SHA256
    private static final int KEY_LENGTH = 256;      // bits
    private static final int SALT_LENGTH = 32;      // bytes
    private static final String ALGORITHM = "PBKDF2WithHmacSHA256";

    private final SecureRandom secureRandom = new SecureRandom();

    public String hashPassword(String password) throws Exception {
        // Generate random salt
        byte[] salt = new byte[SALT_LENGTH];
        secureRandom.nextBytes(salt);

        // Derive key
        byte[] hash = deriveKey(password, salt);

        // Return hash:salt format for storage
        return Base64.getEncoder().encodeToString(hash) + ":" +
               Base64.getEncoder().encodeToString(salt);
    }

    public boolean verifyPassword(String password, String storedHash) throws Exception {
        // Extract hash and salt
        String[] parts = storedHash.split(":");
        byte[] hash = Base64.getDecoder().decode(parts[0]);
        byte[] salt = Base64.getDecoder().decode(parts[1]);

        // Recompute hash
        byte[] computedHash = deriveKey(password, salt);

        // Timing-safe comparison
        return MessageDigest.isEqual(computedHash, hash);
    }

    private byte[] deriveKey(String password, byte[] salt) throws Exception {
        PBEKeySpec spec = new PBEKeySpec(
            password.toCharArray(),
            salt,
            ITERATIONS,
            KEY_LENGTH
        );

        SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);
        return factory.generateSecret(spec).getEncoded();
    }
}

Why this works:

  • 600,000 iterations matches the current OWASP recommendation for PBKDF2-HMAC-SHA256 and raises brute-force cost.
  • Per-user 32-byte random salts prevent rainbow table attacks.
  • SHA-256 avoids deprecated SHA-1 and provides strong hash security.
  • MessageDigest.isEqual() prevents timing leaks in comparisons.
  • The encoded hash:salt format keeps all components together.

SHA-256 Digital Signatures

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;

public class LicenceSigner {

    // SECURE - SHA-256 with a 3072-bit RSA key
    public static KeyPair generateRsaKeyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(3072);
        return generator.generateKeyPair();
    }

    // SECURE - or ECDSA on P-256, which gives comparable strength with a
    // much smaller key and faster signing
    public static KeyPair generateEcKeyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("EC");
        generator.initialize(new ECGenParameterSpec("secp256r1"));
        return generator.generateKeyPair();
    }

    public byte[] sign(byte[] licence, PrivateKey key) throws Exception {
        // "SHA256withRSA" for an RSA key, "SHA256withECDSA" for an EC key
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(key);
        signature.update(licence);
        return signature.sign();
    }

    public boolean verify(byte[] licence, byte[] sig, PublicKey key) throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(key);
        signature.update(licence);
        // verify() returns false for a bad signature and throws for a
        // malformed one - treat both as failure, and never ignore the result
        return signature.verify(sig);
    }
}

Why this works: The signature is computed over a SHA-256 digest, for which no collision is known, so an attacker cannot construct a second document that shares a digest with a signed one. The algorithm name is fixed in code rather than read from the document being verified, which is what stops an attacker downgrading the check by claiming a weaker algorithm. SHA256withECDSA with P-256 is an equally good choice and produces far smaller signatures; prefer RSA only where an existing verifier requires it. For new RSA keys use 3072 bits - NIST rates 2048-bit at 112-bit strength and accepts it for applying protection only through 2030.

RSA-PSS: For new designs, SHA256withRSA/PSS is the better padding scheme and is available through the same Signature API. Plain SHA256withRSA uses PKCS#1 v1.5 padding, which is still acceptable for signatures - the Bleichenbacher attacks concern v1.5 encryption, not signing - but PSS has a security proof that v1.5 lacks.

HMAC-SHA256 for Message Authentication

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.HexFormat;

public class SecureHMACService {
    private static final String ALGORITHM = "HmacSHA256";
    private static final int KEY_SIZE = 32;  // 256 bits

    private final SecretKeySpec secretKey;

    public SecureHMACService(byte[] key) {
        if (key.length < KEY_SIZE) {
            throw new IllegalArgumentException("Key must be at least 256 bits");
        }
        this.secretKey = new SecretKeySpec(key, ALGORITHM);
    }

    public static SecureHMACService withRandomKey() {
        byte[] key = new byte[KEY_SIZE];
        new SecureRandom().nextBytes(key);
        return new SecureHMACService(key);
    }

    public String generateSignature(String data) throws Exception {
        Mac mac = Mac.getInstance(ALGORITHM);
        mac.init(secretKey);

        byte[] signature = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return HexFormat.of().formatHex(signature);
    }

    public boolean verifySignature(String data, String signature) throws Exception {
        String expected = generateSignature(data);

        // Timing-safe comparison
        return MessageDigest.isEqual(
            expected.getBytes(StandardCharsets.UTF_8), 
            signature.getBytes(StandardCharsets.UTF_8)
        );
    }
}

Why this works:

  • HMAC-SHA256 provides integrity and authenticity with modern security strength.
  • A 256-bit key matches SHA-256's security level.
  • HMAC construction prevents length-extension attacks.
  • withRandomKey() generates a fresh 256-bit secret using SecureRandom.
  • Constant-time comparison reduces timing side channels.

Spring Security Crypto Module

import org.springframework.security.crypto.encrypt.BytesEncryptor;
import org.springframework.security.crypto.encrypt.Encryptors;
import org.springframework.security.crypto.keygen.KeyGenerators;

public class SpringCryptoService {
    private final BytesEncryptor encryptor;

    public SpringCryptoService(String password, String hexEncodedSalt) {
        // SECURE - Encryptors.stronger() is AES-256-GCM (authenticated encryption)
        // Encryptors.standard() is AES-256-CBC and is NOT authenticated - avoid it
        this.encryptor = Encryptors.stronger(password, hexEncodedSalt);
    }

    public byte[] encrypt(byte[] plaintext) {
        return encryptor.encrypt(plaintext);
    }

    public byte[] decrypt(byte[] ciphertext) {
        return encryptor.decrypt(ciphertext);
    }

    public static String generateSalt() {
        // 8-byte SecureRandom key, hex-encoded as a String
        return KeyGenerators.string().generateKey();
    }
}

Why this works:

  • Encryptors.stronger() uses AES-256-GCM, so confidentiality and integrity come from a single authenticated mode rather than a hand-assembled Encrypt-then-MAC.
  • A 16-byte random IV is applied per message, so identical plaintexts do not produce identical ciphertexts.
  • The key is PBKDF2-derived from the password and salt, avoiding a raw password used directly as key material.
  • The factory API keeps mode, padding, and IV handling out of application code, where they are easy to get wrong.

Version note: Encryptors.queryableText() was deprecated and removed in Spring Security 6.0; code still calling it will not compile against 6.x. Encryptors.delux() was not removed - it is present in 6.5.x and is the TextEncryptor wrapper around stronger(), so it is AES-256-GCM with hex-encoded output. Reach for it when you need a string rather than bytes: Encryptors.text() is the wrapper around standard() (CBC, unauthenticated) and is the one to avoid. stronger() and delux() share a key derivation and a ciphertext format, so bytes written by one are readable by the other once you hex-decode - verified against spring-security-crypto 6.5.5.

Considerations

Classify the use before choosing a replacement. "Use a modern algorithm" is not a single answer, and picking the wrong primitive is the common failure. For password storage use BCrypt, Argon2 or scrypt through BCryptPasswordEncoder or Argon2PasswordEncoder - never a plain digest, however modern, because speed is the property you are trying to remove. For encryption use Cipher.getInstance("AES/GCM/NoPadding") with a 256-bit key and a unique 12-byte IV per message. For integrity use Mac.getInstance("HmacSHA256"). For signatures use SHA256withRSA with 3072-bit keys, or SHA256withECDSA. For key derivation from a password use PBKDF2WithHmacSHA256 at a current iteration count.

The page's Overview says SHA-256 for hashing; that is not a password instruction. A general-purpose digest is the right tool for integrity and the wrong tool for passwords no matter which digest it is.

Migration Considerations

Changing an algorithm changes the data it produced. Before the fix ships, work out which stores are affected: stored password hashes, encrypted database columns, tokens already issued, and any persisted signatures. Each has a different answer.

Password hashes cannot be converted - you do not have the passwords. Verify against the old format, and rehash with the new one on the next successful login, retiring the old path when the population has drained.

Encrypted columns can be converted, but not instantly. Keep the old key and algorithm readable while a backfill decrypts and re-encrypts each row, and only remove the legacy path once the backfill has completed and been verified. Removing it first turns a migration into data loss.

Common Pitfalls

  • Writing Cipher.getInstance("AES") and assuming a safe default: The transformation string without a mode and padding resolves to ECB on most providers, which leaks structure across blocks. Always name the mode and padding explicitly.
  • Reusing a GCM IV across messages under the same key: GCM fails catastrophically here - it is not a gradual weakening. Two messages sharing an IV leak the XOR of their plaintexts and expose the authentication subkey, breaking integrity for every message under that key. The IV need not be secret, but it must be unique.

Testing

A scanner confirms the weak algorithm is gone. It cannot confirm the replacement works, and a key-strength change is unusually good at passing review while breaking data that already exists. Assert each of these:

  • Ciphertext round-trips. Encrypt, then decrypt with a freshly obtained Cipher instance, and compare against the original plaintext. Reusing the instance hides the most common defect, where the provider generates an IV internally and it is never returned with the ciphertext.
  • Tampering is rejected. Flip one byte of the ciphertext, one byte of the tag, and one byte of the IV, and confirm each throws AEADBadTagException rather than returning plaintext.
  • The key size actually applied. Assert ((RSAPublicKey) keyPair.getPublic()).getModulus().bitLength() == 3072 rather than trusting the initialize() argument, and assert secretKey.getEncoded().length for symmetric keys.
  • Data encrypted before the change still decrypts. Keep a fixture encrypted under the old algorithm and assert the dual-read path returns the original plaintext. This is the test that fails in production if it is missing.
  • Old password hashes still verify, and are upgraded on use. Assert that a correct password checked against a stored legacy hash succeeds, that the stored hash is then rewritten in the new format, and that a wrong password still fails at both stages. Spring Security's DelegatingPasswordEncoder handles the read side by prefix, but the rewrite on successful login is application code and needs its own test.
  • Password hashing is slow enough. Time one matches call and assert it falls in the intended range (roughly 250-500ms). A work factor lowered to speed up the test suite tends to reach production.

Additional Resources