Skip to content

CWE-338: Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG) - Java

Overview

Java code trips this weakness when a non-cryptographic generator - java.util.Random, Math.random() or ThreadLocalRandom - produces a security-sensitive value. These generators are built to be statistically even rather than unpredictable: their output gives away the state behind it, so an attacker who has seen a few values can compute the session tokens, encryption keys and password reset tokens the same instance goes on to produce.

Primary Defence: Use java.security.SecureRandom for all security-sensitive random number generation including session tokens, CSRF tokens, encryption keys, and password reset tokens.

Common Vulnerable Patterns

Using Random for Session Tokens

import java.util.Random;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;

@Path("/api/auth")
public class VulnerableAuthResource {
    private static final Random random = new Random();
    private static final String CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";

    private String generateSessionToken() {
        // VULNERABLE - Using java.util.Random for session tokens
        StringBuilder token = new StringBuilder(32);
        for (int i = 0; i < 32; i++) {
            token.append(CHARS.charAt(random.nextInt(CHARS.length())));
        }
        return token.toString();
    }

    @POST
    @Path("/login")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response login(LoginRequest request) {
        // Authenticate user
        if (authenticateUser(request.getUsername(), request.getPassword())) {
            // VULNERABLE - Predictable session token
            String sessionToken = generateSessionToken();

            // Store in session
            SessionManager.createSession(sessionToken, request.getUsername());

            return Response.ok()
                .entity(new LoginResponse(sessionToken))
                .build();
        }

        return Response.status(Response.Status.UNAUTHORIZED)
            .entity("Invalid credentials")
            .build();
    }

    private boolean authenticateUser(String username, String password) {
        // Authentication logic
        return true;
    }
}

Why this is vulnerable:

  • java.util.Random is a 48-bit linear congruential generator, and 48 bits is small enough to attack directly rather than statistically. nextInt() returns the top 32 bits of the state, so two consecutive outputs are enough: try each of the 2^16 possibilities for the bits the first output discarded, advance the LCG once, and keep the candidate whose result matches the second output. Measured on JDK 26, that search recovers the state in well under a second and then predicts the third output exactly.
  • The bounded nextInt(62) used here leaks about six bits a call rather than thirty-two, which buys nothing: a single 32-character token is 32 consecutive draws off the same state. Counting the alphabet suggests 190 bits of strength; the generator can only ever supply 48, and once one token has been observed it supplies none at all.
  • The instance is static, so every session in the process comes off the same sequence. An attacker who can log in twice has the two outputs the attack needs, and the tokens they then compute are other users'.

Math.random() for Cryptographic Keys

import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;

@RestController
@RequestMapping("/api/keys")
public class VulnerableKeyController {

    private String generateAPIKey() {
        // VULNERABLE - Math.random() is not cryptographically secure
        StringBuilder key = new StringBuilder("sk_");

        for (int i = 0; i < 32; i++) {
            int value = (int) (Math.random() * 36);
            char c = value < 10 ? 
                (char) ('0' + value) : 
                (char) ('A' + value - 10);
            key.append(c);
        }

        return key.toString();
    }

    @PostMapping
    public ResponseEntity<APIKeyResponse> createAPIKey(@RequestBody APIKeyRequest request) {
        // VULNERABLE - Weak API key generation
        String apiKey = generateAPIKey();

        // Store in database
        apiKeyRepository.save(new APIKey(
            request.getUserId(),
            apiKey,
            request.getDescription()
        ));

        return ResponseEntity.ok(new APIKeyResponse(apiKey));
    }
}

Why this is vulnerable:

  • Math.random() is backed by a shared java.util.Random instance, so these keys come off the same 48-bit generator as the tokens above rather than off a cryptographic one.
  • The key is the credential the API authenticates on. An attacker who holds one key of their own has output from that generator, and the keys they then compute are other users' - predicting a key here is the same thing as being issued one.

Time-based Seeding

import java.util.Random;
import jakarta.servlet.http.*;
import jakarta.servlet.annotation.WebServlet;
import java.io.IOException;

@WebServlet("/reset-password")
public class VulnerablePasswordResetServlet extends HttpServlet {

