Encryption and Decryption




1. What is Encryption?

Encryption is the process of converting readable data (plaintext) into an unreadable format (ciphertext) using a mathematical algorithm and a secret key. Only someone who has the correct key can reverse the process (decryption) and recover the original data.

Real-world analogy: Think of encryption like a lockbox. You put a letter (plaintext) inside a lockbox, close it with a key (encryption), and hand it to someone. Only the person with the matching key can open the box and read the letter (decryption). Anyone who intercepts the locked box sees nothing useful.

Encryption is critical in software development for several reasons:

  • Protecting sensitive data — passwords, credit card numbers, personal information, medical records
  • Securing communication — HTTPS, email encryption, messaging apps
  • Compliance — GDPR, HIPAA, PCI DSS all require data encryption
  • Data integrity — ensuring data has not been tampered with during transmission
  • Authentication — proving identity through digital signatures and certificates

There are two fundamental types of encryption:

Type Keys Used Speed Use Case Example Algorithms
Symmetric Encryption Same key for encrypt and decrypt Fast Encrypting large amounts of data AES, DES, ChaCha20
Asymmetric Encryption Public key encrypts, private key decrypts Slow Key exchange, digital signatures RSA, ECDSA, Ed25519

In practice, most systems use both together. For example, HTTPS uses asymmetric encryption to exchange a symmetric key, then uses that symmetric key to encrypt the actual data. This is called hybrid encryption.

Here is a simple visualization of the encryption process:

  • Encryption: Plaintext + Key + Algorithm = Ciphertext
  • Decryption: Ciphertext + Key + Algorithm = Plaintext

Throughout this tutorial, we will explore how Java provides all the tools you need to implement encryption, hashing, and message authentication in your applications.



2. Java Cryptography Architecture (JCA)

Java provides a comprehensive set of cryptographic APIs through the Java Cryptography Architecture (JCA) and the Java Cryptography Extension (JCE). These are part of the standard JDK, so you do not need any external libraries for core cryptographic operations.

Here are the key classes and packages you will use:

Class / Package Purpose Example Use
javax.crypto.Cipher Core encryption/decryption engine AES encryption, RSA encryption
javax.crypto.KeyGenerator Generate symmetric keys Generate AES-256 key
javax.crypto.SecretKey Represents a symmetric key AES key storage
javax.crypto.Mac Message Authentication Code HMAC-SHA256
java.security.KeyPairGenerator Generate public/private key pairs RSA key pair
java.security.MessageDigest Cryptographic hashing SHA-256 hash
java.security.SecureRandom Cryptographically secure random numbers Generating IVs, salts, tokens
java.security.Signature Digital signatures Sign and verify data
java.util.Base64 Encode/decode binary data as text Transmitting encrypted data as strings

The JCA uses a provider model. A provider is a package that implements cryptographic algorithms. The default provider is SunJCE, but you can plug in third-party providers like Bouncy Castle for additional algorithms. In most cases, the built-in providers are sufficient.

The general pattern for using JCA classes follows a consistent factory pattern:

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import java.security.MessageDigest;
import java.security.SecureRandom;

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

        // 1. Cipher -- the core encryption engine
        // Pattern: Cipher.getInstance("Algorithm/Mode/Padding")
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        System.out.println("Cipher algorithm: " + cipher.getAlgorithm());
        // Output: Cipher algorithm: AES/GCM/NoPadding

        // 2. KeyGenerator -- generates symmetric keys
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256); // 256-bit AES key
        System.out.println("KeyGenerator algorithm: " + keyGen.getAlgorithm());
        // Output: KeyGenerator algorithm: AES

        // 3. MessageDigest -- hashing
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        System.out.println("MessageDigest algorithm: " + digest.getAlgorithm());
        // Output: MessageDigest algorithm: SHA-256

        // 4. SecureRandom -- cryptographically strong random numbers
        SecureRandom random = new SecureRandom();
        byte[] randomBytes = new byte[16];
        random.nextBytes(randomBytes);
        System.out.println("Generated " + randomBytes.length + " random bytes");
        // Output: Generated 16 random bytes

        // 5. List available providers
        for (java.security.Provider provider : java.security.Security.getProviders()) {
            System.out.println("Provider: " + provider.getName() + " v" + provider.getVersion());
        }
        // Output: Provider: SUN v22.0, Provider: SunJCE v22.0, ...
    }
}



3. Hashing

Hashing is a one-way process that converts data of any size into a fixed-length string of characters. Unlike encryption, hashing is not reversible — you cannot recover the original data from a hash. The same input always produces the same hash (deterministic), but even a tiny change in input produces a completely different hash (avalanche effect).

Important: Hashing is NOT encryption. Encryption is reversible (with the key); hashing is not. They serve different purposes.

Algorithm Output Size Secure? Use Case
MD5 128 bits (32 hex chars) No — broken, collisions found Checksums only (never for security)
SHA-1 160 bits (40 hex chars) No — deprecated Legacy systems only
SHA-256 256 bits (64 hex chars) Yes Data integrity, file verification
SHA-512 512 bits (128 hex chars) Yes High security requirements

Java provides hashing through the MessageDigest class:

import java.security.MessageDigest;
import java.nio.charset.StandardCharsets;

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

        String data = "Hello, World!";

        // SHA-256 hash
        String sha256Hash = hash(data, "SHA-256");
        System.out.println("SHA-256: " + sha256Hash);
        // Output: SHA-256: dffd6021bb2bd5b0af676290809ec3a53191dd81c7f70a4b28688a362182986f

        // SHA-512 hash
        String sha512Hash = hash(data, "SHA-512");
        System.out.println("SHA-512: " + sha512Hash);
        // Output: SHA-512: 374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6cc69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387

        // MD5 (DO NOT use for security purposes)
        String md5Hash = hash(data, "MD5");
        System.out.println("MD5:    " + md5Hash);
        // Output: MD5:    65a8e27d8879283831b664bd8b7f0ad4

        // Demonstrate avalanche effect -- tiny change, completely different hash
        String data2 = "Hello, World?"; // changed ! to ?
        String hash2 = hash(data2, "SHA-256");
        System.out.println("\nOriginal:  " + sha256Hash);
        System.out.println("Modified:  " + hash2);
        // Completely different hashes despite one character difference
    }

    public static String hash(String data, String algorithm) throws Exception {
        MessageDigest digest = MessageDigest.getInstance(algorithm);
        byte[] hashBytes = digest.digest(data.getBytes(StandardCharsets.UTF_8));
        return bytesToHex(hashBytes);
    }

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

Hashing with Salt: When hashing passwords or sensitive data, you should always add a salt — a random value prepended to the input before hashing. This prevents attackers from using precomputed hash tables (rainbow tables) to crack your hashes.

import java.security.MessageDigest;
import java.security.SecureRandom;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

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

        String password = "mySecretPassword";

        // Generate a random salt (16 bytes = 128 bits)
        byte[] salt = generateSalt();
        System.out.println("Salt: " + Base64.getEncoder().encodeToString(salt));

        // Hash with salt
        String hashedPassword = hashWithSalt(password, salt);
        System.out.println("Hashed: " + hashedPassword);

        // Verify -- same password + same salt = same hash
        String verifyHash = hashWithSalt(password, salt);
        System.out.println("Verified: " + hashedPassword.equals(verifyHash));
        // Output: Verified: true

        // Different password = different hash
        String wrongHash = hashWithSalt("wrongPassword", salt);
        System.out.println("Wrong password match: " + hashedPassword.equals(wrongHash));
        // Output: Wrong password match: false
    }

    public static byte[] generateSalt() {
        byte[] salt = new byte[16];
        new SecureRandom().nextBytes(salt);
        return salt;
    }

    public static String hashWithSalt(String password, byte[] salt) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        digest.update(salt); // add salt first
        byte[] hashBytes = digest.digest(password.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hashBytes);
    }
}

