Java

Chapter 24: Security, Cryptography, and Defensive Coding in Java

By Ali Naqi | November 03, 2026

Chapter 24: Security, Cryptography, and Defensive Coding in Java

🔒 Chapter 24: Security, Cryptography, and Defensive Coding in Java

1. The Core Mindset: Defense in Depth

Writing functional, high-performance code is only half of an engineer's responsibility. The moment an application interfaces with an open network or takes inputs from human users, it enters a hostile environment.

A single poorly handled buffer, an unchecked deserialization stream, or an insecure cryptographic configuration can compromise an entire enterprise infrastructure. In Java, security is structured across multiple layers:

┌────────────────────────────────────────────────────────┐

│ 1. Application Layer: Input Sanitation, Auth, RBAC │

├────────────────────────────────────────────────────────┤

│ 2. Cryptographic Layer: JCA/JCE (AES-GCM, Hashes) │

├────────────────────────────────────────────────────────┤

│ 3. Memory & Language Layer: Immutability, Type Safety │

├────────────────────────────────────────────────────────┤

│ 4. Platform Layer: TLS 1.3, KeyStores, Secure Random │

└────────────────────────────────────────────────────────┘


2. Java Cryptography Architecture (JCA / JCE)

Java does not bake hardcoded cryptographic algorithms directly into its syntax. Instead, it provides the Java Cryptography Architecture (JCA) and Java Cryptography Extension (JCE) via the java.security and javax.crypto packages.

JCA operates via the Provider Architecture:

 * Standard abstract engine classes (Cipher, MessageDigest, Signature, KeyGenerator) provide the public API.

 * Pluggable Security Providers (such as the default SUN / SunJCE provider, or third-party engines like Bouncy Castle) supply the underlying mathematical implementations.

// Discovering active cryptographic providers registered in your JVM

for (Provider provider : Security.getProviders()) {

    System.out.println("Provider: " + provider.getName() + " (v" + provider.getVersionStr() + ")");

}


3. Cryptographically Secure Random Numbers

Standard pseudorandom generators like java.util.Random or Math.random() are deterministic: they rely on a linear congruential formula seeded with the system clock. An attacker observing a small sequence of generated tokens can deduce future states, rendering session IDs, password reset links, and MFA tokens vulnerable.

For any security-sensitive task, always use java.security.SecureRandom, which harvests hardware and operating system entropy (/dev/urandom on Unix-like platforms):

import java.security.SecureRandom;

import java.util.Base64;


public class SecurityTokenGenerator {

    private static final SecureRandom SECURE_RANDOM = new SecureRandom();


    public static String generateOpaqueSessionToken(int byteLength) {

        byte[] tokenBytes = new byte[byteLength];

        SECURE_RANDOM.nextBytes(tokenBytes);

        // URL-safe Base64 representation without padding

        return Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes);

    }


    public static void main(String[] args) {

        System.out.println("Crypto-Secure Token: " + generateOpaqueSessionToken(32));

    }

}


4. Modern Symmetric Encryption: AES with Authenticated Encryption (AES-GCM)

When storing confidential data at rest (credit cards, personally identifiable information), symmetric encryption ensures only parties possessing the secret key can decrypt the payload.

The Vulnerability of Legacy Modes (ECB and CBC)

 * ECB (Electronic Codebook): Encrypts identical blocks of plaintext into identical ciphertext blocks. It leaks data patterns (e.g., the famous ECB Tux penguin leak). Never use ECB.

 * CBC (Cipher Block Chaining): Better than ECB, but vulnerable to padding oracle attacks if authentication is not applied.

The Modern Gold Standard: AES-GCM (Galois/Counter Mode)

AES-GCM is an Authenticated Encryption with Associated Data (AEAD) cipher mode. It encrypts the plaintext while producing an authentication tag that guarantees the data has not been tampered with in transit.

import javax.crypto.Cipher;

import javax.crypto.KeyGenerator;

import javax.crypto.SecretKey;

import javax.crypto.spec.GCMParameterSpec;

import java.nio.ByteBuffer;

import java.nio.charset.StandardCharsets;

import java.security.SecureRandom;

import java.util.Base64;


public class SymmetricEncryptionEngine {


    private static final String ALGORITHM = "AES/GCM/NoPadding";

    private static final int TAG_LENGTH_BIT = 128; // Standard authentication tag length