    private String generateResetToken() {
        // VULNERABLE - Time-based seeding is predictable
        Random random = new Random(System.currentTimeMillis());

        long token = random.nextLong();
        return Long.toHexString(token);
    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) 
            throws IOException {
        String email = request.getParameter("email");

        // VULNERABLE - Predictable reset token
        String resetToken = generateResetToken();

        // Store token
        resetTokenRepository.save(email, resetToken);

        // Send email
        emailService.sendPasswordReset(email, resetToken);

        response.setStatus(HttpServletResponse.SC_OK);
        response.getWriter().write("Reset email sent");
    }
}

Why this is vulnerable:

  • Seeding from System.currentTimeMillis() leaves the generator with one unknown: the millisecond in which the request was handled. An attacker who triggered the reset themselves knows that to within a narrow window, and every candidate millisecond in it reproduces nextLong() exactly.
  • The value being guessed is a password reset token, so a correct guess is control of the account it was issued for.

ThreadLocalRandom for Security

import java.util.concurrent.ThreadLocalRandom;
import org.springframework.stereotype.Service;

@Service
public class VulnerableTokenService {

    public String generateCSRFToken() {
        // VULNERABLE - ThreadLocalRandom is NOT cryptographically secure
        StringBuilder token = new StringBuilder();

        for (int i = 0; i < 32; i++) {
            int value = ThreadLocalRandom.current().nextInt(16);
            token.append(Integer.toHexString(value));
        }

        return token.toString();
    }

    public byte[] generateSalt() {
        // VULNERABLE - Weak salt generation
        byte[] salt = new byte[16];
        ThreadLocalRandom.current().nextBytes(salt);
        return salt;
    }

    public String generateOTP() {
        // VULNERABLE - Predictable OTP
        int otp = ThreadLocalRandom.current().nextInt(100000, 1000000);
        return String.format("%06d", otp);
    }
}

Why this is vulnerable:

  • ThreadLocalRandom is a java.util.Random subclass with a name that reads like a threading fix. It removes contention on a shared instance; it does not change what kind of generator you are drawing from, and its own Javadoc says so.
  • Every value above is security-relevant and none of them is unpredictable. A CSRF token from a statistical generator can be computed by anyone who has watched it; a salt from one loses the per-hash uniqueness it exists to provide, letting one precomputation cover many accounts; and a six-digit OTP is a narrow enough target that predicting the generator is barely even necessary.

Apache Commons RandomStringUtils

import org.apache.commons.lang3.RandomStringUtils;

// VULNERABLE on commons-lang3 3.14.0 and earlier - drew from ThreadLocalRandom.
// SECURE from 3.15.0 onward, where the same call is SecureRandom-backed.
String weakKey = RandomStringUtils.randomAlphanumeric(32);

// SECURE and explicit on 3.16.0+, and the non-deprecated spelling from 3.17.0
String strongKey = RandomStringUtils.secure().nextAlphanumeric(32);

Why this is vulnerable: nothing in RandomStringUtils.randomAlphanumeric announces a random number generator - it reads as a string utility, which is why it survives review and why scanner rules for new Random() and Math.random() miss it. Whether it is actually a finding depends entirely on the version on your classpath, so resolve that before triaging: on 3.14.0 and earlier the static methods drew from ThreadLocalRandom.current() and the API key above is predictable, while from 3.15.0 they draw from a SecureRandom and it is not. Verified by reflection on JDK 26: the private generator behind the static methods is java.util.concurrent.ThreadLocalRandom on 3.14.0 and java.security.SecureRandom on 3.19.0. The static forms are deprecated as of 3.17.0 in favour of the explicit pair, and that pair is what you want in new code precisely because it puts the choice in the call site rather than in a dependency version - RandomStringUtils.insecure() is still ThreadLocalRandom, and a RandomUtils call has the same history.

Weak IV Generation

import javax.crypto.*;
import javax.crypto.spec.*;
import java.util.Random;
import java.util.Base64;

public class VulnerableEncryption {
    private SecretKey key;
    private Random random = new Random();

    public VulnerableEncryption(SecretKey key) {
        this.key = key;
    }

    public String encrypt(String plaintext) throws Exception {
        // VULNERABLE - Weak IV generation
        byte[] iv = new byte[16];
        random.nextBytes(iv);  // Using Random instead of SecureRandom!

        IvParameterSpec ivSpec = new IvParameterSpec(iv);
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);