Warning: Even salted SHA-256 is NOT sufficient for password hashing in production. SHA-256 is too fast — attackers can try billions of guesses per second. For passwords, use a dedicated password hashing algorithm like bcrypt, scrypt, or Argon2 (covered in Section 8).



4. Symmetric Encryption (AES)

Symmetric encryption uses the same key for both encryption and decryption. It is fast and efficient, making it the standard choice for encrypting data at rest and in transit.

AES (Advanced Encryption Standard) is the most widely used symmetric encryption algorithm. It was adopted by the U.S. government in 2001 and is considered secure with proper key sizes (128, 192, or 256 bits).

When using AES, you must choose a cipher mode and padding scheme. The mode determines how blocks of data are processed:

Mode Full Name IV Required? Authenticated? Recommendation
ECB Electronic Codebook No No NEVER use — identical blocks produce identical ciphertext, leaks patterns
CBC Cipher Block Chaining Yes No Acceptable but requires separate HMAC for authentication
CTR Counter Yes No Good for streaming, but no built-in authentication
GCM Galois/Counter Mode Yes Yes Recommended — provides both encryption AND authentication

Key Concepts:

  • IV (Initialization Vector) — a random value used to ensure the same plaintext does not produce the same ciphertext each time. The IV does not need to be secret, but it must be unique for each encryption with the same key.
  • Padding — block ciphers require input to be a multiple of the block size (128 bits for AES). Padding fills the gap. GCM mode does not need padding (use NoPadding).
  • Authentication Tag — GCM mode produces a tag that verifies the ciphertext has not been tampered with.

Let us start with AES key generation:

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

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

        // Generate a new AES-256 key
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256); // 128, 192, or 256 bits
        SecretKey secretKey = keyGen.generateKey();

        // Convert key to Base64 string (for storage)
        String encodedKey = Base64.getEncoder().encodeToString(secretKey.getEncoded());
        System.out.println("AES Key (Base64): " + encodedKey);
        System.out.println("Key length: " + secretKey.getEncoded().length + " bytes");
        // Output: Key length: 32 bytes (256 bits)

        // Reconstruct key from Base64 string
        byte[] decodedKey = Base64.getDecoder().decode(encodedKey);
        SecretKey reconstructedKey = new SecretKeySpec(decodedKey, "AES");
        System.out.println("Keys match: " + secretKey.equals(reconstructedKey));
        // Output: Keys match: true
    }
}

Now let us implement the recommended approach: AES-256-GCM. This mode provides both encryption and authentication (it detects if the ciphertext has been tampered with).

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.security.SecureRandom;
import java.util.Base64;

public class AESGCMEncryption {

    private static final int GCM_IV_LENGTH = 12;     // 96 bits (recommended for GCM)
    private static final int GCM_TAG_LENGTH = 128;    // 128 bits authentication tag

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

        // Generate AES-256 key
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256);
        SecretKey secretKey = keyGen.generateKey();

        String plaintext = "This is a secret message that needs to be encrypted.";
        System.out.println("Original:  " + plaintext);

        // Encrypt
        String encrypted = encrypt(plaintext, secretKey);
        System.out.println("Encrypted: " + encrypted);

        // Decrypt
        String decrypted = decrypt(encrypted, secretKey);
        System.out.println("Decrypted: " + decrypted);

        // Verify
        System.out.println("Match: " + plaintext.equals(decrypted));
        // Output: Match: true

        // Encrypting the same text again produces DIFFERENT ciphertext (because of random IV)
        String encrypted2 = encrypt(plaintext, secretKey);
        System.out.println("\nSame plaintext, different ciphertext:");
        System.out.println("Encryption 1: " + encrypted);
        System.out.println("Encryption 2: " + encrypted2);
        System.out.println("Same ciphertext? " + encrypted.equals(encrypted2));
        // Output: Same ciphertext? false (because IV is different each time)
    }

    public static String encrypt(String plaintext, SecretKey key) throws Exception {
        // Generate a random IV
        byte[] iv = new byte[GCM_IV_LENGTH];
        new SecureRandom().nextBytes(iv);

        // Initialize cipher for encryption
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        cipher.init(Cipher.ENCRYPT_MODE, key, gcmSpec);

        // Encrypt
        byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));

        // Prepend IV to ciphertext (IV is needed for decryption but is not secret)
        byte[] ivAndCiphertext = new byte[iv.length + ciphertext.length];
        System.arraycopy(iv, 0, ivAndCiphertext, 0, iv.length);
        System.arraycopy(ciphertext, 0, ivAndCiphertext, iv.length, ciphertext.length);

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

    public static String decrypt(String encryptedBase64, SecretKey key) throws Exception {
        byte[] ivAndCiphertext = Base64.getDecoder().decode(encryptedBase64);

        // Extract IV from the beginning
        byte[] iv = new byte[GCM_IV_LENGTH];
        System.arraycopy(ivAndCiphertext, 0, iv, 0, iv.length);

        // Extract ciphertext (rest of the bytes)
        byte[] ciphertext = new byte[ivAndCiphertext.length - iv.length];
        System.arraycopy(ivAndCiphertext, iv.length, ciphertext, 0, ciphertext.length);

        // Initialize cipher for decryption
        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
        cipher.init(Cipher.DECRYPT_MODE, key, gcmSpec);

        // Decrypt
        byte[] plaintext = cipher.doFinal(ciphertext);
        return new String(plaintext, "UTF-8");
    }
}

For reference, here is AES-CBC encryption (still commonly used in legacy systems). Note that CBC mode requires a separate HMAC step for authentication, which is why GCM is preferred:

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import java.security.SecureRandom;
import java.util.Base64;

public class AESCBCEncryption {

    private static final int IV_SIZE = 16; // 128 bits for AES block size

    public static String encrypt(String plaintext, SecretKey key) throws Exception {
        // Generate random IV
        byte[] iv = new byte[IV_SIZE];
        new SecureRandom().nextBytes(iv);

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

        // Encrypt
        byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));

        // Prepend IV to ciphertext
        byte[] result = new byte[iv.length + ciphertext.length];
        System.arraycopy(iv, 0, result, 0, iv.length);
        System.arraycopy(ciphertext, 0, result, iv.length, ciphertext.length);

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

    public static String decrypt(String encryptedBase64, SecretKey key) throws Exception {
        byte[] data = Base64.getDecoder().decode(encryptedBase64);

        // Extract IV
        byte[] iv = new byte[IV_SIZE];
        System.arraycopy(data, 0, iv, 0, iv.length);

        // Extract ciphertext
        byte[] ciphertext = new byte[data.length - iv.length];
        System.arraycopy(data, iv.length, ciphertext, 0, ciphertext.length);

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

        return new String(cipher.doFinal(ciphertext), "UTF-8");
    }

    public static void main(String[] args) throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256);
        SecretKey key = keyGen.generateKey();

        String message = "AES-CBC encryption example";
        String encrypted = encrypt(message, key);
        String decrypted = decrypt(encrypted, key);

        System.out.println("Original:  " + message);
        System.out.println("Encrypted: " + encrypted);
        System.out.println("Decrypted: " + decrypted);
        // Output:
        // Original:  AES-CBC encryption example
        // Encrypted: (Base64 string - different each time due to random IV)
        // Decrypted: AES-CBC encryption example
    }
}