    private static final int IV_LENGTH_BYTE = 12; // Recommended 96-bit Initialization Vector


    public static SecretKey generateKey() throws Exception {

        KeyGenerator keyGen = KeyGenerator.getInstance("AES");

        keyGen.init(256, new SecureRandom()); // 256-bit AES

        return keyGen.generateKey();

    }


    public static String encrypt(String plaintext, SecretKey key) throws Exception {

        byte[] iv = new byte[IV_LENGTH_BYTE];

        new SecureRandom().nextBytes(iv); // Unique IV per encryption!


        Cipher cipher = Cipher.getInstance(ALGORITHM);

        GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);

        cipher.init(Cipher.ENCRYPT_MODE, key, spec);


        byte[] cipherText = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));


        // Pack IV and Ciphertext together into a single composite byte array

        byte[] payload = ByteBuffer.allocate(iv.length + cipherText.length)

                .put(iv)

                .put(cipherText)

                .array();


        return Base64.getEncoder().encodeToString(payload);

    }


    public static String decrypt(String base64Payload, SecretKey key) throws Exception {

        byte[] payload = Base64.getDecoder().decode(base64Payload);


        // Extract the 12-byte IV from the payload

        ByteBuffer buffer = ByteBuffer.wrap(payload);

        byte[] iv = new byte[IV_LENGTH_BYTE];

        buffer.get(iv);


        byte[] cipherText = new byte[buffer.remaining()];

        buffer.get(cipherText);


        Cipher cipher = Cipher.getInstance(ALGORITHM);

        GCMParameterSpec spec = new GCMParameterSpec(TAG_LENGTH_BIT, iv);

        cipher.init(Cipher.DECRYPT_MODE, key, spec);


        byte[] plainTextBytes = cipher.doFinal(cipherText);

        return new String(plainTextBytes, StandardCharsets.UTF_8);

    }


    public static void main(String[] args) throws Exception {

        SecretKey secretKey = generateKey();

        String sensitiveData = "CONFIDENTIAL_PII_SSN_9812-44-1234";


        String encrypted = encrypt(sensitiveData, secretKey);

        System.out.println("Ciphertext Payload: " + encrypted);


        String decrypted = decrypt(encrypted, secretKey);

        System.out.println("Decrypted Result: " + decrypted);

    }

}


> Security Rule: Never reuse an Initialization Vector (IV) with the same key in GCM mode. Reusing an IV compromises the mathematical integrity of the cipher and allows an attacker to recover the plaintext.

> 

5. Password Storage: Hash, Salt, and Adaptive Cost Functions

A common mistake in application development is storing passwords using fast cryptographic hashing algorithms like SHA-256 or MD5.

Modern GPUs can compute tens of billions of SHA-256 hashes per second, making brute-force dictionary attacks trivial. Passwords must be hashed using slow, memory-hard, adaptive Key Derivation Functions (KDF) such as Argon2id, bcrypt, or PBKDF2.

Password ──► [ KDF Engine: PBKDF2 / Argon2 ] ──► Irreversible Slow Hash

                    ▲

                    │

Salt (Crypto-Random) ┘ (Prevents rainbow tables)


Hashing Passwords via PBKDF2 (Native JDK Implementation)

import javax.crypto.SecretKeyFactory;

import javax.crypto.spec.PBEKeySpec;

import java.security.NoSuchAlgorithmException;

import java.security.SecureRandom;

import java.security.spec.InvalidKeySpecException;

import java.util.Base64;


public class PasswordHasher {


    private static final String ALGORITHM = "PBKDF2WithHmacSHA256";

    private static final int ITERATIONS = 310_000; // Calibrated for high computational cost

    private static final int KEY_LENGTH = 256;

    private static final int SALT_LENGTH = 16;


    public static String hashPassword(char[] password) throws NoSuchAlgorithmException, InvalidKeySpecException {

        byte[] salt = new byte[SALT_LENGTH];

        new SecureRandom().nextBytes(salt);


        PBEKeySpec spec = new PBEKeySpec(password, salt, ITERATIONS, KEY_LENGTH);

        SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);

        byte[] hash = factory.generateSecret(spec).getEncoded();


        // Format: iterations:salt:hash

        return ITERATIONS + ":" + 

               Base64.getEncoder().encodeToString(salt) + ":" + 