        byte[] ciphertext = cipher.doFinal(plaintext.getBytes());

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

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

// Spring Boot service using vulnerable encryption
import org.springframework.stereotype.Service;

@Service
public class DataEncryptionService {
    private final VulnerableEncryption encryption;

    public DataEncryptionService(SecretKey key) {
        this.encryption = new VulnerableEncryption(key);
    }

    public String encryptSensitiveData(String data) throws Exception {
        // VULNERABLE - Weak IV compromises encryption
        return encryption.encrypt(data);
    }
}

Why this is vulnerable:

  • CBC mode needs an IV the attacker cannot predict before the message is encrypted. Random.nextBytes() does not give one, and the IV is prepended to the ciphertext here, so every IV the instance has produced is visible to whoever holds the output.
  • Once the next IV is known in advance, an attacker who can get chosen plaintext encrypted can test a guess at an earlier plaintext block and confirm it from the ciphertext, recovering the contents without ever touching the key.

Weak UUID Generation

import java.util.Random;
import java.util.UUID;

public class VulnerableUUIDGenerator {
    private static final Random random = new Random();

    public static UUID generateWeakUUID() {
        // VULNERABLE - Custom UUID using weak randomness
        byte[] randomBytes = new byte[16];
        random.nextBytes(randomBytes);

        // Set version and variant bits for UUID
        randomBytes[6] &= 0x0f;
        randomBytes[6] |= 0x40;  // Version 4
        randomBytes[8] &= 0x3f;
        randomBytes[8] |= 0x80;  // Variant 2

        long mostSigBits = 0;
        long leastSigBits = 0;

        for (int i = 0; i < 8; i++) {
            mostSigBits = (mostSigBits << 8) | (randomBytes[i] & 0xff);
        }
        for (int i = 8; i < 16; i++) {
            leastSigBits = (leastSigBits << 8) | (randomBytes[i] & 0xff);
        }

        return new UUID(mostSigBits, leastSigBits);
    }

    public static String generateUserId() {
        // VULNERABLE - Predictable user IDs
        return "user_" + random.nextInt(1000000);
    }
}

// JPA Entity using weak UUIDs
import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {
    @Id
    private UUID id;

    private String username;

    @PrePersist
    public void generateId() {
        // VULNERABLE - Predictable UUID
        this.id = VulnerableUUIDGenerator.generateWeakUUID();
    }
}

Why this is vulnerable:

  • The UUID is the user entity's primary key, so predicting it enumerates accounts: an attacker who recovers the state of the static Random computes the IDs handed to other users instead of guessing at random. Anywhere that ID is what a request quotes to reach a record, that is access as well as enumeration.
  • generateUserId() does not even require the state: nextInt(1000000) is a million values, small enough to walk.

Weak Password Generation

import java.util.Random;

public class VulnerablePasswordGenerator {
    private static final Random random = new Random();
    private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
    private static final String DIGITS = "0123456789";
    private static final String SPECIAL = "!@#$%^&*";

    public static String generateTemporaryPassword(int length) {
        // VULNERABLE - Using Random for password generation
        String allChars = UPPERCASE + LOWERCASE + DIGITS + SPECIAL;
        StringBuilder password = new StringBuilder();

        for (int i = 0; i < length; i++) {
            int index = random.nextInt(allChars.length());
            password.append(allChars.charAt(index));
        }

        return password.toString();
    }
}

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

@Controller("/api/users")
public class UserController {

    @Post("/create")
    public HttpResponse<CreateUserResponse> createUser(@Body CreateUserRequest request) {
        // VULNERABLE - Weak temporary password
        String tempPassword = VulnerablePasswordGenerator.generateTemporaryPassword(12);

        // Create user with temporary password
        User user = userService.createUser(
            request.getUsername(),
            tempPassword
        );

        return HttpResponse.ok(new CreateUserResponse(
            user.getId(),
            tempPassword,
            "Please change your password on first login"
        ));
    }
}

Why this is vulnerable:

