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.Randomis 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 sharedjava.util.Randominstance, 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 reproducesnextLong()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:
ThreadLocalRandomis ajava.util.Randomsubclass 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 Randomcomputes 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
Randominstance, 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) + 100000is 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:
SecureRandomdraws 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
SecureRandomis 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:
SecureRandomensures 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
SecureRandomleave nothing to guess at, and knowing the target's email address does not narrow the search - One-time use: Marking
used=truestops 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
secureRandomtoKeyGenerator.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.Randomleft 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()usesSecureRandominternally (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,0or1), 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
SPECIALstring 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.shufflewith the sameSecureRandomredistributes 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
SecureRandomwith an explicit, predictable seed:new SecureRandom(seedBytes)seeds the instance using exactly the bytes given, rather than the platform's entropy source. IfseedBytescomes from something guessable (a timestamp, a request counter), the resulting instance behaves just as predictably asjava.util.Random. Use the no-argnew SecureRandom()constructor (orSecureRandom.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 toSecureRandombut leaving a utility like an order-ID or invoice-number generator onjava.util.Randombecause 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.