
The java.security package is the platform crypto and policy API. It is not a web application firewall.
If you hash a password, verify a signature, or load a keystore, you want the current JDK APIs and a work factor that is still expensive. The Security Manager is gone. Do not write new code that expected it to sandbox a library.
The usual mistake is copying a 2012 MessageDigest.getInstance("MD5") snippet into a 2026 service and calling it hashing.
This page is which Java security APIs still belong in an app, what JEP 486 changed, and the password and TLS defaults to refuse.
JDK 24 reached General Availability on 18 March 2025. JEP 486 permanently disabled the Security Manager. That API does not enforce anything on JDK 24, JDK 25, or JDK 26. The OpenJDK 25 schedule lists LTS General Availability on 16 September 2025. JDK 26 GA was 17 March 2026. The July 2026 CPU listed baselines 25.0.4+7 and 26.0.2+10. OpenJDK 26.0.2.1 is dated 18 August 2026.
This page is the practitioner map of java.security and javax.crypto in 2026. It is not a glossary of every engine. Pair it with the secure coding checklist for the rest of secret handling, with input validation before anything you digest, and with session management when the token is a cookie instead of a JCA nonce.
Name the JCA engine and let the provider hold the key. A gist that calls Cipher.getInstance(“AES”) or hashes a password with SHA-1 is a notebook, not a control. SecurityManager is an empty box after JDK 24.
SecureCoding
SecurityManager is gone after JDK 24
JEP 411 deprecated the Security Manager for removal in Java 17, 2021. JEP 486, delivered in JDK 24, revised the platform so you cannot enable it and other platform classes no longer consult it. The JEP says the API itself stays until a later removal. On JDK 24 and later, -Djava.security.manager is a boot failure, not a sandbox.
A Security Manager plus FilePermission was an applet-era control. It never replaced OS isolation, a container, or an application check on the object. If you still have a grant policy in a repo that boots on 25, delete the flag and the file. Move the file rule to the process user and to the query that loads the row.
JSSE, JAAS login modules, and SASL still exist. They are not this page. TLS configuration belongs on the connector and the JDK security baseline, not in a homemade SSLSocket sample that still names SSLv3. Transformation strings live in the AES section, not here.
Tokens come from SecureRandom
java.util.Random documents that two instances with the same seed return the same sequence. That is the portability contract. It is also why a session id minted with new Random() is a known stream once anyone sees a few outputs. SecureRandom is the JCA CSPRNG. SecureRandom.getInstanceStrong() is documented to return the strongest implementation the platform advertises. On Linux that is often a blocking generator. Use it when you mint a long-lived key. For request tokens, TokenRng below uses the default SecureRandom, which the JDK seeds from the OS.
import java.security.SecureRandom;
import java.util.Base64;
final class TokenRng {
private static final SecureRandom RNG = new SecureRandom();
static String urlToken(int bytes) {
if (bytes < 32) {
throw new IllegalArgumentException("token too short");
}
byte[] raw = new byte[bytes];
RNG.nextBytes(raw);
return Base64.getUrlEncoder().withoutPadding().encodeToString(raw);
}
}
TokenRng.urlToken(32) is 32 random bytes, not 32 characters of a UUID. Do not seed SecureRandom from System.currentTimeMillis(). Do not copy bytes out of Math.random(). The named fallback if a FIPS provider is required is SecureRandom.getInstance("DRBG") after you have listed that provider in the security configuration you actually ship. I have not pinned a provider name on this page because that string is an install choice.
// Named fallback when a FIPS DRBG is the local policy
SecureRandom fallback = SecureRandom.getInstance("DRBG");
byte[] raw = new byte[32];
fallback.nextBytes(raw);
Hash passwords with PBKDF2, not SHA-1
The JDK 25 MessageDigest page still lists SHA-1, SHA-256, and SHA-384 as required algorithms. Required means the provider ships them. It does not mean SHA-1 is a password store. A single SHA-256 of a password is also the wrong job: it is fast. The OWASP password storage cheat sheet prefers Argon2id. When you must stay on the JDK without a third-party hasher, it names PBKDF2-HMAC-SHA256 at 600000 iterations or more. JDK 25 finalized JEP 510, the javax.crypto.KDF API. That JEP ships HKDF. It explicitly leaves PBKDF2 on SecretKeyFactory. Argon2 is listed as future work. Do not call KDF.getInstance("HKDF-SHA256") on a user password.
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.spec.KeySpec;
import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
final class PasswordHasher {
static final int ITERATIONS = 600_000;
static final int KEY_BITS = 256;
private static final SecureRandom RNG = new SecureRandom();
private static final String ALG = "PBKDF2WithHmacSHA256";
static String hash(char[] password) throws Exception {
byte[] salt = new byte[16];
RNG.nextBytes(salt);
byte[] dk = stretch(password, salt);
return ITERATIONS + ":" + b64(salt) + ":" + b64(dk);
}
static boolean matches(char[] password, String stored) throws Exception {
String[] parts = stored.split(":");
if (parts.length != 3) return false;
final int iter;
try {
iter = Integer.parseInt(parts[0]);
} catch (NumberFormatException ex) {
return false;
}
if (iter < ITERATIONS) return false;
byte[] salt = Base64.getDecoder().decode(parts[1]);
byte[] expected = Base64.getDecoder().decode(parts[2]);
byte[] actual = stretch(password, salt);
return MessageDigest.isEqual(expected, actual);
}
private static byte[] stretch(char[] password, byte[] salt) throws Exception {
KeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_BITS);
return SecretKeyFactory.getInstance(ALG).generateSecret(spec).getEncoded();
}
private static String b64(byte[] raw) {
return Base64.getEncoder().encodeToString(raw);
}
}
PasswordHasher stores iteration count, salt, and digest. matches refuses a record whose iteration count is below 600000 so a silent downgrade cannot verify. Wipe the char[] at the caller when you are done. SHA-512 is a fine integrity digest. It is still a fast password hash. Those are different jobs.
| Job | JCA call | Do not use |
|---|---|---|
| Integrity | SHA-256 | MD5, SHA-1 |
| Password | PBKDF2WithHmacSHA256 | one-shot digest |
| Key expand | KDF HKDF-SHA256 | HKDF on a password |
| Token | TokenRng | java.util.Random |
Name AES/GCM. Never bare AES
The JDK 25 Cipher javadoc is the first-party list. The sample transformation is AES/CBC/PKCS5Padding. The same page lists AES/ECB/PKCS5Padding as a standard name. A call that passes only AES lets the provider pick the mode. SunJCE has historically picked ECB. ECB repeats blocks. That is not confidentiality. CBC without a MAC is the other textbook miss. The string this page uses is AES/GCM/NoPadding. GCM is an AEAD. It takes a 12-byte nonce and a 128-bit tag.
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
final class AesGcmBox {
static final String TRANSFORM = "AES/GCM/NoPadding";
static final int TAG_BITS = 128;
static final int NONCE_BYTES = 12;
private static final SecureRandom RNG = new SecureRandom();
static byte[] seal(SecretKey key, byte[] plaintext, byte[] aad) throws Exception {
byte[] nonce = new byte[NONCE_BYTES];
RNG.nextBytes(nonce);
Cipher c = Cipher.getInstance(TRANSFORM);
c.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, nonce));
if (aad != null) c.updateAAD(aad);
byte[] ct = c.doFinal(plaintext);
byte[] out = new byte[nonce.length + ct.length];
System.arraycopy(nonce, 0, out, 0, nonce.length);
System.arraycopy(ct, 0, out, nonce.length, ct.length);
return out;
}
static byte[] open(SecretKey key, byte[] box, byte[] aad) throws Exception {
if (box.length < NONCE_BYTES + 16) {
throw new IllegalArgumentException("ciphertext too short");
}
byte[] nonce = new byte[NONCE_BYTES];
System.arraycopy(box, 0, nonce, 0, NONCE_BYTES);
byte[] ct = new byte[box.length - NONCE_BYTES];
System.arraycopy(box, NONCE_BYTES, ct, 0, ct.length);
Cipher c = Cipher.getInstance(TRANSFORM);
c.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, nonce));
if (aad != null) c.updateAAD(aad);
return c.doFinal(ct);
}
}
Reuse of a nonce with the same key breaks GCM. AesGcmBox.seal mints a fresh 12-byte nonce every call and prefixes it. Pass a tenant id or object id as AAD so a swapped blob fails. Do not write a homemade CBC-then-HMAC. Do not invent a counter mode. If you need HKDF to expand one master secret into an AES key, that is KDF.getInstance("HKDF-SHA256") from JEP 510, with the example on that JEP. It is not a reason to XOR the password into the key schedule.
// BAD: provider-default mode, often ECB
// Cipher.getInstance("AES")
// BAD: one-shot digest treated as a password store
// MessageDigest.getInstance("SHA-1").digest(password.getBytes())
Compare secrets with MessageDigest.isEqual
The JDK 25 isEqual implementation note says all bytes in the first array are examined unless the second is null or empty, and that the time depends only on the length of the first array, not on the contents. That is the compare you want for a password digest, an API token, or a MAC. Arrays.equals can return on the first mismatch. Do not write a for loop that breaks early on a secret.
boolean tokenOk = MessageDigest.isEqual(
expectedToken.getBytes(java.nio.charset.StandardCharsets.UTF_8),
presentedToken.getBytes(java.nio.charset.StandardCharsets.UTF_8)
);
Length hiding is not free. If one side can be empty, reject empty first, then compare equal-length buffers you copied into fixed arrays. PasswordHasher.matches already goes through isEqual. Keep that path for anything you would be sad to leak one byte at a time.
Load keys from a KeyStore
A 32-byte AES key in application.properties is a password with extra steps. JCA already has KeyStore. PKCS12 is the portable file format. The process user reads it. The app code asks for an alias. The password for the store comes from the environment, not from git.
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyStore;
import javax.crypto.SecretKey;
final class AppKeys {
static SecretKey loadAppKeyStore(Path pkcs12, char[] storePass, String alias)
throws Exception {
KeyStore ks = KeyStore.getInstance("PKCS12");
try (InputStream in = Files.newInputStream(pkcs12)) {
ks.load(in, storePass);
}
KeyStore.ProtectionParameter prot = new KeyStore.PasswordProtection(storePass);
KeyStore.SecretKeyEntry entry =
(KeyStore.SecretKeyEntry) ks.getEntry(alias, prot);
if (entry == null) {
throw new IllegalStateException("missing alias " + alias);
}
return entry.getSecretKey();
}
}
loadAppKeyStore is the named gate. Call it once at boot. Hand AesGcmBox the SecretKey. If the file or the alias is missing, fail the process. Do not invent a fallback key of sixteen zero bytes. A hardware token or a cloud KMS that implements a JCA provider is a later swap of KeyStore.getInstance. The call shape stays.
Prove the hasher, the cipher, and the RNG
You are proving your process, not attacking a foreign host.
- Boot the app with
JAVA_HOMEpointing at 25.0.4 or 26.0.2 or newer.java -versionmust not print 17 if you deleted the manager and still claim this page. - Call
PasswordHasher.hashtwice on the same password. The stored strings must differ.matchesmust return true on both. A record with iteration count 1000 must return false. - Seal a buffer with
AesGcmBox, flip one ciphertext byte, and expectAEADBadTagExceptiononopen. - Grep the tree for
Cipher.getInstance("AES"),MessageDigest.getInstance("SHA-1"),new Random()next to a token, andSecurityManager. - Confirm
loadAppKeyStorethrows if the PKCS12 path is missing. The process must not start on a hardcoded key.
rg -n "Cipher\\.getInstance\\(\"AES\"\\)|MessageDigest\\.getInstance\\(\"(MD5|SHA-1|SHA1)\"\\)|new Random\\(|new SecurityManager|PBKDF2WithHmacSHA1" \
--glob '!target' --glob '!build'
java -version
# Expect: 25.0.4 or 26.0.2 or newer, August 2026 CPU baseline
A hit on SHA-1 for a leftover CMS signature is a ticket, not a password store. A hit on new Random() in a game loop is noise. A hit next to session or token is the review.
Questions we keep getting
Can I still set a Security Manager on JDK 21?
Yes. 21 is the previous LTS and still has the deprecated manager. It warns. JEP 486 is JDK 24 and later. If you are on 21.0.12 from the July 2026 baseline, plan the delete before you move to 25. Do not add a new policy file in 2026.
Is BCrypt better than PBKDF2 in Java?
OWASP prefers Argon2id, then scrypt, then bcrypt, then PBKDF2 when you need a FIPS story. The JDK does not ship Argon2 in KDF yet. JEP 510 says so. Spring Security’s BCryptPasswordEncoder is a fine library choice. This page stays on PasswordHasher so the control is visible in JCA. Do not mix both on the same user table without a prefix.
Should I implement AES myself to avoid provider bugs?
No. Write the transformation string. Let the provider run the rounds. A homemade AES is how you get a timing leak and a wrong mix column. If you do not trust a provider, change providers. Do not paste a cipher from a gist.