  • The generator is a static Random, so every temporary password the service has ever issued comes off one sequence. An attacker who signs up and reads their own password has output from it, and can compute the passwords issued to the accounts created next.
  • The response itself tells the user to change the password at first login, which means the account is live with the generated one until they do. A predicted password is a working login for that whole window.

Sequential Random Numbers

import java.util.Random;
import org.springframework.stereotype.Service;

@Service
public class VulnerableOrderService {
    private Random random = new Random();

    public String generateOrderId() {
        // VULNERABLE - Sequential random numbers are predictable
        long orderId = Math.abs(random.nextLong()) % 1000000000L;
        return String.format("ORD%09d", orderId);
    }

    public String generateInvoiceNumber() {
        // VULNERABLE - Predictable invoice numbers
        int invoiceNum = random.nextInt(900000) + 100000;
        return "INV-" + invoiceNum;
    }

    public String generateConfirmationCode() {
        // VULNERABLE - Predictable confirmation codes
        StringBuilder code = new StringBuilder();
        for (int i = 0; i < 6; i++) {
            code.append(random.nextInt(10));
        }
        return code.toString();
    }
}

Why this is vulnerable:

  • Order IDs, invoice numbers and confirmation codes all come off one Random instance, so output from an attacker's own order predicts the identifiers issued to other customers. Where an order or invoice can be retrieved by quoting its identifier, predicting it is access to that customer's record.
  • The ranges are narrow enough to attack without the generator at all: nextInt(900000) + 100000 is a six-digit invoice number and the confirmation code is six digits too. Walking them also reveals how many orders the business takes.

Secure Patterns

Using SecureRandom for Tokens

import java.security.SecureRandom;
import java.util.Base64;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.*;

@Path("/api/auth")
public class SecureAuthResource {
    private static final SecureRandom secureRandom = new SecureRandom();

    private String generateSessionToken() {
        // SECURE - Using SecureRandom for session tokens
        byte[] randomBytes = new byte[32];  // 256 bits
        secureRandom.nextBytes(randomBytes);

        // Base64 URL-safe encoding
        return Base64.getUrlEncoder().withoutPadding()
            .encodeToString(randomBytes);
    }

    private String generateSessionTokenHex() {
        // Alternative: Hex encoding
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);

        StringBuilder hexString = new StringBuilder();
        for (byte b : randomBytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) hexString.append('0');
            hexString.append(hex);
        }
        return hexString.toString();
    }

    @POST
    @Path("/login")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Response login(LoginRequest request) {
        if (authenticateUser(request.getUsername(), request.getPassword())) {
            // SECURE - Cryptographically strong session token
            String sessionToken = generateSessionToken();

            // Store in database with expiration
            sessionRepository.createSession(
                sessionToken,
                request.getUsername(),
                Instant.now().plus(24, ChronoUnit.HOURS)
            );

            return Response.ok()
                .entity(new LoginResponse(sessionToken))
                .build();
        }

        return Response.status(Response.Status.UNAUTHORIZED)
            .entity("Invalid credentials")
            .build();
    }

    @POST
    @Path("/verify")
    @Produces(MediaType.APPLICATION_JSON)
    public Response verifySession(@HeaderParam("Authorization") String authHeader) {
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return Response.status(Response.Status.UNAUTHORIZED).build();
        }

        String token = authHeader.substring(7);
        Session session = sessionRepository.findByToken(token);

        if (session == null || session.getExpiresAt().isBefore(Instant.now())) {
            return Response.status(Response.Status.UNAUTHORIZED).build();
        }

        return Response.ok()
            .entity(new SessionInfo(session.getUsername()))
            .build();
    }

    private boolean authenticateUser(String username, String password) {
        // Authentication logic
        return true;
    }
}

Why this works:

  • OS-level CSPRNG: SecureRandom draws from system entropy sources and platform RNG providers, so its output does not disclose the state behind it
  • 256-bit entropy: 32-byte tokens give 2^256 possible values, which is out of reach of brute force
  • URL-safe encoding: Base64.getUrlEncoder().withoutPadding() produces a token that survives URLs, headers and cookies unescaped
  • Shared instance: A static SecureRandom is safe to use from multiple threads and pays its initialization cost once rather than per call
  • Security-critical applications: Suitable for session tokens, CSRF tokens, and any identifier that has to be unguessable

Secure API Key Generation