5. Asymmetric Encryption (RSA)

Asymmetric encryption uses a pair of keys: a public key and a private key. Data encrypted with the public key can only be decrypted with the corresponding private key, and vice versa.

Real-world analogy: Think of a mailbox. Anyone can drop a letter into the mailbox (encrypt with the public key), but only the person with the mailbox key can open it and read the letters (decrypt with the private key).

RSA (Rivest-Shamir-Adleman) is the most widely used asymmetric algorithm. It is significantly slower than AES, so it is typically used for:

  • Key exchange — securely share a symmetric key over an insecure channel
  • Digital signatures — prove that data came from the expected sender and was not modified
  • Small data encryption — encrypt small pieces of data (RSA can only encrypt data smaller than the key size minus padding)
Feature Symmetric (AES) Asymmetric (RSA)
Keys 1 shared key 2 keys (public + private)
Speed Very fast ~100x slower
Key distribution Hard (must share secretly) Easy (public key is public)
Data size limit No limit Limited by key size
Use case Bulk data encryption Key exchange, signatures
import javax.crypto.Cipher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.Base64;

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

        // Generate RSA key pair (2048 bits minimum, 4096 for high security)
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2048);
        KeyPair keyPair = keyGen.generateKeyPair();

        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();

        System.out.println("Public key format: " + publicKey.getFormat());
        // Output: Public key format: X.509
        System.out.println("Private key format: " + privateKey.getFormat());
        // Output: Private key format: PKCS#8

        // Encrypt with public key
        String plaintext = "Secret message for RSA";
        String encrypted = encrypt(plaintext, publicKey);
        System.out.println("Encrypted: " + encrypted);

        // Decrypt with private key
        String decrypted = decrypt(encrypted, privateKey);
        System.out.println("Decrypted: " + decrypted);
        // Output: Decrypted: Secret message for RSA
    }

    public static String encrypt(String plaintext, PublicKey publicKey) throws Exception {
        // Use RSA with OAEP padding (recommended over PKCS1)
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.ENCRYPT_MODE, publicKey);
        byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));
        return Base64.getEncoder().encodeToString(ciphertext);
    }

    public static String decrypt(String encryptedBase64, PrivateKey privateKey) throws Exception {
        Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);
        byte[] plaintext = cipher.doFinal(Base64.getDecoder().decode(encryptedBase64));
        return new String(plaintext, "UTF-8");
    }
}

Digital Signatures

Digital signatures work in the opposite direction from encryption. You sign with your private key (proving you are the author) and anyone can verify with your public key (confirming the data was not modified).

import java.security.*;
import java.util.Base64;

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

        // Generate key pair
        KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
        keyGen.initialize(2048);
        KeyPair keyPair = keyGen.generateKeyPair();

        String message = "This document was signed by me.";

        // Sign with private key
        String signature = sign(message, keyPair.getPrivate());
        System.out.println("Signature: " + signature);

        // Verify with public key
        boolean isValid = verify(message, signature, keyPair.getPublic());
        System.out.println("Signature valid: " + isValid);
        // Output: Signature valid: true

        // Tamper with the message
        boolean isTamperedValid = verify("This document was NOT signed by me.", signature, keyPair.getPublic());
        System.out.println("Tampered message valid: " + isTamperedValid);
        // Output: Tampered message valid: false
    }

    public static String sign(String data, PrivateKey privateKey) throws Exception {
        Signature signer = Signature.getInstance("SHA256withRSA");
        signer.initSign(privateKey);
        signer.update(data.getBytes("UTF-8"));
        byte[] signatureBytes = signer.sign();
        return Base64.getEncoder().encodeToString(signatureBytes);
    }

    public static boolean verify(String data, String signatureBase64, PublicKey publicKey) throws Exception {
        Signature verifier = Signature.getInstance("SHA256withRSA");
        verifier.initVerify(publicKey);
        verifier.update(data.getBytes("UTF-8"));
        byte[] signatureBytes = Base64.getDecoder().decode(signatureBase64);
        return verifier.verify(signatureBytes);
    }
}



6. Base64 Encoding

Important: Base64 is NOT encryption. It is an encoding scheme that converts binary data into a text representation using 64 printable ASCII characters. Anyone can decode Base64 — there is no key, no secret, and no security. It is completely reversible by anyone.

Base64 is used when you need to transmit binary data (like encrypted bytes, images, or files) over channels that only support text (like JSON, XML, email, or URLs).

Java 8+ provides the java.util.Base64 class with three variants:

Encoder/Decoder Character Set Use Case
Base64.getEncoder() A-Z, a-z, 0-9, +, / General purpose (may include line breaks with MIME)
Base64.getUrlEncoder() A-Z, a-z, 0-9, -, _ URL-safe (no +, /, or =)
Base64.getMimeEncoder() Same as basic + line breaks Email attachments (MIME format)
import java.util.Base64;
import java.nio.charset.StandardCharsets;

public class Base64Examples {
    public static void main(String[] args) {

        String original = "Hello, Java Encryption!";

        // Basic encoding/decoding
        String encoded = Base64.getEncoder().encodeToString(original.getBytes(StandardCharsets.UTF_8));
        System.out.println("Encoded: " + encoded);
        // Output: Encoded: SGVsbG8sIEphdmEgRW5jcnlwdGlvbiE=

        String decoded = new String(Base64.getDecoder().decode(encoded), StandardCharsets.UTF_8);
        System.out.println("Decoded: " + decoded);
        // Output: Decoded: Hello, Java Encryption!

        // URL-safe encoding (replaces + with - and / with _)
        byte[] binaryData = {(byte) 0xFF, (byte) 0xFE, (byte) 0xFD, (byte) 0xFC};
        String urlSafe = Base64.getUrlEncoder().encodeToString(binaryData);
        System.out.println("URL-safe: " + urlSafe);
        // Output: URL-safe: __79_A==

        // URL-safe without padding (no trailing = signs)
        String urlSafeNoPadding = Base64.getUrlEncoder().withoutPadding().encodeToString(binaryData);
        System.out.println("URL-safe (no padding): " + urlSafeNoPadding);
        // Output: URL-safe (no padding): __79_A

        // Encoding binary data (like encrypted bytes) for storage as text
        byte[] encryptedBytes = new byte[]{0x12, 0x34, 0x56, 0x78, (byte) 0x9A, (byte) 0xBC};
        String storable = Base64.getEncoder().encodeToString(encryptedBytes);
        System.out.println("Binary as text: " + storable);
        // Output: Binary as text: EjRWeJq8

        // Decode back to original bytes
        byte[] restored = Base64.getDecoder().decode(storable);
        System.out.println("Bytes match: " + java.util.Arrays.equals(encryptedBytes, restored));
        // Output: Bytes match: true
    }
}



7. HMAC (Hash-based Message Authentication Code)

An HMAC is a specific type of message authentication code that combines a cryptographic hash function with a secret key. It provides two guarantees:

  • Data integrity — the data has not been modified in transit
  • Authenticity — the data came from someone who knows the secret key

Unlike a plain hash, an HMAC requires a secret key. This means an attacker cannot forge a valid HMAC without knowing the key. This is why HMACs are used in API authentication (like AWS request signing), JWT tokens, webhook verification, and session tokens.