               Base64.getEncoder().encodeToString(hash);

    }


    public static boolean verifyPassword(char[] password, String storedToken) 

            throws NoSuchAlgorithmException, InvalidKeySpecException {

        

        String[] parts = storedToken.split(":");

        int iterations = Integer.parseInt(parts[0]);

        byte[] salt = Base64.getDecoder().decode(parts[1]);

        byte[] storedHash = Base64.getDecoder().decode(parts[2]);


        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, storedHash.length * 8);

        SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM);

        byte[] candidateHash = factory.generateSecret(spec).getEncoded();


        // Constant-time comparison to prevent timing attacks!

        return MessageDigest.isEqual(storedHash, candidateHash);

    }

}


Why char[] Beats String for Passwords

Notice the method takes char[] password, not String password:

 * Strings are immutable and stored in the JVM String Constant Pool. They linger in heap memory until the Garbage Collector sweeps them, during which a memory dump could reveal plain-text credentials.

 * With a primitive char[], you can manually overwrite the array elements with zeros (Arrays.fill(password, '\0')) as soon as authentication finishes, wiping the data from memory immediately.

6. Defensive Coding: Mitigating the OWASP Top Ten

A. Preventing Timing Attacks

When comparing security credentials (tokens, signatures, hashes), standard String.equals() exits on the first mismatched character. An attacker measuring request response latency down to nanoseconds can deduce character by character what the secret token is.

Always use java.security.MessageDigest.isEqual(byte[] a, byte[] b), which runs in constant time regardless of where mismatches occur.

B. Defending Against Insecure Deserialization

As seen in Chapter 14, Java's native serialization (ObjectInputStream) allows remote code execution if untrusted byte streams are deserialized. If an attacker injects a malicious gadget chain payload, the JVM executes code before validation logic ever runs.

Remedies:

 * Prefer modern text-based data interchange formats like JSON or Protocol Buffers.

 * If native serialization is unavoidable, enforce a JVM Serialization Filter (java.io.ObjectInputFilter) to reject unauthorized classes at the boundary:

ObjectInputFilter strictFilter = ObjectInputFilter.Config.createFilter(

    "com.company.dto.*;java.base/*;!*" // Allow specific DTOs, reject everything else

);


C. XML External Entity (XXE) Prevention

When parsing XML documents, Java's default XML parsers allow remote external entity resolution, giving attackers access to the host file system:

DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();

// Disable DTDs and external entities entirely

dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);

dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);

dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);


7. Managing Secrets: The Java KeyStore (JKS / PKCS12)

Hardcoding database passwords or private API keys directly into source code repositories is a major security flaw. Sensitive keys, certificates, and TLS credentials should be stored in encrypted system vaults known as KeyStores (.p12 / PKCS#12 standard):

import java.io.FileInputStream;

import java.security.KeyStore;

import java.security.PrivateKey;

import java.security.cert.Certificate;


public class KeyStoreLoader {

    public static void main(String[] args) throws Exception {

        char[] keystorePassword = System.getenv("KEYSTORE_SECRET").toCharArray();

        KeyStore keyStore = KeyStore.getInstance("PKCS12");


        try (FileInputStream fis = new FileInputStream("application-identity.p12")) {

            keyStore.load(fis, keystorePassword);

        }


        // Retrieve private signing key securely

        KeyStore.ProtectionParameter protParam = new KeyStore.PasswordProtection(keystorePassword);

        KeyStore.PrivateKeyEntry keyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry("auth-signing-key", protParam);


        PrivateKey privateKey = keyEntry.getPrivateKey();

        System.out.println("Loaded Private Key Algorithm: " + privateKey.getAlgorithm());

    }

}


📝 Chapter Summary

 * Defense in Depth requires security considerations across every layer: input validation, memory sanitization, and cryptographic standards.

 * Use SecureRandom instead of Random for all tokens, salts, and nonces.

 * AES-GCM is the standard for symmetric encryption, combining confidentiality with built-in message authentication; never reuse an IV.

 * Store passwords using adaptive, computationally expensive algorithms like PBKDF2 or Argon2; handle sensitive inputs using char[] rather than String to zero-out sensitive memory after use.

 * Use MessageDigest.isEqual() to guard against timing analysis attacks.

 * Guard against deserialization exploits by using ObjectInputFilter or migrating to format boundaries like JSON.

 * Store operational secrets, TLS certificates, and private signing keys in a password-protected PKCS12 KeyStore.