import java.security.SecureRandom;
import java.security.MessageDigest;
import java.util.Base64;
import org.springframework.stereotype.Service;

@Service
public class SecureAPIKeyService {
    private static final SecureRandom secureRandom = new SecureRandom();

    public APIKey generateAPIKey(Long userId, String description) throws Exception {
        // SECURE - Generate cryptographically strong API key
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);

        String apiKey = "sk_live_" + Base64.getUrlEncoder()
            .withoutPadding()
            .encodeToString(randomBytes);

        // Hash API key for storage (never store plaintext)
        String keyHash = hashAPIKey(apiKey);

        // Store in database
        APIKey key = new APIKey();
        key.setUserId(userId);
        key.setKeyHash(keyHash);
        key.setDescription(description);
        key.setCreatedAt(Instant.now());

        apiKeyRepository.save(key);

        // Return plaintext key only once
        key.setPlaintextKey(apiKey);  // Transient field
        return key;
    }

    private String hashAPIKey(String apiKey) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hash = digest.digest(apiKey.getBytes());
        return Base64.getEncoder().encodeToString(hash);
    }

    public boolean verifyAPIKey(String apiKey) throws Exception {
        String keyHash = hashAPIKey(apiKey);

        APIKey key = apiKeyRepository.findByKeyHash(keyHash);

        if (key == null || key.getRevokedAt() != null) {
            return false;
        }

        // Update last used timestamp
        key.setLastUsed(Instant.now());
        apiKeyRepository.save(key);

        return true;
    }
}

// Spring Boot REST controller
import org.springframework.web.bind.annotation.*;
import org.springframework.http.ResponseEntity;

@RestController
@RequestMapping("/api/keys")
public class APIKeyController {
    private final SecureAPIKeyService apiKeyService;

    public APIKeyController(SecureAPIKeyService apiKeyService) {
        this.apiKeyService = apiKeyService;
    }

    @PostMapping
    public ResponseEntity<APIKeyResponse> createAPIKey(@RequestBody APIKeyRequest request) {
        try {
            APIKey key = apiKeyService.generateAPIKey(
                request.getUserId(),
                request.getDescription()
            );

            return ResponseEntity.ok(new APIKeyResponse(
                key.getPlaintextKey(),
                "Store this key securely. It will not be shown again."
            ));
        } catch (Exception e) {
            return ResponseEntity.internalServerError().build();
        }
    }
}

Why this works:

  • 256-bit cryptographic entropy: SecureRandom ensures keys are unpredictable and unique, preventing guessing/enumeration
  • Hash before storage: SHA-256 hashing means compromised databases yield useless hashes, not working keys
  • One-time display: The transient field carries the plaintext key back only at creation, so the caller has to store it and the server keeps nothing that can be replayed
  • Industry-standard prefix: sk_live_ prefix (Stripe convention) distinguishes key types and gives secret scanners something to match when a key is committed by accident

Secure Password Reset Tokens

import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import org.springframework.stereotype.Service;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;

@Service
public class SecurePasswordResetService {
    private static final SecureRandom secureRandom = new SecureRandom();
    private final PasswordResetTokenRepository tokenRepository;
    private final JavaMailSender mailSender;

    public SecurePasswordResetService(
            PasswordResetTokenRepository tokenRepository,
            JavaMailSender mailSender) {
        this.tokenRepository = tokenRepository;
        this.mailSender = mailSender;
    }

    public void initiatePasswordReset(String email) {
        // SECURE - Generate cryptographically strong reset token
        String resetToken = generateResetToken();

        // Store token with expiration
        PasswordResetToken token = new PasswordResetToken();
        token.setEmail(email);
        token.setToken(resetToken);
        token.setExpiresAt(Instant.now().plus(1, ChronoUnit.HOURS));
        token.setUsed(false);

        tokenRepository.save(token);

        // Send email
        sendResetEmail(email, resetToken);
    }

    private String generateResetToken() {
        byte[] randomBytes = new byte[32];
        secureRandom.nextBytes(randomBytes);
        return Base64.getUrlEncoder().withoutPadding()
            .encodeToString(randomBytes);
    }