Common HMAC algorithms:

Algorithm Output Size Use Case
HmacSHA256 256 bits Most common — API signing, JWTs, general purpose
HmacSHA384 384 bits Higher security requirement
HmacSHA512 512 bits Maximum security, slightly slower
HmacMD5 128 bits Do NOT use — MD5 is broken
import javax.crypto.Mac;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

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

        // Generate a key for HMAC
        KeyGenerator keyGen = KeyGenerator.getInstance("HmacSHA256");
        SecretKey hmacKey = keyGen.generateKey();

        String message = "Order #12345: $99.99 charged to card ending in 4242";

        // Generate HMAC
        String hmac = generateHMAC(message, hmacKey);
        System.out.println("Message: " + message);
        System.out.println("HMAC:    " + hmac);

        // Verify HMAC -- same message + same key = same HMAC
        String verifyHmac = generateHMAC(message, hmacKey);
        System.out.println("Valid: " + hmac.equals(verifyHmac));
        // Output: Valid: true

        // Tampered message produces different HMAC
        String tamperedMessage = "Order #12345: $9999.99 charged to card ending in 4242";
        String tamperedHmac = generateHMAC(tamperedMessage, hmacKey);
        System.out.println("Tampered valid: " + hmac.equals(tamperedHmac));
        // Output: Tampered valid: false

        // Using a string-based key (common for API integrations)
        String apiSecret = "my-api-secret-key-do-not-share";
        SecretKeySpec stringKey = new SecretKeySpec(
            apiSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"
        );
        String apiHmac = generateHMAC("webhook-payload-data", stringKey);
        System.out.println("API HMAC: " + apiHmac);
    }

    public static String generateHMAC(String data, SecretKey key) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(key);
        byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hmacBytes);
    }
}

Practical use case: Webhook verification. When services like Stripe or GitHub send webhooks, they include an HMAC signature in the header. You recompute the HMAC using your secret key and compare it to the received signature. If they match, the webhook is legitimate.

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;

public class WebhookVerification {

    /**
     * Verify a webhook signature.
     * The sender computes HMAC-SHA256 of the body using a shared secret
     * and sends it in the X-Signature header.
     */
    public static boolean verifyWebhook(String payload, String receivedSignature, String secret)
            throws Exception {

        SecretKeySpec key = new SecretKeySpec(
            secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"
        );

        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(key);
        byte[] computedBytes = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));

        // Convert to hex string for comparison
        StringBuilder hex = new StringBuilder();
        for (byte b : computedBytes) {
            hex.append(String.format("%02x", b));
        }
        String computedSignature = hex.toString();

        // Use constant-time comparison to prevent timing attacks
        return java.security.MessageDigest.isEqual(
            computedSignature.getBytes(),
            receivedSignature.getBytes()
        );
    }

    public static void main(String[] args) throws Exception {
        String webhookBody = "{\"event\":\"payment.completed\",\"amount\":9999}";
        String sharedSecret = "whsec_test_secret_key";

        // Simulate: sender computes signature
        SecretKeySpec key = new SecretKeySpec(
            sharedSecret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"
        );
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(key);
        byte[] sigBytes = mac.doFinal(webhookBody.getBytes(StandardCharsets.UTF_8));
        StringBuilder sigHex = new StringBuilder();
        for (byte b : sigBytes) {
            sigHex.append(String.format("%02x", b));
        }

        // Receiver verifies
        boolean valid = verifyWebhook(webhookBody, sigHex.toString(), sharedSecret);
        System.out.println("Webhook signature valid: " + valid);
        // Output: Webhook signature valid: true

        // Tampered payload fails
        boolean tampered = verifyWebhook(
            "{\"event\":\"payment.completed\",\"amount\":0}",
            sigHex.toString(), sharedSecret
        );
        System.out.println("Tampered webhook valid: " + tampered);
        // Output: Tampered webhook valid: false
    }
}



8. Password Hashing

Password hashing is a special case that requires algorithms specifically designed to be slow. This might sound counterintuitive, but here is why:

  • SHA-256 can hash billions of values per second on modern GPUs
  • An attacker with a database of SHA-256 password hashes can try every common password in seconds
  • Dedicated password hashing algorithms (bcrypt, scrypt, Argon2) are intentionally slow and memory-intensive
  • They include a work factor (cost parameter) that can be increased as hardware gets faster
Approach Hashes/Second (GPU) Suitable for Passwords?
MD5 ~100 billion Absolutely not
SHA-256 ~10 billion No — too fast
SHA-256 + salt ~10 billion No — still too fast
bcrypt ~10,000 (tunable) Yes — recommended
scrypt ~1,000 (tunable) Yes — memory-hard
Argon2 ~100 (tunable) Yes — winner of PHC, best choice

bcrypt is the most widely used in Java applications. It automatically handles salt generation and embeds the salt in the output, so you do not need to manage salts separately.

Here is a bcrypt implementation using the jBCrypt library (or Spring Security’s BCryptPasswordEncoder):

// Using Spring Security's BCryptPasswordEncoder (most common in Java projects)
// Add dependency: org.springframework.security:spring-security-crypto

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class BCryptPasswordExample {
    public static void main(String[] args) {

        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12); // work factor = 12

        String rawPassword = "MySecureP@ssw0rd!";

        // Hash the password (salt is generated automatically)
        String hashedPassword = encoder.encode(rawPassword);
        System.out.println("Hashed: " + hashedPassword);
        // Output: Hashed: $2a$12$LJ/3Xq... (60 characters, different each time)

        // Hashing the same password again gives a DIFFERENT hash (different salt)
        String hashedPassword2 = encoder.encode(rawPassword);
        System.out.println("Hashed again: " + hashedPassword2);
        System.out.println("Same hash? " + hashedPassword.equals(hashedPassword2));
        // Output: Same hash? false (because salt is different)

        // Verify password -- this is how you check login credentials
        boolean matches = encoder.matches(rawPassword, hashedPassword);
        System.out.println("Correct password: " + matches);
        // Output: Correct password: true

        boolean wrongMatch = encoder.matches("WrongPassword", hashedPassword);
        System.out.println("Wrong password: " + wrongMatch);
        // Output: Wrong password: false
    }
}

If you are not using Spring Security, here is a pure Java approach using PBKDF2 (Password-Based Key Derivation Function 2), which is built into the JDK:

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

public class PBKDF2PasswordHashing {

    private static final int ITERATIONS = 210_000;  // OWASP recommendation (2023+)
    private static final int KEY_LENGTH = 256;       // bits
    private static final int SALT_LENGTH = 16;       // bytes

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

        String password = "MySecureP@ssw0rd!";

        // Hash the password
        String stored = hashPassword(password);
        System.out.println("Stored: " + stored);
        // Output: Stored: 210000:BASE64_SALT:BASE64_HASH

        // Verify
        boolean valid = verifyPassword(password, stored);
        System.out.println("Valid: " + valid);
        // Output: Valid: true