    public boolean validateAndResetPassword(String token, String newPassword) {
        PasswordResetToken resetToken = tokenRepository.findByToken(token);

        if (resetToken == null || 
            resetToken.isUsed() ||
            resetToken.getExpiresAt().isBefore(Instant.now())) {
            return false;
        }

        // Reset password
        User user = userRepository.findByEmail(resetToken.getEmail());
        user.setPassword(passwordEncoder.encode(newPassword));
        userRepository.save(user);

        // Mark token as used
        resetToken.setUsed(true);
        tokenRepository.save(resetToken);

        return true;
    }

    private void sendResetEmail(String email, String token) {
        String resetUrl = "https://example.com/reset-password?token=" + token;

        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(email);
        message.setSubject("Password Reset Request");
        message.setText("Click here to reset your password: " + resetUrl +
            "\n\nThis link expires in 1 hour.");

        mailSender.send(message);
    }
}

Why this works:

  • Cryptographic unpredictability: 256 bits from SecureRandom leave nothing to guess at, and knowing the target's email address does not narrow the search
  • One-time use: Marking used=true stops a token recovered from an intercepted email being replayed after the reset has happened
  • Time-limited window: The one-hour expiration set above limits how long an intercepted link is worth anything
  • Automatic validation: The database timestamp check rejects expired tokens without manual cleanup
  • Proof of email ownership: Stronger than mailing a temporary password - completing the reset requires access to the mailbox

Secure IV and Salt Generation

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

public class SecureEncryption {
    private static final SecureRandom secureRandom = new SecureRandom();
    private final SecretKey key;

    public SecureEncryption(SecretKey key) {
        this.key = key;
    }

    public String encrypt(String plaintext) throws Exception {
        // SECURE - Generate cryptographically strong IV.
        // 12 bytes (96 bits) is the size NIST SP 800-38D recommends for GCM; other
        // lengths are legal but are folded through GHASH, which is slower and loses
        // the clean uniqueness argument that makes a random nonce safe here.
        byte[] iv = new byte[12];
        secureRandom.nextBytes(iv);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));

        byte[] ciphertext = cipher.doFinal(plaintext.getBytes());

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

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

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

        // Extract IV and ciphertext - must match the 12-byte IV written by encrypt()
        byte[] iv = new byte[12];
        byte[] ciphertext = new byte[combined.length - iv.length];
        System.arraycopy(combined, 0, iv, 0, iv.length);
        System.arraycopy(combined, iv.length, ciphertext, 0, ciphertext.length);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(128, iv));

        byte[] plaintext = cipher.doFinal(ciphertext);
        return new String(plaintext);
    }

    public static byte[] generateSalt() {
        // SECURE - Generate cryptographically strong salt
        byte[] salt = new byte[32];  // 256 bits
        secureRandom.nextBytes(salt);
        return salt;
    }

    public static SecretKey generateKey() throws Exception {
        // SECURE - Generate cryptographically strong key
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256, secureRandom);  // Explicitly use SecureRandom
        return keyGen.generateKey();
    }
}

Why this works:

  • Unique random IVs/nonces: A 96-bit GCM nonce from SecureRandom.nextBytes() is unique in practice at any volume this code will see; reusing one under the same key repeats the keystream, and XORing the two ciphertexts then recovers both plaintexts
  • Authenticated encryption: AES-GCM produces an authentication tag alongside the ciphertext, so tampering is detected at decryption rather than passed through as plaintext
  • 256-bit salt uniqueness: 32-byte salts ensure identical passwords produce different hashes, so no precomputed table covers more than one of them
  • Explicit CSPRNG: Passing secureRandom to KeyGenerator.init() keeps the key itself off whatever default the provider would otherwise pick
  • Complete cryptographic workflow: Every random input in the path - salt, nonce and key - comes from the same CSPRNG, with no java.util.Random left in it

UUID.randomUUID() (Uses SecureRandom)

import java.util.UUID;
import jakarta.persistence.*;

@Entity
@Table(name = "users")
public class User {
    // JPA 3.1 (Hibernate 6.2+). Hibernate's @GenericGenerator has been
    // deprecated for removal since 6.5, so do not reach for it; where the
    // version matters, @UuidGenerator(style = RANDOM) pins the random form.
    @Id
    @GeneratedValue(strategy = GenerationType.UUID)
    private UUID id;

    private String username;
    private String email;