        boolean invalid = verifyPassword("WrongPassword", stored);
        System.out.println("Wrong password: " + invalid);
        // Output: Wrong password: false
    }

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

        // Hash with PBKDF2
        PBEKeySpec spec = new PBEKeySpec(
            password.toCharArray(), salt, ITERATIONS, KEY_LENGTH
        );
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        byte[] hash = factory.generateSecret(spec).getEncoded();

        // Store iterations:salt:hash (all needed to verify later)
        return ITERATIONS + ":"
            + Base64.getEncoder().encodeToString(salt) + ":"
            + Base64.getEncoder().encodeToString(hash);
    }

    public static boolean verifyPassword(String password, String stored) throws Exception {
        String[] parts = stored.split(":");
        int iterations = Integer.parseInt(parts[0]);
        byte[] salt = Base64.getDecoder().decode(parts[1]);
        byte[] expectedHash = Base64.getDecoder().decode(parts[2]);

        // Hash the input password with the same salt and iterations
        PBEKeySpec spec = new PBEKeySpec(
            password.toCharArray(), salt, iterations, expectedHash.length * 8
        );
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        byte[] actualHash = factory.generateSecret(spec).getEncoded();

        // Constant-time comparison to prevent timing attacks
        return java.security.MessageDigest.isEqual(expectedHash, actualHash);
    }
}



9. Practical Examples

9.1 File Encryption and Decryption

Encrypting files follows the same AES pattern but processes data in streams to handle large files without loading everything into memory:

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.CipherOutputStream;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import java.io.*;
import java.security.SecureRandom;

public class FileEncryption {

    private static final int GCM_IV_LENGTH = 12;
    private static final int GCM_TAG_LENGTH = 128;

    public static void encryptFile(File inputFile, File outputFile, SecretKey key)
            throws Exception {

        byte[] iv = new byte[GCM_IV_LENGTH];
        new SecureRandom().nextBytes(iv);

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

        try (FileOutputStream fos = new FileOutputStream(outputFile)) {
            // Write IV first (needed for decryption)
            fos.write(iv);

            try (FileInputStream fis = new FileInputStream(inputFile);
                 CipherOutputStream cos = new CipherOutputStream(fos, cipher)) {

                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = fis.read(buffer)) != -1) {
                    cos.write(buffer, 0, bytesRead);
                }
            }
        }
    }

    public static void decryptFile(File inputFile, File outputFile, SecretKey key)
            throws Exception {

        try (FileInputStream fis = new FileInputStream(inputFile)) {
            // Read IV
            byte[] iv = new byte[GCM_IV_LENGTH];
            fis.read(iv);

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

            try (CipherInputStream cis = new CipherInputStream(fis, cipher);
                 FileOutputStream fos = new FileOutputStream(outputFile)) {

                byte[] buffer = new byte[8192];
                int bytesRead;
                while ((bytesRead = cis.read(buffer)) != -1) {
                    fos.write(buffer, 0, bytesRead);
                }
            }
        }
    }

    public static void main(String[] args) throws Exception {
        // Generate key
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256);
        SecretKey key = keyGen.generateKey();

        // Create a test file
        File original = new File("secret_document.txt");
        try (FileWriter fw = new FileWriter(original)) {
            fw.write("This is my secret document with sensitive information.");
        }

        // Encrypt
        File encrypted = new File("secret_document.enc");
        encryptFile(original, encrypted, key);
        System.out.println("File encrypted: " + encrypted.getName());
        System.out.println("Original size: " + original.length() + " bytes");
        System.out.println("Encrypted size: " + encrypted.length() + " bytes");

        // Decrypt
        File decrypted = new File("secret_document_decrypted.txt");
        decryptFile(encrypted, decrypted, key);
        System.out.println("File decrypted: " + decrypted.getName());

        // Verify
        try (BufferedReader br = new BufferedReader(new FileReader(decrypted))) {
            System.out.println("Content: " + br.readLine());
        }
        // Output: Content: This is my secret document with sensitive information.

        // Cleanup
        original.delete();
        encrypted.delete();
        decrypted.delete();
    }
}

9.2 Secure Token Generation

Generating cryptographically secure tokens for session IDs, API keys, password reset links, and verification codes:

import java.security.SecureRandom;
import java.util.Base64;

public class SecureTokenGenerator {

    private static final SecureRandom secureRandom = new SecureRandom();

    /**
     * Generate a URL-safe token (for password reset links, API keys, etc.)
     */
    public static String generateToken(int byteLength) {
        byte[] tokenBytes = new byte[byteLength];
        secureRandom.nextBytes(tokenBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes);
    }

    /**
     * Generate a hex token (for session IDs, CSRF tokens)
     */
    public static String generateHexToken(int byteLength) {
        byte[] tokenBytes = new byte[byteLength];
        secureRandom.nextBytes(tokenBytes);
        StringBuilder hex = new StringBuilder();
        for (byte b : tokenBytes) {
            hex.append(String.format("%02x", b));
        }
        return hex.toString();
    }

    /**
     * Generate a numeric OTP (for SMS verification, 2FA)
     */
    public static String generateOTP(int digits) {
        int max = (int) Math.pow(10, digits);
        int otp = secureRandom.nextInt(max);
        return String.format("%0" + digits + "d", otp);
    }

    public static void main(String[] args) {
        // API key (32 bytes = 256 bits of entropy)
        System.out.println("API Key:     " + generateToken(32));
        // Output: API Key:     xK9mPq... (43 characters, URL-safe)

        // Session ID (16 bytes = 128 bits)
        System.out.println("Session ID:  " + generateHexToken(16));
        // Output: Session ID:  a1b2c3d4e5f6... (32 hex characters)

        // Password reset token (24 bytes = 192 bits)
        System.out.println("Reset Token: " + generateToken(24));
        // Output: Reset Token:  Mj7kL2... (32 characters, URL-safe)

        // 6-digit OTP
        System.out.println("OTP:         " + generateOTP(6));
        // Output: OTP:         384729

        // NEVER use java.util.Random for security tokens!
        // Random is predictable. SecureRandom uses OS entropy sources.
    }
}

9.3 Encrypting Sensitive Configuration

A common need is encrypting sensitive values in configuration files (database passwords, API keys) so they are not stored in plaintext:

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;

public class ConfigEncryptor {

    private static final int GCM_IV_LENGTH = 12;
    private static final int GCM_TAG_LENGTH = 128;
    private static final String PREFIX = "ENC(";
    private static final String SUFFIX = ")";

    private final SecretKey key;

    public ConfigEncryptor(String base64Key) {
        byte[] keyBytes = Base64.getDecoder().decode(base64Key);
        this.key = new SecretKeySpec(keyBytes, "AES");
    }

    public String encryptValue(String plaintext) throws Exception {
        byte[] iv = new byte[GCM_IV_LENGTH];
        new SecureRandom().nextBytes(iv);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
        byte[] ciphertext = cipher.doFinal(plaintext.getBytes("UTF-8"));

        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 PREFIX + Base64.getEncoder().encodeToString(combined) + SUFFIX;
    }

    public String decryptValue(String encryptedValue) throws Exception {
        // Strip ENC(...) wrapper
        String base64 = encryptedValue.substring(PREFIX.length(), encryptedValue.length() - SUFFIX.length());
        byte[] combined = Base64.getDecoder().decode(base64);

        byte[] iv = new byte[GCM_IV_LENGTH];
        System.arraycopy(combined, 0, iv, 0, iv.length);
        byte[] ciphertext = new byte[combined.length - 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(GCM_TAG_LENGTH, iv));
        return new String(cipher.doFinal(ciphertext), "UTF-8");
    }

    public boolean isEncrypted(String value) {
        return value.startsWith(PREFIX) && value.endsWith(SUFFIX);
    }

    public static void main(String[] args) throws Exception {
        // In production, this key comes from environment variable or key vault
        // Generate once: Base64.getEncoder().encodeToString(KeyGenerator...generateKey().getEncoded())
        String masterKey = "k7Hn3pRqSvWxYz1A2bCdEfGhIjKlMnOp4qRsTuVw5X8=";

        ConfigEncryptor encryptor = new ConfigEncryptor(masterKey);

        // Encrypt sensitive config values
        Map config = new HashMap<>();
        config.put("db.url", "jdbc:mysql://localhost:3306/mydb");
        config.put("db.password", encryptor.encryptValue("SuperSecret123!"));
        config.put("api.key", encryptor.encryptValue("sk-abc123def456"));

        // Print config (safe to commit to version control)
        System.out.println("=== Encrypted Config ===");
        for (Map.Entry entry : config.entrySet()) {
            System.out.println(entry.getKey() + " = " + entry.getValue());
        }
        // Output:
        // db.url = jdbc:mysql://localhost:3306/mydb
        // db.password = ENC(base64encodeddata...)
        // api.key = ENC(base64encodeddata...)

        // At runtime, decrypt the values
        System.out.println("\n=== Decrypted at Runtime ===");
        for (Map.Entry entry : config.entrySet()) {
            String value = entry.getValue();
            if (encryptor.isEncrypted(value)) {
                value = encryptor.decryptValue(value);
            }
            System.out.println(entry.getKey() + " = " + value);
        }
        // Output:
        // db.url = jdbc:mysql://localhost:3306/mydb
        // db.password = SuperSecret123!
        // api.key = sk-abc123def456
    }
}



10. Common Mistakes

Cryptography is one of the easiest areas to get wrong in software development. A small mistake can make your encryption completely useless. Here are the most common mistakes Java developers make:

Mistake 1: Using ECB Mode

ECB (Electronic Codebook) mode encrypts each block independently. This means identical plaintext blocks produce identical ciphertext blocks, which leaks patterns in your data.

// WRONG -- ECB mode leaks patterns
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding"); // NEVER DO THIS
cipher.init(Cipher.ENCRYPT_MODE, key);

// Encrypting "AAAAAAAAAAAAAAAA" twice produces the SAME ciphertext
// An attacker can see which blocks are identical

// RIGHT -- Use GCM mode (provides encryption + authentication)
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(128, iv));

Mistake 2: Hardcoding Encryption Keys

// WRONG -- key is visible in source code and version control
private static final String SECRET_KEY = "MyHardcodedKey123";

// WRONG -- key is in a config file committed to Git
// application.properties: encryption.key=MyHardcodedKey123

// RIGHT -- key comes from environment variable
String key = System.getenv("ENCRYPTION_KEY");
if (key == null) {
    throw new IllegalStateException("ENCRYPTION_KEY environment variable not set");
}

// RIGHT -- key comes from a secrets manager (AWS Secrets Manager, HashiCorp Vault)
// String key = secretsManager.getSecret("encryption-key");

Mistake 3: Using Random Instead of SecureRandom

import java.util.Random;
import java.security.SecureRandom;

// WRONG -- java.util.Random is predictable (uses a linear congruential generator)
// If an attacker knows the seed, they can predict ALL future values
Random insecureRandom = new Random();
byte[] iv = new byte[16];
insecureRandom.nextBytes(iv); // PREDICTABLE IV -- defeats the purpose of encryption

// RIGHT -- SecureRandom uses OS entropy (e.g., /dev/urandom on Linux)
SecureRandom secureRandom = new SecureRandom();
byte[] safeIv = new byte[16];
secureRandom.nextBytes(safeIv); // Cryptographically unpredictable

Mistake 4: Using MD5 or SHA-1 for Security

// WRONG -- MD5 has known collision attacks since 2004
MessageDigest md5 = MessageDigest.getInstance("MD5");
byte[] hash = md5.digest(data.getBytes()); // Two different inputs can produce the same hash

// WRONG -- SHA-1 has been broken since 2017 (Google's SHAttered attack)
MessageDigest sha1 = MessageDigest.getInstance("SHA-1");

// RIGHT -- Use SHA-256 or SHA-512
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");

// For passwords, do NOT use any SHA variant -- use bcrypt/scrypt/Argon2

Mistake 5: Not Using Authenticated Encryption

// WRONG -- AES-CBC without HMAC. An attacker can modify the ciphertext
// and you will decrypt garbage without knowing it was tampered with.
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
// ... encrypt ...
// No way to detect if ciphertext was modified!

// RIGHT -- AES-GCM provides authenticated encryption.
// If the ciphertext is modified, decryption throws AEADBadTagException.
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
// ... encrypt ...
// Decryption automatically verifies integrity via the authentication tag

Mistake 6: Reusing IVs with the Same Key

// WRONG -- Using a fixed IV
private static final byte[] FIXED_IV = new byte[12]; // all zeros
// Same key + same IV + different plaintexts = complete security failure in GCM mode

// WRONG -- Using a counter without proper management
int counter = 0;
byte[] iv = ByteBuffer.allocate(12).putInt(counter++).array();
// If the counter resets (app restart), you reuse IVs!

// RIGHT -- Always generate a random IV for each encryption
byte[] iv = new byte[12];
new SecureRandom().nextBytes(iv); // Fresh random IV every time

Mistake 7: Rolling Your Own Crypto

// WRONG -- Inventing your own "encryption" algorithm
public static String myEncrypt(String text) {
    StringBuilder result = new StringBuilder();
    for (char c : text.toCharArray()) {
        result.append((char) (c + 3)); // Caesar cipher -- trivially broken
    }
    return result.toString();
}

// WRONG -- XOR "encryption" with a repeated key
public static byte[] xorEncrypt(byte[] data, byte[] key) {
    byte[] result = new byte[data.length];
    for (int i = 0; i < data.length; i++) {
        result[i] = (byte) (data[i] ^ key[i % key.length]); // trivially broken
    }
    return result;
}

// RIGHT -- Use standard algorithms that have been analyzed by cryptographers
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
// AES has been analyzed by thousands of cryptographers since 1998
// and is approved for TOP SECRET government data



11. Best Practices

Here is a summary of what you should and should not do when implementing cryptography in Java:

Category Do Do Not
Symmetric Encryption Use AES-256-GCM Use ECB mode, DES, 3DES, or Blowfish
Asymmetric Encryption Use RSA-OAEP (2048+ bits) or ECDSA Use RSA-PKCS1v1.5 or RSA less than 2048 bits
Password Hashing Use bcrypt, scrypt, or Argon2 Use SHA-256, MD5, or any fast hash for passwords
General Hashing Use SHA-256 or SHA-512 Use MD5 or SHA-1 for security purposes
Random Numbers Use SecureRandom Use java.util.Random for crypto
Key Storage Use env vars, secrets manager, or key vault Hardcode keys in source code or config files
IV / Nonce Generate a fresh random IV for each encryption Reuse IVs or use fixed/predictable IVs
Authentication Use authenticated encryption (GCM) or HMAC Encrypt without verifying integrity
Algorithm Design Use standard, well-tested algorithms Invent your own encryption scheme
Comparison Use constant-time comparison for MACs/hashes Use String.equals() for crypto comparisons
Key Size AES-256, RSA-2048+, HMAC-SHA256 AES-128 (acceptable), RSA-1024 (broken)
Dependencies Keep crypto libraries updated Use deprecated or unmaintained crypto libraries