    // UUID.randomUUID() uses SecureRandom internally
    public static UUID generateSecureId() {
        return UUID.randomUUID();
    }
}

// Service using secure UUIDs
import org.springframework.stereotype.Service;

@Service
public class UserService {

    public User createUser(String username, String email) {
        User user = new User();
        user.setId(UUID.randomUUID());  // SECURE - Uses SecureRandom
        user.setUsername(username);
        user.setEmail(email);

        return userRepository.save(user);
    }

    public String generateInvitationCode() {
        // SECURE - UUID for invitation codes
        return UUID.randomUUID().toString();
    }
}

Why this works:

  • 122 bits of randomness: UUID.randomUUID() uses SecureRandom internally (since Java 8+), providing 2^122 possible values (6 bits for version/variant)
  • Collision resistance: That keyspace is large enough that trillions of generated UUIDs are not expected to collide, without any central allocator
  • Prevents enumeration: Unpredictability stops an attacker discovering other records by incrementing an ID they already hold
  • Public identifier use: Suits REST APIs, primary keys that appear in URLs, and file names - anywhere a sequential pattern would leak information
  • Security consideration: For session tokens and API keys, prefer an explicit 256-bit token from SecureRandom
  • Balance: A random UUID is unpredictable and globally unique in a single value, in a format the systems downstream already parse

Secure OTP Generation

import java.security.SecureRandom;
import java.time.Instant;
import java.time.temporal.ChronoUnit;

public class SecureOTPService {
    private static final SecureRandom secureRandom = new SecureRandom();

    public String generateNumericOTP(int length) {
        // SECURE - Generate cryptographically strong numeric OTP
        StringBuilder otp = new StringBuilder();

        for (int i = 0; i < length; i++) {
            // SecureRandom for each digit
            otp.append(secureRandom.nextInt(10));
        }

        return otp.toString();
    }

    public String generateAlphanumericOTP(int length) {
        // SECURE - Alphanumeric OTP
        String chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";  // Exclude ambiguous chars
        StringBuilder otp = new StringBuilder();

        for (int i = 0; i < length; i++) {
            int index = secureRandom.nextInt(chars.length());
            otp.append(chars.charAt(index));
        }

        return otp.toString();
    }

    public OTP createOTP(String identifier, OTPType type) {
        String code = type == OTPType.NUMERIC ? 
            generateNumericOTP(6) : 
            generateAlphanumericOTP(8);

        OTP otp = new OTP();
        otp.setIdentifier(identifier);
        otp.setCode(code);
        otp.setType(type);
        otp.setExpiresAt(Instant.now().plus(10, ChronoUnit.MINUTES));
        otp.setUsed(false);

        return otpRepository.save(otp);
    }

    public boolean verifyOTP(String identifier, String code) {
        OTP otp = otpRepository.findByIdentifierAndCode(identifier, code);

        if (otp == null || 
            otp.isUsed() ||
            otp.getExpiresAt().isBefore(Instant.now())) {
            return false;
        }

        // Mark as used
        otp.setUsed(true);
        otpRepository.save(otp);

        return true;
    }
}

Why this works:

  • Cryptographic unpredictability: SecureRandom.nextInt(10) per digit gives a 6-digit OTP 10^6 = 1 million values, so an online guess averages 500,000 attempts
  • Layered defenses: One-time use and the ten-minute expiry above bound the window, and rate limiting the verification endpoint (3-5 attempts) is what makes a keyspace that small impractical to walk
  • Prevents prediction: A million values is only a floor if the generator is strong - with a weak PRNG, past codes give away future ones and the keyspace stops mattering
  • Replay prevention: The expiry timestamp and the used flag stop an intercepted code being submitted a second time
  • Security scaling: 8-digit OTPs give 100M combinations; the alphanumeric form above draws 8 characters from a deliberately unambiguous 32-character alphabet (no I, O, 0 or 1), which is 32^8 ≈ 1.1x10^12. Excluding the confusable characters costs entropy per character, so compare against the alphabet you actually shipped, not against 36
  • Suitable use cases: Email/SMS 2FA, temporary access codes, account verification workflows

Secure Password Generation