Additional guidance:

  • Never log plaintext secrets. Log the fact that encryption/decryption happened, but never log the actual passwords, keys, or sensitive data.
  • Wipe sensitive data from memory when done. Use char[] instead of String for passwords (strings are immutable and linger in memory). Call Arrays.fill(charArray, '\0') when done.
  • Use try-with-resources or finally blocks to ensure keys and sensitive byte arrays are zeroed out.
  • Rotate keys periodically. Have a plan for key rotation without downtime -- encrypt new data with new keys, re-encrypt old data during migration.
  • Test your crypto. Write unit tests that verify encryption round-trips correctly, that different plaintexts produce different ciphertexts, and that tampered ciphertext is rejected.



12. Complete Practical Example -- SecureVault

Let us bring everything together in a practical SecureVault class that provides the four most common cryptographic operations: encrypt/decrypt strings, hash passwords, generate secure tokens, and verify data integrity with HMAC.

This class is designed to be production-quality. You can drop it into your project and use it immediately.

import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;

/**
 * SecureVault -- a utility class for common cryptographic operations.
 *
 * Features:
 * - AES-256-GCM encryption/decryption (authenticated encryption)
 * - PBKDF2 password hashing with salt
 * - HMAC-SHA256 message authentication
 * - Cryptographically secure token generation
 * - SHA-256 hashing for data integrity
 */
public class SecureVault {

    private static final int GCM_IV_LENGTH = 12;       // 96 bits
    private static final int GCM_TAG_LENGTH = 128;      // 128 bits
    private static final int PBKDF2_ITERATIONS = 210_000;
    private static final int PBKDF2_KEY_LENGTH = 256;   // bits
    private static final int SALT_LENGTH = 16;           // bytes

    private final SecureRandom secureRandom = new SecureRandom();

    // ===== ENCRYPTION / DECRYPTION (AES-256-GCM) =====

    /**
     * Generate a new AES-256 encryption key.
     */
    public SecretKey generateEncryptionKey() throws Exception {
        KeyGenerator keyGen = KeyGenerator.getInstance("AES");
        keyGen.init(256);
        return keyGen.generateKey();
    }

    /**
     * Reconstruct a key from its Base64-encoded form.
     */
    public SecretKey keyFromBase64(String base64Key) {
        byte[] keyBytes = Base64.getDecoder().decode(base64Key);
        return new SecretKeySpec(keyBytes, "AES");
    }

    /**
     * Export a key as a Base64 string (for secure storage).
     */
    public String keyToBase64(SecretKey key) {
        return Base64.getEncoder().encodeToString(key.getEncoded());
    }

    /**
     * Encrypt a string using AES-256-GCM.
     * The IV is prepended to the ciphertext in the output.
     */
    public String encrypt(String plaintext, SecretKey key) throws Exception {
        byte[] iv = new byte[GCM_IV_LENGTH];
        secureRandom.nextBytes(iv);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
        byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));

        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);
    }

    /**
     * Decrypt a string that was encrypted with encrypt().
     * Throws AEADBadTagException if the ciphertext was tampered with.
     */
    public String decrypt(String encryptedBase64, SecretKey key) throws Exception {
        byte[] combined = Base64.getDecoder().decode(encryptedBase64);

        byte[] iv = Arrays.copyOfRange(combined, 0, GCM_IV_LENGTH);
        byte[] ciphertext = Arrays.copyOfRange(combined, GCM_IV_LENGTH, combined.length);

        Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
        cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
        byte[] plaintext = cipher.doFinal(ciphertext);

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

    // ===== PASSWORD HASHING (PBKDF2) =====

    /**
     * Hash a password using PBKDF2-HMAC-SHA256.
     * Returns: "iterations:base64Salt:base64Hash"
     */
    public String hashPassword(String password) throws Exception {
        byte[] salt = new byte[SALT_LENGTH];
        secureRandom.nextBytes(salt);

        byte[] hash = pbkdf2(password.toCharArray(), salt, PBKDF2_ITERATIONS, PBKDF2_KEY_LENGTH);

        return PBKDF2_ITERATIONS + ":"
            + Base64.getEncoder().encodeToString(salt) + ":"
            + Base64.getEncoder().encodeToString(hash);
    }

    /**
     * Verify a password against a stored hash.
     */
    public boolean verifyPassword(String password, String storedHash) throws Exception {
        String[] parts = storedHash.split(":");
        int iterations = Integer.parseInt(parts[0]);
        byte[] salt = Base64.getDecoder().decode(parts[1]);
        byte[] expectedHash = Base64.getDecoder().decode(parts[2]);

        byte[] actualHash = pbkdf2(password.toCharArray(), salt, iterations, expectedHash.length * 8);

        return MessageDigest.isEqual(expectedHash, actualHash);
    }

    private byte[] pbkdf2(char[] password, byte[] salt, int iterations, int keyLength)
            throws Exception {
        PBEKeySpec spec = new PBEKeySpec(password, salt, iterations, keyLength);
        SecretKeyFactory factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
        return factory.generateSecret(spec).getEncoded();
    }

    // ===== HMAC (Message Authentication) =====

    /**
     * Generate an HMAC-SHA256 for data integrity verification.
     */
    public String hmac(String data, SecretKey key) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(key);
        byte[] hmacBytes = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hmacBytes);
    }

    /**
     * Verify an HMAC using constant-time comparison.
     */
    public boolean verifyHmac(String data, String expectedHmac, SecretKey key) throws Exception {
        String actualHmac = hmac(data, key);
        return MessageDigest.isEqual(
            actualHmac.getBytes(StandardCharsets.UTF_8),
            expectedHmac.getBytes(StandardCharsets.UTF_8)
        );
    }

    // ===== HASHING (SHA-256) =====

    /**
     * Hash data using SHA-256 (for data integrity, NOT passwords).
     */
    public String sha256(String data) throws Exception {
        MessageDigest digest = MessageDigest.getInstance("SHA-256");
        byte[] hashBytes = digest.digest(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hashBytes);
    }

    // ===== TOKEN GENERATION =====

    /**
     * Generate a URL-safe random token.
     */
    public String generateToken(int byteLength) {
        byte[] tokenBytes = new byte[byteLength];
        secureRandom.nextBytes(tokenBytes);
        return Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes);
    }

    /**
     * Generate a numeric OTP.
     */
    public String generateOTP(int digits) {
        int max = (int) Math.pow(10, digits);
        int otp = secureRandom.nextInt(max);
        return String.format("%0" + digits + "d", otp);
    }
}

Now let us use SecureVault in a complete application that demonstrates every feature:

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;

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

        SecureVault vault = new SecureVault();

        System.out.println("===== SecureVault Demo =====\n");

        // ---- 1. Encryption / Decryption ----
        System.out.println("--- 1. AES-256-GCM Encryption ---");
        SecretKey encKey = vault.generateEncryptionKey();
        String keyBase64 = vault.keyToBase64(encKey);
        System.out.println("Key (Base64): " + keyBase64);

        String secret = "My credit card number is 4111-1111-1111-1111";
        String encrypted = vault.encrypt(secret, encKey);
        System.out.println("Plaintext:  " + secret);
        System.out.println("Encrypted:  " + encrypted);

        String decrypted = vault.decrypt(encrypted, encKey);
        System.out.println("Decrypted:  " + decrypted);
        System.out.println("Match: " + secret.equals(decrypted));

        // Same plaintext, different ciphertext (random IV)
        String encrypted2 = vault.encrypt(secret, encKey);
        System.out.println("Same ciphertext? " + encrypted.equals(encrypted2));
        // Output: Same ciphertext? false

        // Reconstruct key from Base64
        SecretKey restoredKey = vault.keyFromBase64(keyBase64);
        String decrypted2 = vault.decrypt(encrypted, restoredKey);
        System.out.println("Decrypted with restored key: " + decrypted2);

        // ---- 2. Password Hashing ----
        System.out.println("\n--- 2. Password Hashing (PBKDF2) ---");
        String password = "S3cur3P@ssw0rd!";
        String hashedPassword = vault.hashPassword(password);
        System.out.println("Password: " + password);
        System.out.println("Hashed:   " + hashedPassword);

        boolean correctPassword = vault.verifyPassword(password, hashedPassword);
        System.out.println("Correct password: " + correctPassword);
        // Output: Correct password: true

        boolean wrongPassword = vault.verifyPassword("wrongpassword", hashedPassword);
        System.out.println("Wrong password: " + wrongPassword);
        // Output: Wrong password: false

        // ---- 3. HMAC ----
        System.out.println("\n--- 3. HMAC-SHA256 ---");
        SecretKey hmacKey = vault.generateEncryptionKey(); // can reuse AES key for HMAC
        String apiPayload = "{\"orderId\":\"12345\",\"amount\":99.99}";
        String signature = vault.hmac(apiPayload, hmacKey);
        System.out.println("Payload:   " + apiPayload);
        System.out.println("HMAC:      " + signature);

        boolean validHmac = vault.verifyHmac(apiPayload, signature, hmacKey);
        System.out.println("Valid HMAC: " + validHmac);
        // Output: Valid HMAC: true

        String tamperedPayload = "{\"orderId\":\"12345\",\"amount\":0.01}";
        boolean tamperedHmac = vault.verifyHmac(tamperedPayload, signature, hmacKey);
        System.out.println("Tampered HMAC valid: " + tamperedHmac);
        // Output: Tampered HMAC valid: false

        // ---- 4. SHA-256 Hashing ----
        System.out.println("\n--- 4. SHA-256 Hashing ---");
        String fileContent = "Important document content";
        String hash = vault.sha256(fileContent);
        System.out.println("Content: " + fileContent);
        System.out.println("SHA-256: " + hash);

        // Verify integrity
        String hash2 = vault.sha256(fileContent);
        System.out.println("Integrity check: " + hash.equals(hash2));
        // Output: Integrity check: true

        // ---- 5. Token Generation ----
        System.out.println("\n--- 5. Secure Token Generation ---");
        System.out.println("API Key (32 bytes):     " + vault.generateToken(32));
        System.out.println("Session Token (16 bytes): " + vault.generateToken(16));
        System.out.println("Reset Token (24 bytes):  " + vault.generateToken(24));
        System.out.println("6-digit OTP:            " + vault.generateOTP(6));
        System.out.println("4-digit OTP:            " + vault.generateOTP(4));

        // ---- 6. Tamper Detection ----
        System.out.println("\n--- 6. Tamper Detection (GCM) ---");
        String sensitiveData = "Transfer $10,000 to account 98765";
        String encryptedData = vault.encrypt(sensitiveData, encKey);
        System.out.println("Encrypted: " + encryptedData);

        try {
            // Simulate tampering: modify one character in the Base64 string
            char[] chars = encryptedData.toCharArray();
            chars[20] = (chars[20] == 'A') ? 'B' : 'A'; // flip a character
            String tampered = new String(chars);
            vault.decrypt(tampered, encKey); // This should throw an exception
            System.out.println("ERROR: Tampered data was accepted!");
        } catch (Exception e) {
            System.out.println("Tamper detected: " + e.getClass().getSimpleName());
            // Output: Tamper detected: AEADBadTagException
        }

        System.out.println("\n===== Demo Complete =====");
    }
}

The output of the complete demo:

===== SecureVault Demo =====

--- 1. AES-256-GCM Encryption ---
Key (Base64): kR7xN9pQ2sVwYz1A3bCdEfGhIjKlMnOp4qRsTuVw5X8=
Plaintext:  My credit card number is 4111-1111-1111-1111
Encrypted:  (Base64 encoded ciphertext -- different each time)
Decrypted:  My credit card number is 4111-1111-1111-1111
Match: true
Same ciphertext? false
Decrypted with restored key: My credit card number is 4111-1111-1111-1111

--- 2. Password Hashing (PBKDF2) ---
Password: S3cur3P@ssw0rd!
Hashed:   210000:BASE64_SALT:BASE64_HASH
Correct password: true
Wrong password: false

--- 3. HMAC-SHA256 ---
Payload:   {"orderId":"12345","amount":99.99}
HMAC:      (Base64 encoded HMAC)
Valid HMAC: true
Tampered HMAC valid: false

--- 4. SHA-256 Hashing ---
Content: Important document content
SHA-256: (Base64 encoded hash)
Integrity check: true

--- 5. Secure Token Generation ---
API Key (32 bytes):      xK9mPqR7sVwYz1A3bCdEfGhIjKlMnOp4qRsTuVw5X8
Session Token (16 bytes): a1B2c3D4e5F6g7H8i9J0kL
Reset Token (24 bytes):  Mj7kL2nP9qR3sT5uV8wX0yZ1a2B3c4D
6-digit OTP:             384729
4-digit OTP:             9152

--- 6. Tamper Detection (GCM) ---
Encrypted: (Base64 encoded ciphertext)
Tamper detected: AEADBadTagException

===== Demo Complete =====

Here is a summary of the concepts covered in this tutorial and where each appears in the SecureVault example:

# Concept Algorithm / Class Where Used
1 Symmetric Encryption AES-256-GCM encrypt(), decrypt()
2 Key Generation KeyGenerator generateEncryptionKey()
3 Key Serialization Base64, SecretKeySpec keyToBase64(), keyFromBase64()
4 Initialization Vector SecureRandom, GCMParameterSpec Random IV in encrypt()
5 Authenticated Encryption GCM authentication tag Tamper detection in demo section 6
6 Password Hashing PBKDF2-HMAC-SHA256 hashPassword(), verifyPassword()
7 Salt SecureRandom Random salt in hashPassword()
8 HMAC HmacSHA256, Mac hmac(), verifyHmac()
9 Constant-Time Comparison MessageDigest.isEqual() verifyPassword(), verifyHmac()
10 SHA-256 Hashing MessageDigest sha256()
11 Secure Token Generation SecureRandom generateToken(), generateOTP()
12 Base64 Encoding Base64 URL-safe encoder Throughout (key storage, ciphertext, tokens)

Quick Reference

Task Recommended Approach Java Classes
Encrypt data AES-256-GCM Cipher, KeyGenerator, GCMParameterSpec, SecureRandom
Hash passwords bcrypt or PBKDF2 BCryptPasswordEncoder or SecretKeyFactory + PBEKeySpec
Verify data integrity HMAC-SHA256 Mac, SecretKeySpec
Hash data (non-password) SHA-256 MessageDigest
Generate random tokens SecureRandom + Base64 SecureRandom, Base64.getUrlEncoder()
Exchange keys securely RSA-OAEP or ECDH KeyPairGenerator, Cipher
Sign data SHA256withRSA Signature, KeyPairGenerator
Encode binary as text Base64 Base64.getEncoder() / getUrlEncoder()



Subscribe To Our Newsletter
You will receive our latest post and tutorial.
Thank you for subscribing!

required
required


Leave a Reply

Your email address will not be published. Required fields are marked *