import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class SecurePasswordGenerator {
    private static final SecureRandom secureRandom = new SecureRandom();
    private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
    private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
    private static final String DIGITS = "0123456789";
    private static final String SPECIAL = "!@#$%^&*()_+-=[]{}|;:,.<>?";

    public static String generatePassword(int length, boolean includeSpecial) {
        if (length < 12) {
            throw new IllegalArgumentException("Password must be at least 12 characters");
        }

        // SECURE - Build character set
        String allChars = UPPERCASE + LOWERCASE + DIGITS;
        if (includeSpecial) {
            allChars += SPECIAL;
        }

        // SECURE - Generate password with SecureRandom
        StringBuilder password = new StringBuilder(length);

        // Ensure at least one of each required character type
        password.append(UPPERCASE.charAt(secureRandom.nextInt(UPPERCASE.length())));
        password.append(LOWERCASE.charAt(secureRandom.nextInt(LOWERCASE.length())));
        password.append(DIGITS.charAt(secureRandom.nextInt(DIGITS.length())));

        if (includeSpecial) {
            password.append(SPECIAL.charAt(secureRandom.nextInt(SPECIAL.length())));
        }

        // Fill remaining characters
        for (int i = password.length(); i < length; i++) {
            password.append(allChars.charAt(secureRandom.nextInt(allChars.length())));
        }

        // Shuffle to avoid predictable pattern
        return shuffleString(password.toString());
    }

    private static String shuffleString(String input) {
        List<Character> chars = new ArrayList<>();
        for (char c : input.toCharArray()) {
            chars.add(c);
        }
        Collections.shuffle(chars, secureRandom);

        StringBuilder shuffled = new StringBuilder();
        for (char c : chars) {
            shuffled.append(c);
        }
        return shuffled.toString();
    }
}

// Service using secure password generation
import io.micronaut.http.annotation.*;

@Controller("/api/users")
public class UserController {

    @Post("/create")
    public HttpResponse<CreateUserResponse> createUser(@Body CreateUserRequest request) {
        // SECURE - Generate strong temporary password
        String tempPassword = SecurePasswordGenerator.generatePassword(16, true);

        User user = userService.createUser(
            request.getUsername(),
            tempPassword
        );

        // Send password via secure channel
        emailService.sendTemporaryPassword(user.getEmail(), tempPassword);

        return HttpResponse.ok(new CreateUserResponse(
            user.getId(),
            "Temporary password sent to email"
        ));
    }
}

Why this works:

  • Large keyspace: the four pools above total 88 characters (26 + 26 + 10 + 26 punctuation), so a 16-character password is one of 88^16 ≈ 1.3x10^31 - count the actual SPECIAL string rather than assuming the 95 printable ASCII characters, because a shorter symbol set is the usual concession to a downstream system and it changes the figure
  • Policy compliance: Guarantees at least one character from each required category, each of them picked with SecureRandom
  • Pattern prevention: Collections.shuffle with the same SecureRandom redistributes those guaranteed characters through the password, so it does not always open with an "Aa1!" run
  • High entropy: The generator refuses anything under 12 characters and the controller above asks for 16, which leaves enough entropy that knowing the algorithm buys an attacker nothing
  • Use cases: Account provisioning, temporary access codes and password resets - places where a user-chosen password would be the weak link

Common Pitfalls

  • Constructing SecureRandom with an explicit, predictable seed: new SecureRandom(seedBytes) seeds the instance using exactly the bytes given, rather than the platform's entropy source. If seedBytes comes from something guessable (a timestamp, a request counter), the resulting instance behaves just as predictably as java.util.Random. Use the no-arg new SecureRandom() constructor (or SecureRandom.getInstanceStrong()) and let the platform seed it.
  • Truncating secure output to shorten a token: Taking SecureRandom.nextBytes() output but only using the first few bytes of a 32-byte array to keep a token shorter for a URL. The algorithm is still cryptographically strong, but 64 bits of entropy is a meaningfully smaller brute-force target than 256 bits, especially without rate limiting.
  • Partial migration leaving one helper on java.util.Random: Switching the main auth/token generation to SecureRandom but leaving a utility like an order-ID or invoice-number generator on java.util.Random because it wasn't treated as security-relevant at the time - if that identifier is later reused as an access token or lookup key, the weak PRNG becomes exploitable again.

Additional Resources